fix(bogen): vorgemerkte Punkte beim Abschicken verbrauchen
Nach Enter stand unspent_points auf 0, waehrend der Screen den Punkt noch vorgemerkt hielt — die Anzeige rechnete 0 - 1 auf u32 und stuerzte ab. Der Screen leert sein Feld, waehrend er das Kommando uebergibt, dazu saturating_sub an den zwei Lesestellen: unspent_points kann sich auch unter dem offenen Screen aendern. Erster UI-Test des Projekts. handle_key ist ohne Terminal aufrufbar, und sobald ein Screen Zustand haelt, ist die Naht zum Spielzustand eine Fehlerquelle wie jede andere.
This commit is contained in:
@@ -272,3 +272,21 @@ vorbeigeht, testet den Spielablauf nicht.
|
|||||||
**Immer mitgebaut**, nicht hinter `cfg(debug_assertions)`. Das wäre ein `#[cfg]` an der Variante *und*
|
**Immer mitgebaut**, nicht hinter `cfg(debug_assertions)`. Das wäre ein `#[cfg]` an der Variante *und*
|
||||||
an jedem `match` darüber — für ein Spiel, das niemand ausliefert. Ein `#[cfg]` reicht später nach,
|
an jedem `match` darüber — für ein Spiel, das niemand ausliefert. Ein `#[cfg]` reicht später nach,
|
||||||
wenn es je einen Release-Build gibt.
|
wenn es je einen Release-Build gibt.
|
||||||
|
|
||||||
|
### Befund aus M5 — die Naht zwischen Screen-Zustand und Spielzustand
|
||||||
|
|
||||||
|
Ticket 11 trennt sauber: Ein Screen fasst `Game` nicht an, er gibt ein `Command` zurück. Der
|
||||||
|
Charakterbogen hält seit M5 aber selbst Zustand — die **vorgemerkten** Punkte, die erst mit `Enter`
|
||||||
|
abgeschickt werden. Beim Abschicken zieht das Spiel sie ab, der Screen behielt sie: `unspent_points`
|
||||||
|
stand auf 0, `pending` auf 1, und die Anzeige rechnete `0 - 1` auf `u32`. Absturz.
|
||||||
|
|
||||||
|
Das ist keine Schwäche der Trennung, sondern ihr Preis: Wer Zustand doppelt hält, muss sagen, wann er
|
||||||
|
abgeglichen wird. Die Antwort hier ist die einfachste mögliche — **vorgemerkt wird beim Abschicken
|
||||||
|
verbraucht**, der Screen leert sein Feld, während er das Kommando übergibt. Dazu `saturating_sub` an
|
||||||
|
den zwei Lesestellen, weil `unspent_points` sich auch unter dem offenen Screen ändern kann (ein
|
||||||
|
Aufstieg im selben Zug).
|
||||||
|
|
||||||
|
Dabei ist der **erste UI-Test** des Projekts entstanden. Bis M5 war `ui/` ungetestet, weil dort
|
||||||
|
angeblich nur Anzeige liegt — der Absturz zeigt, dass das nicht stimmt: Sobald ein Screen Zustand hält,
|
||||||
|
ist die Naht zum Spielzustand eine Fehlerquelle wie jede andere. `handle_key` ist ohne Terminal
|
||||||
|
aufrufbar, der Test kostet zwanzig Zeilen.
|
||||||
|
|||||||
@@ -132,7 +132,7 @@ die Tickets zurückgetragen:
|
|||||||
Der Zugablauf aus **14** hat damit einen siebten Schritt, `Creature` ein Feld `intent`, und die
|
Der Zugablauf aus **14** hat damit einen siebten Schritt, `Creature` ein Feld `intent`, und die
|
||||||
Sperrgründe, die `ability_block` ohnehin berechnete, haben einen zweiten Nutzen bekommen.
|
Sperrgründe, die `ability_block` ohnehin berechnete, haben einen zweiten Nutzen bekommen.
|
||||||
|
|
||||||
**M5 — Progression** (`feat/m5-progression`, 64 Tests)
|
**M5 — Progression** (`feat/m5-progression`, 65 Tests)
|
||||||
|
|
||||||
| Zusage | Aus | Befund |
|
| Zusage | Aus | Befund |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
@@ -146,6 +146,11 @@ Sperrgründe, die `ability_block` ohnehin berechnete, haben einen zweiten Nutzen
|
|||||||
| Lebensstand folgt dem Zuwachs an **zwei** Stellen | 15 | **es ist eine** — der Aufstieg ändert `max_health` gar nicht |
|
| Lebensstand folgt dem Zuwachs an **zwei** Stellen | 15 | **es ist eine** — der Aufstieg ändert `max_health` gar nicht |
|
||||||
| `luck` = eine Chance auf einen Zusatzzug, auf jeder Tabelle | 16 | **bestätigt**, wirkt dreifach aus einer Formel |
|
| `luck` = eine Chance auf einen Zusatzzug, auf jeder Tabelle | 16 | **bestätigt**, wirkt dreifach aus einer Formel |
|
||||||
|
|
||||||
|
Ein fünfter kam beim Spielen dazu: **11** — der Charakterbogen hält seit M5 selbst Zustand (die
|
||||||
|
vorgemerkten Punkte). Beim Abschicken zog das Spiel sie ab, der Screen behielt sie, und die Anzeige
|
||||||
|
rechnete `0 - 1` auf `u32`. Daraus der **erste UI-Test** des Projekts: Sobald ein Screen Zustand hält,
|
||||||
|
ist die Naht zum Spielzustand eine Fehlerquelle wie jede andere.
|
||||||
|
|
||||||
Vier Befunde nachgetragen: **15** (`strength` und `constitution` las niemand — zwei der fünf
|
Vier Befunde nachgetragen: **15** (`strength` und `constitution` las niemand — zwei der fünf
|
||||||
Wahlmöglichkeiten taten nichts), **15** (von den zwei Zuwachs-Stellen ist eine beweisbar ein No-op),
|
Wahlmöglichkeiten taten nichts), **15** (von den zwei Zuwachs-Stellen ist eine beweisbar ein No-op),
|
||||||
**14/16** (`Rng::chance(0.0)` verbrauchte eine Zahl und verschob den Zufallsstrom — derselbe Seed
|
**14/16** (`Rng::chance(0.0)` verbrauchte eine Zahl und verschob den Zufallsstrom — derselbe Seed
|
||||||
|
|||||||
@@ -578,7 +578,7 @@ fn draw_sheet(frame: &mut Frame, area: Rect, app: &App, pick: usize, pending: &[
|
|||||||
let mut lines = Vec::new();
|
let mut lines = Vec::new();
|
||||||
|
|
||||||
// Erfahrung und Punkte stehen an `Game`, nicht am `Creature` (Ticket 15).
|
// Erfahrung und Punkte stehen an `Game`, nicht am `Creature` (Ticket 15).
|
||||||
let offen = app.game.unspent_points - pending.iter().sum::<u32>();
|
let offen = app.game.unspent_points.saturating_sub(pending.iter().sum::<u32>());
|
||||||
lines.push(Line::from(format!(
|
lines.push(Line::from(format!(
|
||||||
"Erfahrung {}/{}",
|
"Erfahrung {}/{}",
|
||||||
app.game.current_exp,
|
app.game.current_exp,
|
||||||
|
|||||||
@@ -219,7 +219,9 @@ impl Screen {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Screen::CharacterSheet { pick, pending } => {
|
Screen::CharacterSheet { pick, pending } => {
|
||||||
let offen = game.unspent_points - pending.iter().sum::<u32>();
|
// `saturating_sub`, weil `unspent_points` sich unter dem
|
||||||
|
// offenen Screen aendern kann — ein Aufstieg im selben Zug.
|
||||||
|
let offen = game.unspent_points.saturating_sub(pending.iter().sum::<u32>());
|
||||||
match key {
|
match key {
|
||||||
KeyCode::Up | KeyCode::Char('k') => {
|
KeyCode::Up | KeyCode::Char('k') => {
|
||||||
*pick = pick.saturating_sub(1);
|
*pick = pick.saturating_sub(1);
|
||||||
@@ -239,14 +241,19 @@ impl Screen {
|
|||||||
}
|
}
|
||||||
// **Alle** Punkte in einem Kommando und damit in einem Zug
|
// **Alle** Punkte in einem Kommando und damit in einem Zug
|
||||||
// (Ticket 15).
|
// (Ticket 15).
|
||||||
KeyCode::Enter => Response::Command(Command::AssignPoints(
|
KeyCode::Enter => {
|
||||||
Attribute::ASSIGNABLE
|
let punkte = Attribute::ASSIGNABLE
|
||||||
.iter()
|
.iter()
|
||||||
.zip(pending.iter())
|
.zip(pending.iter())
|
||||||
.filter(|(_, n)| **n > 0)
|
.filter(|(_, n)| **n > 0)
|
||||||
.map(|(a, n)| (*a, *n))
|
.map(|(a, n)| (*a, *n))
|
||||||
.collect(),
|
.collect();
|
||||||
)),
|
// **Vorgemerktes wird beim Abschicken verbraucht.** Ohne
|
||||||
|
// das behaelt der Screen Punkte, die das Spiel schon
|
||||||
|
// abgezogen hat — und rechnet `0 - 1` auf `u32`.
|
||||||
|
*pending = [0; 5];
|
||||||
|
Response::Command(Command::AssignPoints(punkte))
|
||||||
|
}
|
||||||
KeyCode::Esc | KeyCode::Char('c' | 'q') => Response::Pop,
|
KeyCode::Esc | KeyCode::Char('c' | 'q') => Response::Pop,
|
||||||
_ => Response::None,
|
_ => Response::None,
|
||||||
}
|
}
|
||||||
@@ -341,3 +348,44 @@ impl Screen {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::data::load_mods;
|
||||||
|
use crate::game::{Creature, Rng};
|
||||||
|
|
||||||
|
fn spiel() -> (Game, Registries) {
|
||||||
|
let (reg, _) = load_mods().expect("core muss laden");
|
||||||
|
let game = Game::new(Creature::new("Test"), ®, Rng::new(1)).expect("Etage 1");
|
||||||
|
(game, reg)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gefunden im Spiel: Nach dem Abschicken stand `unspent_points` auf 0,
|
||||||
|
/// waehrend der Screen den Punkt noch vorgemerkt hielt — `0 - 1` auf `u32`.
|
||||||
|
/// Der erste Test, der **Screen-Zustand gegen Spielzustand** prueft; die
|
||||||
|
/// Trennung aus Ticket 11 macht genau diese Naht zur Fehlerquelle.
|
||||||
|
#[test]
|
||||||
|
fn vorgemerkte_punkte_werden_beim_abschicken_verbraucht() {
|
||||||
|
let (mut game, reg) = spiel();
|
||||||
|
game.unspent_points = 1;
|
||||||
|
let mut screen = Screen::sheet();
|
||||||
|
|
||||||
|
screen.handle_key(KeyCode::Char('+'), &game, ®);
|
||||||
|
let Screen::CharacterSheet { pending, .. } = &screen else { panic!("falscher Screen") };
|
||||||
|
assert_eq!(pending[0], 1, "der Punkt ist vorgemerkt");
|
||||||
|
|
||||||
|
let antwort = screen.handle_key(KeyCode::Enter, &game, ®);
|
||||||
|
let Response::Command(cmd) = antwort else { panic!("Enter muss abschicken") };
|
||||||
|
assert!(matches!(game.execute(cmd, ®), crate::game::Outcome::TurnPassed));
|
||||||
|
assert_eq!(game.unspent_points, 0);
|
||||||
|
|
||||||
|
let Screen::CharacterSheet { pending, .. } = &screen else { panic!("falscher Screen") };
|
||||||
|
assert_eq!(pending[0], 0, "und im Screen nicht mehr vorgemerkt");
|
||||||
|
|
||||||
|
// Der Zustand, der abgestuerzt ist: kein offener Punkt, keine Vormerkung.
|
||||||
|
screen.handle_key(KeyCode::Char('+'), &game, ®);
|
||||||
|
let Screen::CharacterSheet { pending, .. } = &screen else { panic!("falscher Screen") };
|
||||||
|
assert_eq!(pending[0], 0, "ohne offene Punkte laesst sich nichts vormerken");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user