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:
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user