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 @@