docs: ticket 12 (durchstich) aufloesen, korrekturen an 11 und 06, zug im glossar geschaerft

This commit is contained in:
2026-08-12 11:27:19 +02:00
parent 2c19cb2aaf
commit 88195d3c9a
5 changed files with 94 additions and 4 deletions

View File

@@ -52,3 +52,21 @@ Vollständiger Bericht: [`research/06-ratatui-architektur.md`](../research/06-ra
Sieben Punkte sind im Bericht ausdrücklich als nicht verifiziert markiert. Sechs Anschlussfragen bleiben Sieben Punkte sind im Bericht ausdrücklich als nicht verifiziert markiert. Sechs Anschlussfragen bleiben
offen und sind zu Ticket 11 und 12 graduiert. offen und sind zu Ticket 11 und 12 graduiert.
## Nachtrag aus Ticket 12: der Windows-Fallstrick ist gemessen
Der `KeyEventKind::Press`-Filter stand hier als Faktum, aber ungeprüft. Der Durchstich zählt
Tasten-Events ungefiltert **und** gefiltert mit. Unter Windows 11:
```
Tasten 14 → Press 7 → Züge 7
```
Jeder Tastendruck erzeugt zwei `Event::Key` — Press und Release. Ohne den Filter kostete **jeder
Schritt zwei Züge**, und weil ein Zug befristete Modifikatoren tickt, wäre das kein kosmetischer
Fehler, sondern doppelte Spielzeit.
Ebenfalls im Durchstich bestätigt: `ratatui::run`, der Buffer-Zugriff über `buf[(x, y)].set_symbol(…)`
für ein eigenes Grid-Widget, `Clear` auf ein Sub-`Rect` für Popups, und dass `ratatui` die Crate
`crossterm` re-exportiert — ein eigener `Cargo.toml`-Eintrag ist tatsächlich unnötig. Gebaut gegen
ratatui 0.30.2 mit Rust 1.96.

View File

@@ -64,8 +64,22 @@ Weder Angriff noch Flucht sind **eigene Varianten** — beides sind Fähigkeiten
Registry festgelegt, also gehören sie dorthin, und ein Mod kann beide ersetzen: eine Klasse, die Registry festgelegt, also gehören sie dorthin, und ein Mod kann beide ersetzen: eine Klasse, die
besser flieht, ist damit eine Datenänderung statt eines Engine-Sonderfalls. besser flieht, ist damit eine Datenänderung statt eines Engine-Sonderfalls.
Auch Handel kostet einen Zug. Ein `fn costs_turn(&self) -> bool` wäre eine Ausnahmeliste, die mit **Korrektur aus dem Durchstich (Ticket 12): „alle Commands" heißt „alle *ausgeführten*".** Gegen eine
jeder neuen Variante gepflegt werden muss und bei der irgendwann jemand falsch rät. Zehn Züge beim Wand zu laufen darf keinen Zug kosten — sonst tickt ein Gift-Debuff, weil jemand danebengetippt hat.
```rust
pub enum Outcome { TurnPassed, Rejected(&'static str) }
fn execute(&mut self, cmd: Command) -> Outcome
```
Der Zug endet nur bei `TurnPassed`, und der Grund reist mit zurück in die Statuszeile — sonst tut das
Spiel schweigend nichts. Das betrifft mehr als Wände: kaufen ohne Gold, eine Fähigkeit ohne Ausdauer,
absteigen ohne Treppe. Overlays schließen sich trotzdem, auch bei `Rejected`: Die Rückfrage *wurde*
beantwortet.
Davon unberührt: Auch Handel kostet einen Zug. Ein `fn costs_turn(&self) -> bool` wäre eine
Ausnahmeliste, die mit jeder neuen Variante gepflegt werden muss und bei der irgendwann jemand falsch
rät. Zehn Züge beim
Einkauf stören nur, wenn im Laden gekämpft wird; dagegen hilft, dass `Buy(item, 10)` bereits *ein* Einkauf stören nur, wenn im Laden gekämpft wird; dagegen hilft, dass `Buy(item, 10)` bereits *ein*
Command ist. Command ist.
@@ -95,7 +109,9 @@ ohne Command hat den Spielzustand also nachweislich nicht angefasst.
**Ein `Response::Command` schließt alle Overlay-Screens.** Das Flag existiert bereits fürs Rendern und **Ein `Response::Command` schließt alle Overlay-Screens.** Das Flag existiert bereits fürs Rendern und
beantwortet damit auch „schließt sich nach einer Rückfrage von selbst" — eine Zeile in der Schleife, beantwortet damit auch „schließt sich nach einer Rückfrage von selbst" — eine Zeile in der Schleife,
keine zusätzliche `Response`-Variante und keine Antwortliste. `Confirm { text, on_yes: Command }` wird keine zusätzliche `Response`-Variante und keine Antwortliste. `Confirm { text, on_yes: Command }` wird
dadurch für jede Rückfrage wiederverwendbar, ohne eigene Variante je Anlass. Dass `Inventory` als dadurch für jede Rückfrage wiederverwendbar, ohne eigene Variante je Anlass — **`Command` muss dafür
`Clone` sein**, sonst kommt das Kommando nicht aus dem Screen heraus, ohne ihn zu zerstören
(Befund aus Ticket 12). Dass `Inventory` als
Vollbild-Screen nach `UseItem` offen bleibt, fällt aus derselben Regel — man trinkt selten genau einen Vollbild-Screen nach `UseItem` offen bleibt, fällt aus derselben Regel — man trinkt selten genau einen
Trank. Trank.

View File

@@ -1,7 +1,7 @@
# 12 — Prototyp: ratatui-Durchstich # 12 — Prototyp: ratatui-Durchstich
Type: prototype Type: prototype
Status: open Status: resolved
Blocked by: 11 Blocked by: 11
## Question ## Question
@@ -21,3 +21,56 @@ Der Umbau von `print_box` auf ratatui berührt jedes bestehende Modul (`items.rs
Bewusst nicht im Prototyp: Kampf, Inventar, Shop, Speichern, Mods. Bewusst nicht im Prototyp: Kampf, Inventar, Shop, Speichern, Mods.
Ergebnis: lauffähiger Durchstich plus Urteil, ob das Modell trägt oder Ticket 11 nachgebessert wird. Ergebnis: lauffähiger Durchstich plus Urteil, ob das Modell trägt oder Ticket 11 nachgebessert wird.
## Answer
**Urteil: Das Modell trägt.** Ticket 11 wird an genau einer Stelle nachgebessert, und die hatte nichts
mit dem Screen-Stack zu tun.
Der Prototyp liegt als Primärquelle auf dem Branch **`prototyp/12-ratatui-durchstich`**
(`examples/durchstich.rs`, 342 Zeilen, `cargo run --example durchstich`). Er bleibt dort und kommt
nicht nach `main` — Wegwerfcode, der eine Frage beantwortet hat.
### Die fünf Zusagen
| Zusage | Befund |
|---|---|
| `mod game` bleibt ratatui-frei | **hält.** Null Treffer, und zwar ohne dass irgendwer aufpassen musste — die Trennung ergibt sich, wenn `Command` und Spiellogik zusammenliegen |
| Dungeon-Grid als eigenes `impl Widget` | **trägt.** `buf[(x, y)].set_symbol(…)` ist der ganze Trick; kein fertiges Widget wurde vermisst |
| `Vec<Screen>` für Screens *und* Popups | **trägt.** Overlay-Flag entscheidet Rendern *und* Auto-Schließen, wie in 11 abgeleitet |
| `Response` statt Stack-Mutation im Screen | **trägt.** Kein Borrow-Konflikt, weil der Screen den Stack nie sieht |
| `KeyEventKind::Press`-Filter | **gemessen statt geglaubt** — siehe unten |
Popup über Grid, `x` dann `j`: Der Zug zählt hoch und das Popup schließt sich in derselben Iteration.
Die Regel „ein `Response::Command` schließt alle Overlays" ist damit real und kostet die eine Zeile,
die in 11 dafür veranschlagt war.
### Der Windows-Fallstrick ist jetzt eine Zahl
Der Bericht aus 06 führte ihn als Faktum, aber ungemessen. Der Prototyp zählt Tasten-Events
ungefiltert **und** gefiltert mit. Ergebnis unter Windows 11:
```
Tasten 14 → Press 7 → Züge 7
```
Jeder Tastendruck erzeugt zwei `Event::Key` (Press und Release). **Ohne den Filter würde jeder Schritt
zwei Züge kosten** — und weil ein Zug befristete Modifikatoren tickt, wäre das kein kosmetischer
Fehler, sondern doppelte Spielzeit. Der Filter arbeitet, `Press` und `Züge` laufen synchron.
### Zwei Befunde, beide nach Ticket 11 zurückgetragen
**Gegen eine Wand zu laufen kostete einen Zug.** Aufgefallen beim Spielen, nicht beim Nachdenken: Der
Zugzähler lief hoch, obwohl sich nichts bewegte — ein Gift-Debuff hätte getickt, weil jemand
danebengetippt hat. Die Regel und `Outcome { TurnPassed, Rejected(&str) }` stehen jetzt in
[Ticket 11](11-ui-geruest-screen-modell.md).
**`Command` muss `Clone` sein**, sonst kommt das Kommando nicht aus `Confirm { on_yes }` heraus, ohne
den Screen zu zerstören — im Prototyp brauchte das ein `std::mem::replace`.
### Was der Prototyp nicht beantwortet
Kampf, Inventar, Shop, Speichern, Mods — bewusst ausgelassen. Ebenso ungetestet: die
`Registries`-neben-`Game`-Entscheidung aus 07, weil der Durchstich gar keine Registries lädt. Der
Borrow-Konflikt, den sie vermeiden soll, tritt erst auf, wenn `Game::execute` beides gleichzeitig
braucht — das prüft erst die Implementierung.

View File

@@ -68,6 +68,7 @@ auch über Ticket 13.
- [07 — Game-State-Baum](issues/07-game-state-baum.md) ([#8](https://gitlab.hive-brain.de/MDietermann/rustkeep/issues/8)) — `Registries` lebt **neben** `Game`, nicht darin: als Feld kollidierte `&mut game.player` mit `&game.registries` bei jedem Methodenaufruf. Verweise per `String`-ID, keine Indizes (die hängen an der Ladereihenfolge). Ein `Creature` für Held und Gegner, aber **zwei Felder** `player` und `enemies` statt einer `Vec` — Solo-Held heißt, „Gegner greift Gegner an" existiert nicht, und zwei Felder sind ein disjunkter Borrow. Ein `Floor`, kein Backtracking; Händler müssen deshalb auf jeder Etage vorkommen können. **Instanzzustand kommt**: `ItemInstance { def, affixes }`, und **Affixe sind die sechste Registry** — dadurch bleibt der Save eine ID-Liste und der Preis `base_price × Π price_factor`. Stapeln bei gleicher `def` und gleicher `affixes`. `enum CreatureRef`, dazu je ein Eingang `Game::apply` und `Game::attribute` — Letzteres auf `Game`, weil Auren beim Träger wohnen und ein `Creature` seine Attribute allein gar nicht ausrechnen kann. Kein Attribut-Cache. - [07 — Game-State-Baum](issues/07-game-state-baum.md) ([#8](https://gitlab.hive-brain.de/MDietermann/rustkeep/issues/8)) — `Registries` lebt **neben** `Game`, nicht darin: als Feld kollidierte `&mut game.player` mit `&game.registries` bei jedem Methodenaufruf. Verweise per `String`-ID, keine Indizes (die hängen an der Ladereihenfolge). Ein `Creature` für Held und Gegner, aber **zwei Felder** `player` und `enemies` statt einer `Vec` — Solo-Held heißt, „Gegner greift Gegner an" existiert nicht, und zwei Felder sind ein disjunkter Borrow. Ein `Floor`, kein Backtracking; Händler müssen deshalb auf jeder Etage vorkommen können. **Instanzzustand kommt**: `ItemInstance { def, affixes }`, und **Affixe sind die sechste Registry** — dadurch bleibt der Save eine ID-Liste und der Preis `base_price × Π price_factor`. Stapeln bei gleicher `def` und gleicher `affixes`. `enum CreatureRef`, dazu je ein Eingang `Game::apply` und `Game::attribute` — Letzteres auf `Game`, weil Auren beim Träger wohnen und ein `Creature` seine Attribute allein gar nicht ausrechnen kann. Kein Attribut-Cache.
- [08 — Event- und Trigger-System](issues/08-event-trigger-system.md) ([#9](https://gitlab.hive-brain.de/MDietermann/rustkeep/issues/9)) — **Der Trigger ist die vierte Wirkungsform** neben Modifikator, Aktion und Fähigkeit: `{ on, condition, actions }` darf überall stehen, wo `modifiers` und `actions` stehen — sonst entstünde dieselbe Maschine ein zweites Mal in 14 als `on_hit`. Eine Bedingungsgrammatik für Trigger *und* Modifikatoren: getaggtes Enum, Liste ist UND, `not` je Eintrag, **kein ODER** (zwei Einträge oder ein gemeinsames Tag). Neun feste Events ohne Namensraum, jedes mit `source`/`target`; `attack_hit` und `damage_taken` bleiben getrennt, weil der eine beim Angreifer und der andere beim Getroffenen hängt. Im Trigger ist `self` immer der Träger. **Warteschlange statt synchron** — synchrones Feuern wäre ein Borrow-Fehler (über `self.quests` iterieren, während `self.apply` läuft) — mit Zähler bei 100 Events pro Zug statt Zyklengraph. Trigger werden gesucht, nicht indiziert. **Quest = Trigger mit `count`**, siebte Registry, erledigte bleiben markiert stehen. Zwei Nachträge an geschlossene Tickets: `start_quest` als siebte Aktion in 01, und `against: Option<CreatureRef>` plus `events: Vec<Event>` in 07. - [08 — Event- und Trigger-System](issues/08-event-trigger-system.md) ([#9](https://gitlab.hive-brain.de/MDietermann/rustkeep/issues/9)) — **Der Trigger ist die vierte Wirkungsform** neben Modifikator, Aktion und Fähigkeit: `{ on, condition, actions }` darf überall stehen, wo `modifiers` und `actions` stehen — sonst entstünde dieselbe Maschine ein zweites Mal in 14 als `on_hit`. Eine Bedingungsgrammatik für Trigger *und* Modifikatoren: getaggtes Enum, Liste ist UND, `not` je Eintrag, **kein ODER** (zwei Einträge oder ein gemeinsames Tag). Neun feste Events ohne Namensraum, jedes mit `source`/`target`; `attack_hit` und `damage_taken` bleiben getrennt, weil der eine beim Angreifer und der andere beim Getroffenen hängt. Im Trigger ist `self` immer der Träger. **Warteschlange statt synchron** — synchrones Feuern wäre ein Borrow-Fehler (über `self.quests` iterieren, während `self.apply` läuft) — mit Zähler bei 100 Events pro Zug statt Zyklengraph. Trigger werden gesucht, nicht indiziert. **Quest = Trigger mit `count`**, siebte Registry, erledigte bleiben markiert stehen. Zwei Nachträge an geschlossene Tickets: `start_quest` als siebte Aktion in 01, und `against: Option<CreatureRef>` plus `events: Vec<Event>` in 07.
- [11 — UI-Gerüst und Screen-Modell](issues/11-ui-geruest-screen-modell.md) ([#12](https://gitlab.hive-brain.de/MDietermann/rustkeep/issues/12)) — **„Aktion" hiess zwei Dinge**: `Action` bleibt das TOML-Effektvokabular aus 01, `Command` wird das UI-Kommando, Eingang `Game::execute(cmd, reg)` — die eine Stelle, an der ein Zug endet (Modifikatoren ticken, Event-Warteschlange leerarbeiten). Zwölf Commands, **alle kosten einen Zug** (ein `costs_turn`-Flag waere eine Ausnahmeliste); Angriff und Flucht sind keine Varianten, sondern `UseAbility("core:attack")` und `UseAbility("core:flee")` — damit moddbar. Ein gemeinsamer `Vec<Screen>` fuer Screens und Popups; Screens **geben `Response` zurueck statt selbst zu schieben**, sonst haelt ein Screen ein `&mut` auf die `Vec`, in der er liegt. Ein `Response::Command` schliesst alle Overlays — dasselbe Flag, das schon fuers Rendern existiert. Zehn Screens, `ListState` in der Variante (der Stack erhaelt den Cursor gratis, solange der Screen liegt). Nur `KeyEventKind::Press`: bei `Repeat` bestimmte die Tastaturwiederholrate die Spielzeit; gegen zaehes Laufen hilft ein Renn-Kommando in `game/`. Tastenbelegung fest, Pfeile und `hjkl`. ratatui-Freiheit von `game/` prueft ein `#[test]` statt eines Workspace. Nachtrag an 01: `symbol` traegt das Zeichen und einen Farbnamen selbst — `ui/theme.rs` ist Code und damit fuer Modder unerreichbar. - [11 — UI-Gerüst und Screen-Modell](issues/11-ui-geruest-screen-modell.md) ([#12](https://gitlab.hive-brain.de/MDietermann/rustkeep/issues/12)) — **„Aktion" hiess zwei Dinge**: `Action` bleibt das TOML-Effektvokabular aus 01, `Command` wird das UI-Kommando, Eingang `Game::execute(cmd, reg)` — die eine Stelle, an der ein Zug endet (Modifikatoren ticken, Event-Warteschlange leerarbeiten). Zwölf Commands, **alle kosten einen Zug** (ein `costs_turn`-Flag waere eine Ausnahmeliste); Angriff und Flucht sind keine Varianten, sondern `UseAbility("core:attack")` und `UseAbility("core:flee")` — damit moddbar. Ein gemeinsamer `Vec<Screen>` fuer Screens und Popups; Screens **geben `Response` zurueck statt selbst zu schieben**, sonst haelt ein Screen ein `&mut` auf die `Vec`, in der er liegt. Ein `Response::Command` schliesst alle Overlays — dasselbe Flag, das schon fuers Rendern existiert. Zehn Screens, `ListState` in der Variante (der Stack erhaelt den Cursor gratis, solange der Screen liegt). Nur `KeyEventKind::Press`: bei `Repeat` bestimmte die Tastaturwiederholrate die Spielzeit; gegen zaehes Laufen hilft ein Renn-Kommando in `game/`. Tastenbelegung fest, Pfeile und `hjkl`. ratatui-Freiheit von `game/` prueft ein `#[test]` statt eines Workspace. Nachtrag an 01: `symbol` traegt das Zeichen und einen Farbnamen selbst — `ui/theme.rs` ist Code und damit fuer Modder unerreichbar.
- [12 — Prototyp: ratatui-Durchstich](issues/12-prototyp-ratatui-durchstich.md) ([#13](https://gitlab.hive-brain.de/MDietermann/rustkeep/issues/13)) — **Das Modell aus 11 traegt.** Primaerquelle: Branch `prototyp/12-ratatui-durchstich`, `cargo run --example durchstich`. Bestaetigt: `mod game` bleibt ratatui-frei ohne Disziplin, Grid als eigenes `impl Widget` ueber `buf[(x,y)].set_symbol(…)`, `Vec<Screen>` mit Overlay-Flag fuer Rendern *und* Auto-Schliessen, `Response` statt Stack-Mutation (kein Borrow-Aerger). **Der Windows-Fallstrick ist jetzt eine Zahl**: `Tasten 14 → Press 7 → Zuege 7` — ohne Filter kostete jeder Schritt zwei Zuege und damit doppelte Spielzeit. Zwei Korrekturen: 11 sagte „alle Commands kosten einen Zug", richtig ist **alle ausgefuehrten** (`Outcome { TurnPassed, Rejected(&str) }`, Grund reist mit zurueck); und `Command` muss `Clone` sein, damit `Confirm` es herausgeben kann. Ungetestet blieb `Registries` neben `Game` — der Durchstich laedt keine.
## Not yet specified ## Not yet specified

View File

@@ -157,6 +157,8 @@ auf welcher Seite er steht.
## Zug ## Zug
Die einzige Zeiteinheit des Spiels. Ein Feld weitergehen ist ein Zug, eine Kampfaktion ist ein Zug. Die einzige Zeiteinheit des Spiels. Ein Feld weitergehen ist ein Zug, eine Kampfaktion ist ein Zug.
Ein **abgewiesenes** [Command](#command) kostet dagegen keinen — gegen eine Wand zu laufen verbraucht
keine Zeit.
Es gibt keine getrennte Kampfrunden-Uhr: Befristete [Modifikatoren](#modifikator) ticken auch beim Es gibt keine getrennte Kampfrunden-Uhr: Befristete [Modifikatoren](#modifikator) ticken auch beim
Erkunden, damit „ich warte den Debuff draußen ab" eine Entscheidung mit Kosten bleibt. Erkunden, damit „ich warte den Debuff draußen ab" eine Entscheidung mit Kosten bleibt.