// ── Terminal rendering via blessed ──────────────────── import blessed from "blessed"; import type { World, Entity } from "../../src/index"; import { Board, Piece, Score, GameOver, Paused } from "./components"; import { BOARD_W, BOARD_H, ghostY } from "./game"; // ANSI color codes for the 7 piece colors const COLORS: Record = { 0: "\x1b[40m", // black (empty) 1: "\x1b[46m", // cyan (I) 2: "\x1b[43m", // yellow (O) 3: "\x1b[45m", // magenta (T) 4: "\x1b[44m", // blue (J) 5: "\x1b[47m\x1b[30m", // white on black (L) 6: "\x1b[42m", // green (S) 7: "\x1b[41m", // red (Z) }; const RESET = "\x1b[0m"; const GHOST_CHAR = "░"; export function createUI(): { screen: blessed.Widgets.Screen; boardBox: blessed.Widgets.BoxElement; scoreText: blessed.Widgets.TextElement; statusBox: blessed.Widgets.BoxElement; statusText: blessed.Widgets.TextElement; controlsText: blessed.Widgets.TextElement; } { const screen = blessed.screen({ smartCSR: true, title: "Tetris", }); // Board display const boardBox = blessed.box({ parent: screen, top: 2, left: "center", width: BOARD_W * 2 + 2, height: BOARD_H + 2, border: { type: "line" }, style: { border: { fg: "white" } }, }); // Score const scoreText = blessed.text({ parent: screen, top: 2, left: 2, width: 30, height: 3, content: "Score: 0 Lines: 0 Level: 1", style: { fg: "white" }, }); // Status overlay (game over / paused) const statusBox = blessed.box({ parent: screen, top: "center", left: "center", width: 22, height: 5, border: { type: "line" }, style: { border: { fg: "yellow" }, fg: "yellow" }, hidden: true, }); const statusText = blessed.text({ parent: statusBox, top: 1, left: "center", width: 20, align: "center", style: { fg: "yellow" }, }); // Controls hint const controlsText = blessed.text({ parent: screen, bottom: 0, left: "center", width: 60, height: 1, content: "← → : move ↑ : rotate ↓ : soft drop Space : hard drop P : pause Q : quit", style: { fg: "gray" }, }); return { screen, boardBox, scoreText, statusBox, statusText, controlsText }; } /** Render the full game state into the blessed UI. */ export function render( world: World, gameEntity: Entity, ui: ReturnType, ): void { const board = world.get(gameEntity, Board); const piece = world.tryGet(gameEntity, Piece); const score = world.tryGet(gameEntity, Score); const isOver = world.has(gameEntity, GameOver); const isPaused = world.has(gameEntity, Paused); // Build display grid const display = board.grid.map((row) => [...row]); if (piece) { const gy = ghostY(board.grid, piece.shape, piece.x, piece.y); // Ghost for (let r = 0; r < piece.shape.length; r++) { for (let c = 0; c < piece.shape[r].length; c++) { if (!piece.shape[r][c]) continue; const by = gy + r; const bx = piece.x + c; if ( by >= 0 && by < BOARD_H && bx >= 0 && bx < BOARD_W && !display[by][bx] ) { display[by][bx] = -piece.color; } } } // Active piece for (let r = 0; r < piece.shape.length; r++) { for (let c = 0; c < piece.shape[r].length; c++) { if (!piece.shape[r][c]) continue; const by = piece.y + r; const bx = piece.x + c; if (by >= 0 && by < BOARD_H && bx >= 0 && bx < BOARD_W) { display[by][bx] = piece.color; } } } } // Build board string let boardStr = ""; for (let r = 0; r < BOARD_H; r++) { for (let c = 0; c < BOARD_W; c++) { const v = display[r][c]; if (v === 0) { boardStr += " ·"; } else if (v < 0) { boardStr += COLORS[-v] + GHOST_CHAR + GHOST_CHAR + RESET; } else { boardStr += COLORS[v] + " " + RESET; } } if (r < BOARD_H - 1) boardStr += "\n"; } ui.boardBox.setContent(boardStr); // Score if (score) { ui.scoreText.setContent( `Score: ${score.points} Lines: ${score.lines} Level: ${score.level}`, ); } // Status if (isOver) { ui.statusBox.show(); ui.statusText.setContent("GAME OVER\nPress R to restart"); } else if (isPaused) { ui.statusBox.show(); ui.statusText.setContent("PAUSED"); } else { ui.statusBox.hide(); } ui.screen.render(); }