built a harness first, so the simulation could be run without the client and the fault seen rather than argued about. it reproduces in twenty milliseconds what took a battle to observe. what it showed: a troop on its own walks at the enemy towers and stops at its range, both sides, correctly. put a lane full of them together and the ones behind get squeezed backwards past their own towers and into the edge of the map, where they stand hitting nothing. that is the y=250 and y=31750 frida read out of the client. the collision pass was clamped only by the arena, so a troop in a crowd took a shove every tick with nothing to bound it. it is now limited to half a step, which is the property that matters: a crowd can slow a troop but can never carry it backwards faster than it walks. two smaller ones alongside. bot_play treated every object it owned as a landmark, so once one of its own troops drifted it played the next card on top of it and the one after further out again; only buildings count now. and it deployed exactly on a tower's coordinates, leaving the collision pass to dig the troop out of a building it was born inside - it now stands in front. default_target falls back to any enemy when the buildings on that side are gone. the harness keeps all of it honest: drop the push bound and a troop is out of the arena by tick 140.
207 lines
7 KiB
Rust
207 lines
7 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", "Archers", "Goblins", "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);
|
|
session.reserve_instances(entries.len());
|
|
for entry in entries {
|
|
session.push(entry);
|
|
}
|
|
plays += 1;
|
|
if plays <= 6 { println!("bot played at {:?}", (at.x, at.y)); }
|
|
}
|
|
}
|
|
session.advance_to(tick);
|
|
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() {
|
|
if entry.stats().speed < 1 {
|
|
continue;
|
|
}
|
|
let (x, y) = entry.position();
|
|
if !((500..=17500).contains(&x) && (1000..=31000).contains(&y)) {
|
|
println!(
|
|
"t={tick}: troop 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:?}"
|
|
);
|
|
}
|