use crate::geometry::*; use crate::command::{Command, Action}; use crate::json; use crate::constants::*; mod player; use player::*; mod powerup; use powerup::*; mod map; use map::*; use arrayvec::ArrayVec; use fnv::FnvHashSet; // TODO: How much sense does it actually make to split the worms between the players? #[derive(Debug, PartialEq, Eq, Clone)] pub struct GameBoard { pub round: u16, pub max_rounds: u16, pub players: [Player; 2], pub powerups: ArrayVec<[Powerup; 2]>, pub map: Map, pub occupied_cells: FnvHashSet>, pub outcome: SimulationOutcome } #[derive(Debug, PartialEq, Eq, Clone, Copy)] pub enum SimulationOutcome { PlayerWon(usize), Draw, Continue, } impl GameBoard { pub fn new(json: json::State) -> GameBoard { let commando_damage = json.my_player.worms[0].weapon.damage; let commando_range = json.my_player.worms[0].weapon.range; let player = Player { active_worm: json.active_worm_index().unwrap(), moves_score: json.my_player.score - json.my_player.health_score(), select_moves: json.my_player.remaining_worm_selections, worms: json.my_player.worms.iter().map(|w| Worm { id: w.id, health: w.health, position: Point2d::new(w.position.x, w.position.y), weapon_damage: commando_damage, weapon_range: commando_range, bombs: w.banana_bombs.as_ref().map(|b| b.count).unwrap_or(0) }).collect(), }; let opponent = Player { active_worm: 0, moves_score: json.opponents[0].score - json.opponents[0].health_score(), select_moves: json.opponents[0].remaining_worm_selections, worms: json.opponents.iter().flat_map(|o| &o.worms).map(|w| Worm { id: w.id, health: w.health, position: Point2d::new(w.position.x, w.position.y), weapon_damage: commando_damage, weapon_range: commando_range, bombs: if w.health == 100 { 3 } else { 0 } // TODO: parse and check worm type rather, move these out to constants }).collect() }; let mut map = Map::default(); for cell in json.map.iter().flatten() { if cell.cell_type == json::CellType::Dirt { map.set(Point2d::new(cell.x, cell.y)) } } let players = [player, opponent]; let occupied_cells = players.iter() .flat_map(|p| p.worms.iter()) .map(|w| w.position) .collect(); GameBoard { round: json.current_round, max_rounds: json.max_rounds, players, powerups: json.map.iter().flatten().filter_map(|c| { c.powerup.as_ref().map(|p| Powerup { position: Point2d::new(c.x, c.y), value: p.value }) }).collect(), map, occupied_cells, outcome: SimulationOutcome::Continue } } pub fn update(&mut self, json: json::State) { // Much of this becomes easier if this issue is implemented: https://github.com/EntelectChallenge/2019-Worms/issues/44 for w in &json.my_player.worms { if let Some(worm) = self.players[0].find_worm_mut(w.id) { worm.health = w.health; worm.position = Point2d::new(w.position.x, w.position.y); worm.bombs = w.banana_bombs.as_ref().map(|b| b.count).unwrap_or(0); } } for w in json.opponents.iter().flat_map(|o| &o.worms) { if let Some(worm) = self.players[1].find_worm_mut(w.id) { worm.health = w.health; worm.position = Point2d::new(w.position.x, w.position.y); // TODO: How to update opponent worm bombs? } } self.players[0].moves_score = json.my_player.score - json.my_player.health_score(); self.players[1].moves_score = json.opponents[0].score - json.opponents[0].health_score(); self.players[0].select_moves = json.my_player.remaining_worm_selections; self.players[1].select_moves = json.opponents[0].remaining_worm_selections; self.powerups = json.map.iter().flatten().filter_map(|c| { c.powerup.as_ref().map(|p| Powerup { position: Point2d::new(c.x, c.y), value: p.value }) }).collect(); for cell in json.map.iter().flatten() { if cell.cell_type == json::CellType::Air { self.map.clear(Point2d::new(cell.x, cell.y)) } } for player in &mut self.players { player.clear_dead_worms(); player.next_active_worm(); } debug_assert_eq!(json.active_worm_index().unwrap(), self.players[0].active_worm); self.round += 1; debug_assert_eq!(json.current_round, self.round); self.occupied_cells = self.players.iter() .flat_map(|p| p.worms.iter()) .map(|w| w.position) .collect(); } pub fn simulate(&mut self, moves: [Command; 2]) { let actions = [moves[0].action, moves[1].action]; self.simulate_select(moves); self.simulate_moves(actions); self.simulate_digs(actions); self.simulate_bombs(actions); self.simulate_shoots(actions); for player in &mut self.players { player.clear_dead_worms(); player.next_active_worm(); } self.round += 1; self.outcome = match (self.players[0].worms.len(), self.players[1].worms.len(), self.round > self.max_rounds) { (0, 0, _) => SimulationOutcome::Draw, (_, 0, _) => SimulationOutcome::PlayerWon(0), (0, _, _) => SimulationOutcome::PlayerWon(1), (_, _, true) => SimulationOutcome::Draw, _ => SimulationOutcome::Continue }; } fn simulate_select(&mut self, moves: [Command; 2]) { moves.iter().zip(self.players.iter_mut()) .for_each(|(m, player)| { if let Some(worm) = m.worm { debug_assert!(player.select_moves > 0, "Could not make select move, out of select tokens"); player.select_moves = player.select_moves.saturating_sub(1); player.active_worm = player.find_worm_position(worm).unwrap_or(0); } }); } fn simulate_moves(&mut self, actions: [Action; 2]) { match actions { [Action::Move(p1), Action::Move(p2)] if p1.x == p2.x && p1.y == p2.y => { // TODO: Get this from some sort of config rather let damage = 20; debug_assert_eq!(Some(false), self.map.at(Point2d::new(p1.x, p1.y)), "Movement target wasn't empty, ({}, {})", p1.x, p1.y); // Worms have a 50% chance of swapping places // here. I'm treating that as an edge case that I // don't need to handle for now. for player in &mut self.players { if let Some(worm) = player.active_worm_mut() { worm.health = worm.health.saturating_sub(damage); } player.moves_score += 5; } }, _ => { for player_index in 0..actions.len() { if let Action::Move(p) = actions[player_index] { debug_assert_eq!(Some(false), self.map.at(p), "Movement target wasn't empty, ({}, {})", p.x, p.y); self.players[player_index].moves_score += 5; if let Some(worm) = self.players[player_index].active_worm_mut() { debug_assert!( (worm.position.x - p.x).abs() <= 1 && (worm.position.y - p.y).abs() <= 1, "Tried to move too far away, ({}, {})", p.x, p.y ); self.occupied_cells.remove(&worm.position); self.occupied_cells.insert(p); worm.position = p; self.powerups.retain(|power| { if power.position == worm.position { worm.health += power.value; false } else { true } }); } } } } } } fn simulate_digs(&mut self, actions: [Action; 2]) { for player_index in 0..actions.len() { if let Action::Dig(p) = actions[player_index] { debug_assert!( Some(true) == self.map.at(p) || (player_index == 1 && actions[0] == Action::Dig(p)), "Tried to dig through air, ({}, {})", p.x, p.y ); debug_assert!{ (self.players[player_index].active_worm().unwrap().position.x - p.x).abs() <= 1 && (self.players[player_index].active_worm().unwrap().position.y - p.y).abs() <= 1, "Tried to dig too far away, ({}, {})", p.x, p.y }; self.players[player_index].moves_score += 7; self.map.clear(p); } } } fn simulate_bombs(&mut self, actions: [Action; 2]) { // NB: Damage radius has the cell distance rounded UP, throwing range has the cell distance rounded DOWN for player_index in 0..actions.len() { if let Action::Bomb(p) = actions[player_index] { if self.map.at(p).is_some() { if let Some(worm) = self.players[player_index].active_worm_mut() { debug_assert!(worm.bombs > 0, "Worm is throwing a bomb it doesn't have"); debug_assert!((worm.position - p).magnitude_squared() < 6*6); // max range is 5, but it's 5 after rounding down worm.bombs = worm.bombs.saturating_sub(1); // damage as per https://forum.entelect.co.za/uploads/default/original/2X/8/89e6e6cf35791a0448b5a6bbeb63c558ce41804a.jpeg for &(damage_offset, weapon_damage) in BOMB_DAMAGES.iter() { let target = p + damage_offset; if self.map.at(target) == Some(true) { self.map.clear(target); // TODO: How does this score get assigned if both players lobbed a banana? self.players[player_index].moves_score += 7; } let target_own_worm: Option<&mut Worm> = self.players[player_index] .worms.iter_mut() .find(|w| w.position == target); if let Some(target_worm) = target_own_worm { target_worm.health -= weapon_damage; if target_worm.health <= 0 { self.players[player_index].moves_score -= 40; self.occupied_cells.remove(&target_worm.position); } else { self.players[player_index].moves_score -= weapon_damage * 2; } } let target_opponent_worm: Option<&mut Worm> = self.players[GameBoard::opponent(player_index)] .worms.iter_mut() .find(|w| w.position == target); if let Some(target_worm) = target_opponent_worm { target_worm.health -= weapon_damage; if target_worm.health <= 0 { self.players[player_index].moves_score += 40; self.occupied_cells.remove(&target_worm.position); } else { self.players[player_index].moves_score += weapon_damage * 2; } } } } } } } } fn simulate_shoots(&mut self, actions: [Action; 2]) { 'players_loop: for player_index in 0..actions.len() { if let Action::Shoot(dir) = actions[player_index] { if let Some(worm) = self.players[player_index].active_worm() { let (center, weapon_range, weapon_damage) = { (worm.position, worm.weapon_range, worm.weapon_damage) }; let diff = dir.as_vec(); let range = if dir.is_diagonal() { ((f32::from(weapon_range) + 1.) / 2f32.sqrt()).floor() as i8 } else { weapon_range as i8 }; for distance in 1..=range { let target = center + diff * distance; match self.map.at(target) { Some(false) => { let target_own_worm: Option<&mut Worm> = self.players[player_index] .worms.iter_mut() .find(|w| w.position == target); if let Some(target_worm) = target_own_worm { target_worm.health -= weapon_damage; if target_worm.health <= 0 { self.players[player_index].moves_score -= 40; self.occupied_cells.remove(&target_worm.position); } else { self.players[player_index].moves_score -= weapon_damage * 2; } continue 'players_loop; } let target_opponent_worm: Option<&mut Worm> = self.players[GameBoard::opponent(player_index)] .worms.iter_mut() .find(|w| w.position == target); if let Some(target_worm) = target_opponent_worm { target_worm.health -= weapon_damage; if target_worm.health <= 0 { self.players[player_index].moves_score += 40; self.occupied_cells.remove(&target_worm.position); } else { self.players[player_index].moves_score += weapon_damage * 2; } continue 'players_loop; } }, _ => break } } // You get here if the shot missed. Hits are an early return. self.players[player_index].moves_score += 2; } } } } pub fn opponent(player_index: usize) -> usize { (player_index + 1)%2 } pub fn valid_selects(&self, player_index: usize) -> ArrayVec<[i32; 2]> { if self.players[player_index].select_moves > 0 { self.players[player_index].worms .iter() .enumerate() .filter(|(p, _w)| self.players[player_index].active_worm != *p) .map(|(_p, w)| w.id) .collect() } else { ArrayVec::new() } } pub fn valid_move_commands(&self, player_index: usize) -> ArrayVec<[Command;8]> { // TODO: Select and move if let Some(worm) = self.players[player_index].active_worm() { Direction::all() .iter() .map(Direction::as_vec) .map(|d| worm.position + d) .filter(|p| !self.occupied_cells.contains(p)) .filter_map(|p| match self.map.at(p) { Some(false) => Some(Command::new(Action::Move(p))), Some(true) => Some(Command::new(Action::Dig(p))), _ => None, }) .collect() } else { ArrayVec::new() } } pub fn valid_shoot_commands(&self) -> ArrayVec<[Command;24]> { // TODO: Select and shoot Direction::all() .iter() .map(|d| Command::new(Action::Shoot(*d))) .collect() } pub fn valid_bomb_commands(&self, player_index: usize) -> Vec { // TODO: Bombs // TODO: Select and bomb unimplemented!("TODO") } pub fn sensible_shoot_commands(&self, player_index: usize, center: Point2d, weapon_range: u8) -> ArrayVec<[Command;8]> { let range = weapon_range as i8; let dir_range = ((f32::from(weapon_range) + 1.) / 2f32.sqrt()).floor() as i8; self.players[GameBoard::opponent(player_index)].worms .iter() .filter_map(|w| { let diff = w.position - center; if diff.x == 0 && diff.y.abs() <= range { if diff.y > 0 { Some((Direction::South, diff.y)) } else { Some((Direction::North, -diff.y)) } } else if diff.y == 0 && diff.x.abs() <= range { if diff.x > 0 { Some((Direction::East, diff.x)) } else { Some((Direction::West, -diff.x)) } } else if diff.x.abs() == diff.y.abs() && diff.x.abs() <= dir_range { match (diff.x > 0, diff.y > 0) { (true, true) => Some((Direction::SouthEast, diff.x)), (false, true) => Some((Direction::SouthWest, -diff.x)), (true, false) => Some((Direction::NorthEast, diff.x)), (false, false) => Some((Direction::NorthWest, -diff.x)), } } else { None } }) .filter(|(dir, range)| { // TODO if this filtered all players, I don't need to dedup let diff = dir.as_vec(); !(1..*range).any(|distance| self.map.at(center + diff * distance) != Some(false) && !self.players[player_index].worms.iter().any(|w| w.position == center + diff * distance)) }) .map(|(dir, _range)| Command::new(Action::Shoot(dir))) .collect() } } #[cfg(test)] mod test { }