Compare commits

...

6 Commits

Author SHA1 Message Date
0dfc3f4b26 Rhai 2025-11-01 10:02:41 -07:00
786e70fe9a brutus tweaks 2024-03-06 10:38:02 -08:00
00270569db Minor edit 2024-03-06 10:36:04 -08:00
b39c618a2a Added brutus agent 2024-03-06 09:56:04 -08:00
4ee7f8a9ac Minor cleanup & utilities 2024-03-06 09:55:47 -08:00
89aba4f930 Cleanup 2024-03-05 14:58:36 -08:00
20 changed files with 1333 additions and 426 deletions

1
.gitignore vendored
View File

@@ -1 +1,2 @@
/target
.DS_Store

323
Cargo.lock generated
View File

@@ -1,6 +1,26 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
version = 4
[[package]]
name = "ahash"
version = "0.8.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75"
dependencies = [
"cfg-if",
"const-random",
"getrandom 0.3.4",
"once_cell",
"version_check",
"zerocopy",
]
[[package]]
name = "allocator-api2"
version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
[[package]]
name = "anstream"
@@ -56,6 +76,12 @@ version = "1.0.80"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ad32ce52e4161730f7098c077cd2ed6229b5804ccf99e5366be1ab72a98b4e1"
[[package]]
name = "autocfg"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
[[package]]
name = "bitflags"
version = "1.3.2"
@@ -120,12 +146,84 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7"
[[package]]
name = "const-random"
version = "0.1.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359"
dependencies = [
"const-random-macro",
]
[[package]]
name = "const-random-macro"
version = "0.1.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e"
dependencies = [
"getrandom 0.2.12",
"once_cell",
"tiny-keccak",
]
[[package]]
name = "core-error"
version = "0.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "efcdb2972eb64230b4c50646d8498ff73f5128d196a90c7236eec4cbe8619b8f"
dependencies = [
"version_check",
]
[[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 = "crunchy"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
[[package]]
name = "either"
version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a"
[[package]]
name = "equivalent"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
[[package]]
name = "foldhash"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
[[package]]
name = "getrandom"
version = "0.2.12"
@@ -137,12 +235,44 @@ dependencies = [
"wasi",
]
[[package]]
name = "getrandom"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
dependencies = [
"cfg-if",
"libc",
"r-efi",
"wasip2",
]
[[package]]
name = "hashbrown"
version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d"
dependencies = [
"allocator-api2",
"equivalent",
"foldhash",
]
[[package]]
name = "heck"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
[[package]]
name = "instant"
version = "0.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222"
dependencies = [
"cfg-if",
]
[[package]]
name = "itertools"
version = "0.12.1"
@@ -154,9 +284,15 @@ dependencies = [
[[package]]
name = "libc"
version = "0.2.153"
version = "0.2.177"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd"
checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976"
[[package]]
name = "libm"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de"
[[package]]
name = "libredox"
@@ -177,15 +313,51 @@ dependencies = [
"clap",
"itertools",
"rand",
"rayon",
"rhai",
"termion",
]
[[package]]
name = "no-std-compat"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b93853da6d84c2e3c7d730d6473e8817692dd89be387eb01b94d7f108ecb5b8c"
dependencies = [
"spin",
]
[[package]]
name = "num-traits"
version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
dependencies = [
"autocfg",
"libm",
]
[[package]]
name = "numtoa"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8f8bdf33df195859076e54ab11ee78a1b208382d3a26ec40d142ffc1ecc49ef"
[[package]]
name = "once_cell"
version = "1.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
dependencies = [
"portable-atomic",
]
[[package]]
name = "portable-atomic"
version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483"
[[package]]
name = "ppv-lite86"
version = "0.2.17"
@@ -210,6 +382,12 @@ dependencies = [
"proc-macro2",
]
[[package]]
name = "r-efi"
version = "5.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
[[package]]
name = "rand"
version = "0.8.5"
@@ -237,7 +415,27 @@ version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
dependencies = [
"getrandom",
"getrandom 0.2.12",
]
[[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]]
@@ -255,6 +453,67 @@ version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "20145670ba436b55d91fc92d25e71160fbfbdd57831631c8d7d36377a476f1cb"
[[package]]
name = "rhai"
version = "1.23.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "527390cc333a8d2cd8237890e15c36518c26f8b54c903d86fc59f42f08d25594"
dependencies = [
"ahash",
"bitflags 2.4.2",
"core-error",
"hashbrown",
"instant",
"libm",
"no-std-compat",
"num-traits",
"once_cell",
"rhai_codegen",
"smallvec",
"smartstring",
"thin-vec",
]
[[package]]
name = "rhai_codegen"
version = "3.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4322a2a4e8cf30771dd9f27f7f37ca9ac8fe812dddd811096a98483080dabe6"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "smallvec"
version = "1.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
[[package]]
name = "smartstring"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fb72c633efbaa2dd666986505016c32c3044395ceaf881518399d2f4127ee29"
dependencies = [
"autocfg",
"static_assertions",
"version_check",
]
[[package]]
name = "spin"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d"
[[package]]
name = "static_assertions"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
[[package]]
name = "strsim"
version = "0.11.0"
@@ -284,6 +543,21 @@ dependencies = [
"redox_termios",
]
[[package]]
name = "thin-vec"
version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "144f754d318415ac792f9d69fc87abbbfc043ce2ef041c60f16ad828f638717d"
[[package]]
name = "tiny-keccak"
version = "2.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237"
dependencies = [
"crunchy",
]
[[package]]
name = "unicode-ident"
version = "1.0.12"
@@ -296,12 +570,27 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
[[package]]
name = "version_check"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "wasi"
version = "0.11.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]]
name = "wasip2"
version = "1.0.1+wasi-0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7"
dependencies = [
"wit-bindgen",
]
[[package]]
name = "windows-sys"
version = "0.52.0"
@@ -367,3 +656,29 @@ name = "windows_x86_64_msvc"
version = "0.52.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8"
[[package]]
name = "wit-bindgen"
version = "0.46.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59"
[[package]]
name = "zerocopy"
version = "0.8.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.8.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831"
dependencies = [
"proc-macro2",
"quote",
"syn",
]

View File

@@ -8,4 +8,14 @@ anyhow = "1.0.80"
clap = { version = "4.5.1", features = ["derive"] }
itertools = "0.12.1"
rand = "0.8.5"
rayon = "1.9.0"
rhai = { version = "1.23.4", features = [
"sync",
"no_time",
"no_module",
"no_custom_syntax",
"no_std",
"only_i64",
"f32_float",
] }
termion = "3.0.0"

View File

@@ -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.
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%

133
src/agents.rhai/chase.rhai Normal file
View File

@@ -0,0 +1,133 @@
fn random_action(board) {
let symb = rand_symb();
let pos = rand_int(0, 10);
let action = Action(symb, pos);
while !board.can_play(action) {
let symb = rand_symb();
let pos = rand_int(0, 10);
action = Action(symb, pos);
}
return action
}
/// Returns an array of (idx, influence) for each empty slot in the board.
/// - idx is the index of this slot
/// - f32 is the influence of this slot
fn free_slots_by_influence(board) {
// TODO: EDGE CASE
// We fail if we have ___/-_ (div by zero)
// Fill all empty slots with fives and compute starting value
//
// This should always result in an evaluatable expression,
// since parenthesis do not exist.
// The only way to divide by zero is by doing something like /(5-2+3).
let filled = board;
for i in filled.free_spots_idx() {
filled[i] = 5;
}
let base = filled.evaluate();
// Test each slot:
// Increase its value by 1, and record the effect on the
// expression's total value.
// This isn't a perfect metric, but it's pretty good.
let slots = [];
for i in 0..board.size() {
let slot = board[i];
if slot != "" {
continue
}
let b = filled;
b[i] = 6;
if b.evaluate() == () {
print(b)
}
slots.push([i, b.evaluate() - base]);
}
slots.sort(|a, b| b[0].abs() - a[0].abs());
return slots;
}
// Main step function (shared between min and max)
fn chase_step(board, minimize) {
let available_numbers = {
let available = [];
for i in 0..10 {
if !board.contains(i) {
available.push(i);
}
}
available
};
// For the code below, we must guarantee that
// min_slots + max_slots <= available_numbers.len
let n_free = board.free_spots();
if available_numbers.len() < n_free || n_free >= 10 {
return random_action(board);
}
let slots = free_slots_by_influence(board);
if slots.len() == 0 {
return random_action(board);
}
// Get the most influential position
let pos = slots[0][0];
let val = slots[0][1];
// Choose next number if we can't make the move.
// Prevents division by zero.
// This isn't perfect, and may fail if we run out of numbers
// (This is, however, very unlikely)
let selected_symbol = ();
let offset = 0;
while selected_symbol == () || offset < available_numbers.len() {
selected_symbol = {
if minimize {
if val >= 0.0 {
available_numbers[offset]
} else {
available_numbers[available_numbers.len() - 1 - offset]
}
} else {
if val <= 0.0 {
available_numbers[offset]
} else {
available_numbers[available_numbers.len() - 1 - offset]
}
}
};
let action = Action(selected_symbol, pos);
if board.can_play(action) {
return action;
}
offset += 1;
}
// Fallback to random if we can't find a valid move
return random_action(board);
}
// Minimizer step
fn step_min(board) {
chase_step(board, true)
}
// Maximizer step
fn step_max(board) {
chase_step(board, false)
}

View File

@@ -0,0 +1,109 @@
fn random_action(board) {
let symb = rand_symb();
let pos = rand_int(0, 10);
let action = Action(symb, pos);
while !board.can_play(action) {
let symb = rand_symb();
let pos = rand_int(0, 10);
action = Action(symb, pos);
}
return action
}
fn step_symb(board, symb) {
if board.contains(symb) {
print("ERROR: called `step_symb` with a symbol that's already on the board!");
return random_action(board);
}
let board_size = board.size();
let dist = [];
// Initialize distance array with large values (board size + 1)
for i in 0..11 {
dist.push(board_size + 1);
}
// Set boundary conditions
dist[0] = 1;
dist[10] = 1;
// Set distances to 0 for positions with operators
for i in 0..11 {
let cell = board[i];
if cell != () && cell.is_op() {
dist[i] = 0;
}
}
// Diffusion algorithm - propagate distances
let did_something = true;
while did_something {
did_something = false;
for i in 1..10 {
let left_dist = dist[i - 1];
let right_dist = dist[i + 1];
let new_dist = min(left_dist + 1, right_dist + 1);
if new_dist < dist[i] {
did_something = true;
dist[i] = new_dist;
}
}
}
// Find maximum distance
let max_dist = 0;
for d in dist {
if d > max_dist {
max_dist = d;
}
}
// Try to place at positions with maximum distance
loop {
for pos in 0..11 {
if dist[pos] >= max_dist {
let action = Action(symb, pos);
if board.can_play(action) {
return action;
}
}
}
if max_dist == 0 {
return random_action(board);
}
max_dist -= 1;
}
}
fn step_min(board) {
let operators = ["+", "-", "*", "/"];
operators = rand_shuffle(operators);
for op in operators {
if !board.contains(op) {
return step_symb(board, op);
}
}
return random_action(board);
}
fn step_max(board) {
let operators = ["+", "-", "*", "/"];
operators = rand_shuffle(operators);
for op in operators {
if !board.contains(op) {
return step_symb(board, op);
}
}
// TODO: chase
return random_action(board);
}

View File

@@ -0,0 +1,21 @@
fn random_action(board) {
let symb = rand_symb();
let pos = rand_int(0, 10);
let action = Action(symb, pos);
while !board.can_play(action) {
let symb = rand_symb();
let pos = rand_int(0, 10);
action = Action(symb, pos);
}
return action
}
fn step_min(board) {
random_action(board)
}
fn step_max(board) {
random_action(board)
}

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

@@ -0,0 +1,132 @@
use anyhow::Result;
use itertools::Itertools;
use rand::{seq::SliceRandom, thread_rng};
use rayon::iter::{ParallelBridge, ParallelIterator};
use std::{cmp::Ordering, iter};
use super::{Agent, MaximizerAgent, MinimizerAgent};
use crate::{
agents::util::best_board_noop,
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() {
// TODO: only valid (chase)
return Ok(PlayerAction::new_random(&mut rand::thread_rng(), 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() {
// TODO: only valid (diffuse)
return Ok(PlayerAction::new_random(&mut rand::thread_rng(), 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!)
// TODO: only valid (chase)
return Ok(PlayerAction::new_random(&mut rand::thread_rng(), 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)
}
}

View File

@@ -1,102 +0,0 @@
use anyhow::{bail, Result};
use std::num::NonZeroU8;
use super::{Agent, MaximizerAgent, MinimizerAgent, Random};
use crate::{
agents::util::free_slots_by_influence,
board::{Board, PlayerAction},
util::{Player, Symb},
};
pub struct Chase {
player: Player,
}
impl Chase {
pub fn new(player: Player) -> Self {
Self { player }
}
fn step(&mut self, board: &Board, minimize: bool) -> Result<PlayerAction> {
let available_numbers = (0..=9)
.map(|x| match x {
0 => Symb::Zero,
x => Symb::Number(NonZeroU8::new(x).unwrap()),
})
.filter(|x| !board.contains(*x))
.collect::<Vec<_>>();
// For the code below, we must guarantee that
// min_slots + max_slots <= available_numbers.len
let n_free = board.get_board().iter().filter(|x| x.is_none()).count();
if available_numbers.len() < n_free || n_free >= 10 {
return Random::new(self.player).step_min(board);
}
let t = free_slots_by_influence(board);
if t.is_none() {
bail!("could not compute next move!")
}
let t = t.unwrap();
if t.is_empty() {
return Random::new(self.player).step_min(board);
}
let (pos, val) = t[0];
// Choose next number if we can't make the a move.
// Prevents division by zero.
// This isn't perfect, and may fail if we run out of numbers
// (This is, however, very unlikely)
let mut symb = None;
let mut offset = 0;
while symb.is_none()
|| !board.can_play(&PlayerAction {
symb: symb.unwrap(),
pos,
}) {
symb = Some({
if minimize {
if val >= 0.0 {
available_numbers[offset]
} else {
available_numbers[available_numbers.len() - 1 - offset]
}
} else if val <= 0.0 {
available_numbers[offset]
} else {
available_numbers[available_numbers.len() - 1 - offset]
}
});
offset += 1;
}
Ok(PlayerAction {
symb: symb.unwrap(),
pos,
})
}
}
impl Agent for Chase {
fn name(&self) -> &'static str {
"Chase"
}
fn player(&self) -> Player {
self.player
}
}
impl MinimizerAgent for Chase {
fn step_min(&mut self, board: &Board) -> Result<PlayerAction> {
self.step(board, true)
}
}
impl MaximizerAgent for Chase {
fn step_max(&mut self, board: &Board) -> Result<PlayerAction> {
self.step(board, false)
}
}

View File

@@ -1,115 +0,0 @@
use anyhow::Result;
use super::{Agent, Chase, MaximizerAgent, MinimizerAgent, Random};
use crate::{
board::{Board, PlayerAction},
util::{Player, Symb},
};
/// A simple "operator diffusion" MINIMIZER agent.
///
/// Tries to keep operators as far apart as possible, denying large numbers.
pub struct Diffuse {
player: Player,
}
impl Diffuse {
pub fn new(player: Player) -> Self {
Self { player }
}
/// Place a symbol on the board.
/// Assumes `symb` is not already on the board
fn step_symb(&self, board: &Board, symb: Symb) -> PlayerAction {
if board.contains(symb) {
panic!("Called `step_symb` with a symbol that's already on the board!")
}
// Fill distance array with largest possible value
let mut dist = [board.size() + 1; 11];
// Set up initial distances
dist[0] = 1;
*dist.last_mut().unwrap() = 1;
for (i, o) in board.get_board().iter().enumerate() {
if let Some(s) = o {
if s.is_op() {
dist[i] = 0
}
}
}
let mut did_something = true;
while did_something {
did_something = false;
for i in 1..(dist.len() - 1) {
let l = dist[i - 1];
let r = dist[i + 1];
let new = (l + 1).min(r + 1);
if new < dist[i] {
did_something = true;
dist[i] = new;
}
}
}
let mut max_dist = *dist.iter().max().unwrap();
loop {
for (pos, d) in dist.iter().enumerate() {
if *d >= max_dist {
let action = PlayerAction { symb, pos };
if board.can_play(&action) {
return action;
};
}
}
if max_dist == 0 {
return Random::new(self.player).step_max(board).unwrap();
}
max_dist -= 1;
}
}
}
impl Agent for Diffuse {
fn name(&self) -> &'static str {
"Diffuse"
}
fn player(&self) -> Player {
self.player
}
}
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));
if let Some(symb) = symb {
Ok(self.step_symb(board, *symb))
} else {
// No symbols available, play a random number
Chase::new(self.player).step_min(board)
}
}
}
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));
if let Some(symb) = symb {
Ok(self.step_symb(board, *symb))
} else {
// No symbols available, play a random number
Chase::new(self.player).step_max(board)
}
}
}

View File

@@ -35,7 +35,7 @@ impl SymbolSelector {
}
}
fn up(&mut self, board: &Board) {
fn down(&mut self, board: &Board) {
if self.cursor == 0 {
self.cursor = self.symbols.len() - 1;
} 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 {
self.cursor = 0;
} else {
@@ -92,18 +92,22 @@ impl Human {
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
loop {
print!(
"\r{}{}{} {}{}{}{}{}{}",
// Goal
color::Fg(self.player.color()),
if minimize { "Min" } else { "Max" },
color::Fg(color::Reset),
"\r{}{}{}{}{}{}{}",
board_label,
// Cursor
" ".repeat(self.cursor),
color::Fg(self.player.color()),
if board.is_done() {
if board.is_full() {
' '
} else {
self.symbol_selector.current()
@@ -156,7 +160,7 @@ impl Human {
self.symbol_selector.down(board);
break;
}
Key::Char('\n') => {
Key::Char(' ') | Key::Char('\n') => {
let symb = Symb::from_char(self.symbol_selector.current());
if let Some(symb) = symb {
let action = PlayerAction {
@@ -191,7 +195,7 @@ impl Human {
impl Agent for Human {
fn name(&self) -> &'static str {
"Player"
"Human"
}
fn player(&self) -> Player {

View File

@@ -1,13 +1,11 @@
mod chase;
mod diffuse;
mod brutus;
mod human;
mod random;
mod rhai;
pub mod util;
pub use chase::Chase;
pub use diffuse::Diffuse;
pub use brutus::Brutus;
pub use human::Human;
pub use random::Random;
pub use rhai::Rhai;
use crate::{
board::{Board, PlayerAction},

View File

@@ -1,73 +0,0 @@
use crate::{
board::{Board, PlayerAction},
util::{Player, Symb},
};
use anyhow::Result;
use rand::Rng;
use std::num::NonZeroU8;
use super::{Agent, MaximizerAgent, MinimizerAgent};
pub struct Random {
player: Player,
}
impl Random {
pub fn new(player: Player) -> Self {
Self { player }
}
fn random_action(&self, board: &Board) -> PlayerAction {
let mut rng = rand::thread_rng();
let n = board.size();
let pos = rng.gen_range(0..n);
let symb = match rng.gen_range(0..4) {
0 => {
let n = rng.gen_range(0..=9);
if n == 0 {
Symb::Zero
} else {
Symb::Number(NonZeroU8::new(n).unwrap())
}
}
1 => Symb::Div,
2 => Symb::Minus,
3 => Symb::Plus,
4 => Symb::Times,
_ => unreachable!(),
};
PlayerAction { symb, pos }
}
}
impl Agent for Random {
fn name(&self) -> &'static str {
"Random"
}
fn player(&self) -> Player {
self.player
}
}
impl MinimizerAgent for Random {
fn step_min(&mut self, board: &Board) -> Result<PlayerAction> {
let mut action = self.random_action(board);
while !board.can_play(&action) {
action = self.random_action(board);
}
Ok(action)
}
}
impl MaximizerAgent for Random {
fn step_max(&mut self, board: &Board) -> Result<PlayerAction> {
let mut action = self.random_action(board);
while !board.can_play(&action) {
action = self.random_action(board);
}
Ok(action)
}
}

176
src/agents/rhai.rs Normal file
View File

@@ -0,0 +1,176 @@
use anyhow::{anyhow, Context, Result};
use itertools::{Itertools, Permutations};
use rand::{seq::SliceRandom, Rng};
use rhai::{
packages::{
ArithmeticPackage, BasicArrayPackage, BasicFnPackage, BasicIteratorPackage,
BasicMathPackage, BasicStringPackage, LanguageCorePackage, LogicPackage, MoreStringPackage,
Package,
},
CustomType, Dynamic, Engine, EvalAltResult, Position, Scope, TypeBuilder, AST,
};
use std::{sync::Arc, vec::IntoIter};
use super::{Agent, MaximizerAgent, MinimizerAgent};
use crate::{
board::{Board, PlayerAction},
util::{Player, Symb},
};
pub struct RhaiPer<T: Clone, I: Iterator<Item = T>> {
inner: Arc<Permutations<I>>,
}
impl<T: Clone, I: Clone + Iterator<Item = T>> IntoIterator for RhaiPer<T, I> {
type Item = Vec<T>;
type IntoIter = Permutations<I>;
fn into_iter(self) -> Self::IntoIter {
(*self.inner).clone()
}
}
impl<T: Clone, I: Iterator<Item = T>> Clone for RhaiPer<T, I> {
fn clone(&self) -> Self {
Self {
inner: self.inner.clone(),
}
}
}
impl<T: Clone + Send + Sync + 'static, I: Clone + Iterator<Item = T> + Sync + Send + 'static>
CustomType for RhaiPer<T, I>
{
fn build(mut builder: TypeBuilder<Self>) {
builder
.with_name("Perutations")
.is_iterable()
.with_fn("to_string", |_s: &mut Self| "Permutation {}".to_owned())
.with_fn("to_debug", |_s: &mut Self| "Permutation {}".to_owned());
}
}
//
//
//
pub struct Rhai {
player: Player,
engine: Engine,
script: AST,
scope: Scope<'static>,
}
impl Rhai {
pub fn new(player: Player, script: &str) -> Self {
let engine = {
let mut engine = Engine::new_raw();
engine.set_max_expr_depths(100, 100);
engine.set_max_strings_interned(1024);
engine.on_print(|text| println!("{text}"));
engine.on_debug(|text, source, pos| match (source, pos) {
(Some(source), Position::NONE) => println!("{source} | {text}"),
(Some(source), pos) => println!("{source} @ {pos:?} | {text}"),
(None, Position::NONE) => println!("{text}"),
(None, pos) => println!("{pos:?} | {text}"),
});
LanguageCorePackage::new().register_into_engine(&mut engine);
ArithmeticPackage::new().register_into_engine(&mut engine);
BasicIteratorPackage::new().register_into_engine(&mut engine);
LogicPackage::new().register_into_engine(&mut engine);
BasicStringPackage::new().register_into_engine(&mut engine);
MoreStringPackage::new().register_into_engine(&mut engine);
BasicMathPackage::new().register_into_engine(&mut engine);
BasicArrayPackage::new().register_into_engine(&mut engine);
BasicFnPackage::new().register_into_engine(&mut engine);
engine
.register_fn("rand_int", |from: i64, to: i64| {
rand::thread_rng().gen_range(from..=to)
})
.register_fn("rand_bool", |p: f32| rand::thread_rng().gen_bool(p as f64))
.register_fn("rand_symb", || {
Symb::new_random(&mut rand::thread_rng()).to_string()
})
.register_fn("rand_action", |board: Board| {
PlayerAction::new_random(&mut rand::thread_rng(), &board)
})
.register_fn("rand_shuffle", |mut vec: Vec<Dynamic>| {
vec.shuffle(&mut rand::thread_rng());
vec
})
.register_fn("is_op", |s: &str| {
Symb::from_str(s).map(|x| x.is_op()).unwrap_or(false)
})
.register_fn(
"permutations",
|v: Vec<Dynamic>, size: i64| -> Result<Dynamic, Box<EvalAltResult>> {
let size: usize = match size.try_into() {
Ok(x) => x,
Err(_) => {
return Err(format!("Invalid permutation size {size}").into());
}
};
let per = RhaiPer {
inner: v.into_iter().permutations(size).into(),
};
Ok(Dynamic::from(per))
},
);
engine
.build_type::<Board>()
.build_type::<PlayerAction>()
.build_type::<RhaiPer<Dynamic, IntoIter<Dynamic>>>();
engine
};
let script = engine.compile(script).unwrap();
let scope = Scope::new();
Self {
player,
engine,
script,
scope,
}
}
}
impl Agent for Rhai {
fn name(&self) -> &'static str {
"Rhai"
}
fn player(&self) -> Player {
self.player
}
}
impl MinimizerAgent for Rhai {
fn step_min(&mut self, board: &Board) -> Result<PlayerAction> {
let result = self
.engine
.call_fn::<PlayerAction>(&mut self.scope, &self.script, "step_min", (board.clone(),))
.map_err(|x| anyhow!(x.to_string()))
.context("while running rhai step_min")?;
Ok(result)
}
}
impl MaximizerAgent for Rhai {
fn step_max(&mut self, board: &Board) -> Result<PlayerAction> {
let result = self
.engine
.call_fn::<PlayerAction>(&mut self.scope, &self.script, "step_max", (board.clone(),))
.map_err(|x| anyhow!(x.to_string()))
.context("while running rhai step_min")?;
Ok(result)
}
}

View File

@@ -1,14 +1,14 @@
use itertools::Itertools;
use std::num::NonZeroU8;
use std::{mem::swap, num::NonZeroU8};
use crate::{board::Board, util::Symb};
/// Returns an iterator of (sort, coords, char_idx, f32) for each empty slot in the listed partials.
/// - sort is the index of this slot.
/// Returns an iterator of (idx, coords, char_idx, f32) for each empty slot in the listed partials.
/// - idx is the index of this slot in the board string.
/// - coords are the coordinate of this slot's partial
/// - char_idx is the index of this slot in its partial
/// - f32 is the influence of this slot
pub fn free_slots_by_influence(board: &Board) -> Option<Vec<(usize, f32)>> {
fn free_slots_by_influence(board: &Board) -> Option<Vec<(usize, f32)>> {
// Fill all empty slots with fives and compute starting value
let filled = {
// This should always result in an evaluatable expression,
@@ -51,16 +51,12 @@ pub fn free_slots_by_influence(board: &Board) -> Option<Vec<(usize, f32)>> {
Some(slots)
}
/// Find the maximum possible value of the given board
#[allow(dead_code)]
pub fn maximize_value(board: &Board) -> Option<Board> {
/// Find the minimum or maximum possible value of the given board,
/// without adding any operations. Returns None if we couldn't find
/// 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();
// Assume we have 10 or fewer available slots
if n_free >= 10 {
panic!()
}
let available_numbers = (0..=9)
.map(|x| match x {
0 => Symb::Zero,
@@ -69,19 +65,36 @@ pub fn maximize_value(board: &Board) -> Option<Board> {
.filter(|x| !board.contains(*x))
.collect::<Vec<_>>();
if n_free > available_numbers.len() {
return None;
}
let slots = free_slots_by_influence(board)?;
let all_symbols = {
// We need this many from the bottom, and this many from the top.
let neg_count = slots.iter().filter(|(_, x)| *x <= 0.0).count();
let pos_count = slots.iter().filter(|(_, x)| *x > 0.0).count();
let mut a = {
// Number of slots we want to minimize
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 {
swap(&mut neg_count, &mut pos_count);
}
available_numbers
.iter()
.take(neg_count)
.chain(available_numbers.iter().rev().take(pos_count).rev());
.chain(available_numbers.iter().rev().take(pos_count).rev())
.collect_vec()
};
let mut g = slots
if !minimize {
a.reverse();
}
let mut a_iter = a.into_iter();
slots
// Group slots with equal weights
// and count the number of elements in each group
.iter()
@@ -101,37 +114,32 @@ pub fn maximize_value(board: &Board) -> Option<Board> {
// of this set of sets
.multi_cartesian_product()
.map(|x| x.iter().flatten().cloned().collect_vec())
.map(|v| slots.iter().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())
// Finally, attach the coordinate of each slot to each symbol
.map(|v| slots.iter().map(|x| x.0).zip(v).collect_vec())
.collect_vec()
};
let mut best_board = None;
let mut best_value = None;
for i in all_symbols {
let mut i_iter = i.iter();
let filled = Board::from_board(board.get_board().map(|x| match x {
None => i_iter.next().cloned(),
_ => x,
}));
for i_iter in all_symbols {
let mut tmp_board = board.clone();
for (i, s) in i_iter {
tmp_board.get_board_mut()[i] = Some(s);
}
let val = filled.evaluate();
let val = tmp_board.evaluate();
if let Some(val) = val {
if let Some(best) = best_value {
if val > best {
if minimize {
if best_value.is_none() || val < best_value.unwrap() {
best_value = Some(val);
best_board = Some(filled)
best_board = Some(tmp_board)
}
} else {
if best_value.is_none() || val > best_value.unwrap() {
best_value = Some(val);
best_board = Some(filled)
best_board = Some(tmp_board)
}
}
}
}

View File

@@ -1,6 +1,12 @@
use anyhow::Result;
use itertools::Itertools;
use std::fmt::{Display, Write};
use rhai::Array;
use rhai::CustomType;
use rhai::Dynamic;
use rhai::EvalAltResult;
use rhai::Position;
use rhai::TypeBuilder;
use std::fmt::{Debug, Display, Write};
use termion::color::{self, Color};
use super::{PlayerAction, TreeElement};
@@ -74,6 +80,12 @@ impl Display for Board {
}
}
impl Debug for Board {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
Display::fmt(&self, f)
}
}
#[allow(dead_code)]
impl Board {
pub fn new() -> Self {
@@ -93,7 +105,25 @@ impl Board {
&mut self.board
}
pub fn is_done(&self) -> bool {
/// 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_full(&self) -> bool {
self.free_spots == 0
}
@@ -393,3 +423,167 @@ impl Board {
})
}
}
impl IntoIterator for Board {
type Item = String;
type IntoIter = std::vec::IntoIter<Self::Item>;
fn into_iter(self) -> Self::IntoIter {
self.board
.iter()
.map(|x| x.map(|x| x.to_string()).unwrap_or_default())
.collect::<Vec<_>>()
.into_iter()
}
}
impl CustomType for Board {
fn build(mut builder: TypeBuilder<Self>) {
builder
.with_name("Board")
.is_iterable()
.with_fn("to_string", |s: &mut Self| format!("{}", s))
.with_fn("to_debug", |s: &mut Self| format!("{:?}", s))
.with_fn("size", |s: &mut Self| s.board.len() as i64)
.with_fn("len", |s: &mut Self| s.board.len() as i64)
.with_fn("is_full", |s: &mut Self| s.is_full())
.with_fn("free_spots", |s: &mut Self| s.free_spots)
.with_fn("play", |s: &mut Self, act: PlayerAction| {
s.play(act, Player::Red) // Player doesn't matter
})
.with_fn("ith_free_slot", |s: &mut Self, idx: usize| {
s.ith_empty_slot(idx).map(|x| x as i64).unwrap_or(-1)
})
.with_fn("can_play", |s: &mut Self, act: PlayerAction| {
s.can_play(&act)
})
.with_fn("contains", |s: &mut Self, sym: &str| {
match Symb::from_str(sym) {
None => false,
Some(x) => s.contains(x),
}
})
.with_fn("contains", |s: &mut Self, sym: i64| {
let sym = sym.to_string();
match Symb::from_str(&sym) {
None => false,
Some(x) => s.contains(x),
}
})
.with_fn("evaluate", |s: &mut Self| -> Dynamic {
s.evaluate().map(|x| x.into()).unwrap_or(().into())
})
.with_fn("free_spots_idx", |s: &mut Self| -> Array {
s.board
.iter()
.enumerate()
.filter(|(_, x)| x.is_none())
.map(|(i, _)| i as i64)
.map(|x| x.into())
.collect::<Vec<Dynamic>>()
})
.with_indexer_get(
|s: &mut Self, idx: i64| -> Result<String, Box<EvalAltResult>> {
if idx as usize >= s.board.len() {
return Err(
EvalAltResult::ErrorIndexNotFound(idx.into(), Position::NONE).into(),
);
}
let idx = idx as usize;
return Ok(s.board[idx].map(|x| x.to_string()).unwrap_or_default());
},
)
.with_indexer_set(
|s: &mut Self, idx: i64, val: String| -> Result<(), Box<EvalAltResult>> {
let idx: usize = match idx.try_into() {
Ok(x) => x,
Err(_) => {
return Err(EvalAltResult::ErrorIndexNotFound(
idx.into(),
Position::NONE,
)
.into());
}
};
if idx >= s.board.len() {
return Err(EvalAltResult::ErrorIndexNotFound(
(idx as i64).into(),
Position::NONE,
)
.into());
}
match Symb::from_str(&val) {
None => return Err(format!("Invalid symbol {val}").into()),
Some(x) => {
s.board[idx] = Some(x);
s.placed_by[idx] = Some(Player::Red); // Arbitrary
}
}
return Ok(());
},
)
.with_indexer_set(
|s: &mut Self, idx: i64, _val: ()| -> Result<(), Box<EvalAltResult>> {
let idx: usize = match idx.try_into() {
Ok(x) => x,
Err(_) => {
return Err(EvalAltResult::ErrorIndexNotFound(
idx.into(),
Position::NONE,
)
.into());
}
};
if idx >= s.board.len() {
return Err(EvalAltResult::ErrorIndexNotFound(
(idx as i64).into(),
Position::NONE,
)
.into());
}
s.board[idx] = None;
s.placed_by[idx] = None;
return Ok(());
},
)
.with_indexer_set(
|s: &mut Self, idx: i64, val: i64| -> Result<(), Box<EvalAltResult>> {
let idx: usize = match idx.try_into() {
Ok(x) => x,
Err(_) => {
return Err(EvalAltResult::ErrorIndexNotFound(
idx.into(),
Position::NONE,
)
.into());
}
};
if idx >= s.board.len() {
return Err(EvalAltResult::ErrorIndexNotFound(
(idx as i64).into(),
Position::NONE,
)
.into());
}
match Symb::from_str(&val.to_string()) {
None => return Err(format!("Invalid symbol {val}").into()),
Some(x) => {
s.board[idx] = Some(x);
s.placed_by[idx] = Some(Player::Red); // Arbitrary
}
}
return Ok(());
},
);
}
}

View File

@@ -2,6 +2,8 @@
mod board;
mod tree;
use rand::Rng;
use rhai::{CustomType, EvalAltResult, TypeBuilder};
use std::fmt::Display;
pub use board::Board;
@@ -20,3 +22,56 @@ impl Display for PlayerAction {
write!(f, "{} at {}", self.symb, self.pos)
}
}
impl PlayerAction {
pub fn new_random<R: Rng>(rng: &mut R, board: &Board) -> Self {
let n = board.size();
let pos = rng.gen_range(0..n);
let symb = Symb::new_random(rng);
PlayerAction { symb, pos }
}
}
impl CustomType for PlayerAction {
fn build(mut builder: TypeBuilder<Self>) {
builder
.with_name("Action")
.with_fn(
"Action",
|symb: &str, pos: i64| -> Result<Self, Box<EvalAltResult>> {
let symb = match Symb::from_str(symb) {
Some(x) => x,
None => return Err(format!("Invalid symbol {symb:?}").into()),
};
Ok(Self {
symb,
pos: pos as usize,
})
},
)
.with_fn(
"Action",
|symb: i64, pos: i64| -> Result<Self, Box<EvalAltResult>> {
let symb = symb.to_string();
let symb = match Symb::from_str(&symb) {
Some(x) => x,
None => return Err(format!("Invalid symbol {symb:?}").into()),
};
Ok(Self {
symb,
pos: pos as usize,
})
},
)
.with_fn("to_string", |s: &mut Self| -> String {
format!("Action {{{} at {}}}", s.symb, s.pos)
})
.with_fn("to_debug", |s: &mut Self| -> String {
format!("Action {{{} at {}}}", s.symb, s.pos)
})
.with_get("symb", |s: &mut Self| s.symb.to_string())
.with_get("pos", |s: &mut Self| s.pos);
}
}

View File

@@ -1,9 +1,8 @@
use clap::{Parser, ValueEnum};
use std::fmt::Display;
use clap::{Parser, ValueEnum};
use crate::{
agents::{Chase, Diffuse, Human, MaximizerAgent, MinimizerAgent, Random},
agents::{Brutus, Human, MaximizerAgent, MinimizerAgent, Rhai},
util::Player,
};
@@ -18,14 +17,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,23 +34,28 @@ pub enum AgentSelector {
/// A smarter extremum-chaser (medium)
Diffuse,
/// A very smart brute-force agent (hard)
Brutus,
}
impl AgentSelector {
pub fn get_maximizer(&self, player: Player) -> Box<dyn MaximizerAgent> {
match self {
Self::Random => Box::new(Random::new(player)),
Self::Chase => Box::new(Chase::new(player)),
Self::Diffuse => Box::new(Diffuse::new(player)),
Self::Random => Box::new(Rhai::new(player, include_str!("agents.rhai/random.rhai"))),
Self::Diffuse => Box::new(Rhai::new(player, include_str!("agents.rhai/diffuse.rhai"))),
Self::Chase => Box::new(Rhai::new(player, include_str!("agents.rhai/chase.rhai"))),
Self::Brutus => Box::new(Brutus::new(player)),
Self::Human => Box::new(Human::new(player)),
}
}
pub fn get_minimizer(&self, player: Player) -> Box<dyn MinimizerAgent> {
match self {
Self::Random => Box::new(Random::new(player)),
Self::Chase => Box::new(Chase::new(player)),
Self::Diffuse => Box::new(Diffuse::new(player)),
Self::Random => Box::new(Rhai::new(player, include_str!("agents.rhai/random.rhai"))),
Self::Diffuse => Box::new(Rhai::new(player, include_str!("agents.rhai/diffuse.rhai"))),
Self::Chase => Box::new(Rhai::new(player, include_str!("agents.rhai/chase.rhai"))),
Self::Brutus => Box::new(Brutus::new(player)),
Self::Human => Box::new(Human::new(player)),
}
}
@@ -71,6 +69,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;
@@ -18,7 +22,7 @@ fn play_silent(
let mut board = Board::new();
let mut is_maxi_turn = true;
while !board.is_done() {
while !board.is_full() {
// Take action
let action = if is_maxi_turn {
maxi.step_max(&board)?
@@ -60,13 +64,13 @@ fn play(
let mut is_maxi_turn = true;
let board_label = format!(
"{}{:6}{}",
"{}{:<6}{}",
color::Fg(color::LightBlack),
maxi.player(),
maxi.player().to_string(),
color::Fg(color::Reset)
);
while !board.is_done() {
while !board.is_full() {
// Print board
println!(
"\r{}{}{}{}",
@@ -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,70 +121,73 @@ fn play(
fn main() -> Result<()> {
let cli = cli::Cli::parse();
if cli.repeat > 1 {
let mut red_wins = 0f32;
let mut blue_wins = 0f32;
rayon::ThreadPoolBuilder::new()
.num_threads(8)
.build_global()
.unwrap();
for _ in 0..cli.repeat {
if cli.repeat > 1 {
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 if cli.silent {
play_silent(&mut *maxi, &mut *mini)
} else {
play(&mut *maxi, &mut *mini)
} {
let red_board = match play_silent(&mut *maxi, &mut *mini) {
Ok(x) => x,
Err(e) => {
println!("Error: {e}");
continue;
for e in e.chain() {
println!("{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)
} {
let blue_board = match play_silent(&mut *maxi, &mut *mini) {
Ok(x) => x,
Err(e) => {
println!("Error: {e}");
continue;
for e in e.chain() {
println!("{e}");
}
return None;
}
};
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,
Some(Ordering::Equal) => None,
Some(Ordering::Greater) => Some(Player::Red),
Some(Ordering::Less) => Some(Player::Blue),
None => {
println!("Error");
None
}
}
}
})
.collect::<Vec<_>>();
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() {
if red_board.is_full() {
println!(
"\r\n{}{} score:{} {:.2}\n\n",
color::Fg(maxi.player().color()),
@@ -196,13 +206,9 @@ 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() {
if blue_board.is_full() {
println!(
"\r\n{}{} score:{} {:.2}\n\n",
color::Fg(maxi.player().color()),

View File

@@ -3,6 +3,7 @@ use std::{
num::NonZeroU8,
};
use rand::Rng;
use termion::color::{self, Color};
#[derive(PartialEq, Eq, Clone, Copy)]
@@ -31,6 +32,7 @@ impl Display for Player {
}
#[derive(PartialEq, Eq, Clone, Copy, Hash)]
pub enum Symb {
Number(NonZeroU8),
Zero,
@@ -69,6 +71,24 @@ impl Symb {
self == &Self::Minus
}
pub fn new_random<R: Rng>(rng: &mut R) -> Self {
match rng.gen_range(0..4) {
0 => {
let n = rng.gen_range(0..=9);
if n == 0 {
Symb::Zero
} else {
Symb::Number(NonZeroU8::new(n).unwrap())
}
}
1 => Symb::Div,
2 => Symb::Minus,
3 => Symb::Plus,
4 => Symb::Times,
_ => unreachable!(),
}
}
pub const fn get_char(&self) -> Option<char> {
match self {
Self::Plus => Some('+'),
@@ -91,6 +111,14 @@ impl Symb {
}
}
pub fn from_str(s: &str) -> Option<Self> {
if s.chars().count() != 1 {
return None;
}
Self::from_char(s.chars().next()?)
}
pub const fn from_char(c: char) -> Option<Self> {
match c {
'1' => Some(Self::Number(unsafe { NonZeroU8::new_unchecked(1) })),