From 0dfc3f4b26226d39d60ab21a20cb22429a4e5eac Mon Sep 17 00:00:00 2001 From: Mark Date: Sat, 1 Nov 2025 10:02:41 -0700 Subject: [PATCH] Rhai --- .gitignore | 1 + Cargo.lock | 277 ++++++++++++++++++++++++++++++++++- Cargo.toml | 9 ++ src/agents.rhai/chase.rhai | 133 +++++++++++++++++ src/agents.rhai/diffuse.rhai | 109 ++++++++++++++ src/agents.rhai/random.rhai | 21 +++ src/agents/brutus.rs | 28 ++-- src/agents/chase.rs | 102 ------------- src/agents/diffuse.rs | 117 --------------- src/agents/human.rs | 2 +- src/agents/mod.rs | 8 +- src/agents/random.rs | 73 --------- src/agents/rhai.rs | 176 ++++++++++++++++++++++ src/agents/util/partials.rs | 2 +- src/board/board.rs | 172 +++++++++++++++++++++- src/board/mod.rs | 55 +++++++ src/cli.rs | 17 +-- src/main.rs | 14 +- src/util.rs | 28 ++++ 19 files changed, 1007 insertions(+), 337 deletions(-) create mode 100644 src/agents.rhai/chase.rhai create mode 100644 src/agents.rhai/diffuse.rhai create mode 100644 src/agents.rhai/random.rhai delete mode 100644 src/agents/chase.rs delete mode 100644 src/agents/diffuse.rs delete mode 100644 src/agents/random.rs create mode 100644 src/agents/rhai.rs diff --git a/.gitignore b/.gitignore index ea8c4bf..212de44 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ /target +.DS_Store \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index b0373f3..0b12ecc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", +] diff --git a/Cargo.toml b/Cargo.toml index f1cf65a..9911e66 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/src/agents.rhai/chase.rhai b/src/agents.rhai/chase.rhai new file mode 100644 index 0000000..adfda40 --- /dev/null +++ b/src/agents.rhai/chase.rhai @@ -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) +} \ No newline at end of file diff --git a/src/agents.rhai/diffuse.rhai b/src/agents.rhai/diffuse.rhai new file mode 100644 index 0000000..f4ad2f2 --- /dev/null +++ b/src/agents.rhai/diffuse.rhai @@ -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); +} diff --git a/src/agents.rhai/random.rhai b/src/agents.rhai/random.rhai new file mode 100644 index 0000000..8dc56a6 --- /dev/null +++ b/src/agents.rhai/random.rhai @@ -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) +} \ No newline at end of file diff --git a/src/agents/brutus.rs b/src/agents/brutus.rs index bac10d3..bc66cf0 100644 --- a/src/agents/brutus.rs +++ b/src/agents/brutus.rs @@ -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)); } } diff --git a/src/agents/chase.rs b/src/agents/chase.rs deleted file mode 100644 index 1b0b9bd..0000000 --- a/src/agents/chase.rs +++ /dev/null @@ -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 { - 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::>(); - - // 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 { - self.step(board, true) - } -} - -impl MaximizerAgent for Chase { - fn step_max(&mut self, board: &Board) -> Result { - self.step(board, false) - } -} diff --git a/src/agents/diffuse.rs b/src/agents/diffuse.rs deleted file mode 100644 index 69537e4..0000000 --- a/src/agents/diffuse.rs +++ /dev/null @@ -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 { - 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 { - 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) - } - } -} diff --git a/src/agents/human.rs b/src/agents/human.rs index 54a7363..5d74266 100644 --- a/src/agents/human.rs +++ b/src/agents/human.rs @@ -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() diff --git a/src/agents/mod.rs b/src/agents/mod.rs index 4e99170..893811f 100644 --- a/src/agents/mod.rs +++ b/src/agents/mod.rs @@ -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}, diff --git a/src/agents/random.rs b/src/agents/random.rs deleted file mode 100644 index f89255f..0000000 --- a/src/agents/random.rs +++ /dev/null @@ -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 { - 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 { - let mut action = self.random_action(board); - while !board.can_play(&action) { - action = self.random_action(board); - } - Ok(action) - } -} diff --git a/src/agents/rhai.rs b/src/agents/rhai.rs new file mode 100644 index 0000000..9856550 --- /dev/null +++ b/src/agents/rhai.rs @@ -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> { + inner: Arc>, +} + +impl> IntoIterator for RhaiPer { + type Item = Vec; + type IntoIter = Permutations; + + fn into_iter(self) -> Self::IntoIter { + (*self.inner).clone() + } +} + +impl> Clone for RhaiPer { + fn clone(&self) -> Self { + Self { + inner: self.inner.clone(), + } + } +} + +impl + Sync + Send + 'static> + CustomType for RhaiPer +{ + fn build(mut builder: TypeBuilder) { + 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| { + 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, size: i64| -> Result> { + 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::() + .build_type::() + .build_type::>>(); + 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 { + let result = self + .engine + .call_fn::(&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 { + let result = self + .engine + .call_fn::(&mut self.scope, &self.script, "step_max", (board.clone(),)) + .map_err(|x| anyhow!(x.to_string())) + .context("while running rhai step_min")?; + + Ok(result) + } +} diff --git a/src/agents/util/partials.rs b/src/agents/util/partials.rs index b87dde4..4c719b8 100644 --- a/src/agents/util/partials.rs +++ b/src/agents/util/partials.rs @@ -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> { +fn free_slots_by_influence(board: &Board) -> Option> { // Fill all empty slots with fives and compute starting value let filled = { // This should always result in an evaluatable expression, diff --git a/src/board/board.rs b/src/board/board.rs index 36912f9..8190299 100644 --- a/src/board/board.rs +++ b/src/board/board.rs @@ -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; + + fn into_iter(self) -> Self::IntoIter { + self.board + .iter() + .map(|x| x.map(|x| x.to_string()).unwrap_or_default()) + .collect::>() + .into_iter() + } +} + +impl CustomType for Board { + fn build(mut builder: TypeBuilder) { + 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::>() + }) + .with_indexer_get( + |s: &mut Self, idx: i64| -> Result> { + 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> { + 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> { + 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> { + 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(()); + }, + ); + } +} diff --git a/src/board/mod.rs b/src/board/mod.rs index e2d17d7..d3ba0a3 100644 --- a/src/board/mod.rs +++ b/src/board/mod.rs @@ -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(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) { + builder + .with_name("Action") + .with_fn( + "Action", + |symb: &str, pos: i64| -> Result> { + 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> { + 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); + } +} diff --git a/src/cli.rs b/src/cli.rs index dd49788..cddcb86 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -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 { 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 { 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)), } diff --git a/src/main.rs b/src/main.rs index 0a4f26b..5a57e27 100644 --- a/src/main.rs +++ b/src/main.rs @@ -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()), diff --git a/src/util.rs b/src/util.rs index 72d3e80..d92aac8 100644 --- a/src/util.rs +++ b/src/util.rs @@ -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(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 { match self { Self::Plus => Some('+'), @@ -91,6 +111,14 @@ impl Symb { } } + pub fn from_str(s: &str) -> Option { + if s.chars().count() != 1 { + return None; + } + + Self::from_char(s.chars().next()?) + } + pub const fn from_char(c: char) -> Option { match c { '1' => Some(Self::Number(unsafe { NonZeroU8::new_unchecked(1) })),