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.
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<RawNodePtr>,
}
#[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<u32, Option<&'a Node>> {
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());

View File

@ -94,10 +94,10 @@ impl Node {
pub fn expand<'a, E>(
&'a self,
arena: &'a Arena,
evaluate: E,
mut evaluate: E,
) -> impl Iterator<Item = &'a Node> + '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);

View File

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

View File

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

View File

@ -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
}

View File

@ -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;
}
}