Fixing tests and comments
This commit is contained in:
+160
-130
@@ -5,6 +5,10 @@ file. Each scripted object references a script by name via `script_name`. A scri
|
||||
hooks (any may be omitted) and may define arbitrary handler functions reachable via `send()` and
|
||||
scroll choices.
|
||||
|
||||
Every hook receives the same first two parameters — `me` (this object) and `state` (the world) —
|
||||
through which all reads happen. Writes go through free functions (`move`, `say`, `set_tile`, …) that
|
||||
implicitly act on the calling object.
|
||||
|
||||
> **Status:** this document describes the API as it currently exists. It has grown organically and
|
||||
> has known rough edges — duplication, naming inconsistency, and some unwieldy machinery. Those are
|
||||
> catalogued in [Design notes](#design-notes-rough-edges) at the end, as the starting point for a
|
||||
@@ -15,33 +19,36 @@ scroll choices.
|
||||
## Lifecycle hooks
|
||||
|
||||
Hooks are detected by name **and** arity (parameter count). A function with the right name but wrong
|
||||
number of parameters is not recognized as a hook.
|
||||
number of parameters is not recognized as a hook. Every hook takes `me` (an [`ObjectInfo`](#self-and-other-objects-objectinfo)
|
||||
for the object running the script) and `state` (a [`State`](#the-world-state) handle to the world),
|
||||
in that order, before any hook-specific argument.
|
||||
|
||||
### `fn init()`
|
||||
### `fn init(me, state)`
|
||||
|
||||
Called once for every scripted object after the entire map is loaded, before the first frame.
|
||||
Called once for every scripted object after the entire map is loaded, before the first frame (and
|
||||
again after a board transition re-enters this board's script host).
|
||||
|
||||
```rhai
|
||||
fn init() {
|
||||
fn init(me, state) {
|
||||
set_tile(42);
|
||||
log("ready");
|
||||
}
|
||||
```
|
||||
|
||||
### `fn tick(dt)`
|
||||
### `fn tick(me, state, dt)`
|
||||
|
||||
Called every frame. `dt` is the elapsed time since the last tick, in seconds (a `f64`). Timed
|
||||
actions pace themselves through the queue; you do not track time manually for simple walking.
|
||||
|
||||
```rhai
|
||||
fn tick(dt) {
|
||||
if Queue.length() == 0 && !blocked(North) {
|
||||
fn tick(me, state, dt) {
|
||||
if !me.waiting && !me.blocked(North) {
|
||||
move(North);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### `fn bump(id)`
|
||||
### `fn bump(me, state, id)`
|
||||
|
||||
Called when another entity walks into this object's cell.
|
||||
|
||||
@@ -49,13 +56,13 @@ Called when another entity walks into this object's cell.
|
||||
- `id == -1` means the **player** bumped this object.
|
||||
|
||||
```rhai
|
||||
fn bump(id) {
|
||||
fn bump(me, state, id) {
|
||||
if id == -1 { say("Hey! Watch it."); }
|
||||
else { log(`object ${id} bumped me`); }
|
||||
}
|
||||
```
|
||||
|
||||
### `fn grab()`
|
||||
### `fn grab(me, state)`
|
||||
|
||||
Called when the **player walks onto** this object and the object is a *grab* thing (gems, hearts —
|
||||
`solid + grab`). Unlike `bump`, the player ends up on the object's cell; the hook typically adjusts a
|
||||
@@ -63,7 +70,7 @@ player stat and removes the object. It fires followed by an immediate resolve, s
|
||||
`add_gems()` / `alter_health()` applies before the player's move returns.
|
||||
|
||||
```rhai
|
||||
fn grab() {
|
||||
fn grab(me, state) {
|
||||
add_gems(1);
|
||||
die();
|
||||
}
|
||||
@@ -78,129 +85,148 @@ Any other function can be invoked on an object two ways:
|
||||
|
||||
- Another object calls `send(target_id, "fn_name" [, arg])`.
|
||||
- A `scroll()` **choice** is selected: the choice key is dispatched to the source object as a
|
||||
zero-argument function call of that name.
|
||||
function call of that name.
|
||||
|
||||
A handler may take **zero or one** parameter; the host calls the 1-arg form when an arg is supplied,
|
||||
else the 0-arg form. If neither arity exists, the call is silently dropped.
|
||||
The host picks the handler's parameter list by arity, in this order:
|
||||
|
||||
- **3 params** → `(me, state, arg)` — `arg` is the value passed to `send` (or unit for a scroll choice).
|
||||
- **2 params** → `(me, state)`.
|
||||
- **1 param** → `(arg)`.
|
||||
- **0 params** → `()`.
|
||||
|
||||
If no matching function name exists, the call is silently dropped.
|
||||
|
||||
```rhai
|
||||
// reached via send(my_id, "open") or a scroll choice keyed "open"
|
||||
fn open() { set_tile(47); }
|
||||
fn set_level(n){ set_tag(Me.id, "level", true); log(`level ${n}`); }
|
||||
fn open(me, state) { set_tile(47); }
|
||||
fn set_level(me, state, n) { set_tag(me.id, "level", true); log(`level ${n}`); }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Constants in scope
|
||||
|
||||
Pre-set in every object's scope. The object handles are scope constants; direction/color constants
|
||||
come from a global module visible at any call depth (including Rhai→Rhai calls).
|
||||
Unlike the `me`/`state` parameters (which every hook receives), these are injected directly into
|
||||
every object's scope.
|
||||
|
||||
| Name | Type | Description |
|
||||
|------|------|-------------|
|
||||
| `Board` | `Board` | Read-only handle to the current board / world. |
|
||||
| `Queue` | `Queue` | This object's own pending-action queue. |
|
||||
| `Me` | `Me` | Self-reference (id, name, position, tags, glyph). |
|
||||
| `Registry` | `Registry` | Board-scoped key→value store; persists across board transitions. |
|
||||
| `Player` | `Player` | Read-only snapshot of the player's game-global stats. |
|
||||
| `North` / `South` / `East` / `West` | `Direction` | Cardinal directions. |
|
||||
| `Black` … `White` (16) | `String` | EGA/VGA palette colors as `"#RRGGBB"` strings. |
|
||||
|
||||
`Registry` is a **scope constant**: it is only visible at the top level of an engine-called hook, not
|
||||
inside helper functions you define (those run at a deeper call depth — see [Pitfalls](#pitfalls-and-sharp-edges)).
|
||||
The direction/color constants come from a global module and *are* visible at any call depth (including
|
||||
Rhai→Rhai calls).
|
||||
|
||||
The 16 color names, in palette order: `Black`, `Blue`, `Green`, `Cyan`, `Red`, `Magenta`, `Brown`,
|
||||
`LightGray`, `DarkGray`, `BrightBlue`, `BrightGreen`, `BrightCyan`, `BrightRed`, `BrightMagenta`,
|
||||
`Yellow`, `White`. They are plain hex strings, so they are accepted anywhere a color string is (e.g.
|
||||
`set_fg(BrightRed)`).
|
||||
|
||||
> There is **no** `MY_ID` constant any more — use `Me.id`.
|
||||
|
||||
---
|
||||
|
||||
## Self-reference: `Me`
|
||||
## Self and other objects: `ObjectInfo`
|
||||
|
||||
`me` is an `ObjectInfo` — a snapshot of one board object. The same type comes back from
|
||||
[`state.board.get` / `.named` / `.tagged`](#board-reads), so an object you look up has exactly the
|
||||
same members as `me`.
|
||||
|
||||
| Member | Kind | Returns | Description |
|
||||
|--------|------|---------|-------------|
|
||||
| `Me.id` | getter | `i64` | This object's stable `ObjectId`. |
|
||||
| `Me.name` | getter | `String` | Name, or `""` if unnamed. |
|
||||
| `Me.x` / `Me.y` | getter | `i64` | Current cell position. |
|
||||
| `Me.has_tag(tag)` | method | `bool` | Whether this object carries `tag`. |
|
||||
| `Me.tags()` | method | `Array` | All tag strings on this object. |
|
||||
| `Me.glyph()` | method | `Glyph` | This object's glyph (`.tile`, `.fg`, `.bg`). |
|
||||
| `.id` | getter | `i64` | The object's stable `ObjectId`. |
|
||||
| `.name` | getter | `String` or `()` | Name, or unit if unnamed. |
|
||||
| `.x` / `.y` | getter | `i64` | Current cell position. |
|
||||
| `.tags` | getter | `Array` | All tag strings on this object. |
|
||||
| `.glyph` | getter | `Glyph` | The object's glyph (`.tile`, `.fg`, `.bg`). |
|
||||
| `.waiting` | getter | `bool` | Whether the front of the object's queue is a delay (i.e. it is "busy"). |
|
||||
| `.queue` | getter | `Queue` | The object's [pending-action queue](#queue-object). |
|
||||
| `.has_tag(tag)` | method | `bool` | Whether the object carries `tag`. |
|
||||
| `.delay(secs)` | method | — | Append a pause to the object's queue. |
|
||||
| `.blocked(dir)` | method | `bool` | Whether moving the object one step in `dir` is impossible (off-board or a solid). |
|
||||
| `.can_push(dir)` | method | `bool` | Whether the pushable chain ahead of the object in `dir` can be shoved. |
|
||||
|
||||
```rhai
|
||||
if Me.has_tag("locked") { say(`${Me.name} is locked`); }
|
||||
set_tag(Me.id, "seen", true); // self-mutation still needs the explicit id
|
||||
fn tick(me, state, dt) {
|
||||
if me.has_tag("asleep") { return; }
|
||||
let here = me.glyph;
|
||||
log(`my tile is ${here.tile}`);
|
||||
if !me.waiting && !me.blocked(East) { move(East); }
|
||||
}
|
||||
```
|
||||
|
||||
A `Glyph` value (from `Me.glyph()` or `ObjectInfo` lookups) exposes `.tile` (`i64`), `.fg`, `.bg`
|
||||
(both `"#RRGGBB"` strings).
|
||||
A `Glyph` value (from `me.glyph` or a looked-up object) exposes `.tile` (`i64`), `.fg`, `.bg` (both
|
||||
`"#RRGGBB"` strings).
|
||||
|
||||
> Because hooks receive `me`/`state` as **parameters**, they are local to the hook. To use them in a
|
||||
> helper function you define, pass them down explicitly: `fn dance(me) { … }` called as `dance(me)`.
|
||||
|
||||
---
|
||||
|
||||
## Player stats: `Player`
|
||||
## The world: `state`
|
||||
|
||||
`state` is a handle bundling the two read surfaces:
|
||||
|
||||
| Member | Type | Description |
|
||||
|--------|------|-------------|
|
||||
| `state.player` | `Player` | The player's game-global stats and position. |
|
||||
| `state.board` | `Board` | Read-only handle to the current board. |
|
||||
|
||||
### Player stats: `state.player`
|
||||
|
||||
A read-only snapshot of the game-global player state, refreshed before each hook call.
|
||||
|
||||
| Property | Type | Description |
|
||||
|----------|------|-------------|
|
||||
| `Player.gems` | `i64` | Gems collected. |
|
||||
| `Player.health` | `i64` | Current health. |
|
||||
| `Player.max_health` | `i64` | Health ceiling. |
|
||||
| `state.player.gems` | `i64` | Gems collected. |
|
||||
| `state.player.health` | `i64` | Current health. |
|
||||
| `state.player.max_health` | `i64` | Health ceiling. |
|
||||
| `state.player.x` / `.y` | `i64` | Player's current cell. |
|
||||
| `state.player.keys` | `Keyring` | The player's key inventory (see below). |
|
||||
|
||||
```rhai
|
||||
if Player.health < 2 { say("You look hurt."); }
|
||||
if state.player.health < 2 { say("You look hurt."); }
|
||||
if state.player.keys.red { say("You have the red key."); }
|
||||
```
|
||||
|
||||
> The player's **keys** are not readable from scripts yet (you can `set_key` but not query it).
|
||||
The `Keyring` from `state.player.keys` exposes one `bool` getter per color: `.red`, `.orange`,
|
||||
`.yellow`, `.green`, `.blue`, `.cyan`, `.purple`, `.white`.
|
||||
|
||||
---
|
||||
### Board reads
|
||||
|
||||
## Board reads
|
||||
|
||||
### Properties
|
||||
#### Properties
|
||||
|
||||
| Property | Type | Description |
|
||||
|----------|------|-------------|
|
||||
| `Board.player_x` | `i64` | Player's column. |
|
||||
| `Board.player_y` | `i64` | Player's row. |
|
||||
| `Board.width` | `i64` | Board width in cells. |
|
||||
| `Board.height` | `i64` | Board height in cells. |
|
||||
| `state.board.width` | `i64` | Board width in cells. |
|
||||
| `state.board.height` | `i64` | Board height in cells. |
|
||||
|
||||
### Object lookups
|
||||
#### Object lookups
|
||||
|
||||
| Call | Returns | Description |
|
||||
|------|---------|-------------|
|
||||
| `Board.tagged(tag)` | `Array` of `ObjectInfo` | Every object carrying `tag`. |
|
||||
| `Board.named(name)` | `ObjectInfo` or `()` | The object with that name, else unit. |
|
||||
| `Board.get(id)` | `ObjectInfo` or `()` | Look up by id; `-1` returns the player; an invalid/unknown id logs an error and returns unit. |
|
||||
|
||||
An `ObjectInfo` exposes `.id`, `.x`, `.y`, `.name` (`""` if unnamed) and `.tags` (an `Array`). Note
|
||||
it has **no** `has_tag()` or `glyph()` — those exist only on `Me`.
|
||||
| `state.board.tagged(tag)` | `Array` of `ObjectInfo` | Every object carrying `tag`. |
|
||||
| `state.board.named(name)` | `ObjectInfo` or `()` | The object with that name, else unit. |
|
||||
| `state.board.get(id)` | `ObjectInfo` or `()` | Look up by id; an invalid (`id <= 0`) or unknown id logs an error and returns unit. |
|
||||
|
||||
```rhai
|
||||
let enemies = Board.tagged("enemy");
|
||||
let enemies = state.board.tagged("enemy");
|
||||
log(`there are ${enemies.len()} enemies`);
|
||||
|
||||
let door = Board.named("door");
|
||||
let door = state.board.named("door");
|
||||
if door != () { send(door.id, "open"); }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Cell queries (free functions)
|
||||
#### Cell queries
|
||||
|
||||
| Call | Returns | Description |
|
||||
|------|---------|-------------|
|
||||
| `blocked(dir)` | `bool` | Would the **caller** be unable to move one step in `dir` (off-board, a solid, or another object's already-queued move targets that cell). The caller's own queued moves don't count. |
|
||||
| `can_push(x, y, dir)` | `bool` | Does a pushable chain starting at `(x, y)` end in open space when shoved in `dir`? `false` off-board. |
|
||||
| `can_shift(x, y, dir)` | `bool` | Like `can_push` but checks only the **one** cell ahead (pushable source + empty-or-pushable neighbor). The right gate for `shift()`. |
|
||||
| `passable(x, y)` | `bool` | Is `(x, y)` on-board and free of any solid (an enterable hole)? Distinguishes a hole from a blocked solid. |
|
||||
| `combinable(x1, y1, x2, y2)` | `bool` | Can the solids at the two cells share a cell (one empty, or a grab thing + the player)? |
|
||||
| `state.board.can_push(x, y, dir)` | `bool` | Does a pushable chain starting at `(x, y)` end in open space when shoved in `dir`? `false` off-board. |
|
||||
| `state.board.passable(x, y)` | `bool` | Is `(x, y)` on-board and free of any solid (an enterable hole)? Distinguishes a hole from a blocked solid. |
|
||||
|
||||
```rhai
|
||||
fn tick(dt) {
|
||||
if !blocked(East) { move(East); }
|
||||
}
|
||||
```
|
||||
For queries about the calling object itself, use `me.blocked(dir)` and `me.can_push(dir)`
|
||||
([above](#self-and-other-objects-objectinfo)).
|
||||
|
||||
---
|
||||
|
||||
@@ -215,17 +241,23 @@ Board-scoped, persists across board transitions. Stores primitive values.
|
||||
| `Registry.get_or(key, default)` | Stored value **only if** it has the same type as `default`; otherwise `default`. |
|
||||
|
||||
```rhai
|
||||
let count = Registry.get_or("visits", 0);
|
||||
Registry.set("visits", count + 1);
|
||||
fn tick(me, state, dt) {
|
||||
// Per-object counter that survives across ticks and board transitions.
|
||||
let key = `count_${me.id}`;
|
||||
let n = Registry.get_or(key, 0);
|
||||
Registry.set(key, n + 1);
|
||||
}
|
||||
```
|
||||
|
||||
The Registry is shared by the whole board, so namespace per-instance keys by `me.id` as above.
|
||||
|
||||
---
|
||||
|
||||
## Write functions
|
||||
|
||||
Scripts never mutate the world directly. Each write appends an `Action` to this object's **output
|
||||
queue**; actions drain to a shared board queue between script calls and are resolved by the engine
|
||||
after the batch. The *subject* of a write is implicit and varies by function (see the table).
|
||||
Scripts never mutate the world directly. Each write appends an `Action` to the calling object's
|
||||
**output queue**; actions drain to a shared board queue between script calls and are resolved by the
|
||||
engine after the batch. The *subject* of a write is implicit and varies by function (see the table).
|
||||
|
||||
| Call | Subject | Cost | Description |
|
||||
|------|---------|------|-------------|
|
||||
@@ -237,7 +269,7 @@ after the batch. The *subject* of a write is implicit and varies by function (se
|
||||
| `set_fg(color)` | self | 0 | Set foreground color (hex string). |
|
||||
| `set_bg(color)` | self | 0 | Set background color. |
|
||||
| `set_color(fg, bg)` | self | 0 | Set both colors. |
|
||||
| `set_tag(target_id, tag, present)` | any object | 0 | Add (`true`) / remove (`false`) a tag. Use `Me.id` for self. |
|
||||
| `set_tag(target_id, tag, present)` | any object | 0 | Add (`true`) / remove (`false`) a tag. Use `me.id` for self. |
|
||||
| `say(msg)` / `say(msg, secs)` | self | 0 | Speech bubble above this object; default 3 s, or `secs`. Replaces any current bubble. |
|
||||
| `log(msg)` | log | 0 | Append a plain-text line to the game log. |
|
||||
| `scroll(lines)` | UI | 0 | Open a full-screen overlay (see below). |
|
||||
@@ -249,6 +281,9 @@ after the batch. The *subject* of a write is implicit and varies by function (se
|
||||
| `set_key(color, present)` | player | 0 | Give/take a key color (`"blue"`, `"green"`, `"cyan"`, `"red"`, `"purple"`, `"orange"`, `"yellow"`, `"white"`). Unknown name logs and is ignored. |
|
||||
| `die()` | self | 0 | Remove the calling object from the board. |
|
||||
|
||||
`delay`/`now` also exist as methods on `me` and on a queue handle: `me.delay(secs)` and
|
||||
`me.queue.delay(secs)` are equivalent to the free `delay(secs)` for the calling object.
|
||||
|
||||
### `scroll(lines)`
|
||||
|
||||
Opens a full-screen scrollable overlay and pauses game ticks until the player closes it. Each element
|
||||
@@ -258,10 +293,11 @@ of `lines` is either:
|
||||
- a **2-element array** `[choice_key, display_text]` — a selectable choice.
|
||||
|
||||
When the player selects a choice, `choice_key` is dispatched back to the **source object** as a
|
||||
zero-arg function call named `choice_key`.
|
||||
function call named `choice_key` (see [custom handler functions](#custom-handler-functions) for the
|
||||
arity rules — a choice handler is called with no `arg`).
|
||||
|
||||
```rhai
|
||||
fn bump(id) {
|
||||
fn bump(me, state, id) {
|
||||
scroll([
|
||||
"The muffin looks delicious.",
|
||||
"",
|
||||
@@ -269,32 +305,28 @@ fn bump(id) {
|
||||
["ignore", "Walk away"],
|
||||
]);
|
||||
}
|
||||
fn eat() { say("Yeah it was poisoned."); alter_health(-2); }
|
||||
fn ignore() { log("Wise."); }
|
||||
fn eat(me, state) { say("Yeah it was poisoned."); alter_health(-2); }
|
||||
fn ignore(me, state) { log("Wise."); }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## `Queue` object
|
||||
|
||||
Inspection and control of this object's own pending-action queue.
|
||||
Inspection and control of an object's own pending-action queue, reached via `me.queue`. Most scripts
|
||||
do not need it directly — `me.waiting` covers the common "am I busy?" check.
|
||||
|
||||
| Method | Returns | Description |
|
||||
|--------|---------|-------------|
|
||||
| `Queue.length()` | `i64` | Number of pending actions. |
|
||||
| `Queue.clear()` | — | Discard all pending actions (including delays). |
|
||||
| `Queue.peek()` | map or `()` | Front action as a map, or unit if empty. |
|
||||
| `Queue.pop()` | map or `()` | Same, removing the front. |
|
||||
|
||||
`peek`/`pop` return an untyped map with a `"type"` key (`"Move"`, `"SetTile"`, `"Say"`, …) plus
|
||||
variant-specific payload keys (e.g. `"dir"`, `"tile"`, `"msg"`). `Scroll` and `Shift` report `type`
|
||||
only — their payloads are not inspectable through this API.
|
||||
| Member | Kind | Returns | Description |
|
||||
|--------|------|---------|-------------|
|
||||
| `me.queue.length` | getter | `i64` | Number of pending actions. |
|
||||
| `me.queue.clear()` | method | — | Discard all pending actions (including delays). |
|
||||
| `me.queue.delay(secs)` | method | — | Append a pause to the queue. |
|
||||
|
||||
```rhai
|
||||
fn tick(dt) {
|
||||
if Queue.length() == 0 {
|
||||
if !blocked(East) { move(East); }
|
||||
else if !blocked(West) { move(West); }
|
||||
fn tick(me, state, dt) {
|
||||
if !me.waiting {
|
||||
if !me.blocked(East) { move(East); }
|
||||
else if !me.blocked(West) { move(West); }
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -306,11 +338,13 @@ fn tick(dt) {
|
||||
- `move(dir)` costs **0.25 s** of queue delay (~4 moves/second max).
|
||||
- `delay(secs)` adds an arbitrary pause; `now()` can bypass a delay for a zero-cost action already in
|
||||
the queue.
|
||||
- `Queue.clear()` cancels everything, including pending delays.
|
||||
- `me.queue.clear()` cancels everything, including pending delays.
|
||||
- Adjacent delays are always merged: two `delay(0.5)` calls become one `1.0 s` delay.
|
||||
- All other writes (`set_tile`, `say`, `push`, `shift`, `teleport`, stat changes, `die`, …) are
|
||||
zero-cost: a run of them resolves together, and at most one *timed* action (a `move`) is released
|
||||
per object per cycle.
|
||||
- `me.waiting` is `true` while a delay sits at the front of the queue — the idiom for "don't issue a
|
||||
new action until the last one finished" is `if !me.waiting { … }`.
|
||||
|
||||
---
|
||||
|
||||
@@ -326,18 +360,18 @@ crash the game. A script that throws during `tick` logs the error and resumes on
|
||||
```rhai
|
||||
// A guard that patrols east/west and greets the player when bumped.
|
||||
|
||||
fn init() {
|
||||
fn init(me, state) {
|
||||
set_tile(2);
|
||||
log(`${Me.name} standing watch`);
|
||||
log(`${me.name} standing watch`);
|
||||
}
|
||||
|
||||
fn tick(dt) {
|
||||
if Queue.length() > 0 { return; } // still moving
|
||||
if !blocked(East) { move(East); }
|
||||
else if !blocked(West) { move(West); }
|
||||
fn tick(me, state, dt) {
|
||||
if me.waiting { return; } // still moving
|
||||
if !me.blocked(East) { move(East); }
|
||||
else if !me.blocked(West) { move(West); }
|
||||
}
|
||||
|
||||
fn bump(id) {
|
||||
fn bump(me, state, id) {
|
||||
if id == -1 { say("Halt! Who goes there?"); now(); }
|
||||
else { say("Watch it!"); now(); }
|
||||
}
|
||||
@@ -352,15 +386,13 @@ they are shape problems.
|
||||
|
||||
### Duplication — several ways to do one thing
|
||||
|
||||
- **Object introspection has two parallel types with different members.** Self uses `Me`
|
||||
(`Me.has_tag()`, `Me.tags()`, `Me.glyph()`, plus getters); other objects come back as `ObjectInfo`
|
||||
(`.id`, `.x`, `.y`, `.name`, `.tags`) which has **no** `has_tag()` and **no** `glyph()`. The same
|
||||
conceptual "an object" is two incompatible types depending on whether it's you.
|
||||
- **Player position/state is reachable three ways:** `Board.player_x` / `Board.player_y`,
|
||||
`Board.get(-1).x` / `.y`, and the `Player` constant (stats only). Player *position* lives on
|
||||
`Board`, player *stats* live on `Player` — the player is split across two surfaces.
|
||||
- **Tag reads come in three shapes:** `Me.has_tag(t)` (method, bool), `Me.tags()` (method, array),
|
||||
`ObjectInfo.tags` (property, array). Method-vs-property is inconsistent even for the same data.
|
||||
- **Player position/state is reachable two ways.** Player *stats* and *position* both live on
|
||||
`state.player` now (`.gems`/`.health`/`.x`/`.y`/`.keys`), which is an improvement — but a looked-up
|
||||
player no longer exists (`state.board.get(-1)` is gone), so code that wants "the player as an
|
||||
object" has nothing to look up, while every other entity is an `ObjectInfo`.
|
||||
- **Reads are split across three handles** (`me`, `state.player`, `state.board`) with no single
|
||||
"query" entry point; which handle owns a given fact (e.g. cell queries on `state.board` vs. self
|
||||
queries on `me`) has to be memorized.
|
||||
|
||||
### Inconsistency — naming and conventions
|
||||
|
||||
@@ -372,11 +404,14 @@ they are shape problems.
|
||||
**self**; `add_gems`/`alter_health`/`set_key` act on the **player**; `set_tag`/`send` act on an
|
||||
**arbitrary target**; `push`/`shift`/`teleport` act on **cells**. The `set_*` prefix in particular
|
||||
means three different subjects.
|
||||
- **Mixed addressing for movement.** `move`/`blocked`/`push` take a typed `Direction`, while
|
||||
`teleport`/`passable`/`can_push`/`can_shift`/`shift` take raw `i64` coordinates. There's no cell or
|
||||
position value type.
|
||||
- **Self-mutation can't go through `Me`.** `Me` is read-only, so writing your own tag is
|
||||
`set_tag(Me.id, …)` — you pass your own id back to a free function instead of `Me.set_tag(…)`.
|
||||
- **Reads are methods/getters on handles, but writes are free functions.** `me.blocked(dir)` reads,
|
||||
but `move(dir)` (the matching write) is a bare global that acts on `me` implicitly — the symmetry is
|
||||
invisible.
|
||||
- **Mixed addressing for movement.** `move`/`push` take a typed `Direction`, while
|
||||
`teleport`/`passable`/`can_push`/`shift` take raw `i64` coordinates. There's no cell or position
|
||||
value type.
|
||||
- **Self-mutation can't go through `me`.** `me` is read-only, so writing your own tag is
|
||||
`set_tag(me.id, …)` — you pass your own id back to a free function instead of `me.set_tag(…)`.
|
||||
- **Colors are stringly-typed** (`"#RRGGBB"`, with the named constants being strings) while tiles are
|
||||
bare integers with no symbolic names; glyph reads hand back colors as hex strings.
|
||||
- **Overload coverage is uneven.** `say` has a duration overload, `log` does not; `delay` has int and
|
||||
@@ -384,22 +419,17 @@ they are shape problems.
|
||||
|
||||
### Unwieldy — machinery that leaks
|
||||
|
||||
- **Queue introspection is stringly-typed.** `peek`/`pop` return untyped maps keyed by a `"type"`
|
||||
string, and `Scroll`/`Shift` aren't introspectable at all (type only). There is no typed action
|
||||
value.
|
||||
- **The two-tier queue + `now()`/`delay()` model is subtle.** `now()` reorders by *enqueue
|
||||
recency* ("most recently enqueued action to the front"), which is position-dependent and easy to
|
||||
get wrong; the object-queue → board-queue promotion plus per-object ready timer is a lot of
|
||||
get wrong; the object-queue → board-queue promotion plus per-object delay draining is a lot of
|
||||
implicit state to reason about for "do X, wait, do Y".
|
||||
- **Two persistence mechanisms.** Tags and the `Registry` are separate stores with separate APIs;
|
||||
cross-board state must pick one.
|
||||
- **Stringly-typed dispatch.** `send` and scroll choices route by function *name*; a scroll choice
|
||||
key is silently also a function name. Renaming a handler breaks callers with no signal.
|
||||
- **Engine internals leak into the script API.** `combinable(x1,y1,x2,y2)` encodes a very specific
|
||||
rule (a grab thing may share a cell with the player) that exists only to support `shift`/`swap`
|
||||
edge cases.
|
||||
- **Read/write asymmetry on player keys.** `set_key` writes keys, but no read exists — `Player`
|
||||
exposes `gems`/`health`/`max_health` only (there is a literal `TODO` to expose keys).
|
||||
- **Doc/name drift across the codebase.** The function is `shift([[x, y], …])`, but the engine's own
|
||||
module docs and `CLAUDE.md` still describe a `swap([[src_x, src_y, dst_x, dst_y], …])`. The two
|
||||
concepts (rotate-a-cycle vs. batch directed moves) are conflated under both names.
|
||||
- **`Registry` alone is a scope constant.** Now that `me`/`state` are parameters, `Registry` is the
|
||||
one handle with the "invisible inside helper functions" gotcha — an inconsistency with everything
|
||||
else a hook can reach.
|
||||
- **Read/write asymmetry on player keys is now closed** (`state.player.keys.*` reads, `set_key`
|
||||
writes) — but reads are per-color getters while the write is a stringly-named color, so the two
|
||||
sides still don't mirror each other.
|
||||
|
||||
Reference in New Issue
Block a user