From b39c618a2a9c422cdc064639e2cf8e80968fddb6 Mon Sep 17 00:00:00 2001 From: Mark Date: Wed, 6 Mar 2024 09:56:04 -0800 Subject: [PATCH] Added brutus agent --- Cargo.lock | 46 ++++++++++++++++ Cargo.toml | 1 + README.md | 12 ++++- src/agents/brutus.rs | 123 ++++++++++++++++++++++++++++++++++++++++++ src/agents/diffuse.rs | 14 ++--- src/agents/mod.rs | 2 + src/cli.rs | 14 ++--- src/main.rs | 100 +++++++++++++++++----------------- 8 files changed, 247 insertions(+), 65 deletions(-) create mode 100644 src/agents/brutus.rs diff --git a/Cargo.lock b/Cargo.lock index ad1073f..b0373f3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -120,6 +120,31 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" +[[package]] +name = "crossbeam-deque" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" + [[package]] name = "either" version = "1.10.0" @@ -177,6 +202,7 @@ dependencies = [ "clap", "itertools", "rand", + "rayon", "termion", ] @@ -240,6 +266,26 @@ dependencies = [ "getrandom", ] +[[package]] +name = "rayon" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4963ed1bc86e4f3ee217022bd855b297cef07fb9eac5dfa1f788b220b49b3bd" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + [[package]] name = "redox_syscall" version = "0.4.1" diff --git a/Cargo.toml b/Cargo.toml index f9aa35c..f1cf65a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,4 +8,5 @@ anyhow = "1.0.80" clap = { version = "4.5.1", features = ["derive"] } itertools = "0.12.1" rand = "0.8.5" +rayon = "1.9.0" termion = "3.0.0" diff --git a/README.md b/README.md index 8e75dbf..24cfd82 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,15 @@ As always, run this project with `cargo run`. The app takes one argument by defa - `random`: Play against a random agent (very easy) - `chase`: Play against a simple extremum-chasing agent (easy) - `diffuse`: Play against a slightly more intellegent extremum chaser (medium) + - `brutus`: Play against a simple brute-force agent (hard) -For example, `cargo run -- random` will play a game against a random player. Use your arrow keys and space bar to play the game. +For example, `cargo run -- random` will start a game against a random player. Use your arrow keys and space bar to play. -Additional options are available, see `cargo run -- --help`. \ No newline at end of file +Additional options are available, see `cargo run -- --help`. + +Win rates against random are as follows: + - `human`: ~100% + - `random`: ~50% + - `chase`: ~70% + - `diffuse`: ~76% + - `brutus`: ~90% \ No newline at end of file diff --git a/src/agents/brutus.rs b/src/agents/brutus.rs new file mode 100644 index 0000000..74b143f --- /dev/null +++ b/src/agents/brutus.rs @@ -0,0 +1,123 @@ +use std::{cmp::Ordering, iter}; + +use anyhow::Result; +use itertools::Itertools; +use rayon::iter::{ParallelBridge, ParallelIterator}; + +use super::{Agent, Chase, MaximizerAgent, MinimizerAgent}; +use crate::{ + agents::{util::best_board_noop, Diffuse}, + board::{Board, PlayerAction}, + util::{Player, Symb}, +}; + +pub struct Brutus { + player: Player, +} + +impl Brutus { + pub fn new(player: Player) -> Self { + Self { player } + } + + fn step(&mut self, board: &Board, minimize: bool) -> Result { + let symbols = [Symb::Minus, Symb::Times, Symb::Plus, Symb::Div] + .into_iter() + .filter(|x| !board.contains(*x)) + .collect_vec(); + + if symbols.is_empty() { + return Chase::new(self.player).step_max(board); + } + + // Number of free slots + let n_free = board.get_board().iter().filter(|x| x.is_none()).count(); + + // Number of slots we need to fill with numbers + let n_fill = n_free - symbols.len(); + + let mut items = iter::repeat(None) + .take(n_fill) + .chain(symbols.iter().map(|x| Some(x.clone()))) + .permutations(n_free) + .unique() + .par_bridge() + .filter_map(move |x| { + let mut tmp_board = board.clone(); + + for (i, s) in x.iter().enumerate() { + if let Some(s) = s { + let pos = board.ith_empty_slot(i).unwrap(); + if !tmp_board.can_play(&PlayerAction { symb: *s, pos }) { + return None; + } + tmp_board.get_board_mut()[pos] = Some(*s); + } + } + + let min = best_board_noop(&tmp_board, true); + let max = best_board_noop(&tmp_board, false); + if min.is_none() || max.is_none() { + return None; + } + + let v_min = min.unwrap().evaluate().unwrap(); + let v_max = max.unwrap().evaluate().unwrap(); + let v = v_min + v_max / 2.0; + + Some((tmp_board, v)) + }) + .collect::>(); + + if minimize { + // Sort from smallest midpoint to biggest midpoint + items.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(Ordering::Equal)); + } else { + items.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(Ordering::Equal)); + } + + // TODO: why can `items` be empty? + // We shouldn't need this escape hatch + if items.is_empty() { + return if minimize { + Diffuse::new(self.player).step_min(board) + } else { + Diffuse::new(self.player).step_max(board) + }; + } + + let (t, _) = items.first().unwrap(); + + for (i, s) in t.get_board().iter().enumerate() { + if let Some(s) = s { + if s.is_op() && board.get_board()[i].is_none() { + return Ok(PlayerAction { pos: i, symb: *s }); + } + } + } + + unreachable!() + } +} + +impl Agent for Brutus { + fn name(&self) -> &'static str { + "Brutus" + } + + fn player(&self) -> Player { + self.player + } +} + +impl MinimizerAgent for Brutus { + fn step_min(&mut self, board: &Board) -> Result { + self.step(board, true) + } +} + +impl MaximizerAgent for Brutus { + fn step_max(&mut self, board: &Board) -> Result { + self.step(board, false) + } +} diff --git a/src/agents/diffuse.rs b/src/agents/diffuse.rs index 18a3872..69537e4 100644 --- a/src/agents/diffuse.rs +++ b/src/agents/diffuse.rs @@ -1,4 +1,5 @@ use anyhow::Result; +use rand::{seq::SliceRandom, thread_rng}; use super::{Agent, Chase, MaximizerAgent, MinimizerAgent, Random}; use crate::{ @@ -9,6 +10,7 @@ use crate::{ /// A simple "operator diffusion" MINIMIZER agent. /// /// Tries to keep operators as far apart as possible, denying large numbers. +/// Places numbers using the same algorithm as chase. pub struct Diffuse { player: Player, } @@ -86,9 +88,9 @@ impl Agent for Diffuse { impl MinimizerAgent for Diffuse { fn step_min(&mut self, board: &Board) -> Result { - let symb = [Symb::Minus, Symb::Times, Symb::Plus, Symb::Div] - .iter() - .find(|x| !board.contains(**x)); + let mut x = [Symb::Minus, Symb::Times, Symb::Plus, Symb::Div]; + x.shuffle(&mut thread_rng()); + let symb = x.iter().find(|x| !board.contains(**x)); if let Some(symb) = symb { Ok(self.step_symb(board, *symb)) @@ -101,9 +103,9 @@ impl MinimizerAgent for Diffuse { impl MaximizerAgent for Diffuse { fn step_max(&mut self, board: &Board) -> Result { - let symb = [Symb::Minus, Symb::Times, Symb::Plus, Symb::Div] - .iter() - .find(|x| !board.contains(**x)); + let mut x = [Symb::Minus, Symb::Times, Symb::Plus, Symb::Div]; + x.shuffle(&mut thread_rng()); + let symb = x.iter().find(|x| !board.contains(**x)); if let Some(symb) = symb { Ok(self.step_symb(board, *symb)) diff --git a/src/agents/mod.rs b/src/agents/mod.rs index b51e4bc..4e99170 100644 --- a/src/agents/mod.rs +++ b/src/agents/mod.rs @@ -1,9 +1,11 @@ +mod brutus; mod chase; mod diffuse; mod human; mod random; pub mod util; +pub use brutus::Brutus; pub use chase::Chase; pub use diffuse::Diffuse; pub use human::Human; diff --git a/src/cli.rs b/src/cli.rs index d75fdf7..dd49788 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -3,7 +3,7 @@ use std::fmt::Display; use clap::{Parser, ValueEnum}; use crate::{ - agents::{Chase, Diffuse, Human, MaximizerAgent, MinimizerAgent, Random}, + agents::{Brutus, Chase, Diffuse, Human, MaximizerAgent, MinimizerAgent, Random}, util::Player, }; @@ -18,14 +18,8 @@ pub struct Cli { pub red: AgentSelector, /// If this is greater than one, repeat the game this many times and print a summary. - /// Best used with --silent. #[arg(long, short, default_value = "0")] pub repeat: usize, - - /// If this is given, do not print boards. - /// Good for bulk runs with --repeat, bad for human players. - #[arg(long, short, default_value = "false")] - pub silent: bool, } #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)] @@ -41,6 +35,9 @@ pub enum AgentSelector { /// A smarter extremum-chaser (medium) Diffuse, + + /// A very smart brute-force agent (hard) + Brutus, } impl AgentSelector { @@ -49,6 +46,7 @@ impl AgentSelector { Self::Random => Box::new(Random::new(player)), Self::Chase => Box::new(Chase::new(player)), Self::Diffuse => Box::new(Diffuse::new(player)), + Self::Brutus => Box::new(Brutus::new(player)), Self::Human => Box::new(Human::new(player)), } } @@ -58,6 +56,7 @@ impl AgentSelector { Self::Random => Box::new(Random::new(player)), Self::Chase => Box::new(Chase::new(player)), Self::Diffuse => Box::new(Diffuse::new(player)), + Self::Brutus => Box::new(Brutus::new(player)), Self::Human => Box::new(Human::new(player)), } } @@ -71,6 +70,7 @@ impl Display for AgentSelector { match self { Self::Random => "random", Self::Diffuse => "diffuse", + Self::Brutus => "brutus", Self::Chase => "chase", Self::Human => "human", } diff --git a/src/main.rs b/src/main.rs index ed2e2d0..0a4f26b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,11 @@ -use std::cmp::Ordering; +use std::{ + cmp::Ordering, + io::{stdout, Write}, +}; use anyhow::{bail, Result}; use clap::Parser; +use rayon::iter::{IntoParallelIterator, ParallelIterator}; use termion::color::{self}; mod agents; @@ -77,6 +81,9 @@ fn play( ); is_first_turn = false; + print!("Thinking..."); + stdout().flush()?; + // Take action let action = if is_maxi_turn { maxi.step_max(&board)? @@ -114,68 +121,65 @@ fn play( fn main() -> Result<()> { let cli = cli::Cli::parse(); + rayon::ThreadPoolBuilder::new() + .num_threads(8) + .build_global() + .unwrap(); + if cli.repeat > 1 { - let mut red_wins = 0f32; - let mut blue_wins = 0f32; + let x = (0..cli.repeat) + .into_par_iter() + .filter_map(|_| { + let mut maxi = cli.red.get_maximizer(Player::Red); + let mut mini = cli.blue.get_minimizer(Player::Blue); + let red_board = match play_silent(&mut *maxi, &mut *mini) { + Ok(x) => x, + Err(e) => { + println!("Error: {e}"); + return None; + } + }; - for _ in 0..cli.repeat { - let mut maxi = cli.red.get_maximizer(Player::Red); - let mut mini = cli.blue.get_minimizer(Player::Blue); - let red_board = match if cli.silent { - play_silent(&mut *maxi, &mut *mini) - } else { - play(&mut *maxi, &mut *mini) - } { - Ok(x) => x, - Err(e) => { - println!("Error: {e}"); - continue; - } - }; + let mut maxi = cli.blue.get_maximizer(Player::Blue); + let mut mini = cli.red.get_minimizer(Player::Red); + let blue_board = match play_silent(&mut *maxi, &mut *mini) { + Ok(x) => x, + Err(e) => { + println!("Error: {e}"); + return None; + } + }; - let mut maxi = cli.blue.get_maximizer(Player::Blue); - let mut mini = cli.red.get_minimizer(Player::Red); - let blue_board = match if cli.silent { - play_silent(&mut *maxi, &mut *mini) - } else { - play(&mut *maxi, &mut *mini) - } { - Ok(x) => x, - Err(e) => { - println!("Error: {e}"); - continue; + match red_board.evaluate().partial_cmp(&blue_board.evaluate()) { + Some(Ordering::Equal) => None, + Some(Ordering::Greater) => Some(Player::Red), + Some(Ordering::Less) => Some(Player::Blue), + None => { + println!("Error"); + None + } } - }; + }) + .collect::>(); - match red_board.evaluate().partial_cmp(&blue_board.evaluate()) { - Some(Ordering::Equal) => {} - Some(Ordering::Greater) => red_wins += 1.0, - Some(Ordering::Less) => blue_wins += 1.0, - None => { - println!("Error"); - } - } - } + let red_wins = x.iter().filter(|x| **x == Player::Red).count(); + let blue_wins = x.iter().filter(|x| **x == Player::Blue).count(); println!("Played {} rounds\n", cli.repeat); println!( "Red win rate: {:.2} ({})", - red_wins / cli.repeat as f32, + red_wins as f32 / cli.repeat as f32, cli.red, ); println!( "Blue win rate: {:.2} ({}) ", - blue_wins / cli.repeat as f32, + blue_wins as f32 / cli.repeat as f32, cli.blue, ); } else { let mut maxi = cli.red.get_maximizer(Player::Red); let mut mini = cli.blue.get_minimizer(Player::Blue); - let red_board = if cli.silent { - play_silent(&mut *maxi, &mut *mini) - } else { - play(&mut *maxi, &mut *mini) - }?; + let red_board = play(&mut *maxi, &mut *mini)?; if red_board.is_done() { println!( @@ -196,11 +200,7 @@ fn main() -> Result<()> { let mut maxi = cli.blue.get_maximizer(Player::Blue); let mut mini = cli.red.get_minimizer(Player::Red); - let blue_board = if cli.silent { - play_silent(&mut *maxi, &mut *mini) - } else { - play(&mut *maxi, &mut *mini) - }?; + let blue_board = play(&mut *maxi, &mut *mini)?; if blue_board.is_done() { println!(