summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJustin Worthe <justin@worthe-it.co.za>2019-06-27 21:40:31 +0200
committerJustin Worthe <justin@worthe-it.co.za>2019-06-27 21:40:31 +0200
commita7847a3916a785b5c7dbb1f3f98c77ccb2151b8e (patch)
tree8f5b65d9a80b15fee65bd866afecb12b5f26e0bd
parent81e869d0579a257bd823f9629a1b4ec3b6a297f9 (diff)
Select and move and select and shoot implemented
-rw-r--r--src/game.rs281
-rw-r--r--src/strategy.rs32
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<Point2d<i8>>,
- 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<Command> {
// TODO: Bombs
// TODO: Select and bomb
- unimplemented!("TODO")
+ Vec::new()
}
-
- pub fn sensible_shoot_commands(&self, player_index: usize, center: Point2d<i8>, weapon_range: u8) -> ArrayVec<[Command;8]> {
+
+ pub fn sensible_shoot_commands(
+ &self,
+ player_index: usize,
+ center: Point2d<i8>,
+ 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<Node>, 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<Command> {
// 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<Command> {
+ 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()
}