deploy the bot in front of its own towers, nothing else

read the client's memory during a live battle and the board told the
story at a glance - a column of the bot's objects down one lane, x fixed
at 3500, y stepping by exactly two thousand:

  23500, 21500, 19500, ... 3500, 1500, -500, -2500

two thousand is BOT_DEPLOY_AHEAD. the landmark was any building the bot
owned, so a hut it had just played became the reference for the next
card, that card became the reference for the one after, and they walked
up the lane and off the top of the map. every one of them had a movie
clip and sat exactly where its sprite said - they were never invisible,
they were marching into nowhere. the huts spawning from out there are
the 177688 addLogicGameObject calls the hook counted.

the landmark is now one of the two tower refs the battle already keeps
in leader_towers, and nothing else can become one.

the harness missed this because the bot had been given the player's
deck, which has no buildings in it. it now plays a cannon and a hut, and
asserts each deploy lands in front of a tower on the bot's own half -
the old code deploys on top of the king tower and fails.
This commit is contained in:
WiseDev 2026-08-23 17:57:02 +03:00
parent 14786cbc92
commit d13e8458ba
2 changed files with 30 additions and 34 deletions

View file

@ -102,38 +102,23 @@ impl BattleSession {
let card = filled let card = filled
.get(self.random.next(filled.len() as i32).max(0) as usize)? .get(self.random.next(filled.len() as i32).max(0) as usize)?
.clone(); .clone();
let towers: Vec<(i32, i32)> = self let leader = self.mode.battle.leader(crate::bot::BOT_OWNER_INDEX as usize)?;
.mode let (king_x, king_y) = leader.position();
.battle let towers: Vec<(i32, i32)> = self.mode.battle.leader_towers
.objects [crate::bot::BOT_OWNER_INDEX as usize]
.objects
.iter() .iter()
.filter(|entry| { .filter_map(|tower| {
entry.owner_index() == crate::bot::BOT_OWNER_INDEX self.mode
&& entry.stats().is_building() .battle
&& entry.is_alive() .objects
.objects
.iter()
.find(|entry| entry.global_id == *tower && entry.is_alive())
}) })
.map(|entry| entry.position()) .map(|entry| entry.position())
.collect(); .collect();
let leader = self.mode.battle.leader(crate::bot::BOT_OWNER_INDEX as usize)?; let (x, tower_y) = towers
let (king_x, king_y) = leader.position(); .get(self.random.next(towers.len().max(1) as i32).max(0) as usize)
let front = towers
.iter()
.copied()
.fold((king_x, king_y), |best, tower| {
if (tower.1 - king_y).abs() > (best.1 - king_y).abs() {
tower
} else {
best
}
});
let lane: Vec<(i32, i32)> = towers
.iter()
.copied()
.filter(|(_, y)| *y == front.1)
.collect();
let (x, tower_y) = lane
.get(self.random.next(lane.len().max(1) as i32).max(0) as usize)
.copied() .copied()
.unwrap_or((king_x, king_y)); .unwrap_or((king_x, king_y));
let ahead = tower_y + (tower_y - king_y).signum() * BOT_DEPLOY_AHEAD; let ahead = tower_y + (tower_y - king_y).signum() * BOT_DEPLOY_AHEAD;

View file

@ -91,7 +91,7 @@ fn the_bot_deploys_in_front_of_its_own_towers() {
} }
let builder = BattleBuilder::new(&root); let builder = BattleBuilder::new(&root);
let mut deck = logic::model::LogicSpellDeck::default(); let mut deck = logic::model::LogicSpellDeck::default();
for (slot, name) in ["Knight", "Archers", "Goblins", "Giant"].iter().enumerate() { for (slot, name) in ["Knight", "Cannon", "GoblinHut", "Giant"].iter().enumerate() {
deck.slots[slot] = Some(logic::model::LogicSpell { deck.slots[slot] = Some(logic::model::LogicSpell {
data: LogicDataRef::spell(name), data: LogicDataRef::spell(name),
..logic::model::LogicSpell::default() ..logic::model::LogicSpell::default()
@ -113,12 +113,26 @@ fn the_bot_deploys_in_front_of_its_own_towers() {
if tick % 20 == 0 { if tick % 20 == 0 {
if let Some((card, at, instance)) = session.bot_play() { if let Some((card, at, instance)) = session.bot_play() {
let entries = builder.summon(&card, at, 1, 0, instance); let entries = builder.summon(&card, at, 1, 0, instance);
let entries_debug = entries.clone();
session.reserve_instances(entries.len()); session.reserve_instances(entries.len());
for entry in entries { for entry in entries {
session.push(entry); session.push(entry);
} }
assert!(
(17000..25000).contains(&at.y) && (at.x == 3500 || at.x == 14500),
"the bot deployed at {:?}, which is not in front of one of its towers",
(at.x, at.y)
);
plays += 1; plays += 1;
if plays <= 6 { println!("bot played at {:?}", (at.x, at.y)); } if plays <= 8 {
println!(
"bot played {:?} at {:?}, {} entries, speeds {:?}",
card,
(at.x, at.y),
entries_debug.len(),
entries_debug.iter().map(|e| e.stats().speed).collect::<Vec<_>>()
);
}
} }
} }
session.advance_to(tick); session.advance_to(tick);
@ -140,13 +154,10 @@ fn the_bot_deploys_in_front_of_its_own_towers() {
break; break;
} }
for entry in session.mode().battle.objects.objects.iter() { for entry in session.mode().battle.objects.objects.iter() {
if entry.stats().speed < 1 {
continue;
}
let (x, y) = entry.position(); let (x, y) = entry.position();
if !((500..=17500).contains(&x) && (1000..=31000).contains(&y)) { if !((500..=17500).contains(&x) && (1000..=31000).contains(&y)) {
println!( println!(
"t={tick}: troop owner={} speed={} alive={} at ({x}, {y})", "t={tick}: object owner={} speed={} alive={} at ({x}, {y})",
entry.owner_index(), entry.owner_index(),
entry.stats().speed, entry.stats().speed,
entry.is_alive() entry.is_alive()