Compare commits
7 Commits
7394e9db0b
...
master
Author | SHA1 | Date | |
---|---|---|---|
786e70fe9a
|
|||
00270569db
|
|||
b39c618a2a
|
|||
4ee7f8a9ac
|
|||
89aba4f930
|
|||
e8614dd29f
|
|||
b344ae359b
|
60
Cargo.lock
generated
60
Cargo.lock
generated
@ -120,6 +120,31 @@ version = "1.0.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7"
|
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]]
|
[[package]]
|
||||||
name = "either"
|
name = "either"
|
||||||
version = "1.10.0"
|
version = "1.10.0"
|
||||||
@ -170,22 +195,23 @@ dependencies = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "numtoa"
|
name = "minimax"
|
||||||
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",
|
||||||
"clap",
|
"clap",
|
||||||
"itertools",
|
"itertools",
|
||||||
"rand",
|
"rand",
|
||||||
|
"rayon",
|
||||||
"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"
|
||||||
@ -240,6 +266,26 @@ dependencies = [
|
|||||||
"getrandom",
|
"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]]
|
[[package]]
|
||||||
name = "redox_syscall"
|
name = "redox_syscall"
|
||||||
version = "0.4.1"
|
version = "0.4.1"
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "ops"
|
name = "minimax"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
@ -8,4 +8,5 @@ anyhow = "1.0.80"
|
|||||||
clap = { version = "4.5.1", features = ["derive"] }
|
clap = { version = "4.5.1", features = ["derive"] }
|
||||||
itertools = "0.12.1"
|
itertools = "0.12.1"
|
||||||
rand = "0.8.5"
|
rand = "0.8.5"
|
||||||
|
rayon = "1.9.0"
|
||||||
termion = "3.0.0"
|
termion = "3.0.0"
|
||||||
|
36
README.md
Normal file
36
README.md
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
# 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)
|
||||||
|
- `brutus`: Play against a simple brute-force agent (hard)
|
||||||
|
|
||||||
|
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`.
|
||||||
|
|
||||||
|
Win rates against random are as follows:
|
||||||
|
- `human`: ~100%
|
||||||
|
- `random`: ~50%
|
||||||
|
- `chase`: ~70%
|
||||||
|
- `diffuse`: ~76%
|
||||||
|
- `brutus`: ~90%
|
142
src/agents/brutus.rs
Normal file
142
src/agents/brutus.rs
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
use std::{cmp::Ordering, iter};
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use itertools::Itertools;
|
||||||
|
use rand::{seq::SliceRandom, thread_rng};
|
||||||
|
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 if minimize {
|
||||||
|
Chase::new(self.player).step_min(board)
|
||||||
|
} else {
|
||||||
|
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
|
||||||
|
// Add one if we have two or fewer symbols available, so that we can
|
||||||
|
// account for one unused symbol while keeping a reasonable runtime
|
||||||
|
let n_fill = n_free - symbols.len() + if symbols.len() <= 2 { 1 } else { 0 };
|
||||||
|
|
||||||
|
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 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();
|
||||||
|
|
||||||
|
let mut symbols = symbols.clone();
|
||||||
|
symbols.shuffle(&mut thread_rng());
|
||||||
|
|
||||||
|
// Place a random unused symbol
|
||||||
|
for target_s in symbols {
|
||||||
|
for (i, s) in t.get_board().iter().enumerate() {
|
||||||
|
if let Some(s) = s {
|
||||||
|
if board.get_board()[i].is_none() && target_s == *s {
|
||||||
|
return Ok(PlayerAction { pos: i, symb: *s });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Final escape hatch, if we didn't decide to place any symbols
|
||||||
|
// (which is possible, since we add one to free_spots above!)
|
||||||
|
if minimize {
|
||||||
|
Chase::new(self.player).step_min(board)
|
||||||
|
} else {
|
||||||
|
Chase::new(self.player).step_max(board)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
@ -8,11 +8,11 @@ use crate::{
|
|||||||
util::{Player, Symb},
|
util::{Player, Symb},
|
||||||
};
|
};
|
||||||
|
|
||||||
pub struct SimpleMinimax {
|
pub struct Chase {
|
||||||
player: Player,
|
player: Player,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SimpleMinimax {
|
impl Chase {
|
||||||
pub fn new(player: Player) -> Self {
|
pub fn new(player: Player) -> Self {
|
||||||
Self { player }
|
Self { player }
|
||||||
}
|
}
|
||||||
@ -33,13 +33,13 @@ impl SimpleMinimax {
|
|||||||
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.len() == 0 {
|
if t.is_empty() {
|
||||||
return Random::new(self.player).step_min(board);
|
return Random::new(self.player).step_min(board);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -63,12 +63,10 @@ impl SimpleMinimax {
|
|||||||
} else {
|
} else {
|
||||||
available_numbers[available_numbers.len() - 1 - offset]
|
available_numbers[available_numbers.len() - 1 - offset]
|
||||||
}
|
}
|
||||||
|
} else if val <= 0.0 {
|
||||||
|
available_numbers[offset]
|
||||||
} else {
|
} else {
|
||||||
if val <= 0.0 {
|
available_numbers[available_numbers.len() - 1 - offset]
|
||||||
available_numbers[offset]
|
|
||||||
} else {
|
|
||||||
available_numbers[available_numbers.len() - 1 - offset]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
offset += 1;
|
offset += 1;
|
||||||
@ -81,9 +79,9 @@ impl SimpleMinimax {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Agent for SimpleMinimax {
|
impl Agent for Chase {
|
||||||
fn name(&self) -> &'static str {
|
fn name(&self) -> &'static str {
|
||||||
"Minimax"
|
"Chase"
|
||||||
}
|
}
|
||||||
|
|
||||||
fn player(&self) -> Player {
|
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> {
|
fn step_min(&mut self, board: &Board) -> Result<PlayerAction> {
|
||||||
self.step(board, true)
|
self.step(board, true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MaximizerAgent for SimpleMinimax {
|
impl MaximizerAgent for Chase {
|
||||||
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)
|
||||||
}
|
}
|
@ -1,6 +1,7 @@
|
|||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
use rand::{seq::SliceRandom, thread_rng};
|
||||||
|
|
||||||
use super::{Agent, MaximizerAgent, MinimizerAgent, Random, SimpleMinimax};
|
use super::{Agent, Chase, MaximizerAgent, MinimizerAgent, Random};
|
||||||
use crate::{
|
use crate::{
|
||||||
board::{Board, PlayerAction},
|
board::{Board, PlayerAction},
|
||||||
util::{Player, Symb},
|
util::{Player, Symb},
|
||||||
@ -9,6 +10,7 @@ use crate::{
|
|||||||
/// A simple "operator diffusion" MINIMIZER agent.
|
/// A simple "operator diffusion" MINIMIZER agent.
|
||||||
///
|
///
|
||||||
/// Tries to keep operators as far apart as possible, denying large numbers.
|
/// Tries to keep operators as far apart as possible, denying large numbers.
|
||||||
|
/// Places numbers using the same algorithm as chase.
|
||||||
pub struct Diffuse {
|
pub struct Diffuse {
|
||||||
player: Player,
|
player: Player,
|
||||||
}
|
}
|
||||||
@ -86,30 +88,30 @@ impl Agent for Diffuse {
|
|||||||
|
|
||||||
impl MinimizerAgent for Diffuse {
|
impl MinimizerAgent for Diffuse {
|
||||||
fn step_min(&mut self, board: &Board) -> Result<PlayerAction> {
|
fn step_min(&mut self, board: &Board) -> Result<PlayerAction> {
|
||||||
let symb = [Symb::Minus, Symb::Times, Symb::Plus, Symb::Div]
|
let mut x = [Symb::Minus, Symb::Times, Symb::Plus, Symb::Div];
|
||||||
.iter()
|
x.shuffle(&mut thread_rng());
|
||||||
.find(|x| !board.contains(**x));
|
let symb = x.iter().find(|x| !board.contains(**x));
|
||||||
|
|
||||||
if let Some(symb) = symb {
|
if let Some(symb) = symb {
|
||||||
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
|
||||||
SimpleMinimax::new(self.player).step_min(board)
|
Chase::new(self.player).step_min(board)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MaximizerAgent for Diffuse {
|
impl MaximizerAgent for Diffuse {
|
||||||
fn step_max(&mut self, board: &Board) -> Result<PlayerAction> {
|
fn step_max(&mut self, board: &Board) -> Result<PlayerAction> {
|
||||||
let symb = [Symb::Minus, Symb::Times, Symb::Plus, Symb::Div]
|
let mut x = [Symb::Minus, Symb::Times, Symb::Plus, Symb::Div];
|
||||||
.iter()
|
x.shuffle(&mut thread_rng());
|
||||||
.find(|x| !board.contains(**x));
|
let symb = x.iter().find(|x| !board.contains(**x));
|
||||||
|
|
||||||
if let Some(symb) = symb {
|
if let Some(symb) = symb {
|
||||||
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
|
||||||
SimpleMinimax::new(self.player).step_max(board)
|
Chase::new(self.player).step_max(board)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -35,7 +35,7 @@ impl SymbolSelector {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn up(&mut self, board: &Board) {
|
fn down(&mut self, board: &Board) {
|
||||||
if self.cursor == 0 {
|
if self.cursor == 0 {
|
||||||
self.cursor = self.symbols.len() - 1;
|
self.cursor = self.symbols.len() - 1;
|
||||||
} else {
|
} else {
|
||||||
@ -51,7 +51,7 @@ impl SymbolSelector {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn down(&mut self, board: &Board) {
|
fn up(&mut self, board: &Board) {
|
||||||
if self.cursor == self.symbols.len() - 1 {
|
if self.cursor == self.symbols.len() - 1 {
|
||||||
self.cursor = 0;
|
self.cursor = 0;
|
||||||
} else {
|
} else {
|
||||||
@ -92,14 +92,18 @@ impl Human {
|
|||||||
|
|
||||||
self.symbol_selector.check(board);
|
self.symbol_selector.check(board);
|
||||||
|
|
||||||
|
let board_label = format!(
|
||||||
|
"{}{:<6}{}",
|
||||||
|
color::Fg(self.player.color()),
|
||||||
|
if minimize { "Min" } else { "Max" },
|
||||||
|
color::Fg(color::Reset)
|
||||||
|
);
|
||||||
|
|
||||||
// Ask for input until we get a valid move
|
// Ask for input until we get a valid move
|
||||||
loop {
|
loop {
|
||||||
print!(
|
print!(
|
||||||
"\r{}{}{} ╙{}{}{}{}{}╜ {}",
|
"\r{}╙{}{}{}{}{}╜ {}",
|
||||||
// Goal
|
board_label,
|
||||||
color::Fg(self.player.color()),
|
|
||||||
if minimize { "Min" } else { "Max" },
|
|
||||||
color::Fg(color::Reset),
|
|
||||||
// Cursor
|
// Cursor
|
||||||
" ".repeat(self.cursor),
|
" ".repeat(self.cursor),
|
||||||
color::Fg(self.player.color()),
|
color::Fg(self.player.color()),
|
||||||
@ -156,7 +160,7 @@ impl Human {
|
|||||||
self.symbol_selector.down(board);
|
self.symbol_selector.down(board);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
Key::Char('\n') => {
|
Key::Char(' ') | Key::Char('\n') => {
|
||||||
let symb = Symb::from_char(self.symbol_selector.current());
|
let symb = Symb::from_char(self.symbol_selector.current());
|
||||||
if let Some(symb) = symb {
|
if let Some(symb) = symb {
|
||||||
let action = PlayerAction {
|
let action = PlayerAction {
|
||||||
@ -191,7 +195,7 @@ impl Human {
|
|||||||
|
|
||||||
impl Agent for Human {
|
impl Agent for Human {
|
||||||
fn name(&self) -> &'static str {
|
fn name(&self) -> &'static str {
|
||||||
"Player"
|
"Human"
|
||||||
}
|
}
|
||||||
|
|
||||||
fn player(&self) -> Player {
|
fn player(&self) -> Player {
|
||||||
|
@ -1,12 +1,14 @@
|
|||||||
|
mod brutus;
|
||||||
|
mod chase;
|
||||||
mod diffuse;
|
mod diffuse;
|
||||||
mod human;
|
mod human;
|
||||||
mod minimax;
|
|
||||||
mod random;
|
mod random;
|
||||||
pub mod util;
|
pub mod util;
|
||||||
|
|
||||||
|
pub use brutus::Brutus;
|
||||||
|
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::{
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use std::num::NonZeroU8;
|
use std::{mem::swap, num::NonZeroU8};
|
||||||
|
|
||||||
use crate::{board::Board, util::Symb};
|
use crate::{board::Board, util::Symb};
|
||||||
|
|
||||||
/// Returns an iterator of (sort, coords, char_idx, f32) for each empty slot in the listed partials.
|
/// Returns an iterator of (idx, coords, char_idx, f32) for each empty slot in the listed partials.
|
||||||
/// - sort is the index of this slot.
|
/// - idx is the index of this slot in the board string.
|
||||||
/// - coords are the coordinate of this slot's partial
|
/// - coords are the coordinate of this slot's partial
|
||||||
/// - char_idx is the index of this slot in its partial
|
/// - char_idx is the index of this slot in its partial
|
||||||
/// - f32 is the influence of this slot
|
/// - f32 is the influence of this slot
|
||||||
@ -51,16 +51,12 @@ pub fn free_slots_by_influence(board: &Board) -> Option<Vec<(usize, f32)>> {
|
|||||||
Some(slots)
|
Some(slots)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Find the maximum possible value of the given board
|
/// Find the minimum or maximum possible value of the given board,
|
||||||
#[allow(dead_code)]
|
/// without adding any operations. Returns None if we couldn't find
|
||||||
pub fn maximize_value(board: &Board) -> Option<Board> {
|
/// a best board.
|
||||||
|
pub fn best_board_noop(board: &Board, minimize: bool) -> Option<Board> {
|
||||||
let n_free = board.get_board().iter().filter(|x| x.is_none()).count();
|
let n_free = board.get_board().iter().filter(|x| x.is_none()).count();
|
||||||
|
|
||||||
// Assume we have 10 or fewer available slots
|
|
||||||
if n_free >= 10 {
|
|
||||||
panic!()
|
|
||||||
}
|
|
||||||
|
|
||||||
let available_numbers = (0..=9)
|
let available_numbers = (0..=9)
|
||||||
.map(|x| match x {
|
.map(|x| match x {
|
||||||
0 => Symb::Zero,
|
0 => Symb::Zero,
|
||||||
@ -69,19 +65,36 @@ 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)?;
|
if n_free > available_numbers.len() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
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.
|
let mut a = {
|
||||||
let neg_count = slots.iter().filter(|(_, x)| *x <= 0.0).count();
|
// Number of slots we want to minimize
|
||||||
let pos_count = slots.iter().filter(|(_, x)| *x > 0.0).count();
|
let mut neg_count = slots.iter().filter(|(_, x)| *x <= 0.0).count();
|
||||||
|
// Number of slots we want to maximize
|
||||||
|
let mut pos_count = slots.iter().filter(|(_, x)| *x > 0.0).count();
|
||||||
|
|
||||||
let mut a_iter = available_numbers
|
if minimize {
|
||||||
.iter()
|
swap(&mut neg_count, &mut pos_count);
|
||||||
.take(neg_count)
|
}
|
||||||
.chain(available_numbers.iter().rev().take(pos_count).rev());
|
|
||||||
|
|
||||||
let mut g = slots
|
available_numbers
|
||||||
|
.iter()
|
||||||
|
.take(neg_count)
|
||||||
|
.chain(available_numbers.iter().rev().take(pos_count).rev())
|
||||||
|
.collect_vec()
|
||||||
|
};
|
||||||
|
|
||||||
|
if !minimize {
|
||||||
|
a.reverse();
|
||||||
|
}
|
||||||
|
let mut a_iter = a.into_iter();
|
||||||
|
|
||||||
|
slots
|
||||||
// Group slots with equal weights
|
// Group slots with equal weights
|
||||||
// and count the number of elements in each group
|
// and count the number of elements in each group
|
||||||
.iter()
|
.iter()
|
||||||
@ -92,7 +105,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().clone())
|
.map(|_| *a_iter.next().unwrap())
|
||||||
.permutations(s)
|
.permutations(s)
|
||||||
.unique()
|
.unique()
|
||||||
.collect_vec()
|
.collect_vec()
|
||||||
@ -101,37 +114,32 @@ pub fn maximize_value(board: &Board) -> Option<Board> {
|
|||||||
// of this set of sets
|
// of this set of sets
|
||||||
.multi_cartesian_product()
|
.multi_cartesian_product()
|
||||||
.map(|x| x.iter().flatten().cloned().collect_vec())
|
.map(|x| x.iter().flatten().cloned().collect_vec())
|
||||||
.map(|v| slots.iter().zip(v).collect_vec())
|
// Finally, attach the coordinate of each slot to each symbol
|
||||||
.collect_vec();
|
.map(|v| slots.iter().map(|x| x.0).zip(v).collect_vec())
|
||||||
|
.collect_vec()
|
||||||
// Sort these vectors so the order of values
|
|
||||||
// matches the order of empty slots
|
|
||||||
g.iter_mut()
|
|
||||||
.for_each(|v| v.sort_by(|(a, _), (b, _)| b.0.partial_cmp(&a.0).unwrap()));
|
|
||||||
g.into_iter()
|
|
||||||
.map(|v| v.into_iter().map(|(_, s)| s).collect_vec())
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut best_board = None;
|
let mut best_board = None;
|
||||||
let mut best_value = None;
|
let mut best_value = None;
|
||||||
for i in all_symbols {
|
for i_iter in all_symbols {
|
||||||
let mut i_iter = i.iter();
|
let mut tmp_board = board.clone();
|
||||||
let filled = Board::from_board(board.get_board().map(|x| match x {
|
for (i, s) in i_iter {
|
||||||
None => i_iter.next().cloned(),
|
tmp_board.get_board_mut()[i] = Some(s);
|
||||||
_ => x,
|
}
|
||||||
}));
|
|
||||||
|
|
||||||
let val = filled.evaluate();
|
let val = tmp_board.evaluate();
|
||||||
|
|
||||||
if let Some(val) = val {
|
if let Some(val) = val {
|
||||||
if let Some(best) = best_value {
|
if minimize {
|
||||||
if val > best {
|
if best_value.is_none() || val < best_value.unwrap() {
|
||||||
best_value = Some(val);
|
best_value = Some(val);
|
||||||
best_board = Some(filled)
|
best_board = Some(tmp_board)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
best_value = Some(val);
|
if best_value.is_none() || val > best_value.unwrap() {
|
||||||
best_board = Some(filled)
|
best_value = Some(val);
|
||||||
|
best_board = Some(tmp_board)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use std::fmt::Write;
|
use std::fmt::{Debug, Display, Write};
|
||||||
use termion::color::{self, Color};
|
use termion::color::{self, Color};
|
||||||
|
|
||||||
use super::{PlayerAction, TreeElement};
|
use super::{PlayerAction, TreeElement};
|
||||||
@ -65,14 +65,18 @@ pub struct Board {
|
|||||||
last_placed: Option<usize>,
|
last_placed: Option<usize>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ToString for Board {
|
impl Display for Board {
|
||||||
fn to_string(&self) -> String {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
let mut s = String::new();
|
for c in self.board {
|
||||||
s.extend(
|
write!(f, "{}", c.map(|s| s.get_char().unwrap()).unwrap_or('_'))?
|
||||||
self.board
|
}
|
||||||
.map(|x| x.map(|s| s.to_char().unwrap()).unwrap_or('_')),
|
Ok(())
|
||||||
);
|
}
|
||||||
s
|
}
|
||||||
|
|
||||||
|
impl Debug for Board {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
Display::fmt(&self, f)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -95,6 +99,24 @@ impl Board {
|
|||||||
&mut self.board
|
&mut self.board
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get the index of the ith empty slot
|
||||||
|
pub fn ith_empty_slot(&self, mut idx: usize) -> Option<usize> {
|
||||||
|
for (i, c) in self.board.iter().enumerate() {
|
||||||
|
if c.is_none() {
|
||||||
|
if idx == 0 {
|
||||||
|
return Some(i);
|
||||||
|
}
|
||||||
|
idx -= 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if idx == 0 {
|
||||||
|
Some(self.board.len() - 1)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn is_done(&self) -> bool {
|
pub fn is_done(&self) -> bool {
|
||||||
self.free_spots == 0
|
self.free_spots == 0
|
||||||
}
|
}
|
||||||
@ -369,7 +391,7 @@ impl Board {
|
|||||||
if c == '_' {
|
if c == '_' {
|
||||||
Some(None)
|
Some(None)
|
||||||
} else {
|
} else {
|
||||||
Symb::from_char(c).map(|s| Some(s))
|
Symb::from_char(c).map(Some)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
64
src/cli.rs
64
src/cli.rs
@ -1,59 +1,79 @@
|
|||||||
|
use std::fmt::Display;
|
||||||
|
|
||||||
use clap::{Parser, ValueEnum};
|
use clap::{Parser, ValueEnum};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
agents::{Diffuse, Human, MaximizerAgent, MinimizerAgent, Random, SimpleMinimax},
|
agents::{Brutus, Chase, Diffuse, Human, MaximizerAgent, MinimizerAgent, Random},
|
||||||
util::Player,
|
util::Player,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Parser, Debug)]
|
#[derive(Parser, Debug)]
|
||||||
#[command(version, about)]
|
#[command(about)]
|
||||||
pub struct Cli {
|
pub struct Cli {
|
||||||
pub red: AgentSelector,
|
/// The agent that controls the Blue (opponent) player
|
||||||
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.
|
||||||
#[arg(long, short, default_value = "0")]
|
#[arg(long, short, default_value = "0")]
|
||||||
pub repeat: usize,
|
pub repeat: usize,
|
||||||
|
|
||||||
#[arg(long, short, default_value = "false")]
|
|
||||||
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 {
|
||||||
Random,
|
/// A human agent. Asks for input.
|
||||||
Diffuse,
|
|
||||||
Minimax,
|
|
||||||
Human,
|
Human,
|
||||||
|
|
||||||
|
/// A random agent (very easy)
|
||||||
|
Random,
|
||||||
|
|
||||||
|
/// A simple extremum-chaser (easy)
|
||||||
|
Chase,
|
||||||
|
|
||||||
|
/// A smarter extremum-chaser (medium)
|
||||||
|
Diffuse,
|
||||||
|
|
||||||
|
/// A very smart brute-force agent (hard)
|
||||||
|
Brutus,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AgentSelector {
|
impl AgentSelector {
|
||||||
pub fn to_maxi(&self, player: Player) -> Box<dyn MaximizerAgent> {
|
pub fn get_maximizer(&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::Minimax => Box::new(SimpleMinimax::new(player)),
|
Self::Chase => Box::new(Chase::new(player)),
|
||||||
Self::Diffuse => Box::new(Diffuse::new(player)),
|
Self::Diffuse => Box::new(Diffuse::new(player)),
|
||||||
|
Self::Brutus => Box::new(Brutus::new(player)),
|
||||||
Self::Human => Box::new(Human::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 {
|
match self {
|
||||||
Self::Random => Box::new(Random::new(player)),
|
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::Diffuse => Box::new(Diffuse::new(player)),
|
||||||
|
Self::Brutus => Box::new(Brutus::new(player)),
|
||||||
Self::Human => Box::new(Human::new(player)),
|
Self::Human => Box::new(Human::new(player)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ToString for AgentSelector {
|
impl Display for AgentSelector {
|
||||||
fn to_string(&self) -> String {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
match self {
|
write!(
|
||||||
Self::Random => "random",
|
f,
|
||||||
Self::Diffuse => "diffuse",
|
"{}",
|
||||||
Self::Minimax => "minimax",
|
match self {
|
||||||
Self::Human => "human",
|
Self::Random => "random",
|
||||||
}
|
Self::Diffuse => "diffuse",
|
||||||
.to_string()
|
Self::Brutus => "brutus",
|
||||||
|
Self::Chase => "chase",
|
||||||
|
Self::Human => "human",
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
129
src/main.rs
129
src/main.rs
@ -1,7 +1,11 @@
|
|||||||
use std::cmp::Ordering;
|
use std::{
|
||||||
|
cmp::Ordering,
|
||||||
|
io::{stdout, Write},
|
||||||
|
};
|
||||||
|
|
||||||
use anyhow::{bail, Result};
|
use anyhow::{bail, Result};
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
|
use rayon::iter::{IntoParallelIterator, ParallelIterator};
|
||||||
use termion::color::{self};
|
use termion::color::{self};
|
||||||
|
|
||||||
mod agents;
|
mod agents;
|
||||||
@ -59,17 +63,27 @@ 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().to_string(),
|
||||||
|
color::Fg(color::Reset)
|
||||||
|
);
|
||||||
|
|
||||||
while !board.is_done() {
|
while !board.is_done() {
|
||||||
// Print board
|
// Print board
|
||||||
println!(
|
println!(
|
||||||
"\r{}{}{}{}",
|
"\r{}{}{}{}",
|
||||||
" ".repeat(6),
|
board_label,
|
||||||
if is_first_turn { '╓' } else { '║' },
|
if is_first_turn { '╓' } else { '║' },
|
||||||
board.prettyprint()?,
|
board.prettyprint()?,
|
||||||
if is_first_turn { '╖' } else { '║' },
|
if is_first_turn { '╖' } else { '║' },
|
||||||
);
|
);
|
||||||
is_first_turn = false;
|
is_first_turn = false;
|
||||||
|
|
||||||
|
print!("Thinking...");
|
||||||
|
stdout().flush()?;
|
||||||
|
|
||||||
// Take action
|
// Take action
|
||||||
let action = if is_maxi_turn {
|
let action = if is_maxi_turn {
|
||||||
maxi.step_max(&board)?
|
maxi.step_max(&board)?
|
||||||
@ -99,76 +113,73 @@ fn play(
|
|||||||
is_maxi_turn = !is_maxi_turn;
|
is_maxi_turn = !is_maxi_turn;
|
||||||
}
|
}
|
||||||
|
|
||||||
println!("\r{}║{}║", " ".repeat(6), board.prettyprint()?);
|
println!("\r{}║{}║", board_label, board.prettyprint()?);
|
||||||
println!("\r{}╙{}╜", " ".repeat(6), " ".repeat(board.size()));
|
println!("\r{}╙{}╜", board_label, " ".repeat(board.size()));
|
||||||
Ok(board)
|
Ok(board)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() -> Result<()> {
|
fn main() -> Result<()> {
|
||||||
let cli = cli::Cli::parse();
|
let cli = cli::Cli::parse();
|
||||||
|
|
||||||
|
rayon::ThreadPoolBuilder::new()
|
||||||
|
.num_threads(8)
|
||||||
|
.build_global()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
if cli.repeat > 1 {
|
if cli.repeat > 1 {
|
||||||
let mut red_wins = 0f32;
|
let x = (0..cli.repeat)
|
||||||
let mut blue_wins = 0f32;
|
.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.blue.get_maximizer(Player::Blue);
|
||||||
let mut maxi = cli.red.to_maxi(Player::Red);
|
let mut mini = cli.red.get_minimizer(Player::Red);
|
||||||
let mut mini = cli.blue.to_mini(Player::Blue);
|
let blue_board = match play_silent(&mut *maxi, &mut *mini) {
|
||||||
let red_board = match if cli.silent {
|
Ok(x) => x,
|
||||||
play_silent(&mut *maxi, &mut *mini)
|
Err(e) => {
|
||||||
} else {
|
println!("Error: {e}");
|
||||||
play(&mut *maxi, &mut *mini)
|
return None;
|
||||||
} {
|
}
|
||||||
Ok(x) => x,
|
};
|
||||||
Err(e) => {
|
|
||||||
println!("Error: {e}");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut maxi = cli.blue.to_maxi(Player::Blue);
|
match red_board.evaluate().partial_cmp(&blue_board.evaluate()) {
|
||||||
let mut mini = cli.red.to_mini(Player::Red);
|
Some(Ordering::Equal) => None,
|
||||||
let blue_board = match if cli.silent {
|
Some(Ordering::Greater) => Some(Player::Red),
|
||||||
play_silent(&mut *maxi, &mut *mini)
|
Some(Ordering::Less) => Some(Player::Blue),
|
||||||
} else {
|
None => {
|
||||||
play(&mut *maxi, &mut *mini)
|
println!("Error");
|
||||||
} {
|
None
|
||||||
Ok(x) => x,
|
}
|
||||||
Err(e) => {
|
|
||||||
println!("Error: {e}");
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
};
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
match red_board.evaluate().partial_cmp(&blue_board.evaluate()) {
|
let red_wins = x.iter().filter(|x| **x == Player::Red).count();
|
||||||
Some(Ordering::Equal) => {}
|
let blue_wins = x.iter().filter(|x| **x == Player::Blue).count();
|
||||||
Some(Ordering::Greater) => red_wins += 1.0,
|
|
||||||
Some(Ordering::Less) => blue_wins += 1.0,
|
|
||||||
None => {
|
|
||||||
println!("Error");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
println!("Played {} rounds\n", cli.repeat);
|
println!("Played {} rounds\n", cli.repeat);
|
||||||
println!(
|
println!(
|
||||||
"Red win rate: {:.2} ({})",
|
"Red win rate: {:.2} ({})",
|
||||||
red_wins / cli.repeat as f32,
|
red_wins as f32 / cli.repeat as f32,
|
||||||
cli.red.to_string(),
|
cli.red,
|
||||||
);
|
);
|
||||||
println!(
|
println!(
|
||||||
"Blue win rate: {:.2} ({}) ",
|
"Blue win rate: {:.2} ({}) ",
|
||||||
blue_wins / cli.repeat as f32,
|
blue_wins as f32 / cli.repeat as f32,
|
||||||
cli.blue.to_string(),
|
cli.blue,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
let mut maxi = cli.red.to_maxi(Player::Red);
|
let mut maxi = cli.red.get_maximizer(Player::Red);
|
||||||
let mut mini = cli.blue.to_mini(Player::Blue);
|
let mut mini = cli.blue.get_minimizer(Player::Blue);
|
||||||
let red_board = if cli.silent {
|
let red_board = play(&mut *maxi, &mut *mini)?;
|
||||||
play_silent(&mut *maxi, &mut *mini)
|
|
||||||
} else {
|
|
||||||
play(&mut *maxi, &mut *mini)
|
|
||||||
}?;
|
|
||||||
|
|
||||||
if red_board.is_done() {
|
if red_board.is_done() {
|
||||||
println!(
|
println!(
|
||||||
@ -187,13 +198,9 @@ fn main() -> Result<()> {
|
|||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut maxi = cli.blue.to_maxi(Player::Blue);
|
let mut maxi = cli.blue.get_maximizer(Player::Blue);
|
||||||
let mut mini = cli.red.to_mini(Player::Red);
|
let mut mini = cli.red.get_minimizer(Player::Red);
|
||||||
let blue_board = if cli.silent {
|
let blue_board = play(&mut *maxi, &mut *mini)?;
|
||||||
play_silent(&mut *maxi, &mut *mini)
|
|
||||||
} else {
|
|
||||||
play(&mut *maxi, &mut *mini)
|
|
||||||
}?;
|
|
||||||
|
|
||||||
if blue_board.is_done() {
|
if blue_board.is_done() {
|
||||||
println!(
|
println!(
|
||||||
@ -220,7 +227,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.to_string(),
|
cli.red,
|
||||||
color::Fg(color::Reset),
|
color::Fg(color::Reset),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -228,7 +235,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.to_string(),
|
cli.blue,
|
||||||
color::Fg(color::Reset),
|
color::Fg(color::Reset),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -69,7 +69,7 @@ impl Symb {
|
|||||||
self == &Self::Minus
|
self == &Self::Minus
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const fn to_char(&self) -> Option<char> {
|
pub const fn get_char(&self) -> Option<char> {
|
||||||
match self {
|
match self {
|
||||||
Self::Plus => Some('+'),
|
Self::Plus => Some('+'),
|
||||||
Self::Minus => Some('-'),
|
Self::Minus => Some('-'),
|
||||||
|
Reference in New Issue
Block a user