From 7bb551b362f7a268934bae12322b2617cdc587e9 Mon Sep 17 00:00:00 2001 From: tali Date: Sun, 16 Apr 2023 03:00:40 -0400 Subject: [PATCH] iteration counter is stored in the Bot, and incr for each evaluation --- fish/src/bot.rs | 69 +++++++++++++++++++++--------- fish/src/bot/node.rs | 4 +- tidepool/example-configs/100L.toml | 3 -- tidepool/example-configs/18L.toml | 2 +- tidepool/src/config.rs | 6 +-- tidepool/src/main.rs | 20 ++++----- 6 files changed, 64 insertions(+), 40 deletions(-) diff --git a/fish/src/bot.rs b/fish/src/bot.rs index 49ccf56..1a44a48 100644 --- a/fish/src/bot.rs +++ b/fish/src/bot.rs @@ -16,6 +16,7 @@ pub(crate) use bumpalo::Bump as Arena; /// Encompasses an instance of the algorithm. pub struct Bot { + iters: u32, evaluator: Evaluator, algorithm: SegmentedAStar, // 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 algorithm = SegmentedAStar::new(root); Self { + iters: 0, evaluator, algorithm, arena, } } - /// Perform a single "iteration" of work, which may end up improving the suggestion. - /// What defines an iteration is vague, but similar versions of the engine should be - /// deterministic, such that performing the same number of iterations gives the same - /// resulting suggestion. - pub fn think(&mut self) { - self.algorithm.step(&self.arena, &self.evaluator); + /// Runs the bot for up to `gas` more iterations. An "iteration" is a unit of work + /// that is intentionally kept vague, but should be proportional to the amount CPU + /// time. Iterations are deterministic, so similar versions of the engine will produce + /// the same suggestions if run the for the same number of iterations. + pub fn think_for(&mut self, gas: u32) { + // 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 @@ -130,9 +149,6 @@ struct SegmentedAStar { best: Option, } -#[derive(Debug)] -struct ShouldSelect; - impl SegmentedAStar { fn new(root: &Node) -> Self { let mut open = Vec::with_capacity(root.queue().len()); @@ -149,22 +165,26 @@ impl SegmentedAStar { 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) { - Ok(_) => {} - Err(ShouldSelect) => self.select(), + Ok(work) => *iters += work, + 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> { 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() }; if cand.is_terminal() { - self.depth = self.open.len(); // makes expand() fail immediately - self.backup(cand); - return Err(ShouldSelect); + return Err(Some(cand)); } self.depth += 1; @@ -172,17 +192,24 @@ impl SegmentedAStar { 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()); } - 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)) { tracing::debug!( - "{} suggestion: {cand:?}", + "{} suggestion @ {iters}: {cand:?}", self.best.map_or("1st", |_| "new") ); self.best = Some(cand.into()); diff --git a/fish/src/bot/node.rs b/fish/src/bot/node.rs index 0e927e1..8cfbb16 100644 --- a/fish/src/bot/node.rs +++ b/fish/src/bot/node.rs @@ -94,10 +94,10 @@ impl Node { pub fn expand<'a, E>( &'a self, arena: &'a Arena, - evaluate: E, + mut evaluate: E, ) -> impl Iterator + 'a where - E: Fn(&Mat, Queue<'_>) -> i32 + 'a, + E: FnMut(&Mat, Queue<'_>) -> i32 + 'a, { let placements = self.queue().reachable().flat_map(|ty| { let locs = find_locations(self.matrix(), ty); diff --git a/tidepool/example-configs/100L.toml b/tidepool/example-configs/100L.toml index 2431ec4..b0be272 100644 --- a/tidepool/example-configs/100L.toml +++ b/tidepool/example-configs/100L.toml @@ -1,6 +1,3 @@ [game] goal = 100 rules = "jstris" - -[bot] -iters = 9000 diff --git a/tidepool/example-configs/18L.toml b/tidepool/example-configs/18L.toml index 732d4af..73eb68c 100644 --- a/tidepool/example-configs/18L.toml +++ b/tidepool/example-configs/18L.toml @@ -1,2 +1,2 @@ game.goal = 18 -bot.iters = 5000 +bot.iters = 500_000 diff --git a/tidepool/src/config.rs b/tidepool/src/config.rs index 0e191fd..5af58d0 100644 --- a/tidepool/src/config.rs +++ b/tidepool/src/config.rs @@ -43,14 +43,14 @@ pub struct BotConfig { // TODO: algorithm // TODO: capabililties #[serde(default = "defaults::iters")] - pub iters: usize, + pub iters: u32, #[serde(default = "defaults::weights", deserialize_with = "de::weights")] pub weights: Weights, } impl BotConfig { pub const DEFAULT: Self = Self { - iters: 10_000, + iters: 100_000, weights: Weights::DEFAULT, }; } @@ -80,7 +80,7 @@ mod defaults { GameRulesConfig::JSTRIS } - pub const fn iters() -> usize { + pub const fn iters() -> u32 { BotConfig::DEFAULT.iters } diff --git a/tidepool/src/main.rs b/tidepool/src/main.rs index 379469c..fbc3c5d 100644 --- a/tidepool/src/main.rs +++ b/tidepool/src/main.rs @@ -275,6 +275,7 @@ fn print_single_progress( if cleared == goal { writeln!(writer) } else { + write!(writer, " ")?; writer.flush() } } @@ -313,6 +314,7 @@ fn print_multi_progress( )?; } + write!(writer, " ")?; writer.flush() } @@ -360,17 +362,15 @@ fn run_simulation( mino::Queue::new(sim.queue().hold(), sim.queue().next()), ); - for i in 0..config.bot.iters { - bot.think(); + while bot.iterations() < config.bot.iters { + 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) { - break; - } - if tx.send(Msg::Heartbeat(id)).is_err() { - break; - } + if exit_early.load(atomic::Ordering::Relaxed) { + break; + } + if tx.send(Msg::Heartbeat(id)).is_err() { + break; } }