removed boardstate
This commit is contained in:
+4
-4
@@ -92,8 +92,8 @@ An earlier design had `cells: Vec<(Glyph, usize)>` where the `usize` indexed a p
|
||||
**Why `Board` is the complete unit (no wrapper struct):**
|
||||
An earlier design had `GameMap { board: Board, player: Player, ... }`. This was eliminated because the split was artificial: there's no meaningful use of a `Board` without a player position, and no meaningful use of a player without a `Board`. ZZT itself treats a board as containing everything — the grid, the objects, and the player entry point. Collapsing to a single struct matches the domain model.
|
||||
|
||||
**`BoardState` / `GameState`**
|
||||
`BoardState` is the scriptable world — currently just `{ board: Board }`, with room for runtime-only state (e.g. a shared variable blackboard). `GameState` holds it behind an `Rc<RefCell<BoardState>>`, alongside the message `log` and a `ScriptHost`. Keeping the world a **sibling** of the script host (rather than owned by it) is the key ownership move: host functions capture a clone of that `Rc` — a `'static` handle to a *different* `RefCell` than the running engine — so they can read/queue writes without aliasing the borrow that's executing scripts. Front-ends and internal logic reach the board only through `board() -> Ref<Board>` / `board_mut() -> RefMut<Board>` (no `pub board`). `try_move` moves the player; `run_init()` / `tick(dt)` run object hooks; `apply_commands()` drains the script command queue *after* each batch — the deferred apply is when `&mut` is finally free of any outstanding borrow.
|
||||
**`GameState`**
|
||||
Owns the board behind an `Rc<RefCell<Board>>`, alongside the message `log` and a `ScriptHost`. Keeping the board a **sibling** of the script host (rather than owned by it) is the key ownership move: host functions capture a clone of that `Rc` — a `'static` handle to a *different* `RefCell` than the running engine — so they can read/queue writes without aliasing the borrow that's executing scripts. Front-ends and internal logic reach the board only through `board() -> Ref<Board>` / `board_mut() -> RefMut<Board>` (no `pub board`). `try_move` moves the player; `run_init()` / `tick(dt)` run object hooks; `apply_commands()` drains the script command queue *after* each batch — the deferred apply is when `&mut` is finally free of any outstanding borrow.
|
||||
|
||||
---
|
||||
|
||||
@@ -103,11 +103,11 @@ An earlier design had `GameMap { board: Board, player: Player, ... }`. This was
|
||||
|
||||
### `script.rs` — Rhai scripting runtime
|
||||
|
||||
`ScriptHost` owns the Rhai `Engine`, the scripts referenced by a board's objects (compiled once per name via `Engine::compile`), a persistent per-object `Scope`, and a shared **command queue**. Built with `ScriptHost::new(&Rc<RefCell<BoardState>>)`, which registers the API, compiles scripts, and records each object's available hooks (detected with `AST::iter_functions()` by name/arity) but **runs nothing** — compile/unknown-script failures are queued as `GameCommand::Error`.
|
||||
`ScriptHost` owns the Rhai `Engine`, the scripts referenced by a board's objects (compiled once per name via `Engine::compile`), a persistent per-object `Scope`, and a shared **command queue**. Built with `ScriptHost::new(&Rc<RefCell<Board>>)`, which registers the API, compiles scripts, and records each object's available hooks (detected with `AST::iter_functions()` by name/arity) but **runs nothing** — compile/unknown-script failures are queued as `GameCommand::Error`.
|
||||
|
||||
Two optional lifecycle hooks per object: `init()` (zero-arg, run once) and `tick(dt)` (elapsed seconds as `f64`, run per frame), driven by `run_init()` / `run_tick(dt)`. Runtime errors become `Error` commands, not fatal.
|
||||
|
||||
**Reads vs writes.** Scripts **read** the world directly: a read-only `BoardView` (a registered type with getters only — read-only by construction) is pushed into each scope as `board`, so a script writes `board.player_x`; the getter briefly borrows the shared `BoardState`. Scripts **write** by enqueuing `Command { source, kind }` where `kind: GameCommand` is `Move(Direction)` / `SetTile(u32)` / `Log(LogLine)` / `Error(String)`. The host fns `move`/`set_tile`/`log` push into a shared `Rc<RefCell<Vec<Command>>>` cloned into each closure (the same `'static`-closure trick the old log sink used, generalized); `GameState::apply_commands` drains and applies them after the batch. Reads-are-live, writes-are-deferred gives frame-coherent semantics and keeps script execution free of any `&mut GameState` borrow.
|
||||
**Reads vs writes.** Scripts **read** the world directly: a read-only `BoardView` (a registered type with getters only — read-only by construction) is pushed into each scope as `board`, so a script writes `board.player_x`; the getter briefly borrows the shared `Board`. Scripts **write** by enqueuing `Command { source, kind }` where `kind: GameCommand` is `Move(Direction)` / `SetTile(u32)` / `Log(LogLine)` / `Error(String)`. The host fns `move`/`set_tile`/`log` push into a shared `Rc<RefCell<Vec<Command>>>` cloned into each closure (the same `'static`-closure trick the old log sink used, generalized); `GameState::apply_commands` drains and applies them after the batch. Reads-are-live, writes-are-deferred gives frame-coherent semantics and keeps script execution free of any `&mut GameState` borrow.
|
||||
|
||||
**Sender identity** (which object issued a command) rides Rhai's per-call **tag**: `run` calls `call_fn_with_options(...with_tag(object_index)...)` and the host fns read it via `NativeCallContext::tag()`, so scripts write `move(north)` without a `this` argument (`north`/`south`/`east`/`west` are `Direction` constants in scope; `impl From<Direction> for (i32,i32)` yields the delta). The object's identity is currently its index into `Board::objects` — a stopgap (a `// TODO` flags replacing it with stable unique ids that survive spawn/destroy). Default `call_fn` rewinds the scope per call, so script-local persistence across ticks is still future work; the per-object `Scope` is the seam for that. `Engine`/`Scope` are not `Send` — fine for the single-threaded kiln-tui.
|
||||
|
||||
|
||||
@@ -53,16 +53,15 @@ Root `Cargo.toml`: `members = ["kiln-core", "kiln-tui"]`.
|
||||
- `Player` — `x: i32, y: i32`
|
||||
- `ObjectDef` — scripted object placed on the board: `x`, `y`, `glyph: Glyph`, `passable: bool`, `opaque: bool`, `script_name: Option<String>`. `passable` defaults `false` and `opaque` defaults `true` in map files. Its `init()`/`tick(dt)` hooks are run by the scripting runtime (see `script.rs`).
|
||||
- `PortalDef` — parsed from map files, stored on Board; not yet runtime-wired
|
||||
- `BoardState` — the scriptable world: currently just `board: Board` (room to grow, e.g. a shared variable blackboard). Held by `GameState` behind `Rc<RefCell<BoardState>>` so script host functions can share a handle to it without aliasing the running engine.
|
||||
- `GameState` — holds `board_state: Rc<RefCell<BoardState>>`, the message `log: Vec<LogLine>`, and a `ScriptHost`. Front-ends/logic reach the board through `board() -> Ref<Board>` / `board_mut() -> RefMut<Board>` (there is no `pub board` field). `try_move` moves the player; `run_init()` runs object `init()` hooks once at startup; `tick(dt)` runs object `tick(dt)` hooks every frame. After each script batch, `apply_commands()` drains the script command queue: `Log`/`Error` → `log`, `SetTile` → the source object's glyph, `Move(dir)` → `move_object` (bounds + `is_passable`). This deferred apply (writes after the batch) is what keeps script execution borrow-safe.
|
||||
- `GameState` — holds `board: Rc<RefCell<Board>>`, the message `log: Vec<LogLine>`, and a `ScriptHost`. The board sits behind `Rc<RefCell<…>>` as a sibling of the `ScriptHost` so script host functions can hold a shared handle to it without aliasing the borrow that's running the engine. Front-ends/logic reach the board through `board() -> Ref<Board>` / `board_mut() -> RefMut<Board>` (there is no `pub board` field). `try_move` moves the player; `run_init()` runs object `init()` hooks once at startup; `tick(dt)` runs object `tick(dt)` hooks every frame. After each script batch, `apply_commands()` drains the script command queue: `Log`/`Error` → `log`, `SetTile` → the source object's glyph, `Move(dir)` → `move_object` (bounds + `is_passable`). This deferred apply (writes after the batch) is what keeps script execution borrow-safe.
|
||||
|
||||
**`kiln-core/src/log.rs`** — styled log messages:
|
||||
- `LogSpan { text, fg: Option<Rgba8>, bg: Option<Rgba8> }` and `LogLine { spans: Vec<LogSpan> }` — a UI-agnostic styled message (colors are core `Rgba8`, not a front-end type). `LogLine::raw()`, a chainable `push()`, and `append()` build messages; each front-end converts a `LogLine` to its own styled text at render time.
|
||||
|
||||
**`kiln-core/src/script.rs`** — Rhai scripting runtime:
|
||||
- `ScriptHost` — owns the Rhai `Engine`, the compiled scripts referenced by a board's objects (compiled once per name), a per-object persistent `Scope`, and a shared command queue. Built with `ScriptHost::new(&Rc<RefCell<BoardState>>)` (registers the API, compiles scripts, reports compile/unknown-script failures as `GameCommand::Error`; runs nothing).
|
||||
- `ScriptHost` — owns the Rhai `Engine`, the compiled scripts referenced by a board's objects (compiled once per name), a per-object persistent `Scope`, and a shared command queue. Built with `ScriptHost::new(&Rc<RefCell<Board>>)` (registers the API, compiles scripts, reports compile/unknown-script failures as `GameCommand::Error`; runs nothing).
|
||||
- Lifecycle hooks per object: `init()` (zero-arg) and `tick(dt)` (elapsed seconds as `f64`), both optional — detected via `AST::iter_functions()` by name and arity. Driven by `run_init()` / `run_tick(dt)`; runtime errors become `Error` commands, not fatal.
|
||||
- **Reads (direct):** scripts read through `BoardView` — a read-only handle (getters only, so read-only by construction) pushed into each scope as `board`, e.g. `board.player_x`, `board.player_y`, `board.width`, `board.height`. Each getter briefly borrows the shared `BoardState`.
|
||||
- **Reads (direct):** scripts read through `BoardView` — a read-only handle (getters only, so read-only by construction) pushed into each scope as `board`, e.g. `board.player_x`, `board.player_y`, `board.width`, `board.height`. Each getter briefly borrows the shared `Board`.
|
||||
- **Writes (command queue):** host fns `move(dir)`, `set_tile(n)`, `log(s)` push a `Command { source, kind: GameCommand }` (`GameCommand` = `Move(Direction)` / `SetTile(u32)` / `Log(LogLine)` / `Error(String)`) into a shared `Rc<RefCell<Vec<Command>>>`, drained by `GameState::take_commands` and applied after the batch. This is how scripts mutate without a `&mut GameState` borrow.
|
||||
- **Sender identity:** `source` (which object issued a command) rides the per-call **tag** — `run` calls `call_fn_with_options(...with_tag(object_index)...)` and the host fns read it via `NativeCallContext::tag()`. Scripts write `move(north)` without naming themselves; `north`/`south`/`east`/`west` are `Direction` constants in scope, and `impl From<Direction> for (i32,i32)` gives the delta.
|
||||
- `GameState` (hence the `Engine`/`Scope`) is single-threaded / not `Send`; fine for kiln-tui.
|
||||
|
||||
+7
-19
@@ -393,22 +393,10 @@ impl Board {
|
||||
}
|
||||
}
|
||||
|
||||
/// The scriptable world: the current [`Board`] plus (in the future) runtime-only
|
||||
/// state such as a shared variable blackboard.
|
||||
///
|
||||
/// `BoardState` is a sibling of the [`ScriptHost`] inside [`GameState`] and is
|
||||
/// held behind an `Rc<RefCell<…>>`. Keeping it separate from the script engine is
|
||||
/// what lets host functions hold a shared handle to the world without aliasing the
|
||||
/// borrow that is currently running the engine.
|
||||
pub struct BoardState {
|
||||
/// The active game board.
|
||||
pub board: Board,
|
||||
}
|
||||
|
||||
/// Holds the active game world and provides game-logic operations.
|
||||
///
|
||||
/// `GameState` is the boundary between the engine (rendering, input) and the
|
||||
/// game data. It owns the world ([`BoardState`], behind a shared `Rc<RefCell>`),
|
||||
/// game data. It owns the board (behind a shared `Rc<RefCell<Board>>`),
|
||||
/// the message log, and the [`ScriptHost`] driving object scripts. Front-ends and
|
||||
/// internal logic reach the board through [`board`](GameState::board) /
|
||||
/// [`board_mut`](GameState::board_mut). Scripts read the board directly and
|
||||
@@ -416,7 +404,7 @@ pub struct BoardState {
|
||||
/// after each script batch.
|
||||
pub struct GameState {
|
||||
/// The scriptable world, shared with the script host's read getters.
|
||||
board_state: Rc<RefCell<BoardState>>,
|
||||
board: Rc<RefCell<Board>>,
|
||||
/// The in-game message log, oldest first (newest pushed at the end).
|
||||
pub log: Vec<LogLine>,
|
||||
/// The Rhai scripting runtime driving this board's scripted objects.
|
||||
@@ -430,10 +418,10 @@ impl GameState {
|
||||
/// call [`GameState::run_init`] once the game is ready to start. Any script
|
||||
/// compile errors are surfaced into the log here.
|
||||
pub fn new(board: Board) -> Self {
|
||||
let board_state = Rc::new(RefCell::new(BoardState { board }));
|
||||
let scripts = ScriptHost::new(&board_state);
|
||||
let board = Rc::new(RefCell::new(board));
|
||||
let scripts = ScriptHost::new(&board);
|
||||
let mut state = Self {
|
||||
board_state,
|
||||
board,
|
||||
log: Vec::new(),
|
||||
scripts,
|
||||
};
|
||||
@@ -444,12 +432,12 @@ impl GameState {
|
||||
|
||||
/// Borrows the active board for reading (e.g. by a front-end renderer).
|
||||
pub fn board(&self) -> Ref<'_, Board> {
|
||||
Ref::map(self.board_state.borrow(), |bs| &bs.board)
|
||||
self.board.borrow()
|
||||
}
|
||||
|
||||
/// Borrows the active board for mutation.
|
||||
pub fn board_mut(&self) -> RefMut<'_, Board> {
|
||||
RefMut::map(self.board_state.borrow_mut(), |bs| &mut bs.board)
|
||||
self.board.borrow_mut()
|
||||
}
|
||||
|
||||
/// Appends a styled message to the log.
|
||||
|
||||
+14
-21
@@ -11,7 +11,7 @@
|
||||
//!
|
||||
//! Scripts **read** the world directly through a read-only [`BoardView`] (getters
|
||||
//! only) pushed into each scope as `board`, e.g. `board.player_x`. The view holds
|
||||
//! an `Rc<RefCell<BoardState>>` and borrows it briefly per getter.
|
||||
//! an `Rc<RefCell<Board>>` and borrows it briefly per getter.
|
||||
//!
|
||||
//! Scripts **write** by enqueuing [`Command`]s: host functions (`move`,
|
||||
//! `set_tile`, `log`) push into a shared queue that [`GameState`] drains and
|
||||
@@ -23,7 +23,7 @@
|
||||
//!
|
||||
//! [`GameState`]: crate::game::GameState
|
||||
|
||||
use crate::game::BoardState;
|
||||
use crate::game::Board;
|
||||
use crate::log::LogLine;
|
||||
use rhai::{AST, CallFnOptions, Engine, FuncArgs, ImmutableString, NativeCallContext, Scope};
|
||||
use std::cell::RefCell;
|
||||
@@ -81,10 +81,10 @@ impl From<Direction> for (i32, i32) {
|
||||
}
|
||||
|
||||
/// A read-only handle to the world, exposed to scripts as `board`. Holds a shared
|
||||
/// reference to the [`BoardState`]; only getters are registered, so it is
|
||||
/// reference to the [`Board`]; only getters are registered, so it is
|
||||
/// read-only by construction.
|
||||
#[derive(Clone)]
|
||||
struct BoardView(Rc<RefCell<BoardState>>);
|
||||
struct BoardView(Rc<RefCell<Board>>);
|
||||
|
||||
/// A compiled script plus which lifecycle hooks it defines.
|
||||
struct CompiledScript {
|
||||
@@ -128,15 +128,14 @@ impl ScriptHost {
|
||||
/// fresh scope per scripted object. Compile errors and references to unknown
|
||||
/// scripts are queued as [`GameCommand::Error`] (retrievable via
|
||||
/// [`Self::take_commands`]); no script is run here.
|
||||
pub fn new(state: &Rc<RefCell<BoardState>>) -> Self {
|
||||
pub fn new(board_cell: &Rc<RefCell<Board>>) -> Self {
|
||||
let commands: CommandQueue = Rc::new(RefCell::new(Vec::new()));
|
||||
|
||||
let mut engine = Engine::new();
|
||||
register_read_api(&mut engine);
|
||||
register_write_api(&mut engine, &commands);
|
||||
|
||||
let board_state = state.borrow();
|
||||
let board = &board_state.board;
|
||||
let board = board_cell.borrow();
|
||||
|
||||
// Compile each referenced script once; attribute failures to the first
|
||||
// object that references the script.
|
||||
@@ -200,12 +199,12 @@ impl ScriptHost {
|
||||
scripts.contains_key(name).then(|| ObjectRuntime {
|
||||
object_index: i,
|
||||
script_name: name.clone(),
|
||||
scope: new_object_scope(state),
|
||||
scope: new_object_scope(board_cell),
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
drop(board_state);
|
||||
drop(board);
|
||||
|
||||
Self {
|
||||
engine,
|
||||
@@ -280,16 +279,10 @@ impl ScriptHost {
|
||||
/// borrow the shared state briefly.
|
||||
fn register_read_api(engine: &mut Engine) {
|
||||
engine.register_type_with_name::<BoardView>("BoardView");
|
||||
engine.register_get("player_x", |b: &mut BoardView| {
|
||||
b.0.borrow().board.player.x as i64
|
||||
});
|
||||
engine.register_get("player_y", |b: &mut BoardView| {
|
||||
b.0.borrow().board.player.y as i64
|
||||
});
|
||||
engine.register_get("width", |b: &mut BoardView| b.0.borrow().board.width as i64);
|
||||
engine.register_get("height", |b: &mut BoardView| {
|
||||
b.0.borrow().board.height as i64
|
||||
});
|
||||
engine.register_get("player_x", |b: &mut BoardView| b.0.borrow().player.x as i64);
|
||||
engine.register_get("player_y", |b: &mut BoardView| b.0.borrow().player.y as i64);
|
||||
engine.register_get("width", |b: &mut BoardView| b.0.borrow().width as i64);
|
||||
engine.register_get("height", |b: &mut BoardView| b.0.borrow().height as i64);
|
||||
}
|
||||
|
||||
/// Registers the write API: the `Direction` type and the `move`/`set_tile`/`log`
|
||||
@@ -335,9 +328,9 @@ fn source_of(ctx: &NativeCallContext) -> usize {
|
||||
|
||||
/// Builds a fresh per-object scope, seeded with the read-only `board` view and
|
||||
/// the four direction constants (`north`/`south`/`east`/`west`).
|
||||
fn new_object_scope(state: &Rc<RefCell<BoardState>>) -> Scope<'static> {
|
||||
fn new_object_scope(board: &Rc<RefCell<Board>>) -> Scope<'static> {
|
||||
let mut scope = Scope::new();
|
||||
scope.push_constant("board", BoardView(state.clone()));
|
||||
scope.push_constant("board", BoardView(board.clone()));
|
||||
scope.push_constant("north", Direction::North);
|
||||
scope.push_constant("south", Direction::South);
|
||||
scope.push_constant("east", Direction::East);
|
||||
|
||||
Reference in New Issue
Block a user