// Map, tiles, generation and path finding. // // The map size is chosen per game (64..256 tiles per side). Only one map is active at a time; // its dimensions are kept in a thread-local so the many small helpers (idx, xy, inb) stay simple. // Map::generate and loading a savegame (Map::activate) set them. use serde::{Deserialize, Deserializer, Serialize, Serializer}; use std::cell::Cell; use std::cmp::Reverse; use std::collections::BinaryHeap; pub const MIN_SIZE: usize = 64; pub const MAX_SIZE: usize = 256; thread_local! { static DIMS: Cell<(usize, usize)> = Cell::new((64, 64)); } fn set_dims(w: usize, h: usize) { DIMS.with(|d| d.set((w, h))); } #[inline] pub fn mw() -> usize { DIMS.with(|d| d.get().0) } #[inline] pub fn mh() -> usize { DIMS.with(|d| d.get().1) } #[inline] pub fn 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, Torture, Tower, // floor tile occupied by a defence tower (not walkable) Temple, } // order matters for the packed savegame codec: only append new kinds const KINDS: [Kind; 15] = [ Kind::Rock, Kind::Earth, Kind::Gold, Kind::Floor, Kind::Treasury, Kind::Lair, Kind::Farm, Kind::Training, Kind::Library, Kind::Core, Kind::Portal, Kind::Gate, Kind::Torture, Kind::Tower, Kind::Temple, ]; #[derive(Clone, Copy, PartialEq, Eq, Debug, Serialize, Deserialize)] pub enum Owner { None, Player, Hero, } const OWNERS: [Owner; 3] = [Owner::None, Owner::Player, Owner::Hero]; // Short serde names (only used by version 1 savegames, newer ones use the packed codec) #[derive(Clone, Copy, PartialEq, 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 #[serde(rename = "rf", default)] pub reinforced: bool, // wall reinforced by imps } 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, reinforced: false } } 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 | Kind::Torture | Kind::Temple ) } } pub fn idx(x: i32, y: i32) -> usize { y as usize * mw() + x as usize } pub fn xy(i: usize) -> (i32, i32) { let w = mw(); ((i % w) as i32, (i / w) 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 { #[serde(default = "default_size")] pub w: usize, #[serde(default = "default_size")] pub h: usize, #[serde(serialize_with = "tiles_ser", deserialize_with = "tiles_de")] pub t: Vec, pub core: (i32, i32), // top-left corner of the core (3x3) #[serde(default)] pub portals: Vec<(i32, i32)>, // top-left corners of the portals (3x3) #[serde(default, rename = "portal", skip_serializing)] legacy_portal: Option<(i32, i32)>, // version 1 savegames had a single portal pub gate: (i32, i32), #[serde(default)] pub treasures: Vec<(i32, i32)>, // centres of the treasure caves } fn default_size() -> usize { 64 } impl Map { // Make this map's dimensions the active ones (after loading a savegame) pub fn activate(&mut self) { set_dims(self.w, self.h); if self.portals.is_empty() { if let Some(p) = self.legacy_portal.take() { self.portals.push(p); } } } 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() } // Index of the portal containing tile (x, y) pub fn portal_at(&self, x: i32, y: i32) -> Option { self.portals.iter().position(|&(px, py)| x >= px && x < px + 3 && y >= py && y < py + 3) } pub fn portal_owned(&self, k: usize) -> bool { let (px, py) = self.portals[k]; self.t[idx(px, py)].owner == Owner::Player } pub fn generate(seed: u32, w: usize, h: usize) -> Map { let w = w.clamp(MIN_SIZE, MAX_SIZE); let h = h.clamp(MIN_SIZE, MAX_SIZE); set_dims(w, h); let mut s = seed; loop { let m = Map::try_generate(s, w, h); // 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, w: usize, h: usize) -> Map { let (wi, hi) = (w as i32, h as i32); let mut rng = Rng(seed | 1); for _ in 0..10 { rng.next(); } // content scales with the area; one portal per 64x64 tiles let area = (w * h) as f32 / 4096.0; let scale = |n: f32| (n * area).round() as i32; let n_portals = (area.round() as usize).max(1); let n_treasures = n_portals - 1; let mut t = vec![Tile::new(Kind::Earth); w * h]; for (i, tile) in t.iter_mut().enumerate() { let (x, y) = ((i % w) as i32, (i / w) as i32); tile.var = (rng.next() % 4) as u8; tile.hp = 6.0; if x < 2 || y < 2 || x >= wi - 2 || y >= hi - 2 { tile.kind = Kind::Rock; } } let inner = |x: i32, y: i32| x >= 2 && y >= 2 && x < wi - 2 && y < hi - 2; let core = (11, hi / 2 + rng.range(-5, 3)); let gate = (wi - 13 + rng.range(-2, 2), rng.range(10, hi - 14)); // places features far apart: best of several random candidates let mut taken: Vec<(i32, i32)> = vec![(core.0 + 1, core.1 + 1), (gate.0 + 1, gate.1 + 1)]; let place = |rng: &mut Rng, taken: &mut Vec<(i32, i32)>, margin: i32| -> (i32, i32) { let mut best = (margin, margin); let mut bd = -1; for _ in 0..40 { let p = (rng.range(margin, wi - margin - 3), rng.range(margin, hi - margin - 3)); let d = taken.iter().map(|q| (p.0 - q.0).pow(2) + (p.1 - q.1).pow(2)).min().unwrap_or(0); if d > bd { bd = d; best = p; } } taken.push(best); best }; // the first portal is close to the start let first = (core.0 + rng.range(13, 19), (core.1 + if rng.f() < 0.5 { rng.range(-14, -10) } else { rng.range(10, 14) }).clamp(5, hi - 9)); taken.push(first); let mut portals = vec![first]; for _ in 1..n_portals { portals.push(place(&mut rng, &mut taken, 6)); } let mut treasures = Vec::new(); for _ in 0..n_treasures { let p = place(&mut rng, &mut taken, 7); treasures.push((p.0 + 1, p.1 + 1)); } let blob = |t: &mut Vec, rng: &mut Rng, cx: i32, cy: i32, r: i32, k: Kind, gold: (i32, i32)| { for y in cy - r..=cy + r { for x in cx - r..=cx + r { if !inner(x, y) { 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[y as usize * w + x as usize]; tl.kind = k; if k == Kind::Gold { tl.gold = rng.range(gold.0, gold.1); } } } } }; for _ in 0..scale(22.0) { let (cx, cy) = (rng.range(4, wi - 5), rng.range(4, hi - 5)); let r = rng.range(1, 3); blob(&mut t, &mut rng, cx, cy, r, Kind::Rock, (0, 0)); } for _ in 0..scale(16.0) { let (cx, cy) = (rng.range(4, wi - 5), rng.range(4, hi - 5)); let r = rng.range(1, 2); blob(&mut t, &mut rng, cx, cy, r, Kind::Gold, (250, 550)); } // 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, (250, 550)); } 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 inner(x, y) { let tl = &mut t[y as usize * w + x as usize]; tl.kind = Kind::Floor; tl.owner = owner; tl.gold = 0; } } } }; // small natural caves for _ in 0..scale(4.0) { let (cx, cy) = (rng.range(20, wi - 16), rng.range(6, hi - 6)); let (cw, ch) = (rng.range(1, 2), rng.range(1, 2)); room(&mut t, cx - cw, cy - ch, cx + cw, cy + ch, Owner::None); } // treasure caves: a cave surrounded by very rich gold veins (guarded by heroes, see Game::new) for &(cx, cy) in &treasures { blob(&mut t, &mut rng, cx, cy, 4, Kind::Gold, (1000, 1600)); room(&mut t, cx - 2, cy - 2, cx + 2, cy + 2, Owner::None); } // starting area and core room(&mut t, core.0 - 3, core.1 - 3, core.0 + 5, core.1 + 5, Owner::Player); for y in 0..3 { for x in 0..3 { let tl = &mut t[(core.1 + y) as usize * w + (core.0 + x) as usize]; tl.kind = Kind::Core; tl.owner = Owner::Player; } } // portal caves for &(px, py) in &portals { room(&mut t, px - 2, py - 2, px + 4, py + 4, Owner::None); for y in 0..3 { for x in 0..3 { t[(py + y) as usize * w + (px + x) as usize].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[(gate.1 + y) as usize * w + (gate.0 + x) as usize]; tl.kind = Kind::Gate; tl.owner = Owner::Hero; } } for tl in t.iter_mut() { if tl.kind == Kind::Gold { tl.hp = 6.0; } } Map { w, h, t, core, portals, legacy_portal: None, gate, treasures } } // 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; self.t.len()]; let mut prev = vec![usize::MAX; self.t.len()]; 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 if tl.reinforced => Some(70), Kind::Earth => Some(7), Kind::Gold => Some(12), Kind::Rock | Kind::Core | Kind::Tower => 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) } } // ------------------------------------------------------------------ compact tile codec // // Savegame version 2 stores the tiles as one base64 string: the 2-bit texture variants, // then run-length encoded tile records (kind, owner, marked, hp, gold, food, reserved, bed). // A 256x256 map shrinks from ~4.5 MB of JSON to a few hundred KB. const B64: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; fn b64_encode(data: &[u8]) -> String { let mut out = String::with_capacity(data.len() * 4 / 3 + 4); for chunk in data.chunks(3) { let b = [chunk[0], *chunk.get(1).unwrap_or(&0), *chunk.get(2).unwrap_or(&0)]; let v = (b[0] as u32) << 16 | (b[1] as u32) << 8 | b[2] as u32; for k in 0..4 { if k <= chunk.len() { out.push(B64[(v >> (18 - 6 * k) & 63) as usize] as char); } else { out.push('='); } } } out } fn b64_decode(s: &str) -> Option> { let mut val = [255u8; 256]; for (i, &c) in B64.iter().enumerate() { val[c as usize] = i as u8; } let bytes: Vec = s.bytes().filter(|&c| c != b'=').collect(); let mut out = Vec::with_capacity(bytes.len() * 3 / 4); for chunk in bytes.chunks(4) { let mut v = 0u32; for (k, &c) in chunk.iter().enumerate() { let d = val[c as usize]; if d == 255 { return None; } v |= (d as u32) << (18 - 6 * k); } let n = chunk.len() * 6 / 8; for k in 0..n { out.push((v >> (16 - 8 * k)) as u8); } } Some(out) } const REC: usize = 23; fn tile_record(t: &Tile) -> [u8; REC] { let mut r = [0u8; REC]; r[0] = KINDS.iter().position(|&k| k == t.kind).unwrap_or(0) as u8; r[1] = OWNERS.iter().position(|&o| o == t.owner).unwrap_or(0) as u8; r[2] = t.marked as u8 | (t.reinforced as u8) << 1; r[3..7].copy_from_slice(&t.hp.to_le_bytes()); r[7..11].copy_from_slice(&t.gold.to_le_bytes()); r[11..15].copy_from_slice(&t.food.to_le_bytes()); r[15..19].copy_from_slice(&t.reserved.to_le_bytes()); r[19..23].copy_from_slice(&t.bed.to_le_bytes()); r } fn tiles_ser(tiles: &[Tile], s: S) -> Result { let mut out = Vec::new(); out.extend_from_slice(&(tiles.len() as u32).to_le_bytes()); // texture variants, 4 per byte for chunk in tiles.chunks(4) { let mut b = 0u8; for (k, t) in chunk.iter().enumerate() { b |= (t.var & 3) << (k * 2); } out.push(b); } // runs of identical records let mut i = 0; while i < tiles.len() { let rec = tile_record(&tiles[i]); let mut run = 1; while i + run < tiles.len() && run < 65535 && tile_record(&tiles[i + run]) == rec { run += 1; } out.extend_from_slice(&(run as u16).to_le_bytes()); out.extend_from_slice(&rec); i += run; } s.serialize_str(&format!("v2:{}", b64_encode(&out))) } #[derive(Deserialize)] #[serde(untagged)] enum TilesIn { Packed(String), Plain(Vec), // version 1 savegames } fn tiles_de<'de, D: Deserializer<'de>>(d: D) -> Result, D::Error> { use serde::de::Error; match TilesIn::deserialize(d)? { TilesIn::Plain(v) => Ok(v), TilesIn::Packed(s) => { let data = s.strip_prefix("v2:").and_then(b64_decode).ok_or_else(|| D::Error::custom("bad tile data"))?; let rd32 = |o: usize| -> Option<[u8; 4]> { data.get(o..o + 4).map(|s| [s[0], s[1], s[2], s[3]]) }; let count = u32::from_le_bytes(rd32(0).ok_or_else(|| D::Error::custom("bad tile data"))?) as usize; if count > MAX_SIZE * MAX_SIZE { return Err(D::Error::custom("map too large")); } let mut tiles = Vec::with_capacity(count); let mut o = 4 + (count + 3) / 4; while tiles.len() < count { let run = data.get(o..o + 2).map(|s| u16::from_le_bytes([s[0], s[1]])).ok_or_else(|| D::Error::custom("bad tile data"))? as usize; let r = data.get(o + 2..o + 2 + REC).ok_or_else(|| D::Error::custom("bad tile data"))?; let f = |a: usize| [r[a], r[a + 1], r[a + 2], r[a + 3]]; let t = Tile { kind: *KINDS.get(r[0] as usize).unwrap_or(&Kind::Rock), owner: *OWNERS.get(r[1] as usize).unwrap_or(&Owner::None), marked: r[2] & 1 != 0, reinforced: r[2] & 2 != 0, hp: f32::from_le_bytes(f(3)), gold: i32::from_le_bytes(f(7)), food: f32::from_le_bytes(f(11)), reserved: u32::from_le_bytes(f(15)), bed: u32::from_le_bytes(f(19)), var: 0, }; for _ in 0..run.min(count - tiles.len()) { tiles.push(t); } o += 2 + REC; } for (i, t) in tiles.iter_mut().enumerate() { t.var = (data[4 + i / 4] >> ((i % 4) * 2)) & 3; } Ok(tiles) } } }