This commit is contained in:
2025-11-01 10:02:41 -07:00
parent 786e70fe9a
commit 0dfc3f4b26
19 changed files with 1007 additions and 337 deletions

1
.gitignore vendored
View File

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

277
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,6 +146,35 @@ 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"
@@ -145,12 +200,30 @@ 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"
@@ -162,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"
@@ -179,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"
@@ -203,15 +314,50 @@ dependencies = [
"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"
@@ -236,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"
@@ -263,7 +415,7 @@ version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
dependencies = [
"getrandom",
"getrandom 0.2.12",
]
[[package]]
@@ -301,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"
@@ -330,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"
@@ -342,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"
@@ -413,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

@@ -9,4 +9,13 @@ 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"

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

View File

@@ -1,13 +1,12 @@
use std::{cmp::Ordering, iter};
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, Chase, MaximizerAgent, MinimizerAgent};
use super::{Agent, MaximizerAgent, MinimizerAgent};
use crate::{
agents::{util::best_board_noop, Diffuse},
agents::util::best_board_noop,
board::{Board, PlayerAction},
util::{Player, Symb},
};
@@ -28,11 +27,8 @@ impl Brutus {
.collect_vec();
if symbols.is_empty() {
return if minimize {
Chase::new(self.player).step_min(board)
} else {
Chase::new(self.player).step_max(board)
};
// TODO: only valid (chase)
return Ok(PlayerAction::new_random(&mut rand::thread_rng(), board));
}
// Number of free slots
@@ -86,11 +82,8 @@ impl Brutus {
// 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)
};
// TODO: only valid (diffuse)
return Ok(PlayerAction::new_random(&mut rand::thread_rng(), board));
}
let (t, _) = items.first().unwrap();
@@ -111,11 +104,8 @@ impl Brutus {
// 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)
}
// TODO: only valid (chase)
return Ok(PlayerAction::new_random(&mut rand::thread_rng(), board));
}
}

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,117 +0,0 @@
use anyhow::Result;
use rand::{seq::SliceRandom, thread_rng};
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.
/// Places numbers using the same algorithm as chase.
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 mut x = [Symb::Minus, Symb::Times, Symb::Plus, Symb::Div];
x.shuffle(&mut thread_rng());
let symb = x.iter().find(|x| !board.contains(**x));
if let Some(symb) = symb {
Ok(self.step_symb(board, *symb))
} 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 mut x = [Symb::Minus, Symb::Times, Symb::Plus, Symb::Div];
x.shuffle(&mut thread_rng());
let symb = x.iter().find(|x| !board.contains(**x));
if let Some(symb) = symb {
Ok(self.step_symb(board, *symb))
} else {
// No symbols available, play a random number
Chase::new(self.player).step_max(board)
}
}
}

View File

@@ -107,7 +107,7 @@ impl Human {
// Cursor
" ".repeat(self.cursor),
color::Fg(self.player.color()),
if board.is_done() {
if board.is_full() {
' '
} else {
self.symbol_selector.current()

View File

@@ -1,15 +1,11 @@
mod brutus;
mod chase;
mod diffuse;
mod human;
mod random;
mod rhai;
pub mod util;
pub use brutus::Brutus;
pub use chase::Chase;
pub use diffuse::Diffuse;
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

@@ -8,7 +8,7 @@ use crate::{board::Board, util::Symb};
/// - 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,

View File

@@ -1,5 +1,11 @@
use anyhow::Result;
use itertools::Itertools;
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};
@@ -117,7 +123,7 @@ impl Board {
}
}
pub fn is_done(&self) -> bool {
pub fn is_full(&self) -> bool {
self.free_spots == 0
}
@@ -417,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::{Brutus, Chase, Diffuse, Human, MaximizerAgent, MinimizerAgent, Random},
agents::{Brutus, Human, MaximizerAgent, MinimizerAgent, Rhai},
util::Player,
};
@@ -43,9 +42,9 @@ pub enum AgentSelector {
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)),
}
@@ -53,9 +52,9 @@ impl AgentSelector {
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)),
}

View File

@@ -22,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)?
@@ -70,7 +70,7 @@ fn play(
color::Fg(color::Reset)
);
while !board.is_done() {
while !board.is_full() {
// Print board
println!(
"\r{}{}{}{}",
@@ -136,6 +136,9 @@ fn main() -> Result<()> {
Ok(x) => x,
Err(e) => {
println!("Error: {e}");
for e in e.chain() {
println!("{e}");
}
return None;
}
};
@@ -146,6 +149,9 @@ fn main() -> Result<()> {
Ok(x) => x,
Err(e) => {
println!("Error: {e}");
for e in e.chain() {
println!("{e}");
}
return None;
}
};
@@ -181,7 +187,7 @@ fn main() -> Result<()> {
let mut mini = cli.blue.get_minimizer(Player::Blue);
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()),
@@ -202,7 +208,7 @@ fn main() -> Result<()> {
let mut mini = cli.red.get_minimizer(Player::Red);
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) })),