Let flyers path over water and lava everywhere

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Richard ZinkandClaude Opus 5.5 committed 2026-11-05 14:46:16 +01:00
1 parent 574660e173
commit 1ef2239fbf
7 files changed
+85 -23

No files matched your search

+1 -1
View File
@@ -195,7 +195,7 @@ impl Game {
e.repath = 3.0 + self.rng.f() * 4.0; 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) }; 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 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; e.path = p;
} }
} }
+47 -10
View File
@@ -1692,7 +1692,7 @@ impl Game {
} }
let cur = e.tile(); 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 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 end = p.last().copied().unwrap_or(cur);
let Some(t) = near(end) else { return false }; let Some(t) = near(end) else { return false };
e.path = p; e.path = p;
@@ -3387,10 +3387,10 @@ impl Game {
// ---------------------------------------------------------------- creatures // ---------------------------------------------------------------- creatures
fn find_room_tile(&self, cur: usize, kind: Kind, me: u32, need_food: bool) -> Option<Vec<usize>> { fn find_room_tile(&self, e: &Ent, kind: Kind, me: u32, need_food: bool) -> Option<Vec<usize>> {
self.map.search( self.map.search(
cur, e.tile(),
self.map.walk_cost(), self.map.creature_cost(flies(e.kind)),
|i| { |i| {
let t = &self.map.t[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) 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; return;
} }
if e.hunger > 1.0 && !(self.flag.is_some() && e.hunger < 1.5) { 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); let t = p.last().copied().unwrap_or(cur);
self.map.t[t].reserved = me; self.map.t[t].reserved = me;
e.path = p; e.path = p;
@@ -3939,7 +3939,7 @@ impl Game {
if self.counts[6] > 0 { if self.counts[6] > 0 {
let r2 = self.rng.f(); let r2 = self.rng.f();
if r2 < 0.3 || (e.kind == EK::Priestess && r2 < 0.7) { 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); let t = p.last().copied().unwrap_or(cur);
self.map.t[t].reserved = me; self.map.t[t].reserved = me;
e.path = p; 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 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); let t = p.last().copied().unwrap_or(cur);
self.map.t[t].reserved = me; self.map.t[t].reserved = me;
e.path = p; e.path = p;
@@ -3987,7 +3987,7 @@ impl Game {
if self.forge_orders() && self.counts[ROOM_FORGE] > 0 { if self.forge_orders() && self.counts[ROOM_FORGE] > 0 {
let r = self.rng.f(); let r = self.rng.f();
if (e.kind == EK::Cogling && r < 0.9) || r < 0.25 { 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); let t = p.last().copied().unwrap_or(cur);
self.map.t[t].reserved = me; self.map.t[t].reserved = me;
e.path = p; e.path = p;
@@ -3999,7 +3999,7 @@ impl Game {
} }
let r = self.rng.f(); let r = self.rng.f();
if (e.kind == EK::Mage && r < 0.8) || r < 0.2 { 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); let t = p.last().copied().unwrap_or(cur);
self.map.t[t].reserved = me; self.map.t[t].reserved = me;
e.path = p; e.path = p;
@@ -4009,7 +4009,7 @@ impl Game {
} }
} }
if r < 0.8 && self.gold >= 5.0 { 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); let t = p.last().copied().unwrap_or(cur);
self.map.t[t].reserved = me; self.map.t[t].reserved = me;
e.path = p; e.path = p;
@@ -5935,6 +5935,43 @@ mod tests {
assert!(best <= 2.0); 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] #[test]
fn flyers_cross_rivers_flee_and_heal() { fn flyers_cross_rivers_flee_and_heal() {
let (mut g, rx) = river_game(true); let (mut g, rx) = river_game(true);
+31 -6
View File
@@ -656,7 +656,9 @@ impl Map {
let mut step = c * 10; let mut step = c * 10;
if k >= 4 { if k >= 4 {
// diagonal only if both neighbours are walkable (no corner cutting) // 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; continue;
} }
step = 14; step = 14;
@@ -673,9 +675,22 @@ impl Map {
} }
pub fn walk_cost(&self) -> impl Fn(usize) -> Option<u32> + '_ { pub fn walk_cost(&self) -> impl Fn(usize) -> Option<u32> + '_ {
self.creature_cost(false)
}
// flying creatures also cross water and lava
pub fn fly_cost(&self) -> impl Fn(usize) -> Option<u32> + '_ {
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<u32> + '_ {
move |i| { move |i| {
let t = &self.t[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) { if t.walkable() && !(t.kind == Kind::Door && t.res & 1 == 1) {
Some(1) Some(1)
} else { } else {
@@ -684,9 +699,9 @@ impl Map {
} }
} }
// flying creatures also cross water and lava // path for a walking or flying creature
pub fn fly_cost(&self) -> impl Fn(usize) -> Option<u32> + '_ { pub fn creature_path(&self, fly: bool, start: usize, target: usize) -> Option<Vec<usize>> {
move |i| if self.t[i].walkable() || self.t[i].liquid() { Some(1) } else { None } self.search(start, self.creature_cost(fly), |i| i == target, true)
} }
pub fn fly_path_to(&self, start: usize, target: usize) -> Option<Vec<usize>> { pub fn fly_path_to(&self, start: usize, target: usize) -> Option<Vec<usize>> {
@@ -694,9 +709,15 @@ impl Map {
} }
pub fn hero_cost(&self) -> impl Fn(usize) -> Option<u32> + '_ { pub fn hero_cost(&self) -> impl Fn(usize) -> Option<u32> + '_ {
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<u32> + '_ {
move |i| { move |i| {
let tl = &self.t[i]; let tl = &self.t[i];
match tl.kind { match tl.kind {
Kind::Water | Kind::Lava if fly => Some(1),
Kind::Earth if tl.reinforced => Some(70), Kind::Earth if tl.reinforced => Some(70),
Kind::Steel => Some(300), Kind::Steel => Some(300),
Kind::Earth => Some(7), Kind::Earth => Some(7),
@@ -720,7 +741,11 @@ impl Map {
} }
pub fn hero_path_to_core(&self, start: usize) -> Option<Vec<usize>> { pub fn hero_path_to_core(&self, start: usize) -> Option<Vec<usize>> {
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<Vec<usize>> {
self.search(start, self.hero_cost_as(fly), |i| self.adj_core(i), true)
} }
+1 -1
View File
@@ -97,7 +97,7 @@ impl Game {
let mut n = 0; let mut n = 0;
for (&j, &spot) in cands.iter().zip(spots.iter()).take(GUARDS) { for (&j, &spot) in cands.iter().zip(spots.iter()).take(GUARDS) {
let cur = self.ents[j].tile(); 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; let me = self.ents[j].id + 1;
self.unreserve(me); self.unreserve(me);
self.map.t[spot].reserved = me; self.map.t[spot].reserved = me;
+1 -1
View File
@@ -99,7 +99,7 @@ impl Game {
if d > 1.1 { if d > 1.1 {
if e.path.is_empty() || e.repath <= 0.0 { if e.path.is_empty() || e.repath <= 0.0 {
e.repath = 0.8; 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; e.path = p;
} }
} }
+2 -2
View File
@@ -366,7 +366,7 @@ impl Game {
e.repath = 1.5; e.repath = 1.5;
// attackers dig their way like heroes; the player's creatures only walk // attackers dig their way like heroes; the player's creatures only walk
let goal = |i: usize| self.target_core_adjacent(i, target); 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 { match found {
Some(p) => e.path = p, Some(p) => e.path = p,
None => { None => {
@@ -411,7 +411,7 @@ impl Game {
e.repath = 3.0 + self.rng.f() * 3.0; 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) }; 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 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; e.path = p;
} }
} }
+2 -2
View File
@@ -263,7 +263,7 @@ impl Game {
CRATED => { CRATED => {
if cur != crate_tile { if cur != crate_tile {
if e.path.is_empty() { 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, Some(p) => e.path = p,
None => { None => {
self.traps[k].carrier = 0; self.traps[k].carrier = 0;
@@ -279,7 +279,7 @@ impl Game {
CARRIED => { CARRIED => {
if cur != tile { if cur != tile {
if e.path.is_empty() { 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, Some(p) => e.path = p,
None => { None => {
// no way there: the crate goes back into the forge // no way there: the crate goes back into the forge