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.
|
/// 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());
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -1,6 +1,3 @@
|
||||||
[game]
|
[game]
|
||||||
goal = 100
|
goal = 100
|
||||||
rules = "jstris"
|
rules = "jstris"
|
||||||
|
|
||||||
[bot]
|
|
||||||
iters = 9000
|
|
||||||
|
|
|
@ -1,2 +1,2 @@
|
||||||
game.goal = 18
|
game.goal = 18
|
||||||
bot.iters = 5000
|
bot.iters = 500_000
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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,17 +362,15 @@ 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 exit_early.load(atomic::Ordering::Relaxed) {
|
||||||
if i % 4096 == 4095 {
|
break;
|
||||||
if exit_early.load(atomic::Ordering::Relaxed) {
|
}
|
||||||
break;
|
if tx.send(Msg::Heartbeat(id)).is_err() {
|
||||||
}
|
break;
|
||||||
if tx.send(Msg::Heartbeat(id)).is_err() {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue