Added brutus agent
parent
4ee7f8a9ac
commit
b39c618a2a
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
12
README.md
12
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`.
|
||||
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%
|
|
@ -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<PlayerAction> {
|
||||
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::<Vec<_>>();
|
||||
|
||||
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<PlayerAction> {
|
||||
self.step(board, true)
|
||||
}
|
||||
}
|
||||
|
||||
impl MaximizerAgent for Brutus {
|
||||
fn step_max(&mut self, board: &Board) -> Result<PlayerAction> {
|
||||
self.step(board, false)
|
||||
}
|
||||
}
|
|
@ -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<PlayerAction> {
|
||||
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<PlayerAction> {
|
||||
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))
|
||||
|
|
|
@ -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;
|
||||
|
|
14
src/cli.rs
14
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",
|
||||
}
|
||||
|
|
100
src/main.rs
100
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::<Vec<_>>();
|
||||
|
||||
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!(
|
||||
|
|
Loading…
Reference in New Issue