From 76a1bd423c41db64eaa38863eea5e82537563365 Mon Sep 17 00:00:00 2001 From: Mark Date: Tue, 5 Mar 2024 11:43:16 -0800 Subject: [PATCH] Internal tweaks --- src/agents/diffuse.rs | 28 ++++++++--- src/agents/{player.rs => human.rs} | 29 ++++++++---- src/agents/minimax.rs | 76 ++++++++++++++++++++++-------- src/agents/random.rs | 28 ++++++++--- src/agents/util/partials.rs | 33 ++++++++----- src/board/board.rs | 75 ++++++++++++++++++----------- src/board/mod.rs | 9 ++++ src/board/tree.rs | 5 +- 8 files changed, 201 insertions(+), 82 deletions(-) rename src/agents/{player.rs => human.rs} (91%) diff --git a/src/agents/diffuse.rs b/src/agents/diffuse.rs index a80e6a6..6838e33 100644 --- a/src/agents/diffuse.rs +++ b/src/agents/diffuse.rs @@ -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 { 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) } } } diff --git a/src/agents/player.rs b/src/agents/human.rs similarity index 91% rename from src/agents/player.rs rename to src/agents/human.rs index 5a72306..e971afd 100644 --- a/src/agents/player.rs +++ b/src/agents/human.rs @@ -9,7 +9,7 @@ use crate::{ util::{Player, Symb}, }; -use super::{MaximizerAgent, MinimizerAgent}; +use super::{Agent, MaximizerAgent, MinimizerAgent}; struct SymbolSelector { symbols: Vec, @@ -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 { self.step(board, true) } } -impl MaximizerAgent for PlayerAgent { +impl MaximizerAgent for Human { fn step_max(&mut self, board: &Board) -> Result { self.step(board, false) } diff --git a/src/agents/minimax.rs b/src/agents/minimax.rs index 675f863..a867da8 100644 --- a/src/agents/minimax.rs +++ b/src/agents/minimax.rs @@ -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 { 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 } } diff --git a/src/agents/random.rs b/src/agents/random.rs index bc3370b..f89255f 100644 --- a/src/agents/random.rs +++ b/src/agents/random.rs @@ -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 { 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 { let mut action = self.random_action(board); while !board.can_play(&action) { diff --git a/src/agents/util/partials.rs b/src/agents/util/partials.rs index 8fd9d42..d5cd2bc 100644 --- a/src/agents/util/partials.rs +++ b/src/agents/util/partials.rs @@ -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> { // 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 { 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::>(); - 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 } diff --git a/src/board/board.rs b/src/board/board.rs index f4f550e..3686cde 100644 --- a/src/board/board.rs +++ b/src/board/board.rs @@ -13,8 +13,8 @@ enum InterTreeElement { } impl InterTreeElement { - fn to_value(&self) -> TreeElement { - match self { + fn to_value(&self) -> Option { + 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 { 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 { 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 { - self.to_tree().evaluate() + self.to_tree()?.evaluate() } pub fn from_board(board: [Option; 11]) -> Self { diff --git a/src/board/mod.rs b/src/board/mod.rs index ecedd96..e2d17d7 100644 --- a/src/board/mod.rs +++ b/src/board/mod.rs @@ -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) + } +} diff --git a/src/board/tree.rs b/src/board/tree.rs index 602c42c..93e67af 100644 --- a/src/board/tree.rs +++ b/src/board/tree.rs @@ -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