// ── Tetris game logic (pure functions, no ECS dependency) ── export const BOARD_W = 10; export const BOARD_H = 20; // ── Tetrominoes ────────────────────────────────────── export const PIECES: { shape: number[][]; color: number }[] = [ { shape: [[1, 1, 1, 1]], color: 1 }, // I { shape: [[1, 1], [1, 1]], color: 2 }, // O { shape: [[0, 1, 0], [1, 1, 1]], color: 3 }, // T { shape: [[1, 0, 0], [1, 1, 1]], color: 4 }, // J { shape: [[0, 0, 1], [1, 1, 1]], color: 5 }, // L { shape: [[0, 1, 1], [1, 1, 0]], color: 6 }, // S { shape: [[1, 1, 0], [0, 1, 1]], color: 7 }, // Z ]; export function randomPiece(): { shape: number[][]; color: number } { const p = PIECES[Math.floor(Math.random() * PIECES.length)]; return { shape: p.shape.map((r) => [...r]), color: p.color }; } // ── Collision ──────────────────────────────────────── export function collides( grid: Uint8Array[], shape: number[][], px: number, py: number, ): boolean { for (let r = 0; r < shape.length; r++) { for (let c = 0; c < shape[r].length; c++) { if (!shape[r][c]) continue; const bx = px + c; const by = py + r; if (bx < 0 || bx >= BOARD_W || by >= BOARD_H) return true; if (by < 0) continue; // above the board is ok if (grid[by][bx]) return true; } } return false; } // ── Lock piece onto the grid ───────────────────────── export function lockPiece( grid: Uint8Array[], shape: number[][], color: number, px: number, py: number, ): void { for (let r = 0; r < shape.length; r++) { for (let c = 0; c < shape[r].length; c++) { if (!shape[r][c]) continue; const bx = px + c; const by = py + r; if (by >= 0 && by < BOARD_H && bx >= 0 && bx < BOARD_W) { grid[by][bx] = color; } } } } // ── Line clearing ──────────────────────────────────── export function clearLines(grid: Uint8Array[]): number { let cleared = 0; for (let r = BOARD_H - 1; r >= 0; r--) { if (grid[r].every((v) => v !== 0)) { // Shift everything above down for (let rr = r; rr > 0; rr--) { grid[rr] = grid[rr - 1]; } grid[0] = new Uint8Array(BOARD_W); cleared++; r++; // re-check this row } } return cleared; } // ── Scoring ────────────────────────────────────────── const LINE_SCORES = [0, 100, 300, 500, 800]; export function scoreForLines(lines: number, level: number): number { return (LINE_SCORES[lines] ?? lines * 200) * level; } // ── Ghost piece (hard-drop preview) ────────────────── export function ghostY( grid: Uint8Array[], shape: number[][], px: number, py: number, ): number { let gy = py; while (!collides(grid, shape, px, gy + 1)) { gy++; } return gy; } // ── Rotation (clockwise) ───────────────────────────── export function rotateCW(shape: number[][]): number[][] { const rows = shape.length; const cols = shape[0].length; const rotated: number[][] = []; for (let c = 0; c < cols; c++) { const row: number[] = []; for (let r = rows - 1; r >= 0; r--) { row.push(shape[r][c]); } rotated.push(row); } return rotated; } // ── Wall kick ──────────────────────────────────────── /** Try to rotate with basic wall kicks. Returns the rotated shape and x offset, or null. */ export function tryRotate( grid: Uint8Array[], shape: number[][], px: number, py: number, ): { shape: number[][]; x: number } | null { const rotated = rotateCW(shape); // Try offsets: 0, -1, +1, -2, +2 for (const dx of [0, -1, 1, -2, 2]) { if (!collides(grid, rotated, px + dx, py)) { return { shape: rotated, x: px + dx }; } } return null; }