summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorJustin Worthe <justin@worthe-it.co.za>2019-08-13 22:00:10 +0200
committerJustin Worthe <justin@worthe-it.co.za>2019-08-13 22:00:10 +0200
commitdabedf442d65fcaec36d0a30ed3ed2327b39a44d (patch)
treea9bbcb4e97dcb98050fd872997ace284f2f764e0 /src
parent33b9c9e05a3693d944342753288fda824f0da13c (diff)
Trimming down on redundant code and tweaking perf
Diffstat (limited to 'src')
-rw-r--r--src/bin/explore-config.rs132
-rw-r--r--src/constants.rs6
-rw-r--r--src/game.rs379
-rw-r--r--src/game/player.rs22
-rw-r--r--src/game/powerup.rs1
-rw-r--r--src/json.rs3
-rw-r--r--src/strategy/mcts.rs230
-rw-r--r--src/strategy/minimax.rs104
8 files changed, 289 insertions, 588 deletions
diff --git a/src/bin/explore-config.rs b/src/bin/explore-config.rs
index 33ff82b..12d9f86 100644
--- a/src/bin/explore-config.rs
+++ b/src/bin/explore-config.rs
@@ -1,5 +1,7 @@
use std::path::Path;
+use std::sync::Mutex;
+use rayon::prelude::*;
use time::PreciseTime;
use steam_powered_wyrm::game;
@@ -13,35 +15,109 @@ fn main() {
);
let depth = 100;
- {
- // TODO: Player 0 seems to be winning here consistently. I
- // probably have a bug. Also, I expected this to give the same
- // result each time and it doesn't.
-
- let start_time = PreciseTime::now();
- let config1 = ScoreConfig::default();
- let config2 = ScoreConfig::default();
- let mut state = initial_state.clone();
-
- while state.outcome == game::SimulationOutcome::Continue {
- let commands = [
- choose_move_with_normalized_perf(&state, &config1, 0, depth),
- choose_move_with_normalized_perf(&state, &config2, 1, depth),
- ];
- state.simulate(commands);
- println!("Commands: {:?}", commands);
+ let configs = ScoreConfigTrials {
+ max_health_weight: vec![0., 1.],
+ total_health_weight: vec![0., 1.],
+ points_weight: vec![0., 1.],
+ victory_weight: vec![3000.],
+ snowball_weight: vec![0., 100.],
+ bomb_weight: vec![0., 100.],
+ explore_exploit_weight: vec![1., 10.],
+ }
+ .reify();
+
+ eprintln!("{} configs being tested", configs.len());
+
+ let victories = Mutex::new(vec![0; configs.len()]);
+
+ for i in 0..configs.len() {
+ eprintln!("Progress: {} of {}", i, configs.len());
+
+ (i + 1..configs.len())
+ .collect::<Vec<usize>>()
+ .par_iter()
+ .for_each(|j| {
+ let start_time = PreciseTime::now();
+ let mut state = initial_state.clone();
+
+ while state.outcome == game::SimulationOutcome::Continue {
+ let commands = [
+ choose_move_with_normalized_perf(&state, &configs[i], 0, depth),
+ choose_move_with_normalized_perf(&state, &configs[*j], 1, depth),
+ ];
+ state.simulate(commands);
+ }
+
+ eprintln!(
+ "Runtime: {}ms",
+ start_time.to(PreciseTime::now()).num_milliseconds()
+ );
+ match state.outcome {
+ game::SimulationOutcome::PlayerWon(0) => victories.lock().unwrap()[i] += 1,
+ game::SimulationOutcome::PlayerWon(1) => victories.lock().unwrap()[*j] += 1,
+ _ => {}
+ };
+ });
+ }
+
+ println!("victories, max_health_weight, total_health_weight, points_weight, victory_weight, snowball_weight, bomb_weight, explore_exploit_weight");
+ victories
+ .lock()
+ .map(|victories| {
+ for (config, victories) in configs.into_iter().zip(victories.iter()) {
+ println!(
+ "{}, {}, {}, {}, {}, {}, {}, {}",
+ victories,
+ config.max_health_weight,
+ config.total_health_weight,
+ config.points_weight,
+ config.victory_weight,
+ config.snowball_weight,
+ config.bomb_weight,
+ config.explore_exploit_weight
+ );
+ }
+ })
+ .unwrap();
+}
+
+pub struct ScoreConfigTrials {
+ pub max_health_weight: Vec<f32>,
+ pub total_health_weight: Vec<f32>,
+ pub points_weight: Vec<f32>,
+ pub victory_weight: Vec<f32>,
+ pub snowball_weight: Vec<f32>,
+ pub bomb_weight: Vec<f32>,
+ pub explore_exploit_weight: Vec<f32>,
+}
+
+impl ScoreConfigTrials {
+ fn reify(self) -> Vec<ScoreConfig> {
+ let mut result = Vec::new();
+ for max_health_weight in &self.max_health_weight {
+ for total_health_weight in &self.total_health_weight {
+ for points_weight in &self.points_weight {
+ for victory_weight in &self.victory_weight {
+ for snowball_weight in &self.snowball_weight {
+ for bomb_weight in &self.bomb_weight {
+ for explore_exploit_weight in &self.explore_exploit_weight {
+ result.push(ScoreConfig {
+ max_health_weight: *max_health_weight,
+ total_health_weight: *total_health_weight,
+ points_weight: *points_weight,
+ victory_weight: *victory_weight,
+ snowball_weight: *snowball_weight,
+ bomb_weight: *bomb_weight,
+ explore_exploit_weight: *explore_exploit_weight,
+ });
+ }
+ }
+ }
+ }
+ }
+ }
}
- println!("{:?}", state.outcome);
- println!(
- "Runtime: {}ms",
- start_time.to(PreciseTime::now()).num_milliseconds()
- );
- println!(
- "Health: {} - {}",
- state.players[0].health(),
- state.players[1].health()
- );
- println!("Round: {}", state.round);
+ result
}
}
diff --git a/src/constants.rs b/src/constants.rs
index 1c2b8a1..e2db8fb 100644
--- a/src/constants.rs
+++ b/src/constants.rs
@@ -156,6 +156,12 @@ impl MapRow {
}
}
+pub const HEALTH_PACK_VALUE: i32 = 10;
+
+pub const SHOOT_RANGE: i8 = 4;
+pub const SHOOT_RANGE_DIAGONAL: i8 = 3;
+pub const SHOOT_DAMAGE: i32 = 8;
+
pub const BOMB_RANGE: i8 = 5;
pub const BOMB_DAMAGED_SPACES: usize = 13;
pub const BOMB_DAMAGES: [(Vec2d<i8>, i32); BOMB_DAMAGED_SPACES] = [
diff --git a/src/game.rs b/src/game.rs
index 4e2f7a9..11108e3 100644
--- a/src/game.rs
+++ b/src/game.rs
@@ -34,9 +34,6 @@ pub enum SimulationOutcome {
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(),
@@ -49,8 +46,6 @@ impl GameBoard {
id: w.id,
health: w.health,
position: Point2d::new(w.position.x, w.position.y),
- weapon_damage: commando_damage,
- weapon_range: commando_range,
rounds_until_unfrozen: w.rounds_until_unfrozen,
bombs: w.banana_bombs.as_ref().map(|b| b.count).unwrap_or(0),
snowballs: w.snowballs.as_ref().map(|b| b.count).unwrap_or(0),
@@ -70,8 +65,6 @@ impl GameBoard {
id: w.id,
health: w.health,
position: Point2d::new(w.position.x, w.position.y),
- weapon_damage: commando_damage,
- weapon_range: commando_range,
rounds_until_unfrozen: w.rounds_until_unfrozen,
bombs: if w.profession == json::WormType::Agent {
STARTING_BOMBS
@@ -110,9 +103,8 @@ impl GameBoard {
.iter()
.flatten()
.filter_map(|c| {
- c.powerup.as_ref().map(|p| Powerup {
+ c.powerup.as_ref().map(|_p| Powerup {
position: Point2d::new(c.x, c.y),
- value: p.value,
})
})
.collect(),
@@ -138,25 +130,24 @@ impl GameBoard {
}
}
- // TODO: Good enough for now, but what if these worms are
- // frozen? Then it looks like the move was a banana, but it
- // actually does nothing.
- if json
- .opponents
- .iter()
- .any(|o| o.previous_command.starts_with("banana"))
- {
- for worm in &mut self.players[1].worms {
- worm.bombs = worm.bombs.saturating_sub(1);
+ if !self.players[1].active_worm_is_frozen() {
+ if json
+ .opponents
+ .iter()
+ .any(|o| o.previous_command.starts_with("banana"))
+ {
+ for worm in &mut self.players[1].worms {
+ worm.bombs = worm.bombs.saturating_sub(1);
+ }
}
- }
- if json
- .opponents
- .iter()
- .any(|o| o.previous_command.starts_with("snowball"))
- {
- for worm in &mut self.players[1].worms {
- worm.snowballs = worm.snowballs.saturating_sub(1);
+ if json
+ .opponents
+ .iter()
+ .any(|o| o.previous_command.starts_with("snowball"))
+ {
+ for worm in &mut self.players[1].worms {
+ worm.snowballs = worm.snowballs.saturating_sub(1);
+ }
}
}
@@ -171,9 +162,8 @@ impl GameBoard {
.iter()
.flatten()
.filter_map(|c| {
- c.powerup.as_ref().map(|p| Powerup {
+ c.powerup.as_ref().map(|_p| Powerup {
position: Point2d::new(c.x, c.y),
- value: p.value,
})
})
.collect();
@@ -251,7 +241,8 @@ impl GameBoard {
// TODO: Question order of actions on the forums: https://forum.entelect.co.za/t/possible-bug-in-order-of-moves/822
self.simulate_snowballs(actions);
// This check needs to happen again because the worm may have
- // been frozen on the previous command.
+ // been frozen on the previous command. This will probably be
+ // removed after the bug is fixed.
let actions = self.identify_actions(moves);
self.simulate_shoots(actions);
@@ -372,7 +363,7 @@ impl GameBoard {
self.powerups.retain(|power| {
if power.position == worm.position {
- worm.health += power.value;
+ worm.health += HEALTH_PACK_VALUE;
false
} else {
true
@@ -511,14 +502,13 @@ impl GameBoard {
'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 center = worm.position;
let diff = dir.as_vec();
let range = if dir.is_diagonal() {
- ((f32::from(weapon_range) + 1.) / 2f32.sqrt()).floor() as i8
+ SHOOT_RANGE_DIAGONAL
} else {
- weapon_range as i8
+ SHOOT_RANGE
};
for distance in 1..=range {
@@ -531,9 +521,9 @@ impl GameBoard {
.find(|w| w.position == target);
if let Some(target_worm) = target_own_worm {
- target_worm.health -= weapon_damage;
+ target_worm.health -= SHOOT_DAMAGE;
self.players[player_index].moves_score -=
- weapon_damage * ATTACK_SCORE_MULTIPLIER;
+ SHOOT_DAMAGE * ATTACK_SCORE_MULTIPLIER;
if target_worm.health <= 0 {
self.players[player_index].moves_score -= KILL_SCORE;
}
@@ -547,9 +537,9 @@ impl GameBoard {
.find(|w| w.position == target);
if let Some(target_worm) = target_opponent_worm {
- target_worm.health -= weapon_damage;
+ target_worm.health -= SHOOT_DAMAGE;
self.players[player_index].moves_score +=
- weapon_damage * ATTACK_SCORE_MULTIPLIER;
+ SHOOT_DAMAGE * ATTACK_SCORE_MULTIPLIER;
if target_worm.health <= 0 {
self.players[player_index].moves_score += KILL_SCORE;
}
@@ -585,217 +575,164 @@ impl GameBoard {
(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_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(Action::Move(p)),
- Some(true) => Some(Action::Dig(p)),
- _ => None,
- })
- .collect()
- }
+ fn selects_iter(&self, player_index: usize) -> impl Iterator<Item = (Option<i32>, &Worm)> {
+ let no_select = self.players[player_index]
+ .active_worm()
+ .into_iter()
+ .map(|w| (None, w));
- pub fn valid_move_commands(&self, player_index: usize) -> ArrayVec<[Command; 24]> {
- let active = self.players[player_index].active_worm();
- let no_select = active
+ let has_select_moves = self.players[player_index].select_moves > 0;
+ let active_worm_index = self.players[player_index].active_worm;
+ let selects = self.players[player_index]
+ .worms
.iter()
- .flat_map(|w| self.valid_moves_for_worm(w))
- .map(Command::new);
+ .enumerate()
+ .filter(move |(p, _w)| has_select_moves && active_worm_index != *p)
+ .map(|(_p, w)| (Some(w.id), w));
- 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()
+ no_select.chain(selects)
}
- 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
+ fn pruned_valid_move_commands(&self, player_index: usize) -> ArrayVec<[Command; 8]> {
+ self.players[player_index]
+ .active_worm()
+ .into_iter()
+ .flat_map(|worm| {
+ // TODO: If you aren't on lava, don't step onto the lava
+ Direction::ALL
.iter()
- .map(move |d| Command::with_select(*select_worm, Action::Shoot(*d)))
+ .map(Direction::as_vec)
+ .map(move |d| worm.position + d)
+ .filter(|p| !self.occupied_cells.contains(p))
+ .filter_map(|p| match self.map.at(p) {
+ Some(false) => Some(Action::Move(p)),
+ Some(true) => Some(Action::Dig(p)),
+ _ => None,
+ })
+ .map(Command::new)
})
- .chain(no_select)
.collect()
}
- pub fn valid_bomb_commands(&self, player_index: usize) -> Vec<Command> {
- let agent_worm = self.players[player_index]
- .worms
- .iter()
- .enumerate()
- .find(|(_p, w)| w.bombs > 0);
- match agent_worm {
- Some((worm_i, worm)) => {
- let select = if worm_i == self.players[player_index].active_worm {
- None
- } else {
- Some(worm.id)
- };
-
- if select.is_none() || self.players[player_index].select_moves > 0 {
- let mut result = Vec::with_capacity((BOMB_RANGE * 2 + 1).pow(2) as usize - 12);
-
- for y in worm.position.y - BOMB_RANGE..=worm.position.y + BOMB_RANGE {
- for x in worm.position.x - BOMB_RANGE..=worm.position.x + BOMB_RANGE {
- let target = Point2d::new(x, y);
- if self.map.at(target).is_some()
- && (worm.position - target).magnitude_squared()
- < (BOMB_RANGE + 1).pow(2)
- {
- result.push(Command {
- worm: select,
- action: Action::Bomb(target),
- });
- }
+ fn pruned_valid_bomb_commands(&self, player_index: usize) -> Vec<Command> {
+ self.selects_iter(player_index)
+ .filter(|(_, worm)| worm.bombs > 0)
+ .flat_map(|(select, worm)| {
+ let mut result = Vec::with_capacity((BOMB_RANGE * 2 + 1).pow(2) as usize - 12);
+
+ // TODO: Don't push the ones where you're hurt by the bomb
+ // TODO: Only push the ones that hurt an opponent worm
+ for y in worm.position.y - BOMB_RANGE..=worm.position.y + BOMB_RANGE {
+ for x in worm.position.x - BOMB_RANGE..=worm.position.x + BOMB_RANGE {
+ let target = Point2d::new(x, y);
+ if self.map.at(target).is_some()
+ && (worm.position - target).magnitude_squared()
+ < (BOMB_RANGE + 1).pow(2)
+ {
+ result.push(Command {
+ worm: select,
+ action: Action::Bomb(target),
+ });
}
}
-
- result
- } else {
- Vec::new()
}
- }
- None => Vec::new(),
- }
- }
-
- pub fn valid_snowball_commands(&self, player_index: usize) -> Vec<Command> {
- let tech_worm = self.players[player_index]
- .worms
- .iter()
- .enumerate()
- .find(|(_p, w)| w.snowballs > 0);
- match tech_worm {
- Some((worm_i, worm)) => {
- let select = if worm_i == self.players[player_index].active_worm {
- None
- } else {
- Some(worm.id)
- };
- if select.is_none() || self.players[player_index].select_moves > 0 {
- let mut result =
- Vec::with_capacity((SNOWBALL_RANGE * 2 + 1).pow(2) as usize - 12); // -12 is from 3 on each corner
+ result
+ })
+ .collect()
+ }
- for y in worm.position.y - SNOWBALL_RANGE..=worm.position.y + SNOWBALL_RANGE {
- for x in worm.position.x - SNOWBALL_RANGE..=worm.position.x + SNOWBALL_RANGE
+ fn pruned_valid_snowball_commands(&self, player_index: usize) -> Vec<Command> {
+ self.selects_iter(player_index)
+ .filter(|(_, worm)| worm.snowballs > 0)
+ .flat_map(|(select, worm)| {
+ let mut result = Vec::with_capacity((SNOWBALL_RANGE * 2 + 1).pow(2) as usize - 12);
+
+ // TODO: Don't push the ones where you're freezing yourself
+ // TODO: Only push the ones that freeze an opponent worm
+ for y in worm.position.y - SNOWBALL_RANGE..=worm.position.y + SNOWBALL_RANGE {
+ for x in worm.position.x - SNOWBALL_RANGE..=worm.position.x + SNOWBALL_RANGE {
+ let target = Point2d::new(x, y);
+ if self.map.at(target).is_some()
+ && (worm.position - target).magnitude_squared()
+ < (SNOWBALL_RANGE + 1).pow(2)
{
- let target = Point2d::new(x, y);
- if self.map.at(target).is_some()
- && (worm.position - target).magnitude_squared()
- < (SNOWBALL_RANGE + 1).pow(2)
- {
- result.push(Command {
- worm: select,
- action: Action::Snowball(target),
- });
- }
+ result.push(Command {
+ worm: select,
+ action: Action::Snowball(target),
+ });
}
}
-
- result
- } else {
- Vec::new()
}
- }
- None => Vec::new(),
- }
- }
- // TODO: encorporate this for earlier filtering
- 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
- .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
- }
+ result
})
- .filter(|(dir, range)| {
- let diff = dir.as_vec();
- // NB: This is up to range EXCLUSIVE, so if there's
- // anything in the way, even another good shot, skip.
- !(1..*range).any(|distance| {
- self.map.at(center + diff * distance) != Some(false)
- && !self
- .players
- .iter()
- .flat_map(|p| p.worms.iter())
- .any(|w| w.position == center + diff * distance)
- })
+ .collect()
+ }
+
+ fn pruned_valid_shoot_commands(&self, player_index: usize) -> Vec<Command> {
+ self.selects_iter(player_index)
+ .flat_map(|(select, worm)| {
+ self.players[GameBoard::opponent(player_index)]
+ .worms
+ .iter()
+ .filter_map(move |w| {
+ let diff = w.position - worm.position;
+ if diff.x == 0 && diff.y.abs() <= SHOOT_RANGE {
+ if diff.y > 0 {
+ Some((Direction::South, diff.y))
+ } else {
+ Some((Direction::North, -diff.y))
+ }
+ } else if diff.y == 0 && diff.x.abs() <= SHOOT_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() <= SHOOT_RANGE_DIAGONAL
+ {
+ 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(move |(dir, range)| {
+ let diff = dir.as_vec();
+ // NB: This is up to range EXCLUSIVE, so if there's
+ // anything in the way, even another good shot, skip.
+ !(1..*range).any(|distance| {
+ self.map.at(worm.position + diff * distance) != Some(false)
+ && !self
+ .players
+ .iter()
+ .flat_map(|p| p.worms.iter())
+ .any(|w| w.position == worm.position + diff * distance)
+ })
+ })
+ .map(move |(dir, _range)| Command {
+ worm: select,
+ action: Action::Shoot(dir),
+ })
})
- .map(|(dir, _range)| Command::new(Action::Shoot(dir)))
.collect()
}
- // TODO: If all of this can be iterators, I can pass in the final filter to this function and only collect once.
- pub fn valid_moves(&self, player_index: usize) -> Vec<Command> {
+ pub fn pruned_valid_moves(&self, player_index: usize) -> Vec<Command> {
if self.players[player_index].active_worm_is_frozen_after_tick() {
vec![Command::new(Action::DoNothing)]
} else {
- self.valid_shoot_commands(player_index)
+ self.pruned_valid_shoot_commands(player_index)
.iter()
- .chain(self.valid_move_commands(player_index).iter())
- .chain(self.valid_bomb_commands(player_index).iter())
- .chain(self.valid_snowball_commands(player_index).iter())
+ .chain(self.pruned_valid_move_commands(player_index).iter())
+ .chain(self.pruned_valid_bomb_commands(player_index).iter())
+ .chain(self.pruned_valid_snowball_commands(player_index).iter())
.chain([Command::new(Action::DoNothing)].iter())
.cloned()
.collect()
diff --git a/src/game/player.rs b/src/game/player.rs
index 8b63f9f..3d86c6d 100644
--- a/src/game/player.rs
+++ b/src/game/player.rs
@@ -14,8 +14,6 @@ pub struct Worm {
pub id: i32,
pub health: i32,
pub position: Point2d<i8>,
- pub weapon_damage: i32,
- pub weapon_range: u8,
pub bombs: u8,
pub snowballs: u8,
pub rounds_until_unfrozen: u8,
@@ -113,8 +111,6 @@ mod test {
id: 1,
health: 50,
position: Point2d::new(0, 0),
- weapon_damage: 5,
- weapon_range: 5,
rounds_until_unfrozen: 0,
bombs: 0,
snowballs: 0,
@@ -123,8 +119,6 @@ mod test {
id: 2,
health: 10,
position: Point2d::new(0, 0),
- weapon_damage: 5,
- weapon_range: 5,
rounds_until_unfrozen: 0,
bombs: 0,
snowballs: 0,
@@ -133,8 +127,6 @@ mod test {
id: 3,
health: -2,
position: Point2d::new(0, 0),
- weapon_damage: 5,
- weapon_range: 5,
rounds_until_unfrozen: 0,
bombs: 0,
snowballs: 0,
@@ -162,8 +154,6 @@ mod test {
id: 1,
health: 0,
position: Point2d::new(0, 0),
- weapon_damage: 5,
- weapon_range: 5,
rounds_until_unfrozen: 0,
bombs: 0,
snowballs: 0,
@@ -172,8 +162,6 @@ mod test {
id: 2,
health: 10,
position: Point2d::new(0, 0),
- weapon_damage: 5,
- weapon_range: 5,
rounds_until_unfrozen: 0,
bombs: 0,
snowballs: 0,
@@ -182,8 +170,6 @@ mod test {
id: 3,
health: 2,
position: Point2d::new(0, 0),
- weapon_damage: 5,
- weapon_range: 5,
rounds_until_unfrozen: 0,
bombs: 0,
snowballs: 0,
@@ -211,8 +197,6 @@ mod test {
id: 1,
health: 0,
position: Point2d::new(0, 0),
- weapon_damage: 5,
- weapon_range: 5,
rounds_until_unfrozen: 0,
bombs: 0,
snowballs: 0,
@@ -221,8 +205,6 @@ mod test {
id: 2,
health: 10,
position: Point2d::new(0, 0),
- weapon_damage: 5,
- weapon_range: 5,
rounds_until_unfrozen: 0,
bombs: 0,
snowballs: 0,
@@ -231,8 +213,6 @@ mod test {
id: 3,
health: 2,
position: Point2d::new(0, 0),
- weapon_damage: 5,
- weapon_range: 5,
rounds_until_unfrozen: 0,
bombs: 0,
snowballs: 0,
@@ -260,8 +240,6 @@ mod test {
id: 1,
health: -10,
position: Point2d::new(0, 0),
- weapon_damage: 5,
- weapon_range: 5,
rounds_until_unfrozen: 0,
bombs: 0,
snowballs: 0,
diff --git a/src/game/powerup.rs b/src/game/powerup.rs
index 2f07816..f8a8e2f 100644
--- a/src/game/powerup.rs
+++ b/src/game/powerup.rs
@@ -3,5 +3,4 @@ use crate::geometry::*;
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub struct Powerup {
pub position: Point2d<i8>,
- pub value: i32
}
diff --git a/src/json.rs b/src/json.rs
index 66864cf..fc6ba82 100644
--- a/src/json.rs
+++ b/src/json.rs
@@ -15,6 +15,9 @@ pub fn read_state_from_json_file(filename: &Path) -> Result<State, Box<Error>> {
Ok(state)
}
+// TODO: Narrow numeric types
+// TODO: comment out stuff I don't want / need
+
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct State {
diff --git a/src/strategy/mcts.rs b/src/strategy/mcts.rs
deleted file mode 100644
index c122fa1..0000000
--- a/src/strategy/mcts.rs
+++ /dev/null
@@ -1,230 +0,0 @@
-use crate::command::{Action, Command};
-use crate::game::{GameBoard, SimulationOutcome};
-
-use std::cmp;
-use std::collections::HashMap;
-use std::ops::*;
-use time::{Duration, PreciseTime};
-
-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 {
- None => Node {
- state: state.clone(),
- score_sum: ScoreSum::new(),
- player_score_sums: [HashMap::new(), HashMap::new()],
- unexplored: mcts_move_combo(state),
- children: HashMap::new(),
- },
- Some(mut node) => node
- .children
- .drain()
- .map(|(_k, n)| n)
- .find(|n| n.state == *state)
- .unwrap_or_else(|| {
- eprintln!("Previous round did not appear in the cache");
- Node {
- state: state.clone(),
- score_sum: ScoreSum::new(),
- player_score_sums: [HashMap::new(), HashMap::new()],
- unexplored: mcts_move_combo(state),
- children: HashMap::new(),
- }
- }),
- };
-
- while start_time.to(PreciseTime::now()) < max_time {
- let _ = mcts(&mut root_node);
- }
-
- eprintln!("Number of simulations: {}", root_node.score_sum.visit_count);
- for (command, score_sum) in &root_node.player_score_sums[0] {
- eprintln!(
- "{} = {} ({} visits)",
- command,
- score_sum.avg().val,
- score_sum.visit_count
- );
- }
-
- let chosen_command = best_player_move(&root_node);
-
- root_node
- .children
- .retain(|[c1, _], _| *c1 == chosen_command);
-
- (chosen_command, root_node)
-}
-
-pub struct Node {
- state: GameBoard,
- score_sum: ScoreSum,
- player_score_sums: [HashMap<Command, ScoreSum>; 2],
- unexplored: Vec<[Command; 2]>,
- children: HashMap<[Command; 2], Node>,
-}
-
-#[derive(Clone, Copy, Debug, PartialEq, PartialOrd)]
-struct Score {
- val: f32,
-}
-
-impl AddAssign for Score {
- fn add_assign(&mut self, other: Self) {
- self.val = self.val + other.val;
- }
-}
-
-impl Div<u32> for Score {
- type Output = Self;
- fn div(self, other: u32) -> Self {
- Score {
- val: self.val / other as f32,
- }
- }
-}
-
-impl cmp::Eq for Score {}
-impl cmp::Ord for Score {
- fn cmp(&self, other: &Score) -> cmp::Ordering {
- self.val
- .partial_cmp(&other.val)
- .unwrap_or(cmp::Ordering::Equal)
- }
-}
-
-struct ScoreSum {
- sum: Score,
- visit_count: u32,
-}
-
-impl ScoreSum {
- fn new() -> ScoreSum {
- ScoreSum {
- sum: Score { val: 0. },
- visit_count: 0,
- }
- }
- fn with_initial(score: Score) -> ScoreSum {
- ScoreSum {
- sum: score,
- visit_count: 1,
- }
- }
- fn avg(&self) -> Score {
- self.sum / self.visit_count
- }
-}
-
-impl AddAssign<Score> for ScoreSum {
- fn add_assign(&mut self, other: Score) {
- self.sum += other;
- self.visit_count = self.visit_count.saturating_add(1);
- }
-}
-
-fn mcts(node: &mut Node) -> Score {
- if node.state.outcome != SimulationOutcome::Continue {
- score(&node.state)
- } else if let Some(commands) = node.unexplored.pop() {
- let mut new_state = node.state.clone();
- new_state.simulate(commands);
- let score = rollout(&new_state);
- let unexplored = if new_state.outcome == SimulationOutcome::Continue {
- mcts_move_combo(&new_state)
- } else {
- Vec::new()
- };
-
- let new_node = Node {
- state: new_state,
- score_sum: ScoreSum::with_initial(score),
- player_score_sums: [HashMap::new(), HashMap::new()],
- unexplored,
- children: HashMap::new(),
- };
- node.children.insert(commands, new_node);
-
- update(node, commands, score);
- score
- } else {
- let commands = choose_existing(node);
- let score = mcts(
- node.children
- .get_mut(&commands)
- .expect("The existing node hasn't been tried yet"),
- );
- update(node, commands, score);
- score
- }
-}
-
-fn mcts_move_combo(state: &GameBoard) -> Vec<[Command; 2]> {
- let player_moves = state.valid_moves(0);
- let opponent_moves = state.valid_moves(1);
- debug_assert!(!player_moves.is_empty(), "No player moves");
- debug_assert!(!opponent_moves.is_empty(), "No opponent moves");
-
- let mut result = Vec::with_capacity(player_moves.len() * opponent_moves.len());
- for p in &player_moves {
- for o in &opponent_moves {
- result.push([*p, *o]);
- }
- }
-
- result
-}
-
-fn best_player_move(node: &Node) -> Command {
- node.player_score_sums[0]
- .iter()
- .max_by_key(|(_command, score_sum)| score_sum.avg())
- .map(|(command, _score_sum)| *command)
- .unwrap_or_else(|| Command::new(Action::DoNothing))
-}
-
-fn score(state: &GameBoard) -> Score {
- Score {
- val: match state.outcome {
- SimulationOutcome::PlayerWon(0) => 3000.,
- SimulationOutcome::PlayerWon(1) => -3000.,
- _ => (state.players[0].score() - state.players[1].score()) as f32,
- },
- }
-}
-
-fn rollout(state: &GameBoard) -> Score {
- score(state)
-}
-
-fn choose_existing(node: &Node) -> [Command; 2] {
- [choose_one_existing(node, 0), choose_one_existing(node, 1)]
-}
-
-fn choose_one_existing(node: &Node, player_index: usize) -> Command {
- let ln_n = (node.score_sum.visit_count as f32).ln();
- let c = 100.;
- let multiplier = if player_index == 0 { 1. } else { -1. };
- node.player_score_sums[player_index]
- .iter()
- .max_by_key(|(_command, score_sum)| {
- (multiplier * (score_sum.avg().val + c * (ln_n / score_sum.visit_count as f32).sqrt()))
- as i32
- })
- .map(|(command, _score_sum)| *command)
- .unwrap_or_else(|| Command::new(Action::DoNothing))
-}
-
-fn update(node: &mut Node, commands: [Command; 2], score: Score) {
- *node.player_score_sums[0]
- .entry(commands[0])
- .or_insert_with(ScoreSum::new) += score;
- *node.player_score_sums[1]
- .entry(commands[1])
- .or_insert_with(ScoreSum::new) += score;
- node.score_sum += score;
-}
diff --git a/src/strategy/minimax.rs b/src/strategy/minimax.rs
index 2c52127..5a7bbcd 100644
--- a/src/strategy/minimax.rs
+++ b/src/strategy/minimax.rs
@@ -7,21 +7,15 @@ use std::cmp;
use std::ops::*;
use time::{Duration, PreciseTime};
-// TODO: Calibrate these weightings somehow? Some sort of generate and sort based on playing against each other?
-// What about:
-// - Creating a list (mins and maxes)
-// - Keep adding a new guess, run against all, and sort the list by fitness.
-// - Repeat until list has many values
-// - Somehow prioritize sticking new items in based on what's going well? Or maximally different? Keep dividing all the ranges in half?
#[derive(Debug, Clone)]
pub struct ScoreConfig {
- max_health_weight: f32,
- total_health_weight: f32,
- points_weight: f32,
- victory_weight: f32,
- snowball_weight: f32,
- bomb_weight: f32,
- explore_exploit_weight: f32,
+ pub max_health_weight: f32,
+ pub total_health_weight: f32,
+ pub points_weight: f32,
+ pub victory_weight: f32,
+ pub snowball_weight: f32,
+ pub bomb_weight: f32,
+ pub explore_exploit_weight: f32,
}
impl Default for ScoreConfig {
@@ -38,7 +32,6 @@ impl Default for ScoreConfig {
}
}
-// TODO: Cache results from last round based on player / opponent move and worm positions
pub fn choose_move(
state: &GameBoard,
config: &ScoreConfig,
@@ -86,15 +79,15 @@ pub fn choose_move_with_normalized_perf(
let _ = expand_tree(&mut root_node, state.clone(), config);
}
- eprintln!("Number of simulations: {}", root_node.score_sum.visit_count);
- for (command, score_sum) in &root_node.player_score_sums[player_index] {
- eprintln!(
- "{} = {} ({} visits)",
- command,
- score_sum.avg().val,
- score_sum.visit_count
- );
- }
+ // eprintln!("Number of simulations: {}", root_node.score_sum.visit_count);
+ // for (command, score_sum) in &root_node.player_score_sums[player_index] {
+ // eprintln!(
+ // "{} = {} ({} visits)",
+ // command,
+ // score_sum.avg().val,
+ // score_sum.visit_count
+ // );
+ // }
best_player_move(&root_node, player_index)
}
@@ -178,7 +171,6 @@ fn expand_tree(node: &mut Node, mut state: GameBoard, config: &ScoreConfig) -> S
if state.outcome != SimulationOutcome::Continue {
score(&state, config)
} else if let Some(commands) = node.unexplored.pop() {
- // TODO: Explore preemptively doing the rollout?
state.simulate(commands);
let score = score(&state, config);
let unexplored = if state.outcome == SimulationOutcome::Continue {
@@ -196,8 +188,6 @@ fn expand_tree(node: &mut Node, mut state: GameBoard, config: &ScoreConfig) -> S
node.children.insert(commands, new_node);
update(node, commands, score);
- // TODO: Prune dominated moves
-
score
} else {
let commands = choose_existing(node, config);
@@ -215,8 +205,8 @@ fn expand_tree(node: &mut Node, mut state: GameBoard, config: &ScoreConfig) -> S
}
fn move_combos(state: &GameBoard) -> Vec<[Command; 2]> {
- let player_moves = pruned_moves(state, 0);
- let opponent_moves = pruned_moves(state, 1);
+ let player_moves = state.pruned_valid_moves(0);
+ let opponent_moves = state.pruned_valid_moves(1);
debug_assert!(!player_moves.is_empty(), "No player moves");
debug_assert!(!opponent_moves.is_empty(), "No opponent moves");
@@ -255,8 +245,6 @@ fn score(state: &GameBoard, config: &ScoreConfig) -> Score {
let snowballs = state.players[0].snowballs() as f32 - state.players[1].snowballs() as f32;
let bombs = state.players[0].bombs() as f32 - state.players[1].bombs() as f32;
- // TODO: None of these attributes give any signal early on.
- // TODO: Try adding new features here. Something about board position?
Score {
val: max_health * config.max_health_weight
+ total_health * config.total_health_weight
@@ -318,59 +306,3 @@ fn update(node: &mut Node, commands: [Command; 2], score: Score) {
.or_insert_with(ScoreSum::new) += score;
node.score_sum += score;
}
-
-fn pruned_moves(state: &GameBoard, player_index: usize) -> Vec<Command> {
- let sim_with_idle_opponent = |cmd| {
- let mut idle_commands = [
- Command::new(Action::DoNothing),
- Command::new(Action::DoNothing),
- ];
- idle_commands[player_index] = cmd;
- let mut state_cpy = state.clone();
- state_cpy.simulate(idle_commands);
- state_cpy
- };
-
- let mut do_nothing_state = state.clone();
- do_nothing_state.simulate([
- Command::new(Action::DoNothing),
- Command::new(Action::DoNothing),
- ]);
-
- let opponent_index = GameBoard::opponent(player_index);
- let my_starting_health = do_nothing_state.players[player_index].health();
- let opponent_starting_health = do_nothing_state.players[opponent_index].health();
-
- state
- .valid_moves(player_index)
- .into_iter()
- .filter(|command| {
- // TODO: Some of these filters could be done with better
- // performance by running them while generating the list
- // of valid moves.
-
- // NB: These rules should pass for doing nothing, otherwise
- // we need some other mechanism for sticking in a do
- // nothing option.
-
- let idle_opponent_state = sim_with_idle_opponent(*command);
- let hurt_self = idle_opponent_state.players[player_index].health() < my_starting_health;
- let hurt_opponent =
- idle_opponent_state.players[opponent_index].health() < opponent_starting_health;
- let frozen_opponent = idle_opponent_state.players[opponent_index]
- .worms
- .iter()
- .any(|w| w.rounds_until_unfrozen == FREEZE_DURATION);
-
- let is_select = command.worm.is_some();
-
- let is_attack = command.action.is_attack();
- let is_snowball = command.action.is_snowball();
-
- !hurt_self
- && (!is_select || hurt_opponent)
- && (!is_attack || hurt_opponent)
- && (!is_snowball || frozen_opponent)
- })
- .collect()
-}