iteration counter is stored in the Bot, and incr for each evaluation
This commit is contained in:
parent
27e5394a8c
commit
7bb551b362
|
@ -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());
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -1,6 +1,3 @@
|
|||
[game]
|
||||
goal = 100
|
||||
rules = "jstris"
|
||||
|
||||
[bot]
|
||||
iters = 9000
|
||||
|
|
|
@ -1,2 +1,2 @@
|
|||
game.goal = 18
|
||||
bot.iters = 5000
|
||||
bot.iters = 500_000
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue