From a7847a3916a785b5c7dbb1f3f98c77ccb2151b8e Mon Sep 17 00:00:00 2001 From: Justin Worthe Date: Thu, 27 Jun 2019 21:40:31 +0200 Subject: Select and move and select and shoot implemented --- src/game.rs | 281 ++++++++++++++++++++++++++++++++++++-------------------- src/strategy.rs | 32 ++----- 2 files changed, 190 insertions(+), 123 deletions(-) diff --git a/src/game.rs b/src/game.rs index 9423e72..5fe22a3 100644 --- a/src/game.rs +++ b/src/game.rs @@ -1,7 +1,7 @@ +use crate::command::{Action, Command}; +use crate::constants::*; use crate::geometry::*; -use crate::command::{Command, Action}; use crate::json; -use crate::constants::*; mod player; use player::*; @@ -24,7 +24,7 @@ pub struct GameBoard { pub powerups: ArrayVec<[Powerup; 2]>, pub map: Map, pub occupied_cells: FnvHashSet>, - pub outcome: SimulationOutcome + pub outcome: SimulationOutcome, } #[derive(Debug, PartialEq, Eq, Clone, Copy)] @@ -43,28 +43,38 @@ impl GameBoard { 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(), + 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() + 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(); @@ -75,30 +85,36 @@ impl GameBoard { } let players = [player, opponent]; - let occupied_cells = players.iter() - .flat_map(|p| p.worms.iter()) - .map(|w| w.position) - .collect(); - + 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 + 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(), + .collect(), map, occupied_cells, - outcome: SimulationOutcome::Continue + 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; @@ -120,12 +136,17 @@ impl GameBoard { 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 + 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(); + .collect(); for cell in json.map.iter().flatten() { if cell.cell_type == json::CellType::Air { @@ -137,12 +158,17 @@ impl GameBoard { player.clear_dead_worms(); player.next_active_worm(); } - debug_assert_eq!(json.active_worm_index().unwrap(), self.players[0].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() + self.occupied_cells = self + .players + .iter() .flat_map(|p| p.worms.iter()) .map(|w| w.position) .collect(); @@ -164,20 +190,29 @@ impl GameBoard { self.round += 1; - self.outcome = match (self.players[0].worms.len(), self.players[1].worms.len(), self.round > self.max_rounds) { + 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 + _ => SimulationOutcome::Continue, }; } fn simulate_select(&mut self, moves: [Command; 2]) { - moves.iter().zip(self.players.iter_mut()) + 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"); + 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); } @@ -190,7 +225,13 @@ impl GameBoard { // 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); + 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. @@ -200,26 +241,34 @@ impl GameBoard { } 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); + 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 + (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; @@ -234,16 +283,18 @@ impl GameBoard { } } } - + 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 + 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!{ + 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 @@ -258,14 +309,14 @@ impl GameBoard { 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 - + 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 @@ -280,9 +331,10 @@ impl GameBoard { } let target_own_worm: Option<&mut Worm> = self.players[player_index] - .worms.iter_mut() + .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 { @@ -293,9 +345,11 @@ impl GameBoard { } } - let target_opponent_worm: Option<&mut Worm> = self.players[GameBoard::opponent(player_index)] - .worms.iter_mut() - .find(|w| w.position == target); + 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 { @@ -310,16 +364,14 @@ impl GameBoard { } } } - } - + 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) - }; + 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() { @@ -333,9 +385,10 @@ impl GameBoard { match self.map.at(target) { Some(false) => { let target_own_worm: Option<&mut Worm> = self.players[player_index] - .worms.iter_mut() + .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 { @@ -347,9 +400,11 @@ impl GameBoard { continue 'players_loop; } - let target_opponent_worm: Option<&mut Worm> = self.players[GameBoard::opponent(player_index)] - .worms.iter_mut() - .find(|w| w.position == target); + 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 { @@ -358,11 +413,11 @@ impl GameBoard { } else { self.players[player_index].moves_score += weapon_damage * 2; } - + continue 'players_loop; } - }, - _ => break + } + _ => break, } } @@ -374,12 +429,13 @@ impl GameBoard { } pub fn opponent(player_index: usize) -> usize { - (player_index + 1)%2 + (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 + self.players[player_index] + .worms .iter() .enumerate() .filter(|(p, _w)| self.players[player_index].active_worm != *p) @@ -390,44 +446,65 @@ impl GameBoard { } } - 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() + pub fn valid_moves_for_worm(&self, worm: &Worm) -> ArrayVec<[Action; 8]> { + 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))), + Some(false) => Some(Action::Move(p)), + Some(true) => Some(Action::Dig(p)), _ => None, }) .collect() - } else { - ArrayVec::new() - } } - pub fn valid_shoot_commands(&self) -> ArrayVec<[Command;24]> { - // TODO: Select and shoot - Direction::all() + pub fn valid_move_commands(&self, player_index: usize) -> ArrayVec<[Command; 24]> { + let active = self.players[player_index].active_worm(); + let no_select = active .iter() - .map(|d| Command::new(Action::Shoot(*d))) + .flat_map(|w| self.valid_moves_for_worm(w)) + .map(Command::new); + + self.valid_selects(player_index).iter() + .flat_map(|select_worm| self.players[player_index].find_worm(*select_worm)) + .flat_map(move |w| self.valid_moves_for_worm(w).into_iter().map(move |a| Command::with_select(w.id, a))) + .chain(no_select) + .collect() + } + + pub fn valid_shoot_commands(&self, player_index: usize) -> ArrayVec<[Command; 24]> { + let all_dirs = Direction::all(); + let no_select = all_dirs + .iter() + .map(|d| Command::new(Action::Shoot(*d))); + + self.valid_selects(player_index).iter().flat_map(|select_worm| { + all_dirs + .iter() + .map(move |d| Command::with_select(*select_worm, Action::Shoot(*d))) + }).chain(no_select) .collect() } pub fn valid_bomb_commands(&self, player_index: usize) -> Vec { // TODO: Bombs // TODO: Select and bomb - unimplemented!("TODO") + Vec::new() } - - pub fn sensible_shoot_commands(&self, player_index: usize, center: Point2d, weapon_range: u8) -> ArrayVec<[Command;8]> { + + 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 + self.players[GameBoard::opponent(player_index)] + .worms .iter() .filter_map(|w| { let diff = w.position - center; @@ -457,9 +534,13 @@ impl GameBoard { .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)) + !(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() @@ -467,6 +548,4 @@ impl GameBoard { } #[cfg(test)] -mod test { - -} +mod test {} diff --git a/src/strategy.rs b/src/strategy.rs index 46cb2cd..502e9f2 100644 --- a/src/strategy.rs +++ b/src/strategy.rs @@ -8,7 +8,6 @@ use time::{Duration, PreciseTime}; use rand; use rand::prelude::*; -use arrayvec::ArrayVec; pub fn choose_move(state: &GameBoard, previous_root: Option, start_time: &PreciseTime, max_time: Duration) -> (Command, Node) { let mut root_node = match previous_root { @@ -244,7 +243,7 @@ fn update(node: &mut Node, commands: [Command; 2], score: Score) { node.score_sum += score; } -fn rollout_moves(state: &GameBoard, player_index: usize) -> ArrayVec<[Command; 8]> { +fn rollout_moves(state: &GameBoard, player_index: usize) -> Vec { // TODO: Have this return one move, chosen randomly? // TODO: Allow new select / bomb moves if let Some(worm) = state.players[player_index].active_worm() { @@ -252,31 +251,20 @@ fn rollout_moves(state: &GameBoard, player_index: usize) -> ArrayVec<[Command; 8 let shoots = state.sensible_shoot_commands(player_index, worm.position, worm.weapon_range); if !shoots.is_empty() { - return shoots; + return shoots.into_iter().collect(); } - state.valid_move_commands(player_index) + state.valid_move_commands(player_index).into_iter().collect() } else { [Command::new(Action::DoNothing)].into_iter().cloned().collect() } } -fn valid_moves(state: &GameBoard, player_index: usize) -> ArrayVec<[Command; 17]> { - // TODO: Move / Dig, Shoot, Bomb, Select to another worm and repeat - // 24 move/digs - // 24 shoots - // 109 bombs (sub those out of range) - // 1 nothing - // TOTAL: 158 possible moves - if let Some(worm) = state.players[player_index].active_worm() { - - state.valid_shoot_commands() - .iter() - .chain(state.valid_move_commands(player_index).iter()) - .chain(state.valid_bomb_commands(player_index).iter()) - .cloned() - .collect() - } else { - [Command::new(Action::DoNothing)].into_iter().cloned().collect() - } +fn valid_moves(state: &GameBoard, player_index: usize) -> Vec { + state.valid_shoot_commands(player_index) + .iter() + .chain(state.valid_move_commands(player_index).iter()) + .chain(state.valid_bomb_commands(player_index).iter()) + .cloned() + .collect() } -- cgit v1.2.3