diff --git a/src/dwarves.rs b/src/dwarves.rs index 5c02942..3ad8c5a 100644 --- a/src/dwarves.rs +++ b/src/dwarves.rs @@ -195,7 +195,7 @@ impl Game { e.repath = 3.0 + self.rng.f() * 4.0; let (tx, ty) = if from_home(e.x, e.y) > 3.0 { (hx, hy) } else { (hx + self.rng.f() * 4.0 - 2.0, hy + self.rng.f() * 4.0 - 2.0) }; if self.map.walk(tx as i32, ty as i32) { - if let Some(p) = self.map.path_to(e.tile(), idx(tx as i32, ty as i32)) { + if let Some(p) = self.map.creature_path(flies(e.kind), e.tile(), idx(tx as i32, ty as i32)) { e.path = p; } } diff --git a/src/game.rs b/src/game.rs index 175e507..32393a3 100644 --- a/src/game.rs +++ b/src/game.rs @@ -1692,7 +1692,7 @@ impl Game { } let cur = e.tile(); let near = |i: usize| tasks.iter().copied().find(|&t| i != t && adj4(i, t) || (i == t && self.map.t[t].walkable())); - let Some(p) = self.map.search(cur, self.map.walk_cost(), |i| near(i).is_some(), true) else { return false }; + let Some(p) = self.map.search(cur, self.map.creature_cost(flies(e.kind)), |i| near(i).is_some(), true) else { return false }; let end = p.last().copied().unwrap_or(cur); let Some(t) = near(end) else { return false }; e.path = p; @@ -3387,10 +3387,10 @@ impl Game { // ---------------------------------------------------------------- creatures - fn find_room_tile(&self, cur: usize, kind: Kind, me: u32, need_food: bool) -> Option> { + fn find_room_tile(&self, e: &Ent, kind: Kind, me: u32, need_food: bool) -> Option> { self.map.search( - cur, - self.map.walk_cost(), + e.tile(), + self.map.creature_cost(flies(e.kind)), |i| { let t = &self.map.t[i]; t.kind == kind && t.owner == self.me && (t.reserved == 0 || t.reserved == me) && (!need_food || t.food >= 1.0) @@ -3905,7 +3905,7 @@ impl Game { return; } if e.hunger > 1.0 && !(self.flag.is_some() && e.hunger < 1.5) { - if let Some(p) = self.find_room_tile(cur, Kind::Farm, me, true) { + if let Some(p) = self.find_room_tile(e, Kind::Farm, me, true) { let t = p.last().copied().unwrap_or(cur); self.map.t[t].reserved = me; e.path = p; @@ -3939,7 +3939,7 @@ impl Game { if self.counts[6] > 0 { let r2 = self.rng.f(); if r2 < 0.3 || (e.kind == EK::Priestess && r2 < 0.7) { - if let Some(p) = self.find_room_tile(cur, Kind::Temple, me, false) { + if let Some(p) = self.find_room_tile(e, Kind::Temple, me, false) { let t = p.last().copied().unwrap_or(cur); self.map.t[t].reserved = me; e.path = p; @@ -3974,7 +3974,7 @@ impl Game { } } if self.counts[crate::rooms::ROOM_ARENA] > 0 && e.hp > e.max_hp * 0.8 && self.rng.f() < 0.25 { - if let Some(p) = self.find_room_tile(cur, Kind::Arena, me, false) { + if let Some(p) = self.find_room_tile(e, Kind::Arena, me, false) { let t = p.last().copied().unwrap_or(cur); self.map.t[t].reserved = me; e.path = p; @@ -3987,7 +3987,7 @@ impl Game { if self.forge_orders() && self.counts[ROOM_FORGE] > 0 { let r = self.rng.f(); if (e.kind == EK::Cogling && r < 0.9) || r < 0.25 { - if let Some(p) = self.find_room_tile(cur, Kind::Forge, me, false) { + if let Some(p) = self.find_room_tile(e, Kind::Forge, me, false) { let t = p.last().copied().unwrap_or(cur); self.map.t[t].reserved = me; e.path = p; @@ -3999,7 +3999,7 @@ impl Game { } let r = self.rng.f(); if (e.kind == EK::Mage && r < 0.8) || r < 0.2 { - if let Some(p) = self.find_room_tile(cur, Kind::Library, me, false) { + if let Some(p) = self.find_room_tile(e, Kind::Library, me, false) { let t = p.last().copied().unwrap_or(cur); self.map.t[t].reserved = me; e.path = p; @@ -4009,7 +4009,7 @@ impl Game { } } if r < 0.8 && self.gold >= 5.0 { - if let Some(p) = self.find_room_tile(cur, Kind::Training, me, false) { + if let Some(p) = self.find_room_tile(e, Kind::Training, me, false) { let t = p.last().copied().unwrap_or(cur); self.map.t[t].reserved = me; e.path = p; @@ -5935,6 +5935,43 @@ mod tests { assert!(best <= 2.0); } + #[test] + fn flyers_take_every_way_across_rivers() { + let (mut g, rx) = river_game(false); + let (cx, cy) = g.map.core; + // open floor on the far bank with one training tile of the player + for y in cy - 2..=cy + 2 { + for x in rx + 3..rx + 7 { + let t = &mut g.map.t[idx(x, y)]; + t.kind = Kind::Floor; + t.owner = Owner::None; + } + } + let room = idx(rx + 4, cy); + g.map.t[room].kind = Kind::Training; + g.map.t[room].owner = g.me; + let owl = g.spawn(EK::Owl, false, (cx + 4) as f32 + 0.5, cy as f32 + 0.5, 1); + let kobold = g.spawn(EK::Kobold, false, (cx + 4) as f32 + 0.5, cy as f32 + 0.5, 1); + let owl = g.ents.iter().find(|e| e.id == owl).unwrap().clone(); + let kobold = g.ents.iter().find(|e| e.id == kobold).unwrap().clone(); + // room searches: the owl flies over the river to the training room, the kobold cannot get there + let p = g.find_room_tile(&owl, Kind::Training, owl.id + 1, false).expect("the owl finds the room across the river"); + assert_eq!(p.last().copied(), Some(room)); + assert!(g.find_room_tile(&kobold, Kind::Training, kobold.id + 1, false).map_or(true, |p| p.last() != Some(&room))); + // flyers cut corners over water: a diagonal crossing is as short as on open ground + let (a, b) = (idx(rx - 1, cy - 1), idx(rx + 3, cy + 1)); + let p = g.map.creature_path(true, a, b).expect("flight path"); + assert!(p.len() <= 5, "diagonal flight over the river: {} steps", p.len()); + assert!(p.iter().any(|&i| g.map.t[i].liquid())); + // attacking flyers (the fairy spy) also fly over rivers instead of searching for a bridge + assert!(g.map.hero_cost_as(true)(idx(rx + 1, cy)).is_some()); + assert_eq!(g.map.hero_cost()(idx(rx + 1, cy)), None); + // a locked door stops flyers too + g.map.t[idx(rx + 5, cy)].kind = Kind::Door; + g.map.t[idx(rx + 5, cy)].res = 1; + assert_eq!(g.map.fly_cost()(idx(rx + 5, cy)), None); + } + #[test] fn flyers_cross_rivers_flee_and_heal() { let (mut g, rx) = river_game(true); diff --git a/src/map.rs b/src/map.rs index 62b6566..a4bed9a 100644 --- a/src/map.rs +++ b/src/map.rs @@ -656,7 +656,9 @@ impl Map { 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) { + // (judged with the same cost, so flyers may cut corners over water and lava) + let open = |cx: i32, cy: i32| cost(idx(cx, cy)) == Some(1); + if c > 1 || !open(x + dx, y) || !open(x, y + dy) { continue; } step = 14; @@ -673,9 +675,22 @@ impl Map { } pub fn walk_cost(&self) -> impl Fn(usize) -> Option + '_ { + self.creature_cost(false) + } + + // flying creatures also cross water and lava + pub fn fly_cost(&self) -> impl Fn(usize) -> Option + '_ { + self.creature_cost(true) + } + + // the dungeon's creatures walk on open ground; flyers also cross water and lava + pub fn creature_cost(&self, fly: bool) -> impl Fn(usize) -> Option + '_ { move |i| { let t = &self.t[i]; - // a locked door stops the dungeon's own creatures too + if fly && t.liquid() { + return Some(1); + } + // a locked door stops the dungeon's own creatures too (flyers included) if t.walkable() && !(t.kind == Kind::Door && t.res & 1 == 1) { Some(1) } else { @@ -684,9 +699,9 @@ impl Map { } } - // flying creatures also cross water and lava - pub fn fly_cost(&self) -> impl Fn(usize) -> Option + '_ { - move |i| if self.t[i].walkable() || self.t[i].liquid() { Some(1) } else { None } + // path for a walking or flying creature + pub fn creature_path(&self, fly: bool, start: usize, target: usize) -> Option> { + self.search(start, self.creature_cost(fly), |i| i == target, true) } pub fn fly_path_to(&self, start: usize, target: usize) -> Option> { @@ -694,9 +709,15 @@ impl Map { } pub fn hero_cost(&self) -> impl Fn(usize) -> Option + '_ { + self.hero_cost_as(false) + } + + // attackers dig through earth; flying ones also cross water and lava + pub fn hero_cost_as(&self, fly: bool) -> impl Fn(usize) -> Option + '_ { move |i| { let tl = &self.t[i]; match tl.kind { + Kind::Water | Kind::Lava if fly => Some(1), Kind::Earth if tl.reinforced => Some(70), Kind::Steel => Some(300), Kind::Earth => Some(7), @@ -720,7 +741,11 @@ impl Map { } pub fn hero_path_to_core(&self, start: usize) -> Option> { - self.search(start, self.hero_cost(), |i| self.adj_core(i), true) + self.hero_path_to_core_as(false, start) + } + + pub fn hero_path_to_core_as(&self, fly: bool, start: usize) -> Option> { + self.search(start, self.hero_cost_as(fly), |i| self.adj_core(i), true) } diff --git a/src/orders.rs b/src/orders.rs index 5924742..23d6ccd 100644 --- a/src/orders.rs +++ b/src/orders.rs @@ -97,7 +97,7 @@ impl Game { let mut n = 0; for (&j, &spot) in cands.iter().zip(spots.iter()).take(GUARDS) { let cur = self.ents[j].tile(); - let Some(p) = self.map.path_to(cur, spot) else { continue }; + let Some(p) = self.map.creature_path(flies(self.ents[j].kind), cur, spot) else { continue }; let me = self.ents[j].id + 1; self.unreserve(me); self.map.t[spot].reserved = me; diff --git a/src/personality.rs b/src/personality.rs index 9e6dcc2..bfa2442 100644 --- a/src/personality.rs +++ b/src/personality.rs @@ -99,7 +99,7 @@ impl Game { if d > 1.1 { if e.path.is_empty() || e.repath <= 0.0 { e.repath = 0.8; - if let Some(p) = self.map.path_to(e.tile(), idx(tx as i32, ty as i32)) { + if let Some(p) = self.map.creature_path(flies(e.kind), e.tile(), idx(tx as i32, ty as i32)) { e.path = p; } } diff --git a/src/rival.rs b/src/rival.rs index cedac10..aa72472 100644 --- a/src/rival.rs +++ b/src/rival.rs @@ -366,7 +366,7 @@ impl Game { e.repath = 1.5; // attackers dig their way like heroes; the player's creatures only walk let goal = |i: usize| self.target_core_adjacent(i, target); - let found = if e.side() == 0 { self.map.search(cur, self.map.walk_cost(), goal, true) } else { self.map.search(cur, self.map.hero_cost(), goal, true) }; + let found = if e.side() == 0 { self.map.search(cur, self.map.creature_cost(flies(e.kind)), goal, true) } else { self.map.search(cur, self.map.hero_cost_as(flies(e.kind)), goal, true) }; match found { Some(p) => e.path = p, None => { @@ -411,7 +411,7 @@ impl Game { e.repath = 3.0 + self.rng.f() * 3.0; let (tx, ty) = if d2 > 36.0 { (hx, hy) } else { (hx + self.rng.f() * 8.0 - 4.0, hy + self.rng.f() * 8.0 - 4.0) }; if self.map.walk(tx as i32, ty as i32) { - if let Some(p) = self.map.path_to(e.tile(), idx(tx as i32, ty as i32)) { + if let Some(p) = self.map.creature_path(flies(e.kind), e.tile(), idx(tx as i32, ty as i32)) { e.path = p; } } diff --git a/src/traps.rs b/src/traps.rs index a88750c..a04c9ae 100644 --- a/src/traps.rs +++ b/src/traps.rs @@ -263,7 +263,7 @@ impl Game { CRATED => { if cur != crate_tile { if e.path.is_empty() { - match self.map.path_to(cur, crate_tile) { + match self.map.creature_path(flies(e.kind), cur, crate_tile) { Some(p) => e.path = p, None => { self.traps[k].carrier = 0; @@ -279,7 +279,7 @@ impl Game { CARRIED => { if cur != tile { if e.path.is_empty() { - match self.map.path_to(cur, tile) { + match self.map.creature_path(flies(e.kind), cur, tile) { Some(p) => e.path = p, None => { // no way there: the crate goes back into the forge