From 604b8987baef66adbdffd8808fd4a15051c47b1a Mon Sep 17 00:00:00 2001 From: MDietermann Date: Wed, 12 Aug 2026 11:17:31 +0200 Subject: [PATCH] prototyp: abgewiesene kommandos kosten keinen zug, zaehler beschriftet --- examples/durchstich.rs | 70 +++++++++++++++++++++++++++++------------- 1 file changed, 49 insertions(+), 21 deletions(-) diff --git a/examples/durchstich.rs b/examples/durchstich.rs index a04f777..adfb88e 100644 --- a/examples/durchstich.rs +++ b/examples/durchstich.rs @@ -35,6 +35,16 @@ mod game { Wait, } + /// BEFUND aus dem Durchstich: Ticket 11 sagte "alle Commands kosten einen + /// Zug". Richtig ist "alle *ausgefuehrten*". Gegen eine Wand zu laufen darf + /// keine Zeit verbrauchen — sonst tickt ein Gift-Debuff, weil jemand + /// danebengetippt hat. Der Grund muss mit zurueck, sonst steht der Spieler + /// vor einem Spiel, das schweigend nichts tut. + pub enum Outcome { + TurnPassed, + Rejected(&'static str), + } + pub struct Game { pub map: Vec<&'static str>, pub player: (u16, u16), @@ -78,8 +88,8 @@ mod game { } /// Ticket 11: der **eine** Eingang, an dem ein Zug endet. - pub fn execute(&mut self, cmd: Command) { - match cmd { + pub fn execute(&mut self, cmd: Command) -> Outcome { + let outcome = match cmd { Command::Move(dir) => { let (x, y) = self.player; let (nx, ny) = match dir { @@ -89,23 +99,28 @@ mod game { Direction::East => (x + 1, y), }; if self.tile(nx, ny) == '#' { - self.last = String::from("Wand"); + Outcome::Rejected("Da ist eine Wand") } else { self.player = (nx, ny); self.last = format!("nach {},{}", nx, ny); + Outcome::TurnPassed } - self.moves_executed += 1; } Command::Wait => { self.last = String::from("gewartet"); - self.moves_executed += 1; + Outcome::TurnPassed } - } + }; - // Ticket 01: eine Uhr, der Zug. Hier endet er — genau einmal, - // egal welches Kommando. TODO: befristete Modifikatoren ticken - // lassen und die Event-Warteschlange aus Ticket 08 leerarbeiten. - self.turn += 1; + // Ticket 01: eine Uhr, der Zug. Hier endet er — aber nur, wenn + // wirklich etwas geschehen ist. TODO: befristete Modifikatoren + // ticken lassen und die Event-Warteschlange aus Ticket 08 + // leerarbeiten — beides gehoert genau hierhin. + if let Outcome::TurnPassed = outcome { + self.turn += 1; + self.moves_executed += 1; + } + outcome } } } @@ -113,7 +128,7 @@ mod game { // ============================================================================ // mod ui — ab hier darf ratatui vorkommen // ============================================================================ -use game::{Command, Direction, Game}; +use game::{Command, Direction, Game, Outcome}; use ratatui::crossterm::event::{self, Event, KeyCode, KeyEventKind}; use ratatui::layout::Rect; use ratatui::style::{Color, Style, Stylize}; @@ -211,10 +226,13 @@ impl Widget for GridWidget<'_> { struct App { game: Game, screens: Vec, - /// Instrumentierung: Ohne den Press-Filter muesste dieser Zaehler unter - /// Windows doppelt so schnell steigen wie `game.moves_executed`. + /// Instrumentierung: `total` zaehlt jedes `Event::Key`, `press` nur die + /// gefilterten. Unter Windows laufen die beiden auseinander — genau das + /// belegt, dass der Filter aus Ticket 11 kein Aberglaube ist. key_events_total: u64, key_events_press: u64, + /// Grund des letzten abgewiesenen Kommandos, sonst `None`. + rejected: Option<&'static str>, } fn main() -> std::io::Result<()> { @@ -223,6 +241,7 @@ fn main() -> std::io::Result<()> { screens: vec![Screen::MainMenu], key_events_total: 0, key_events_press: 0, + rejected: None, }; ratatui::run(|terminal| { @@ -256,9 +275,14 @@ fn main() -> std::io::Result<()> { } Response::Push(s) => app.screens.push(s), Response::Command(cmd) => { - app.game.execute(cmd); + app.rejected = match app.game.execute(cmd) { + Outcome::TurnPassed => None, + Outcome::Rejected(reason) => Some(reason), + }; // Zusage 3: Ein Command schliesst alle Overlays — eine Zeile, - // keine Sonderbehandlung je Screen. + // keine Sonderbehandlung je Screen. Auch bei Rejected: die + // Rueckfrage *wurde* beantwortet, sie darf nicht kleben + // bleiben, nur weil das Kommando ins Leere lief. while app.screens.last().is_some_and(Screen::is_overlay) { app.screens.pop(); } @@ -317,19 +341,23 @@ fn draw(frame: &mut Frame, app: &App) { /// Regel 5 des Prototyp-Skills: Zustand nach jeder Aktion sichtbar machen. fn status(app: &App) -> Paragraph<'static> { let (x, y) = app.game.player; + let (note, color) = match app.rejected { + Some(reason) => (reason.to_string(), Color::Red), + None => (app.game.last.clone(), Color::Cyan), + }; Paragraph::new(format!( - "Zug {} │ Züge {} │ Events {} (Press {}) │ Pos {},{} │ Stack {} │ {}", + "Zug {} │ Pos {},{} │ Stack {} │ Tasten {} → Press {} → Züge {} │ {}", app.game.turn, - app.game.moves_executed, - app.key_events_total, - app.key_events_press, x, y, app.screens.len(), - app.game.last, + app.key_events_total, + app.key_events_press, + app.game.moves_executed, + note, )) .right_aligned() - .style(Style::new().fg(Color::Cyan)) + .style(Style::new().fg(color)) } fn centered(area: Rect, w: u16, h: u16) -> Rect {