prototyp: abgewiesene kommandos kosten keinen zug, zaehler beschriftet

This commit is contained in:
2026-08-12 11:17:31 +02:00
parent 2ecf71af37
commit 604b8987ba

View File

@@ -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");
Outcome::TurnPassed
}
};
// 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;
}
}
// 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;
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<Screen>,
/// 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 {