Compare commits
No commits in common. "6ef40f608d3065d4faa12cddacf14adad52a939a" and "e3dac7edcc3776a5bee267b793f399e2b7126e45" have entirely different histories.
6ef40f608d
...
e3dac7edcc
|
|
@ -14,37 +14,23 @@ 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 clearedLines: number[] = []
|
let tetrisRows: number[] = []
|
||||||
let clearedLinesTimer = 0
|
let tetrisTimer = 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: "cyan",
|
I: "#6fc6ff", // light blue (C64 cyan)
|
||||||
O: "yellow",
|
O: "#fce94f", // yellow
|
||||||
T: "magenta",
|
T: "#d87bfb", // light purple
|
||||||
S: "lime",
|
S: "#8ae234", // light green
|
||||||
Z: "red",
|
Z: "#f35f5f", // red
|
||||||
J: "blue",
|
J: "#3465a4", // blue
|
||||||
L: "orange",
|
L: "#e9b96e", // orange
|
||||||
}
|
}
|
||||||
|
|
||||||
// I, O, T, L, J, S, Z
|
// I, O, T, L, J, S, Z
|
||||||
|
|
@ -60,6 +46,10 @@ const SHAPES: Record<string, number[][][]> = {
|
||||||
]],
|
]],
|
||||||
|
|
||||||
T: [
|
T: [
|
||||||
|
[
|
||||||
|
[0, 1, 0],
|
||||||
|
[1, 1, 1],
|
||||||
|
],
|
||||||
[
|
[
|
||||||
[1, 0],
|
[1, 0],
|
||||||
[1, 1],
|
[1, 1],
|
||||||
|
|
@ -73,10 +63,6 @@ const SHAPES: Record<string, number[][][]> = {
|
||||||
[0, 1],
|
[0, 1],
|
||||||
[1, 1],
|
[1, 1],
|
||||||
[0, 1]
|
[0, 1]
|
||||||
],
|
|
||||||
[
|
|
||||||
[0, 1, 0],
|
|
||||||
[1, 1, 1],
|
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
|
|
||||||
|
|
@ -123,6 +109,10 @@ const SHAPES: Record<string, number[][][]> = {
|
||||||
],
|
],
|
||||||
|
|
||||||
S: [
|
S: [
|
||||||
|
[
|
||||||
|
[0, 1, 1],
|
||||||
|
[1, 1, 0]
|
||||||
|
],
|
||||||
[
|
[
|
||||||
[1, 0],
|
[1, 0],
|
||||||
[1, 1],
|
[1, 1],
|
||||||
|
|
@ -136,14 +126,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],
|
||||||
|
|
@ -157,48 +147,44 @@ 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
|
||||||
clearedLinesTimer = TETRIS_FRAMES
|
tetrisRows.length = 0
|
||||||
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) {
|
||||||
if (dead) return
|
const keys = input.pressed
|
||||||
|
|
||||||
if (input.justPressed.has(" ")) {
|
if (input.justPressed.has(" ")) {
|
||||||
rotateShape()
|
player.rotation += 1
|
||||||
|
if (player.rotation === SHAPES[player.shape]!.length)
|
||||||
|
player.rotation = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// tetris animation
|
// tetris animation
|
||||||
if (clearedLines.length > 0) {
|
if (tetrisRows.length > 0) {
|
||||||
clearedLinesTimer--
|
tetrisTimer--
|
||||||
if (clearedLinesTimer <= 0) {
|
if (tetrisTimer <= 0) {
|
||||||
removeClearedLines()
|
removeTetrisRows()
|
||||||
clearedLines = []
|
tetrisRows = []
|
||||||
}
|
}
|
||||||
} else if (anyFullRows()) {
|
} else if (anyFullRows()) {
|
||||||
clearedLines = findFullRows()
|
tetrisRows = findFullRows()
|
||||||
clearedLinesTimer = TETRIS_FRAMES
|
tetrisTimer = TETRIS_FRAMES
|
||||||
}
|
}
|
||||||
|
|
||||||
const hit = isBlocked()
|
const hit = blocked()
|
||||||
if (!hit) lockTimer = LOCK_DELAY
|
if (!hit) lockTimer = LOCK_DELAY
|
||||||
|
|
||||||
if (hit) {
|
if (hit) {
|
||||||
|
|
@ -210,8 +196,25 @@ export function update(_delta: number, input: InputState) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (++moveTick % MOVE_TICK === 0)
|
if (++moveTick % MOVE_TICK === 0) {
|
||||||
detectMovement(input)
|
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)
|
if (++downTick % DOWN_TICK === 0)
|
||||||
if (!collision(player.x, player.y + 1, player.rotation)) {
|
if (!collision(player.x, player.y + 1, player.rotation)) {
|
||||||
|
|
@ -220,7 +223,8 @@ export function update(_delta: number, input: InputState) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function draw(game: GameContext) {
|
export function draw(game: GameContext) {
|
||||||
game.clear(UI_BG_COLOR)
|
// game.clear("#6C6FF6")
|
||||||
|
game.clear("#654321")
|
||||||
|
|
||||||
const boardW = COLS * CELL
|
const boardW = COLS * CELL
|
||||||
const boardH = ROWS * CELL
|
const boardH = ROWS * CELL
|
||||||
|
|
@ -231,39 +235,37 @@ export function draw(game: GameContext) {
|
||||||
c.save()
|
c.save()
|
||||||
c.translate(offsetX, offsetY)
|
c.translate(offsetX, offsetY)
|
||||||
|
|
||||||
// lines (shows through cracks between blocks)
|
// background
|
||||||
game.rectfill(0, 0, boardW, boardH, LINE_COLOR)
|
game.rectfill(0, 0, boardW, boardH, "black")
|
||||||
|
|
||||||
// draw border
|
|
||||||
drawBorder(game)
|
|
||||||
|
|
||||||
// draw board
|
// draw board
|
||||||
drawBoard(game)
|
for (let row = 0; row < ROWS; row++) {
|
||||||
|
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
|
||||||
drawPlayer(game)
|
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
|
||||||
|
|
||||||
// "next shape" UI
|
const x = player.x + Number(col)
|
||||||
drawPreview(game)
|
const y = player.y + Number(row)
|
||||||
|
|
||||||
// high score
|
drawBlock(game, x, y, COLORS[player.shape]!)
|
||||||
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", 60)
|
game.centerText("GAME OVER", "red", 24)
|
||||||
}
|
|
||||||
|
|
||||||
function spawn() {
|
|
||||||
player = nextShape
|
|
||||||
player.x = 3
|
|
||||||
player.y = 0
|
|
||||||
nextShape = newShape()
|
|
||||||
|
|
||||||
if (collision(player.x, player.y, player.rotation)) {
|
|
||||||
dead = true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -291,12 +293,11 @@ function findFullRows(): number[] {
|
||||||
return rows
|
return rows
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeClearedLines() {
|
function removeTetrisRows() {
|
||||||
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 (!clearedLines.includes(y))
|
if (!tetrisRows.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
|
||||||
|
|
@ -304,7 +305,7 @@ function removeClearedLines() {
|
||||||
newGrid.unshift([])
|
newGrid.unshift([])
|
||||||
|
|
||||||
grid = newGrid
|
grid = newGrid
|
||||||
clearedLines = []
|
tetrisRows = []
|
||||||
}
|
}
|
||||||
|
|
||||||
function collision(x: number, y: number, rotation: number): boolean {
|
function collision(x: number, y: number, rotation: number): boolean {
|
||||||
|
|
@ -341,10 +342,10 @@ function lockShape() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
spawn()
|
player = newShape()
|
||||||
}
|
}
|
||||||
|
|
||||||
function isBlocked(): boolean {
|
function blocked(): boolean {
|
||||||
const shape = SHAPES[player.shape]![player.rotation]!
|
const shape = SHAPES[player.shape]![player.rotation]!
|
||||||
let hit = false
|
let hit = false
|
||||||
|
|
||||||
|
|
@ -368,28 +369,6 @@ function isBlocked(): 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,
|
||||||
|
|
@ -398,116 +377,4 @@ function drawBlock(game: GameContext, x: number, y: number, color: string) {
|
||||||
((y + 1) * CELL) - .5,
|
((y + 1) * CELL) - .5,
|
||||||
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]!)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Loading…
Reference in New Issue
Block a user