Files
knl_meowscendence/src/front/static/ts/views/Tetris.ts

884 lines
26 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import Aview from "./Aview.ts";
import { isLogged } from "../main.js";
import { dragElement } from "./drag.js";
import { setOnekoState, setBallPos, setOnekoOffset } from "../oneko.ts";
export default class extends Aview {
running: boolean;
constructor() {
super();
this.setTitle("tetris (local match)");
setOnekoState("tetris");
this.running = true;
}
async getHTML() {
return `
<div id="window" class="absolute default-border">
<div id="window-header" class="bg-linear-to-r from-orange-200 to-orange-300 flex flex-row min-w-75 justify-between px-2">
<span class="font-[Kubasta]">tetris_game.ts</span>
<div>
<button> - </button>
<button> □ </button>
<a href="/" data-link> × </a>
</div>
</div>
<div id="main-div" class="bg-neutral-200 dark:bg-neutral-800 text-center p-10 space-y-4 reverse-border">
<div class="flex flex-row justify-center items-start space-x-4">
<canvas id="hold" class="reverse-border" width="140" height="100"></canvas>
<canvas id="board" class="reverse-border" width="300" height="600"></canvas>
<canvas id="queue" class="reverse-border" width="140" height="420"></canvas>
</div>
<div id="game-buttons" class="hidden flex mt-4">
<button id="game-retry" class="default-button w-full mx-4 py-2">play again</button>
<a id="game-back" class="default-button w-full mx-4 py-2" href="/tetris" data-link>back</a>
</div>
</div>
</div>
`;
}
async run() {
dragElement(document.getElementById("window"));
const COLS = 10;
const ROWS = 20;
const BLOCK = 30; // pixels per block
const view = this;
type Cell = number; // 0 empty, >0 occupied (color index)
// Tetromino definitions: each piece is an array of rotations, each rotation is a 2D matrix
const TETROMINOES: { [key: string]: number[][][] } = {
I: [
[
[0, 0, 0, 0],
[1, 1, 1, 1],
[0, 0, 0, 0],
[0, 0, 0, 0],
],
[
[0, 0, 1, 0],
[0, 0, 1, 0],
[0, 0, 1, 0],
[0, 0, 1, 0],
],
[
[0, 0, 0, 0],
[0, 0, 0, 0],
[1, 1, 1, 1],
[0, 0, 0, 0],
],
[
[0, 1, 0, 0],
[0, 1, 0, 0],
[0, 1, 0, 0],
[0, 1, 0, 0],
],
],
J: [
[
[2, 0, 0],
[2, 2, 2],
[0, 0, 0],
],
[
[0, 2, 2],
[0, 2, 0],
[0, 2, 0],
],
[
[0, 0, 0],
[2, 2, 2],
[0, 0, 2],
],
[
[0, 2, 0],
[0, 2, 0],
[2, 2, 0],
],
],
L: [
[
[0, 0, 3],
[3, 3, 3],
[0, 0, 0],
],
[
[0, 3, 0],
[0, 3, 0],
[0, 3, 3],
],
[
[0, 0, 0],
[3, 3, 3],
[3, 0, 0],
],
[
[3, 3, 0],
[0, 3, 0],
[0, 3, 0],
],
],
O: [
[
[4, 4],
[4, 4],
],
],
S: [
[
[0, 5, 5],
[5, 5, 0],
[0, 0, 0],
],
[
[0, 5, 0],
[0, 5, 5],
[0, 0, 5],
],
[
[0, 0, 0],
[0, 5, 5],
[5, 5, 0],
],
[
[5, 0, 0],
[5, 5, 0],
[0, 5, 0],
],
],
T: [
[
[0, 6, 0],
[6, 6, 6],
[0, 0, 0],
],
[
[0, 6, 0],
[0, 6, 6],
[0, 6, 0],
],
[
[0, 0, 0],
[6, 6, 6],
[0, 6, 0],
],
[
[0, 6, 0],
[6, 6, 0],
[0, 6, 0],
],
],
Z: [
[
[7, 7, 0],
[0, 7, 7],
[0, 0, 0],
],
[
[0, 0, 7],
[0, 7, 7],
[0, 7, 0],
],
[
[0, 0, 0],
[7, 7, 0],
[0, 7, 7],
],
[
[0, 7, 0],
[7, 7, 0],
[7, 0, 0],
],
],
};
const COLORS = [
[ "#000000", "#000000" ] , // placeholder for 0
[ "#00d2e1", "#0080a8" ], // I - cyan
[ "#0092e9", "#001fbf" ], // J - blue
[ "#e79700", "#c75700" ], // L - orange
[ "#d8c800", "#8f7700" ], // O - yellow
[ "#59e000", "#038b00" ], // S - green
[ "#de1fdf", "#870087" ], // T - purple
[ "#f06600", "#c10d07" ], // Z - red
];
class Piece {
shape: number[][];
rotations: number[][][];
rotationIndex: number;
x: number;
y: number;
colorIndex: number;
constructor(public type: string) {
this.rotations = TETROMINOES[type];
this.rotationIndex = 0;
this.shape = this.rotations[this.rotationIndex];
this.colorIndex = this.findColorIndex();
this.x = Math.floor((COLS - this.shape[0].length) / 2);
this.y = -2; //start on tiles 21 and 22
}
findColorIndex() {
for (const row of this.shape)
for (const v of row)
if (v)
return v;
return 1;
}
rotateCW() {
this.rotationIndex = (this.rotationIndex + 1) % this.rotations.length;
this.shape = this.rotations[this.rotationIndex];
}
rotateCCW() {
this.rotationIndex =
(this.rotationIndex - 1 + this.rotations.length) %
this.rotations.length;
this.shape = this.rotations[this.rotationIndex];
}
getCells(): { x: number; y: number; val: number }[] {
const cells: { x: number; y: number; val: number }[] = [];
for (let r = 0; r < this.shape.length; r++) {
for (let c = 0; c < this.shape[r].length; c++) {
const val = this.shape[r][c];
if (val) cells.push({ x: this.x + c, y: this.y + r, val });
}
}
return cells;
}
}
class Game {
board: Cell[][];
canvas: HTMLCanvasElement | null;
holdCanvas: HTMLCanvasElement | null;
queueCanvas: HTMLCanvasElement | null;
ctx: CanvasRenderingContext2D | null;
holdCtx: CanvasRenderingContext2D | null;
queueCtx: CanvasRenderingContext2D | null;
piece: Piece | null = null;
holdPiece: Piece | null = null;
canHold: boolean = true;
nextQueue: string[] = [];
score: number = 0;
level: number = 1;
lines: number = 0;
dropInterval: number = 1000;
lastDrop: number = 0;
isLocking: boolean = false;
lockRotationCount: number = 0;
lockLastRotationCount: number = 0;
isGameOver: boolean = false;
isPaused: boolean = false;
constructor(canvasId: string) {
const el = document.getElementById(
canvasId,
) as HTMLCanvasElement | null;
this.canvas = el;
if (!this.canvas)
throw console.error("no canvas :c");
this.canvas.width = COLS * BLOCK;
this.canvas.height = ROWS * BLOCK;
const ctx = this.canvas.getContext("2d");
this.ctx = ctx;
if (!this.ctx)
throw console.error("no ctx D:");
this.holdCanvas = document.getElementById("hold") as HTMLCanvasElement;
this.queueCanvas = document.getElementById("queue") as HTMLCanvasElement;
if (!this.holdCanvas || !this.queueCanvas)
throw console.error("no canvas :c");
this.holdCtx = this.holdCanvas.getContext("2d");
this.queueCtx = this.queueCanvas.getContext("2d");
if (!this.holdCtx || !this.queueCtx)
return;
this.holdCtx.clearRect(0, 0, 200, 200);
this.queueCtx.clearRect(0, 0, 500, 500);
this.board = this.createEmptyBoard();
this.fillBag();
this.spawnPiece();
this.registerListeners();
requestAnimationFrame(this.loop.bind(this));
}
createEmptyBoard(): Cell[][] {
const b: Cell[][] = [];
for (let r = 0; r < ROWS; r++) {
const row: Cell[] = new Array(COLS).fill(0);
b.push(row);
}
return b;
}
fillBag() {
// classic 7-bag randomizer
const pieces = Object.keys(TETROMINOES);
const bag = [...pieces];
for (let i = bag.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[bag[i], bag[j]] = [bag[j], bag[i]];
}
this.nextQueue.push(...bag);
}
hold() {
if (!this.canHold) return;
[this.piece, this.holdPiece] = [this.holdPiece, this.piece];
if (!this.piece) this.spawnPiece();
if (!this.piece) return;
this.piece.x = Math.floor((COLS - this.piece.shape[0].length) / 2);
this.piece.y = -2;
this.piece.rotationIndex = 0;
this.piece.shape = this.piece.rotations[this.piece.rotationIndex];
this.canHold = false;
this.drawHold();
}
spawnPiece() {
this.canHold = true;
if (this.nextQueue.length < 7) this.fillBag();
const type = this.nextQueue.shift()!;
this.piece = new Piece(type);
if (this.collides(this.piece)) {
this.isGameOver = true;
}
this.drawHold();
this.drawQueue();
}
collides(piece: Piece): boolean {
for (const cell of piece.getCells()) {
if (cell.y >= ROWS) return true;
if (cell.x < 0 || cell.x >= COLS) return true;
if (cell.y >= 0 && this.board[cell.y][cell.x]) return true;
}
return false;
}
getGhostOffset(piece: Piece): number {
let y: number = 0;
while (true) {
for (const cell of piece.getCells()) {
if (
cell.y + y >= ROWS ||
(cell.y + y >= 0 && this.board[cell.y + y][cell.x])
)
return y - 1;
}
y++;
}
}
lockPiece() {
if (!this.piece) return;
this.isLocking = false;
let isValid: boolean = false;
for (const cell of this.piece.getCells()) {
if (cell.y >= 0 && cell.y < ROWS && cell.x >= 0 && cell.x < COLS)
this.board[cell.y][cell.x] = cell.val;
if (cell.y > 0) isValid = true;
}
if (!isValid) this.isGameOver = true;
this.clearLines();
this.spawnPiece();
}
clearLines() {
let linesCleared = 0;
outer: for (let r = ROWS - 1; r >= 0; r--) {
for (let c = 0; c < COLS; c++) if (!this.board[r][c]) continue outer;
this.board.splice(r, 1);
this.board.unshift(new Array(COLS).fill(0));
linesCleared++;
r++;
}
if (linesCleared > 0) {
this.lines += linesCleared;
const points = [0, 40, 100, 300, 1200];
this.score += (points[linesCleared] || 0) * this.level;
// level up every 10 lines (Fixed Goal System)
const newLevel = Math.floor(this.lines / 10) + 1;
if (newLevel > this.level) {
this.level = newLevel;
this.dropInterval = Math.max(100, 1000 - (this.level - 1) * 75);
}
}
}
rotatePiece(dir: "cw" | "ccw") {
if (!this.piece) return;
if (this.isLocking && this.lockRotationCount < 15)
this.lockRotationCount++;
// Try wall kicks
const originalIndex = this.piece.rotationIndex;
if (dir === "cw") this.piece.rotateCW();
else this.piece.rotateCCW();
const kicks = [0, -1, 1, -2, 2];
for (const k of kicks) {
this.piece.x += k;
if (!this.collides(this.piece)) return;
this.piece.x -= k;
}
// no kick, revert
this.piece.rotationIndex = originalIndex;
this.piece.shape = this.piece.rotations[originalIndex];
}
movePiece(dx: number, dy: number) {
if (!this.piece) return;
this.piece.x += dx;
this.piece.y += dy;
if (this.collides(this.piece)) {
this.piece.x -= dx;
this.piece.y -= dy;
return false;
}
return true;
}
hardDrop() {
if (!this.piece) return;
let dropped = 0;
while (this.movePiece(0, 1)) dropped++;
this.score += dropped * 2;
this.lockPiece();
}
softDrop() {
if (!this.piece) return;
if (!this.movePiece(0, 1)) return;
else this.score += 1;
}
keys: Record<string, boolean> = {};
direction: number = 0;
inputDelay = 200;
inputTimestamp = Date.now();
move: boolean = false;
inputManager() {
if (this.move || Date.now() > this.inputTimestamp + this.inputDelay)
{
if (this.keys["ArrowLeft"] && !this.keys["ArrowRight"])
this.movePiece(-1, 0);
else if (!this.keys["ArrowLeft"] && this.keys["ArrowRight"])
this.movePiece(1, 0);
else if (this.keys["ArrowLeft"] && this.keys["ArrowRight"])
this.movePiece(this.direction, 0);
this.move = false;
}
}
removeListeners() {
window.removeEventListener("keydown", (e) => {
this.keys[e.key] = true;
if (this.isGameOver) return;
if (e.key === "p" || e.key === "P" || e.key === "Escape")
this.isPaused = !this.isPaused;
if (this.isPaused) return;
if (e.key === "ArrowLeft")
{
this.inputTimestamp = Date.now();
this.direction = -1;//this.movePiece(-1, 0);
this.move = true;
}
else if (e.key === "ArrowRight")
{
this.inputTimestamp = Date.now();
this.direction = 1;//this.movePiece(1, 0);
this.move = true;
}
else if (e.key === "ArrowDown") this.softDrop();
else if (e.code === "Space") {
e.preventDefault();
this.hardDrop();
} else if (e.key === "Shift" || e.key === "c" || e.key === "C") {
e.preventDefault();
this.hold();
} else if (e.key === "x" || e.key === "X" || e.key === "ArrowUp") {
e.preventDefault();
this.rotatePiece("cw");
} else if (e.key === "z" || e.key === "Z" || e.key === "Control") {
e.preventDefault();
this.rotatePiece("ccw");
}
});
document.removeEventListener("keyup", (e) => {
this.keys[e.key] = false;
});
}
registerListeners() {
window.addEventListener("keydown", (e) => {
this.keys[e.key] = true;
if (this.isGameOver) return;
if (e.key === "p" || e.key === "P" || e.key === "Escape")
this.isPaused = !this.isPaused;
if (this.isPaused) return;
if (e.key === "ArrowLeft")
{
this.inputTimestamp = Date.now();
this.direction = -1;//this.movePiece(-1, 0);
this.move = true;
}
else if (e.key === "ArrowRight")
{
this.inputTimestamp = Date.now();
this.direction = 1;//this.movePiece(1, 0);
this.move = true;
}
else if (e.key === "ArrowDown") this.softDrop();
else if (e.code === "Space") {
//e.preventDefault();
this.hardDrop();
} else if (e.key === "Shift" || e.key === "c" || e.key === "C") {
this.hold();
} else if (e.key === "x" || e.key === "X" || e.key === "ArrowUp") {
//e.preventDefault();
this.rotatePiece("cw");
} else if (e.key === "z" || e.key === "Z" || e.key === "Control") {
this.rotatePiece("ccw");
}
});
document.addEventListener("keyup", (e) => {
this.keys[e.key] = false;
});
}
async loop(timestamp: number) {
if (!view.running) return this.removeListeners();
if (!this.lastDrop) this.lastDrop = timestamp;
if (!this.isPaused)
{
this.inputManager();
if (this.isLocking ? timestamp - this.lastDrop > 500 : timestamp - this.lastDrop > this.dropInterval)
{
if (this.isLocking && this.lockRotationCount == this.lockLastRotationCount)
this.lockPiece();
this.lockLastRotationCount = this.lockRotationCount;
if (!this.movePiece(0, 1))
{
if (!this.isLocking)
{
this.lockRotationCount = 0;
this.lockLastRotationCount = 0;
this.isLocking = true;
}
}
else if (this.isLocking)
this.lockRotationCount = 0;
this.lastDrop = timestamp;
}
}
this.draw();
if (this.isGameOver)
{
if (await isLogged())
{
let uuid = document.cookie.match(new RegExp('(^| )' + "uuid" + '=([^;]+)'))[2];
fetch(`http://localhost:3002/users/${uuid}/matchHistory?game=tetris`, {
method: "POST",
headers: { "Content-Type": "application/json", },
credentials: "include",
body: JSON.stringify({
"game": "tetris",
"myScore": this.score,
"date": Date.now(),
}),
});
}
document.getElementById("game-buttons")?.classList.remove("hidden");
return ;
}
requestAnimationFrame(this.loop.bind(this));
}
drawGrid() {
const ctx = this.ctx;
if (!ctx || !this.canvas)
return;
ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
ctx.strokeStyle = window.matchMedia('(prefers-color-scheme: dark)').matches ? "oklch(14.5% 0 0)" : "oklch(55.6% 0 0)";
for (let r = 0; r <= ROWS; r++) {
// horizontal lines
ctx.beginPath();
ctx.moveTo(0, r * BLOCK);
ctx.lineTo(COLS * BLOCK, r * BLOCK);
ctx.stroke();
}
for (let c = 0; c <= COLS; c++) {
ctx.beginPath();
ctx.moveTo(c * BLOCK, 0);
ctx.lineTo(c * BLOCK, ROWS * BLOCK);
ctx.stroke();
}
}
drawBoard() {
this.drawGrid();
for (let r = 0; r < ROWS; r++) {
for (let c = 0; c < COLS; c++) {
const val = this.board[r][c];
if (val) this.fillBlock(c, r, COLORS[val], this.ctx);
else this.clearBlock(c, r);
}
}
}
drawPiece() {
if (!this.piece) return;
for (const cell of this.piece.getCells())
if (cell.y >= 0) this.fillBlock(cell.x, cell.y, COLORS[cell.val], this.ctx);
let offset: number = this.getGhostOffset(this.piece);
for (const cell of this.piece.getCells())
if (cell.y + offset >= 0 && offset > 0)
this.fillGhostBlock(cell.x, cell.y + offset, COLORS[cell.val]);
}
drawHold() {
if (!this.holdCtx || !this.holdCanvas) return;
this.holdCtx.fillStyle = window.matchMedia('(prefers-color-scheme: dark)').matches ? "oklch(20.5% 0 0)" : "oklch(70.8% 0 0)";
this.holdCtx.fillRect(0, 0, this.holdCanvas.width, this.holdCanvas.height);
if (!this.holdPiece) return;
let y: number = 0;
for (const row of this.holdPiece.rotations[0]) {
let x: number = 0;
for (const val of row) {
if (val)
this.fillBlock(x + (4 - this.holdPiece.rotations[0].length)/ 2 + 0.35, y + 0.65, this.canHold ? COLORS[this.holdPiece.findColorIndex()] : ["#8c8c84", "#393934"], this.holdCtx);
x++;
}
y++;
}
}
drawQueue() {
if (!this.queueCtx || !this.queueCanvas) return ;
this.queueCtx.fillStyle = window.matchMedia('(prefers-color-scheme: dark)').matches ? "oklch(20.5% 0 0)" : "oklch(70.8% 0 0)";
this.queueCtx.fillRect(0, 0, this.queueCanvas.width, this.queueCanvas.height);
let placement: number = 0;
for (const nextPiece of this.nextQueue.slice(0, 5)) {
let y: number = 0;
for (const row of TETROMINOES[nextPiece][0]) {
let x: number = 0;
for (const val of row) {
if (val)
this.fillBlock(x + (4 - TETROMINOES[nextPiece][0].length) / 2 + 0.25, y + 0.5 + placement * 2.69 - (nextPiece ==="I" ? 0.35 : 0), COLORS[["I", "J", "L", "O", "S", "T", "Z"].indexOf(nextPiece) + 1], this.queueCtx);
x++;
}
y++;
}
placement++;
}
}
adjustColor(hex: string, amount: number): string {
let color = hex.startsWith('#') ? hex.slice(1) : hex;
const num = parseInt(color, 16);
let r = (num >> 16) + amount;
let g = ((num >> 8) & 0x00FF) + amount;
let b = (num & 0x0000FF) + amount;
r = Math.max(Math.min(255, r), 0);
g = Math.max(Math.min(255, g), 0);
b = Math.max(Math.min(255, b), 0);
return `#${(r << 16 | g << 8 | b).toString(16).padStart(6, '0')}`;
}
fillBlock(x: number, y: number, color: string[], ctx: CanvasRenderingContext2D | null) {
if (!ctx) return;
const grad = ctx.createLinearGradient(x * BLOCK, y * BLOCK, x * BLOCK, y * BLOCK + BLOCK);
grad.addColorStop(0, color[0]);
grad.addColorStop(1, color[1]);
ctx.fillStyle = grad;
ctx.fillRect(Math.round(x * BLOCK) + 4, Math.round(y * BLOCK) + 4, BLOCK - 4, BLOCK - 4);
const X = Math.round(x * BLOCK);
const Y = Math.round(y * BLOCK);
const W = BLOCK;
const H = BLOCK;
const S = 4;
ctx.lineWidth = S;
ctx.beginPath();
ctx.strokeStyle = color[0];
ctx.moveTo(X, Y + S / 2);
ctx.lineTo(X + W, Y + S / 2);
ctx.moveTo(X + S / 2, Y);
ctx.lineTo(X + S / 2, Y + H);
ctx.stroke();
ctx.beginPath();
ctx.strokeStyle = this.adjustColor(color[1], -20);
ctx.moveTo(X, Y + H - S / 2);
ctx.lineTo(X + W, Y + H - S / 2);
ctx.moveTo(X + W - S / 2, Y);
ctx.lineTo(X + W - S / 2, Y + H);
ctx.stroke();
}
fillGhostBlock(x: number, y: number, color: string[]) {
if (!this.ctx) return;
const ctx = this.ctx;
const X = x * BLOCK;
const Y = y * BLOCK;
const W = BLOCK;
const H = BLOCK;
const S = 4;
ctx.lineWidth = S;
ctx.beginPath();
ctx.strokeStyle = this.adjustColor(color[0], -40);
ctx.moveTo(X, Y + S / 2);
ctx.lineTo(X + W, Y + S / 2);
ctx.moveTo(X + S / 2, Y);
ctx.lineTo(X + S / 2, Y + H);
ctx.stroke();
ctx.beginPath();
ctx.strokeStyle = this.adjustColor(color[1], -60);
ctx.moveTo(X, Y + H - S / 2);
ctx.lineTo(X + W, Y + H - S / 2);
ctx.moveTo(X + W - S / 2, Y);
ctx.lineTo(X + W - S / 2, Y + H);
ctx.stroke();
//ctx.strokeRect(x * BLOCK + 1, y * BLOCK + 1, BLOCK - 2, BLOCK - 2);
}
clearBlock(x: number, y: number) {
if (!this.ctx) return;
const ctx = this.ctx;
ctx.fillStyle = window.matchMedia('(prefers-color-scheme: dark)').matches ? "oklch(20.5% 0 0)" : "oklch(70.8% 0 0)";
ctx.fillRect(x * BLOCK + 1, y * BLOCK + 1, BLOCK - 2, BLOCK - 2);
}
drawHUD() {
if (!this.ctx || !this.canvas) return;
const ctx = this.ctx;
ctx.fillStyle = "rgba(0,0,0,0.6)";
ctx.fillRect(4, 4, 120, 60);
ctx.fillStyle = "#fff";
ctx.font = "12px Kubasta";
ctx.fillText(`score: ${this.score}`, 8, 20);
ctx.fillText(`lines: ${this.lines}`, 8, 36);
ctx.fillText(`level: ${this.level}`, 8, 52);
if (this.isPaused) {
ctx.fillStyle = "rgba(0,0,0,0.7)";
ctx.fillRect(0, this.canvas.height / 2 - 24, this.canvas.width, 48);
ctx.fillStyle = "#fff";
ctx.font = "24px Kubasta";
ctx.textAlign = "center";
ctx.fillText(
"paused",
this.canvas.width / 2,
this.canvas.height / 2 + 8,
);
ctx.textAlign = "start";
}
if (this.isGameOver) {
ctx.fillStyle = "rgba(0,0,0,0.7)";
ctx.fillRect(0, this.canvas.height / 2 - 36, this.canvas.width, 72);
ctx.fillStyle = "#fff";
ctx.font = "28px Kubasta";
ctx.textAlign = "center";
ctx.fillText(
"game over",
this.canvas.width / 2,
this.canvas.height / 2 + 8,
);
ctx.textAlign = "start";
}
}
draw() {
if (!this.ctx || !this.canvas) return;
// clear everything
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
this.ctx.fillStyle = "#000";
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
this.ctx.strokeStyle = "#111";
for (let r = 0; r <= ROWS; r++) {
this.ctx.beginPath();
this.ctx.moveTo(0, r * BLOCK);
this.ctx.lineTo(COLS * BLOCK, r * BLOCK);
this.ctx.stroke();
}
for (let c = 0; c <= COLS; c++) {
this.ctx.beginPath();
this.ctx.moveTo(c * BLOCK, 0);
this.ctx.lineTo(c * BLOCK, ROWS * BLOCK);
this.ctx.stroke();
}
this.drawBoard();
this.drawPiece();
this.drawHUD();
this.drawQueue();
}
}
document.getElementById("game-retry")?.addEventListener("click", () => { document.getElementById("game-buttons")?.classList.add("hidden"); const game = new Game("board"); });
const game = new Game("board");
}
}