Big API refactor
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
//! ## Board read API (`state.board.*`)
|
||||
//!
|
||||
//! - `board.tagged(tag) -> Array[ObjectInfo]` — objects carrying a tag
|
||||
//! - `board.named(name) -> ObjectInfo | ()` — object with that name (or unit)
|
||||
//! - `board.get(id) -> ObjectInfo | ()` — look up by id; `-1` returns player info
|
||||
//! - `board.width` - Board width
|
||||
//! - `board.height` - Board height
|
||||
//!
|
||||
//! ## Cell queries
|
||||
//!
|
||||
//! - `board.can_push(x, y, dir) -> bool` — is the pushable chain at `(x, y)` shovable in `dir`
|
||||
//! - `board.passable(x, y) -> bool` — is `(x, y)` on-board and free of any solid (a hole)
|
||||
//! cell (one empty, or a grab thing and the player)
|
||||
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
use rhai::{Dynamic, Engine, ImmutableString};
|
||||
use crate::{Board, Direction};
|
||||
use crate::api::object_info::ObjectInfo;
|
||||
use crate::script::Registerable;
|
||||
use crate::utils::{ErrorSink, ObjectId};
|
||||
|
||||
/// A read-only handle to the world, exposed to scripts as `Board`.
|
||||
pub type BoardRef = Rc<RefCell<Board>>;
|
||||
|
||||
impl Registerable for BoardRef {
|
||||
fn register(engine: &mut Engine, error_sink: ErrorSink) {
|
||||
engine.register_type_with_name::<BoardRef>("Board");
|
||||
engine.register_get("width", |b: &mut BoardRef| b.borrow().width as i64);
|
||||
engine.register_get("height", |b: &mut BoardRef| b.borrow().height as i64);
|
||||
|
||||
// can_push(x, y, dir) — true if (x, y) holds a pushable whose chain can be
|
||||
// shoved one step in dir (the read-only half of push()). False off-board.
|
||||
engine.register_fn("can_push", move |board: BoardRef, x: i64, y: i64, dir: Direction| -> bool {
|
||||
let board = board.borrow();
|
||||
board.in_bounds((x, y)) && board.can_push(x as usize, y as usize, dir)
|
||||
});
|
||||
|
||||
// passable(x, y) — true if (x, y) is on-board and holds no solid (an empty cell
|
||||
// a mover could enter). Off-board is not passable. Lets a script distinguish a
|
||||
// hole from a blocked solid (which can_shift/can_push alone cannot).
|
||||
engine.register_fn("passable", move |board: BoardRef, x: i64, y: i64| -> bool {
|
||||
let board = board.borrow();
|
||||
board.in_bounds((x, y)) && board.is_passable(x as usize, y as usize)
|
||||
});
|
||||
|
||||
// Board.get(id) -> ObjectInfo | () (unknown id logs error)
|
||||
engine.register_fn("get", move |board: &mut BoardRef, id: i64| -> Dynamic {
|
||||
if id <= 0 {
|
||||
error_sink.error(format!("Board.get: invalid id {id}"));
|
||||
return Dynamic::UNIT;
|
||||
}
|
||||
|
||||
if let Some(obj) = ObjectInfo::from_id(id as ObjectId, board.clone()) {
|
||||
Dynamic::from(obj)
|
||||
} else {
|
||||
error_sink.error(format!("Board.get: no object with id {id}"));
|
||||
Dynamic::UNIT
|
||||
}
|
||||
});
|
||||
|
||||
// Board.named(name) -> ObjectInfo | ()
|
||||
engine.register_fn("named", move |board_ref: &mut BoardRef, name: ImmutableString| -> Dynamic {
|
||||
let board = board_ref.borrow();
|
||||
board
|
||||
.objects
|
||||
.iter()
|
||||
.find_map(|(_id, def)| {
|
||||
if def.name.as_deref() == Some(name.as_str()) {
|
||||
Some(Dynamic::from(ObjectInfo::from_def(def, board_ref.clone())))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.unwrap_or(Dynamic::UNIT)
|
||||
},
|
||||
);
|
||||
|
||||
// Board.tagged(tag) -> Array[ObjectInfo]
|
||||
engine.register_fn("tagged", move |board_ref: &mut BoardRef, tag: ImmutableString| -> rhai::Array {
|
||||
let board = board_ref.borrow();
|
||||
board
|
||||
.objects
|
||||
.iter()
|
||||
.filter_map(|(_id, def)| {
|
||||
if def.tags.contains(tag.as_str()) {
|
||||
Some(Dynamic::from(ObjectInfo::from_def(def, board_ref.clone())))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
pub mod state;
|
||||
pub mod player;
|
||||
pub mod board;
|
||||
pub mod object_info;
|
||||
pub mod queue;
|
||||
@@ -0,0 +1,137 @@
|
||||
//! ## Object Rhai API
|
||||
//!
|
||||
//! ### Getters
|
||||
//!
|
||||
//! - x, y -> Board location of object
|
||||
//! - id -> The object's board-unique id
|
||||
//! - name -> The object's name, or ()
|
||||
//! - waiting -> bool for whether or not the front of this object's queue is a delay action
|
||||
//! - queue -> the action queue for this object
|
||||
//! - tags -> Array of tags for this object
|
||||
//! - glyph -> The object's current glyph
|
||||
//!
|
||||
//! ### Functions
|
||||
//!
|
||||
//! - has_tag(tag) -> Whether the object has this tag
|
||||
//! - delay(secs) -> Queue a delay action
|
||||
//! - can_push(dir) -> Whether a push in this direction would succeed
|
||||
//! - blocked(dir) -> Whether a solid neighbors me in this direction
|
||||
//!
|
||||
//! Note on blocking: can_push and blocked only take into account the contents of the board
|
||||
//! at the start of the call; if another thing moves before your actions are flushed to the
|
||||
//! gamestate, a move might still fail. This is a TODO.
|
||||
|
||||
use rhai::{Dynamic, Engine};
|
||||
use crate::api::board::BoardRef;
|
||||
use crate::api::queue::ObjQueue;
|
||||
use crate::Direction;
|
||||
use crate::object_def::ObjectDef;
|
||||
use crate::script::{BoardQueue, Registerable};
|
||||
use crate::utils::{ErrorSink, ObjectId};
|
||||
|
||||
/// A snapshot of one board object, returned by `Board.tagged`, `Board.named`,
|
||||
/// and `Board.get`. Passed by value — scripts read fields, not a live reference.
|
||||
/// It also contains things like script_name used by ScriptHost, since we can't keep a live
|
||||
/// borrow of the board while doing anything: we create one of these loose and then
|
||||
/// it borrows the board only for the duration of what it needs.
|
||||
#[derive(Clone)]
|
||||
pub struct ObjectInfo {
|
||||
pub id: ObjectId,
|
||||
pub x: i64,
|
||||
pub y: i64,
|
||||
pub board: BoardRef,
|
||||
pub script_name: Option<String>,
|
||||
pub queue: ObjQueue
|
||||
}
|
||||
|
||||
impl ObjectInfo {
|
||||
pub fn from_id(id: ObjectId, board: BoardRef) -> Option<ObjectInfo> {
|
||||
let b = board.borrow();
|
||||
let obj = b.objects.get(&id)?;
|
||||
Some(ObjectInfo {
|
||||
id,
|
||||
x: obj.x as i64,
|
||||
y: obj.y as i64,
|
||||
board: board.clone(),
|
||||
script_name: obj.script_name.clone(),
|
||||
queue: obj.queue.clone()
|
||||
})
|
||||
}
|
||||
|
||||
pub fn from_def(obj: &ObjectDef, board: BoardRef) -> ObjectInfo {
|
||||
Self {
|
||||
id: obj.id,
|
||||
x: obj.x as i64,
|
||||
y: obj.y as i64,
|
||||
board: board.clone(),
|
||||
script_name: obj.script_name.clone(),
|
||||
queue: obj.queue.clone()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn drain(&mut self, target: BoardQueue, dt: f64) {
|
||||
let mut b = self.board.borrow_mut();
|
||||
if let Some(def) = b.objects.get_mut(&self.id) {
|
||||
def.queue.drain(self.id, target, dt)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Registerable for ObjectInfo {
|
||||
fn register(engine: &mut Engine, _error_sink: ErrorSink) {
|
||||
engine.register_type_with_name::<ObjectInfo>("ObjectInfo")
|
||||
.register_get("x", |obj: &mut ObjectInfo| obj.x)
|
||||
.register_get("y", |obj: &mut ObjectInfo| obj.y)
|
||||
.register_get("id", |obj: &mut ObjectInfo| obj.id as i64)
|
||||
.register_get("waiting", |obj: &mut ObjectInfo| obj.queue.waiting())
|
||||
.register_get("queue", |obj: &mut ObjectInfo| obj.queue.clone());
|
||||
|
||||
engine.register_get("name", |o: &mut ObjectInfo| {
|
||||
let board = o.board.borrow();
|
||||
let obj = board.objects.get(&o.id);
|
||||
if let Some(ObjectDef { name: Some(name), ..}) = obj {
|
||||
Dynamic::from(name.clone())
|
||||
} else {
|
||||
Dynamic::UNIT
|
||||
}
|
||||
});
|
||||
|
||||
engine.register_get("tags", |o: &mut ObjectInfo| -> rhai::Array {
|
||||
let board = o.board.borrow();
|
||||
let obj = board.objects.get(&o.id);
|
||||
if let Some(ObjectDef { tags, .. }) = obj {
|
||||
tags.iter().map(|t| Dynamic::from(t.clone())).collect()
|
||||
} else {
|
||||
rhai::Array::new()
|
||||
}
|
||||
});
|
||||
|
||||
engine.register_get("glyph", |o: &mut ObjectInfo| {
|
||||
let board = o.board.borrow();
|
||||
let obj = board.objects.get(&o.id).unwrap();
|
||||
obj.glyph
|
||||
});
|
||||
|
||||
engine.register_fn("has_tag", |o: &mut ObjectInfo, t: String| {
|
||||
if let Some(ObjectDef { tags, .. }) = o.board.borrow().objects.get(&o.id) {
|
||||
tags.contains(&t)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
});
|
||||
|
||||
engine.register_fn("delay", |o: &mut ObjectInfo, dt: f64| o.queue.delay(dt));
|
||||
|
||||
engine.register_fn("can_push", move |o: &mut ObjectInfo, dir: Direction| {
|
||||
let board = o.board.borrow();
|
||||
board.in_bounds((o.x, o.y)) && board.can_push(o.x as usize, o.y as usize, dir)
|
||||
});
|
||||
|
||||
engine.register_fn("blocked", move |o: &mut ObjectInfo, dir: Direction| -> bool {
|
||||
let board = o.board.borrow();
|
||||
let tx = o.x + dir.dx();
|
||||
let ty = o.y + dir.dy();
|
||||
board.in_bounds((o.x, o.y)) && !board.is_passable(tx as usize, ty as usize)
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
use rhai::Engine;
|
||||
use crate::player::Player;
|
||||
use crate::script::Registerable;
|
||||
use crate::utils::{ErrorSink, PlayerPos};
|
||||
|
||||
/// GameState stores player state but Board stores its position, and we want one
|
||||
/// object to register with Rhai
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct PlayerWithPos(pub Player, pub PlayerPos);
|
||||
|
||||
impl Registerable for PlayerWithPos {
|
||||
fn register(engine: &mut Engine, _error_sink: ErrorSink) {
|
||||
engine.register_type_with_name::<PlayerWithPos>("Player")
|
||||
.register_get("gems", |player: &mut PlayerWithPos| player.0.gems)
|
||||
.register_get("health", |player: &mut PlayerWithPos| player.0.health)
|
||||
.register_get("max_health", |player: &mut PlayerWithPos| player.0.max_health)
|
||||
.register_get("keys", |player: &mut PlayerWithPos| player.0.keys)
|
||||
.register_get("x", |player: &mut PlayerWithPos| player.1.x)
|
||||
.register_get("y", |player: &mut PlayerWithPos| player.1.y);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
//! ## Queue API
|
||||
//!
|
||||
//! `queue.length`, `queue.clear()`, `queue.peek()`, `queue.pop()`, `queue.delay()`
|
||||
|
||||
use std::cell::RefCell;
|
||||
use std::collections::VecDeque;
|
||||
use std::rc::Rc;
|
||||
use rhai::{Dynamic, Engine};
|
||||
use crate::action::{action_to_map, Action, BoardAction};
|
||||
use crate::script::{BoardQueue, Registerable};
|
||||
use crate::utils::{ErrorSink, ObjectId};
|
||||
|
||||
/// A single object's output queue.
|
||||
#[derive(Clone)]
|
||||
pub struct ObjQueue(Rc<RefCell<VecDeque<Action>>>);
|
||||
|
||||
impl ObjQueue {
|
||||
pub fn new() -> Self {
|
||||
Self(Rc::new(RefCell::new(VecDeque::new())))
|
||||
}
|
||||
|
||||
/// Add a delay to the tail of the queue, or alter the value already there by the given amount.
|
||||
/// A delay action's value can never be less than 0.0
|
||||
/// TODO This really ought to be Duration
|
||||
pub fn delay(&mut self, delay: f64) {
|
||||
let mut q = self.0.borrow_mut();
|
||||
if let Some(Action::Delay(r)) = q.back_mut() {
|
||||
*r = (*r + delay).max(0.0)
|
||||
} else {
|
||||
q.push_back(Action::Delay(delay.max(0.0)))
|
||||
}
|
||||
}
|
||||
|
||||
/// Add an action to the tail of the queue
|
||||
pub fn act(&mut self, action: Action) {
|
||||
self.0.borrow_mut().push_back(action);
|
||||
}
|
||||
|
||||
/// promote the most recently enqueued action to the front.
|
||||
pub fn now(&mut self) {
|
||||
let mut q = self.0.borrow_mut();
|
||||
if let Some(back) = q.pop_back() {
|
||||
q.push_front(back);
|
||||
}
|
||||
}
|
||||
|
||||
/// Drains object `i`'s output queue onto a target queue: first advances
|
||||
/// leading `Delay` actions by dt, then drains actions into the target
|
||||
/// queue until we run out (or hit another delay)
|
||||
pub fn drain(&mut self, source: ObjectId, target: BoardQueue, mut dt: f64) {
|
||||
let mut queue = self.0.borrow_mut();
|
||||
loop {
|
||||
match queue.front_mut() {
|
||||
None => break, // No more actions, we're done
|
||||
Some(Action::Delay(time)) => { // A delay, decrease it by dt
|
||||
if dt < *time { // Delay is too long, bail out
|
||||
*time -= dt;
|
||||
break;
|
||||
} else { // dt eats the delay, pop it and continue
|
||||
dt -= *time;
|
||||
queue.pop_front();
|
||||
}
|
||||
}
|
||||
Some(_) => {
|
||||
let action = queue.pop_front().unwrap();
|
||||
target.borrow_mut().push(BoardAction { source, action });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Return whether this queue has an `Action::Delay` in the front
|
||||
pub fn waiting(&mut self) -> bool {
|
||||
matches!(self.0.borrow().front(), Some(Action::Delay(_)))
|
||||
}
|
||||
}
|
||||
|
||||
impl Registerable for ObjQueue {
|
||||
fn register(engine: &mut Engine, error_sink: ErrorSink) {
|
||||
engine.register_type_with_name::<ObjQueue>("Queue");
|
||||
engine.register_get("length", |q: &mut ObjQueue| q.0.borrow().len() as i64);
|
||||
engine.register_fn("clear", |q: &mut ObjQueue| q.0.borrow_mut().clear());
|
||||
|
||||
// peek(): front action as a Rhai map, or () if empty.
|
||||
engine.register_fn("peek", |q: &mut ObjQueue| -> Dynamic {
|
||||
q.0.borrow()
|
||||
.front()
|
||||
.map(|a| Dynamic::from(action_to_map(a)))
|
||||
.unwrap_or(Dynamic::UNIT)
|
||||
});
|
||||
|
||||
// pop(): same, but removes the front.
|
||||
engine.register_fn("pop", |q: &mut ObjQueue| -> Dynamic {
|
||||
q.0.borrow_mut()
|
||||
.pop_front()
|
||||
.as_ref()
|
||||
.map(|a| Dynamic::from(action_to_map(a)))
|
||||
.unwrap_or(Dynamic::UNIT)
|
||||
});
|
||||
|
||||
// Appends a [`Action::Delay`] to the back of `queue`, merging with an existing
|
||||
// trailing delay to prevent adjacent delays from accumulating.
|
||||
engine.register_fn("delay", move |q: &mut ObjQueue, secs: Dynamic| {
|
||||
let secs: Result<f64, &str> = if secs.is_float() {
|
||||
secs.as_float()
|
||||
} else if secs.is_int() {
|
||||
secs.as_int().map(|i| i as f64)
|
||||
} else {
|
||||
Err("delay() must be an int or float")
|
||||
};
|
||||
|
||||
match secs {
|
||||
Ok(secs) => q.delay(secs),
|
||||
Err(msg) => error_sink.error(msg.to_string())
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
use rhai::Engine;
|
||||
use crate::api::board::BoardRef;
|
||||
use crate::api::player::PlayerWithPos;
|
||||
use crate::game::GameState;
|
||||
use crate::script::Registerable;
|
||||
use crate::utils::ErrorSink;
|
||||
|
||||
/// The host-provided context handed to every script hook for the duration of one
|
||||
/// call. Currently just the player snapshot, but it exists so more host state can
|
||||
/// be threaded through the `run_*` methods without changing each signature again.
|
||||
#[derive(Clone)]
|
||||
pub struct ScriptState {
|
||||
pub player: PlayerWithPos,
|
||||
pub board: BoardRef,
|
||||
}
|
||||
|
||||
impl ScriptState {
|
||||
pub fn from_game_state(game_state: &GameState) -> Self {
|
||||
Self {
|
||||
player: PlayerWithPos(game_state.player, game_state.board().player),
|
||||
board: game_state.board_rc(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Registerable for ScriptState {
|
||||
fn register(engine: &mut Engine, _error_sink: ErrorSink) {
|
||||
engine.register_type_with_name::<ScriptState>("State")
|
||||
.register_get("player", |state: &mut ScriptState| state.player)
|
||||
.register_get("board", |state: &mut ScriptState| state.board.clone());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user