M1a: deterministischer Rust-Kern #3
@@ -0,0 +1,2 @@
|
|||||||
|
# Same line endings on every machine.
|
||||||
|
* text=auto eol=lf
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
# Rust
|
||||||
|
/target/
|
||||||
@@ -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.
|
||||||
Generated
+7
@@ -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"
|
||||||
@@ -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
|
||||||
@@ -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"
|
||||||
@@ -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.
|
||||||
@@ -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<PathBuf>) {
|
||||||
|
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")
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user