feat: m2 etagengenerierung — prefabs, floors, korridore, abstieg und sieg

Der handgebaute Testraum ist weg; Etagen entstehen aus Daten.

- game/rng.rs: xorshift64*, Seed im Game (Ticket 14)
- game/generate.rs: freie Platzierung, Pflichtraeume zuerst, Kette
  i->i-1 fuer strukturelle Erreichbarkeit, acht Orientierungen
- data/: [[prefabs]] und [[floors]] als neunte und zehnte Registry,
  Ladepruefungen fuer rechteckige Layouts und Pflichtraum-Flaeche
- Descend plus Siegbedingung: keine [[floors]] mit der naechsten Nummer
  heisst gewonnen — die Zahl 5 steht nirgends im Code
- GameOver-Screen, mitlaufender Bildausschnitt im Grid

Befund: Korridore stanzten 2340 Loecher in gezeichnete Waende, weil sie
AUF der Tuer starteten. Erst Schritt nach aussen, dann L-Form, sonst
Wegsuche ueber den freien Raum — die kann eine Raumwand nicht beruehren.
Der Test zaehlt jetzt null.

21 Tests.
This commit is contained in:
2026-08-12 16:14:23 +02:00
parent 3714bad313
commit f998e55210
14 changed files with 1121 additions and 82 deletions

View File

@@ -11,7 +11,7 @@ use super::grid::GridWidget;
use super::screen::{Response, Screen};
use super::theme;
use crate::data::Registries;
use crate::game::{Attribute, CreatureRef, Game, Outcome};
use crate::game::{Attribute, CreatureRef, Game, Outcome, RunOutcome};
pub struct App {
/// `game` und `registries` sind **getrennte Felder**, damit die disjunkten
@@ -66,6 +66,11 @@ pub fn run(game: Game, registries: Registries, warnings: usize, log_name: Option
while app.screens.last().is_some_and(Screen::is_overlay) {
app.screens.pop();
}
// Ticket 10/14: Der Lauf endet ueber `Game`, nicht ueber
// die UI — die reagiert nur darauf.
if app.game.over.is_some() {
app.screens.push(Screen::GameOver);
}
}
Response::Quit => break Ok(()),
}
@@ -82,6 +87,7 @@ fn draw(frame: &mut Frame, app: &App) {
Screen::MainMenu => draw_main_menu(frame, area, app),
Screen::Explore => draw_explore(frame, area, app),
Screen::CharacterSheet => draw_sheet(frame, area, app),
Screen::GameOver => draw_game_over(frame, area, app),
}
}
}
@@ -128,7 +134,7 @@ fn draw_explore(frame: &mut Frame, area: Rect, app: &App) {
};
frame.render_widget(
Paragraph::new(format!(
"Zug {} │ {} │ c Charakterbogen │ {}",
"Zug {} │ {} │ hjkl/Pfeile .warten >absteigen c Bogen │ {}",
app.game.turn, app.game.player.name, note
))
.right_aligned()
@@ -137,6 +143,38 @@ fn draw_explore(frame: &mut Frame, area: Rect, app: &App) {
);
}
/// Der Lauf wird am Ende **erzaehlt** — das ist der Moment, fuer den man die
/// Etagen gespielt hat, und der Grund, warum Ticket 14 hier keinen Einzeiler im
/// Hauptmenue wollte.
fn draw_game_over(frame: &mut Frame, area: Rect, app: &App) {
let (titel, farbe, zeile) = match app.game.over {
Some(RunOutcome::Won) => (
" Gewonnen ",
Color::Green,
"Du steigst ans Licht — der Keep liegt hinter dir.",
),
_ => (" Gestorben ", Color::Red, "Der Keep behält dich."),
};
let text = vec![
Line::from(app.game.player.name.clone().bold()),
Line::from(""),
Line::from(zeile),
Line::from(""),
Line::from(format!("Etage {}", app.game.floor.number)),
Line::from(format!("Züge {}", app.game.turn)),
Line::from(""),
Line::from("Taste drücken").style(Style::new().fg(Color::DarkGray)),
];
frame.render_widget(
Paragraph::new(text)
.centered()
.block(Block::bordered().title(titel).style(Style::new().fg(farbe))),
area,
);
}
/// Der Charakterbogen ist eine **Schleife ueber Registries**, kein gemauertes
/// Layout — Modder koennen Slots und Wahlkategorien ergaenzen (Ticket 02).
fn draw_sheet(frame: &mut Frame, area: Rect, app: &App) {