From 477b9a41dd4addaf99ebfb39207f97fc346d643a Mon Sep 17 00:00:00 2001 From: Steven Wroblewski Date: Wed, 30 Sep 2026 20:08:01 +0200 Subject: [PATCH] =?UTF-8?q?Ger=C3=BCst=20f=C3=BCr=20den=20Rust-Kern=20anle?= =?UTF-8?q?gen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Workspace, leeres Crate grovewright-core und ein Wächter-Test, der den Quelltext des Kerns auf Verstöße gegen die Gleichschritt-Regeln prüft. Co-Authored-By: Claude Opus 5.5 --- .gitattributes | 2 + .gitignore | 2 + CLAUDE.md | 54 +++++++++++++++++++++++++ Cargo.lock | 7 ++++ Cargo.toml | 8 ++++ core/Cargo.toml | 13 ++++++ core/src/lib.rs | 7 ++++ core/tests/forbidden_apis.rs | 78 ++++++++++++++++++++++++++++++++++++ 8 files changed, 171 insertions(+) create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 CLAUDE.md create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 core/Cargo.toml create mode 100644 core/src/lib.rs create mode 100644 core/tests/forbidden_apis.rs diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..2c10f62 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Same line endings on every machine. +* text=auto eol=lf diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4b67358 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +# Rust +/target/ diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..54935aa --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,54 @@ +# Grovewright + +Aufbauspiel für ein bis vier Spieler, in dem die Fabrik lebt. Gebaut wird zuerst eine Vertical Slice mit 2–3 Stunden Spielzeit. + +Das verbindliche Konzept steht in [docs/KONZEPT.md](docs/KONZEPT.md). Lies es, bevor du Spielregeln, Architektur oder Umfang änderst. Was dort nicht steht, ist nicht beschlossen. + +## Aufbau + +| Ordner | Inhalt | +|---|---| +| `core/` | Rust-Kern: die gesamte gemeinsame Welt. Ohne Zugriff auf die Engine, ohne fremde Crates. | +| `docs/KONZEPT.md` | Spielkonzept und Entscheidungen | +| `docs/superpowers/plans/` | Umsetzungspläne | + +Die Brücke zu Godot (`bridge/`), das Godot-Projekt (`game/`) und die Inhaltsdateien (`content/`) kommen mit späteren Plänen dazu. + +Faustregel: Was im Speicherstand steht oder alle Spieler betrifft, gehört in 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/`: + +- Nur ganze Zahlen, keine Gleitkommazahlen. +- Keine Uhr, keine Dateien, kein Netzwerk, keine Threads, keine Umgebungsvariablen. +- Zufall nur über `rng::Rng` mit fester Startzahl. +- 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`. +- 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_`. +- 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. + +## Prüfen + +Vor jedem Commit müssen alle drei Befehle fehlerfrei laufen: + +```bash +cargo fmt --all -- --check +cargo clippy --workspace --all-targets -- -D warnings +cargo test --workspace +``` + +## Sprache + +- Code, Bezeichner und Kommentare im Code: Englisch. +- Dokumente, Pläne, Commit-Nachrichten, Issues und Pull Requests: Deutsch. +- Texte im Spiel: Deutsch und Englisch über Übersetzungsdateien. + +## Git + +- Archiv: `https://git.race-cave.cloud/steven/Grovewright.git` (Gitea). +- Gearbeitet wird auf einem Branch pro Plan, nicht direkt auf `main`. +- Nach jedem abgeschlossenen Arbeitsschritt wird committet und hochgeladen. Das ist freigegeben. diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..f3da7f1 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,7 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "grovewright-core" +version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..795adc1 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,8 @@ +[workspace] +resolver = "2" +members = ["core"] + +# The shipped game and the tests must agree on what happens when a number +# overflows, so release builds keep the overflow checks that debug builds have. +[profile.release] +overflow-checks = true diff --git a/core/Cargo.toml b/core/Cargo.toml new file mode 100644 index 0000000..58f56d7 --- /dev/null +++ b/core/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "grovewright-core" +version = "0.1.0" +edition = "2021" +rust-version = "1.94" +publish = false +description = "Deterministic simulation core of Grovewright" + +[lints.rust] +unsafe_code = "forbid" + +[lints.clippy] +float_arithmetic = "deny" diff --git a/core/src/lib.rs b/core/src/lib.rs new file mode 100644 index 0000000..de69b99 --- /dev/null +++ b/core/src/lib.rs @@ -0,0 +1,7 @@ +//! Deterministic core of Grovewright. +//! +//! Everything that belongs to the shared world lives here. The core never +//! touches the engine, the clock, files or the network, and it calculates +//! only with whole numbers. Given the same commands it produces the same +//! world on every machine. That is what lets every player simulate the whole +//! world and exchange nothing but commands. diff --git a/core/tests/forbidden_apis.rs b/core/tests/forbidden_apis.rs new file mode 100644 index 0000000..8913c42 --- /dev/null +++ b/core/tests/forbidden_apis.rs @@ -0,0 +1,78 @@ +//! Guards the lockstep rules: the core must not use anything whose result +//! can differ between machines or between runs. + +use std::fs; +use std::path::{Path, PathBuf}; + +/// Text that must not appear anywhere in the source of the core, and why. +const FORBIDDEN: &[(&str, &str)] = &[ + ("f32", "floating point results can differ between machines"), + ("f64", "floating point results can differ between machines"), + ( + "HashMap", + "iteration order differs between runs; use BTreeMap or Vec", + ), + ( + "HashSet", + "iteration order differs between runs; use BTreeSet or Vec", + ), + ("std::time", "the core must not read the clock"), + ("Instant", "the core must not read the clock"), + ("SystemTime", "the core must not read the clock"), + ("std::thread", "the core must stay single-threaded"), + ("std::env", "the core must not depend on the environment"), + ( + "std::fs", + "the core must not touch files; the shell does that", + ), + ( + "std::net", + "the core must not open connections; the shell does that", + ), + ("unsafe", "the core forbids unsafe code"), +]; + +fn rust_files(dir: &Path, out: &mut Vec) { + for entry in fs::read_dir(dir).expect("source directory is readable") { + let path = entry.expect("directory entry is readable").path(); + if path.is_dir() { + rust_files(&path, out); + } else if path.extension().is_some_and(|ext| ext == "rs") { + out.push(path); + } + } +} + +#[test] +fn core_source_uses_no_forbidden_apis() { + let src = Path::new(env!("CARGO_MANIFEST_DIR")).join("src"); + let mut files = Vec::new(); + rust_files(&src, &mut files); + files.sort(); + assert!( + !files.is_empty(), + "no source files found in {}", + src.display() + ); + + let mut problems = Vec::new(); + for file in &files { + let text = fs::read_to_string(file).expect("source file is readable"); + for (number, line) in text.lines().enumerate() { + for (needle, reason) in FORBIDDEN { + if line.contains(needle) { + problems.push(format!( + "{}:{}: `{needle}`: {reason}", + file.display(), + number + 1 + )); + } + } + } + } + assert!( + problems.is_empty(), + "forbidden APIs in the core:\n{}", + problems.join("\n") + ); +}