diff --git a/app/nose/bin/tetris.ts b/app/nose/bin/tetris.ts new file mode 100644 index 0000000..daca53d --- /dev/null +++ b/app/nose/bin/tetris.ts @@ -0,0 +1,278 @@ +/// +export const game = true + +import type { GameContext, InputState } from "@/shared/game" +import { randomElement, randomIndex } from "@/shared/utils.ts" + +const COLS = 10 +const ROWS = 20 +const CELL = 25 +const DOWN_TICK = 10 +const MOVE_TICK = 3 + +let player: { x: number, y: number, shape: string, rotation: number } +let grid: string[][] = [] +let dead = false +let downTick = 0 +let moveTick = 0 + +const COLORS: Record = { + I: "cyan", + O: "yellow", + T: "purple", + S: "green", + Z: "red", + J: "blue", + L: "orange", +} + +// I, O, T, L, J, S, Z +const SHAPES: Record = { + I: [ + [[1, 1, 1, 1]], + [[1], [1], [1], [1]] + ], + + O: [[ + [1, 1], + [1, 1] + ]], + + T: [ + [ + [0, 1, 0], + [1, 1, 1], + ], + [ + [1, 0], + [1, 1], + [1, 0] + ], + [ + [1, 1, 1], + [0, 1, 0], + ], + [ + [0, 1], + [1, 1], + [0, 1] + ] + ], + + L: [ + [ + [1, 0], + [1, 0], + [1, 1] + ], + [ + [1, 1, 1], + [1, 0, 0] + ], + [ + [1, 1], + [0, 1], + [0, 1] + ], + [ + [0, 0, 1], + [1, 1, 1] + ] + ], + + J: [ + [ + [0, 1], + [0, 1], + [1, 1] + ], + [ + [1, 0, 0], + [1, 1, 1] + ], + [ + [1, 1], + [1, 0], + [1, 0] + ], + [ + [1, 1, 1], + [0, 0, 1] + ] + ], + + S: [ + [ + [0, 1, 1], + [1, 1, 0] + ], + [ + [1, 0], + [1, 1], + [0, 1] + ], + [ + [0, 1, 1], + [1, 1, 0], + ], + [ + [1, 0], + [1, 1], + [0, 1] + ] + ], + + Z: [ + [ + [1, 1, 0], + [0, 1, 1] + ], + [ + [0, 1], + [1, 1], + [1, 0] + ], + [ + [1, 1, 0], + [0, 1, 1] + ], + [ + [0, 1], + [1, 1], + [1, 0] + ] + ], +} + +export function init() { + dead = false + downTick = 0 + moveTick = 0 + + player = newShape() +} + +export function update(_delta: number, input: InputState) { + const keys = input.pressed + + if (input.justPressed.has(" ")) { + player.rotation += 1 + if (player.rotation === SHAPES[player.shape]!.length) + 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 + } + } + } + + 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 (++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 (++downTick % DOWN_TICK === 0) { + player.y += 1 + } +} + +export function draw(game: GameContext) { + // game.clear("#6C6FF6") + game.clear("#654321") + + const boardW = COLS * CELL + const boardH = ROWS * CELL + const offsetX = (game.width - boardW) / 2 + const offsetY = (game.height - boardH) / 2 + + const c = game.ctx + c.save() + c.translate(offsetX, offsetY) + + // background + game.rectfill(0, 0, boardW, boardH, "black") + + // draw board + for (let row = 0; row < ROWS; row++) { + for (let col = 0; col < COLS; col++) { + drawBlock(game, col, row, grid[row]?.[col] || "gray") + } + } + + // 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]!) + } + } + + c.restore() + + // ya dead + if (dead) { + game.centerText("GAME OVER", "red", 24) + } +} + +function newShape() { + const shape = randomElement(Object.keys(SHAPES))! + const newShape = { x: 3, y: 0, shape, rotation: randomIndex(SHAPES[shape]!)! } + // const shape = SHAPES["O"] + // const newShape = { x: 3, y: 0, shape, rotation: randomIndex(SHAPES[shape]!)! } + console.log("new shape", newShape) + return newShape +} + +function drawBlock(game: GameContext, x: number, y: number, color: string) { + game.rectfill( + x * CELL, + y * CELL, + ((x + 1) * CELL) - .5, + ((y + 1) * CELL) - .5, + color + ) +} \ No newline at end of file