Rhai
This commit is contained in:
133
src/agents.rhai/chase.rhai
Normal file
133
src/agents.rhai/chase.rhai
Normal 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)
|
||||
}
|
||||
109
src/agents.rhai/diffuse.rhai
Normal file
109
src/agents.rhai/diffuse.rhai
Normal 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);
|
||||
}
|
||||
21
src/agents.rhai/random.rhai
Normal file
21
src/agents.rhai/random.rhai
Normal 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)
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
@@ -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},
|
||||
|
||||
@@ -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
176
src/agents/rhai.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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(());
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
17
src/cli.rs
17
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<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)),
|
||||
}
|
||||
|
||||
14
src/main.rs
14
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()),
|
||||
|
||||
28
src/util.rs
28
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<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) })),
|
||||
|
||||
Reference in New Issue
Block a user