Added brutus agent

This commit is contained in:
2024-03-06 09:56:04 -08:00
parent 4ee7f8a9ac
commit b39c618a2a
8 changed files with 247 additions and 65 deletions

123
src/agents/brutus.rs Normal file
View File

@@ -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)
}
}

View File

@@ -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))

View File

@@ -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;

View File

@@ -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",
}

View File

@@ -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!(