Internal tweaks
parent
171d8b8d6c
commit
76a1bd423c
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue