iteration counter is stored in the Bot, and incr for each evaluation

This commit is contained in:
tali 2023-04-16 03:00:40 -04:00
parent 27e5394a8c
commit 7bb551b362
6 changed files with 64 additions and 40 deletions

View File

@ -16,6 +16,7 @@ pub(crate) use bumpalo::Bump as Arena;
/// Encompasses an instance of the algorithm. /// Encompasses an instance of the algorithm.
pub struct Bot { pub struct Bot {
iters: u32,
evaluator: Evaluator, evaluator: Evaluator,
algorithm: SegmentedAStar, algorithm: SegmentedAStar,
// IMPORTANT: `arena` must occur after `algorithm` so that it is dropped last. // IMPORTANT: `arena` must occur after `algorithm` so that it is dropped last.
@ -31,18 +32,36 @@ impl Bot {
let evaluator = Evaluator::new(weights, root); let evaluator = Evaluator::new(weights, root);
let algorithm = SegmentedAStar::new(root); let algorithm = SegmentedAStar::new(root);
Self { Self {
iters: 0,
evaluator, evaluator,
algorithm, algorithm,
arena, arena,
} }
} }
/// Perform a single "iteration" of work, which may end up improving the suggestion. /// Runs the bot for up to `gas` more iterations. An "iteration" is a unit of work
/// What defines an iteration is vague, but similar versions of the engine should be /// that is intentionally kept vague, but should be proportional to the amount CPU
/// deterministic, such that performing the same number of iterations gives the same /// time. Iterations are deterministic, so similar versions of the engine will produce
/// resulting suggestion. /// the same suggestions if run the for the same number of iterations.
pub fn think(&mut self) { pub fn think_for(&mut self, gas: u32) {
self.algorithm.step(&self.arena, &self.evaluator); // NOTICE: The actual number of iterations may slightly exceed the provided gas due to
// how the bot is currently structured. This shouldn't have a substantial impact over
// the long run since the overshoot will be very small in terms of CPU
// time.
//
// Runs will be deterministic as long as two runs end on the same *target*
// iterations on the last call to `think_for`, e.g. "bot.think_for(5000)" is the
// same as "bot.think_for(2500); bot.think_for(5000 - bot.iterations());"
let max_iters = self.iters + gas;
while self.iters < max_iters {
self.algorithm
.step(&self.arena, &self.evaluator, &mut self.iters);
}
}
/// Returns the number of iterations done so far.
pub fn iterations(&self) -> u32 {
self.iters
} }
/// Return the current best suggested placement. Returns `None` under two possible /// Return the current best suggested placement. Returns `None` under two possible
@ -130,9 +149,6 @@ struct SegmentedAStar {
best: Option<RawNodePtr>, best: Option<RawNodePtr>,
} }
#[derive(Debug)]
struct ShouldSelect;
impl SegmentedAStar { impl SegmentedAStar {
fn new(root: &Node) -> Self { fn new(root: &Node) -> Self {
let mut open = Vec::with_capacity(root.queue().len()); let mut open = Vec::with_capacity(root.queue().len());
@ -149,22 +165,26 @@ impl SegmentedAStar {
self.best.map(|node| unsafe { node.as_node() }) self.best.map(|node| unsafe { node.as_node() })
} }
fn step(&mut self, arena: &Arena, eval: &Evaluator) { fn step(&mut self, arena: &Arena, eval: &Evaluator, iters: &mut u32) {
*iters += 1;
match self.expand(arena, eval) { match self.expand(arena, eval) {
Ok(_) => {} Ok(work) => *iters += work,
Err(ShouldSelect) => self.select(), Err(maybe_cand) => {
if let Some(cand) = maybe_cand {
self.backup(cand, *iters);
}
self.select();
}
} }
} }
fn expand<'a>(&mut self, arena: &'a Arena, eval: &Evaluator) -> Result<&'a Node, ShouldSelect> { fn expand<'a>(&mut self, arena: &'a Arena, eval: &Evaluator) -> Result<u32, Option<&'a Node>> {
let open_set = self.open.get_mut(self.depth); let open_set = self.open.get_mut(self.depth);
let cand = open_set.map_or(None, |set| set.pop()).ok_or(ShouldSelect)?; let cand = open_set.map_or(None, |set| set.pop()).ok_or(None)?;
let cand = unsafe { cand.0.as_node() }; let cand = unsafe { cand.0.as_node() };
if cand.is_terminal() { if cand.is_terminal() {
self.depth = self.open.len(); // makes expand() fail immediately return Err(Some(cand));
self.backup(cand);
return Err(ShouldSelect);
} }
self.depth += 1; self.depth += 1;
@ -172,17 +192,24 @@ impl SegmentedAStar {
self.open.resize_with(self.depth + 1, BinaryHeap::new); self.open.resize_with(self.depth + 1, BinaryHeap::new);
} }
for suc in cand.expand(arena, |m, q| eval.evaluate(m, q)) { let mut work = 0;
let evaluate = |mat: &Mat, queue: Queue<'_>| {
// each evaluated board state = +1 unit work
work += 1;
eval.evaluate(mat, queue)
};
for suc in cand.expand(arena, evaluate) {
self.open[self.depth].push(suc.into()); self.open[self.depth].push(suc.into());
} }
Ok(cand) Ok(work)
} }
fn backup(&mut self, cand: &Node) { fn backup(&mut self, cand: &Node, iters: u32) {
if self.best().map_or(true, |best| cand.is_better(best)) { if self.best().map_or(true, |best| cand.is_better(best)) {
tracing::debug!( tracing::debug!(
"{} suggestion: {cand:?}", "{} suggestion @ {iters}: {cand:?}",
self.best.map_or("1st", |_| "new") self.best.map_or("1st", |_| "new")
); );
self.best = Some(cand.into()); self.best = Some(cand.into());

View File

@ -94,10 +94,10 @@ impl Node {
pub fn expand<'a, E>( pub fn expand<'a, E>(
&'a self, &'a self,
arena: &'a Arena, arena: &'a Arena,
evaluate: E, mut evaluate: E,
) -> impl Iterator<Item = &'a Node> + 'a ) -> impl Iterator<Item = &'a Node> + 'a
where where
E: Fn(&Mat, Queue<'_>) -> i32 + 'a, E: FnMut(&Mat, Queue<'_>) -> i32 + 'a,
{ {
let placements = self.queue().reachable().flat_map(|ty| { let placements = self.queue().reachable().flat_map(|ty| {
let locs = find_locations(self.matrix(), ty); let locs = find_locations(self.matrix(), ty);

View File

@ -1,6 +1,3 @@
[game] [game]
goal = 100 goal = 100
rules = "jstris" rules = "jstris"
[bot]
iters = 9000

View File

@ -1,2 +1,2 @@
game.goal = 18 game.goal = 18
bot.iters = 5000 bot.iters = 500_000

View File

@ -43,14 +43,14 @@ pub struct BotConfig {
// TODO: algorithm // TODO: algorithm
// TODO: capabililties // TODO: capabililties
#[serde(default = "defaults::iters")] #[serde(default = "defaults::iters")]
pub iters: usize, pub iters: u32,
#[serde(default = "defaults::weights", deserialize_with = "de::weights")] #[serde(default = "defaults::weights", deserialize_with = "de::weights")]
pub weights: Weights, pub weights: Weights,
} }
impl BotConfig { impl BotConfig {
pub const DEFAULT: Self = Self { pub const DEFAULT: Self = Self {
iters: 10_000, iters: 100_000,
weights: Weights::DEFAULT, weights: Weights::DEFAULT,
}; };
} }
@ -80,7 +80,7 @@ mod defaults {
GameRulesConfig::JSTRIS GameRulesConfig::JSTRIS
} }
pub const fn iters() -> usize { pub const fn iters() -> u32 {
BotConfig::DEFAULT.iters BotConfig::DEFAULT.iters
} }

View File

@ -275,6 +275,7 @@ fn print_single_progress(
if cleared == goal { if cleared == goal {
writeln!(writer) writeln!(writer)
} else { } else {
write!(writer, " ")?;
writer.flush() writer.flush()
} }
} }
@ -313,6 +314,7 @@ fn print_multi_progress(
)?; )?;
} }
write!(writer, " ")?;
writer.flush() writer.flush()
} }
@ -360,11 +362,10 @@ fn run_simulation(
mino::Queue::new(sim.queue().hold(), sim.queue().next()), mino::Queue::new(sim.queue().hold(), sim.queue().next()),
); );
for i in 0..config.bot.iters { while bot.iterations() < config.bot.iters {
bot.think(); let gas = std::cmp::min(50_000, config.bot.iters - bot.iterations());
bot.think_for(gas);
// periodically check back with the main thread to see if we should exit
if i % 4096 == 4095 {
if exit_early.load(atomic::Ordering::Relaxed) { if exit_early.load(atomic::Ordering::Relaxed) {
break; break;
} }
@ -372,7 +373,6 @@ fn run_simulation(
break; break;
} }
} }
}
let Some(placement) = bot.suggest() else { let Some(placement) = bot.suggest() else {
tracing::warn!("gave up :( pieces={}, id={id}", sim.pieces()); tracing::warn!("gave up :( pieces={}, id={id}", sim.pieces());