import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent"; import { type Component, Key, matchesKey, type TUI, truncateToWidth, visibleWidth, } from "@earendil-works/pi-tui"; import { boardPrompt, type Direction, PacManGame } from "./game.ts"; const SAVE_TYPE = "pac-man-save"; const colors = { green: "\x1b[32m", red: "\x1b[31m", blue: "\x1b[34m", reset: "\x1b[0m", }; class PacManComponent implements Component { private error = ""; private game: PacManGame; private onClose: () => void; private onMove: (direction: Direction) => void; private onNew: () => void; private tui: TUI; constructor(tui: TUI, game: PacManGame, onClose: () => void, onMove: (direction: Direction) => void, onNew: () => void) { this.tui = tui; this.game = game; this.onClose = onClose; this.onMove = onMove; this.onNew = onNew; } handleInput(data: string): void { if (matchesKey(data, "q")) return this.onClose(); if (matchesKey(data, "n")) return this.onNew(); const direction = inputDirection(data); if (direction) this.onMove(direction); } render(width: number): string[] { return [ this.center("PAC-MAN", width), "", ...colorBoard(this.game).split("\n").map((line) => this.center(line, width)), "", this.center(this.game.statusText(), width), this.error ? this.center(`Error: ${this.error}`, width) : "", this.center("Arrows/WASD=move n=new q=quit", width), ].map((line) => truncateToWidth(line, width)); } setGame(game: PacManGame): void { this.game = game; this.error = ""; this.tui.requestRender(); } setError(error: string): void { this.error = error; this.tui.requestRender(); } center(text: string, width: number): string { return " ".repeat(Math.max(0, Math.floor((width - visibleWidth(text)) / 2))) + text; } invalidate(): void {} } export default function pacMan(pi: ExtensionAPI): void { let game: PacManGame | null = null; let active: PacManComponent | null = null; pi.on("session_start", (_event, ctx) => { game = loadGame(ctx.sessionManager.getBranch()); }); pi.registerCommand("pac-man", { description: "Play Pac-Man with deterministic local ghosts", handler: async (args, ctx) => { if (ctx.mode !== "tui") { ctx.ui.notify("Pac-Man overlay only works in TUI mode", "error"); return; } if (!game || game.status !== "playing" || args.trim() === "new") { game = new PacManGame(); save(); } await show(ctx); }, }); function handleMove(direction: Direction): void { if (!game?.move(direction)) return active?.setError(game?.status === "playing" ? `Blocked: ${direction}` : "Press n for a new game"); save(); active?.setGame(game); } async function show(ctx: ExtensionCommandContext): Promise { await ctx.ui.custom( (tui, _theme, _keybindings, done) => { active = new PacManComponent( tui, game!, () => { active = null; done(undefined); }, handleMove, () => { game = new PacManGame(); save(); active?.setGame(game); }, ); return active; }, { overlay: true, overlayOptions: { width: "50%", maxHeight: "70%", anchor: "center", margin: { top: 1 }, }, }, ); } function save(): void { if (game) pi.appendEntry(SAVE_TYPE, game.toState()); } } export function colorBoard(game: PacManGame): string { return boardPrompt(game).replace(/[PBKICXg]/g, (cell) => { if (cell === "P") return color(cell, colors.green); if (cell === "g") return color(cell, colors.blue); return color(cell, colors.red); }); } function color(text: string, ansi: string): string { return `${ansi}${text}${colors.reset}`; } function inputDirection(data: string): Direction | null { if (matchesKey(data, Key.up) || matchesKey(data, "w")) return "up"; if (matchesKey(data, Key.right) || matchesKey(data, "d")) return "right"; if (matchesKey(data, Key.down) || matchesKey(data, "s")) return "down"; if (matchesKey(data, Key.left) || matchesKey(data, "a")) return "left"; return null; } function loadGame(entries: Array<{ type: string; customType?: string; data?: unknown }>): PacManGame | null { for (let i = entries.length - 1; i >= 0; i--) { const entry = entries[i]; if (entry.type === "custom" && entry.customType === SAVE_TYPE) { try { return PacManGame.fromState(entry.data); } catch { return null; } } } return null; }