From 4e7a9fdee7248f7691a69198822a67ae67e564c5 Mon Sep 17 00:00:00 2001 From: bitsearch Date: Thu, 19 Feb 2026 14:18:27 +0100 Subject: [PATCH] modulaire tetris --- Transcendence/README.md | 52 +- Transcendence/srcs/frontend/src/pieces.js | 99 +++ Transcendence/srcs/frontend/src/renderer.js | 116 +++ Transcendence/srcs/frontend/src/tetris.css | 243 ++++++ Transcendence/srcs/frontend/src/tetris.html | 828 +------------------- Transcendence/srcs/frontend/src/tetris.js | 350 +++++++++ Transcendence/srcs/frontend/src/ui.js | 76 ++ 7 files changed, 958 insertions(+), 806 deletions(-) create mode 100644 Transcendence/srcs/frontend/src/pieces.js create mode 100644 Transcendence/srcs/frontend/src/renderer.js create mode 100644 Transcendence/srcs/frontend/src/tetris.css create mode 100644 Transcendence/srcs/frontend/src/tetris.js create mode 100644 Transcendence/srcs/frontend/src/ui.js diff --git a/Transcendence/README.md b/Transcendence/README.md index ab014cb..19d47eb 100644 --- a/Transcendence/README.md +++ b/Transcendence/README.md @@ -51,4 +51,54 @@ DATABASE - historique de la game, qui a dessine quoi precedemment ainsi que les timers des rounds, sera aussi utile si on veut faire les stats de compte a l'avenir. - contient la liste des mots utilisable par les joueurs -21/01 Ajout de avatar_url dans la table users \ No newline at end of file +21/01 Ajout de avatar_url dans la table users + +///////////////////////////////////////////////////////////////////////////////////////// + +TETRIS + +Feuille de route + - Ajout du jeu Tetris au projet Transcendence + - Bouton Tetris qui redirige vers une page dédiée (tetris.html) + - Architecture modulaire : le monolithe HTML a été découpé en 5 fichiers séparés : + tetris.html — structure de la page + tetris.css — styles (thème cyberpunk) + pieces.js — définition des 7 pièces Tetris et leurs rotations + tetris.js — logique du jeu (classe Tetris) + renderer.js — rendu canvas (grille, hold, next) + ui.js — glue UI : boutons, overlay, liaison game ↔ DOM + +Architecture — classe Tetris (tetris.js) + Constructeur + new Tetris(onRender, onGameOver) + onRender : callback appelé à chaque frame pour redessiner + onGameOver : callback appelé avec le score final + + Méthodes publiques + start() — initialise et lance une partie + stop() — arrête la partie en cours + pause() — bascule pause / reprise + configure({ timeToDown, — modifie les paramètres de difficulté + hardening, (efficace uniquement avant start() + decrementTTD }) pour timeToDown) + + Paramètres de difficulté (configurables via le panneau Settings) + timeToDown (ms, défaut 1000) — intervalle entre deux descentes automatiques. + Plus la valeur est petite, plus le jeu est rapide. + hardening (pts, défaut 1000) — nombre de points de score cumulés avant chaque + accélération. Augmenter = progression plus lente. + decrementTTD (ms, défaut 100) — réduction de timeToDown à chaque palier atteint. + Augmenter = accélération plus brutale. + + Flux de jeu + spawn → tick (toutes les timeToDown ms) → moveDown ou lockPiece + → verifierLignes (score + lignes) → _makeHarder → spawn suivant + → game over si la pièce spawne dans une case occupée + +Contrôles clavier + ← → Déplacer la pièce + ↓ Descente douce (+1 pt) + Espace Hard drop (+2 pts par case) + Q Rotation gauche + W Rotation droite + C Hold (stocker / échanger la pièce courante) \ No newline at end of file diff --git a/Transcendence/srcs/frontend/src/pieces.js b/Transcendence/srcs/frontend/src/pieces.js new file mode 100644 index 0000000..f541825 --- /dev/null +++ b/Transcendence/srcs/frontend/src/pieces.js @@ -0,0 +1,99 @@ +// ───────────────────────────────────────────── +// PIÈCES +// ───────────────────────────────────────────── + +class Piece { + constructor(startX, startY) { + this.position = { x: startX, y: startY }; + this.currentRotation = 0; + this.rotations = this.defineRotations(); + this.shape = this.rotations[0]; + this.color = this.getColor(); + } + defineRotations() { return [[[1]]]; } + getColor() { return 1; } + getPosition() { return { ...this.position }; } + getShape() { return this.shape; } + moveDown() { this.position.y++; } + moveLeft() { this.position.x--; } + moveRight() { this.position.x++; } + rotateLeft() { + this.currentRotation = (this.currentRotation - 1 + this.rotations.length) % this.rotations.length; + this.shape = this.rotations[this.currentRotation]; + } + rotateRight() { + this.currentRotation = (this.currentRotation + 1) % this.rotations.length; + this.shape = this.rotations[this.currentRotation]; + } +} + +class PieceT extends Piece { + defineRotations() { + return [ + [[0,1,0],[1,1,1],[0,0,0]], + [[0,1,0],[0,1,1],[0,1,0]], + [[0,0,0],[1,1,1],[0,1,0]], + [[0,1,0],[1,1,0],[0,1,0]] + ]; + } + getColor() { return 1; } +} + +class PieceL extends Piece { + defineRotations() { + return [ + [[0,0,1],[1,1,1],[0,0,0]], + [[0,1,0],[0,1,0],[0,1,1]], + [[0,0,0],[1,1,1],[1,0,0]], + [[1,1,0],[0,1,0],[0,1,0]] + ]; + } + getColor() { return 2; } +} + +class PieceReverseL extends Piece { + defineRotations() { + return [ + [[1,0,0],[1,1,1],[0,0,0]], + [[0,1,1],[0,1,0],[0,1,0]], + [[0,0,0],[1,1,1],[0,0,1]], + [[0,1,0],[0,1,0],[1,1,0]] + ]; + } + getColor() { return 3; } +} + +class PieceI extends Piece { + defineRotations() { + return [ + [[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]] + ]; + } + getColor() { return 4; } +} + +class PieceZ extends Piece { + defineRotations() { + return [ + [[1,1,0],[0,1,1],[0,0,0]], + [[0,0,1],[0,1,1],[0,1,0]] + ]; + } + getColor() { return 5; } +} + +class PieceReverseZ extends Piece { + defineRotations() { + return [ + [[0,1,1],[1,1,0],[0,0,0]], + [[0,1,0],[0,1,1],[0,0,1]] + ]; + } + getColor() { return 6; } +} + +class PieceO extends Piece { + defineRotations() { return [[[1,1],[1,1]]]; } + getColor() { return 7; } +} diff --git a/Transcendence/srcs/frontend/src/renderer.js b/Transcendence/srcs/frontend/src/renderer.js new file mode 100644 index 0000000..597d41c --- /dev/null +++ b/Transcendence/srcs/frontend/src/renderer.js @@ -0,0 +1,116 @@ +// ───────────────────────────────────────────── +// RENDU +// ───────────────────────────────────────────── + +const CELL = 30; +const COLORS = ['#070712','#a855f7','#f97316','#3b82f6','#06b6d4','#ef4444','#22c55e','#eab308']; + +const ctxMain = document.getElementById('canvas-main').getContext('2d'); +const ctxNext = document.getElementById('canvas-next').getContext('2d'); +const ctxHold = document.getElementById('canvas-hold').getContext('2d'); + +function drawCell(ctx, x, y, colorIndex, size) { + const p = 1; + ctx.fillStyle = COLORS[colorIndex]; + ctx.fillRect(x * size + p, y * size + p, size - p * 2, size - p * 2); + // Highlight + ctx.fillStyle = 'rgba(255,255,255,0.25)'; + ctx.fillRect(x * size + p, y * size + p, size - p * 2, 3); + ctx.fillRect(x * size + p, y * size + p, 3, size - p * 2); + // Ombre + ctx.fillStyle = 'rgba(0,0,0,0.35)'; + ctx.fillRect(x * size + p, (y + 1) * size - p - 3, size - p * 2, 3); + ctx.fillRect((x + 1) * size - p - 3, y * size + p, 3, size - p * 2); +} + +function clearCanvas(ctx, w, h) { + ctx.fillStyle = '#070712'; + ctx.fillRect(0, 0, w, h); +} + +function drawGridLines(ctx, cols, rows, size) { + ctx.strokeStyle = 'rgba(255,255,255,0.04)'; + ctx.lineWidth = 1; + for (let x = 0; x <= cols; x++) { + ctx.beginPath(); ctx.moveTo(x * size, 0); ctx.lineTo(x * size, rows * size); ctx.stroke(); + } + for (let y = 0; y <= rows; y++) { + ctx.beginPath(); ctx.moveTo(0, y * size); ctx.lineTo(cols * size, y * size); ctx.stroke(); + } +} + +function drawGhost(ctx, piece, grid) { + if (!piece) return; + const ghost = { x: piece.getPosition().x, y: piece.getPosition().y }; + const shape = piece.getShape(); + + while (true) { + ghost.y++; + let valid = true; + for (let row = 0; row < shape.length && valid; row++) + for (let col = 0; col < shape[row].length && valid; col++) + if (shape[row][col] !== 0) { + const ny = ghost.y + row; + const nx = ghost.x + col; + if (ny >= grid.length || grid[ny][nx] !== 0) valid = false; + } + if (!valid) { ghost.y--; break; } + } + + if (ghost.y === piece.getPosition().y) return; + + ctx.strokeStyle = 'rgba(255,255,255,0.15)'; + ctx.lineWidth = 1; + for (let row = 0; row < shape.length; row++) + for (let col = 0; col < shape[row].length; col++) + if (shape[row][col] !== 0) + ctx.strokeRect( + (ghost.x + col) * CELL + 2, + (ghost.y + row) * CELL + 2, + CELL - 4, CELL - 4 + ); +} + +function drawMiniPiece(ctx, piece, canvasW, canvasH) { + clearCanvas(ctx, canvasW, canvasH); + if (!piece) return; + const shape = piece.getShape(); + const color = piece.getColor(); + const s = 20; + const offsetX = Math.floor((canvasW / s - shape[0].length) / 2); + const offsetY = Math.floor((canvasH / s - shape.length) / 2); + for (let row = 0; row < shape.length; row++) + for (let col = 0; col < shape[row].length; col++) + if (shape[row][col] !== 0) + drawCell(ctx, offsetX + col, offsetY + row, color, s); +} + +function render() { + // Grille principale + clearCanvas(ctxMain, 300, 600); + drawGridLines(ctxMain, 10, 20, CELL); + + for (let y = 0; y < game.grid.length; y++) + for (let x = 0; x < game.grid[y].length; x++) + if (game.grid[y][x] !== 0) + drawCell(ctxMain, x, y, game.grid[y][x], CELL); + + // Ghost + pièce courante + if (game.currentPiece) { + drawGhost(ctxMain, game.currentPiece, game.grid); + const { x, y } = game.currentPiece.getPosition(); + const shape = game.currentPiece.getShape(); + const color = game.currentPiece.getColor(); + for (let row = 0; row < shape.length; row++) + for (let col = 0; col < shape[row].length; col++) + if (shape[row][col] !== 0) + drawCell(ctxMain, x + col, y + row, color, CELL); + } + + // Panneaux miniatures + drawMiniPiece(ctxNext, game.nextPiece, 100, 80); + drawMiniPiece(ctxHold, game.storedPiece, 100, 80); + + // Score + document.getElementById('score-display').textContent = game.score; +} diff --git a/Transcendence/srcs/frontend/src/tetris.css b/Transcendence/srcs/frontend/src/tetris.css new file mode 100644 index 0000000..1dd568f --- /dev/null +++ b/Transcendence/srcs/frontend/src/tetris.css @@ -0,0 +1,243 @@ +:root { + --bg: #070712; + --panel: #0d0d1f; + --border: #1a1a3e; + --accent: #00ffe7; + --accent2:#ff00aa; + --dim: #3a3a6a; + --text: #c0c0e0; +} + +* { margin: 0; padding: 0; box-sizing: border-box; } + +body { + background: var(--bg); + font-family: 'Share Tech Mono', monospace; + color: var(--text); + min-height: 100vh; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + overflow: hidden; +} + +body::before { + content: ''; + position: fixed; + inset: 0; + background-image: + linear-gradient(rgba(0,255,231,0.03) 1px, transparent 1px), + linear-gradient(90deg, rgba(0,255,231,0.03) 1px, transparent 1px); + background-size: 40px 40px; + pointer-events: none; + z-index: 0; +} + +h1 { + font-family: 'Orbitron', monospace; + font-weight: 900; + font-size: 2.2rem; + letter-spacing: 0.4em; + color: var(--accent); + text-shadow: 0 0 20px var(--accent), 0 0 40px var(--accent); + margin-bottom: 20px; + position: relative; + z-index: 1; +} + +#app { + display: flex; + gap: 16px; + align-items: flex-start; + position: relative; + z-index: 1; +} + +.panel { + background: var(--panel); + border: 1px solid var(--border); + border-radius: 6px; + padding: 14px; + width: 130px; + box-shadow: 0 0 20px rgba(0,255,231,0.05); +} + +.panel-title { + font-family: 'Orbitron', monospace; + font-size: 0.6rem; + letter-spacing: 0.2em; + color: var(--accent); + text-transform: uppercase; + margin-bottom: 10px; + text-align: center; +} + +canvas { display: block; border-radius: 4px; } + +#canvas-main { + border: 1px solid var(--border); + box-shadow: 0 0 30px rgba(0,255,231,0.08), inset 0 0 30px rgba(0,0,0,0.5); +} + +#canvas-next, #canvas-hold { + border: 1px solid var(--border); + margin: 0 auto; +} + +.score-block { + margin-top: 14px; + text-align: center; +} + +.score-label { + font-size: 0.55rem; + letter-spacing: 0.2em; + color: var(--dim); + text-transform: uppercase; + margin-bottom: 4px; +} + +.score-value { + font-family: 'Orbitron', monospace; + font-size: 1.4rem; + font-weight: 700; + color: var(--accent); + text-shadow: 0 0 10px var(--accent); +} + +.btn-group { + display: flex; + flex-direction: column; + gap: 8px; + margin-top: 14px; +} + +button { + font-family: 'Orbitron', monospace; + font-size: 0.55rem; + letter-spacing: 0.15em; + font-weight: 700; + text-transform: uppercase; + padding: 10px 8px; + border: 1px solid; + border-radius: 4px; + cursor: pointer; + transition: all 0.2s; + background: transparent; + width: 100%; +} + +#btn-start { color: var(--accent); border-color: var(--accent); } +#btn-start:hover:not(:disabled) { background: var(--accent); color: var(--bg); box-shadow: 0 0 15px var(--accent); } + +#btn-pause { color: var(--accent2); border-color: var(--accent2); } +#btn-pause:hover:not(:disabled) { background: var(--accent2); color: var(--bg); box-shadow: 0 0 15px var(--accent2); } + +#btn-stop { color: #ef4444; border-color: #ef4444; } +#btn-stop:hover:not(:disabled) { background: #ef4444; color: var(--bg); box-shadow: 0 0 15px #ef4444; } + +button:disabled { opacity: 0.3; cursor: not-allowed; } + +.controls-list { + margin-top: 14px; + font-size: 0.6rem; + line-height: 2; + color: var(--dim); +} +.controls-list span { color: var(--text); } + +#main-wrapper { position: relative; } + +#overlay { + display: none; + position: absolute; + top: 0; left: 0; + width: 300px; + height: 600px; + background: rgba(7,7,18,0.88); + border-radius: 4px; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 12px; + z-index: 10; + pointer-events: none; +} +#overlay.visible { display: flex; } + +#overlay-title { + font-family: 'Orbitron', monospace; + font-size: 1.4rem; + font-weight: 900; + letter-spacing: 0.2em; + color: var(--accent2); + text-shadow: 0 0 20px var(--accent2); +} + +#overlay-score { + font-family: 'Orbitron', monospace; + font-size: 0.9rem; + color: var(--accent); +} + +/* ── Settings Panel ── */ +#settings-panel { + background: var(--panel); + border: 1px solid var(--border); + border-radius: 6px; + padding: 14px 20px; + margin-top: 0px; /* plus bas */ + margin-left: -800px; /* vers la gauche */ + box-shadow: 0 0 20px rgba(0,255,231,0.05); + position: relative; + z-index: 1; + display: flex; + flex-direction: column; + gap: 10px; + width: fit-content; +} + +.settings-title { + font-family: 'Orbitron', monospace; + font-size: 0.6rem; + letter-spacing: 0.2em; + color: var(--accent); + text-transform: uppercase; + text-align: center; + margin-bottom: 4px; +} + +.settings-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; + font-size: 0.6rem; + color: var(--dim); + letter-spacing: 0.05em; +} + +#settings-panel input[type="number"] { + background: var(--bg); + border: 1px solid var(--border); + border-radius: 4px; + color: var(--accent); + font-family: 'Orbitron', monospace; + font-size: 0.65rem; + padding: 4px 8px; + width: 80px; + text-align: right; + outline: none; + transition: border-color 0.2s; +} + +#settings-panel input[type="number"]:focus { + border-color: var(--accent); + box-shadow: 0 0 8px rgba(0,255,231,0.2); +} + +#settings-panel input[type="number"]:disabled { + opacity: 0.3; + cursor: not-allowed; +} diff --git a/Transcendence/srcs/frontend/src/tetris.html b/Transcendence/srcs/frontend/src/tetris.html index 2d846ed..20e0e6e 100644 --- a/Transcendence/srcs/frontend/src/tetris.html +++ b/Transcendence/srcs/frontend/src/tetris.html @@ -5,190 +5,7 @@ TETRIS - + @@ -212,7 +29,6 @@ -
@@ -238,625 +54,27 @@
- - + +
+
Paramètres
+
+ + +
+
+ + +
+
+ + +
+
+ + + + + + diff --git a/Transcendence/srcs/frontend/src/tetris.js b/Transcendence/srcs/frontend/src/tetris.js new file mode 100644 index 0000000..91aabee --- /dev/null +++ b/Transcendence/srcs/frontend/src/tetris.js @@ -0,0 +1,350 @@ +// ───────────────────────────────────────────── +// LOGIQUE TETRIS +// ───────────────────────────────────────────── + +class Tetris { + constructor(onRender, onGameOver) { + this.onRender = onRender; + this.onGameOver = onGameOver; + + this.grid = this._createGrid(10, 20); + this.bufferGrid = this._createGrid(10, 5); + this.currentPiece = null; + this.storedPiece = null; + this.nextPiece = null; + + this.score = 0; + this.initialTimeToDown = 1000; + this.timeToDown = 1000; + this.hardening = 1000; + this.count = 0; + this.decrementTTD = 100; + + this.isRunning = false; + this.isPaused = false; + this.canStore = true; + + this.animationFrameId = null; + this.lastTime = 0; + this.accumulator = 0; + + this._keyHandler = this._handleKey.bind(this); + } + + configure({ timeToDown, hardening, decrementTTD }) { + if (timeToDown !== undefined) this.initialTimeToDown = this.timeToDown = timeToDown; + if (hardening !== undefined) this.hardening = hardening; + if (decrementTTD !== undefined) this.decrementTTD = decrementTTD; + } + + _createGrid(w, h) { + return Array.from({ length: h }, () => Array(w).fill(0)); + } + + start() { + if (this.isRunning) return; + this.isRunning = true; + this.isPaused = false; + this.grid = this._createGrid(10, 20); + this.score = 0; + this.timeToDown = this.initialTimeToDown; + this.storedPiece = null; + this.canStore = true; + this._spawnNewPiece(); + document.addEventListener('keydown', this._keyHandler); + this._startGameLoop(); + } + + stop() { + this.isRunning = false; + this.isPaused = false; + if (this.animationFrameId !== null) { + cancelAnimationFrame(this.animationFrameId); + this.animationFrameId = null; + } + this.accumulator = 0; + this.lastTime = 0; + document.removeEventListener('keydown', this._keyHandler); + } + + pause() { + if (!this.isRunning) return; + this.isPaused = !this.isPaused; + if (!this.isPaused) { + this.lastTime = 0; + this._startGameLoop(); + } + } + + _startGameLoop() { + this.lastTime = 0; + this.accumulator = 0; + + const gameLoop = (currentTime) => { + if (!this.isRunning) return; + + if (this.isPaused) { + this.animationFrameId = requestAnimationFrame(gameLoop); + return; + } + + if (this.lastTime === 0) { + this.lastTime = currentTime; + this.animationFrameId = requestAnimationFrame(gameLoop); + return; + } + + const deltaTime = currentTime - this.lastTime; + this.lastTime = currentTime; + this.accumulator += deltaTime; + + while (this.accumulator >= this.timeToDown) { + this._tick(); + this.accumulator -= this.timeToDown; + if (this.accumulator > this.timeToDown * 3) { + this.accumulator = 0; + break; + } + } + + this.onRender(); + this.animationFrameId = requestAnimationFrame(gameLoop); + }; + + this.animationFrameId = requestAnimationFrame(gameLoop); + } + + _tick() { + if (!this.currentPiece) return; + if (this._canMoveDown()) { + this.currentPiece.moveDown(); + } else { + this._lockPiece(); + this.verifierLignes(); + this._makeHarder(); + this._spawnNewPiece(); + this.canStore = true; + if (!this._canSpawn()) this._gameOver(); + } + } + + _handleKey(e) { + if (!this.isRunning || !this.currentPiece) return; + + switch (e.key) { + case 'ArrowLeft': + e.preventDefault(); + if (!this.isPaused && this._canMoveLeft()) this.currentPiece.moveLeft(); + break; + case 'ArrowRight': + e.preventDefault(); + if (!this.isPaused && this._canMoveRight()) this.currentPiece.moveRight(); + break; + case 'ArrowDown': + e.preventDefault(); + if (!this.isPaused && this._canMoveDown()) { + this.currentPiece.moveDown(); + this.score += 1; + this.accumulator = 0; + } + break; + case ' ': + e.preventDefault(); + if (!this.isPaused) this._hardDrop(); + break; + case 'q': case 'Q': + e.preventDefault(); + if (!this.isPaused) this._rotatePiece(-1); + break; + case 'w': case 'W': + e.preventDefault(); + if (!this.isPaused) this._rotatePiece(1); + break; + case 'c': case 'C': + e.preventDefault(); + if (!this.isPaused) this._storePiece(); + break; + } + + this.onRender(); + } + + _hardDrop() { + if (!this.currentPiece) return; + let dist = 0; + while (this._canMoveDown()) { this.currentPiece.moveDown(); dist++; } + this.score += dist * 2; + this._lockPiece(); + this.verifierLignes(); + this._makeHarder(); + this._spawnNewPiece(); + this.canStore = true; + this.accumulator = 0; + if (!this._canSpawn()) this._gameOver(); + } + + _rotatePiece(direction) { + if (!this.currentPiece) return; + const originalPos = { ...this.currentPiece.getPosition() }; + + if (direction === -1) this.currentPiece.rotateLeft(); + else this.currentPiece.rotateRight(); + + if (!this._isValidPosition()) { + this.currentPiece.moveRight(); + if (this._isValidPosition()) return; + + this.currentPiece.moveLeft(); + this.currentPiece.moveLeft(); + if (this._isValidPosition()) return; + + this.currentPiece.moveLeft(); + if (this._isValidPosition()) return; + + this.currentPiece.moveRight(); + this.currentPiece.moveRight(); + this.currentPiece.position.y--; + if (this._isValidPosition()) return; + + this.currentPiece.position.y = originalPos.y; + this.currentPiece.position.x = originalPos.x; + if (direction === -1) this.currentPiece.rotateRight(); + else this.currentPiece.rotateLeft(); + } + } + + _storePiece() { + if (!this.canStore || !this.currentPiece) return; + + if (this.storedPiece === null) { + this.storedPiece = this.currentPiece; + this._spawnNewPiece(); + } else { + const temp = this.storedPiece; + this.storedPiece = this.currentPiece; + this.currentPiece = temp; + this.currentPiece.position.x = 3; + this.currentPiece.position.y = 0; + } + this.canStore = false; + this.accumulator = 0; + } + + _spawnNewPiece() { + this.currentPiece = this.nextPiece || this._createRandomPiece(); + this.nextPiece = this._createRandomPiece(); + this._updateBufferGrid(); + } + + _createRandomPiece() { + const types = [PieceT, PieceL, PieceReverseL, PieceI, PieceZ, PieceReverseZ, PieceO]; + return new types[Math.floor(Math.random() * types.length)](3, 0); + } + + _updateBufferGrid() { + this.bufferGrid = this._createGrid(10, 5); + if (!this.nextPiece) return; + const shape = this.nextPiece.getShape(); + const offsetX = Math.floor((10 - shape[0].length) / 2); + for (let y = 0; y < shape.length; y++) + for (let x = 0; x < shape[y].length; x++) + if (shape[y][x] !== 0) + this.bufferGrid[y + 1][x + offsetX] = this.nextPiece.getColor(); + } + + verifierLignes() { + let cleared = 0; + for (let y = this.grid.length - 1; y >= 0; y--) { + if (this.grid[y].every(c => c !== 0)) { + this.grid.splice(y, 1); + this.grid.unshift(Array(10).fill(0)); + cleared++; + y++; + } + } + const points = [0, 100, 300, 500, 800]; + this.score += points[cleared]; + this.count += points[cleared]; + } + + _makeHarder() { + if (this.count >= this.hardening){ + this.count = 0; + this.timeToDown -= this.decrementTTD; + } + } + + _canMoveDown() { + if (!this.currentPiece) return false; + const { x, y } = this.currentPiece.getPosition(); + const shape = this.currentPiece.getShape(); + for (let row = 0; row < shape.length; row++) + for (let col = 0; col < shape[row].length; col++) + if (shape[row][col] !== 0) { + const ny = y + row + 1; + const nx = x + col; + if (ny >= this.grid.length || this.grid[ny][nx] !== 0) return false; + } + return true; + } + + _canMoveLeft() { + if (!this.currentPiece) return false; + const { x, y } = this.currentPiece.getPosition(); + const shape = this.currentPiece.getShape(); + for (let row = 0; row < shape.length; row++) + for (let col = 0; col < shape[row].length; col++) + if (shape[row][col] !== 0) { + const nx = x + col - 1; + if (nx < 0 || this.grid[y + row][nx] !== 0) return false; + } + return true; + } + + _canMoveRight() { + if (!this.currentPiece) return false; + const { x, y } = this.currentPiece.getPosition(); + const shape = this.currentPiece.getShape(); + for (let row = 0; row < shape.length; row++) + for (let col = 0; col < shape[row].length; col++) + if (shape[row][col] !== 0) { + const nx = x + col + 1; + if (nx >= this.grid[0].length || this.grid[y + row][nx] !== 0) return false; + } + return true; + } + + _isValidPosition() { + if (!this.currentPiece) return false; + const { x, y } = this.currentPiece.getPosition(); + const shape = this.currentPiece.getShape(); + for (let row = 0; row < shape.length; row++) + for (let col = 0; col < shape[row].length; col++) + if (shape[row][col] !== 0) { + const gx = x + col; + const gy = y + row; + if (gx < 0 || gx >= this.grid[0].length || + gy < 0 || gy >= this.grid.length || + this.grid[gy][gx] !== 0) return false; + } + return true; + } + + _canSpawn() { return this._isValidPosition(); } + + _lockPiece() { + if (!this.currentPiece) return; + const { x, y } = this.currentPiece.getPosition(); + const shape = this.currentPiece.getShape(); + const color = this.currentPiece.getColor(); + for (let row = 0; row < shape.length; row++) + for (let col = 0; col < shape[row].length; col++) + if (shape[row][col] !== 0) + this.grid[y + row][x + col] = color; + } + + _gameOver() { + this.stop(); + this.onGameOver(this.score); + } +} diff --git a/Transcendence/srcs/frontend/src/ui.js b/Transcendence/srcs/frontend/src/ui.js new file mode 100644 index 0000000..488e57f --- /dev/null +++ b/Transcendence/srcs/frontend/src/ui.js @@ -0,0 +1,76 @@ +// ───────────────────────────────────────────── +// UI +// ───────────────────────────────────────────── + +const btnStart = document.getElementById('btn-start'); +const btnPause = document.getElementById('btn-pause'); +const btnStop = document.getElementById('btn-stop'); +const overlay = document.getElementById('overlay'); +const inputTTD = document.getElementById('input-ttd'); +const inputHardening = document.getElementById('input-hardening'); +const inputDecrement = document.getElementById('input-decrement'); + +function updateButtons() { + btnStart.disabled = game.isRunning; + btnPause.disabled = !game.isRunning; + btnStop.disabled = !game.isRunning; + btnPause.textContent = game.isPaused ? 'Resume' : 'Pause'; + inputTTD.disabled = game.isRunning; + inputHardening.disabled = game.isRunning; + inputDecrement.disabled = game.isRunning; +} + +function showOverlay(title, score) { + document.getElementById('overlay-title').textContent = title; + document.getElementById('overlay-score').textContent = score !== undefined ? `Score : ${score}` : ''; + overlay.classList.add('visible'); +} + +function hideOverlay() { + overlay.classList.remove('visible'); +} + +// ───────────────────────────────────────────── +// INIT +// ───────────────────────────────────────────── + +const game = new Tetris( + () => { render(); updateButtons(); }, + (score) => { render(); updateButtons(); showOverlay('GAME OVER', score); } +); + +btnStart.addEventListener('click', () => { + hideOverlay(); + game.start(); + updateButtons(); + render(); +}); + +btnPause.addEventListener('click', () => { + game.pause(); + updateButtons(); + if (game.isPaused) showOverlay('PAUSE'); + else hideOverlay(); +}); + +btnStop.addEventListener('click', () => { + game.stop(); + updateButtons(); + render(); + showOverlay('STOPPED'); +}); + +function applySettings() { + game.configure({ + timeToDown: parseInt(inputTTD.value, 10), + hardening: parseInt(inputHardening.value, 10), + decrementTTD: parseInt(inputDecrement.value, 10), + }); +} + +inputTTD.addEventListener('change', applySettings); +inputHardening.addEventListener('change', applySettings); +inputDecrement.addEventListener('change', applySettings); + +render(); +updateButtons();