WIP: modified A* with bumpalo managed nodes
This commit is contained in:
parent
d52e4da215
commit
15d69fe128
|
@ -26,6 +26,12 @@ version = "1.3.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bumpalo"
|
||||||
|
version = "3.12.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0d261e256854913907f67ed06efbc3338dfe6179796deefc1ff763fc1aee5535"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cc"
|
name = "cc"
|
||||||
version = "1.0.78"
|
version = "1.0.78"
|
||||||
|
@ -102,6 +108,7 @@ version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ahash",
|
"ahash",
|
||||||
"anyhow",
|
"anyhow",
|
||||||
|
"bumpalo",
|
||||||
"clap",
|
"clap",
|
||||||
"hashbrown",
|
"hashbrown",
|
||||||
"mino",
|
"mino",
|
||||||
|
|
|
@ -20,6 +20,7 @@ mino = { path = "../mino" }
|
||||||
|
|
||||||
ahash = "0.8"
|
ahash = "0.8"
|
||||||
anyhow = { version = "1.0", optional = true }
|
anyhow = { version = "1.0", optional = true }
|
||||||
|
bumpalo = "3.12"
|
||||||
clap = { version = "4.0", features = ["derive"], optional = true }
|
clap = { version = "4.0", features = ["derive"], optional = true }
|
||||||
hashbrown = "0.13"
|
hashbrown = "0.13"
|
||||||
serde = { version = "1.0", features = ["derive"], optional = true }
|
serde = { version = "1.0", features = ["derive"], optional = true }
|
||||||
|
|
301
fish/src/ai.rs
301
fish/src/ai.rs
|
@ -1,117 +1,250 @@
|
||||||
//! AI engine.
|
//! AI engine.
|
||||||
|
|
||||||
use crate::{eval, find};
|
use core::cell::Cell;
|
||||||
use alloc::vec::Vec;
|
use core::ops::Deref;
|
||||||
use core::{future::Future, pin::Pin, task::Poll};
|
use core::pin::Pin;
|
||||||
|
|
||||||
|
use alloc::boxed::Box;
|
||||||
use mino::srs::{Piece, PieceType};
|
use mino::srs::{Piece, PieceType};
|
||||||
use mino::{Mat, MatBuf};
|
use mino::{Mat, MatBuf};
|
||||||
|
|
||||||
#[derive(Debug)]
|
use alloc::vec::Vec;
|
||||||
#[non_exhaustive]
|
use bumpalo::Bump;
|
||||||
|
|
||||||
|
use crate::eval::evaluate;
|
||||||
|
use crate::find::cap::All;
|
||||||
|
use crate::find::{FindLocations, FindLocationsBuffers};
|
||||||
|
|
||||||
|
use self::search::ModifiedAStar;
|
||||||
|
|
||||||
|
mod search;
|
||||||
|
|
||||||
pub struct Ai {
|
pub struct Ai {
|
||||||
root_matrix: MatBuf,
|
search: search::ModifiedAStar<Graph>,
|
||||||
root_previews: Vec<PieceType>,
|
best: Option<Node>,
|
||||||
path: Vec<Piece>,
|
_arena: Pin<Box<Bump>>,
|
||||||
cur_mat: MatBuf,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn evaluate(mat: &Mat, depth: usize) -> i32 {
|
pub struct Exhausted;
|
||||||
let w_height = 5;
|
|
||||||
let w_ideps = 10;
|
|
||||||
let w_mdse = 10;
|
|
||||||
let w_pc = 10;
|
|
||||||
|
|
||||||
let mut rating = 0;
|
|
||||||
rating += eval::max_height(mat) * w_height;
|
|
||||||
rating += eval::i_deps(mat) * w_ideps;
|
|
||||||
rating += eval::mystery_mdse(mat) * w_mdse;
|
|
||||||
rating += (depth as i32) * w_pc;
|
|
||||||
rating
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Ai {
|
impl Ai {
|
||||||
// TODO: personality config
|
pub fn new(init_mat: &Mat, init_previews: &[PieceType], init_hold: Option<PieceType>) -> Self {
|
||||||
pub fn new() -> Self {
|
let arena = Box::pin(Bump::new());
|
||||||
|
let init_queue = Queue::alloc(&*arena, init_previews, init_hold);
|
||||||
|
let graph = Graph::new(&*arena, init_mat, init_queue);
|
||||||
|
let search = ModifiedAStar::new(graph);
|
||||||
Self {
|
Self {
|
||||||
root_matrix: MatBuf::new(),
|
best: None,
|
||||||
root_previews: Vec::with_capacity(8),
|
search,
|
||||||
path: Vec::with_capacity(8),
|
_arena: arena,
|
||||||
cur_mat: MatBuf::new(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn start(
|
pub fn think(&mut self) -> Result<(), Exhausted> {
|
||||||
&mut self,
|
let node = self.search.step().ok_or(Exhausted)?;
|
||||||
init_mat: &Mat,
|
if self.best.map_or(true, |best| node > best) {
|
||||||
init_previews: &[PieceType],
|
tracing::debug!("new best: {node:?} ({})", node.rating);
|
||||||
init_hold: Option<PieceType>,
|
self.best = Some(node);
|
||||||
) {
|
}
|
||||||
// init root node
|
Ok(())
|
||||||
self.root_matrix.copy_from(init_mat);
|
|
||||||
self.root_previews.clear();
|
|
||||||
self.root_previews.extend_from_slice(init_previews);
|
|
||||||
self.root_previews.extend(init_hold); // TODO: actual hold logic
|
|
||||||
|
|
||||||
// init search state
|
|
||||||
self.path.clear();
|
|
||||||
self.cur_mat.copy_from(&self.root_matrix);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn think_1_cycle(&mut self) -> bool {
|
pub fn suggestion(&self) -> impl Iterator<Item = Piece> + '_ {
|
||||||
let depth = self.path.len();
|
self.best.iter().flat_map(|n| n.trace())
|
||||||
if depth >= self.root_previews.len() {
|
}
|
||||||
return true;
|
}
|
||||||
|
|
||||||
|
struct Graph {
|
||||||
|
arena: *const Bump,
|
||||||
|
root: Node,
|
||||||
|
find_buf: Option<FindLocationsBuffers>,
|
||||||
|
children_buf: Vec<Node>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Graph {
|
||||||
|
fn new(arena: *const Bump, root_mat: &Mat, root_queue: Queue) -> Self {
|
||||||
|
let root = Node::new_root(arena, root_mat, root_queue);
|
||||||
|
Self {
|
||||||
|
arena,
|
||||||
|
root,
|
||||||
|
find_buf: None,
|
||||||
|
children_buf: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl search::Graph for Graph {
|
||||||
|
type Node = Node;
|
||||||
|
|
||||||
|
fn root(&mut self) -> Self::Node {
|
||||||
|
self.root.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn expand(&mut self, node: Self::Node) -> &[Self::Node] {
|
||||||
|
self.children_buf.clear();
|
||||||
|
|
||||||
|
for ty in node.queue.current() {
|
||||||
|
let find_buf = self.find_buf.take().unwrap_or_default();
|
||||||
|
let mut locs = FindLocations::with_buffers(&node.mat, ty, All, find_buf);
|
||||||
|
for loc in &mut locs {
|
||||||
|
let piece = Piece { ty, loc };
|
||||||
|
let node = node.succ(self.arena, piece);
|
||||||
|
self.children_buf.push(node);
|
||||||
|
}
|
||||||
|
self.find_buf = Some(locs.into_buffers());
|
||||||
}
|
}
|
||||||
|
|
||||||
let ty = self.root_previews[depth];
|
tracing::trace!("expanded to create {} children", self.children_buf.len());
|
||||||
let locs = find::find_locations(&self.cur_mat, ty, find::cap::All);
|
&self.children_buf
|
||||||
let pcs = locs.map(|loc| Piece { ty, loc });
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let mut rate_mat: MatBuf = MatBuf::new();
|
#[derive(Copy, Clone, Debug)]
|
||||||
let mut best_mat: MatBuf = MatBuf::new();
|
#[repr(transparent)]
|
||||||
let mut best: Option<(i32, Piece)> = None;
|
struct Node(*const NodeData);
|
||||||
|
|
||||||
for pc in pcs {
|
struct NodeData {
|
||||||
rate_mat.copy_from(&self.cur_mat);
|
mat: MatBuf,
|
||||||
pc.cells().fill(&mut rate_mat);
|
queue: Queue,
|
||||||
rate_mat.clear_lines();
|
pcnt: usize,
|
||||||
let rating = evaluate(&rate_mat, depth);
|
rating: i32,
|
||||||
|
back_edge: Cell<Option<Edge>>,
|
||||||
|
}
|
||||||
|
|
||||||
let best_rating = best.clone().map_or(i32::MAX, |(r, _)| r);
|
#[derive(Copy, Clone)]
|
||||||
if rating < best_rating {
|
struct Edge {
|
||||||
best = Some((rating, pc));
|
piece: Piece,
|
||||||
best_mat.copy_from(&rate_mat);
|
pred: Node,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl search::Node for Node {
|
||||||
|
fn is_terminal(&self) -> bool {
|
||||||
|
self.queue.is_empty()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Node {
|
||||||
|
fn new_root(arena: *const Bump, mat: &Mat, queue: Queue) -> Self {
|
||||||
|
Self::new(arena, mat, queue, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn succ(self, arena: *const Bump, piece: Piece) -> Self {
|
||||||
|
let mut mat: MatBuf = MatBuf::new();
|
||||||
|
mat.copy_from(&self.mat);
|
||||||
|
piece.cells().fill(&mut mat);
|
||||||
|
mat.clear_lines();
|
||||||
|
let queue = self.queue.succ(piece.ty);
|
||||||
|
let pcnt = self.pcnt + 1;
|
||||||
|
let succ = Self::new(arena, &mat, queue, pcnt);
|
||||||
|
succ.back_edge.set(Some(Edge {
|
||||||
|
piece,
|
||||||
|
pred: self.clone(),
|
||||||
|
}));
|
||||||
|
succ
|
||||||
|
}
|
||||||
|
|
||||||
|
fn new(arena: *const Bump, mat: &Mat, queue: Queue, pcnt: usize) -> Self {
|
||||||
|
let arena = unsafe { &*arena };
|
||||||
|
let rating = evaluate(mat, pcnt);
|
||||||
|
let node_data = NodeData::alloc(arena, mat, queue, pcnt, rating);
|
||||||
|
Self(node_data)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn trace(self) -> Vec<Piece> {
|
||||||
|
let mut pieces = Vec::with_capacity(self.pcnt);
|
||||||
|
let mut parent = Some(self);
|
||||||
|
while let Some(node) = parent.take() {
|
||||||
|
if let Some(edge) = node.back_edge.get() {
|
||||||
|
pieces.push(edge.piece);
|
||||||
|
parent = Some(edge.pred);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
pieces.reverse();
|
||||||
let pc = match best {
|
pieces
|
||||||
Some((_, pc)) => pc,
|
|
||||||
None => return true, // no locations; game over
|
|
||||||
};
|
|
||||||
|
|
||||||
self.path.push(pc);
|
|
||||||
self.cur_mat.copy_from(&best_mat);
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn suggestion(&self) -> Vec<Piece> {
|
|
||||||
self.path.clone()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Think<'a>(&'a mut Ai);
|
impl core::cmp::Ord for Node {
|
||||||
|
fn cmp(&self, other: &Self) -> core::cmp::Ordering {
|
||||||
|
other.rating.cmp(&self.rating)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Future for Think<'_> {
|
impl core::cmp::Eq for Node {}
|
||||||
type Output = ();
|
|
||||||
|
|
||||||
fn poll(mut self: Pin<&mut Self>, _cx: &mut core::task::Context<'_>) -> Poll<Self::Output> {
|
impl core::cmp::PartialOrd for Node {
|
||||||
let ai: &mut Ai = &mut self.0;
|
fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
|
||||||
|
Some(self.cmp(other))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if ai.think_1_cycle() {
|
impl core::cmp::PartialEq for Node {
|
||||||
// TODO: if <limits reached> then return Poll::Ready)
|
fn eq(&self, other: &Self) -> bool {
|
||||||
Poll::Pending
|
self.cmp(other).is_eq()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Deref for Node {
|
||||||
|
type Target = NodeData;
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
unsafe { &*self.0 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NodeData {
|
||||||
|
fn alloc<'a>(arena: &'a Bump, mat: &Mat, queue: Queue, pcnt: usize, rating: i32) -> &'a Self {
|
||||||
|
let node = arena.alloc_with(|| NodeData {
|
||||||
|
mat: MatBuf::new(),
|
||||||
|
rating,
|
||||||
|
pcnt,
|
||||||
|
queue,
|
||||||
|
back_edge: Cell::new(None),
|
||||||
|
});
|
||||||
|
node.mat.copy_from(mat);
|
||||||
|
node
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Queue {
|
||||||
|
next: *const [PieceType],
|
||||||
|
held: Option<PieceType>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Queue {
|
||||||
|
fn alloc(arena: &Bump, previews: &[PieceType], hold: Option<PieceType>) -> Self {
|
||||||
|
Queue {
|
||||||
|
next: arena.alloc_slice_copy(previews),
|
||||||
|
held: hold,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn next(&self) -> &[PieceType] {
|
||||||
|
unsafe { &*self.next }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn current(&self) -> impl Iterator<Item = PieceType> {
|
||||||
|
[self.next().first().copied(), self.held]
|
||||||
|
.into_iter()
|
||||||
|
.flatten()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_empty(&self) -> bool {
|
||||||
|
self.next().is_empty() && self.held.is_none()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn succ(&self, ty: PieceType) -> Self {
|
||||||
|
let (hd, tl) = match self.next() {
|
||||||
|
[hd, tl @ ..] => (Some(*hd), tl),
|
||||||
|
[] => (None, &[][..]),
|
||||||
|
};
|
||||||
|
if self.held == Some(ty) {
|
||||||
|
Self { next: tl, held: hd }
|
||||||
} else {
|
} else {
|
||||||
Poll::Ready(())
|
debug_assert_eq!(hd, Some(ty));
|
||||||
|
Self {
|
||||||
|
next: tl,
|
||||||
|
held: self.held,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,83 @@
|
||||||
|
use alloc::{collections::BinaryHeap, vec::Vec};
|
||||||
|
|
||||||
|
pub trait Graph {
|
||||||
|
type Node: Node;
|
||||||
|
|
||||||
|
fn root(&mut self) -> Self::Node;
|
||||||
|
fn expand(&mut self, node: Self::Node) -> &[Self::Node];
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait Node: Clone + core::fmt::Debug + Ord {
|
||||||
|
fn is_terminal(&self) -> bool;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ModifiedAStar<G: Graph> {
|
||||||
|
graph: G,
|
||||||
|
fringe: Vec<BinaryHeap<G::Node>>,
|
||||||
|
depth: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct NoneAvailable;
|
||||||
|
|
||||||
|
impl<G: Graph> ModifiedAStar<G> {
|
||||||
|
pub fn new(mut graph: G) -> Self {
|
||||||
|
Self {
|
||||||
|
fringe: Vec::from_iter([BinaryHeap::from_iter([graph.root()])]),
|
||||||
|
depth: 0,
|
||||||
|
graph,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn step(&mut self) -> Option<G::Node> {
|
||||||
|
loop {
|
||||||
|
match self.expand() {
|
||||||
|
Ok(Some(term_node)) => break Some(term_node),
|
||||||
|
Ok(None) => continue,
|
||||||
|
Err(NoneAvailable) => match self.select() {
|
||||||
|
Ok(()) => continue,
|
||||||
|
Err(NoneAvailable) => break None,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn expand(&mut self) -> Result<Option<G::Node>, NoneAvailable> {
|
||||||
|
tracing::trace!("expand depth = {}", self.depth);
|
||||||
|
|
||||||
|
let set = self.fringe.get_mut(self.depth);
|
||||||
|
self.depth += 1;
|
||||||
|
|
||||||
|
let node = set.and_then(|s| s.pop()).ok_or(NoneAvailable)?;
|
||||||
|
if node.is_terminal() {
|
||||||
|
tracing::trace!("found terminal node {node:?}");
|
||||||
|
return Ok(Some(node));
|
||||||
|
}
|
||||||
|
|
||||||
|
let children = self.graph.expand(node).iter().cloned();
|
||||||
|
self.fringe.resize_with(self.depth + 1, BinaryHeap::new);
|
||||||
|
self.fringe[self.depth].extend(children);
|
||||||
|
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn select(&mut self) -> Result<(), NoneAvailable> {
|
||||||
|
let mut best = None;
|
||||||
|
|
||||||
|
for (depth, set) in self.fringe.iter().enumerate() {
|
||||||
|
if let Some(node) = set.peek() {
|
||||||
|
if best.as_ref().map_or(true, |best| node > best) {
|
||||||
|
best = Some(node.clone());
|
||||||
|
self.depth = depth;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(best) = best {
|
||||||
|
tracing::trace!("selected depth = {}, best = {:?}", self.depth, best);
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
tracing::trace!("fringe exhausted; no nodes remaining");
|
||||||
|
Err(NoneAvailable)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -112,24 +112,21 @@ fn print_best_move(_settings: Settings) -> anyhow::Result<()> {
|
||||||
let input: fish::io::InputState =
|
let input: fish::io::InputState =
|
||||||
serde_json::from_reader(std::io::stdin()).context("error parsing input state")?;
|
serde_json::from_reader(std::io::stdin()).context("error parsing input state")?;
|
||||||
|
|
||||||
let mut mat = input.matrix.to_mat();
|
let mat = input.matrix.to_mat();
|
||||||
mat.clear_lines();
|
// mat.clear_lines();
|
||||||
|
|
||||||
// TODO: ai init config, e.g. personality
|
// TODO: ai init config, e.g. personality
|
||||||
let mut ai = fish::Ai::new();
|
// TODO: attack state (combo/b2b)
|
||||||
|
let mut ai = fish::Ai::new(&mat, &input.queue.previews, input.queue.hold);
|
||||||
// TODO: hold
|
|
||||||
// TODO: attack state
|
|
||||||
ai.start(&mat, &input.queue.previews, input.queue.hold);
|
|
||||||
|
|
||||||
// TODO: resource limits (cycles,nodes,time)
|
// TODO: resource limits (cycles,nodes,time)
|
||||||
let mut cycles = 0;
|
let mut cycles = 0;
|
||||||
loop {
|
loop {
|
||||||
if ai.think_1_cycle() {
|
tracing::trace!("thinking... ({cycles})");
|
||||||
|
if matches!(ai.think(), Err(fish::ai::Exhausted)) || cycles > 100_000 {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
cycles += 1;
|
cycles += 1;
|
||||||
tracing::trace!("thinking ({cycles})...");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// print suggestions trace
|
// print suggestions trace
|
||||||
|
|
Loading…
Reference in New Issue