」 feat(front): added tetris front and some minor fixes

This commit is contained in:
y-syo
2025-10-20 16:18:48 +02:00
parent 8a9224bd88
commit b5a91f0bab
8 changed files with 938 additions and 32 deletions

View File

@ -35,7 +35,7 @@ const routes = [
{ path: "/tetris", view: () => import("./views/TetrisMenu.ts") },
{ path: "/tetris/solo", view: () => import("./views/Tetris.ts") },
{ path: "/tetris/versus", view: () => import("./views/Tetris.ts") },
{ path: "/tetris/versus", view: () => import("./views/TetrisVersus.ts") },
{ path: "/login", view: () => import("./views/LoginPage.ts") },
{ path: "/register", view: () => import("./views/RegisterPage.ts") },

View File

@ -147,18 +147,19 @@ export default class extends Aview {
if (await isLogged())
{
let uuid = document.cookie.match(new RegExp('(^| )' + "uuid" + '=([^;]+)'))[2];
fetch(`http://localhost:3002/users/${uuid}/matchHistory`, {
fetch(`http://localhost:3002/users/${uuid}/matchHistory?game=pong`, {
method: "POST",
headers: { "Content-Type": "application/json", },
credentials: "include",
body: JSON.stringify({ "opponent": p2_name, "myScore": p1_score, "opponentScore": p2_score })
body: JSON.stringify({
"game": "pong",
"opponent": p2_name,
"myScore": p1_score,
"opponentScore": p2_score,
"date": Date.now(),
}),
});
}
// ------------------------------------------------------------------------------------------------------------------------------------------
//
// insert the fetch to the ScoreStore api here
//
// ------------------------------------------------------------------------------------------------------------------------------------------
match_over = true;
}
else

View File

@ -1,5 +1,5 @@
import Aview from "./Aview.ts"
import { dragElement } from "./drag.js"
import { dragElement } from "./drag.ts"
import { setOnekoState } from "../oneko.ts"
import { isLogged, navigationManager } from "../main.ts"
@ -29,11 +29,11 @@ export default class extends Aview {
<h1 class="text-gray-900 dark:text-white text-lg pt-0 pb-4">welcome back ! please login.</h1>
<input type="text" id="username" placeholder="username" class="bg-white text-neutral-900 px-4 py-2 input-border" required></input>
<input type="password" id="password" placeholder="password" class="bg-white text-neutral-900 px-4 py-2 input-border" required></input>
<p id="login-error-message" class="hidden text-red-700 dark:text-red-500"></p>
</br>
<button id="login-button" type="submit" class="default-button w-full">login</button>
</form>
<p id="login-error-message" class="hidden text-red-700 dark:text-red-500"></p>
<hr class="my-4 w-64 reverse-border">
<div class="flex flex-col space-y-4 w-full">

View File

@ -58,13 +58,13 @@ export default class extends Aview {
}
let userdata = await userdata_req.json();
const matchCount_req = await fetch(`http://localhost:3002/users/${uuid}/matchHistory/count`, {
const matchCount_req = await fetch(`http://localhost:3002/users/${uuid}/matchHistory/count?game=tetris`, {
method: "GET",
credentials: "include",
});
const matchCount = await matchCount_req.json();
const matches_req = await fetch(`http://localhost:3002/users/${uuid}/matchHistory?iStart=0&iEnd=${matchCount.n_matches}`, {
const matches_req = await fetch(`http://localhost:3002/users/${uuid}/matchHistory?game=tetris&iStart=0&iEnd=${matchCount.n_matches}`, {
method: "GET",
credentials: "include",
});

View File

@ -15,18 +15,18 @@ export default class extends Aview {
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]">register.ts</span>
<div>
<button> - </button>
<button> □ </button>
<a href="/" data-link> × </a>
</div>
</div>
<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]">register.ts</span>
<div>
<button> - </button>
<button> □ </button>
<a href="/" data-link> × </a>
</div>
</div>
<div class="bg-neutral-200 dark:bg-neutral-800 text-center pb-10 pt-5 px-10 reverse-border flex flex-col items-center">
<form method="dialog" class="space-y-4">
<p class="text-gray-900 dark:text-white text-lg pt-0 pb-4">welcome ! please register.</p>
<h1 class="text-gray-900 dark:text-white text-lg pt-0 pb-4">welcome ! please register.</h1>
<input type="text" id="username" placeholder="username" class="bg-white text-neutral-900 px-4 py-2 input-border" required></input>
<input type="password" id="password" placeholder="password" class="bg-white text-neutral-900 px-4 py-2 input-border" required></input>
<button id="register-button" type="submit" class="default-button w-full">register</button>

View File

@ -1,6 +1,7 @@
import Aview from "./Aview.ts";
import { isLogged } from "../main.js";
import { dragElement } from "./drag.js";
import { setOnekoState, setBallPos, setOnekoOffset } from "../oneko.ts"
import { setOnekoState, setBallPos, setOnekoOffset } from "../oneko.ts";
export default class extends Aview {
running: boolean;
@ -538,12 +539,12 @@ export default class extends Aview {
else if (e.code === "Space") {
e.preventDefault();
this.hardDrop();
} else if (e.code === "ShiftLeft") {
} 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")
this.rotatePiece("cw");
else if (e.key === "z" || e.key === "Z" || e.code === "ControlLeft")
else if (e.key === "z" || e.key === "Z" || e.key === "Control")
this.rotatePiece("ccw");
});
@ -552,7 +553,7 @@ export default class extends Aview {
});
}
loop(timestamp: number) {
async loop(timestamp: number) {
if (!this.lastDrop) this.lastDrop = timestamp;
if (!this.isPaused)
{
@ -580,6 +581,22 @@ export default class extends Aview {
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",
"opponent": "xd",
"myScore": this.score,
"opponentScore": 0,
"date": Date.now(),
}),
});
}
document.getElementById("game-buttons")?.classList.remove("hidden");
return ;
}

View File

@ -0,0 +1,888 @@
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="board1-hold" class="reverse-border" width="140" height="100"></canvas>
<canvas id="board1-board" class="reverse-border" width="300" height="600"></canvas>
<canvas id="board1-queue" class="reverse-border" width="140" height="420"></canvas>
<canvas id="board2-hold" class="reverse-border" width="140" height="100"></canvas>
<canvas id="board2-board" class="reverse-border" width="300" height="600"></canvas>
<canvas id="board2-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() {
let game1: Game;
let game2: Game;
dragElement(document.getElementById("window"));
const COLS = 10;
const ROWS = 20;
const BLOCK = 30; // pixels per block
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
[ "#8c8c84", "#393934" ], // garbage - gray
];
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;
garbage: number = 0;
dropInterval: number = 1000;
lastDrop: number = 0;
isLocking: boolean = false;
lockRotationCount: number = 0;
lockLastRotationCount: number = 0;
isGameOver: boolean = false;
isPaused: boolean = false;
id: number;
constructor(canvasId: string, id: number) {
this.id = id;
const el = document.getElementById(
canvasId + "-board",
) 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(canvasId + "-hold") as HTMLCanvasElement;
this.queueCanvas = document.getElementById(canvasId + "-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();
if (id == 0)
this.fillBag();
else
this.nextQueue = game1.nextQueue;
this.spawnPiece();
if (id != 0)
this.piece.type = game1.piece.type;
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;
if (this.garbage)
{
const empty_spot = Math.floor(Math.random() * 10);
while (this.garbage)
{
//if () // if anything else than 0 on top, die >:3
this.board.shift();
this.board.push(Array(COLS).fill(8));
this.board[19][empty_spot] = 0;
this.garbage--;
}
}
this.clearLines();
this.spawnPiece();
}
addGarbage(lines: number) {
this.garbage += lines;
}
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);
}
if (this.garbage)
{
while (linesCleared)
{
this.garbage--;
linesCleared--;
if (!this.garbage)
break;
}
}
if (this.id == 0 && linesCleared)
game2.addGarbage(linesCleared < 4 ? linesCleared - 1 : linesCleared);
else
game1.addGarbage(linesCleared < 4 ? linesCleared - 1 : linesCleared);
}
}
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() {
const left = this.id === 0 ? this.keys["KeyA"] : this.keys["Numpad4"]
const right = this.id === 0 ? this.keys["KeyD"] : this.keys["Numpad6"]
if (this.move || Date.now() > this.inputTimestamp + this.inputDelay)
{
if (left && !right)
this.movePiece(-1, 0);
else if (!left && right)
this.movePiece(1, 0);
else if (left && right)
this.movePiece(this.direction, 0);
this.move = false;
}
/*if (this.keys["ArrowDown"])
this.softDrop();*/
}
registerListeners() {
window.addEventListener("keydown", (e) => {
this.keys[e.code] = true;
if (this.isGameOver) return;
if (e.key === "p" || e.key === "P" || e.key === "Escape")
this.isPaused = !this.isPaused;
if (this.isPaused) return;
if (this.id === 0 ? e.code === "KeyA" : e.code === "Numpad4")
{
this.inputTimestamp = Date.now();
this.direction = -1;//this.movePiece(-1, 0);
this.move = true;
}
else if (this.id === 0 ? e.code === "KeyD" : e.code === "Numpad6")
{
this.inputTimestamp = Date.now();
this.direction = 1;//this.movePiece(1, 0);
this.move = true;
}
else if (this.id === 0 ? e.code === "KeyS" : e.code === "Numpad5") this.softDrop();
else if (this.id === 0 ? e.code === "Space" : e.code === "Numpad0") {
e.preventDefault();
this.hardDrop();
} else if (this.id === 0 ? e.code === "ShiftLeft" : e.code === "NumpadEnter") {
e.preventDefault();
this.hold();
} else if (this.id === 0 ? e.code === "KeyE" : e.code === "Numpad9")
this.rotatePiece("cw");
else if (this.id === 0 ? e.code === "KeyQ" : e.code === "Numpad7")
this.rotatePiece("ccw");
});
document.addEventListener("keyup", (e) => {
this.keys[e.code] = false;
});
}
async loop(timestamp: number) {
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",
"opponent": "xd",
"myScore": this.score,
"opponentScore": 0,
"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 = "#222";
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() {
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.holdPiece || !this.holdCtx) return;
this.holdCtx.clearRect(0, 0, 200, 200);
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.5, this.canHold ? COLORS[this.holdPiece.findColorIndex()] : COLORS[8], this.holdCtx);
x++;
}
y++;
}
}
drawQueue() {
if (!this.queueCtx) return ;
this.queueCtx.clearRect(0, 0, 500, 500);
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.clearRect(x * BLOCK + 1, y * BLOCK + 1, BLOCK - 2, BLOCK - 2);
}
drawHUD() {
if (!this.ctx || !this.canvas) return;
const ctx = this.ctx;
if (this.garbage)
{
ctx.fillStyle ="red";
ctx.fillRect(0, this.canvas.height - BLOCK * this.garbage, 6, BLOCK * this.garbage);
}
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"); game1 = new Game("board1", 0); game2 = new Game("board2", 1); });
game1 = new Game("board1", 0);
game2 = new Game("board2", 1);
}
}

View File

@ -26,7 +26,7 @@ export default class extends Aview {
async run() {
const generateBracket = async (playerCount: number) => {
document.getElementById("bracket").innerHTML = "";
const rounds = Math.ceil(Math.log2(playerCount));
const totalSlots = 2 ** rounds;
const byes = totalSlots - playerCount;
@ -34,10 +34,10 @@ export default class extends Aview {
let odd = 0;
if (playerCount % 2)
{
console.error("odd numbers are temporarily invalids");
return ;
/*++odd;
--playerCount;*/
//console.error("odd numbers are temporarily invalids");
//return ;
++odd;
--playerCount;
}
let notPowPlayersCount = 0;
@ -106,7 +106,7 @@ export default class extends Aview {
input.placeholder = `Player ${++playerCount}`;
input.className =
"w-32 h-10 p-2 text-sm border rounded bg-white shadow disabled:bg-gray-200";
roundColumn.appendChild(input);
roundColumn.appendChild(input);
--notPowPlayersCount;
nextRound.push("");
}