diff options
| author | skylar <me@casper> | 2026-11-10 06:50:08 -0800 |
|---|---|---|
| committer | skylar <me@casper> | 2026-11-10 06:50:08 -0800 |
| commit | d36c54d996f3c14ae811312dbbe621e12a39edde (patch) | |
| tree | 1807134323479ac90bf69d28e410a7f70cb235a1 /src/dungeon/mod.rs | |
Orrery: game loop, name generator and derelict dungeons
An ASCII sci-fi-fantasy game for the terminal, built on ratatui.
- Game loop with a fixed 30 Hz tick and a stack of scenes; the kitty
keyboard protocol is turned on where the terminal supports it, so later
real-time scenes can tell when a key is held.
- Stable hierarchical seeds: the whole universe derives from one u64,
with tests pinning the exact outputs so old seeds keep working.
- Name generator with four cultures, each with its own syllables and
templates, plus unidentified/true names for artifacts.
- Title screen with an animated orrery, and a name forge for browsing
generated names.
- Derelict ships to board: ship-shaped maps with an engine room, spine
corridor, side rooms and bridge; symmetric shadowcasting sight;
monsters that wake on sight and hunt; loot carried out through the
airlock goes into the hold.
- `orrery names` and `orrery ship` print the generators' output without
the TUI, for tuning.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'src/dungeon/mod.rs')
| -rw-r--r-- | src/dungeon/mod.rs | 156 |
1 files changed, 156 insertions, 0 deletions
diff --git a/src/dungeon/mod.rs b/src/dungeon/mod.rs new file mode 100644 index 0000000..d3227a5 --- /dev/null +++ b/src/dungeon/mod.rs @@ -0,0 +1,156 @@ +//! Turn-based dungeons: the map, field of view, derelict-ship generation, +//! and the rules of a crawl. Drawing lives in `scenes::derelict`. + +pub mod crawl; +pub mod fov; +pub mod ship; + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub struct Pos { + pub x: i32, + pub y: i32, +} + +impl Pos { + pub const fn new(x: i32, y: i32) -> Pos { + Pos { x, y } + } + + pub const fn offset(self, dx: i32, dy: i32) -> Pos { + Pos::new(self.x + dx, self.y + dy) + } + + /// Steps from here to `other` when diagonal moves are allowed. + pub fn steps_to(self, other: Pos) -> i32 { + (self.x - other.x).abs().max((self.y - other.y).abs()) + } + + /// The eight cells around this one. + pub fn neighbours(self) -> impl Iterator<Item = Pos> { + const DIRS: [(i32, i32); 8] = + [(-1, -1), (0, -1), (1, -1), (-1, 0), (1, 0), (-1, 1), (0, 1), (1, 1)]; + DIRS.into_iter().map(move |(dx, dy)| self.offset(dx, dy)) + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Tile { + /// Outside the ship. + Void, + /// The outer skin of the ship. + Hull, + /// An inside wall. + Wall, + Floor, + /// Floor strewn with wreckage. Walkable; it just looks different. + Debris, + Door { + open: bool, + }, + /// The way out: the airlock your own ship is docked to. + Airlock, +} + +impl Tile { + /// Whether you can step onto it. Closed doors open when bumped instead, + /// and stepping into the airlock leaves the ship. + pub fn walkable(self) -> bool { + matches!(self, Tile::Floor | Tile::Debris | Tile::Door { open: true }) + } + + pub fn blocks_sight(self) -> bool { + matches!( + self, + Tile::Void | Tile::Hull | Tile::Wall | Tile::Door { open: false } | Tile::Airlock + ) + } +} + +/// A named area of the map, shown to the player while they stand in it. +pub struct Room { + pub name: &'static str, +} + +pub struct Map { + pub width: i32, + pub height: i32, + tiles: Vec<Tile>, + room_of: Vec<Option<u16>>, + pub rooms: Vec<Room>, +} + +impl Map { + pub fn new(width: i32, height: i32, fill: Tile) -> Map { + let cells = (width * height) as usize; + Map { + width, + height, + tiles: vec![fill; cells], + room_of: vec![None; cells], + rooms: Vec::new(), + } + } + + pub fn contains(&self, p: Pos) -> bool { + p.x >= 0 && p.y >= 0 && p.x < self.width && p.y < self.height + } + + /// Index into a per-cell `Vec` laid out like the map. Panics off the map. + pub fn index(&self, p: Pos) -> usize { + assert!(self.contains(p), "{p:?} is off the map"); + (p.y * self.width + p.x) as usize + } + + /// The tile at `p`; everything off the map is `Void`. + pub fn tile(&self, p: Pos) -> Tile { + if self.contains(p) { self.tiles[self.index(p)] } else { Tile::Void } + } + + pub fn set(&mut self, p: Pos, tile: Tile) { + let i = self.index(p); + self.tiles[i] = tile; + } + + pub fn room_at(&self, p: Pos) -> Option<&Room> { + if !self.contains(p) { + return None; + } + self.room_of[self.index(p)].map(|r| &self.rooms[usize::from(r)]) + } + + pub fn add_room(&mut self, name: &'static str) -> u16 { + self.rooms.push(Room { name }); + (self.rooms.len() - 1) as u16 + } + + pub fn set_room(&mut self, p: Pos, room: Option<u16>) { + let i = self.index(p); + self.room_of[i] = room; + } + + pub fn positions(&self) -> impl Iterator<Item = Pos> + use<> { + let (w, h) = (self.width, self.height); + (0..h).flat_map(move |y| (0..w).map(move |x| Pos::new(x, y))) + } + + /// Steps from `from` to every cell reachable through walkable tiles and + /// closed doors, moving diagonally too; `None` where unreachable. + pub fn distances_from(&self, from: Pos) -> Vec<Option<u32>> { + let mut dist = vec![None; self.tiles.len()]; + let mut queue = std::collections::VecDeque::from([from]); + dist[self.index(from)] = Some(0); + while let Some(p) = queue.pop_front() { + let d = dist[self.index(p)].unwrap(); + for n in p.neighbours() { + let tile = self.tile(n); + if (tile.walkable() || tile == (Tile::Door { open: false })) + && dist[self.index(n)].is_none() + { + dist[self.index(n)] = Some(d + 1); + queue.push_back(n); + } + } + } + dist + } +} |