// Map, tiles, generation and path finding. use serde::{Deserialize, Serialize}; use std::cmp::Reverse; use std::collections::BinaryHeap; pub const MW: usize = 64; pub const MH: usize = 64; pub const N: usize = MW * MH; #[derive(Clone, Copy, PartialEq, Eq, Debug, Serialize, Deserialize)] pub enum Kind { Rock, Earth, Gold, Floor, Treasury, Lair, Farm, Training, Library, Core, Portal, Gate, } #[derive(Clone, Copy, PartialEq, Eq, Debug, Serialize, Deserialize)] pub enum Owner { None, Player, Hero, } // Short serde names keep savegames small (the map has 4096 tiles) #[derive(Clone, Copy, Serialize, Deserialize)] pub struct Tile { #[serde(rename = "k")] pub kind: Kind, #[serde(rename = "o")] pub owner: Owner, #[serde(rename = "h")] pub hp: f32, // dig resistance #[serde(rename = "g")] pub gold: i32, // gold left (gold vein) #[serde(rename = "m")] pub marked: bool, // marked for digging #[serde(rename = "r")] pub reserved: u32, // entity id + 1 working on this tile #[serde(rename = "f")] pub food: f32, // mushrooms in the mushroom garden #[serde(rename = "b")] pub bed: u32, // entity id + 1 sleeping here #[serde(rename = "v")] pub var: u8, // texture variant } impl Tile { pub fn new(kind: Kind) -> Tile { Tile { kind, owner: Owner::None, hp: 0.0, gold: 0, marked: false, reserved: 0, food: 0.0, bed: 0, var: 0 } } pub fn solid(&self) -> bool { matches!(self.kind, Kind::Rock | Kind::Earth | Kind::Gold) } pub fn diggable(&self) -> bool { matches!(self.kind, Kind::Earth | Kind::Gold) } pub fn walkable(&self) -> bool { matches!( self.kind, Kind::Floor | Kind::Treasury | Kind::Lair | Kind::Farm | Kind::Training | Kind::Library | Kind::Portal | Kind::Gate ) } } pub fn idx(x: i32, y: i32) -> usize { y as usize * MW + x as usize } pub fn xy(i: usize) -> (i32, i32) { ((i % MW) as i32, (i / MW) as i32) } pub fn inb(x: i32, y: i32) -> bool { x >= 0 && y >= 0 && (x as usize) < MW && (y as usize) < MH } pub const DIR4: [(i32, i32); 4] = [(0, -1), (1, 0), (0, 1), (-1, 0)]; pub const DIR8: [(i32, i32); 8] = [(0, -1), (1, 0), (0, 1), (-1, 0), (1, -1), (1, 1), (-1, 1), (-1, -1)]; #[derive(Serialize, Deserialize)] pub struct Rng(pub u32); impl Rng { pub fn next(&mut self) -> u32 { let mut x = self.0; x ^= x << 13; x ^= x >> 17; x ^= x << 5; self.0 = x; x } pub fn f(&mut self) -> f32 { (self.next() >> 8) as f32 / (1u32 << 24) as f32 } pub fn range(&mut self, a: i32, b: i32) -> i32 { if b <= a { return a; } a + (self.next() % (b - a + 1) as u32) as i32 } } #[derive(Serialize, Deserialize)] pub struct Map { pub t: Vec, pub core: (i32, i32), // top-left corner of the core (3x3) pub portal: (i32, i32), pub gate: (i32, i32), } impl Map { pub fn get(&self, x: i32, y: i32) -> Option<&Tile> { if inb(x, y) { Some(&self.t[idx(x, y)]) } else { None } } pub fn walk(&self, x: i32, y: i32) -> bool { inb(x, y) && self.t[idx(x, y)].walkable() } pub fn generate(seed: u32) -> Map { let mut s = seed; loop { let m = Map::try_generate(s); // make sure heroes can reach the core if m.hero_path_to_core(idx(m.gate.0 + 1, m.gate.1 + 1)).is_some() { return m; } s = s.wrapping_mul(747796405).wrapping_add(2891336453) | 1; } } fn try_generate(seed: u32) -> Map { let mut rng = Rng(seed | 1); for _ in 0..10 { rng.next(); } let mut t = vec![Tile::new(Kind::Earth); N]; for (i, tile) in t.iter_mut().enumerate() { let (x, y) = xy(i); tile.var = (rng.next() % 4) as u8; tile.hp = 6.0; if x < 2 || y < 2 || x >= MW as i32 - 2 || y >= MH as i32 - 2 { tile.kind = Kind::Rock; } } let core = (11, rng.range(26, 35)); let portal = (rng.range(24, 30), if core.1 > 31 { rng.range(12, 18) } else { rng.range(44, 50) }); let gate = (rng.range(51, 55), rng.range(10, 50)); let blob = |t: &mut Vec, rng: &mut Rng, cx: i32, cy: i32, r: i32, k: Kind| { for y in cy - r..=cy + r { for x in cx - r..=cx + r { if !inb(x, y) || x < 2 || y < 2 || x >= MW as i32 - 2 || y >= MH as i32 - 2 { continue; } let d = ((x - cx) * (x - cx) + (y - cy) * (y - cy)) as f32; if d <= (r as f32 + 0.5).powi(2) - rng.f() * r as f32 { let tl = &mut t[idx(x, y)]; tl.kind = k; if k == Kind::Gold { tl.gold = rng.range(250, 550); } } } } }; // rock clusters for _ in 0..22 { let cx = rng.range(4, MW as i32 - 5); let cy = rng.range(4, MH as i32 - 5); let r = rng.range(1, 3); blob(&mut t, &mut rng, cx, cy, r, Kind::Rock); } // gold veins for _ in 0..16 { let cx = rng.range(4, MW as i32 - 5); let cy = rng.range(4, MH as i32 - 5); let r = rng.range(1, 2); blob(&mut t, &mut rng, cx, cy, r, Kind::Gold); } // guaranteed gold near the start for k in 0..3 { let cx = core.0 + rng.range(-3, 9); let cy = core.1 + if k % 2 == 0 { rng.range(-9, -6) } else { rng.range(7, 9) }; blob(&mut t, &mut rng, cx, cy, 1, Kind::Gold); } let room = |t: &mut Vec, x0: i32, y0: i32, x1: i32, y1: i32, owner: Owner| { for y in y0..=y1 { for x in x0..=x1 { if inb(x, y) && x >= 2 && y >= 2 && x < MW as i32 - 2 && y < MH as i32 - 2 { let tl = &mut t[idx(x, y)]; tl.kind = Kind::Floor; tl.owner = owner; tl.gold = 0; } } } }; // small natural caves for _ in 0..4 { let cx = rng.range(20, 48); let cy = rng.range(6, 58); let w = rng.range(1, 2); let h = rng.range(1, 2); room(&mut t, cx - w, cy - h, cx + w, cy + h, Owner::None); } // starting area room(&mut t, core.0 - 3, core.1 - 3, core.0 + 5, core.1 + 5, Owner::Player); // core 3x3 for y in 0..3 { for x in 0..3 { let tl = &mut t[idx(core.0 + x, core.1 + y)]; tl.kind = Kind::Core; tl.owner = Owner::Player; } } // portal cave room(&mut t, portal.0 - 2, portal.1 - 2, portal.0 + 4, portal.1 + 4, Owner::None); for y in 0..3 { for x in 0..3 { t[idx(portal.0 + x, portal.1 + y)].kind = Kind::Portal; } } // hero gate room(&mut t, gate.0 - 2, gate.1 - 2, gate.0 + 4, gate.1 + 4, Owner::Hero); for y in 0..3 { for x in 0..3 { let tl = &mut t[idx(gate.0 + x, gate.1 + y)]; tl.kind = Kind::Gate; tl.owner = Owner::Hero; } } for tl in t.iter_mut() { if tl.kind == Kind::Gold { tl.hp = 6.0; } } Map { t, core, portal, gate } } // Generic Dijkstra search. // cost(i) -> None = impassable, Some(c) = cost of entering. // goal(i) -> true = goal reached. pub fn search(&self, start: usize, cost: C, goal: G, diag: bool) -> Option> where C: Fn(usize) -> Option, G: Fn(usize) -> bool, { let mut dist = vec![u32::MAX; N]; let mut prev = vec![usize::MAX; N]; let mut heap = BinaryHeap::new(); dist[start] = 0; heap.push(Reverse((0u32, start))); while let Some(Reverse((d, i))) = heap.pop() { if d > dist[i] { continue; } if goal(i) { let mut path = Vec::new(); let mut c = i; while c != start { path.push(c); c = prev[c]; } path.reverse(); return Some(path); } let (x, y) = xy(i); let dirs: &[(i32, i32)] = if diag { &DIR8 } else { &DIR4 }; for (k, &(dx, dy)) in dirs.iter().enumerate() { let nx = x + dx; let ny = y + dy; if !inb(nx, ny) { continue; } let ni = idx(nx, ny); let Some(c) = cost(ni) else { continue }; let mut step = c * 10; if k >= 4 { // diagonal only if both neighbours are walkable (no corner cutting) if c > 1 || !self.walk(x + dx, y) || !self.walk(x, y + dy) { continue; } step = 14; } let nd = d + step; if nd < dist[ni] { dist[ni] = nd; prev[ni] = i; heap.push(Reverse((nd, ni))); } } } None } pub fn walk_cost(&self) -> impl Fn(usize) -> Option + '_ { move |i| if self.t[i].walkable() { Some(1) } else { None } } pub fn hero_cost(&self) -> impl Fn(usize) -> Option + '_ { move |i| { let tl = &self.t[i]; match tl.kind { Kind::Earth => Some(7), Kind::Gold => Some(12), Kind::Rock | Kind::Core => None, _ => Some(1), } } } pub fn adj_core(&self, i: usize) -> bool { let (x, y) = xy(i); DIR4.iter().any(|&(dx, dy)| self.get(x + dx, y + dy).map_or(false, |t| t.kind == Kind::Core)) } pub fn hero_path_to_core(&self, start: usize) -> Option> { self.search(start, self.hero_cost(), |i| self.adj_core(i), true) } pub fn path_to(&self, start: usize, target: usize) -> Option> { self.search(start, self.walk_cost(), |i| i == target, true) } }