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

@@ -1,4 +1,10 @@
//! Etage — Ticket 10 entscheidet den Innenbau, M1 baut einen Testraum von Hand.
//! Etage — Innenbau aus Ticket 10.
//!
//! `Room.enemy_spawns`, `loot` und `cleared` liest in M2 noch niemand: Sie
//! brauchen Kampf (M3) und Beutetabellen (M4). Der Generator fuellt sie aber
//! bereits, weil die Marker im Prefab stehen — sie erst spaeter zu lesen
//! hiesse, die Prefabs zweimal zu parsen.
#![allow(dead_code)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Tile {
@@ -9,16 +15,53 @@ pub enum Tile {
Entrance,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Rect {
pub x: u16,
pub y: u16,
pub w: u16,
pub h: u16,
}
impl Rect {
/// Ueberlappt oder beruehrt — inklusive einem Feld Rand, damit zwei Raeume
/// nicht Wand an Wand kleben.
pub fn touches(&self, other: &Rect) -> bool {
self.x < other.x + other.w + 1
&& other.x < self.x + self.w + 1
&& self.y < other.y + other.h + 1
&& other.y < self.y + self.h + 1
}
pub fn contains(&self, x: u16, y: u16) -> bool {
x >= self.x && y >= self.y && x < self.x + self.w && y < self.y + self.h
}
}
#[derive(Debug)]
pub struct Room {
/// Fuer `room_entered` und die Trigger am Prefab (Ticket 08/10).
pub prefab: String,
pub rect: Rect,
/// Die `e`-Plaetze aus dem Prefab.
// TODO(M3): wird zu `enemies: Vec<Creature>` — was einzieht, entscheidet
// die `creatures`-Zeile der Etage (Ticket 10/14).
pub enemy_spawns: Vec<(u16, u16)>,
/// Die `l`-Plaetze. Ticket 16: erst beim **Aufheben** aufgeloest, deshalb
/// stehen hier Positionen und keine Gegenstaende.
pub loot: Vec<(u16, u16)>,
pub cleared: bool,
}
#[derive(Debug)]
pub struct Floor {
pub number: u8,
pub width: u16,
pub height: u16,
pub tiles: Vec<Tile>,
// TODO(M2): `rooms: Vec<Room>` und `player_room: Option<usize>` aus Ticket
// 10 — mit ihnen kommen `room_entered`, Kampfbeginn und die
// Raum-Beutetabellen. M1 hat genau einen Raum und braucht die
// Buchfuehrung noch nicht.
pub rooms: Vec<Room>,
/// `None` ist ein echter Zustand: Korridore gehoeren zu keinem Raum.
pub player_room: Option<usize>,
}
impl Floor {
@@ -29,40 +72,7 @@ impl Floor {
self.tiles[y as usize * self.width as usize + x as usize]
}
/// Handgebauter Testraum. Ticket 10 legt fest, dass Prefabs als ASCII-Bloecke
/// geschrieben werden — deshalb steht der Raum hier schon in derselben Form,
/// auch wenn der Generator erst in M2 kommt.
pub fn test_room() -> Self {
const LAYOUT: &[&str] = &[
"########################",
"#......................#",
"#..####....######......#",
"#..#..........#........#",
"#..#..........#...##...#",
"#.....####....#...##...#",
"#........#........>....#",
"#..<.....#.............#",
"########################",
];
let width = LAYOUT[0].len() as u16;
let height = LAYOUT.len() as u16;
let tiles = LAYOUT
.iter()
.flat_map(|row| row.chars())
.map(|c| match c {
'#' => Tile::Wall,
'>' => Tile::Stairs,
'<' => Tile::Entrance,
'+' => Tile::Door,
_ => Tile::Floor,
})
.collect();
Self { number: 1, width, height, tiles }
}
/// Startfeld: das `<` im Layout, sonst das erste begehbare Feld.
/// Startfeld: das `<` aus dem Eingangs-Prefab, sonst das erste begehbare.
pub fn entrance(&self) -> (u16, u16) {
for y in 0..self.height {
for x in 0..self.width {
@@ -71,21 +81,18 @@ impl Floor {
}
}
}
for y in 0..self.height {
for x in 0..self.width {
if self.tile(x, y) != Tile::Wall {
return (x, y);
}
}
}
(1, 1)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn testraum_ist_rechteckig_und_hat_eingang() {
let f = Floor::test_room();
assert_eq!(f.tiles.len(), f.width as usize * f.height as usize);
assert_eq!(f.tile(f.entrance().0, f.entrance().1), Tile::Entrance);
// Rand ist geschlossen, sonst laeuft der Held aus der Karte.
assert_eq!(f.tile(0, 0), Tile::Wall);
assert_eq!(f.tile(f.width - 1, f.height - 1), Tile::Wall);
/// In welchem Raum steht diese Position — `None` heisst Korridor.
pub fn room_at(&self, x: u16, y: u16) -> Option<usize> {
self.rooms.iter().position(|r| r.rect.contains(x, y))
}
}