diff --git a/crates/game-service/src/battle_session.rs b/crates/game-service/src/battle_session.rs index 4b0bccb..e512089 100644 --- a/crates/game-service/src/battle_session.rs +++ b/crates/game-service/src/battle_session.rs @@ -9,6 +9,7 @@ pub fn snapshot_interval_ticks() -> i32 { .unwrap_or(SNAPSHOT_INTERVAL_TICKS) } pub const MAX_CATCH_UP_TICKS: i32 = 40; +pub const BOT_DEPLOY_AHEAD: i32 = 2000; use std::time::Instant; use logic::battle::{ LogicGameMode, LogicGameObjectEntry, LogicVector2, BATTLE_TICKS_PER_SECOND, BATTLE_TYPE_PVP, @@ -107,24 +108,35 @@ impl BattleSession { .objects .objects .iter() - .filter(|entry| entry.owner_index() == crate::bot::BOT_OWNER_INDEX) + .filter(|entry| { + entry.owner_index() == crate::bot::BOT_OWNER_INDEX + && entry.stats().is_building() + && entry.is_alive() + }) .map(|entry| entry.position()) .collect(); let leader = self.mode.battle.leader(crate::bot::BOT_OWNER_INDEX as usize)?; let (king_x, king_y) = leader.position(); - let ahead = towers + let front = towers .iter() - .map(|(_, y)| *y) - .fold(king_y, |best, y| if (y - king_y).abs() > (best - king_y).abs() { y } else { best }); - let lane_x = towers - .iter() - .filter(|(_, y)| *y == ahead) - .map(|(x, _)| *x) - .collect::>(); - let x = lane_x - .get(self.random.next(lane_x.len().max(1) as i32).max(0) as usize) .copied() - .unwrap_or(king_x); + .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() + .unwrap_or((king_x, king_y)); + let ahead = tower_y + (tower_y - king_y).signum() * BOT_DEPLOY_AHEAD; Some((card, LogicVector2::new(x, ahead), self.next_instance())) } pub fn pushes_snapshots(&self) -> bool { diff --git a/crates/game-service/tests/walk_to_tower.rs b/crates/game-service/tests/walk_to_tower.rs new file mode 100644 index 0000000..864abf4 --- /dev/null +++ b/crates/game-service/tests/walk_to_tower.rs @@ -0,0 +1,207 @@ +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:?}" + ); +} diff --git a/crates/logic/src/battle/logic_simulation.rs b/crates/logic/src/battle/logic_simulation.rs index ed4d3fe..2997a1b 100644 --- a/crates/logic/src/battle/logic_simulation.rs +++ b/crates/logic/src/battle/logic_simulation.rs @@ -268,20 +268,24 @@ impl LogicBattle { fn default_target(&self, index: usize) -> Option { let owner = self.objects.objects[index].owner_index(); let from = self.objects.objects[index].position(); - let mut best: Option<(i32, usize)> = None; + let mut building: Option<(i32, usize)> = None; + let mut anything: Option<(i32, usize)> = None; for (other, entry) in self.objects.objects.iter().enumerate() { if other == index || entry.owner_index() == owner || !entry.is_alive() { continue; } - if !entry.stats().is_building() { - continue; - } let distance = distance_squared(from, entry.position()); - if best.map(|(closest, _)| distance < closest).unwrap_or(true) { - best = Some((distance, other)); + let closer = |best: &Option<(i32, usize)>| { + best.map(|(closest, _)| distance < closest).unwrap_or(true) + }; + if entry.stats().is_building() && closer(&building) { + building = Some((distance, other)); + } + if closer(&anything) { + anything = Some((distance, other)); } } - best.map(|(_, other)| other) + building.or(anything).map(|(_, other)| other) } fn next_waypoint( &mut self, @@ -446,14 +450,22 @@ impl LogicBattle { if push.2 == 0 { continue; } + let mut shove = (push.0 / push.2 as i64, push.1 / push.2 as i64); + let allowance = (stats.speed / 2).max(1) as i64; + let travelled = + crate::logic_sqrt(distance_squared((0, 0), (shove.0 as i32, shove.1 as i32))) as i64; + if travelled > allowance { + shove.0 = shove.0 * allowance / travelled; + shove.1 = shove.1 * allowance / travelled; + } let (limit_x, limit_y) = self .tilemap .as_ref() .map(|map| (map.width() * SUBTILE_UNITS - 1, map.height() * SUBTILE_UNITS - 1)) .unwrap_or((i32::MAX, i32::MAX)); let base = self.objects.objects[index].body.base_mut(); - base.position.x = (base.position.x + (push.0 / push.2 as i64) as i32).clamp(0, limit_x); - base.position.y = (base.position.y + (push.1 / push.2 as i64) as i32).clamp(0, limit_y); + base.position.x = (base.position.x + shove.0 as i32).clamp(0, limit_x); + base.position.y = (base.position.y + shove.1 as i32).clamp(0, limit_y); } } fn resolve_attacks(&mut self) {