play.bat gibt Argumente an das Spiel weiter, und play-zweites-fenster.bat startet ein zweites Fenster mit eigenem Profil. CLAUDE.md beschreibt die gemeinsame Welt, das Vertrauensmodell der Reichweite, die Netzwerkregeln und den Test zweier Instanzen. Ein Fragebogen begleitet den Probelauf. Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
8.6 KiB
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. 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. |
bridge/ |
Rust-Brücke (GDExtension): macht den Kern für Godot aufrufbar und enthält keine Spielregeln. |
game/ |
Godot-Projekt mit der GDScript-Hülle: Darstellung, Eingabe, Oberfläche, Dateien. |
docs/KONZEPT.md |
Spielkonzept und Entscheidungen |
docs/superpowers/plans/ |
Umsetzungspläne |
docs/playtests/ |
Fragebögen für Playtests |
Faustregel: Was im Speicherstand steht oder alle Spieler betrifft, gehört in den Kern. Die Hülle darf Gleitkommazahlen nutzen, weil sie nichts an der gemeinsamen Welt ändert; sie schickt nur Befehle.
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 gelten für den Quelltext des Kerns in core/src/ diese Regeln (die Tests in core/tests/ dürfen zum Beispiel Dateien lesen):
- Nur ganze Zahlen, keine Gleitkommazahlen.
- Keine Uhr, keine Dateien, kein Netzwerk, keine Threads, keine Umgebungsvariablen, kein globaler veränderlicher Zustand.
- Zufall nur über
rng::Rngmit fester Startzahl. - Keine Sammlung mit wechselnder Reihenfolge. Statt einer Hash-Tabelle nimmst du
Vec,BTreeMapoderBTreeSet. - Die Welt ändert sich nur durch einen
CommandinWorld::step. Einzige Ausnahme ist das Laden eines Speicherstands mitWorld::load, das alle Spieler abmeldet. - Wer eine Sitzung aufzeichnet, ruft
Recording::stepstattWorld::stepauf. So enthält die Aufzeichnung genau die ausgeführten Befehle. - Alles, was zum Zustand der Welt gehört, steht im Snapshot. Wer ein Feld ergänzt, ergänzt es im Kodierer und im Dekodierer seiner Struktur (
snapshot.rsfür die Welt,player.rsfür Spieler,inventory.rsfür das Inventar,map.rsfür die Karte), prüft es beim Dekodieren oder incheck_restorableund erhöhtSNAPSHOT_VERSION. - Wo eine Rechnung im Spiel überlaufen kann, steht eine ausdrückliche Wahl:
saturating_,wrapping_oderchecked_. Taktzähler laufen praktisch nie über und werden normal addiert: Die Welt zählt inu64, eine Aufzeichnung inu32, was bei 20 Takten pro Sekunde für über sechs Jahre reicht. Takte aus geladenen Dateien begrenztMAX_RESTORABLE_TICK. - Keine fremden Crates im Kern.
Drei Wächter prüfen das. core/clippy.toml sperrt Pfade wie std::collections::HashMap oder std::fs::read, auch über Aliase und gruppierte Importe. Der Test core/tests/forbidden_apis.rs durchsucht zusätzlich den Text nach verbotenen Begriffen; sie dürfen dort auch in Kommentaren nicht vorkommen. Gruppierte Importe aus std (use std::{…}) sind verboten, weil sie dem Text die Modulnamen verbergen: Jeder Pfad steht in einer eigenen use-Zeile. core/tests/no_dependencies.rs hält die Abhängigkeiten leer.
Gemeinsame Welt im Netz
- Ein Spieler ist Host, die anderen sind Gäste. Der Host (
core/src/host.rs) legt fest, welche Befehle in welchem Takt laufen, und schickt jeden Takt an seine Gäste. Jeder Gast (core/src/guest.rs) rechnet jeden Takt selbst nach. Allein spielen heißt: Host ohne Gäste. - Ein Gast bekommt beim Beitritt die Welt des Hosts als Snapshot. Einmal pro Sekunde liegt einem Takt die Prüfsumme des Hosts bei. Weicht die Welt des Gastes ab, holt er sich die Welt des Hosts neu und hebt die Aufzeichnung der abgewichenen Welt auf.
- Die Nachrichten stehen in
core/src/protocol.rs. Wer eine Nachricht ändert, erhöhtPROTOCOL_VERSION. - Die Regel-Kennung (
core/src/rules.rs) leitet sich aus den Endzuständen eingefrorener Skripte ab. Rechner mit verschiedener Regel-Kennung spielen nicht zusammen, und Aufzeichnungen anderer Regeln werden abgelehnt. Eine neue Regel braucht deshalb ein neues Skript inSCRIPTS, das sie benutzt; bestehende Skripte ändert niemand. - Gäste schicken nur Aktionen, ohne Spielernummer. Der Host macht daraus Befehle für den Spieler der Verbindung und nimmt höchstens
MAX_ACTIONS_PER_TICKpro Spieler und Takt an. - Die Reichweite der Spitzhacke prüft nur die Hülle des eigenen Spielers. Figuren laufen außerhalb des Gleichschritts, also kennt der Kern ihre Position nicht. Ein veränderter Client könnte deshalb aus der Ferne graben. Für ein Koop-Spiel unter Freunden nehmen wir das hin.
Regeln für Brücke und Hülle
- Die Brücke übersetzt nur. Neue Spielregeln gehören in den Kern, nie in
bridge/odergame/. - GDScript mit Typangaben schreiben (
var x := …, Rückgabetypen an jeder Funktion). - Eingabeaktionen legt
game/scripts/input_setup.gdan, nichtproject.godot. - Alle Texte, die Spieler sehen, stehen in
game/i18n/texts.csv(Deutsch und Englisch) und werden übertr()geholt. - Tests, die Dateien schreiben, nutzen
TestFiles.temporary_dir()(Ordner unteruser://tests), nie die echten Dateien des Spielers. - Godot erzeugt beim Import zu jedem Skript eine
.uid-Datei. Sie gehört ins Archiv. Der Ordnergame/.godot/gehört nicht hinein. - Das Netzwerk liegt in
game/scripts/net_link.gd(ENet, Port 24680) undgame/scripts/session_link.gd. Nachrichten des Kerns gehen zuverlässig und in Reihenfolge über Kanal 0, Positionen der Figuren unzuverlässig über Kanal 1. Eine Verbindung schließtNetLink.close_peermitpeer_disconnect_later, damit wartende Nachrichten wie eine Ablehnung noch ankommen. - Nur der Host speichert. Pausieren gibt es nur ohne Gäste, Laden mit F9 nur beim Spielen allein.
- Tests mit Netzwerk lauschen nur auf
127.0.0.1und lassen das System den Port wählen (Port 0), damit sie nie an einem belegten Port scheitern.
Prüfen
Vor jedem Commit müssen alle Befehle fehlerfrei laufen:
cargo fmt --all -- --check
cargo clippy --workspace --all-targets -- -D warnings
cargo test --workspace
cargo build -p grovewright-bridge
godot --headless --path game --import
godot --headless --path game -s res://tests/run_tests.gd
Der erste Import in einem frischen Checkout stürzt mit Godot 4.7.2 und der Rust-Erweiterung beim Beenden ab (Exit-Code 139 in der Bash). Der Import ist dann trotzdem erfolgt; ein zweiter Aufruf läuft fehlerfrei durch. play.bat wiederholt den Import deshalb einmal.
Der Test game/tests/test_two_instances.gd startet einen zweiten Godot-Prozess als Gast (game/tools/guest_bot.gd) und prüft, dass beide Welten gleich bleiben. Er braucht deshalb die gebaute Brücke und den Import wie alle anderen Tests der Hülle.
Schlägt core/tests/golden.rs fehl, haben sich Regeln oder ein Format geändert. Sind Speicherstände oder Aufzeichnungen betroffen, erhöhst du die Versionskonstante, die die Fehlermeldung nennt. Erst danach passt du den festgeschriebenen Wert bewusst an.
Für Änderungen am Aussehen gibt es ein Werkzeug, das kurz ein Fenster öffnet und ein Bildschirmfoto speichert:
godot --path game -s res://tools/screenshot.gd -- <Pfad der PNG-Datei>
Spielen
play.bat im Hauptordner baut die Brücke und startet das Spiel. Spielstand, Spielerschlüssel und Aufzeichnungen liegen unter %APPDATA%\Godot\app_userdata\Grovewright.
Argumente hinter play.bat gehen an das Spiel:
| Argument | Wirkung |
|---|---|
--profile=NAME |
Eigener Ordner unter profiles\NAME für Schlüssel, Spielstand und Aufzeichnungen. Zwei Fenster auf einem Rechner brauchen zwei Profile, sonst sind sie derselbe Spieler. |
--name=NAME |
Name des Spielers |
--solo, --host |
Startet ohne Menü allein beziehungsweise mit offener Welt |
--port=PORT |
Port einer offenen Welt |
--join=ADRESSE |
Tritt ohne Menü einem Host bei; die Adresse darf auf :PORT enden |
play-zweites-fenster.bat startet ein zweites Fenster mit dem Profil zweites-fenster.
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
game/i18n/texts.csv.
Git
- Archiv:
https://git.race-cave.cloud/steven/Grovewright.git(Gitea). Issues und Pull Requests sind freigegeben. - Gearbeitet wird auf einem Branch pro Plan, nicht direkt auf
main. - Nach jedem abgeschlossenen Arbeitsschritt wird committet und hochgeladen. Das ist freigegeben.
- Commit-Nachrichten enden mit einer Zeile
Co-Authored-By: Claude <Modell> <noreply@anthropic.com>, die das Claude-Modell nennt, das den Commit erstellt hat.