diff --git a/app/nose/bin/tetris.ts b/app/nose/bin/tetris.ts index 455db0f..41f3ae4 100644 --- a/app/nose/bin/tetris.ts +++ b/app/nose/bin/tetris.ts @@ -14,14 +14,15 @@ const LOCK_DELAY = 5 type Shape = { x: number, y: number, shape: string, rotation: number } +let score = 0 let player: Shape let nextShape: Shape let grid: string[][] = [] let dead = false let downTick = 0 let moveTick = 0 -let tetrisRows: number[] = [] -let tetrisTimer = 0 +let clearedLines: number[] = [] +let clearedLinesTimer = 0 let lockTimer = 0 const BORDER_COLOR = "#b388ff" @@ -29,6 +30,13 @@ const BG_COLOR = "#222" const LINE_COLOR = "black" const UI_BG_COLOR = "#4c3a87" +const LINE_SCORES = [ + 100, + 300, + 500, + 800 +] + const COLORS: Record = { I: "cyan", O: "yellow", @@ -158,12 +166,13 @@ const SHAPES: Record = { } export function init() { + score = 0 dead = false downTick = 0 moveTick = 0 - tetrisTimer = TETRIS_FRAMES lockTimer = 0 - tetrisRows.length = 0 + clearedLinesTimer = TETRIS_FRAMES + clearedLines.length = 0 grid.length = 0 player = newShape() @@ -171,25 +180,25 @@ export function init() { } export function update(_delta: number, input: InputState) { - const keys = input.pressed + if (dead) return if (input.justPressed.has(" ")) { rotateShape() } // tetris animation - if (tetrisRows.length > 0) { - tetrisTimer-- - if (tetrisTimer <= 0) { - removeTetrisRows() - tetrisRows = [] + if (clearedLines.length > 0) { + clearedLinesTimer-- + if (clearedLinesTimer <= 0) { + removeClearedLines() + clearedLines = [] } } else if (anyFullRows()) { - tetrisRows = findFullRows() - tetrisTimer = TETRIS_FRAMES + clearedLines = findFullRows() + clearedLinesTimer = TETRIS_FRAMES } - const hit = blocked() + const hit = isBlocked() if (!hit) lockTimer = LOCK_DELAY if (hit) { @@ -201,25 +210,8 @@ export function update(_delta: number, input: InputState) { } } - if (++moveTick % MOVE_TICK === 0) { - 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 (++moveTick % MOVE_TICK === 0) + detectMovement(input) if (++downTick % DOWN_TICK === 0) if (!collision(player.x, player.y + 1, player.rotation)) { @@ -228,7 +220,6 @@ export function update(_delta: number, input: InputState) { } export function draw(game: GameContext) { - // game.clear("#654321") // brown game.clear(UI_BG_COLOR) const boardW = COLS * CELL @@ -244,46 +235,35 @@ export function draw(game: GameContext) { game.rectfill(0, 0, boardW, boardH, LINE_COLOR) // draw border - for (let col = -1; col <= COLS; col++) { - drawBlock(game, col, -1, BORDER_COLOR) - drawBlock(game, col, ROWS, BORDER_COLOR) - } - for (let row = 0; row < ROWS; row++) { - drawBlock(game, -1, row, BORDER_COLOR) - drawBlock(game, COLS, row, BORDER_COLOR) - } + drawBorder(game) // draw board - for (let row = 0; row < ROWS; row++) { - for (let col = 0; col < COLS; col++) { - let color = grid[row]?.[col] || BG_COLOR - if (tetrisRows.includes(row)) color = "white" - drawBlock(game, col, row, color) - } - } + drawBoard(game) // player shape - 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) - - drawBlock(game, x, y, COLORS[player.shape]!) - } - } + drawPlayer(game) // "next shape" UI drawPreview(game) + // high score + game.text(`Score: ${score}`, (COLS + 2) * CELL, (8 * CELL) + 10, "yellow", 15) + c.restore() // ya dead - if (dead) { - game.centerText("GAME OVER", "red", 24) + if (dead) + game.centerText("GAME OVER", "red", 60) +} + +function spawn() { + player = nextShape + player.x = 3 + player.y = 0 + nextShape = newShape() + + if (collision(player.x, player.y, player.rotation)) { + dead = true } } @@ -311,11 +291,12 @@ function findFullRows(): number[] { return rows } -function removeTetrisRows() { +function removeClearedLines() { const newGrid: string[][] = [] + score += LINE_SCORES[clearedLines.length - 1] || 0 for (let y = 0; y < ROWS; y++) - if (!tetrisRows.includes(y)) + if (!clearedLines.includes(y)) newGrid.push(grid[y] ?? []) // add empty rows on top to restore height @@ -323,7 +304,7 @@ function removeTetrisRows() { newGrid.unshift([]) grid = newGrid - tetrisRows = [] + clearedLines = [] } function collision(x: number, y: number, rotation: number): boolean { @@ -360,13 +341,10 @@ function lockShape() { } } - player = nextShape - player.x = 3 - player.y = 0 - nextShape = newShape() + spawn() } -function blocked(): boolean { +function isBlocked(): boolean { const shape = SHAPES[player.shape]![player.rotation]! let hit = false @@ -390,6 +368,28 @@ function blocked(): boolean { return hit } +function detectMovement(input: InputState) { + const keys = input.pressed + + 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 +} + function drawBlock(game: GameContext, x: number, y: number, color: string) { game.rectfill( x * CELL, @@ -429,20 +429,19 @@ function drawPreview(game: GameContext) { // draw board for (let row = 0; row < rows; row++) { for (let col = 0; col < cols; col++) { - let color = grid[row]?.[col] || BG_COLOR - drawBlock(game, previewX + col, previewY + row, color) + drawBlock(game, previewX + col, previewY + row, BG_COLOR) } } // draw border for (let col = -1; col < cols; col++) { - drawBlock(game, previewX + col + 1, -1, BORDER_COLOR) - drawBlock(game, previewX + col + 1, rows, BORDER_COLOR) + drawBlock(game, previewX + col + 1, previewY + -1, BORDER_COLOR) + drawBlock(game, previewX + col + 1, previewY + rows, BORDER_COLOR) } for (let row = -1; row < rows; row++) { - drawBlock(game, previewX, row, BORDER_COLOR) - drawBlock(game, previewX + cols, row, BORDER_COLOR) + drawBlock(game, previewX, previewY + row, BORDER_COLOR) + drawBlock(game, previewX + cols, previewY + row, BORDER_COLOR) } const next = SHAPES[nextShape.shape]![0]! @@ -476,3 +475,39 @@ function rotateShape() { return } } + +function drawBorder(game: GameContext) { + for (let col = -1; col <= COLS; col++) { + drawBlock(game, col, -1, BORDER_COLOR) + drawBlock(game, col, ROWS, BORDER_COLOR) + } + for (let row = 0; row < ROWS; row++) { + drawBlock(game, -1, row, BORDER_COLOR) + drawBlock(game, COLS, row, BORDER_COLOR) + } +} + +function drawBoard(game: GameContext) { + for (let row = 0; row < ROWS; row++) { + for (let col = 0; col < COLS; col++) { + let color = grid[row]?.[col] || BG_COLOR + if (clearedLines.includes(row)) color = "white" + drawBlock(game, col, row, color) + } + } +} + +function drawPlayer(game: GameContext) { + 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) + + drawBlock(game, x, y, COLORS[player.shape]!) + } + } +} \ No newline at end of file