446fad7b55
paket.bat baut aus dem ausgecheckten Stand ein ZIP, das auf jedem Windows 10 oder 11 mit 64 Bit ohne Git, Rust und Godot läuft: das exportierte Spiel, die Brücke mit statisch gelinkter C-Laufzeit und eine Anleitung für Mitspieler. Die Exportvoreinstellung lässt Tests und Werkzeuge weg und nimmt die Texte mit; dist/ bleibt außerhalb des Archivs. CLAUDE.md und der Fragebogen von M1d beschreiben das Paket. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
136 lines
14 KiB
Markdown
136 lines
14 KiB
Markdown
# 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. |
|
||
| `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 |
|
||
| `paket/` | Anleitung, die dem Paket für andere Rechner beiliegt |
|
||
|
||
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::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`. Ausnahmen sind das Laden eines Speicherstands mit `World::load`, das alle Spieler abmeldet, und die Übernahme der Welt des Hosts mit `World::from_snapshot`, die ein Gast beim Beitritt und nach einer Abweichung macht.
|
||
- Wer eine Sitzung aufzeichnet, ruft `Recording::step` statt `World::step` auf. 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.rs` für die Welt, `player.rs` für Spieler, `inventory.rs` für das Inventar, `map.rs` für die Karte), prüft es beim Dekodieren oder in `check_restorable` und erhöht `SNAPSHOT_VERSION`.
|
||
- Wo eine Rechnung im Spiel überlaufen kann, steht eine ausdrückliche Wahl: `saturating_`, `wrapping_` oder `checked_`. Taktzähler laufen praktisch nie über und werden normal addiert: Die Welt zählt in `u64`, eine Aufzeichnung in `u32`, was bei 20 Takten pro Sekunde für über sechs Jahre reicht. Takte aus geladenen Dateien begrenzt `MAX_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 die Hülle speichert die Aufzeichnung der abgewichenen Welt.
|
||
- Die Nachrichten stehen in `core/src/protocol.rs`. Wer eine Nachricht ändert, erhöht `PROTOCOL_VERSION`. Der Anfang des Grußes und die ganze Ablehnung ändern sich dabei nie, damit Spiele jeder Version einander noch sagen können, dass sie nicht zusammenpassen; `core/tests/golden.rs` hält beides fest.
|
||
- 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 in `SCRIPTS`, 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_TICK` pro Spieler und Takt an.
|
||
- Ein Gast zählt die Abklingzeit der Spitzhacke in seinen eigenen Takten, die denen des Hosts um die Laufzeit nachlaufen. Kommt sein Schlag an, während die Abklingzeit seines Spielers noch läuft, hält ihn der Host bis zum ersten Takt danach; kommen mehrere, gilt der neueste. Sonst ginge übers Internet etwa jeder fünfte Schlag eines Gastes verloren.
|
||
- 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. Außerdem stehen die Schlüssel aller Spieler in jedem Snapshot, den ein Gast bekommt; ein veränderter Client könnte sich damit als ein Spieler ausgeben, der gerade nicht online ist. Für ein Koop-Spiel unter Freunden nehmen wir beides hin.
|
||
|
||
## Regeln für Brücke und Hülle
|
||
|
||
- Die Brücke übersetzt nur. Neue Spielregeln gehören in den Kern, nie in `bridge/` oder `game/`.
|
||
- GDScript mit Typangaben schreiben (`var x := …`, Rückgabetypen an jeder Funktion).
|
||
- Eingabeaktionen legt `game/scripts/input_setup.gd` an, nicht `project.godot`.
|
||
- Alle Texte, die Spieler sehen, stehen in `game/i18n/texts.csv` (Deutsch und Englisch) und werden über `tr()` geholt.
|
||
- Tests, die Dateien schreiben, nutzen `TestFiles.temporary_dir()` (Ordner unter `user://tests`), nie die echten Dateien des Spielers.
|
||
- Godot erzeugt beim Import zu jedem Skript eine `.uid`-Datei. Sie gehört ins Archiv. Der Ordner `game/.godot/` gehört nicht hinein.
|
||
- Das Netzwerk liegt in `game/scripts/net_link.gd` (ENet, Port 24680) und `game/scripts/session_link.gd`. Nachrichten des Kerns gehen zuverlässig und in Reihenfolge über Kanal 0, Positionen der Figuren unzuverlässig, aber geordnet über Kanal 1; ENet drosselt sie nie. Eine Verbindung, deren Pakete unbeantwortet bleiben, gilt nach etwa `NetLink.TIMEOUT_MSEC` als verloren; ENet prüft die Grenze nur, wenn eine Wiederholung fällig ist. `NetLink.close_peer` schließt eine Verbindung mit `peer_disconnect_later`, damit wartende Nachrichten wie eine Ablehnung noch ankommen.
|
||
- Nur der Host speichert. Laden mit F9 geht nur ohne geöffnete Welt („Allein spielen“).
|
||
- Eine Pause beginnt nur ohne Gäste und lässt sich immer beenden. Nimmt der Host einen Gast an, endet sie; eine bloße Verbindung ohne Gruß beendet sie nicht.
|
||
- Hält der Kern mit einem internen Fehler an, schließt ein Host seine Welt für Gäste, und ein Gast kehrt ins Menü zurück. Eine angehaltene Sitzung hinterlässt keine Aufzeichnung.
|
||
- Tests mit Netzwerk lauschen nur auf `127.0.0.1` und lassen das System den Port wählen (Port 0), damit sie nie an einem belegten Port scheitern.
|
||
|
||
## Grafik
|
||
|
||
- Alles auf dem Spielfeld zeichnet `Art` (`game/scripts/art/art.gd`), jedes Ding unter einer Kennung wie `terrain/soft_rock`. Andere Skripte zeichnen keine eigenen Formen oder Farben aufs Spielfeld; ausgenommen sind Texte wie die Namen über den Figuren.
|
||
- Die Grafik ist Pixelgrafik aus Code (Konzept 7.4): 16 × 16 Pixel pro Kachel, schräg von vorn gesehen. `PixelStyle` (`game/scripts/art/pixel_style.gd`) zeichnet jede Kennung Pixel für Pixel in ein kleines Bild und hebt es auf. Die Stile `WarmStyle`, `PastelStyle` und `GlowStyle` wählen nur Palette, Umrisse und Leuchten; Warm ist der Standard. F2 wechselt im Spiel, „Stilproben ansehen“ im Menü öffnet die Vitrine, und das Spiel merkt sich die Wahl.
|
||
- Eine neue Kennung bekommt eine Konstante in `Art` und einen Eintrag in `Art.IDS`, einen Zweig mit Platzhalter in `ArtStyle.draw`, eine Zeichnung in `PixelStyle` und ihre Farben in jeder Palette. Fehlt nur die Zeichnung, zeichnet `ArtStyle` den Platzhalter; fehlt eine Farbe, bricht die Zeichnung mit einem Fehler ab. Nur Kennungen in `Art.IDS` lassen sich durch Bilder ersetzen, und `game/tests/test_art.gd` lässt jeden Stil jede dieser Kennungen zeichnen.
|
||
- Damit die Pixel scharf bleiben, stellt `project.godot` die Texturfilterung auf „Nearest“ und rastet 2D-Transformationen auf ganze Bildschirmpixel ein. Die Kamera vergrößert ganzzahlig (`CAMERA_ZOOM` in `main.gd`), und die Stile setzen ihre Bilder auf ganze Pixel.
|
||
- Ein Bild `game/art/<Kennung>.png` ersetzt die Zeichnung seiner Kennung, ohne dass sich Code ändert. So kann ein Artist Grafik austauschen. Ein solches Bild ist ein einzelnes festes Bild; Kachelränder und Bewegung zeichnet bisher nur der Code.
|
||
- Die Karte zeichnet in Blöcken von 16 × 16 Kacheln (`MapView`); nach einem Treffer werden nur die Blöcke um die Kachel neu gezeichnet. Jede Kachel kennt ihre Nachbarn (`TileShape`), damit Felsmassen außen rund und innen geschlossen sind und zum offenen Boden hin ihre Vorderkante zeigen.
|
||
|
||
## Prüfen
|
||
|
||
Vor jedem Commit müssen alle Befehle fehlerfrei laufen:
|
||
|
||
```bash
|
||
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, Aufzeichnungen oder Nachrichten betroffen, erhöhst du die Versionskonstante, die die Fehlermeldung nennt (`SNAPSHOT_VERSION`, `RECORDING_VERSION` oder `PROTOCOL_VERSION`). 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. `--style=NAME` wählt einen Stil, `--showcase` zeigt die Vitrine statt des Spiels:
|
||
|
||
```bash
|
||
godot --path game -s res://tools/screenshot.gd -- <Pfad der PNG-Datei> [--style=NAME] [--showcase]
|
||
```
|
||
|
||
## 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`.
|
||
|
||
Ein laufendes Spiel hält `target\debug\grovewright_bridge.dll` offen. Ändern sich danach Brücke oder Kern, scheitert `cargo build` mit „Zugriff verweigert (os error 5)“, bis alle Spielfenster geschlossen sind.
|
||
|
||
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. Erlaubt sind bis zu 32 Zeichen aus A bis Z, a bis z, Ziffern, `-` und `_`; einen anderen Namen übergeht das Spiel mit einer Warnung im Log und nimmt das Standardprofil. |
|
||
| `--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 |
|
||
| `--style=NAME` | Zeichnet im Stil `warm`, `pastel` oder `glow` statt im zuletzt gewählten |
|
||
|
||
`play-zweites-fenster.bat` startet ein zweites Fenster mit dem Profil `zweites-fenster`. Die Datei reicht weitere Argumente ebenfalls an das Spiel weiter, etwa `--name=Zweiter`.
|
||
|
||
## Paket für andere Rechner
|
||
|
||
`paket.bat` baut aus dem ausgecheckten Stand ein Paket, das auf jedem Windows 10 oder 11 mit 64 Bit ohne Git, Rust und Godot läuft: `dist\Grovewright-<Commit>-windows.zip` mit `Grovewright.exe`, `Grovewright.pck`, der Brücke und der Anleitung `paket\LIESMICH.txt`. Ungesicherte Änderungen landen mit im Paket, der Name nennt aber nur den Commit.
|
||
|
||
- Für das Paket baut Cargo die Brücke als Release mit statisch gelinkter C-Laufzeit nach `target\x86_64-pc-windows-msvc\release`, damit Mitspieler kein Visual C++ Redistributable brauchen. `game/grovewright.gdextension` zeigt für Release-Exporte dorthin.
|
||
- Die Exportvoreinstellung „Windows“ steht in `game/export_presets.cfg`. Sie lässt `tests/` und `tools/` weg und nimmt `i18n/*.csv` ausdrücklich mit, weil das Spiel seine Texte zur Laufzeit liest.
|
||
- Der Export braucht die Exportvorlagen der installierten Godot-Version unter `%APPDATA%\Godot\export_templates\4.7.2.stable`, etwa über „Editor > Exportvorlagen verwalten“ im Godot-Editor.
|
||
- Pakete und Starts mit `play.bat` spielen zusammen, solange Regel-Kennung und `PROTOCOL_VERSION` gleich sind; sonst lehnt der Host den Beitritt ab. Am sichersten nutzen alle denselben Commit.
|
||
- Ein ausgepacktes Paket lässt sich mit einem eigenen `APPDATA` prüfen, etwa `Grovewright.exe --headless --quit-after 600 -- --solo`. So schreibt es nicht in die echten Spielerdateien, und Fehler stehen in `<APPDATA>\Godot\app_userdata\Grovewright\logs\godot.log`.
|
||
|
||
## 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.
|