// ── Blackjack: BT-driven game loop with command-based input ── // // Architecture: // Behaviour Tree (buildTree) — controls game flow: // parallel // ├── dealerPlay (leaf) — generator loop, auto-plays dealer hand // └── 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 // // Cards as entities with tag components: // Each card is an entity with a Card component ({ rank, suit, order }). // Tag components (InDeck, InPlayerHand, InDealerHand) mark which // collection the card belongs to. Queries find cards by tag. // Order is tracked via the `order` field on Card. // // Usage: // npx tsx examples/blackjack/main.ts import { World } from "../../src/index"; import { buildTree } from "../../src/bt/index"; import { CommandQueue } from "../../src/commands/index"; import { Score, Bet, GamePhase, InDealerHand, createCardHelpers, } from "./components"; import { registerCommands, resolveRound, Hit, Stand, NewRound, BetMore, BetLess, } from "./commands"; import { dealerShouldHit } from "./game"; import { createUI, render } from "./render"; import { startInput, type Key } from "./input"; // ── Setup ──────────────────────────────────────────── const world = new World(); world.addSingleton(Score); world.addSingleton(Bet); world.addSingleton(GamePhase); const ui = createUI(); const cards = createCardHelpers(world); const commands = new CommandQueue(world); registerCommands(world, commands, cards); // ── Behaviour Tree ─────────────────────────────────── const runner = buildTree(world, { kind: "parallel", children: [ { kind: "leaf", *run() { while (true) { const dt: number = yield; const phase = world.getSingleton(GamePhase); if (phase.phase !== "dealerTurn") continue; if (dealerShouldHit(cards.getHand(InDealerHand))) { const cardEntity = cards.drawCard(); if (cardEntity) { cards.dealTo(cardEntity, InDealerHand); } } else { resolveRound(world, cards); } } }, }, { kind: "repeat", child: { kind: "sequential", children: [ { kind: "leaf", run: () => { commands.execute(); }, }, { kind: "leaf", run: () => { render(world, ui); }, }, ], }, }, ], }); // ── Input → Command mapping ────────────────────────── const keyToCommand: Partial> = { h: Hit, s: Stand, n: NewRound, up: BetMore, down: BetLess, }; startInput(ui.screen, (key) => { const cmd = keyToCommand[key]; if (cmd) { const cmdEntity = world.spawn(); world.add(cmdEntity, cmd); } }); // ── Game loop ──────────────────────────────────────── world.setSingleton(GamePhase, { phase: "betting", message: "Welcome to Blackjack! Press N to start.", }); cards.buildDeck(); cards.shuffleDeck(); 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); });