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:
117
src/game/mod.rs
117
src/game/mod.rs
@@ -4,14 +4,28 @@ pub mod attributes;
|
||||
pub mod command;
|
||||
pub mod creature;
|
||||
pub mod floor;
|
||||
pub mod generate;
|
||||
pub mod rng;
|
||||
|
||||
pub use attributes::{Attribute, Modifier};
|
||||
pub use command::{Command, Direction, Outcome};
|
||||
pub use creature::{Creature, CreatureRef, ItemInstance};
|
||||
pub use floor::{Floor, Tile};
|
||||
pub use rng::Rng;
|
||||
|
||||
use crate::data::Registries;
|
||||
|
||||
/// Wie ein Lauf endet (Ticket 10/14). Sieg ueber das **Fehlen** der naechsten
|
||||
/// Etage — es gibt keine Zahl „5" im Code.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum RunOutcome {
|
||||
/// TODO(M3): wird gesetzt, sobald es Kampf gibt und der Held sterben kann.
|
||||
/// Der `GameOver`-Screen zeigt beide Enden bereits.
|
||||
#[allow(dead_code)]
|
||||
Died,
|
||||
Won,
|
||||
}
|
||||
|
||||
pub struct Game {
|
||||
pub player: Creature,
|
||||
/// Der **laufende Kampf**, nicht die Etage (Ticket 07/14). Leer = kein Kampf.
|
||||
@@ -19,27 +33,36 @@ pub struct Game {
|
||||
pub floor: Floor,
|
||||
pub player_pos: (u16, u16),
|
||||
pub turn: u64,
|
||||
/// Im Speicherstand, damit Nachladen nicht neu wuerfelt (Ticket 14).
|
||||
pub rng: Rng,
|
||||
pub over: Option<RunOutcome>,
|
||||
/// Letzte Meldung fuer die Statuszeile.
|
||||
pub last: String,
|
||||
// TODO(M3): `events: Vec<Event>` (Warteschlange aus Ticket 08) und der
|
||||
// xorshift-Seed aus Ticket 14. Beide brauchen erst einen Trigger
|
||||
// bzw. einen Wurf, um etwas zu tun.
|
||||
// TODO(M3): `events: Vec<Event>` — die Warteschlange aus Ticket 08.
|
||||
// TODO(M4): `quests`, `current_exp`, `unspent_points` (Ticket 15 — an
|
||||
// `Game`, nicht am `Creature`).
|
||||
}
|
||||
|
||||
impl Game {
|
||||
pub fn new(player: Creature) -> Self {
|
||||
let floor = Floor::test_room();
|
||||
/// Erzeugt die erste Etage. Schlaegt fehl, wenn keine `[[floors]]` mit
|
||||
/// `number = 1` existiert — dann ist `core` kaputt, und das faellt beim
|
||||
/// `cargo test` auf (Ticket 04).
|
||||
pub fn new(player: Creature, reg: &Registries, mut rng: Rng) -> Option<Self> {
|
||||
let def = reg.floor_number(1)?;
|
||||
let mut floor = generate::generate(def, reg, &mut rng);
|
||||
let player_pos = floor.entrance();
|
||||
Self {
|
||||
floor.player_room = floor.room_at(player_pos.0, player_pos.1);
|
||||
|
||||
Some(Self {
|
||||
player,
|
||||
enemies: Vec::new(),
|
||||
floor,
|
||||
player_pos,
|
||||
turn: 0,
|
||||
rng,
|
||||
over: None,
|
||||
last: String::from("—"),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn creature(&self, who: CreatureRef) -> &Creature {
|
||||
@@ -86,13 +109,14 @@ impl Game {
|
||||
}
|
||||
|
||||
/// Der **eine** Eingang, an dem ein Zug endet (Ticket 11).
|
||||
pub fn execute(&mut self, cmd: Command, _reg: &Registries) -> Outcome {
|
||||
pub fn execute(&mut self, cmd: Command, reg: &Registries) -> Outcome {
|
||||
let outcome = match cmd {
|
||||
Command::Move(dir) => self.step(dir),
|
||||
Command::Wait => {
|
||||
self.last = String::from("gewartet");
|
||||
Outcome::TurnPassed
|
||||
}
|
||||
Command::Descend => self.descend(reg),
|
||||
};
|
||||
|
||||
// Nur ausgefuehrte Kommandos kosten einen Zug (Ticket 12).
|
||||
@@ -119,12 +143,51 @@ impl Game {
|
||||
}
|
||||
|
||||
self.player_pos = (nx, ny);
|
||||
|
||||
// Korridore gehoeren zu keinem Raum — `None` ist ein echter Zustand.
|
||||
let room = self.floor.room_at(nx, ny);
|
||||
if room != self.floor.player_room {
|
||||
self.floor.player_room = room;
|
||||
// TODO(M3): hier feuert `room_entered` (Ticket 08/10) und schiebt
|
||||
// die Gegner des Raums nach `Game.enemies`.
|
||||
}
|
||||
|
||||
self.last = match self.floor.tile(nx, ny) {
|
||||
Tile::Stairs => String::from("Eine Treppe führt nach unten"),
|
||||
_ => format!("{},{}", nx, ny),
|
||||
Tile::Stairs => String::from("Eine Treppe führt nach unten — '>' zum Absteigen"),
|
||||
_ => self.room_label(),
|
||||
};
|
||||
Outcome::TurnPassed
|
||||
}
|
||||
|
||||
fn room_label(&self) -> String {
|
||||
match self.floor.player_room {
|
||||
Some(i) => self.floor.rooms[i].prefab.clone(),
|
||||
None => String::from("Korridor"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Ticket 10: Der Abstieg von der **letzten definierten Etage gewinnt den
|
||||
/// Lauf**. Es gibt keine Zahl „5" im Code — die Engine sucht `[[floors]]`
|
||||
/// mit `number = aktuell + 1`, und findet sie keine, ist Schluss. Ein Mod,
|
||||
/// der Etage 6 ergaenzt, verlaengert das Spiel ohne eine Zeile Engine-Code.
|
||||
fn descend(&mut self, reg: &Registries) -> Outcome {
|
||||
if self.floor.tile(self.player_pos.0, self.player_pos.1) != Tile::Stairs {
|
||||
return Outcome::Rejected("Hier ist keine Treppe");
|
||||
}
|
||||
|
||||
let Some(def) = reg.floor_number(self.floor.number + 1) else {
|
||||
self.over = Some(RunOutcome::Won);
|
||||
self.last = String::from("Du steigst ans Licht.");
|
||||
return Outcome::TurnPassed;
|
||||
};
|
||||
|
||||
self.floor = generate::generate(def, reg, &mut self.rng);
|
||||
self.player_pos = self.floor.entrance();
|
||||
self.floor.player_room = self.floor.room_at(self.player_pos.0, self.player_pos.1);
|
||||
self.enemies.clear();
|
||||
self.last = format!("Etage {}", self.floor.number);
|
||||
Outcome::TurnPassed
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -138,7 +201,38 @@ mod tests {
|
||||
hero.options = vec![String::from("core:dwarf"), String::from("core:warrior")];
|
||||
hero.equipment
|
||||
.insert(String::from("core:hand"), ItemInstance::new("core:iron_sword"));
|
||||
(Game::new(hero), reg)
|
||||
let game = Game::new(hero, ®, Rng::new(99)).expect("Etage 1 muss existieren");
|
||||
(game, reg)
|
||||
}
|
||||
|
||||
/// Ticket 10: Sieg ueber das **Fehlen** der naechsten Etage. Wer bis unter
|
||||
/// die letzte definierte Etage steigt, hat gewonnen — ohne dass irgendwo
|
||||
/// eine Fuenf im Code steht.
|
||||
#[test]
|
||||
fn abstieg_unter_die_letzte_etage_gewinnt() {
|
||||
let (mut game, reg) = zwergenkrieger();
|
||||
let letzte = reg.floors.values().map(|f| f.number).max().expect("Etagen da");
|
||||
|
||||
for _ in 0..letzte {
|
||||
let treppe = (0..game.floor.height)
|
||||
.flat_map(|y| (0..game.floor.width).map(move |x| (x, y)))
|
||||
.find(|&(x, y)| game.floor.tile(x, y) == Tile::Stairs)
|
||||
.expect("jede Etage hat eine Treppe (Pflichtraum)");
|
||||
|
||||
game.player_pos = treppe;
|
||||
assert!(matches!(game.execute(Command::Descend, ®), Outcome::TurnPassed));
|
||||
}
|
||||
|
||||
assert_eq!(game.over, Some(RunOutcome::Won));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn absteigen_ohne_treppe_kostet_keinen_zug() {
|
||||
let (mut game, reg) = zwergenkrieger();
|
||||
game.player_pos = game.floor.entrance();
|
||||
|
||||
assert!(matches!(game.execute(Command::Descend, ®), Outcome::Rejected(_)));
|
||||
assert_eq!(game.turn, 0);
|
||||
}
|
||||
|
||||
/// Der Zielsatz von Meilenstein 1 als Behauptung: ein Wert, der aus einem
|
||||
@@ -177,6 +271,7 @@ mod tests {
|
||||
#[test]
|
||||
fn wand_kostet_keinen_zug() {
|
||||
let (mut game, reg) = zwergenkrieger();
|
||||
// Die Ecke oben links ist auf jeder erzeugten Etage Fels.
|
||||
game.player_pos = (1, 1);
|
||||
|
||||
assert!(matches!(
|
||||
|
||||
Reference in New Issue
Block a user