feat(bt): add declarative tree builder
Introduce `buildTree` to allow defining behaviour trees using a declarative `TreeDef` object instead of manual entity spawning and relationship wiring. This simplifies tree construction and manages leaf handlers internally.
This commit is contained in:
+45
-61
@@ -1,7 +1,7 @@
|
||||
// ── Tetris: BT-driven game loop with command-based input ──
|
||||
//
|
||||
// Architecture:
|
||||
// Behaviour Tree (TaskRunner) — controls game flow:
|
||||
// Behaviour Tree (buildTree) — controls game flow:
|
||||
// root (repeat)
|
||||
// └── seq (sequential)
|
||||
// ├── handleInput (leaf) — reads queued commands, mutates state
|
||||
@@ -19,7 +19,7 @@
|
||||
// npx tsx examples/tetris/main.ts
|
||||
|
||||
import { World } from "../../src/index";
|
||||
import { TaskRunner, Task, ChildOf } from "../../src/bt/index";
|
||||
import { buildTree } from "../../src/bt/index";
|
||||
import { CommandQueue } from "../../src/commands/index";
|
||||
|
||||
import { Board, Piece, Score, GameOver, Paused, TickTimer } from "./components";
|
||||
@@ -208,64 +208,48 @@ function lockAndSpawn(): void {
|
||||
}
|
||||
|
||||
// ── Behaviour Tree ───────────────────────────────────
|
||||
const runner = new TaskRunner(world);
|
||||
|
||||
// Build the BT structure:
|
||||
// root (repeat)
|
||||
// └── seq (sequential)
|
||||
// ├── handleInput (leaf)
|
||||
// ├── gravityTick (leaf)
|
||||
// └── render (leaf)
|
||||
|
||||
const root = world.spawn();
|
||||
world.add(root, Task, { kind: "repeat" });
|
||||
|
||||
const seq = world.spawn();
|
||||
world.add(seq, Task, { kind: "sequential" });
|
||||
world.relate(seq, ChildOf, root);
|
||||
|
||||
const handleInputTask = world.spawn();
|
||||
world.add(handleInputTask, Task, { kind: "leaf" });
|
||||
world.relate(handleInputTask, ChildOf, seq);
|
||||
|
||||
const gravityTask = world.spawn();
|
||||
world.add(gravityTask, Task, { kind: "leaf" });
|
||||
world.relate(gravityTask, ChildOf, seq);
|
||||
|
||||
const renderTask = world.spawn();
|
||||
world.add(renderTask, Task, { kind: "leaf" });
|
||||
world.relate(renderTask, ChildOf, seq);
|
||||
|
||||
// ── Leaf handlers ────────────────────────────────────
|
||||
runner.onLeaf = (_w, entity) => {
|
||||
if (entity === handleInputTask) {
|
||||
commands.execute();
|
||||
runner.succeed(entity);
|
||||
} else if (entity === gravityTask) {
|
||||
if (world.hasSingleton(GameOver) || world.hasSingleton(Paused)) {
|
||||
runner.succeed(entity);
|
||||
return;
|
||||
}
|
||||
const timer = world.getSingleton(TickTimer);
|
||||
timer.accumulator += 16;
|
||||
if (timer.accumulator >= timer.interval) {
|
||||
timer.accumulator -= timer.interval;
|
||||
if (hasActivePiece()) {
|
||||
const piece = world.getSingleton(Piece);
|
||||
const board = world.getSingleton(Board);
|
||||
if (!collides(board.grid, piece.shape, piece.x, piece.y + 1)) {
|
||||
piece.y++;
|
||||
} else {
|
||||
lockAndSpawn();
|
||||
}
|
||||
}
|
||||
}
|
||||
runner.succeed(entity);
|
||||
} else if (entity === renderTask) {
|
||||
render(world, ui);
|
||||
runner.succeed(entity);
|
||||
}
|
||||
};
|
||||
const runner = buildTree(world, {
|
||||
kind: "repeat",
|
||||
child: {
|
||||
kind: "sequential",
|
||||
children: [
|
||||
{
|
||||
kind: "leaf",
|
||||
run: () => {
|
||||
commands.execute();
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: "leaf",
|
||||
run: () => {
|
||||
if (world.hasSingleton(GameOver) || world.hasSingleton(Paused)) {
|
||||
return;
|
||||
}
|
||||
const timer = world.getSingleton(TickTimer);
|
||||
timer.accumulator += 16;
|
||||
if (timer.accumulator >= timer.interval) {
|
||||
timer.accumulator -= timer.interval;
|
||||
if (hasActivePiece()) {
|
||||
const piece = world.getSingleton(Piece);
|
||||
const board = world.getSingleton(Board);
|
||||
if (!collides(board.grid, piece.shape, piece.x, piece.y + 1)) {
|
||||
piece.y++;
|
||||
} else {
|
||||
lockAndSpawn();
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: "leaf",
|
||||
run: () => {
|
||||
render(world, ui);
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
// ── Input → Command mapping ──────────────────────────
|
||||
const keyToCommand: Partial<Record<Key, typeof MoveLeft>> = {
|
||||
@@ -287,7 +271,7 @@ startInput(ui.screen, (key) => {
|
||||
});
|
||||
|
||||
// ── Game loop ────────────────────────────────────────
|
||||
runner.schedule(root);
|
||||
runner.schedule((runner as any).root);
|
||||
|
||||
const TICK_MS = 16;
|
||||
const interval = setInterval(() => {
|
||||
|
||||
Reference in New Issue
Block a user