diff --git a/src/front/static/ts/main.ts b/src/front/static/ts/main.ts index f3005fe..b18641f 100644 --- a/src/front/static/ts/main.ts +++ b/src/front/static/ts/main.ts @@ -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") }, diff --git a/src/front/static/ts/views/Game.ts b/src/front/static/ts/views/Game.ts index 5e29d7b..07d72b8 100644 --- a/src/front/static/ts/views/Game.ts +++ b/src/front/static/ts/views/Game.ts @@ -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 diff --git a/src/front/static/ts/views/LoginPage.ts b/src/front/static/ts/views/LoginPage.ts index 0311473..0ed05c1 100644 --- a/src/front/static/ts/views/LoginPage.ts +++ b/src/front/static/ts/views/LoginPage.ts @@ -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 {

welcome back ! please login.

- -
+ +
diff --git a/src/front/static/ts/views/Profile.ts b/src/front/static/ts/views/Profile.ts index fe4f4c2..2f2f899 100644 --- a/src/front/static/ts/views/Profile.ts +++ b/src/front/static/ts/views/Profile.ts @@ -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", }); diff --git a/src/front/static/ts/views/RegisterPage.ts b/src/front/static/ts/views/RegisterPage.ts index 7e374a5..30618f1 100644 --- a/src/front/static/ts/views/RegisterPage.ts +++ b/src/front/static/ts/views/RegisterPage.ts @@ -15,18 +15,18 @@ export default class extends Aview { async getHTML() { return `
-
- register.ts -
- - - × -
-
+
+ register.ts +
+ + + × +
+
-

welcome ! please register.

+

welcome ! please register.

diff --git a/src/front/static/ts/views/Tetris.ts b/src/front/static/ts/views/Tetris.ts index 2b84fa3..bdf9ede 100644 --- a/src/front/static/ts/views/Tetris.ts +++ b/src/front/static/ts/views/Tetris.ts @@ -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 ; } diff --git a/src/front/static/ts/views/TetrisVersus.ts b/src/front/static/ts/views/TetrisVersus.ts new file mode 100644 index 0000000..bdc4426 --- /dev/null +++ b/src/front/static/ts/views/TetrisVersus.ts @@ -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 ` +
+
+ tetris_game.ts +
+ + + × +
+
+ + +
+
+ + + + + + +
+ +
+
+ `; + } + + 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 = {}; + 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); + } +} diff --git a/src/front/static/ts/views/TournamentMenu.ts b/src/front/static/ts/views/TournamentMenu.ts index 1f798ae..bc2bbd6 100644 --- a/src/front/static/ts/views/TournamentMenu.ts +++ b/src/front/static/ts/views/TournamentMenu.ts @@ -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(""); }