Internal tweaks

master
Mark 2024-03-05 11:43:16 -08:00
parent 171d8b8d6c
commit 76a1bd423c
Signed by: Mark
GPG Key ID: C6D63995FE72FD80
8 changed files with 201 additions and 82 deletions

View File

@ -1,17 +1,23 @@
use anyhow::Result;
use super::{MaximizerAgent, MinimizerAgent, SimpleMinimax};
use super::{Agent, MaximizerAgent, MinimizerAgent, Random, SimpleMinimax};
use crate::{
board::{Board, PlayerAction},
util::Symb,
util::{Player, Symb},
};
/// A simple "operator diffusion" MINIMIZER agent.
///
/// Tries to keep operators as far apart as possible, denying large numbers.
pub struct Diffuse {}
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 {
@ -60,7 +66,7 @@ impl Diffuse {
}
if max_dist == 0 {
panic!("there is no valid move!")
return Random::new(self.player).step_max(board).unwrap();
}
max_dist -= 1;
@ -68,6 +74,16 @@ impl Diffuse {
}
}
impl Agent for Diffuse {
fn name(&self) -> &'static str {
"Diffuse"
}
fn player(&self) -> Player {
self.player
}
}
impl MinimizerAgent for Diffuse {
fn step_min(&mut self, board: &Board) -> Result<PlayerAction> {
let symb = [Symb::Minus, Symb::Times, Symb::Plus, Symb::Div]
@ -78,7 +94,7 @@ impl MinimizerAgent for Diffuse {
Ok(self.step_symb(board, *symb))
} else {
// No symbols available, play a random number
SimpleMinimax {}.step_min(board)
SimpleMinimax::new(self.player).step_min(board)
}
}
}
@ -93,7 +109,7 @@ impl MaximizerAgent for Diffuse {
Ok(self.step_symb(board, *symb))
} else {
// No symbols available, play a random number
SimpleMinimax {}.step_max(board)
SimpleMinimax::new(self.player).step_max(board)
}
}
}

View File

@ -9,7 +9,7 @@ use crate::{
util::{Player, Symb},
};
use super::{MaximizerAgent, MinimizerAgent};
use super::{Agent, MaximizerAgent, MinimizerAgent};
struct SymbolSelector {
symbols: Vec<char>,
@ -68,13 +68,13 @@ impl SymbolSelector {
}
}
pub struct PlayerAgent {
pub struct Human {
player: Player,
cursor: usize,
symbol_selector: SymbolSelector,
}
impl PlayerAgent {
impl Human {
pub fn new(player: Player) -> Self {
Self {
player,
@ -115,11 +115,7 @@ impl PlayerAgent {
.iter()
.map(|x| {
if board.contains(Symb::from_char(*x).unwrap()) {
format!(
"{}{x}{}",
color::Fg(color::LightBlack),
color::Fg(color::Reset),
)
" ".to_string()
} else if *x == self.symbol_selector.current() {
format!(
"{}{x}{}",
@ -138,6 +134,9 @@ impl PlayerAgent {
let stdin = stdin();
for c in stdin.keys() {
match c.unwrap() {
Key::Ctrl('c') => {
panic!("ctrl-c, exitint")
}
Key::Char('q') => bail!("player ended game"),
Key::Right => {
self.cursor = cursor_max.min(self.cursor + 1);
@ -190,13 +189,23 @@ impl PlayerAgent {
}
}
impl MinimizerAgent for PlayerAgent {
impl Agent for Human {
fn name(&self) -> &'static str {
"Player"
}
fn player(&self) -> Player {
self.player
}
}
impl MinimizerAgent for Human {
fn step_min(&mut self, board: &Board) -> Result<PlayerAction> {
self.step(board, true)
}
}
impl MaximizerAgent for PlayerAgent {
impl MaximizerAgent for Human {
fn step_max(&mut self, board: &Board) -> Result<PlayerAction> {
self.step(board, false)
}

View File

@ -1,16 +1,22 @@
use anyhow::Result;
use anyhow::{bail, Result};
use std::num::NonZeroU8;
use super::{MaximizerAgent, MinimizerAgent, RandomAgent};
use super::{Agent, MaximizerAgent, MinimizerAgent, Random};
use crate::{
agents::util::free_slots_by_influence,
board::{Board, PlayerAction},
util::Symb,
util::{Player, Symb},
};
pub struct SimpleMinimax {}
pub struct SimpleMinimax {
player: Player,
}
impl SimpleMinimax {
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 {
@ -24,34 +30,64 @@ impl SimpleMinimax {
// 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 RandomAgent {}.step_min(board);
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.len() == 0 {
return RandomAgent {}.step_min(board);
return Random::new(self.player).step_min(board);
}
let (pos, val) = t[0];
let symb = {
if minimize {
if val >= 0.0 {
available_numbers[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 {
available_numbers[available_numbers.len() - 1]
if val <= 0.0 {
available_numbers[offset]
} else {
available_numbers[available_numbers.len() - 1 - offset]
}
}
} else {
if val <= 0.0 {
available_numbers[0]
} else {
available_numbers[available_numbers.len() - 1]
}
}
};
});
offset += 1;
}
Ok(PlayerAction { symb, pos })
Ok(PlayerAction {
symb: symb.unwrap(),
pos,
})
}
}
impl Agent for SimpleMinimax {
fn name(&self) -> &'static str {
"Minimax"
}
fn player(&self) -> Player {
self.player
}
}

View File

@ -1,16 +1,22 @@
use crate::{
board::{Board, PlayerAction},
util::Symb,
util::{Player, Symb},
};
use anyhow::Result;
use rand::Rng;
use std::num::NonZeroU8;
use super::{MaximizerAgent, MinimizerAgent};
use super::{Agent, MaximizerAgent, MinimizerAgent};
pub struct RandomAgent {}
pub struct Random {
player: Player,
}
impl Random {
pub fn new(player: Player) -> Self {
Self { player }
}
impl RandomAgent {
fn random_action(&self, board: &Board) -> PlayerAction {
let mut rng = rand::thread_rng();
let n = board.size();
@ -36,7 +42,17 @@ impl RandomAgent {
}
}
impl MinimizerAgent for RandomAgent {
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) {
@ -46,7 +62,7 @@ impl MinimizerAgent for RandomAgent {
}
}
impl MaximizerAgent for RandomAgent {
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) {

View File

@ -8,14 +8,21 @@ 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) -> Vec<(usize, f32)> {
pub fn free_slots_by_influence(board: &Board) -> Option<Vec<(usize, f32)>> {
// Fill all empty slots with fives and compute starting value
let filled = Board::from_board(board.get_board().map(|x| match x {
None => Symb::from_char('5'),
_ => x,
}));
let filled = {
// 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 f = Board::from_board(board.get_board().map(|x| match x {
None => Symb::from_char('5'),
_ => x,
}));
let base = filled.evaluate().unwrap();
f
};
let base = filled.evaluate()?;
// Test each slot:
// Increase its value by 1, and record its effect on the
@ -36,13 +43,17 @@ pub fn free_slots_by_influence(board: &Board) -> Vec<(usize, f32)> {
.collect();
// Sort by most to least influence
slots.sort_by(|a, b| b.1.abs().partial_cmp(&a.1.abs()).unwrap());
slots
slots.sort_by(|a, b| {
b.1.abs()
.partial_cmp(&a.1.abs())
.unwrap_or(std::cmp::Ordering::Equal)
});
Some(slots)
}
/// Find the maximum possible value of the given board
#[allow(dead_code)]
pub fn maximize_value(board: &Board) -> Board {
pub fn maximize_value(board: &Board) -> Option<Board> {
let n_free = board.get_board().iter().filter(|x| x.is_none()).count();
// Assume we have 10 or fewer available slots
@ -58,7 +69,7 @@ pub fn maximize_value(board: &Board) -> Board {
.filter(|x| !board.contains(*x))
.collect::<Vec<_>>();
let slots = free_slots_by_influence(&board);
let slots = free_slots_by_influence(&board)?;
let all_symbols = {
// We need this many from the bottom, and this many from the top.
@ -125,5 +136,5 @@ pub fn maximize_value(board: &Board) -> Board {
}
}
best_board.unwrap()
best_board
}

View File

@ -13,8 +13,8 @@ enum InterTreeElement {
}
impl InterTreeElement {
fn to_value(&self) -> TreeElement {
match self {
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('-') {
@ -23,18 +23,24 @@ impl InterTreeElement {
if s.contains('_') {
Box::new(TreeElement::Partial(s.to_string()))
} else {
Box::new(TreeElement::Number(s.parse().unwrap()))
Box::new(TreeElement::Number(match s.parse() {
Ok(x) => x,
_ => return None,
}))
}
},
}
} else if s.contains('_') {
TreeElement::Partial(s.to_string())
} else {
TreeElement::Number(s.parse().unwrap())
TreeElement::Number(match s.parse() {
Ok(x) => x,
_ => return None,
})
}
}
_ => unreachable!(),
}
_ => return None,
})
}
}
@ -96,7 +102,7 @@ impl Board {
pub fn prettyprint(&self) -> Result<String> {
let mut s = String::new();
// Print board
for (i, (symb, p)) in self.board.iter().zip(self.placed_by.iter()).enumerate() {
for (i, (symb, _)) in self.board.iter().zip(self.placed_by.iter()).enumerate() {
match symb {
Some(symb) => write!(
s,
@ -105,12 +111,9 @@ impl Board {
// 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 {
color::Fg(&color::Red as &dyn Color)
color::Fg(&color::Magenta as &dyn Color)
} else {
match p {
Some(player) => color::Fg(player.color()),
None => color::Fg(&color::Reset as &dyn Color),
}
color::Fg(&color::Reset as &dyn Color)
},
symb,
color::Fg(color::Reset)
@ -210,8 +213,10 @@ impl Board {
for s in self.board.iter() {
match s {
Some(Symb::Div) => {
tokens.push(Token::Value(current_num.clone()));
current_num.clear();
if !current_num.is_empty() {
tokens.push(Token::Value(current_num.clone()));
current_num.clear();
}
tokens.push(Token::OpDiv);
is_neg = true;
}
@ -219,21 +224,27 @@ impl Board {
if is_neg {
current_num = format!("-{}", current_num);
} else {
tokens.push(Token::Value(current_num.clone()));
current_num.clear();
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) => {
tokens.push(Token::Value(current_num.clone()));
current_num.clear();
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) => {
tokens.push(Token::Value(current_num.clone()));
current_num.clear();
if !current_num.is_empty() {
tokens.push(Token::Value(current_num.clone()));
current_num.clear();
}
tokens.push(Token::OpMult);
is_neg = true;
}
@ -252,11 +263,14 @@ impl Board {
}
}
tokens.push(Token::Value(current_num));
if !current_num.is_empty() {
tokens.push(Token::Value(current_num.clone()));
}
tokens
}
pub fn to_tree(&self) -> TreeElement {
pub fn to_tree(&self) -> Option<TreeElement> {
let tokens = self.tokenize();
let mut tree: Vec<_> = tokens
@ -283,8 +297,13 @@ impl Board {
_ => false,
} {
did_something = true;
let l = tree[i - 1].to_value();
let r = tree[i + 1].to_value();
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 {
@ -318,14 +337,14 @@ impl Board {
}
}
match tree.into_iter().next().unwrap() {
Some(match tree.into_iter().next().unwrap() {
InterTreeElement::Processed(x) => x,
x => x.to_value(),
}
x => x.to_value()?,
})
}
pub fn evaluate(&self) -> Option<f32> {
self.to_tree().evaluate()
self.to_tree()?.evaluate()
}
pub fn from_board(board: [Option<Symb>; 11]) -> Self {

View File

@ -2,12 +2,21 @@
mod board;
mod tree;
use std::fmt::Display;
pub use board::Board;
pub use tree::TreeElement;
use crate::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)
}
}

View File

@ -116,7 +116,10 @@ impl TreeElement {
Self::Div { l, r } => {
let l = l.evaluate();
let r = r.evaluate();
if let (Some(l), Some(r)) = (l, r) {
if r == Some(0.0) {
None
} else if let (Some(l), Some(r)) = (l, r) {
Some(l / r)
} else {
None