add Move::lazy_children

This commit is contained in:
Simon Gardling 2025-02-18 13:41:45 -05:00
parent 7ec23f1bab
commit ad28713775
Signed by: titaniumtown
GPG Key ID: 9AB28AC10ECE533D

View File

@ -29,6 +29,8 @@ struct Move {
value: i64, value: i64,
color: Piece, color: Piece,
lazy_children: bool,
} }
impl Move { impl Move {
@ -82,25 +84,39 @@ impl FutureMoves {
} }
} }
const LAZY_EXPIRE: usize = 6;
/// Generate children for all children of `nodes` /// Generate children for all children of `nodes`
fn extend_layers(&mut self) { fn extend_layers(&mut self) {
let mut next_nodes: Vec<usize> = (0..self.arena.len()) let mut next_nodes: Vec<usize> = (0..self.arena.len())
.filter(|&idx| self.arena[idx].children.is_empty()) // we want to select all nodes that don't have children, or are lazy (need to maybe be regenerated)
.filter(|&idx| self.arena[idx].children.is_empty() || self.arena[idx].lazy_children)
.filter(|&idx| self.is_connected_to_root(idx)) // put here so this will not extend needlessly before prunes .filter(|&idx| self.is_connected_to_root(idx)) // put here so this will not extend needlessly before prunes
.collect(); .collect();
for _ in self.current_depth..=self.max_depth { for i in self.current_depth..=self.max_depth {
// TODO! use `i` in order to prune along-the-way
// i.e, every 4 moves of color `self.agent_color`, do a pruning step,
// only keeping the top [`Move`]
next_nodes = next_nodes next_nodes = next_nodes
.into_iter() .into_iter()
.progress_with_style( .progress_with_style(
ProgressStyle::with_template("Generating children: ({pos}/{len}) {per_sec}") ProgressStyle::with_template(&format!(
.unwrap(), "Generating children (depth: {}/{}): ({{pos}}/{{len}}) {{per_sec}}",
i, self.max_depth
))
.unwrap(),
) )
.flat_map(|node_idx| self.generate_children(node_idx)) .flat_map(|node_idx| {
self.generate_children(
node_idx,
if self.arena[node_idx].lazy_children
&& self.depth_of(node_idx) - 1 < Self::LAZY_EXPIRE
{
false
} else {
// this is a non-lazy_children child, should it be lazy?
i > Self::LAZY_EXPIRE
},
)
})
.flatten() .flatten()
.collect(); .collect();
} }
@ -124,17 +140,23 @@ impl FutureMoves {
} }
/// Creates children for a parent (`parent`), returns an iterator it's children's indexes /// Creates children for a parent (`parent`), returns an iterator it's children's indexes
fn generate_children(&mut self, parent_idx: usize) -> Option<impl Iterator<Item = usize>> { fn generate_children(
&mut self,
parent_idx: usize,
lazy_children: bool,
) -> Option<impl Iterator<Item = usize>> {
// early-exit if a winner for the parent already exists // early-exit if a winner for the parent already exists
if self.arena[parent_idx].winner != Winner::None { if self.arena[parent_idx].winner != Winner::None {
return None; return None;
} }
let new_color = !self.arena[parent_idx].color;
let parent_lazy = self.arena[parent_idx].lazy_children;
let mut new: Vec<Move> = let mut new: Vec<Move> =
// use [`Board::all_positions`] here instead of [`Board::possible_moves`] // use [`Board::all_positions`] here instead of [`Board::possible_moves`]
// because we use [`Board::what_if`] later and we want to reduce calls to [`Board::propegate_from_dry`] // because we use [`Board::what_if`] later and we want to reduce calls to [`Board::propegate_from_dry`]
Board::all_positions() Board::all_positions()
.flat_map(|(i, j)| self.arena[parent_idx].board.what_if(i, j, !self.arena[parent_idx].color).map(|x| (i, j, x))) .flat_map(|(i, j)| self.arena[parent_idx].board.what_if(i, j, new_color).map(|x| (i, j, x)))
.map(|(i, j, new_board)| Move { .map(|(i, j, new_board)| Move {
i, i,
j, j,
@ -143,22 +165,37 @@ impl FutureMoves {
parent: Some(parent_idx), parent: Some(parent_idx),
children: Vec::new(), children: Vec::new(),
value: 0, value: 0,
color: !self.arena[parent_idx].color color: new_color,
lazy_children,
}).collect(); }).collect();
// negative, because we want the max value to be at the first index
new.sort_by_key(|x| -x.compute_self_value(self.agent_color));
// keep the TOP_K children `self.agent_color`-color moves // keep the TOP_K children `self.agent_color`-color moves
const TOP_K_CHILDREN: usize = 1; const TOP_K_CHILDREN: usize = 1;
// we want to keep only the best move of the agent // we want to keep only the best move of the agent
if !self.arena[parent_idx].color == self.agent_color && new.len() > TOP_K_CHILDREN { if lazy_children && new_color == self.agent_color && new.len() > TOP_K_CHILDREN {
// TODO! Move this to `extend_layers` so we can prune based on recursive [`Move`] value // TODO! Move this to `extend_layers` so we can prune based on recursive [`Move`] value
// negative, because we want the max value to be at the first index
new.sort_by_key(|x| -x.compute_self_value(self.agent_color));
new.drain(TOP_K_CHILDREN..); new.drain(TOP_K_CHILDREN..);
} }
let start_idx = self.arena.len(); let start_idx = self.arena.len();
self.arena.extend(new); if parent_lazy && !lazy_children {
// this move's children are being regenerated after lazy child expiration, don't append first node
if new.len() > 1 {
self.arena.extend(new.drain(1..));
} else {
// nothing will be appended
// even though it was sorted the first time around
// there's still no more than one element (which is already in the arena)
return None;
}
} else {
self.arena.extend(new);
}
let new_indices = start_idx..self.arena.len(); let new_indices = start_idx..self.arena.len();
self.arena[parent_idx].children.extend(new_indices.clone()); self.arena[parent_idx].children.extend(new_indices.clone());
@ -250,6 +287,7 @@ impl FutureMoves {
children: Vec::new(), children: Vec::new(),
value: 0, value: 0,
color: !self.agent_color, color: !self.agent_color,
lazy_children: false,
}); });
self.update_root_idx(0); self.update_root_idx(0);
} }
@ -348,7 +386,7 @@ pub struct ComplexAgent {
impl ComplexAgent { impl ComplexAgent {
pub const fn new(color: Piece) -> Self { pub const fn new(color: Piece) -> Self {
const MAX_DEPTH: usize = 15; const MAX_DEPTH: usize = 10;
Self { Self {
color, color,
future_moves: FutureMoves::new(color, MAX_DEPTH), future_moves: FutureMoves::new(color, MAX_DEPTH),