From dabedf442d65fcaec36d0a30ed3ed2327b39a44d Mon Sep 17 00:00:00 2001 From: Justin Worthe Date: Tue, 13 Aug 2019 22:00:10 +0200 Subject: Trimming down on redundant code and tweaking perf --- Cargo.lock | 134 ++++++++++++++++ Cargo.toml | 1 + src/bin/explore-config.rs | 132 ++++++++++++---- src/constants.rs | 6 + src/game.rs | 379 +++++++++++++++++++--------------------------- src/game/player.rs | 22 --- src/game/powerup.rs | 1 - src/json.rs | 3 + src/strategy/mcts.rs | 230 ---------------------------- src/strategy/minimax.rs | 104 +++---------- 10 files changed, 424 insertions(+), 588 deletions(-) delete mode 100644 src/strategy/mcts.rs diff --git a/Cargo.lock b/Cargo.lock index 15bad26..f5c96f0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,6 +8,55 @@ dependencies = [ "nodrop 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[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" @@ -18,11 +67,24 @@ name = "itoa" 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" @@ -33,6 +95,14 @@ name = "num-traits" 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" @@ -49,16 +119,64 @@ dependencies = [ "proc-macro2 0.4.27 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[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" @@ -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::>() + .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, + pub total_health_weight: Vec, + pub points_weight: Vec, + pub victory_weight: Vec, + pub snowball_weight: Vec, + pub bomb_weight: Vec, + pub explore_exploit_weight: Vec, +} + +impl ScoreConfigTrials { + fn reify(self) -> Vec { + 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, 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, &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 { - 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 { + 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 { - 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 { + 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, - 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 { + 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 { + pub fn pruned_valid_moves(&self, player_index: usize) -> Vec { 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, - 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, - 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> { 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, - 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; 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 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 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 { - 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() -} -- cgit v1.2.3