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.
267 lines
9.1 KiB
Rust
267 lines
9.1 KiB
Rust
use std::path::Path;
|
|
use std::sync::Arc;
|
|
use game_service::battle::BattleBuilder;
|
|
use logic::battle::{LogicVector2, BATTLE_TICKS_PER_SECOND};
|
|
use logic::data::{table, LogicDataRef, LogicDataTables};
|
|
use logic::model::LogicClientAvatar;
|
|
use titan::LogicLong;
|
|
fn assets() -> std::path::PathBuf {
|
|
Path::new(env!("CARGO_MANIFEST_DIR")).join("../../assets")
|
|
}
|
|
fn avatar(low: i32) -> LogicClientAvatar {
|
|
let account = LogicLong::new(0, low);
|
|
LogicClientAvatar {
|
|
avatar_id: account,
|
|
account_id: account,
|
|
home_id: account,
|
|
name_change_state: -1,
|
|
..LogicClientAvatar::default()
|
|
}
|
|
}
|
|
#[test]
|
|
fn a_troop_walks_at_the_enemy_towers_and_stops_in_range() {
|
|
let root = assets();
|
|
let tables = LogicDataTables::load_from_dir(&root).expect("tables");
|
|
LogicDataTables::install(Arc::new(tables));
|
|
let builder = BattleBuilder::new(&root);
|
|
let mut mode = builder
|
|
.build(
|
|
LogicDataRef::by_name(table::LOCATIONS, "PvP_goblin"),
|
|
LogicDataRef::None,
|
|
LogicDataRef::by_name(table::ARENAS, "Arena_T"),
|
|
vec![avatar(5), avatar(0)],
|
|
[None, None],
|
|
1,
|
|
)
|
|
.expect("battle");
|
|
let towers: Vec<(i32, (i32, i32))> = mode
|
|
.battle
|
|
.objects
|
|
.objects
|
|
.iter()
|
|
.map(|entry| (entry.owner_index(), entry.position()))
|
|
.collect();
|
|
println!("towers: {towers:?}");
|
|
let spell = LogicDataRef::spell("Knight");
|
|
let entries = builder.summon(&spell, LogicVector2::new(9000, 20000), 0, 0, 100);
|
|
assert!(!entries.is_empty(), "the knight spell summons nothing");
|
|
let id = entries[0].global_id;
|
|
for entry in entries {
|
|
mode.battle.objects.push(entry);
|
|
}
|
|
let mut last = None;
|
|
for tick in 0..(30 * BATTLE_TICKS_PER_SECOND) {
|
|
mode.battle.tick(tick);
|
|
let here = mode
|
|
.battle
|
|
.objects
|
|
.objects
|
|
.iter()
|
|
.find(|entry| entry.global_id == id)
|
|
.map(|entry| entry.position());
|
|
if here.is_some() {
|
|
last = here;
|
|
}
|
|
if tick % 40 == 0 {
|
|
println!("t={tick:4} pos={here:?}");
|
|
}
|
|
for entry in mode.battle.objects.objects.iter() {
|
|
let (x, y) = entry.position();
|
|
assert!(
|
|
(0..=18000).contains(&x) && (0..=33000).contains(&y),
|
|
"t={tick}: an object left the arena at ({x}, {y})"
|
|
);
|
|
}
|
|
}
|
|
let end = last.expect("the knight never existed");
|
|
println!("last seen at {end:?}");
|
|
let tower = (14500, 25500);
|
|
let reach = ((end.0 - tower.0) as f64).hypot((end.1 - tower.1) as f64);
|
|
println!("distance to the near enemy tower: {reach:.0}");
|
|
assert!(
|
|
reach < 2500.0,
|
|
"the knight never reached the enemy tower, stopped {reach:.0} away at {end:?}"
|
|
);
|
|
}
|
|
#[test]
|
|
fn the_bot_deploys_in_front_of_its_own_towers() {
|
|
let root = assets();
|
|
if let Ok(tables) = LogicDataTables::load_from_dir(&root) {
|
|
LogicDataTables::install(Arc::new(tables));
|
|
}
|
|
let builder = BattleBuilder::new(&root);
|
|
let mut deck = logic::model::LogicSpellDeck::default();
|
|
for (slot, name) in ["Knight", "Cannon", "GoblinHut", "Giant"].iter().enumerate() {
|
|
deck.slots[slot] = Some(logic::model::LogicSpell {
|
|
data: LogicDataRef::spell(name),
|
|
..logic::model::LogicSpell::default()
|
|
});
|
|
}
|
|
let mode = builder
|
|
.build(
|
|
LogicDataRef::by_name(table::LOCATIONS, "PvP_goblin"),
|
|
LogicDataRef::None,
|
|
LogicDataRef::by_name(table::ARENAS, "Arena_T"),
|
|
vec![avatar(5), avatar(0)],
|
|
[Some(deck.clone()), Some(deck)],
|
|
7,
|
|
)
|
|
.expect("battle");
|
|
let mut session = game_service::battle_session::BattleSession::new(mode, Vec::new());
|
|
let mut plays = 0;
|
|
for tick in 0..(180 * BATTLE_TICKS_PER_SECOND) {
|
|
if tick % 20 == 0 {
|
|
if let Some((card, at, instance)) = session.bot_play() {
|
|
let entries = builder.summon(&card, at, 1, 0, instance);
|
|
let entries_debug = entries.clone();
|
|
session.reserve_instances(entries.len());
|
|
for entry in entries {
|
|
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;
|
|
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);
|
|
for (side, leader) in session.mode().battle.leaders.iter().enumerate() {
|
|
let alive = session
|
|
.mode()
|
|
.battle
|
|
.objects
|
|
.objects
|
|
.iter()
|
|
.any(|entry| entry.global_id == *leader);
|
|
assert!(
|
|
alive,
|
|
"t={tick}: side {side}'s leader {leader:?} is no longer in the battle"
|
|
);
|
|
}
|
|
if session.is_finished() || session.towers_standing().0 == 0 {
|
|
println!("battle over at t={tick} after {plays} bot plays");
|
|
break;
|
|
}
|
|
for entry in session.mode().battle.objects.objects.iter() {
|
|
let (x, y) = entry.position();
|
|
if !((500..=17500).contains(&x) && (1000..=31000).contains(&y)) {
|
|
println!(
|
|
"t={tick}: object owner={} speed={} alive={} at ({x}, {y})",
|
|
entry.owner_index(),
|
|
entry.stats().speed,
|
|
entry.is_alive()
|
|
);
|
|
for other in session.mode().battle.objects.objects.iter() {
|
|
println!(
|
|
" owner={} speed={} alive={} at {:?}",
|
|
other.owner_index(),
|
|
other.stats().speed,
|
|
other.is_alive(),
|
|
other.position()
|
|
);
|
|
}
|
|
panic!("a troop walked out of the arena");
|
|
}
|
|
}
|
|
}
|
|
assert!(plays > 0, "the bot never played a card");
|
|
}
|
|
#[test]
|
|
fn a_bot_troop_walks_at_the_player_towers() {
|
|
let root = assets();
|
|
if let Ok(tables) = LogicDataTables::load_from_dir(&root) {
|
|
LogicDataTables::install(Arc::new(tables));
|
|
}
|
|
let builder = BattleBuilder::new(&root);
|
|
let mut mode = builder
|
|
.build(
|
|
LogicDataRef::by_name(table::LOCATIONS, "PvP_goblin"),
|
|
LogicDataRef::None,
|
|
LogicDataRef::by_name(table::ARENAS, "Arena_T"),
|
|
vec![avatar(5), avatar(0)],
|
|
[None, None],
|
|
1,
|
|
)
|
|
.expect("battle");
|
|
let entries = builder.summon(
|
|
&LogicDataRef::spell("Knight"),
|
|
LogicVector2::new(14500, 23500),
|
|
1,
|
|
0,
|
|
200,
|
|
);
|
|
let id = entries[0].global_id;
|
|
for entry in entries {
|
|
mode.battle.objects.push(entry);
|
|
}
|
|
let mut last = None;
|
|
for tick in 0..(20 * BATTLE_TICKS_PER_SECOND) {
|
|
mode.battle.tick(tick);
|
|
let here = mode
|
|
.battle
|
|
.objects
|
|
.objects
|
|
.iter()
|
|
.find(|e| e.global_id == id)
|
|
.map(|e| e.position());
|
|
if here.is_some() {
|
|
last = here;
|
|
}
|
|
if tick % 40 == 0 {
|
|
println!("t={tick:4} bot troop at {here:?}");
|
|
}
|
|
}
|
|
let end = last.expect("the bot troop never existed");
|
|
println!("bot troop finished at {end:?}");
|
|
assert!(
|
|
end.1 < 23500,
|
|
"the bot troop walked away from the player towers to {end:?}"
|
|
);
|
|
}
|
|
#[test]
|
|
fn both_leaders_resolve_to_a_summoner() {
|
|
let root = assets();
|
|
if let Ok(tables) = LogicDataTables::load_from_dir(&root) {
|
|
LogicDataTables::install(Arc::new(tables));
|
|
}
|
|
let builder = BattleBuilder::new(&root);
|
|
let mode = builder
|
|
.build(
|
|
LogicDataRef::by_name(table::LOCATIONS, "PvP_goblin"),
|
|
LogicDataRef::None,
|
|
LogicDataRef::by_name(table::ARENAS, "Arena_T"),
|
|
vec![avatar(5), avatar(0)],
|
|
[None, None],
|
|
1,
|
|
)
|
|
.expect("battle");
|
|
println!("leaders: {:?}", mode.battle.leaders);
|
|
for (side, leader) in mode.battle.leaders.iter().enumerate() {
|
|
assert!(
|
|
!leader.is_none(),
|
|
"side {side} has no leader; LogicBattle::resetSimulatedManaTimers reads both \
|
|
without checking for null and takes the client down with it"
|
|
);
|
|
let found = mode
|
|
.battle
|
|
.objects
|
|
.objects
|
|
.iter()
|
|
.find(|entry| entry.global_id == *leader);
|
|
assert!(
|
|
found.is_some(),
|
|
"side {side}'s leader {leader:?} matches no object in the battle"
|
|
);
|
|
}
|
|
}
|