Compare commits

...

4 Commits

Author SHA1 Message Date
6ef40f608d score, game over 2025-09-29 11:12:11 -07:00
f7bf91f085 fix rotation 2025-09-29 10:47:50 -07:00
7d5062afab preview next shape, better colors 2025-09-29 10:40:38 -07:00
e5d792ada0 preview next shape 2025-09-29 10:17:15 -07:00

View File

@ -14,23 +14,37 @@ const LOCK_DELAY = 5
type Shape = { x: number, y: number, shape: string, rotation: number } type Shape = { x: number, y: number, shape: string, rotation: number }
let score = 0
let player: Shape let player: Shape
let nextShape: Shape
let grid: string[][] = [] let grid: string[][] = []
let dead = false let dead = false
let downTick = 0 let downTick = 0
let moveTick = 0 let moveTick = 0
let tetrisRows: number[] = [] let clearedLines: number[] = []
let tetrisTimer = 0 let clearedLinesTimer = 0
let lockTimer = 0 let lockTimer = 0
const BORDER_COLOR = "#b388ff"
const BG_COLOR = "#222"
const LINE_COLOR = "black"
const UI_BG_COLOR = "#4c3a87"
const LINE_SCORES = [
100,
300,
500,
800
]
const COLORS: Record<string, string> = { const COLORS: Record<string, string> = {
I: "#6fc6ff", // light blue (C64 cyan) I: "cyan",
O: "#fce94f", // yellow O: "yellow",
T: "#d87bfb", // light purple T: "magenta",
S: "#8ae234", // light green S: "lime",
Z: "#f35f5f", // red Z: "red",
J: "#3465a4", // blue J: "blue",
L: "#e9b96e", // orange L: "orange",
} }
// I, O, T, L, J, S, Z // I, O, T, L, J, S, Z
@ -46,10 +60,6 @@ const SHAPES: Record<string, number[][][]> = {
]], ]],
T: [ T: [
[
[0, 1, 0],
[1, 1, 1],
],
[ [
[1, 0], [1, 0],
[1, 1], [1, 1],
@ -63,6 +73,10 @@ const SHAPES: Record<string, number[][][]> = {
[0, 1], [0, 1],
[1, 1], [1, 1],
[0, 1] [0, 1]
],
[
[0, 1, 0],
[1, 1, 1],
] ]
], ],
@ -109,10 +123,6 @@ const SHAPES: Record<string, number[][][]> = {
], ],
S: [ S: [
[
[0, 1, 1],
[1, 1, 0]
],
[ [
[1, 0], [1, 0],
[1, 1], [1, 1],
@ -126,14 +136,14 @@ const SHAPES: Record<string, number[][][]> = {
[1, 0], [1, 0],
[1, 1], [1, 1],
[0, 1] [0, 1]
],
[
[0, 1, 1],
[1, 1, 0]
] ]
], ],
Z: [ Z: [
[
[1, 1, 0],
[0, 1, 1]
],
[ [
[0, 1], [0, 1],
[1, 1], [1, 1],
@ -147,44 +157,48 @@ const SHAPES: Record<string, number[][][]> = {
[0, 1], [0, 1],
[1, 1], [1, 1],
[1, 0] [1, 0]
],
[
[1, 1, 0],
[0, 1, 1]
] ]
], ],
} }
export function init() { export function init() {
score = 0
dead = false dead = false
downTick = 0 downTick = 0
moveTick = 0 moveTick = 0
tetrisTimer = TETRIS_FRAMES
lockTimer = 0 lockTimer = 0
tetrisRows.length = 0 clearedLinesTimer = TETRIS_FRAMES
clearedLines.length = 0
grid.length = 0 grid.length = 0
player = newShape() player = newShape()
nextShape = newShape()
} }
export function update(_delta: number, input: InputState) { export function update(_delta: number, input: InputState) {
const keys = input.pressed if (dead) return
if (input.justPressed.has(" ")) { if (input.justPressed.has(" ")) {
player.rotation += 1 rotateShape()
if (player.rotation === SHAPES[player.shape]!.length)
player.rotation = 0
} }
// tetris animation // tetris animation
if (tetrisRows.length > 0) { if (clearedLines.length > 0) {
tetrisTimer-- clearedLinesTimer--
if (tetrisTimer <= 0) { if (clearedLinesTimer <= 0) {
removeTetrisRows() removeClearedLines()
tetrisRows = [] clearedLines = []
} }
} else if (anyFullRows()) { } else if (anyFullRows()) {
tetrisRows = findFullRows() clearedLines = findFullRows()
tetrisTimer = TETRIS_FRAMES clearedLinesTimer = TETRIS_FRAMES
} }
const hit = blocked() const hit = isBlocked()
if (!hit) lockTimer = LOCK_DELAY if (!hit) lockTimer = LOCK_DELAY
if (hit) { if (hit) {
@ -196,25 +210,8 @@ export function update(_delta: number, input: InputState) {
} }
} }
if (++moveTick % MOVE_TICK === 0) { if (++moveTick % MOVE_TICK === 0)
if ( detectMovement(input)
(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) if (++downTick % DOWN_TICK === 0)
if (!collision(player.x, player.y + 1, player.rotation)) { if (!collision(player.x, player.y + 1, player.rotation)) {
@ -223,8 +220,7 @@ export function update(_delta: number, input: InputState) {
} }
export function draw(game: GameContext) { export function draw(game: GameContext) {
// game.clear("#6C6FF6") game.clear(UI_BG_COLOR)
game.clear("#654321")
const boardW = COLS * CELL const boardW = COLS * CELL
const boardH = ROWS * CELL const boardH = ROWS * CELL
@ -235,37 +231,39 @@ export function draw(game: GameContext) {
c.save() c.save()
c.translate(offsetX, offsetY) c.translate(offsetX, offsetY)
// background // lines (shows through cracks between blocks)
game.rectfill(0, 0, boardW, boardH, "black") game.rectfill(0, 0, boardW, boardH, LINE_COLOR)
// draw border
drawBorder(game)
// draw board // draw board
for (let row = 0; row < ROWS; row++) { drawBoard(game)
for (let col = 0; col < COLS; col++) {
let color = grid[row]?.[col] || "gray"
if (tetrisRows.includes(row)) color = "white"
drawBlock(game, col, row, color)
}
}
// player shape // player shape
const shape = SHAPES[player.shape]![player.rotation]! drawPlayer(game)
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) // "next shape" UI
const y = player.y + Number(row) drawPreview(game)
drawBlock(game, x, y, COLORS[player.shape]!) // high score
} game.text(`Score: ${score}`, (COLS + 2) * CELL, (8 * CELL) + 10, "yellow", 15)
}
c.restore() c.restore()
// ya dead // ya dead
if (dead) { if (dead)
game.centerText("GAME OVER", "red", 24) 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
} }
} }
@ -293,11 +291,12 @@ function findFullRows(): number[] {
return rows return rows
} }
function removeTetrisRows() { function removeClearedLines() {
const newGrid: string[][] = [] const newGrid: string[][] = []
score += LINE_SCORES[clearedLines.length - 1] || 0
for (let y = 0; y < ROWS; y++) for (let y = 0; y < ROWS; y++)
if (!tetrisRows.includes(y)) if (!clearedLines.includes(y))
newGrid.push(grid[y] ?? []) newGrid.push(grid[y] ?? [])
// add empty rows on top to restore height // add empty rows on top to restore height
@ -305,7 +304,7 @@ function removeTetrisRows() {
newGrid.unshift([]) newGrid.unshift([])
grid = newGrid grid = newGrid
tetrisRows = [] clearedLines = []
} }
function collision(x: number, y: number, rotation: number): boolean { function collision(x: number, y: number, rotation: number): boolean {
@ -342,10 +341,10 @@ function lockShape() {
} }
} }
player = newShape() spawn()
} }
function blocked(): boolean { function isBlocked(): boolean {
const shape = SHAPES[player.shape]![player.rotation]! const shape = SHAPES[player.shape]![player.rotation]!
let hit = false let hit = false
@ -369,6 +368,28 @@ function blocked(): boolean {
return hit 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) { function drawBlock(game: GameContext, x: number, y: number, color: string) {
game.rectfill( game.rectfill(
x * CELL, x * CELL,
@ -378,3 +399,115 @@ function drawBlock(game: GameContext, x: number, y: number, color: string) {
color color
) )
} }
// I, O, T, L, J, S, Z
const PREVIEW_OFFSETS: Record<string, [number, number]> = {
I: [2, 3],
O: [3, 2],
T: [3, 2],
L: [3, 2],
J: [3, 2],
S: [3, 2],
Z: [3, 2],
}
// draw next shape (top-right corner)
function drawPreview(game: GameContext) {
const previewX = COLS + 2
const previewY = 0
const cols = 7
const rows = 7
// lines (shows through cracks between blocks)
game.rectfill(
previewX * CELL,
previewY * CELL,
(previewX + cols) * CELL,
(previewY + rows) * CELL,
LINE_COLOR
)
// draw board
for (let row = 0; row < rows; row++) {
for (let col = 0; col < cols; col++) {
drawBlock(game, previewX + col, previewY + row, BG_COLOR)
}
}
// draw border
for (let col = -1; col < cols; col++) {
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, previewY + row, BORDER_COLOR)
drawBlock(game, previewX + cols, previewY + row, BORDER_COLOR)
}
const next = SHAPES[nextShape.shape]![0]!
for (let row = 0; row < next.length; row++) {
for (let col = 0; col < next[row]!.length; col++) {
if (!next[row]![col]) continue
const [offsetX, offsetY] = PREVIEW_OFFSETS[nextShape.shape]!
drawBlock(game, previewX + col + offsetX, previewY + row + offsetY, COLORS[nextShape.shape]!)
}
}
}
function rotateShape() {
const oldRot = player.rotation
const newRot = (oldRot + 1) % SHAPES[player.shape]!.length
if (!collision(player.x, player.y, newRot)) {
player.rotation = newRot
return
}
if (!collision(player.x + 1, player.y, newRot)) {
player.x += 1
player.rotation = newRot
return
}
if (!collision(player.x - 1, player.y, newRot)) {
player.x -= 1
player.rotation = newRot
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]!)
}
}
}