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); 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() { 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:?}" ); } #[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" ); } }