// ── Tetris: BT-driven game loop with command-based input ── // // Architecture: // Behaviour Tree (buildTree) — controls game flow: // parallel // ├── gravityTick (leaf) — generator loop, auto-drop piece on timer // └── repeat // └── seq (sequential) // ├── handleInput (leaf) — reads queued commands // └── render (leaf) — draws via blessed // // CommandQueue — processes input: // Keyboard → spawn command entities → CommandQueue.execute() // → handlers mutate game state // // Singleton components — global state accessed via world.*Singleton(): // Board, Piece, Score, GameOver, Paused, TickTimer // // Usage: // npx tsx examples/tetris/main.ts import { World } from "../../src/index"; import { buildTree } from "../../src/bt/index"; import { CommandQueue } from "../../src/commands/index"; import { Board, Piece, Score, GameOver, Paused, TickTimer, createPieceHelpers, } from "./components"; import { registerCommands, MoveLeft, MoveRight, Rotate, SoftDrop, HardDrop, TogglePause, Restart, } from "./commands"; import { collides } from "./game"; import { createUI, render } from "./render"; import { startInput, type Key } from "./input"; // ── Setup ──────────────────────────────────────────── const world = new World(); world.addSingleton(Board); world.addSingleton(Score); world.addSingleton(TickTimer); const ui = createUI(); const pieces = createPieceHelpers(world); const commands = new CommandQueue(world); registerCommands(world, commands, pieces); pieces.spawnPiece(); // ── Behaviour Tree ─────────────────────────────────── const runner = buildTree(world, { kind: "parallel", children: [ { kind: "leaf", *run() { while (true) { const dt: number = yield; if (world.hasSingleton(GameOver) || world.hasSingleton(Paused)) { continue; } const timer = world.getSingleton(TickTimer); timer.accumulator += dt; if (timer.accumulator >= timer.interval) { timer.accumulator -= timer.interval; if (world.hasSingleton(Piece)) { const piece = world.getSingleton(Piece); const board = world.getSingleton(Board); if (!collides(board.grid, piece.shape, piece.x, piece.y + 1)) { piece.y++; } else { pieces.lockAndSpawn(); } } } } }, }, { kind: "repeat", child: { kind: "sequential", children: [ { kind: "leaf", run: () => { commands.execute(); }, }, { kind: "leaf", run: () => { render(world, ui); }, }, ], }, }, ], }); // ── Input → Command mapping ────────────────────────── const keyToCommand: Partial> = { left: MoveLeft, right: MoveRight, up: Rotate, down: SoftDrop, space: HardDrop, p: TogglePause, r: Restart, }; startInput(ui.screen, (key) => { const cmd = keyToCommand[key]; if (cmd) { const cmdEntity = world.spawn(); world.add(cmdEntity, cmd); } }); // ── Game loop ──────────────────────────────────────── runner.schedule((runner as any).root); const TICK_MS = 16; const interval = setInterval(() => { runner.tick(TICK_MS); }, TICK_MS); process.on("SIGINT", () => { clearInterval(interval); ui.screen.destroy(); process.exit(0); });