x
This commit is contained in:
17
rust/minimax/Cargo.toml
Normal file
17
rust/minimax/Cargo.toml
Normal file
@@ -0,0 +1,17 @@
|
||||
[package]
|
||||
name = "minimax"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
anyhow = { workspace = true }
|
||||
itertools = { workspace = true }
|
||||
rand = { workspace = true }
|
||||
|
||||
rhai = { workspace = true }
|
||||
parking_lot = { workspace = true }
|
||||
|
||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||
rhai = { workspace = true, features = ["wasm-bindgen"] }
|
||||
web-sys = { workspace = true }
|
||||
wasm-bindgen = { workspace = true }
|
||||
18
rust/minimax/src/agents/mod.rs
Normal file
18
rust/minimax/src/agents/mod.rs
Normal file
@@ -0,0 +1,18 @@
|
||||
mod rhai;
|
||||
|
||||
pub use rhai::Rhai;
|
||||
|
||||
use crate::board::{Board, PlayerAction};
|
||||
use anyhow::Result;
|
||||
|
||||
pub trait Agent {
|
||||
type ErrorType;
|
||||
|
||||
fn name(&self) -> &'static str;
|
||||
|
||||
/// Try to minimize the value of a board.
|
||||
fn step_min(&mut self, board: &Board) -> Result<PlayerAction, Self::ErrorType>;
|
||||
|
||||
/// Try to maximize the value of a board.
|
||||
fn step_max(&mut self, board: &Board) -> Result<PlayerAction, Self::ErrorType>;
|
||||
}
|
||||
264
rust/minimax/src/agents/rhai.rs
Normal file
264
rust/minimax/src/agents/rhai.rs
Normal file
@@ -0,0 +1,264 @@
|
||||
use anyhow::Result;
|
||||
use itertools::{Itertools, Permutations};
|
||||
use parking_lot::Mutex;
|
||||
use rand::{seq::SliceRandom, Rng};
|
||||
use rhai::{
|
||||
packages::{
|
||||
ArithmeticPackage, BasicArrayPackage, BasicFnPackage, BasicIteratorPackage,
|
||||
BasicMathPackage, BasicStringPackage, LanguageCorePackage, LogicPackage, MoreStringPackage,
|
||||
Package,
|
||||
},
|
||||
CallFnOptions, CustomType, Dynamic, Engine, EvalAltResult, OptimizationLevel, ParseError,
|
||||
Position, Scope, TypeBuilder, AST,
|
||||
};
|
||||
use std::{sync::Arc, vec::IntoIter};
|
||||
|
||||
use super::Agent;
|
||||
use crate::{
|
||||
board::{Board, PlayerAction},
|
||||
util::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 + 'static, I: Clone + Iterator<Item = T> + '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<R: Rng + 'static> {
|
||||
#[expect(dead_code)]
|
||||
rng: Arc<Mutex<R>>,
|
||||
|
||||
engine: Engine,
|
||||
script: AST,
|
||||
scope: Scope<'static>,
|
||||
print_callback: Arc<dyn Fn(&str) + 'static>,
|
||||
}
|
||||
|
||||
impl<R: Rng + 'static> Rhai<R> {
|
||||
pub fn new(
|
||||
script: &str,
|
||||
rng: R,
|
||||
print_callback: impl Fn(&str) + 'static,
|
||||
debug_callback: impl Fn(&str) + 'static,
|
||||
) -> Result<Self, ParseError> {
|
||||
let rng = Arc::new(Mutex::new(rng));
|
||||
let print_callback = Arc::new(print_callback);
|
||||
|
||||
let engine = {
|
||||
let mut engine = Engine::new_raw();
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
fn performance() -> Result<web_sys::Performance, wasm_bindgen::JsValue> {
|
||||
use wasm_bindgen::JsCast;
|
||||
|
||||
let global = web_sys::js_sys::global();
|
||||
let performance = web_sys::js_sys::Reflect::get(&global, &"performance".into())?;
|
||||
performance.dyn_into::<web_sys::Performance>()
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
let start = {
|
||||
let performance = performance().expect("performance should be available");
|
||||
|
||||
// In milliseconds
|
||||
performance.now()
|
||||
};
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let start = {
|
||||
use std::time::Instant;
|
||||
Instant::now()
|
||||
};
|
||||
|
||||
let max_secs: u64 = 5;
|
||||
engine.on_progress(move |ops| {
|
||||
if ops % 10_000 != 0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
let elapsed_s = {
|
||||
let performance = performance().expect("performance should be available");
|
||||
|
||||
// In milliseconds
|
||||
((performance.now() - start) / 1000.0).round() as u64
|
||||
};
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let elapsed_s = { start.elapsed().as_secs() };
|
||||
|
||||
if elapsed_s > max_secs {
|
||||
return Some(
|
||||
format!("Turn ran for more than {max_secs} seconds, exiting.").into(),
|
||||
);
|
||||
}
|
||||
|
||||
return None;
|
||||
});
|
||||
|
||||
// Do not use FULL, rand functions are not pure
|
||||
engine.set_optimization_level(OptimizationLevel::Simple);
|
||||
|
||||
engine.disable_symbol("eval");
|
||||
engine.set_max_expr_depths(100, 100);
|
||||
engine.set_max_strings_interned(1024);
|
||||
engine.set_strict_variables(false);
|
||||
engine.on_print({
|
||||
let callback = print_callback.clone();
|
||||
move |s| callback(s)
|
||||
});
|
||||
engine.on_debug(move |text, source, pos| {
|
||||
debug_callback(&match (source, pos) {
|
||||
(Some(source), Position::NONE) => format!("{source} | {text}"),
|
||||
(Some(source), pos) => format!("{source} @ {pos:?} | {text}"),
|
||||
(None, Position::NONE) => format!("{text}"),
|
||||
(None, pos) => format!("{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", {
|
||||
let rng = rng.clone();
|
||||
move |from: i64, to: i64| rng.lock().gen_range(from..=to)
|
||||
})
|
||||
.register_fn("rand_bool", {
|
||||
let rng = rng.clone();
|
||||
move |p: f32| rng.lock().gen_bool(p as f64)
|
||||
})
|
||||
.register_fn("rand_symb", {
|
||||
let rng = rng.clone();
|
||||
move || Symb::new_random(&mut *rng.lock()).to_string()
|
||||
})
|
||||
.register_fn("rand_action", {
|
||||
let rng = rng.clone();
|
||||
move |board: Board| PlayerAction::new_random(&mut *rng.lock(), &board)
|
||||
})
|
||||
.register_fn("rand_shuffle", {
|
||||
let rng = rng.clone();
|
||||
move |mut vec: Vec<Dynamic>| {
|
||||
vec.shuffle(&mut *rng.lock());
|
||||
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)?;
|
||||
let scope = Scope::new(); // Not used
|
||||
|
||||
Ok(Self {
|
||||
rng,
|
||||
engine,
|
||||
script,
|
||||
scope,
|
||||
print_callback,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn print(&self, text: &str) {
|
||||
(self.print_callback)(text);
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Rng + 'static> Agent for Rhai<R> {
|
||||
type ErrorType = EvalAltResult;
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"Rhai"
|
||||
}
|
||||
|
||||
fn step_min(&mut self, board: &Board) -> Result<PlayerAction, Self::ErrorType> {
|
||||
let res = self.engine.call_fn_with_options::<PlayerAction>(
|
||||
CallFnOptions::new().eval_ast(false),
|
||||
&mut self.scope,
|
||||
&self.script,
|
||||
"step_min",
|
||||
(board.clone(),),
|
||||
);
|
||||
|
||||
match res {
|
||||
Ok(x) => Ok(x),
|
||||
Err(err) => Err(*err),
|
||||
}
|
||||
}
|
||||
|
||||
fn step_max(&mut self, board: &Board) -> Result<PlayerAction, Self::ErrorType> {
|
||||
let res = self.engine.call_fn_with_options::<PlayerAction>(
|
||||
CallFnOptions::new().eval_ast(false),
|
||||
&mut self.scope,
|
||||
&self.script,
|
||||
"step_max",
|
||||
(board.clone(),),
|
||||
);
|
||||
|
||||
match res {
|
||||
Ok(x) => Ok(x),
|
||||
Err(err) => Err(*err),
|
||||
}
|
||||
}
|
||||
}
|
||||
596
rust/minimax/src/board/board.rs
Normal file
596
rust/minimax/src/board/board.rs
Normal file
@@ -0,0 +1,596 @@
|
||||
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 super::{PlayerAction, TreeElement};
|
||||
use crate::util::Symb;
|
||||
|
||||
#[derive(Debug)]
|
||||
enum InterTreeElement {
|
||||
Unprocessed(Token),
|
||||
Processed(TreeElement),
|
||||
}
|
||||
|
||||
impl InterTreeElement {
|
||||
fn to_value(&self) -> Option<TreeElement> {
|
||||
Some(match self {
|
||||
InterTreeElement::Processed(x) => x.clone(),
|
||||
InterTreeElement::Unprocessed(Token::Value(s)) => {
|
||||
if let Some(s) = s.strip_prefix('-') {
|
||||
TreeElement::Neg {
|
||||
r: {
|
||||
if s.contains('_') {
|
||||
Box::new(TreeElement::Partial(s.to_string()))
|
||||
} else {
|
||||
Box::new(TreeElement::Number(match s.parse() {
|
||||
Ok(x) => x,
|
||||
_ => return None,
|
||||
}))
|
||||
}
|
||||
},
|
||||
}
|
||||
} else if s.contains('_') {
|
||||
TreeElement::Partial(s.to_string())
|
||||
} else {
|
||||
TreeElement::Number(match s.parse() {
|
||||
Ok(x) => x,
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
}
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
enum Token {
|
||||
Value(String),
|
||||
OpAdd,
|
||||
OpSub,
|
||||
OpMult,
|
||||
OpDiv,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Board {
|
||||
board: [Option<Symb>; 11],
|
||||
placed_by: [Option<String>; 11],
|
||||
|
||||
/// Number of Nones in `board`
|
||||
free_spots: usize,
|
||||
|
||||
/// Index of the last board index that was changed
|
||||
last_placed: Option<usize>,
|
||||
}
|
||||
|
||||
impl Display for Board {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
for c in self.board {
|
||||
write!(f, "{}", c.map(|s| s.get_char().unwrap()).unwrap_or('_'))?
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
Self {
|
||||
free_spots: 11,
|
||||
board: Default::default(),
|
||||
placed_by: Default::default(),
|
||||
last_placed: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_board(&self) -> &[Option<Symb>; 11] {
|
||||
&self.board
|
||||
}
|
||||
|
||||
pub fn get_board_mut(&mut self) -> &mut [Option<Symb>; 11] {
|
||||
&mut self.board
|
||||
}
|
||||
|
||||
/// Get the index of the ith empty slot
|
||||
pub fn ith_empty_slot(&self, mut idx: usize) -> Option<usize> {
|
||||
for (i, c) in self.board.iter().enumerate() {
|
||||
if c.is_none() {
|
||||
if idx == 0 {
|
||||
return Some(i);
|
||||
}
|
||||
idx -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
if idx == 0 {
|
||||
Some(self.board.len() - 1)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_full(&self) -> bool {
|
||||
self.free_spots == 0
|
||||
}
|
||||
|
||||
pub fn prettyprint(&self) -> String {
|
||||
const RESET: &str = "\x1b[0m";
|
||||
const MAGENTA: &str = "\x1b[35m";
|
||||
|
||||
let mut s = String::new();
|
||||
// Print board
|
||||
for (i, (symb, _)) in self.board.iter().zip(self.placed_by.iter()).enumerate() {
|
||||
match symb {
|
||||
Some(symb) => write!(
|
||||
s,
|
||||
"{}{}{RESET}",
|
||||
// If index matches last placed, draw symbol in red.
|
||||
// If last_placed is None, this check will always fail
|
||||
// since self.board.len is always greater than i.
|
||||
if self.last_placed.unwrap_or(self.board.len()) == i {
|
||||
MAGENTA
|
||||
} else {
|
||||
RESET
|
||||
},
|
||||
symb,
|
||||
)
|
||||
.unwrap(),
|
||||
|
||||
None => write!(s, "_").unwrap(),
|
||||
}
|
||||
}
|
||||
|
||||
s
|
||||
}
|
||||
|
||||
pub fn size(&self) -> usize {
|
||||
self.board.len()
|
||||
}
|
||||
|
||||
pub fn get_last_placed(&self) -> Option<usize> {
|
||||
self.last_placed
|
||||
}
|
||||
|
||||
pub fn contains(&self, s: Symb) -> bool {
|
||||
self.board.iter().contains(&Some(s))
|
||||
}
|
||||
|
||||
/// Is the given action valid?
|
||||
pub fn can_play(&self, action: &PlayerAction) -> bool {
|
||||
match &self.board[action.pos] {
|
||||
Some(_) => return false,
|
||||
None => {
|
||||
// Check for duplicate symbols
|
||||
if self.contains(action.symb) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check syntax
|
||||
match action.symb {
|
||||
Symb::Minus => {
|
||||
if action.pos == self.board.len() - 1 {
|
||||
return false;
|
||||
}
|
||||
|
||||
let r = &self.board[action.pos + 1];
|
||||
if r.is_some_and(|s| s.is_op() && !s.is_minus()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Symb::Zero => {
|
||||
if action.pos != 0 {
|
||||
let l = &self.board[action.pos - 1];
|
||||
if l == &Some(Symb::Div) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Symb::Div | Symb::Plus | Symb::Times => {
|
||||
if action.pos == 0 || action.pos == self.board.len() - 1 {
|
||||
return false;
|
||||
}
|
||||
|
||||
let l = &self.board[action.pos - 1];
|
||||
let r = &self.board[action.pos + 1];
|
||||
|
||||
if action.symb == Symb::Div && r == &Some(Symb::Zero) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if l.is_some_and(|s| s.is_op())
|
||||
|| r.is_some_and(|s| s.is_op() && !s.is_minus())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
/// Place the marked symbol at the given position.
|
||||
/// Returns true for valid moves and false otherwise.
|
||||
pub fn play(&mut self, action: PlayerAction, player: impl Into<String>) -> bool {
|
||||
if !self.can_play(&action) {
|
||||
return false;
|
||||
}
|
||||
|
||||
self.board[action.pos] = Some(action.symb);
|
||||
self.placed_by[action.pos] = Some(player.into());
|
||||
self.free_spots -= 1;
|
||||
self.last_placed = Some(action.pos);
|
||||
true
|
||||
}
|
||||
|
||||
fn tokenize(&self) -> Vec<Token> {
|
||||
let mut tokens = Vec::new();
|
||||
let mut is_neg = true; // if true, - is negative. if false, subtract.
|
||||
let mut current_num = String::new();
|
||||
|
||||
for s in self.board.iter() {
|
||||
match s {
|
||||
Some(Symb::Div) => {
|
||||
if !current_num.is_empty() {
|
||||
tokens.push(Token::Value(current_num.clone()));
|
||||
current_num.clear();
|
||||
}
|
||||
tokens.push(Token::OpDiv);
|
||||
is_neg = true;
|
||||
}
|
||||
Some(Symb::Minus) => {
|
||||
if is_neg {
|
||||
current_num = format!("-{}", current_num);
|
||||
} else {
|
||||
if !current_num.is_empty() {
|
||||
tokens.push(Token::Value(current_num.clone()));
|
||||
current_num.clear();
|
||||
}
|
||||
tokens.push(Token::OpSub);
|
||||
is_neg = true;
|
||||
}
|
||||
}
|
||||
Some(Symb::Plus) => {
|
||||
if !current_num.is_empty() {
|
||||
tokens.push(Token::Value(current_num.clone()));
|
||||
current_num.clear();
|
||||
}
|
||||
tokens.push(Token::OpAdd);
|
||||
is_neg = true;
|
||||
}
|
||||
Some(Symb::Times) => {
|
||||
if !current_num.is_empty() {
|
||||
tokens.push(Token::Value(current_num.clone()));
|
||||
current_num.clear();
|
||||
}
|
||||
tokens.push(Token::OpMult);
|
||||
is_neg = true;
|
||||
}
|
||||
Some(Symb::Zero) => {
|
||||
current_num.push('0');
|
||||
is_neg = false;
|
||||
}
|
||||
Some(Symb::Number(x)) => {
|
||||
current_num.push_str(&x.to_string());
|
||||
is_neg = false;
|
||||
}
|
||||
None => {
|
||||
current_num.push('_');
|
||||
is_neg = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !current_num.is_empty() {
|
||||
tokens.push(Token::Value(current_num.clone()));
|
||||
}
|
||||
|
||||
tokens
|
||||
}
|
||||
|
||||
pub fn to_tree(&self) -> Option<TreeElement> {
|
||||
let tokens = self.tokenize();
|
||||
|
||||
let mut tree: Vec<_> = tokens
|
||||
.iter()
|
||||
.map(|x| InterTreeElement::Unprocessed(x.clone()))
|
||||
.collect();
|
||||
|
||||
let mut priority_level = 0;
|
||||
let mut did_something;
|
||||
while tree.len() > 1 {
|
||||
did_something = false;
|
||||
for i in 0..tree.len() {
|
||||
if match priority_level {
|
||||
0 => matches!(
|
||||
tree[i],
|
||||
InterTreeElement::Unprocessed(Token::OpMult)
|
||||
| InterTreeElement::Unprocessed(Token::OpDiv)
|
||||
),
|
||||
1 => matches!(
|
||||
tree[i],
|
||||
InterTreeElement::Unprocessed(Token::OpAdd)
|
||||
| InterTreeElement::Unprocessed(Token::OpSub)
|
||||
),
|
||||
_ => false,
|
||||
} {
|
||||
did_something = true;
|
||||
|
||||
if i == 0 || i + 1 >= tree.len() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let l = tree[i - 1].to_value()?;
|
||||
let r = tree[i + 1].to_value()?;
|
||||
|
||||
let v = match tree[i] {
|
||||
InterTreeElement::Unprocessed(Token::OpAdd) => TreeElement::Add {
|
||||
l: Box::new(l),
|
||||
r: Box::new(r),
|
||||
},
|
||||
InterTreeElement::Unprocessed(Token::OpDiv) => TreeElement::Div {
|
||||
l: Box::new(l),
|
||||
r: Box::new(r),
|
||||
},
|
||||
InterTreeElement::Unprocessed(Token::OpMult) => TreeElement::Mul {
|
||||
l: Box::new(l),
|
||||
r: Box::new(r),
|
||||
},
|
||||
InterTreeElement::Unprocessed(Token::OpSub) => TreeElement::Sub {
|
||||
l: Box::new(l),
|
||||
r: Box::new(r),
|
||||
},
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
tree.remove(i - 1);
|
||||
tree.remove(i - 1);
|
||||
tree[i - 1] = InterTreeElement::Processed(v);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if !did_something {
|
||||
priority_level += 1;
|
||||
}
|
||||
}
|
||||
|
||||
Some(match tree.into_iter().next().unwrap() {
|
||||
InterTreeElement::Processed(x) => x,
|
||||
x => x.to_value()?,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn evaluate(&self) -> Option<f32> {
|
||||
self.to_tree()?.evaluate()
|
||||
}
|
||||
|
||||
pub fn from_board(board: [Option<Symb>; 11]) -> Self {
|
||||
let free_spots = board.iter().filter(|x| x.is_none()).count();
|
||||
Self {
|
||||
board,
|
||||
placed_by: Default::default(),
|
||||
free_spots,
|
||||
last_placed: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse a board from a string
|
||||
pub fn from_string(s: &str) -> Option<Self> {
|
||||
if s.len() != 11 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let x = s
|
||||
.chars()
|
||||
.filter_map(|c| {
|
||||
if c == '_' {
|
||||
Some(None)
|
||||
} else {
|
||||
Symb::from_char(c).map(Some)
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if x.len() != 11 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut free_spots = 11;
|
||||
let mut board = [None; 11];
|
||||
for i in 0..x.len() {
|
||||
board[i] = x[i];
|
||||
if x[i].is_some() {
|
||||
free_spots -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
Some(Self {
|
||||
board,
|
||||
placed_by: Default::default(),
|
||||
free_spots,
|
||||
last_placed: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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, "NONE".to_owned()) // 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("NONE".to_owned()); // 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("NULL".to_owned()); // Arbitrary
|
||||
}
|
||||
}
|
||||
|
||||
return Ok(());
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
77
rust/minimax/src/board/mod.rs
Normal file
77
rust/minimax/src/board/mod.rs
Normal file
@@ -0,0 +1,77 @@
|
||||
#[allow(clippy::module_inception)]
|
||||
mod board;
|
||||
mod tree;
|
||||
|
||||
use rand::Rng;
|
||||
use rhai::{CustomType, EvalAltResult, TypeBuilder};
|
||||
use std::fmt::Display;
|
||||
|
||||
pub use board::Board;
|
||||
pub use tree::TreeElement;
|
||||
|
||||
use crate::util::Symb;
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct PlayerAction {
|
||||
pub symb: Symb,
|
||||
pub pos: usize,
|
||||
}
|
||||
|
||||
impl Display for PlayerAction {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
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);
|
||||
}
|
||||
}
|
||||
143
rust/minimax/src/board/tree.rs
Normal file
143
rust/minimax/src/board/tree.rs
Normal file
@@ -0,0 +1,143 @@
|
||||
use std::fmt::{Debug, Display};
|
||||
|
||||
#[derive(PartialEq, Clone)]
|
||||
pub enum TreeElement {
|
||||
Partial(String),
|
||||
Number(f32),
|
||||
Add {
|
||||
l: Box<TreeElement>,
|
||||
r: Box<TreeElement>,
|
||||
},
|
||||
Sub {
|
||||
l: Box<TreeElement>,
|
||||
r: Box<TreeElement>,
|
||||
},
|
||||
Mul {
|
||||
l: Box<TreeElement>,
|
||||
r: Box<TreeElement>,
|
||||
},
|
||||
Div {
|
||||
l: Box<TreeElement>,
|
||||
r: Box<TreeElement>,
|
||||
},
|
||||
Neg {
|
||||
r: Box<TreeElement>,
|
||||
},
|
||||
}
|
||||
|
||||
impl Display for TreeElement {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Partial(s) => write!(f, "{s}")?,
|
||||
Self::Number(n) => write!(f, "{n}")?,
|
||||
Self::Add { l, r } => write!(f, "({l}+{r})")?,
|
||||
Self::Div { l, r } => write!(f, "({l}÷{r})")?,
|
||||
Self::Mul { l, r } => write!(f, "({l}×{r})")?,
|
||||
Self::Sub { l, r } => write!(f, "({l}-{r})")?,
|
||||
Self::Neg { r } => write!(f, "(-{r})")?,
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for TreeElement {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
Display::fmt(&self, f)
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
impl TreeElement {
|
||||
pub fn left(&self) -> Option<&TreeElement> {
|
||||
match self {
|
||||
Self::Add { l, .. }
|
||||
| Self::Sub { l, .. }
|
||||
| Self::Mul { l, .. }
|
||||
| Self::Div { l, .. } => Some(&**l),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn right(&self) -> Option<&TreeElement> {
|
||||
match self {
|
||||
Self::Add { r, .. }
|
||||
| Self::Neg { r, .. }
|
||||
| Self::Sub { r, .. }
|
||||
| Self::Mul { r, .. }
|
||||
| Self::Div { r, .. } => Some(&**r),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn left_mut(&mut self) -> Option<&mut TreeElement> {
|
||||
match self {
|
||||
Self::Add { l, .. }
|
||||
| Self::Sub { l, .. }
|
||||
| Self::Mul { l, .. }
|
||||
| Self::Div { l, .. } => Some(&mut **l),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn right_mut(&mut self) -> Option<&mut TreeElement> {
|
||||
match self {
|
||||
Self::Add { r, .. }
|
||||
| Self::Neg { r, .. }
|
||||
| Self::Sub { r, .. }
|
||||
| Self::Mul { r, .. }
|
||||
| Self::Div { r, .. } => Some(&mut **r),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn evaluate(&self) -> Option<f32> {
|
||||
match self {
|
||||
Self::Number(x) => Some(*x),
|
||||
// Try to parse strings of a partial
|
||||
Self::Partial(s) => s.parse().ok(),
|
||||
Self::Add { l, r } => {
|
||||
let l = l.evaluate();
|
||||
let r = r.evaluate();
|
||||
if let (Some(l), Some(r)) = (l, r) {
|
||||
Some(l + r)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
Self::Mul { l, r } => {
|
||||
let l = l.evaluate();
|
||||
let r = r.evaluate();
|
||||
if let (Some(l), Some(r)) = (l, r) {
|
||||
Some(l * r)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
Self::Div { l, r } => {
|
||||
let l = l.evaluate();
|
||||
let r = r.evaluate();
|
||||
|
||||
if r == Some(0.0) {
|
||||
None
|
||||
} else if let (Some(l), Some(r)) = (l, r) {
|
||||
Some(l / r)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
Self::Sub { l, r } => {
|
||||
let l = l.evaluate();
|
||||
let r = r.evaluate();
|
||||
if let (Some(l), Some(r)) = (l, r) {
|
||||
Some(l - r)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
Self::Neg { r } => {
|
||||
let r = r.evaluate();
|
||||
r.map(|r| -r)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
3
rust/minimax/src/lib.rs
Normal file
3
rust/minimax/src/lib.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
pub mod agents;
|
||||
pub mod board;
|
||||
pub mod util;
|
||||
111
rust/minimax/src/util.rs
Normal file
111
rust/minimax/src/util.rs
Normal file
@@ -0,0 +1,111 @@
|
||||
use std::{
|
||||
fmt::{Debug, Display},
|
||||
num::NonZeroU8,
|
||||
};
|
||||
|
||||
use rand::Rng;
|
||||
|
||||
#[derive(PartialEq, Eq, Clone, Copy, Hash)]
|
||||
|
||||
pub enum Symb {
|
||||
Number(NonZeroU8),
|
||||
Zero,
|
||||
Plus,
|
||||
Minus,
|
||||
Times,
|
||||
Div,
|
||||
}
|
||||
|
||||
impl Display for Symb {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Number(x) => write!(f, "{x}")?,
|
||||
Self::Zero => write!(f, "0")?,
|
||||
Self::Plus => write!(f, "+")?,
|
||||
Self::Minus => write!(f, "-")?,
|
||||
Self::Div => write!(f, "÷")?,
|
||||
Self::Times => write!(f, "×")?,
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
impl Debug for Symb {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
Display::fmt(self, f)
|
||||
}
|
||||
}
|
||||
|
||||
impl Symb {
|
||||
/// Is this symbol a plain binary operator?
|
||||
pub fn is_op(&self) -> bool {
|
||||
matches!(self, Symb::Div | Symb::Plus | Symb::Times | Symb::Minus)
|
||||
}
|
||||
|
||||
pub fn is_minus(&self) -> bool {
|
||||
self == &Self::Minus
|
||||
}
|
||||
|
||||
pub fn new_random<R: Rng>(rng: &mut R) -> Self {
|
||||
match rng.gen_range(0..=13) {
|
||||
0 => Symb::Zero,
|
||||
n @ 1..=9 => Symb::Number(NonZeroU8::new(n).unwrap()),
|
||||
10 => Symb::Div,
|
||||
11 => Symb::Minus,
|
||||
12 => Symb::Plus,
|
||||
13 => Symb::Times,
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn get_char(&self) -> Option<char> {
|
||||
match self {
|
||||
Self::Plus => Some('+'),
|
||||
Self::Minus => Some('-'),
|
||||
Self::Times => Some('×'),
|
||||
Self::Div => Some('÷'),
|
||||
Self::Zero => Some('0'),
|
||||
Self::Number(x) => match x.get() {
|
||||
1 => Some('1'),
|
||||
2 => Some('2'),
|
||||
3 => Some('3'),
|
||||
4 => Some('4'),
|
||||
5 => Some('5'),
|
||||
6 => Some('6'),
|
||||
7 => Some('7'),
|
||||
8 => Some('8'),
|
||||
9 => Some('9'),
|
||||
_ => None,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
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) })),
|
||||
'2' => Some(Self::Number(unsafe { NonZeroU8::new_unchecked(2) })),
|
||||
'3' => Some(Self::Number(unsafe { NonZeroU8::new_unchecked(3) })),
|
||||
'4' => Some(Self::Number(unsafe { NonZeroU8::new_unchecked(4) })),
|
||||
'5' => Some(Self::Number(unsafe { NonZeroU8::new_unchecked(5) })),
|
||||
'6' => Some(Self::Number(unsafe { NonZeroU8::new_unchecked(6) })),
|
||||
'7' => Some(Self::Number(unsafe { NonZeroU8::new_unchecked(7) })),
|
||||
'8' => Some(Self::Number(unsafe { NonZeroU8::new_unchecked(8) })),
|
||||
'9' => Some(Self::Number(unsafe { NonZeroU8::new_unchecked(9) })),
|
||||
'0' => Some(Self::Zero),
|
||||
'+' => Some(Self::Plus),
|
||||
'-' => Some(Self::Minus),
|
||||
'*' => Some(Self::Times),
|
||||
'/' => Some(Self::Div),
|
||||
'×' => Some(Self::Times),
|
||||
'÷' => Some(Self::Div),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user