refactor(examples): reorganized code
Move code to avoid big main.ts and changed the bt to match the pattern
This commit is contained in:
@@ -1,4 +1,24 @@
|
||||
import { defineComponent } from "../../src/component";
|
||||
import type { World } from "../../src/index";
|
||||
import type { CommandQueue } from "../../src/commands/index";
|
||||
import {
|
||||
Board,
|
||||
Piece,
|
||||
Score,
|
||||
GameOver,
|
||||
Paused,
|
||||
TickTimer,
|
||||
createPieceHelpers,
|
||||
} from "./components";
|
||||
import {
|
||||
collides,
|
||||
lockPiece,
|
||||
clearLines,
|
||||
scoreForLines,
|
||||
tryRotate,
|
||||
} from "./game";
|
||||
|
||||
// ── Command definitions ──────────────────────────────
|
||||
|
||||
/** Move the active piece left. */
|
||||
export const MoveLeft = defineComponent("moveLeft", {});
|
||||
@@ -20,3 +40,85 @@ export const TogglePause = defineComponent("togglePause", {});
|
||||
|
||||
/** Restart after game over. */
|
||||
export const Restart = defineComponent("restart", {});
|
||||
|
||||
// ── Command handlers ─────────────────────────────────
|
||||
|
||||
export function registerCommands(
|
||||
world: World,
|
||||
commands: CommandQueue,
|
||||
pieces: ReturnType<typeof createPieceHelpers>,
|
||||
): void {
|
||||
const hasActivePiece = () => world.hasSingleton(Piece);
|
||||
const isBlocked = () =>
|
||||
world.hasSingleton(GameOver) || world.hasSingleton(Paused);
|
||||
|
||||
commands.handle(MoveLeft, () => {
|
||||
if (!hasActivePiece() || isBlocked()) return;
|
||||
const piece = world.getSingleton(Piece);
|
||||
const board = world.getSingleton(Board);
|
||||
if (!collides(board.grid, piece.shape, piece.x - 1, piece.y)) {
|
||||
piece.x--;
|
||||
}
|
||||
});
|
||||
|
||||
commands.handle(MoveRight, () => {
|
||||
if (!hasActivePiece() || isBlocked()) return;
|
||||
const piece = world.getSingleton(Piece);
|
||||
const board = world.getSingleton(Board);
|
||||
if (!collides(board.grid, piece.shape, piece.x + 1, piece.y)) {
|
||||
piece.x++;
|
||||
}
|
||||
});
|
||||
|
||||
commands.handle(Rotate, () => {
|
||||
if (!hasActivePiece() || isBlocked()) return;
|
||||
const piece = world.getSingleton(Piece);
|
||||
const board = world.getSingleton(Board);
|
||||
const result = tryRotate(board.grid, piece.shape, piece.x, piece.y);
|
||||
if (result) {
|
||||
piece.shape = result.shape;
|
||||
piece.x = result.x;
|
||||
}
|
||||
});
|
||||
|
||||
commands.handle(SoftDrop, () => {
|
||||
if (!hasActivePiece() || isBlocked()) return;
|
||||
const piece = world.getSingleton(Piece);
|
||||
const board = world.getSingleton(Board);
|
||||
if (!collides(board.grid, piece.shape, piece.x, piece.y + 1)) {
|
||||
piece.y++;
|
||||
}
|
||||
});
|
||||
|
||||
commands.handle(HardDrop, () => {
|
||||
if (!hasActivePiece() || isBlocked()) return;
|
||||
const piece = world.getSingleton(Piece);
|
||||
const board = world.getSingleton(Board);
|
||||
while (!collides(board.grid, piece.shape, piece.x, piece.y + 1)) {
|
||||
piece.y++;
|
||||
}
|
||||
pieces.lockAndSpawn();
|
||||
});
|
||||
|
||||
commands.handle(TogglePause, () => {
|
||||
if (world.hasSingleton(GameOver)) return;
|
||||
if (world.hasSingleton(Paused)) {
|
||||
world.removeSingleton(Paused);
|
||||
} else {
|
||||
world.addSingleton(Paused);
|
||||
}
|
||||
});
|
||||
|
||||
commands.handle(Restart, () => {
|
||||
if (!world.hasSingleton(GameOver)) return;
|
||||
const board = world.getSingleton(Board);
|
||||
for (let r = 0; r < board.grid.length; r++) {
|
||||
board.grid[r].fill(0);
|
||||
}
|
||||
world.setSingleton(Score, { points: 0, lines: 0, level: 1 });
|
||||
world.setSingleton(TickTimer, { accumulator: 0, interval: 800 });
|
||||
world.removeSingleton(GameOver);
|
||||
if (world.hasSingleton(Piece)) world.removeSingleton(Piece);
|
||||
pieces.spawnPiece();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,6 +1,16 @@
|
||||
import { defineComponent } from "../../src/component";
|
||||
import type { World } from "../../src/index";
|
||||
import {
|
||||
randomPiece,
|
||||
collides,
|
||||
lockPiece,
|
||||
clearLines,
|
||||
scoreForLines,
|
||||
BOARD_W,
|
||||
} from "./game";
|
||||
|
||||
// ── Component definitions ────────────────────────────
|
||||
|
||||
// ── Board ────────────────────────────────────────────
|
||||
/** The playfield grid (20 rows × 10 cols). 0 = empty, non-zero = color index. */
|
||||
export const Board = defineComponent("board", {
|
||||
grid: Array.from({ length: 20 }, () => new Uint8Array(10)) as Uint8Array[],
|
||||
@@ -29,3 +39,45 @@ export const TickTimer = defineComponent("tickTimer", {
|
||||
accumulator: 0,
|
||||
interval: 800, // ms between gravity ticks
|
||||
});
|
||||
|
||||
// ── Piece helpers ────────────────────────────────────
|
||||
|
||||
export function createPieceHelpers(world: World) {
|
||||
return {
|
||||
spawnPiece(): void {
|
||||
const p = randomPiece();
|
||||
world.addSingleton(Piece, {
|
||||
shape: p.shape,
|
||||
color: p.color,
|
||||
x: Math.floor((BOARD_W - p.shape[0].length) / 2),
|
||||
y: 0,
|
||||
});
|
||||
},
|
||||
|
||||
lockAndSpawn(): void {
|
||||
const piece = world.getSingleton(Piece);
|
||||
const board = world.getSingleton(Board);
|
||||
|
||||
lockPiece(board.grid, piece.shape, piece.color, piece.x, piece.y);
|
||||
world.removeSingleton(Piece);
|
||||
|
||||
const cleared = clearLines(board.grid);
|
||||
if (cleared > 0) {
|
||||
const score = world.getSingleton(Score);
|
||||
score.lines += cleared;
|
||||
score.points += scoreForLines(cleared, score.level);
|
||||
score.level = Math.floor(score.lines / 10) + 1;
|
||||
const timer = world.getSingleton(TickTimer);
|
||||
timer.interval = Math.max(100, 800 - (score.level - 1) * 70);
|
||||
}
|
||||
|
||||
this.spawnPiece();
|
||||
|
||||
const newPiece = world.getSingleton(Piece);
|
||||
if (collides(board.grid, newPiece.shape, newPiece.x, newPiece.y)) {
|
||||
world.removeSingleton(Piece);
|
||||
world.addSingleton(GameOver);
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
+63
-203
@@ -2,11 +2,12 @@
|
||||
//
|
||||
// Architecture:
|
||||
// Behaviour Tree (buildTree) — controls game flow:
|
||||
// root (repeat)
|
||||
// └── seq (sequential)
|
||||
// ├── handleInput (leaf) — reads queued commands, mutates state
|
||||
// ├── gravityTick (leaf) — auto-drop piece on timer
|
||||
// └── render (leaf) — draws via blessed
|
||||
// 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()
|
||||
@@ -22,9 +23,18 @@ import { World } from "../../src/index";
|
||||
import { buildTree } from "../../src/bt/index";
|
||||
import { CommandQueue } from "../../src/commands/index";
|
||||
|
||||
import { Board, Piece, Score, GameOver, Paused, TickTimer } from "./components";
|
||||
import {
|
||||
Board,
|
||||
Piece,
|
||||
Score,
|
||||
GameOver,
|
||||
Paused,
|
||||
TickTimer,
|
||||
createPieceHelpers,
|
||||
} from "./components";
|
||||
|
||||
import {
|
||||
registerCommands,
|
||||
MoveLeft,
|
||||
MoveRight,
|
||||
Rotate,
|
||||
@@ -34,15 +44,7 @@ import {
|
||||
Restart,
|
||||
} from "./commands";
|
||||
|
||||
import {
|
||||
randomPiece,
|
||||
collides,
|
||||
lockPiece,
|
||||
clearLines,
|
||||
scoreForLines,
|
||||
tryRotate,
|
||||
BOARD_W,
|
||||
} from "./game";
|
||||
import { collides } from "./game";
|
||||
|
||||
import { createUI, render } from "./render";
|
||||
import { startInput, type Key } from "./input";
|
||||
@@ -50,208 +52,67 @@ import { startInput, type Key } from "./input";
|
||||
// ── Setup ────────────────────────────────────────────
|
||||
const world = new World();
|
||||
|
||||
// Singleton game state — one entity holds all of these
|
||||
world.addSingleton(Board);
|
||||
world.addSingleton(Score);
|
||||
world.addSingleton(TickTimer);
|
||||
|
||||
// Create blessed UI
|
||||
const ui = createUI();
|
||||
|
||||
// Spawn the first piece
|
||||
spawnPiece();
|
||||
|
||||
// ── Command handlers ─────────────────────────────────
|
||||
const pieces = createPieceHelpers(world);
|
||||
const commands = new CommandQueue(world);
|
||||
|
||||
function spawnPiece(): void {
|
||||
const p = randomPiece();
|
||||
world.addSingleton(Piece, {
|
||||
shape: p.shape,
|
||||
color: p.color,
|
||||
x: Math.floor((BOARD_W - p.shape[0].length) / 2),
|
||||
y: 0,
|
||||
});
|
||||
}
|
||||
|
||||
function hasActivePiece(): boolean {
|
||||
return world.hasSingleton(Piece);
|
||||
}
|
||||
|
||||
// Move left
|
||||
commands.handle(MoveLeft, () => {
|
||||
if (
|
||||
!hasActivePiece() ||
|
||||
world.hasSingleton(GameOver) ||
|
||||
world.hasSingleton(Paused)
|
||||
)
|
||||
return;
|
||||
const piece = world.getSingleton(Piece);
|
||||
const board = world.getSingleton(Board);
|
||||
if (!collides(board.grid, piece.shape, piece.x - 1, piece.y)) {
|
||||
piece.x--;
|
||||
}
|
||||
});
|
||||
|
||||
// Move right
|
||||
commands.handle(MoveRight, () => {
|
||||
if (
|
||||
!hasActivePiece() ||
|
||||
world.hasSingleton(GameOver) ||
|
||||
world.hasSingleton(Paused)
|
||||
)
|
||||
return;
|
||||
const piece = world.getSingleton(Piece);
|
||||
const board = world.getSingleton(Board);
|
||||
if (!collides(board.grid, piece.shape, piece.x + 1, piece.y)) {
|
||||
piece.x++;
|
||||
}
|
||||
});
|
||||
|
||||
// Rotate
|
||||
commands.handle(Rotate, () => {
|
||||
if (
|
||||
!hasActivePiece() ||
|
||||
world.hasSingleton(GameOver) ||
|
||||
world.hasSingleton(Paused)
|
||||
)
|
||||
return;
|
||||
const piece = world.getSingleton(Piece);
|
||||
const board = world.getSingleton(Board);
|
||||
const result = tryRotate(board.grid, piece.shape, piece.x, piece.y);
|
||||
if (result) {
|
||||
piece.shape = result.shape;
|
||||
piece.x = result.x;
|
||||
}
|
||||
});
|
||||
|
||||
// Soft drop
|
||||
commands.handle(SoftDrop, () => {
|
||||
if (
|
||||
!hasActivePiece() ||
|
||||
world.hasSingleton(GameOver) ||
|
||||
world.hasSingleton(Paused)
|
||||
)
|
||||
return;
|
||||
const piece = world.getSingleton(Piece);
|
||||
const board = world.getSingleton(Board);
|
||||
if (!collides(board.grid, piece.shape, piece.x, piece.y + 1)) {
|
||||
piece.y++;
|
||||
}
|
||||
});
|
||||
|
||||
// Hard drop
|
||||
commands.handle(HardDrop, () => {
|
||||
if (
|
||||
!hasActivePiece() ||
|
||||
world.hasSingleton(GameOver) ||
|
||||
world.hasSingleton(Paused)
|
||||
)
|
||||
return;
|
||||
const piece = world.getSingleton(Piece);
|
||||
const board = world.getSingleton(Board);
|
||||
while (!collides(board.grid, piece.shape, piece.x, piece.y + 1)) {
|
||||
piece.y++;
|
||||
}
|
||||
lockAndSpawn();
|
||||
});
|
||||
|
||||
// Toggle pause
|
||||
commands.handle(TogglePause, () => {
|
||||
if (world.hasSingleton(GameOver)) return;
|
||||
if (world.hasSingleton(Paused)) {
|
||||
world.removeSingleton(Paused);
|
||||
} else {
|
||||
world.addSingleton(Paused);
|
||||
}
|
||||
});
|
||||
|
||||
// Restart
|
||||
commands.handle(Restart, () => {
|
||||
if (!world.hasSingleton(GameOver)) return;
|
||||
const board = world.getSingleton(Board);
|
||||
for (let r = 0; r < board.grid.length; r++) {
|
||||
board.grid[r].fill(0);
|
||||
}
|
||||
world.setSingleton(Score, { points: 0, lines: 0, level: 1 });
|
||||
world.setSingleton(TickTimer, { accumulator: 0, interval: 800 });
|
||||
world.removeSingleton(GameOver);
|
||||
if (world.hasSingleton(Piece)) world.removeSingleton(Piece);
|
||||
spawnPiece();
|
||||
});
|
||||
|
||||
// ── Lock piece & spawn next ──────────────────────────
|
||||
function lockAndSpawn(): void {
|
||||
const piece = world.getSingleton(Piece);
|
||||
const board = world.getSingleton(Board);
|
||||
|
||||
lockPiece(board.grid, piece.shape, piece.color, piece.x, piece.y);
|
||||
world.removeSingleton(Piece);
|
||||
|
||||
const cleared = clearLines(board.grid);
|
||||
if (cleared > 0) {
|
||||
const score = world.getSingleton(Score);
|
||||
score.lines += cleared;
|
||||
score.points += scoreForLines(cleared, score.level);
|
||||
score.level = Math.floor(score.lines / 10) + 1;
|
||||
const timer = world.getSingleton(TickTimer);
|
||||
timer.interval = Math.max(100, 800 - (score.level - 1) * 70);
|
||||
}
|
||||
|
||||
spawnPiece();
|
||||
|
||||
const newPiece = world.getSingleton(Piece);
|
||||
if (collides(board.grid, newPiece.shape, newPiece.x, newPiece.y)) {
|
||||
world.removeSingleton(Piece);
|
||||
world.addSingleton(GameOver);
|
||||
}
|
||||
}
|
||||
registerCommands(world, commands, pieces);
|
||||
pieces.spawnPiece();
|
||||
|
||||
// ── Behaviour Tree ───────────────────────────────────
|
||||
const runner = buildTree(world, {
|
||||
kind: "repeat",
|
||||
child: {
|
||||
kind: "sequential",
|
||||
children: [
|
||||
{
|
||||
kind: "leaf",
|
||||
run: () => {
|
||||
commands.execute();
|
||||
},
|
||||
},
|
||||
{
|
||||
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 (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: "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: "leaf",
|
||||
run: () => {
|
||||
render(world, ui);
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: "repeat",
|
||||
child: {
|
||||
kind: "sequential",
|
||||
children: [
|
||||
{
|
||||
kind: "leaf",
|
||||
run: () => {
|
||||
commands.execute();
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: "leaf",
|
||||
run: () => {
|
||||
render(world, ui);
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// ── Input → Command mapping ──────────────────────────
|
||||
@@ -281,7 +142,6 @@ const interval = setInterval(() => {
|
||||
runner.tick(TICK_MS);
|
||||
}, TICK_MS);
|
||||
|
||||
// Cleanup on exit
|
||||
process.on("SIGINT", () => {
|
||||
clearInterval(interval);
|
||||
ui.screen.destroy();
|
||||
|
||||
Reference in New Issue
Block a user