Reworked minimax agent
This commit is contained in:
@@ -1,96 +1,129 @@
|
||||
use super::{TreeCoords, TreeDir};
|
||||
use crate::{board::TreeElement, util::Symb};
|
||||
use itertools::Itertools;
|
||||
use std::num::NonZeroU8;
|
||||
|
||||
/// Find the coordinates of all partials in the given tree
|
||||
pub fn find_partials(tree: &TreeElement) -> Vec<TreeCoords> {
|
||||
let mut partials = Vec::new();
|
||||
let mut current_coords = TreeCoords::new();
|
||||
use crate::{board::Board, util::Symb};
|
||||
|
||||
loop {
|
||||
let t = current_coords.get_from(tree).unwrap();
|
||||
match t {
|
||||
TreeElement::Number(_) | TreeElement::Partial(_) => {
|
||||
if let TreeElement::Partial(_) = t {
|
||||
partials.push(current_coords);
|
||||
}
|
||||
/// Returns an iterator of (sort, coords, char_idx, f32) for each empty slot in the listed partials.
|
||||
/// - sort is the index of this slot.
|
||||
/// - 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)> {
|
||||
// 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 {
|
||||
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())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let base = filled.evaluate().unwrap();
|
||||
|
||||
/// Fill empty slots in the given partials, in order.
|
||||
/// Will panic if we run out of numbers to fill with.
|
||||
///
|
||||
/// Returns a new tree with filled partials.
|
||||
pub fn fill_partials<'a>(
|
||||
tree: &'a TreeElement,
|
||||
partials: impl Iterator<Item = &'a TreeCoords>,
|
||||
mut numbers: impl Iterator<Item = &'a Symb>,
|
||||
) -> TreeElement {
|
||||
let mut tmp_tree = tree.clone();
|
||||
for p in partials {
|
||||
let x = p.get_from_mut(&mut tmp_tree).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, f32)> = board
|
||||
.get_board()
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(i, s)| if s.is_some() { None } else { Some(i) })
|
||||
.map(|i| {
|
||||
let mut new_tree = filled.clone();
|
||||
new_tree.get_board_mut()[i] = Some(Symb::from_char('6').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!("{}", 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!(),
|
||||
// This shouldn't ever be None
|
||||
(i, new_tree.evaluate().unwrap() - base)
|
||||
})
|
||||
.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()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user