Compare commits

...

2 Commits

Author SHA1 Message Date
Steven Wroblewski c75b629dfd Regeln und Konzept an den Stand angleichen
CLAUDE.md: Die Regeln für den Kern gelten für den Quelltext in
core/src/, nicht für die Tests. World::set_all_offline ist als einzige
Ausnahme von der Regel genannt, dass sich die Welt nur durch Befehle
ändert. Die Überlaufregel sagt jetzt, dass Taktzähler normal addiert
werden und Werte aus geladenen Dateien beim Laden geprüft werden. Ein
neuer Absatz erklärt, was bei einem Fehler in golden.rs zu tun ist.

KONZEPT.md: Das Konzept ist bestätigt. Hochladen, Branches, Issues und
Pull Requests sind freigegeben, und die veraltete Angabe, das Archiv
sei noch leer, ist entfernt. Die Reihenfolge der Rückkehrer steht
jetzt bei den Festlegungen statt bei den offenen Punkten.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 22:18:45 +02:00
Steven Wroblewski 3857fe99a6 Festgeschriebene Werte und Test für überlange Namen
Die neuen Tests in golden.rs vergleichen gegen einmal aufgezeichnete
Werte statt gegen eine zweite Berechnung im selben Build. So fällt
über Builds hinweg auf, wenn sich Regeln oder Formate unbeabsichtigt
ändern: der Startzustand der Welt, das Ende einer festen Befehlsfolge
und die Prüfsumme der Aufzeichnung. Die Fehlermeldung nennt die
Versionskonstante, die dann erhöht werden muss.

Der neue Test in world.rs deckt ab, dass überlange Namen auf beiden
Beitrittswegen gekürzt gespeichert werden: beim ersten Beitritt und
bei der Rückkehr.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 22:18:35 +02:00
4 changed files with 181 additions and 9 deletions
+5 -3
View File
@@ -18,15 +18,15 @@ Faustregel: Was im Speicherstand steht oder alle Spieler betrifft, gehört in de
## Regeln für den Kern ## Regeln für den Kern
Alle Spieler simulieren die Welt selbst und tauschen nur Befehle aus (Gleichschritt). Das funktioniert nur, wenn der Kern auf jedem Rechner exakt gleich rechnet. Deshalb gilt in `core/`: Alle Spieler simulieren die Welt selbst und tauschen nur Befehle aus (Gleichschritt). Das funktioniert nur, wenn der Kern auf jedem Rechner exakt gleich rechnet. Deshalb gilt für den Quelltext des Kerns in `core/src/`. Die Tests in `core/tests/` dürfen zum Beispiel Dateien lesen.
- Nur ganze Zahlen, keine Gleitkommazahlen. - Nur ganze Zahlen, keine Gleitkommazahlen.
- Keine Uhr, keine Dateien, kein Netzwerk, keine Threads, keine Umgebungsvariablen. - Keine Uhr, keine Dateien, kein Netzwerk, keine Threads, keine Umgebungsvariablen.
- Zufall nur über `rng::Rng` mit fester Startzahl. - Zufall nur über `rng::Rng` mit fester Startzahl.
- Keine Sammlung mit wechselnder Reihenfolge. Statt einer Hash-Tabelle nimmst du `Vec`, `BTreeMap` oder `BTreeSet`. - Keine Sammlung mit wechselnder Reihenfolge. Statt einer Hash-Tabelle nimmst du `Vec`, `BTreeMap` oder `BTreeSet`.
- Die Welt ändert sich nur durch einen `Command` in `World::step`. - Die Welt ändert sich nur durch einen `Command` in `World::step`. Einzige Ausnahme ist `World::set_all_offline`: Es darf nur direkt nach dem Laden eines Speicherstands aufgerufen werden, bevor jemand beigetreten ist.
- Alles, was zum Zustand der Welt gehört, steht im Snapshot. Wer ein Feld ergänzt, ergänzt es in `snapshot.rs` und erhöht `SNAPSHOT_VERSION`. - Alles, was zum Zustand der Welt gehört, steht im Snapshot. Wer ein Feld ergänzt, ergänzt es in `snapshot.rs` und erhöht `SNAPSHOT_VERSION`.
- Wo eine Rechnung überlaufen kann, steht eine ausdrückliche Wahl: `saturating_`, `wrapping_` oder `checked_`. - Wo eine Rechnung im Spiel überlaufen kann, steht eine ausdrückliche Wahl: `saturating_`, `wrapping_` oder `checked_`. Taktzähler (`u64` bei 20 Takten pro Sekunde) laufen praktisch nie über und werden normal addiert. Werte aus geladenen Dateien müssen deshalb beim Laden geprüft werden.
- Keine fremden Crates im Kern. - Keine fremden Crates im Kern.
Der Test `core/tests/forbidden_apis.rs` durchsucht den Quelltext des Kerns nach verbotenen Begriffen. Sie dürfen dort auch in Kommentaren nicht vorkommen. Der Test `core/tests/forbidden_apis.rs` durchsucht den Quelltext des Kerns nach verbotenen Begriffen. Sie dürfen dort auch in Kommentaren nicht vorkommen.
@@ -41,6 +41,8 @@ cargo clippy --workspace --all-targets -- -D warnings
cargo test --workspace cargo test --workspace
``` ```
Schlägt `core/tests/golden.rs` fehl, haben sich Regeln oder ein Format geändert. Die Fehlermeldung nennt die Versionskonstante, die dann erhöht werden muss. Erst danach wird der festgeschriebene Wert bewusst angepasst.
## Sprache ## Sprache
- Code, Bezeichner und Kommentare im Code: Englisch. - Code, Bezeichner und Kommentare im Code: Englisch.
+147
View File
@@ -0,0 +1,147 @@
//! Pinned values for everything that must not change unnoticed.
//!
//! Every other test compares two computations inside the same build, so a
//! change that affects both sides alike stays green. This file compares
//! against values recorded once. If one of them fails, rules or encodings
//! changed, on purpose or by accident. If saves or recordings are affected,
//! raise the version constant named in the failure message, then update the
//! value here deliberately.
use grovewright_core::command::Command;
use grovewright_core::hash::fnv1a64;
use grovewright_core::map::Pos;
use grovewright_core::mapgen::outcrop_origin;
use grovewright_core::player::PlayerId;
use grovewright_core::replay::Recording;
use grovewright_core::world::World;
const SEED: u64 = 2026;
/// The commands of a session of 60 ticks. It is built from literal values
/// and the outcrop origin of the map, not from constants such as
/// `DIG_COOLDOWN_TICKS`, so that changing a rule changes the result and not
/// the script.
///
/// Never change or extend this script. To pin more, add a new script and a
/// new pinned value instead.
fn frozen_script(world: &World) -> Vec<Vec<Command>> {
let origin = outcrop_origin(world.map());
let right_of_origin = Pos::new(origin.x + 1, origin.y);
// Pairs of (tick, command). Commands of the same tick are applied in the
// order listed here.
let scripted = [
// Two players join in the same tick.
(
0,
Command::Join {
key: [1; 16],
name: "Ada".to_owned(),
},
),
(
0,
Command::Join {
key: [2; 16],
name: "Grace".to_owned(),
},
),
// Player 0 hits the outcrop origin three times, five ticks apart.
// The third hit breaks the rock and gives a stone.
(
2,
Command::Dig {
player: PlayerId(0),
pos: origin,
},
),
(
7,
Command::Dig {
player: PlayerId(0),
pos: origin,
},
),
(
12,
Command::Dig {
player: PlayerId(0),
pos: origin,
},
),
// Player 1 hits the tile to the right of the origin once.
(
20,
Command::Dig {
player: PlayerId(1),
pos: right_of_origin,
},
),
// Player 1 leaves and returns with a name of 40 bytes, which the
// world shortens to 32.
(
30,
Command::Leave {
player: PlayerId(1),
},
),
(
40,
Command::Join {
key: [2; 16],
name: "ä".repeat(20),
},
),
];
let mut ticks = vec![Vec::new(); 60];
for (tick, command) in scripted {
ticks[tick].push(command);
}
ticks
}
/// Covers the map generator, the map encoding (terrain numbers, byte order of
/// width and height) and the snapshot header.
#[test]
fn the_starting_snapshot_is_unchanged() {
let actual = fnv1a64(&World::new(SEED).to_snapshot());
assert_eq!(
actual, 0x1b78_d996_1586_fbf1,
"the starting world or the snapshot format changed; if saves are \
affected, raise SNAPSHOT_VERSION in core/src/snapshot.rs; then \
update this value"
);
}
/// Covers the rules of joining, leaving and digging, the shortening of names
/// and the encoding of players.
#[test]
fn the_frozen_session_ends_in_the_same_state() {
let mut world = World::new(SEED);
for commands in frozen_script(&world) {
world.step(&commands);
}
assert_eq!(
world.state_hash(),
0xc304_43df_fc74_dcfc,
"the rules or the snapshot format changed; old recordings no longer \
verify; if saves are affected, raise SNAPSHOT_VERSION in \
core/src/snapshot.rs; then update this value"
);
}
/// Covers the recording header and layout and the wire format of commands.
#[test]
fn the_recording_format_is_unchanged() {
let mut world = World::new(SEED);
let mut recording = Recording::start(&world);
for commands in frozen_script(&world) {
recording.push_tick(&commands);
world.step(&commands);
}
assert_eq!(
fnv1a64(&recording.to_bytes(&world)),
0x2312_5a7d_d8ef_5ae0,
"the recording or command format changed; raise RECORDING_VERSION in \
core/src/replay.rs; then update this value"
);
}
+25 -1
View File
@@ -2,7 +2,9 @@ use grovewright_core::command::Command;
use grovewright_core::inventory::ITEM_STONE; use grovewright_core::inventory::ITEM_STONE;
use grovewright_core::map::{Pos, Terrain}; use grovewright_core::map::{Pos, Terrain};
use grovewright_core::mapgen::{outcrop_origin, spawn_pos}; use grovewright_core::mapgen::{outcrop_origin, spawn_pos};
use grovewright_core::player::{PlayerId, PlayerKey, MAX_KNOWN_PLAYERS, MAX_ONLINE_PLAYERS}; use grovewright_core::player::{
PlayerId, PlayerKey, MAX_KNOWN_PLAYERS, MAX_NAME_BYTES, MAX_ONLINE_PLAYERS,
};
use grovewright_core::world::{Event, World, DIG_COOLDOWN_TICKS, TICKS_PER_SECOND}; use grovewright_core::world::{Event, World, DIG_COOLDOWN_TICKS, TICKS_PER_SECOND};
fn key(n: u8) -> PlayerKey { fn key(n: u8) -> PlayerKey {
@@ -112,6 +114,28 @@ fn a_returning_player_can_change_their_name() {
assert_eq!(world.player(id).unwrap().name, "Neuer Name"); assert_eq!(world.player(id).unwrap().name, "Neuer Name");
} }
#[test]
fn overlong_names_are_stored_shortened_on_both_join_paths() {
let mut world = World::new(1);
world.step(&[Command::Join {
key: key(1),
name: "ä".repeat(20),
}]);
assert_eq!(world.player(PlayerId(0)).unwrap().name, "ä".repeat(16));
world.step(&[Command::Leave {
player: PlayerId(0),
}]);
world.step(&[Command::Join {
key: key(1),
name: "x".repeat(MAX_NAME_BYTES + 10),
}]);
assert_eq!(
world.player(PlayerId(0)).unwrap().name,
"x".repeat(MAX_NAME_BYTES)
);
}
#[test] #[test]
fn a_fifth_online_player_is_refused() { fn a_fifth_online_player_is_refused() {
let mut world = World::new(1); let mut world = World::new(1);
+4 -5
View File
@@ -1,7 +1,7 @@
# Grovewright: Spielkonzept # Grovewright: Spielkonzept
**Stand:** 30. September 2026 **Stand:** 30. September 2026
**Status:** Entwurf, wartet auf Bestätigung **Status:** Bestätigt am 30. September 2026
Dieses Dokument hält fest, worauf wir uns im Konzeptgespräch geeinigt haben. Es ist die Grundlage für den Bau. Was hier nicht steht, ist nicht beschlossen. Dieses Dokument hält fest, worauf wir uns im Konzeptgespräch geeinigt haben. Es ist die Grundlage für den Bau. Was hier nicht steht, ist nicht beschlossen.
@@ -196,7 +196,7 @@ Geprüft am 30. September 2026:
| Rust | 1.98.1 | | Rust | 1.98.1 |
| Rust-Anbindung für Godot | `godot` 0.5.5, unterstützt Godot 4.7, verlangt Rust ab 1.94 | | Rust-Anbindung für Godot | `godot` 0.5.5, unterstützt Godot 4.7, verlangt Rust ab 1.94 |
| Git | 2.54.0 | | Git | 2.54.0 |
| Git-Archiv | `https://git.race-cave.cloud/steven/Grovewright.git`, erreichbar und noch leer | | Git-Archiv | `https://git.race-cave.cloud/steven/Grovewright.git` (Gitea) |
Es sind keine weiteren Installationen nötig. Es sind keine weiteren Installationen nötig.
@@ -227,7 +227,8 @@ Zwei Meilensteine sind Entscheidungspunkte:
| Plattform | Windows | | Plattform | Windows |
| Scheitern | Kein Game Over, kein Verlust von Fortschritt | | Scheitern | Kein Game Over, kein Verlust von Fortschritt |
| Speichern | Automatisch in festen Abständen und zusätzlich von Hand | | Speichern | Automatisch in festen Abständen und zusätzlich von Hand |
| Versionsverwaltung | Lokales Git-Archiv mit einem Eintrag pro Arbeitsschritt. Hochgeladen wird nur mit deiner Freigabe. | | Versionsverwaltung | Git-Archiv mit einem Eintrag pro Arbeitsschritt, hochgeladen nach jedem Schritt. Freigegeben sind auch Branches, Issues und Pull Requests. |
| Rückkehrer | Nach der ersten Ruine kommen Schmiedin und Hirte, nach der zweiten die Gärtnerin |
## 10. Backlog für die Steam-Option ## 10. Backlog für die Steam-Option
@@ -250,10 +251,8 @@ Offen, zu klären im jeweiligen Meilenstein:
- **Wachstumsdauern.** Die Richtwerte aus 3.1 werden im Playtest eingestellt. - **Wachstumsdauern.** Die Richtwerte aus 3.1 werden im Playtest eingestellt.
- **Verhältnis Werkstatt zu Hain.** Wie viel Hain ein Ofen braucht, entscheidet, ob die Bodenregel reizt oder nervt. - **Verhältnis Werkstatt zu Hain.** Wie viel Hain ein Ofen braucht, entscheidet, ob die Bodenregel reizt oder nervt.
- **Andocken.** Wie Ranken, Ladetiere, Kisten und Ruinen genau aneinander anschließen, wird in den Meilensteinen 2 und 3 festgelegt. - **Andocken.** Wie Ranken, Ladetiere, Kisten und Ruinen genau aneinander anschließen, wird in den Meilensteinen 2 und 3 festgelegt.
- **Reihenfolge der Rückkehrer.** Vorschlag: Nach der ersten Ruine kommen Schmiedin und Hirte, nach der zweiten die Gärtnerin. Das ist noch nicht besprochen.
- **Grafikstil.** Du wählst im ersten Meilenstein aus drei Stilproben. - **Grafikstil.** Du wählst im ersten Meilenstein aus drei Stilproben.
- **Namen.** Alle Namen für Arten, Tiere und Waren sind Arbeitstitel. - **Namen.** Alle Namen für Arten, Tiere und Waren sind Arbeitstitel.
- **Hochladen.** Ob nach jedem Arbeitsschritt ins Git-Archiv hochgeladen wird, ist noch nicht freigegeben.
- **Titel.** Eine Websuche fand kein Spiel namens „Grovewright". Vor einem Steam-Release ist eine Markenrecherche nötig. - **Titel.** Eine Websuche fand kein Spiel namens „Grovewright". Vor einem Steam-Release ist eine Markenrecherche nötig.
Risiken: Risiken: