summaryrefslogtreecommitdiff
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
parent33b9c9e05a3693d944342753288fda824f0da13c (diff)
Trimming down on redundant code and tweaking perf
-rw-r--r--Cargo.lock134
-rw-r--r--Cargo.toml1
-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
10 files changed, 424 insertions, 588 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 15bad26..f5c96f0 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -9,6 +9,55 @@ dependencies = [
]
[[package]]
+name = "cfg-if"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "crossbeam-deque"
+version = "0.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "crossbeam-epoch 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "crossbeam-utils 0.6.6 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "crossbeam-epoch"
+version = "0.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "arrayvec 0.4.10 (registry+https://github.com/rust-lang/crates.io-index)",
+ "cfg-if 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)",
+ "crossbeam-utils 0.6.6 (registry+https://github.com/rust-lang/crates.io-index)",
+ "lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "memoffset 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "scopeguard 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "crossbeam-queue"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "crossbeam-utils 0.6.6 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "crossbeam-utils"
+version = "0.6.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "cfg-if 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)",
+ "lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "either"
+version = "1.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
name = "fnv"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -19,11 +68,24 @@ version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
+name = "lazy_static"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
name = "libc"
version = "0.2.51"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
+name = "memoffset"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "rustc_version 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
name = "nodrop"
version = "0.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -34,6 +96,14 @@ version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
+name = "num_cpus"
+version = "1.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "libc 0.2.51 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
name = "proc-macro2"
version = "0.4.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -50,16 +120,64 @@ dependencies = [
]
[[package]]
+name = "rayon"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "crossbeam-deque 0.6.3 (registry+https://github.com/rust-lang/crates.io-index)",
+ "either 1.5.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "rayon-core 1.5.0 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "rayon-core"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "crossbeam-deque 0.6.3 (registry+https://github.com/rust-lang/crates.io-index)",
+ "crossbeam-queue 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "crossbeam-utils 0.6.6 (registry+https://github.com/rust-lang/crates.io-index)",
+ "lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "num_cpus 1.10.1 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
name = "redox_syscall"
version = "0.1.54"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
+name = "rustc_version"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "semver 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
name = "ryu"
version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
+name = "scopeguard"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "semver"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "semver-parser 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "semver-parser"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
name = "serde"
version = "1.0.90"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -94,6 +212,7 @@ dependencies = [
"arrayvec 0.4.10 (registry+https://github.com/rust-lang/crates.io-index)",
"fnv 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
"num-traits 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)",
+ "rayon 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
"serde 1.0.90 (registry+https://github.com/rust-lang/crates.io-index)",
"serde_json 1.0.39 (registry+https://github.com/rust-lang/crates.io-index)",
"time 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -145,15 +264,30 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
[metadata]
"checksum arrayvec 0.4.10 (registry+https://github.com/rust-lang/crates.io-index)" = "92c7fb76bc8826a8b33b4ee5bb07a247a81e76764ab4d55e8f73e3a4d8808c71"
+"checksum cfg-if 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)" = "b486ce3ccf7ffd79fdeb678eac06a9e6c09fc88d33836340becb8fffe87c5e33"
+"checksum crossbeam-deque 0.6.3 (registry+https://github.com/rust-lang/crates.io-index)" = "05e44b8cf3e1a625844d1750e1f7820da46044ff6d28f4d43e455ba3e5bb2c13"
+"checksum crossbeam-epoch 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)" = "fedcd6772e37f3da2a9af9bf12ebe046c0dfe657992377b4df982a2b54cd37a9"
+"checksum crossbeam-queue 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7c979cd6cfe72335896575c6b5688da489e420d36a27a0b9eb0c73db574b4a4b"
+"checksum crossbeam-utils 0.6.6 (registry+https://github.com/rust-lang/crates.io-index)" = "04973fa96e96579258a5091af6003abde64af786b860f18622b82e026cca60e6"
+"checksum either 1.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "5527cfe0d098f36e3f8839852688e63c8fff1c90b2b405aef730615f9a7bcf7b"
"checksum fnv 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)" = "2fad85553e09a6f881f739c29f0b00b0f01357c743266d478b68951ce23285f3"
"checksum itoa 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)" = "1306f3464951f30e30d12373d31c79fbd52d236e5e896fd92f96ec7babbbe60b"
+"checksum lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "bc5729f27f159ddd61f4df6228e827e86643d4d3e7c32183cb30a1c08f604a14"
"checksum libc 0.2.51 (registry+https://github.com/rust-lang/crates.io-index)" = "bedcc7a809076656486ffe045abeeac163da1b558e963a31e29fbfbeba916917"
+"checksum memoffset 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "ce6075db033bbbb7ee5a0bbd3a3186bbae616f57fb001c485c7ff77955f8177f"
"checksum nodrop 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)" = "2f9667ddcc6cc8a43afc9b7917599d7216aa09c463919ea32c59ed6cac8bc945"
"checksum num-traits 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)" = "0b3a5d7cc97d6d30d8b9bc8fa19bf45349ffe46241e8816f50f62f6d6aaabee1"
+"checksum num_cpus 1.10.1 (registry+https://github.com/rust-lang/crates.io-index)" = "bcef43580c035376c0705c42792c294b66974abbfd2789b511784023f71f3273"
"checksum proc-macro2 0.4.27 (registry+https://github.com/rust-lang/crates.io-index)" = "4d317f9caece796be1980837fd5cb3dfec5613ebdb04ad0956deea83ce168915"
"checksum quote 0.6.12 (registry+https://github.com/rust-lang/crates.io-index)" = "faf4799c5d274f3868a4aae320a0a182cbd2baee377b378f080e16a23e9d80db"
+"checksum rayon 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "a4b0186e22767d5b9738a05eab7c6ac90b15db17e5b5f9bd87976dd7d89a10a4"
+"checksum rayon-core 1.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ebbe0df8435ac0c397d467b6cad6d25543d06e8a019ef3f6af3c384597515bd2"
"checksum redox_syscall 0.1.54 (registry+https://github.com/rust-lang/crates.io-index)" = "12229c14a0f65c4f1cb046a3b52047cdd9da1f4b30f8a39c5063c8bae515e252"
+"checksum rustc_version 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a"
"checksum ryu 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)" = "eb9e9b8cde282a9fe6a42dd4681319bfb63f121b8a8ee9439c6f4107e58a46f7"
+"checksum scopeguard 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b42e15e59b18a828bbf5c58ea01debb36b9b096346de35d941dcb89009f24a0d"
+"checksum semver 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403"
+"checksum semver-parser 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3"
"checksum serde 1.0.90 (registry+https://github.com/rust-lang/crates.io-index)" = "aa5f7c20820475babd2c077c3ab5f8c77a31c15e16ea38687b4c02d3e48680f4"
"checksum serde_derive 1.0.90 (registry+https://github.com/rust-lang/crates.io-index)" = "58fc82bec244f168b23d1963b45c8bf5726e9a15a9d146a067f9081aeed2de79"
"checksum serde_json 1.0.39 (registry+https://github.com/rust-lang/crates.io-index)" = "5a23aa71d4a4d43fdbfaac00eff68ba8a06a51759a89ac3304323e800c4dd40d"
diff --git a/Cargo.toml b/Cargo.toml
index 4e5f811..891fcb3 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -10,6 +10,7 @@ time = "0.1.42"
num-traits = "0.2.6"
arrayvec = "0.4.10"
fnv = "1.0.6"
+rayon = "1.1.0"
[profile.release]
debug = true
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()
-}