Reworked minimax agent
parent
14c524c599
commit
65e8eb7998
|
@ -0,0 +1,68 @@
|
||||||
|
use anyhow::Result;
|
||||||
|
use std::num::NonZeroU8;
|
||||||
|
|
||||||
|
use super::{MaximizerAgent, MinimizerAgent, RandomAgent};
|
||||||
|
use crate::{
|
||||||
|
agents::util::free_slots_by_influence,
|
||||||
|
board::{Board, PlayerAction},
|
||||||
|
util::Symb,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub struct SimpleMinimax {}
|
||||||
|
|
||||||
|
impl SimpleMinimax {
|
||||||
|
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 RandomAgent {}.step_min(board);
|
||||||
|
}
|
||||||
|
|
||||||
|
let t = free_slots_by_influence(&board);
|
||||||
|
|
||||||
|
if t.len() == 0 {
|
||||||
|
return RandomAgent {}.step_min(board);
|
||||||
|
}
|
||||||
|
|
||||||
|
let (pos, val) = t[0];
|
||||||
|
|
||||||
|
let symb = {
|
||||||
|
if minimize {
|
||||||
|
if val >= 0.0 {
|
||||||
|
available_numbers[0]
|
||||||
|
} else {
|
||||||
|
available_numbers[available_numbers.len() - 1]
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if val <= 0.0 {
|
||||||
|
available_numbers[0]
|
||||||
|
} else {
|
||||||
|
available_numbers[available_numbers.len() - 1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(PlayerAction { symb, pos })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MinimizerAgent for SimpleMinimax {
|
||||||
|
fn step_min(&mut self, board: &Board) -> Result<PlayerAction> {
|
||||||
|
self.step(board, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MaximizerAgent for SimpleMinimax {
|
||||||
|
fn step_max(&mut self, board: &Board) -> Result<PlayerAction> {
|
||||||
|
self.step(board, false)
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,216 +0,0 @@
|
||||||
use std::{iter, num::NonZeroU8};
|
|
||||||
|
|
||||||
use anyhow::Result;
|
|
||||||
use itertools::Itertools;
|
|
||||||
|
|
||||||
use super::{
|
|
||||||
util::{fill_partials, TreeCoords},
|
|
||||||
MaximizerAgent, MinimizerAgent, RandomAgent,
|
|
||||||
};
|
|
||||||
use crate::{
|
|
||||||
agents::util::{find_partials, free_chars},
|
|
||||||
board::{Board, PlayerAction, TreeElement},
|
|
||||||
util::Symb,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub struct MinMaxTree {}
|
|
||||||
|
|
||||||
fn find_best_numbers_v1<'a, F>(
|
|
||||||
tree: &TreeElement,
|
|
||||||
partials: &[TreeCoords],
|
|
||||||
numbers: impl Iterator<Item = &'a Symb>,
|
|
||||||
minimize: bool,
|
|
||||||
|
|
||||||
// Returns true if we want to maximize the given partial,
|
|
||||||
// and false if we want to fix it.
|
|
||||||
filter: F,
|
|
||||||
) -> Vec<Symb>
|
|
||||||
where
|
|
||||||
F: Fn(&&TreeCoords) -> bool,
|
|
||||||
{
|
|
||||||
// Fill maximizer slots with arbitrary numbers
|
|
||||||
let min_tree_base = fill_partials(
|
|
||||||
tree,
|
|
||||||
partials.iter().filter(|x| !filter(x)),
|
|
||||||
iter::repeat(&Symb::Number(NonZeroU8::new(5).unwrap())),
|
|
||||||
);
|
|
||||||
|
|
||||||
let partials_to_optimize: Vec<TreeCoords> = partials.iter().filter(filter).cloned().collect();
|
|
||||||
let n_empty = free_chars(tree, partials_to_optimize.iter()).len();
|
|
||||||
println!("{:?}", n_empty);
|
|
||||||
|
|
||||||
let trees: Vec<(f32, Vec<&Symb>)> = numbers
|
|
||||||
.permutations(n_empty)
|
|
||||||
.unique()
|
|
||||||
.filter_map(move |l| {
|
|
||||||
let mut i = l.iter();
|
|
||||||
let mut tmp_tree = min_tree_base.clone();
|
|
||||||
for p in &partials_to_optimize {
|
|
||||||
let x = p.get_from_mut(&mut tmp_tree).unwrap();
|
|
||||||
|
|
||||||
let x_str = match x {
|
|
||||||
TreeElement::Partial(s) => s,
|
|
||||||
_ => unreachable!(),
|
|
||||||
};
|
|
||||||
let mut new_str = String::new();
|
|
||||||
for c in x_str.chars() {
|
|
||||||
if c == '_' {
|
|
||||||
new_str.push_str(&format!("{}", i.next().unwrap()))
|
|
||||||
} else {
|
|
||||||
new_str.push(c);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*x = TreeElement::Number(new_str.parse().unwrap())
|
|
||||||
}
|
|
||||||
|
|
||||||
println!("{:?}", tmp_tree);
|
|
||||||
tmp_tree.evaluate().map(|x| (x, l))
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
let mut best_list: Option<Vec<&Symb>> = None;
|
|
||||||
let mut best_value: Option<f32> = None;
|
|
||||||
|
|
||||||
for (x, list) in trees {
|
|
||||||
if let Some(m) = best_value {
|
|
||||||
if (minimize && x < m) || (!minimize && x > m) {
|
|
||||||
best_value = Some(x);
|
|
||||||
best_list = Some(list);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
best_value = Some(x);
|
|
||||||
best_list = Some(list);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
best_list.unwrap().into_iter().cloned().collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn find_best_numbers(
|
|
||||||
tree: &TreeElement,
|
|
||||||
partials: &[TreeCoords],
|
|
||||||
|
|
||||||
// The numbers we're allowed to add, sorted in ascending order
|
|
||||||
available_numbers: &[Symb],
|
|
||||||
) -> TreeElement {
|
|
||||||
// Fill all empty slots with fives
|
|
||||||
let tree_filled = fill_partials(
|
|
||||||
tree,
|
|
||||||
partials.iter(),
|
|
||||||
iter::repeat(&Symb::Number(NonZeroU8::new(5).unwrap())),
|
|
||||||
);
|
|
||||||
|
|
||||||
let base = tree_filled.evaluate().unwrap();
|
|
||||||
|
|
||||||
// Test each slot:
|
|
||||||
// Increase its value by 1, and record its effect on the
|
|
||||||
// expression's total value.
|
|
||||||
// This isn't a perfect metric, but it's pretty good.
|
|
||||||
let mut slots: Vec<(usize, &TreeCoords, usize, f32)> = free_chars(tree, partials.iter())
|
|
||||||
.into_iter()
|
|
||||||
.enumerate()
|
|
||||||
.map(|(i_slot, (c, i))| {
|
|
||||||
let mut new_tree = tree_filled.clone();
|
|
||||||
let p = c.get_from_mut(&mut new_tree).unwrap();
|
|
||||||
match p {
|
|
||||||
TreeElement::Partial(s) => s.replace_range(i..i + 1, "6"),
|
|
||||||
_ => unreachable!(),
|
|
||||||
}
|
|
||||||
// This shouldn't ever be None.
|
|
||||||
(i_slot, c, i, new_tree.evaluate().unwrap() - base)
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
// Sort by least to most influence
|
|
||||||
slots.sort_by(|a, b| a.3.partial_cmp(&b.3).unwrap());
|
|
||||||
|
|
||||||
let all_symbols = {
|
|
||||||
// We need this many from the bottom, and this many from the top.
|
|
||||||
let neg_count = slots.iter().filter(|(_, _, _, x)| *x <= 0.0).count();
|
|
||||||
let pos_count = slots.iter().filter(|(_, _, _, x)| *x > 0.0).count();
|
|
||||||
|
|
||||||
let mut a_iter = available_numbers
|
|
||||||
.iter()
|
|
||||||
.take(neg_count)
|
|
||||||
.chain(available_numbers.iter().rev().take(pos_count).rev());
|
|
||||||
|
|
||||||
let mut g = slots
|
|
||||||
// Group slots with equal weights
|
|
||||||
// and count the number of elements in each group
|
|
||||||
.iter()
|
|
||||||
.group_by(|x| x.3)
|
|
||||||
.into_iter()
|
|
||||||
.map(|(_, x)| x.count())
|
|
||||||
// Generate the digits we should try for each group of
|
|
||||||
// equal-weight slots
|
|
||||||
.map(|s| {
|
|
||||||
(0..s)
|
|
||||||
.map(|_| a_iter.next().unwrap().clone())
|
|
||||||
.permutations(s)
|
|
||||||
.unique()
|
|
||||||
.collect_vec()
|
|
||||||
})
|
|
||||||
// Now, covert this to an array of all cartesian products
|
|
||||||
// of this set of sets
|
|
||||||
.multi_cartesian_product()
|
|
||||||
.map(|x| x.iter().flatten().cloned().collect_vec())
|
|
||||||
.map(|v| slots.iter().zip(v).collect_vec())
|
|
||||||
.collect_vec();
|
|
||||||
|
|
||||||
// Sort these vectors so the order of values
|
|
||||||
// matches the order of empty slots
|
|
||||||
g.iter_mut()
|
|
||||||
.for_each(|v| v.sort_by(|(a, _), (b, _)| a.0.partial_cmp(&b.0).unwrap()));
|
|
||||||
g.into_iter()
|
|
||||||
.map(|v| v.into_iter().map(|(_, s)| s).collect_vec())
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut best_tree = None;
|
|
||||||
let mut best_value = None;
|
|
||||||
for i in all_symbols {
|
|
||||||
let tmp_tree = fill_partials(&tree, partials.iter(), i.iter());
|
|
||||||
let val = tmp_tree.evaluate();
|
|
||||||
|
|
||||||
if let Some(val) = val {
|
|
||||||
if let Some(best) = best_value {
|
|
||||||
if val > best {
|
|
||||||
best_value = Some(val);
|
|
||||||
best_tree = Some(tmp_tree)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
best_value = Some(val);
|
|
||||||
best_tree = Some(tmp_tree)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
best_tree.unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
impl MinMaxTree {}
|
|
||||||
|
|
||||||
impl MinimizerAgent for MinMaxTree {
|
|
||||||
fn step_min(&mut self, board: &Board) -> Result<PlayerAction> {
|
|
||||||
let tree = board.to_tree();
|
|
||||||
let partials = find_partials(&tree);
|
|
||||||
|
|
||||||
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
|
|
||||||
// that is, min_slots + max_slots <= available_numbers.len
|
|
||||||
if available_numbers.len() < free_chars(&tree, partials.iter()).len() {
|
|
||||||
return RandomAgent {}.step_max(board);
|
|
||||||
}
|
|
||||||
|
|
||||||
let t = find_best_numbers(&tree, &partials, &available_numbers);
|
|
||||||
|
|
||||||
println!("{:?}", t);
|
|
||||||
RandomAgent {}.step_max(board)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,11 +1,11 @@
|
||||||
mod diffuse;
|
mod diffuse;
|
||||||
mod minmaxtree;
|
mod minimax;
|
||||||
mod player;
|
mod player;
|
||||||
mod random;
|
mod random;
|
||||||
pub mod util;
|
pub mod util;
|
||||||
|
|
||||||
pub use diffuse::DiffuseAgent;
|
pub use diffuse::Diffuse;
|
||||||
pub use minmaxtree::MinMaxTree;
|
pub use minimax::SimpleMinimax;
|
||||||
pub use player::PlayerAgent;
|
pub use player::PlayerAgent;
|
||||||
pub use random::RandomAgent;
|
pub use random::RandomAgent;
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,3 @@
|
||||||
/// Common helper functions that may be used by agents.
|
/// Common helper functions that may be used by agents.
|
||||||
mod partials;
|
mod partials;
|
||||||
mod treecoords;
|
|
||||||
|
|
||||||
pub use partials::*;
|
pub use partials::*;
|
||||||
pub use treecoords::*;
|
|
||||||
|
|
|
@ -1,96 +1,129 @@
|
||||||
use super::{TreeCoords, TreeDir};
|
use itertools::Itertools;
|
||||||
use crate::{board::TreeElement, util::Symb};
|
use std::num::NonZeroU8;
|
||||||
|
|
||||||
/// Find the coordinates of all partials in the given tree
|
use crate::{board::Board, util::Symb};
|
||||||
pub fn find_partials(tree: &TreeElement) -> Vec<TreeCoords> {
|
|
||||||
let mut partials = Vec::new();
|
|
||||||
let mut current_coords = TreeCoords::new();
|
|
||||||
|
|
||||||
loop {
|
/// Returns an iterator of (sort, coords, char_idx, f32) for each empty slot in the listed partials.
|
||||||
let t = current_coords.get_from(tree).unwrap();
|
/// - sort is the index of this slot.
|
||||||
match t {
|
/// - coords are the coordinate of this slot's partial
|
||||||
TreeElement::Number(_) | TreeElement::Partial(_) => {
|
/// - char_idx is the index of this slot in its partial
|
||||||
if let TreeElement::Partial(_) = t {
|
/// - f32 is the influence of this slot
|
||||||
partials.push(current_coords);
|
pub fn free_slots_by_influence(board: &Board) -> 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,
|
||||||
|
}));
|
||||||
|
|
||||||
loop {
|
let base = filled.evaluate().unwrap();
|
||||||
match current_coords.pop() {
|
|
||||||
Some((TreeDir::Left, _)) => {
|
|
||||||
current_coords.push(
|
|
||||||
TreeDir::Right,
|
|
||||||
match current_coords.get_from(tree) {
|
|
||||||
Some(TreeElement::Add { .. }) => current_coords.is_inverted(),
|
|
||||||
Some(TreeElement::Mul { .. }) => current_coords.is_inverted(),
|
|
||||||
Some(TreeElement::Sub { .. }) => !current_coords.is_inverted(),
|
|
||||||
Some(TreeElement::Div { .. }) => !current_coords.is_inverted(),
|
|
||||||
_ => unreachable!(),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
Some((TreeDir::Right, _)) => {}
|
|
||||||
Some((TreeDir::This, _)) => unreachable!(),
|
|
||||||
None => return partials,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
TreeElement::Div { .. }
|
|
||||||
| TreeElement::Mul { .. }
|
|
||||||
| TreeElement::Sub { .. }
|
|
||||||
| TreeElement::Add { .. } => current_coords.push(TreeDir::Left, current_coords.is_inverted()),
|
|
||||||
TreeElement::Neg { .. } => {
|
|
||||||
current_coords.push(TreeDir::Right, !current_coords.is_inverted())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Fill empty slots in the given partials, in order.
|
// Test each slot:
|
||||||
/// Will panic if we run out of numbers to fill with.
|
// Increase its value by 1, and record its effect on the
|
||||||
///
|
// expression's total value.
|
||||||
/// Returns a new tree with filled partials.
|
// This isn't a perfect metric, but it's pretty good.
|
||||||
pub fn fill_partials<'a>(
|
let mut slots: Vec<(usize, f32)> = board
|
||||||
tree: &'a TreeElement,
|
.get_board()
|
||||||
partials: impl Iterator<Item = &'a TreeCoords>,
|
.iter()
|
||||||
mut numbers: impl Iterator<Item = &'a Symb>,
|
.enumerate()
|
||||||
) -> TreeElement {
|
.filter_map(|(i, s)| if s.is_some() { None } else { Some(i) })
|
||||||
let mut tmp_tree = tree.clone();
|
.map(|i| {
|
||||||
for p in partials {
|
let mut new_tree = filled.clone();
|
||||||
let x = p.get_from_mut(&mut tmp_tree).unwrap();
|
new_tree.get_board_mut()[i] = Some(Symb::from_char('6').unwrap());
|
||||||
|
|
||||||
let x_str = match x {
|
// This shouldn't ever be None
|
||||||
TreeElement::Partial(s) => s,
|
(i, new_tree.evaluate().unwrap() - base)
|
||||||
_ => unreachable!(),
|
|
||||||
};
|
|
||||||
let mut new_str = String::new();
|
|
||||||
for c in x_str.chars() {
|
|
||||||
if c == '_' {
|
|
||||||
new_str.push_str(&format!("{}", numbers.next().unwrap()))
|
|
||||||
} else {
|
|
||||||
new_str.push(c);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*x = TreeElement::Partial(new_str)
|
|
||||||
}
|
|
||||||
|
|
||||||
tmp_tree
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Find all empty slots in the given partials
|
|
||||||
/// Returns (coords of partial, index of slot in string)
|
|
||||||
pub fn free_chars<'a>(
|
|
||||||
tree: &'a TreeElement,
|
|
||||||
partials: impl Iterator<Item = &'a TreeCoords>,
|
|
||||||
) -> Vec<(&TreeCoords, usize)> {
|
|
||||||
partials
|
|
||||||
.flat_map(|x| match x.get_from(tree) {
|
|
||||||
Some(TreeElement::Partial(s)) => {
|
|
||||||
s.chars()
|
|
||||||
.enumerate()
|
|
||||||
.filter_map(move |(i, c)| if c == '_' { Some((x, i)) } else { None })
|
|
||||||
}
|
|
||||||
_ => unreachable!(),
|
|
||||||
})
|
})
|
||||||
.collect()
|
.collect();
|
||||||
|
|
||||||
|
// Sort by most to least influence
|
||||||
|
slots.sort_by(|a, b| b.1.abs().partial_cmp(&a.1.abs()).unwrap());
|
||||||
|
slots
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Find the maximum possible value of the given board
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn maximize_value(board: &Board) -> Board {
|
||||||
|
let n_free = board.get_board().iter().filter(|x| x.is_none()).count();
|
||||||
|
|
||||||
|
// Assume we have 10 or fewer available slots
|
||||||
|
if n_free >= 10 {
|
||||||
|
panic!()
|
||||||
|
}
|
||||||
|
|
||||||
|
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<_>>();
|
||||||
|
|
||||||
|
let slots = free_slots_by_influence(&board);
|
||||||
|
|
||||||
|
let all_symbols = {
|
||||||
|
// We need this many from the bottom, and this many from the top.
|
||||||
|
let neg_count = slots.iter().filter(|(_, x)| *x <= 0.0).count();
|
||||||
|
let pos_count = slots.iter().filter(|(_, x)| *x > 0.0).count();
|
||||||
|
|
||||||
|
let mut a_iter = available_numbers
|
||||||
|
.iter()
|
||||||
|
.take(neg_count)
|
||||||
|
.chain(available_numbers.iter().rev().take(pos_count).rev());
|
||||||
|
|
||||||
|
let mut g = slots
|
||||||
|
// Group slots with equal weights
|
||||||
|
// and count the number of elements in each group
|
||||||
|
.iter()
|
||||||
|
.group_by(|x| x.1)
|
||||||
|
.into_iter()
|
||||||
|
.map(|(_, x)| x.count())
|
||||||
|
// Generate the digits we should try for each group of
|
||||||
|
// equal-weight slots
|
||||||
|
.map(|s| {
|
||||||
|
(0..s)
|
||||||
|
.map(|_| a_iter.next().unwrap().clone())
|
||||||
|
.permutations(s)
|
||||||
|
.unique()
|
||||||
|
.collect_vec()
|
||||||
|
})
|
||||||
|
// Now, covert this to an array of all cartesian products
|
||||||
|
// of this set of sets
|
||||||
|
.multi_cartesian_product()
|
||||||
|
.map(|x| x.iter().flatten().cloned().collect_vec())
|
||||||
|
.map(|v| slots.iter().zip(v).collect_vec())
|
||||||
|
.collect_vec();
|
||||||
|
|
||||||
|
// Sort these vectors so the order of values
|
||||||
|
// matches the order of empty slots
|
||||||
|
g.iter_mut()
|
||||||
|
.for_each(|v| v.sort_by(|(a, _), (b, _)| b.0.partial_cmp(&a.0).unwrap()));
|
||||||
|
g.into_iter()
|
||||||
|
.map(|v| v.into_iter().map(|(_, s)| s).collect_vec())
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut best_board = None;
|
||||||
|
let mut best_value = None;
|
||||||
|
for i in all_symbols {
|
||||||
|
let mut i_iter = i.iter();
|
||||||
|
let filled = Board::from_board(board.get_board().map(|x| match x {
|
||||||
|
None => i_iter.next().cloned(),
|
||||||
|
_ => x,
|
||||||
|
}));
|
||||||
|
|
||||||
|
let val = filled.evaluate();
|
||||||
|
|
||||||
|
if let Some(val) = val {
|
||||||
|
if let Some(best) = best_value {
|
||||||
|
if val > best {
|
||||||
|
best_value = Some(val);
|
||||||
|
best_board = Some(filled)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
best_value = Some(val);
|
||||||
|
best_board = Some(filled)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
best_board.unwrap()
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,137 +0,0 @@
|
||||||
use std::fmt::{Debug, Display};
|
|
||||||
|
|
||||||
use crate::board::TreeElement;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
||||||
pub enum TreeDir {
|
|
||||||
Right,
|
|
||||||
Left,
|
|
||||||
This,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Copy)]
|
|
||||||
pub struct TreeCoords {
|
|
||||||
len: usize,
|
|
||||||
coords: [TreeDir; 4],
|
|
||||||
inversion: [bool; 4],
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for TreeCoords {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
if self.is_inverted() {
|
|
||||||
write!(f, "-")?
|
|
||||||
} else {
|
|
||||||
write!(f, "+")?
|
|
||||||
}
|
|
||||||
|
|
||||||
for c in self.coords {
|
|
||||||
match c {
|
|
||||||
TreeDir::Left => write!(f, "L")?,
|
|
||||||
TreeDir::Right => write!(f, "R")?,
|
|
||||||
TreeDir::This => break,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Debug for TreeCoords {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
Display::fmt(self, f)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
impl TreeCoords {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self {
|
|
||||||
len: 0,
|
|
||||||
coords: [TreeDir::This; 4],
|
|
||||||
inversion: [false; 4],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn push(&mut self, dir: TreeDir, invert: bool) {
|
|
||||||
if self.len == 4 || dir == TreeDir::This {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
self.coords[self.len] = dir;
|
|
||||||
self.inversion[self.len] = invert;
|
|
||||||
self.len += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn pop(&mut self) -> Option<(TreeDir, bool)> {
|
|
||||||
if self.len == 0 {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
self.len -= 1;
|
|
||||||
let dir = self.coords[self.len];
|
|
||||||
let inv = self.inversion[self.len];
|
|
||||||
self.coords[self.len] = TreeDir::This;
|
|
||||||
self.inversion[self.len] = false;
|
|
||||||
Some((dir, inv))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_inverted(&self) -> bool {
|
|
||||||
if self.len == 0 {
|
|
||||||
false
|
|
||||||
} else {
|
|
||||||
self.inversion[self.len - 1]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_from<'a>(&self, mut tree: &'a TreeElement) -> Option<&'a TreeElement> {
|
|
||||||
for i in 0..self.len {
|
|
||||||
match &self.coords[i] {
|
|
||||||
TreeDir::Left => {
|
|
||||||
if let Some(t) = tree.left() {
|
|
||||||
tree = t
|
|
||||||
} else {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
TreeDir::Right => {
|
|
||||||
if let Some(t) = tree.right() {
|
|
||||||
tree = t
|
|
||||||
} else {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
TreeDir::This => return Some(tree),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Some(tree)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_from_mut<'a>(&self, mut tree: &'a mut TreeElement) -> Option<&'a mut TreeElement> {
|
|
||||||
for i in 0..self.len {
|
|
||||||
match &self.coords[i] {
|
|
||||||
TreeDir::Left => {
|
|
||||||
if let Some(t) = tree.left_mut() {
|
|
||||||
tree = t
|
|
||||||
} else {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
TreeDir::Right => {
|
|
||||||
if let Some(t) = tree.right_mut() {
|
|
||||||
tree = t
|
|
||||||
} else {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
TreeDir::This => return Some(tree),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Some(tree)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,4 +1,6 @@
|
||||||
use std::fmt::Display;
|
use anyhow::Result;
|
||||||
|
use itertools::Itertools;
|
||||||
|
use std::fmt::Write;
|
||||||
use termion::color::{self, Color};
|
use termion::color::{self, Color};
|
||||||
|
|
||||||
use super::{PlayerAction, TreeElement};
|
use super::{PlayerAction, TreeElement};
|
||||||
|
@ -47,7 +49,8 @@ enum Token {
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct Board {
|
pub struct Board {
|
||||||
board: [Option<(Symb, Player)>; 11],
|
board: [Option<Symb>; 11],
|
||||||
|
placed_by: [Option<Player>; 11],
|
||||||
|
|
||||||
/// Number of Nones in `board`
|
/// Number of Nones in `board`
|
||||||
free_spots: usize,
|
free_spots: usize,
|
||||||
|
@ -56,30 +59,14 @@ pub struct Board {
|
||||||
last_placed: Option<usize>,
|
last_placed: Option<usize>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Display for Board {
|
impl ToString for Board {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn to_string(&self) -> String {
|
||||||
// Print board
|
let mut s = String::new();
|
||||||
for (i, o) in self.board.iter().enumerate() {
|
s.extend(
|
||||||
match o {
|
self.board
|
||||||
Some((symb, player)) => write!(
|
.map(|x| x.map(|s| s.to_char().unwrap()).unwrap_or('_')),
|
||||||
f,
|
);
|
||||||
"{}{}{}",
|
s
|
||||||
// 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 {
|
|
||||||
color::Fg(&color::Red as &dyn Color)
|
|
||||||
} else {
|
|
||||||
color::Fg(player.color())
|
|
||||||
},
|
|
||||||
symb,
|
|
||||||
color::Fg(color::Reset)
|
|
||||||
)?,
|
|
||||||
|
|
||||||
None => write!(f, "_")?,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -89,34 +76,58 @@ impl Board {
|
||||||
Self {
|
Self {
|
||||||
free_spots: 11,
|
free_spots: 11,
|
||||||
board: Default::default(),
|
board: Default::default(),
|
||||||
|
placed_by: Default::default(),
|
||||||
last_placed: None,
|
last_placed: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn iter(&self) -> impl Iterator<Item = &Option<(Symb, Player)>> {
|
pub fn get_board(&self) -> &[Option<Symb>; 11] {
|
||||||
self.board.iter()
|
&self.board
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get(&self, idx: usize) -> Option<&Option<(Symb, Player)>> {
|
pub fn get_board_mut(&mut self) -> &mut [Option<Symb>; 11] {
|
||||||
self.board.get(idx)
|
&mut self.board
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_done(&self) -> bool {
|
pub fn is_done(&self) -> bool {
|
||||||
self.free_spots == 0
|
self.free_spots == 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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() {
|
||||||
|
match symb {
|
||||||
|
Some(symb) => write!(
|
||||||
|
s,
|
||||||
|
"{}{}{}",
|
||||||
|
// 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 {
|
||||||
|
color::Fg(&color::Red as &dyn Color)
|
||||||
|
} else {
|
||||||
|
match p {
|
||||||
|
Some(player) => color::Fg(player.color()),
|
||||||
|
None => color::Fg(&color::Reset as &dyn Color),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
symb,
|
||||||
|
color::Fg(color::Reset)
|
||||||
|
)?,
|
||||||
|
|
||||||
|
None => write!(s, "_")?,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(s)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn size(&self) -> usize {
|
pub fn size(&self) -> usize {
|
||||||
self.board.len()
|
self.board.len()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn contains(&self, s: Symb) -> bool {
|
pub fn contains(&self, s: Symb) -> bool {
|
||||||
for i in self.board.iter().flatten() {
|
self.board.iter().contains(&Some(s))
|
||||||
if i.0 == s {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Is the given action valid?
|
/// Is the given action valid?
|
||||||
|
@ -137,14 +148,14 @@ impl Board {
|
||||||
}
|
}
|
||||||
|
|
||||||
let r = &self.board[action.pos + 1];
|
let r = &self.board[action.pos + 1];
|
||||||
if r.is_some_and(|(s, _)| s.is_op() && !s.is_minus()) {
|
if r.is_some_and(|s| s.is_op() && !s.is_minus()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Symb::Zero => {
|
Symb::Zero => {
|
||||||
if action.pos != 0 {
|
if action.pos != 0 {
|
||||||
let l = &self.board[action.pos - 1].map(|x| x.0);
|
let l = &self.board[action.pos - 1];
|
||||||
if l == &Some(Symb::Div) {
|
if l == &Some(Symb::Div) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -156,8 +167,8 @@ impl Board {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
let l = &self.board[action.pos - 1].map(|x| x.0);
|
let l = &self.board[action.pos - 1];
|
||||||
let r = &self.board[action.pos + 1].map(|x| x.0);
|
let r = &self.board[action.pos + 1];
|
||||||
|
|
||||||
if action.symb == Symb::Div && r == &Some(Symb::Zero) {
|
if action.symb == Symb::Div && r == &Some(Symb::Zero) {
|
||||||
return false;
|
return false;
|
||||||
|
@ -184,7 +195,8 @@ impl Board {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.board[action.pos] = Some((action.symb, player));
|
self.board[action.pos] = Some(action.symb);
|
||||||
|
self.placed_by[action.pos] = Some(player);
|
||||||
self.free_spots -= 1;
|
self.free_spots -= 1;
|
||||||
self.last_placed = Some(action.pos);
|
self.last_placed = Some(action.pos);
|
||||||
true
|
true
|
||||||
|
@ -195,7 +207,7 @@ impl Board {
|
||||||
let mut is_neg = true; // if true, - is negative. if false, subtract.
|
let mut is_neg = true; // if true, - is negative. if false, subtract.
|
||||||
let mut current_num = String::new();
|
let mut current_num = String::new();
|
||||||
|
|
||||||
for s in self.board.iter().map(|x| x.map(|(s, _)| s)) {
|
for s in self.board.iter() {
|
||||||
match s {
|
match s {
|
||||||
Some(Symb::Div) => {
|
Some(Symb::Div) => {
|
||||||
tokens.push(Token::Value(current_num.clone()));
|
tokens.push(Token::Value(current_num.clone()));
|
||||||
|
@ -316,8 +328,18 @@ impl Board {
|
||||||
self.to_tree().evaluate()
|
self.to_tree().evaluate()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Hacky method to parse a board from a string
|
pub fn from_board(board: [Option<Symb>; 11]) -> Self {
|
||||||
pub fn from_string(s: &str, current_player: Player) -> Option<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 {
|
if s.len() != 11 {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
@ -328,7 +350,7 @@ impl Board {
|
||||||
if c == '_' {
|
if c == '_' {
|
||||||
Some(None)
|
Some(None)
|
||||||
} else {
|
} else {
|
||||||
Symb::from_char(c).map(|s| Some((s, current_player)))
|
Symb::from_char(c).map(|s| Some(s))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
@ -348,6 +370,7 @@ impl Board {
|
||||||
|
|
||||||
Some(Self {
|
Some(Self {
|
||||||
board,
|
board,
|
||||||
|
placed_by: Default::default(),
|
||||||
free_spots,
|
free_spots,
|
||||||
last_placed: None,
|
last_placed: None,
|
||||||
})
|
})
|
||||||
|
|
Loading…
Reference in New Issue