master
Mark 2024-03-05 14:35:26 -08:00
parent b344ae359b
commit e8614dd29f
Signed by: Mark
GPG Key ID: C6D63995FE72FD80
11 changed files with 70 additions and 69 deletions

14
Cargo.lock generated
View File

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

View File

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

View File

@ -1,4 +1,4 @@
# Symbol Strike
# Minimax
## Rules
@ -20,7 +20,7 @@ A board's syntax must always be valid. Namely, the following rules are enforced:
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)
- `minimax`: Play against a simple extremum-chasing agent (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.

View File

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

View File

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

View File

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

View File

@ -69,7 +69,7 @@ pub fn maximize_value(board: &Board) -> Option<Board> {
.filter(|x| !board.contains(*x))
.collect::<Vec<_>>();
let slots = free_slots_by_influence(&board)?;
let slots = free_slots_by_influence(board)?;
let all_symbols = {
// 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
.map(|s| {
(0..s)
.map(|_| a_iter.next().unwrap().clone())
.map(|_| *a_iter.next().unwrap())
.permutations(s)
.unique()
.collect_vec()

View File

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

View File

@ -1,7 +1,9 @@
use std::fmt::Display;
use clap::{Parser, ValueEnum};
use crate::{
agents::{Diffuse, Human, MaximizerAgent, MinimizerAgent, Random, SimpleMinimax},
agents::{Chase, Diffuse, Human, MaximizerAgent, MinimizerAgent, Random},
util::Player,
};
@ -35,40 +37,43 @@ pub enum AgentSelector {
Random,
/// A simple extremum-chaser (easy)
Minimax,
Chase,
/// A smarter extremum-chaser (medium)
Diffuse,
}
impl AgentSelector {
pub fn to_maxi(&self, player: Player) -> Box<dyn MaximizerAgent> {
pub fn get_maximizer(&self, player: Player) -> Box<dyn MaximizerAgent> {
match self {
Self::Random => Box::new(Random::new(player)),
Self::Minimax => Box::new(SimpleMinimax::new(player)),
Self::Chase => Box::new(Chase::new(player)),
Self::Diffuse => Box::new(Diffuse::new(player)),
Self::Human => Box::new(Human::new(player)),
}
}
pub fn to_mini(&self, player: Player) -> Box<dyn MinimizerAgent> {
pub fn get_minimizer(&self, player: Player) -> Box<dyn MinimizerAgent> {
match self {
Self::Random => Box::new(Random::new(player)),
Self::Minimax => Box::new(SimpleMinimax::new(player)),
Self::Chase => Box::new(Chase::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()
impl Display for AgentSelector {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
match self {
Self::Random => "random",
Self::Diffuse => "diffuse",
Self::Chase => "chase",
Self::Human => "human",
}
)
}
}

View File

@ -62,7 +62,7 @@ fn play(
let board_label = format!(
"{}{:6}{}",
color::Fg(color::LightBlack),
maxi.player().to_string(),
maxi.player(),
color::Fg(color::Reset)
);
@ -119,8 +119,8 @@ fn main() -> Result<()> {
let mut blue_wins = 0f32;
for _ in 0..cli.repeat {
let mut maxi = cli.red.to_maxi(Player::Red);
let mut mini = cli.blue.to_mini(Player::Blue);
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 {
@ -133,8 +133,8 @@ fn main() -> Result<()> {
}
};
let mut maxi = cli.blue.to_maxi(Player::Blue);
let mut mini = cli.red.to_mini(Player::Red);
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 {
@ -161,16 +161,16 @@ fn main() -> Result<()> {
println!(
"Red win rate: {:.2} ({})",
red_wins / cli.repeat as f32,
cli.red.to_string(),
cli.red,
);
println!(
"Blue win rate: {:.2} ({}) ",
blue_wins / cli.repeat as f32,
cli.blue.to_string(),
cli.blue,
);
} else {
let mut maxi = cli.red.to_maxi(Player::Red);
let mut mini = cli.blue.to_mini(Player::Blue);
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 {
@ -194,8 +194,8 @@ fn main() -> Result<()> {
return Ok(());
}
let mut maxi = cli.blue.to_maxi(Player::Blue);
let mut mini = cli.red.to_mini(Player::Red);
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 {
@ -227,7 +227,7 @@ fn main() -> Result<()> {
println!(
"\r\n{}Red ({}){} wins!",
color::Fg(Player::Red.color()),
cli.red.to_string(),
cli.red,
color::Fg(color::Reset),
);
}
@ -235,7 +235,7 @@ fn main() -> Result<()> {
println!(
"\r\n{}Blue ({}){} wins!",
color::Fg(Player::Blue.color()),
cli.blue.to_string(),
cli.blue,
color::Fg(color::Reset),
);
}

View File

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