From e33fb44c16b3743425e9bab194d041cf16a0d349 Mon Sep 17 00:00:00 2001 From: Chris Wanstrath Date: Mon, 29 Sep 2025 00:07:31 -0700 Subject: [PATCH] wow, tetris --- app/nose/bin/tetris.ts | 167 +++++++++++++++++++++++++---------------- 1 file changed, 102 insertions(+), 65 deletions(-) diff --git a/app/nose/bin/tetris.ts b/app/nose/bin/tetris.ts index 1201b28..cb05feb 100644 --- a/app/nose/bin/tetris.ts +++ b/app/nose/bin/tetris.ts @@ -10,6 +10,7 @@ const CELL = 25 const DOWN_TICK = 10 const MOVE_TICK = 3 const TETRIS_FRAMES = 5 +const LOCK_DELAY = 5 type Shape = { x: number, y: number, shape: string, rotation: number } @@ -20,6 +21,7 @@ let downTick = 0 let moveTick = 0 let tetrisRows: number[] = [] let tetrisTimer = 0 +let lockTimer = 0 const COLORS: Record = { I: "cyan", @@ -154,22 +156,10 @@ export function init() { downTick = 0 moveTick = 0 tetrisTimer = TETRIS_FRAMES + lockTimer = 0 tetrisRows.length = 0 grid.length = 0 - grid[ROWS - 1] = [ - "magenta", - "magenta", - "magenta", - "magenta", - "", - "magenta", - "magenta", - "magenta", - "magenta", - "magenta", - ] - player = newShape() } @@ -182,66 +172,54 @@ export function update(_delta: number, input: InputState) { player.rotation = 0 } - // save to grid - const shape = SHAPES[player.shape]![player.rotation]! - if (shape) { - let hit = false - outer: for (const row in shape) { - for (const col in shape[row]!) { - const filled = shape[row][col] - if (!filled) continue - - const x = player.x + Number(col) - const y = player.y + Number(row) - - grid[y + 1] ??= [] - - if ((y + 1) >= ROWS || grid[y + 1]![x]) { - hit = true - break outer - } - } + // tetris animation + if (tetrisRows.length > 0) { + tetrisTimer-- + if (tetrisTimer <= 0) { + removeTetrisRows() + tetrisRows = [] } + } else if (anyFullRows()) { + tetrisRows = findFullRows() + tetrisTimer = TETRIS_FRAMES + } - if (tetrisRows.length > 0) { - tetrisTimer-- - if (tetrisTimer <= 0) { - removeTetrisRows() - tetrisRows = [] - } - } else if (anyFullRows()) { - tetrisRows = findFullRows() - tetrisTimer = TETRIS_FRAMES - } + const hit = blocked() + if (!hit) lockTimer = LOCK_DELAY - if (hit) { - const shape = SHAPES[player.shape]![player.rotation]! - for (const row in shape) { - for (const col in shape[row]!) { - const filled = shape[row][col] - if (!filled) continue - - const x = player.x + Number(col) - const y = player.y + Number(row) - - grid[y] ??= [] - grid[y][x] = COLORS[player.shape]! - } - } - - player = newShape() + if (hit) { + if (lockTimer > 0) { + lockTimer-- + } else { + lockShape() + return } } if (++moveTick % MOVE_TICK === 0) { - if (keys.has("ArrowLeft") || keys.has("a")) player.x = Math.max(0, player.x - 1) - if (keys.has("ArrowRight") || keys.has("d")) player.x = Math.min(COLS, player.x + 1) - if (keys.has("ArrowDown") || keys.has("s")) { player.y += 1 } + if ( + (keys.has("ArrowLeft") || keys.has("a")) && + !collision(player.x - 1, player.y, player.rotation) + ) + player.x = Math.max(0, player.x - 1) + + if ( + (keys.has("ArrowRight") || keys.has("d")) && + !collision(player.x + 1, player.y, player.rotation) + ) + player.x = Math.min(COLS, player.x + 1) + + if ( + (keys.has("ArrowDown") || keys.has("s")) && + !collision(player.x, player.y + 1, player.rotation) + ) + player.y += 1 } - if (++downTick % DOWN_TICK === 0) { - player.y += 1 - } + if (++downTick % DOWN_TICK === 0) + if (!collision(player.x, player.y + 1, player.rotation)) { + player.y++ + } } export function draw(game: GameContext) { @@ -315,7 +293,6 @@ function findFullRows(): number[] { return rows } - function removeTetrisRows() { const newGrid: string[][] = [] @@ -331,6 +308,66 @@ function removeTetrisRows() { tetrisRows = [] } +function collision(x: number, y: number, rotation: number): boolean { + const shape = SHAPES[player.shape]![rotation]! + + for (let row = 0; row < shape.length; row++) { + for (let col = 0; col < shape[row]!.length; col++) { + if (!shape[row]![col]) continue + + const nx = x + col + const ny = y + row + + if (nx < 0 || nx >= COLS) return true + if (ny >= ROWS) return true + if (grid[ny]?.[nx]) return true + } + } + + return false +} + +function lockShape() { + const shape = SHAPES[player.shape]![player.rotation]! + for (const row in shape) { + for (const col in shape[row]!) { + const filled = shape[row][col] + if (!filled) continue + + const x = player.x + Number(col) + const y = player.y + Number(row) + + grid[y] ??= [] + grid[y][x] = COLORS[player.shape]! + } + } + + player = newShape() +} + +function blocked(): boolean { + const shape = SHAPES[player.shape]![player.rotation]! + let hit = false + + outer: for (const row in shape) { + for (const col in shape[row]!) { + const filled = shape[row][col] + if (!filled) continue + + const x = player.x + Number(col) + const y = player.y + Number(row) + + grid[y + 1] ??= [] + + if ((y + 1) >= ROWS || grid[y + 1]![x]) { + hit = true + break outer + } + } + } + + return hit +} function drawBlock(game: GameContext, x: number, y: number, color: string) { game.rectfill(