From 7394e9db0b10a52fd291f649237e1b96541fff4f Mon Sep 17 00:00:00 2001 From: Mark Date: Tue, 5 Mar 2024 11:43:23 -0800 Subject: [PATCH] UI cleanup --- Cargo.lock | 234 ++++++++++++++++++++++++++++++++++++++------- Cargo.toml | 2 +- src/agents/mod.rs | 20 ++-- src/cli.rs | 59 ++++++++++++ src/main.rs | 235 ++++++++++++++++++++++++++++++++++------------ src/util.rs | 12 +-- 6 files changed, 455 insertions(+), 107 deletions(-) create mode 100644 src/cli.rs diff --git a/Cargo.lock b/Cargo.lock index 6da9829..623766b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,54 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "anstream" +version = "0.6.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d96bd03f33fe50a863e394ee9718a706f988b9079b20c3784fb726e7678b62fb" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc" + +[[package]] +name = "anstyle-parse" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7" +dependencies = [ + "anstyle", + "windows-sys", +] + [[package]] name = "anyhow" version = "1.0.80" @@ -27,29 +75,50 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] -name = "crossbeam-deque" -version = "0.8.5" +name = "clap" +version = "4.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" +checksum = "c918d541ef2913577a0f9566e9ce27cb35b6df072075769e0b26cb5a554520da" dependencies = [ - "crossbeam-epoch", - "crossbeam-utils", + "clap_builder", + "clap_derive", ] [[package]] -name = "crossbeam-epoch" -version = "0.9.18" +name = "clap_builder" +version = "4.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +checksum = "9f3e7391dad68afb0c2ede1bf619f579a3dc9c2ec67f089baa397123a2f3d1eb" dependencies = [ - "crossbeam-utils", + "anstream", + "anstyle", + "clap_lex", + "strsim", ] [[package]] -name = "crossbeam-utils" -version = "0.8.19" +name = "clap_derive" +version = "4.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" +checksum = "307bc0538d5f0f83b8248db3087aa92fe504e4691294d0c96c0eabc33f47ba47" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce" + +[[package]] +name = "colorchoice" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" [[package]] name = "either" @@ -68,6 +137,12 @@ dependencies = [ "wasi", ] +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + [[package]] name = "itertools" version = "0.12.1" @@ -105,9 +180,9 @@ name = "ops" version = "0.1.0" dependencies = [ "anyhow", + "clap", "itertools", "rand", - "rayon", "termion", ] @@ -117,6 +192,24 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" +[[package]] +name = "proc-macro2" +version = "1.0.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" +dependencies = [ + "proc-macro2", +] + [[package]] name = "rand" version = "0.8.5" @@ -147,26 +240,6 @@ 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" @@ -182,6 +255,23 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "20145670ba436b55d91fc92d25e71160fbfbdd57831631c8d7d36377a476f1cb" +[[package]] +name = "strsim" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ee073c9e4cd00e28217186dbe12796d692868f432bf2e97ee73bed0c56dfa01" + +[[package]] +name = "syn" +version = "2.0.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b699d15b36d1f02c3e7c69f8ffef53de37aefae075d8488d4ba1a7788d574a07" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "termion" version = "3.0.0" @@ -194,8 +284,86 @@ dependencies = [ "redox_termios", ] +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "utf8parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd37b7e5ab9018759f893a1952c9420d060016fc19a472b4bb20d1bdd694d1b" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcf46cf4c365c6f2d1cc93ce535f2c8b244591df96ceee75d8e83deb70a9cac9" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da9f259dd3bcf6990b55bffd094c4f7235817ba4ceebde8e6d11cd0c5633b675" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b474d8268f99e0995f25b9f095bc7434632601028cf86590aea5c8a5cb7801d3" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1515e9a29e5bed743cb4415a9ecf5dfca648ce85ee42e15873c3cd8610ff8e02" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eee091590e89cc02ad514ffe3ead9eb6b660aedca2183455434b93546371a03" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ca79f2451b49fa9e2af39f0747fe999fcda4f5e241b2898624dca97a1f2177" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8" diff --git a/Cargo.toml b/Cargo.toml index b4fd351..920a618 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,7 +5,7 @@ edition = "2021" [dependencies] 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/src/agents/mod.rs b/src/agents/mod.rs index ec49bb1..cfa4806 100644 --- a/src/agents/mod.rs +++ b/src/agents/mod.rs @@ -1,23 +1,31 @@ mod diffuse; +mod human; mod minimax; -mod player; mod random; pub mod util; pub use diffuse::Diffuse; +pub use human::Human; pub use minimax::SimpleMinimax; -pub use player::PlayerAgent; -pub use random::RandomAgent; +pub use random::Random; -use crate::board::{Board, PlayerAction}; +use crate::{ + board::{Board, PlayerAction}, + util::Player, +}; use anyhow::Result; +pub trait Agent { + fn name(&self) -> &'static str; + fn player(&self) -> Player; +} + /// An agent that tries to minimize the value of a board. -pub trait MinimizerAgent { +pub trait MinimizerAgent: Agent { fn step_min(&mut self, board: &Board) -> Result; } /// An agent that tries to maximize the value of a board. -pub trait MaximizerAgent { +pub trait MaximizerAgent: Agent { fn step_max(&mut self, board: &Board) -> Result; } diff --git a/src/cli.rs b/src/cli.rs new file mode 100644 index 0000000..6bfa0ba --- /dev/null +++ b/src/cli.rs @@ -0,0 +1,59 @@ +use clap::{Parser, ValueEnum}; + +use crate::{ + agents::{Diffuse, Human, MaximizerAgent, MinimizerAgent, Random, SimpleMinimax}, + util::Player, +}; + +#[derive(Parser, Debug)] +#[command(version, about)] +pub struct Cli { + pub red: AgentSelector, + pub blue: AgentSelector, + + #[arg(long, short, default_value = "0")] + pub repeat: usize, + + #[arg(long, short, default_value = "false")] + pub silent: bool, +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)] +pub enum AgentSelector { + Random, + Diffuse, + Minimax, + Human, +} + +impl AgentSelector { + pub fn to_maxi(&self, player: Player) -> Box { + match self { + Self::Random => Box::new(Random::new(player)), + Self::Minimax => Box::new(SimpleMinimax::new(player)), + Self::Diffuse => Box::new(Diffuse::new(player)), + Self::Human => Box::new(Human::new(player)), + } + } + + pub fn to_mini(&self, player: Player) -> Box { + match self { + Self::Random => Box::new(Random::new(player)), + Self::Minimax => Box::new(SimpleMinimax::new(player)), + Self::Diffuse => Box::new(Diffuse::new(player)), + Self::Human => Box::new(Human::new(player)), + } + } +} + +impl ToString for AgentSelector { + fn to_string(&self) -> String { + match self { + Self::Random => "random", + Self::Diffuse => "diffuse", + Self::Minimax => "minimax", + Self::Human => "human", + } + .to_string() + } +} diff --git a/src/main.rs b/src/main.rs index d2063c0..d9b82a5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,19 +1,59 @@ use std::cmp::Ordering; use anyhow::{bail, Result}; +use clap::Parser; use termion::color::{self}; mod agents; mod board; +mod cli; mod util; use board::Board; use util::{Player, Symb}; +fn play_silent( + maxi: &mut dyn agents::MaximizerAgent, + mini: &mut dyn agents::MinimizerAgent, +) -> Result { + let mut board = Board::new(); + let mut is_maxi_turn = true; + + while !board.is_done() { + // Take action + let action = if is_maxi_turn { + maxi.step_max(&board)? + } else { + mini.step_min(&board)? + }; + + if !board.play( + action, + if is_maxi_turn { + maxi.player() + } else { + mini.player() + }, + ) { + bail!( + "agent {} made an invalid move {}!", + if is_maxi_turn { + maxi.name() + } else { + mini.name() + }, + action + ) + } + + is_maxi_turn = !is_maxi_turn; + } + + Ok(board) +} + fn play( maxi: &mut dyn agents::MaximizerAgent, - maxi_player: Player, mini: &mut dyn agents::MinimizerAgent, - mini_player: Player, ) -> Result { let mut board = Board::new(); let mut is_first_turn = true; @@ -40,12 +80,20 @@ fn play( if !board.play( action, if is_maxi_turn { - maxi_player + maxi.player() } else { - mini_player + mini.player() }, ) { - bail!("agent made invalid move") + bail!( + "agent {} made an invalid move {}!", + if is_maxi_turn { + maxi.name() + } else { + mini.name() + }, + action + ) } is_maxi_turn = !is_maxi_turn; @@ -57,75 +105,140 @@ fn play( } fn main() -> Result<()> { - rayon::ThreadPoolBuilder::new() - .num_threads(4) - .build_global() - .unwrap(); + let cli = cli::Cli::parse(); - let mut maxi = agents::PlayerAgent::new(Player::Human); - let mut mini = agents::Diffuse {}; + if cli.repeat > 1 { + let mut red_wins = 0f32; + let mut blue_wins = 0f32; - let a = play(&mut maxi, Player::Human, &mut mini, Player::Computer)?; - if a.is_done() { + for _ in 0..cli.repeat { + let mut maxi = cli.red.to_maxi(Player::Red); + let mut mini = cli.blue.to_mini(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.to_maxi(Player::Blue); + let mut mini = cli.red.to_mini(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) => {} + Some(Ordering::Greater) => red_wins += 1.0, + Some(Ordering::Less) => blue_wins += 1.0, + None => { + println!("Error"); + } + } + } + + println!("Played {} rounds\n", cli.repeat); println!( - "\r\n{}Your score:{} {:.2}\n\n", - color::Fg(Player::Human.color()), - color::Fg(color::Reset), - a.evaluate().unwrap() + "Red win rate: {:.2} ({})", + red_wins / cli.repeat as f32, + cli.red.to_string(), + ); + println!( + "Blue win rate: {:.2} ({}) ", + blue_wins / cli.repeat as f32, + cli.blue.to_string(), ); } else { - println!( - "\r\n{}Quitting{}\r\n", - color::Fg(color::Red), - color::Fg(color::Reset), - ); - return Ok(()); - } + let mut maxi = cli.red.to_maxi(Player::Red); + let mut mini = cli.blue.to_mini(Player::Blue); + let red_board = if cli.silent { + play_silent(&mut *maxi, &mut *mini) + } else { + play(&mut *maxi, &mut *mini) + }?; - let mut mini = agents::PlayerAgent::new(Player::Human); - let mut maxi = agents::Diffuse {}; - - let b = play(&mut maxi, Player::Computer, &mut mini, Player::Human)?; - if b.is_done() { - println!( - "\r\n{}Computer score:{} {:.2}\n\n", - color::Fg(Player::Human.color()), - color::Fg(color::Reset), - b.evaluate().unwrap() - ); - } else { - println!( - "\r\n{}Quitting{}\r\n", - color::Fg(color::Red), - color::Fg(color::Reset), - ); - return Ok(()); - } - - match a.evaluate().partial_cmp(&b.evaluate()) { - Some(Ordering::Equal) => { - println!("\r\nTie"); - } - Some(Ordering::Greater) => { + if red_board.is_done() { println!( - "\r\n{}Human wins{}", - color::Fg(Player::Human.color()), + "\r\n{}{} score:{} {:.2}\n\n", + color::Fg(maxi.player().color()), + maxi.name(), color::Fg(color::Reset), + red_board.evaluate().unwrap() ); - } - Some(Ordering::Less) => { + } else { println!( - "\r\n{}Computer wins{}", - color::Fg(Player::Computer.color()), - color::Fg(color::Reset), - ); - } - None => { - println!( - "\r\n{}Error{}", + "\r\n{}Quitting{}\r\n", color::Fg(color::Red), color::Fg(color::Reset), ); + return Ok(()); + } + + let mut maxi = cli.blue.to_maxi(Player::Blue); + let mut mini = cli.red.to_mini(Player::Red); + let blue_board = if cli.silent { + play_silent(&mut *maxi, &mut *mini) + } else { + play(&mut *maxi, &mut *mini) + }?; + + if blue_board.is_done() { + println!( + "\r\n{}{} score:{} {:.2}\n\n", + color::Fg(maxi.player().color()), + maxi.name(), + color::Fg(color::Reset), + blue_board.evaluate().unwrap() + ); + } else { + println!( + "\r\n{}Quitting{}\r\n", + color::Fg(color::Red), + color::Fg(color::Reset), + ); + return Ok(()); + } + + match red_board.evaluate().partial_cmp(&blue_board.evaluate()) { + Some(Ordering::Equal) => { + println!("\r\nTie"); + } + Some(Ordering::Greater) => { + println!( + "\r\n{}Red ({}){} wins!", + color::Fg(Player::Red.color()), + cli.red.to_string(), + color::Fg(color::Reset), + ); + } + Some(Ordering::Less) => { + println!( + "\r\n{}Blue ({}){} wins!", + color::Fg(Player::Blue.color()), + cli.blue.to_string(), + color::Fg(color::Reset), + ); + } + None => { + println!( + "\r\n{}Error{}", + color::Fg(color::Red), + color::Fg(color::Reset), + ); + } } } diff --git a/src/util.rs b/src/util.rs index 19642da..1f032c8 100644 --- a/src/util.rs +++ b/src/util.rs @@ -7,15 +7,15 @@ use termion::color::{self, Color}; #[derive(PartialEq, Eq, Clone, Copy)] pub enum Player { - Human, - Computer, + Red, + Blue, } impl Player { pub fn color(&self) -> &dyn Color { match self { - Player::Computer => &color::LightBlack, - Player::Human => &color::Magenta, + Player::Red => &color::LightRed, + Player::Blue => &color::LightBlue, } } } @@ -23,8 +23,8 @@ impl Player { impl Display for Player { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let s = match self { - Self::Computer => "Max", - Self::Human => "Min", + Self::Red => "Red ", + Self::Blue => "Blue", }; write!(f, "{s}") }