Compare commits

..

No commits in common. "e8614dd29f4eeef136a063084f6f57cf326a7259" and "7394e9db0b10a52fd291f649237e1b96541fff4f" have entirely different histories.

11 changed files with 72 additions and 123 deletions

14
Cargo.lock generated
View File

@ -170,7 +170,13 @@ dependencies = [
] ]
[[package]] [[package]]
name = "minimax" name = "numtoa"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8f8bdf33df195859076e54ab11ee78a1b208382d3a26ec40d142ffc1ecc49ef"
[[package]]
name = "ops"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
@ -180,12 +186,6 @@ dependencies = [
"termion", "termion",
] ]
[[package]]
name = "numtoa"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8f8bdf33df195859076e54ab11ee78a1b208382d3a26ec40d142ffc1ecc49ef"
[[package]] [[package]]
name = "ppv-lite86" name = "ppv-lite86"
version = "0.2.17" version = "0.2.17"

View File

@ -1,5 +1,5 @@
[package] [package]
name = "minimax" name = "ops"
version = "0.1.0" version = "0.1.0"
edition = "2021" edition = "2021"

View File

@ -1,28 +0,0 @@
# Minimax
## Rules
This game is played in two rounds, starting with an empty eleven-space board. Red always goes first.
On Red's board (i.e, duing the first round), Red's job is to maximize the value of the expression; Blue's job is to minimize it.
Players take turns placing the fourteen symbols `0123456789+-×÷` on the board, with the maximizing player taking the first move.
A board's syntax must always be valid. Namely, the following rules are enforced:
- Each symbol may only be used once
- The binary operators `+-×÷` may not be next to one another, and may not be at the end slots.
- The unary operator `-` (negative) must have a number as an argument. Therefore, it cannot be left of an operator (like `-×`), and it may not be in the rightmost slot.
- `0` may not follow `÷`. This prevents most cases of zero-division, but isn't perfect. `÷-0` will break the game, and `÷0_+` is forbidden despite being valid syntax once the empty slot is filled (for example, with `÷03+`). This is done to simplyify game logic, and might be improved later.
## Building & Running
As always, run this project with `cargo run`. The app takes one argument by default: the name of the blue player. This can be any of the following:
- `human`: Play against a human
- `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)
For example, `cargo run -- random` will play a game against a random player.
Additional options are available, see `cargo run -- --help`.

View File

@ -1,6 +1,6 @@
use anyhow::Result; use anyhow::Result;
use super::{Agent, Chase, MaximizerAgent, MinimizerAgent, Random}; use super::{Agent, MaximizerAgent, MinimizerAgent, Random, SimpleMinimax};
use crate::{ use crate::{
board::{Board, PlayerAction}, board::{Board, PlayerAction},
util::{Player, Symb}, util::{Player, Symb},
@ -94,7 +94,7 @@ impl MinimizerAgent for Diffuse {
Ok(self.step_symb(board, *symb)) Ok(self.step_symb(board, *symb))
} else { } else {
// No symbols available, play a random number // No symbols available, play a random number
Chase::new(self.player).step_min(board) SimpleMinimax::new(self.player).step_min(board)
} }
} }
} }
@ -109,7 +109,7 @@ impl MaximizerAgent for Diffuse {
Ok(self.step_symb(board, *symb)) Ok(self.step_symb(board, *symb))
} else { } else {
// No symbols available, play a random number // No symbols available, play a random number
Chase::new(self.player).step_max(board) SimpleMinimax::new(self.player).step_max(board)
} }
} }
} }

View File

@ -8,11 +8,11 @@ use crate::{
util::{Player, Symb}, util::{Player, Symb},
}; };
pub struct Chase { pub struct SimpleMinimax {
player: Player, player: Player,
} }
impl Chase { impl SimpleMinimax {
pub fn new(player: Player) -> Self { pub fn new(player: Player) -> Self {
Self { player } Self { player }
} }
@ -33,13 +33,13 @@ impl Chase {
return Random::new(self.player).step_min(board); return Random::new(self.player).step_min(board);
} }
let t = free_slots_by_influence(board); let t = free_slots_by_influence(&board);
if t.is_none() { if t.is_none() {
bail!("could not compute next move!") bail!("could not compute next move!")
} }
let t = t.unwrap(); let t = t.unwrap();
if t.is_empty() { if t.len() == 0 {
return Random::new(self.player).step_min(board); return Random::new(self.player).step_min(board);
} }
@ -63,11 +63,13 @@ impl Chase {
} else { } else {
available_numbers[available_numbers.len() - 1 - offset] available_numbers[available_numbers.len() - 1 - offset]
} }
} else if val <= 0.0 { } else {
if val <= 0.0 {
available_numbers[offset] available_numbers[offset]
} else { } else {
available_numbers[available_numbers.len() - 1 - offset] available_numbers[available_numbers.len() - 1 - offset]
} }
}
}); });
offset += 1; offset += 1;
} }
@ -79,9 +81,9 @@ impl Chase {
} }
} }
impl Agent for Chase { impl Agent for SimpleMinimax {
fn name(&self) -> &'static str { fn name(&self) -> &'static str {
"Chase" "Minimax"
} }
fn player(&self) -> Player { fn player(&self) -> Player {
@ -89,13 +91,13 @@ impl Agent for Chase {
} }
} }
impl MinimizerAgent for Chase { impl MinimizerAgent for SimpleMinimax {
fn step_min(&mut self, board: &Board) -> Result<PlayerAction> { fn step_min(&mut self, board: &Board) -> Result<PlayerAction> {
self.step(board, true) self.step(board, true)
} }
} }
impl MaximizerAgent for Chase { impl MaximizerAgent for SimpleMinimax {
fn step_max(&mut self, board: &Board) -> Result<PlayerAction> { fn step_max(&mut self, board: &Board) -> Result<PlayerAction> {
self.step(board, false) self.step(board, false)
} }

View File

@ -1,12 +1,12 @@
mod chase;
mod diffuse; mod diffuse;
mod human; mod human;
mod minimax;
mod random; mod random;
pub mod util; pub mod util;
pub use chase::Chase;
pub use diffuse::Diffuse; pub use diffuse::Diffuse;
pub use human::Human; pub use human::Human;
pub use minimax::SimpleMinimax;
pub use random::Random; pub use random::Random;
use crate::{ use crate::{

View File

@ -69,7 +69,7 @@ pub fn maximize_value(board: &Board) -> Option<Board> {
.filter(|x| !board.contains(*x)) .filter(|x| !board.contains(*x))
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let slots = free_slots_by_influence(board)?; let slots = free_slots_by_influence(&board)?;
let all_symbols = { let all_symbols = {
// We need this many from the bottom, and this many from the top. // We need this many from the bottom, and this many from the top.
@ -92,7 +92,7 @@ pub fn maximize_value(board: &Board) -> Option<Board> {
// equal-weight slots // equal-weight slots
.map(|s| { .map(|s| {
(0..s) (0..s)
.map(|_| *a_iter.next().unwrap()) .map(|_| a_iter.next().unwrap().clone())
.permutations(s) .permutations(s)
.unique() .unique()
.collect_vec() .collect_vec()

View File

@ -1,6 +1,6 @@
use anyhow::Result; use anyhow::Result;
use itertools::Itertools; use itertools::Itertools;
use std::fmt::{Display, Write}; use std::fmt::Write;
use termion::color::{self, Color}; use termion::color::{self, Color};
use super::{PlayerAction, TreeElement}; use super::{PlayerAction, TreeElement};
@ -65,12 +65,14 @@ pub struct Board {
last_placed: Option<usize>, last_placed: Option<usize>,
} }
impl Display for Board { impl ToString for Board {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn to_string(&self) -> String {
for c in self.board { let mut s = String::new();
write!(f, "{}", c.map(|s| s.get_char().unwrap()).unwrap_or('_'))? s.extend(
} self.board
Ok(()) .map(|x| x.map(|s| s.to_char().unwrap()).unwrap_or('_')),
);
s
} }
} }
@ -367,7 +369,7 @@ impl Board {
if c == '_' { if c == '_' {
Some(None) Some(None)
} else { } else {
Symb::from_char(c).map(Some) Symb::from_char(c).map(|s| Some(s))
} }
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();

View File

@ -1,79 +1,59 @@
use std::fmt::Display;
use clap::{Parser, ValueEnum}; use clap::{Parser, ValueEnum};
use crate::{ use crate::{
agents::{Chase, Diffuse, Human, MaximizerAgent, MinimizerAgent, Random}, agents::{Diffuse, Human, MaximizerAgent, MinimizerAgent, Random, SimpleMinimax},
util::Player, util::Player,
}; };
#[derive(Parser, Debug)] #[derive(Parser, Debug)]
#[command(about)] #[command(version, about)]
pub struct Cli { pub struct Cli {
/// The agent that controls the Blue (opponent) player pub red: AgentSelector,
pub blue: AgentSelector, pub blue: AgentSelector,
/// The agent that controls the Red (home) player
#[arg(long, default_value = "human")]
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")] #[arg(long, short, default_value = "0")]
pub repeat: usize, 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")] #[arg(long, short, default_value = "false")]
pub silent: bool, pub silent: bool,
} }
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)] #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
pub enum AgentSelector { pub enum AgentSelector {
/// A human agent. Asks for input.
Human,
/// A random agent (very easy)
Random, Random,
/// A simple extremum-chaser (easy)
Chase,
/// A smarter extremum-chaser (medium)
Diffuse, Diffuse,
Minimax,
Human,
} }
impl AgentSelector { impl AgentSelector {
pub fn get_maximizer(&self, player: Player) -> Box<dyn MaximizerAgent> { pub fn to_maxi(&self, player: Player) -> Box<dyn MaximizerAgent> {
match self { match self {
Self::Random => Box::new(Random::new(player)), Self::Random => Box::new(Random::new(player)),
Self::Chase => Box::new(Chase::new(player)), Self::Minimax => Box::new(SimpleMinimax::new(player)),
Self::Diffuse => Box::new(Diffuse::new(player)), Self::Diffuse => Box::new(Diffuse::new(player)),
Self::Human => Box::new(Human::new(player)), Self::Human => Box::new(Human::new(player)),
} }
} }
pub fn get_minimizer(&self, player: Player) -> Box<dyn MinimizerAgent> { pub fn to_mini(&self, player: Player) -> Box<dyn MinimizerAgent> {
match self { match self {
Self::Random => Box::new(Random::new(player)), Self::Random => Box::new(Random::new(player)),
Self::Chase => Box::new(Chase::new(player)), Self::Minimax => Box::new(SimpleMinimax::new(player)),
Self::Diffuse => Box::new(Diffuse::new(player)), Self::Diffuse => Box::new(Diffuse::new(player)),
Self::Human => Box::new(Human::new(player)), Self::Human => Box::new(Human::new(player)),
} }
} }
} }
impl Display for AgentSelector { impl ToString for AgentSelector {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn to_string(&self) -> String {
write!(
f,
"{}",
match self { match self {
Self::Random => "random", Self::Random => "random",
Self::Diffuse => "diffuse", Self::Diffuse => "diffuse",
Self::Chase => "chase", Self::Minimax => "minimax",
Self::Human => "human", Self::Human => "human",
} }
) .to_string()
} }
} }

View File

@ -59,18 +59,11 @@ fn play(
let mut is_first_turn = true; let mut is_first_turn = true;
let mut is_maxi_turn = true; let mut is_maxi_turn = true;
let board_label = format!(
"{}{:6}{}",
color::Fg(color::LightBlack),
maxi.player(),
color::Fg(color::Reset)
);
while !board.is_done() { while !board.is_done() {
// Print board // Print board
println!( println!(
"\r{}{}{}{}", "\r{}{}{}{}",
board_label, " ".repeat(6),
if is_first_turn { '╓' } else { '║' }, if is_first_turn { '╓' } else { '║' },
board.prettyprint()?, board.prettyprint()?,
if is_first_turn { '╖' } else { '║' }, if is_first_turn { '╖' } else { '║' },
@ -106,8 +99,8 @@ fn play(
is_maxi_turn = !is_maxi_turn; is_maxi_turn = !is_maxi_turn;
} }
println!("\r{}{}", board_label, board.prettyprint()?); println!("\r{}{}", " ".repeat(6), board.prettyprint()?);
println!("\r{}{}", board_label, " ".repeat(board.size())); println!("\r{}{}", " ".repeat(6), " ".repeat(board.size()));
Ok(board) Ok(board)
} }
@ -119,8 +112,8 @@ fn main() -> Result<()> {
let mut blue_wins = 0f32; let mut blue_wins = 0f32;
for _ in 0..cli.repeat { for _ in 0..cli.repeat {
let mut maxi = cli.red.get_maximizer(Player::Red); let mut maxi = cli.red.to_maxi(Player::Red);
let mut mini = cli.blue.get_minimizer(Player::Blue); let mut mini = cli.blue.to_mini(Player::Blue);
let red_board = match if cli.silent { let red_board = match if cli.silent {
play_silent(&mut *maxi, &mut *mini) play_silent(&mut *maxi, &mut *mini)
} else { } else {
@ -133,8 +126,8 @@ fn main() -> Result<()> {
} }
}; };
let mut maxi = cli.blue.get_maximizer(Player::Blue); let mut maxi = cli.blue.to_maxi(Player::Blue);
let mut mini = cli.red.get_minimizer(Player::Red); let mut mini = cli.red.to_mini(Player::Red);
let blue_board = match if cli.silent { let blue_board = match if cli.silent {
play_silent(&mut *maxi, &mut *mini) play_silent(&mut *maxi, &mut *mini)
} else { } else {
@ -161,16 +154,16 @@ fn main() -> Result<()> {
println!( println!(
"Red win rate: {:.2} ({})", "Red win rate: {:.2} ({})",
red_wins / cli.repeat as f32, red_wins / cli.repeat as f32,
cli.red, cli.red.to_string(),
); );
println!( println!(
"Blue win rate: {:.2} ({}) ", "Blue win rate: {:.2} ({}) ",
blue_wins / cli.repeat as f32, blue_wins / cli.repeat as f32,
cli.blue, cli.blue.to_string(),
); );
} else { } else {
let mut maxi = cli.red.get_maximizer(Player::Red); let mut maxi = cli.red.to_maxi(Player::Red);
let mut mini = cli.blue.get_minimizer(Player::Blue); let mut mini = cli.blue.to_mini(Player::Blue);
let red_board = if cli.silent { let red_board = if cli.silent {
play_silent(&mut *maxi, &mut *mini) play_silent(&mut *maxi, &mut *mini)
} else { } else {
@ -194,8 +187,8 @@ fn main() -> Result<()> {
return Ok(()); return Ok(());
} }
let mut maxi = cli.blue.get_maximizer(Player::Blue); let mut maxi = cli.blue.to_maxi(Player::Blue);
let mut mini = cli.red.get_minimizer(Player::Red); let mut mini = cli.red.to_mini(Player::Red);
let blue_board = if cli.silent { let blue_board = if cli.silent {
play_silent(&mut *maxi, &mut *mini) play_silent(&mut *maxi, &mut *mini)
} else { } else {
@ -227,7 +220,7 @@ fn main() -> Result<()> {
println!( println!(
"\r\n{}Red ({}){} wins!", "\r\n{}Red ({}){} wins!",
color::Fg(Player::Red.color()), color::Fg(Player::Red.color()),
cli.red, cli.red.to_string(),
color::Fg(color::Reset), color::Fg(color::Reset),
); );
} }
@ -235,7 +228,7 @@ fn main() -> Result<()> {
println!( println!(
"\r\n{}Blue ({}){} wins!", "\r\n{}Blue ({}){} wins!",
color::Fg(Player::Blue.color()), color::Fg(Player::Blue.color()),
cli.blue, cli.blue.to_string(),
color::Fg(color::Reset), color::Fg(color::Reset),
); );
} }

View File

@ -69,7 +69,7 @@ impl Symb {
self == &Self::Minus self == &Self::Minus
} }
pub const fn get_char(&self) -> Option<char> { pub const fn to_char(&self) -> Option<char> {
match self { match self {
Self::Plus => Some('+'), Self::Plus => Some('+'),
Self::Minus => Some('-'), Self::Minus => Some('-'),