From 0f69f4fb6f9d255329ae94eece29b8f2f9f454d0 Mon Sep 17 00:00:00 2001 From: bitsearch Date: Thu, 19 Feb 2026 16:28:22 +0100 Subject: [PATCH] tetris duel bugged --- Transcendence/srcs/backend/services/socket.js | 98 +++++++++++++ .../srcs/frontend/src/ajout du multiplayer | 42 +++++- Transcendence/srcs/frontend/src/duel.js | 138 ++++++++++++++++++ Transcendence/srcs/frontend/src/renderer.js | 18 ++- Transcendence/srcs/frontend/src/tetris.css | 126 +++++++++++++++- Transcendence/srcs/frontend/src/tetris.html | 111 +++++++++----- Transcendence/srcs/frontend/src/tetris.js | 21 ++- Transcendence/srcs/frontend/src/ui.js | 76 +++++++++- 8 files changed, 577 insertions(+), 53 deletions(-) create mode 100644 Transcendence/srcs/frontend/src/duel.js diff --git a/Transcendence/srcs/backend/services/socket.js b/Transcendence/srcs/backend/services/socket.js index ac2cf65..e556a6c 100644 --- a/Transcendence/srcs/backend/services/socket.js +++ b/Transcendence/srcs/backend/services/socket.js @@ -7,6 +7,9 @@ import playerStatsService from './player_stats.js'; // Store game state per room const gameRooms = new Map(); +// Store tetris duel rooms { roomCode → Map } +const tetrisRooms = new Map(); + // Store io instance globally for use in routes let ioInstance = null; @@ -422,8 +425,75 @@ function setupSocketIO(io) io.to(roomId).emit('game-ended'); }); + // ============================================ + // TETRIS DUEL EVENTS + // ============================================ + + socket.on('tetris:join', ({ roomCode }) => { + const code = String(roomCode).toUpperCase().slice(0, 8); + + // Quitter l'ancienne room tetris si besoin + if (socket.tetrisRoomCode) { + _tetrisLeave(socket); + } + + if (!tetrisRooms.has(code)) { + tetrisRooms.set(code, new Map()); + } + const room = tetrisRooms.get(code); + + if (room.size >= 2) { + socket.emit('tetris:room-status', { status: 'full', players: [] }); + return; + } + + room.set(socket.id, socket); + socket.tetrisRoomCode = code; + + const players = [...room.values()].map(s => s.user.username); + + if (room.size === 1) { + socket.emit('tetris:room-status', { status: 'waiting', players }); + } else { + // Notifier les deux joueurs + for (const s of room.values()) { + s.emit('tetris:room-status', { status: 'ready', players }); + } + // Notifier l'adversaire qu'un nouveau joueur a rejoint + for (const [id, s] of room) { + if (id !== socket.id) { + s.emit('tetris:opponent-joined', { username: socket.user.username }); + } + } + } + }); + + socket.on('tetris:leave', () => { + _tetrisLeave(socket); + }); + + // Relay pur : grid-update → adversaire uniquement + socket.on('tetris:grid-update', (data) => { + _tetrisRelayToOpponent(socket, 'tetris:grid-update', data); + }); + + // Relay pur : lines-cleared → adversaire uniquement + socket.on('tetris:lines-cleared', (data) => { + _tetrisRelayToOpponent(socket, 'tetris:lines-cleared', data); + }); + + // game-over → relayé en opponent-game-over chez l'adversaire + socket.on('tetris:game-over', (data) => { + _tetrisRelayToOpponent(socket, 'tetris:opponent-game-over', data); + }); + socket.on('disconnect', async () => { + // Nettoyage room tetris + if (socket.tetrisRoomCode) { + _tetrisLeave(socket); + } + console.log(`User disconnected: ${socket.user.username}`); // Notify game room if player was in one @@ -453,5 +523,33 @@ function setupSocketIO(io) }); } +// ── Helpers tetris duel ────────────────────────────────────────────────── + +function _tetrisLeave(socket) { + const code = socket.tetrisRoomCode; + if (!code) return; + const room = tetrisRooms.get(code); + if (room) { + room.delete(socket.id); + // Notifier l'adversaire restant + for (const s of room.values()) { + s.emit('tetris:opponent-left'); + s.emit('tetris:room-status', { status: 'waiting', players: [s.user.username] }); + } + if (room.size === 0) tetrisRooms.delete(code); + } + socket.tetrisRoomCode = null; +} + +function _tetrisRelayToOpponent(socket, event, data) { + const code = socket.tetrisRoomCode; + if (!code) return; + const room = tetrisRooms.get(code); + if (!room) return; + for (const [id, s] of room) { + if (id !== socket.id) s.emit(event, data); + } +} + export { broadcastRoomsList }; export default setupSocketIO; \ No newline at end of file diff --git a/Transcendence/srcs/frontend/src/ajout du multiplayer b/Transcendence/srcs/frontend/src/ajout du multiplayer index 834cd94..512ff84 100644 --- a/Transcendence/srcs/frontend/src/ajout du multiplayer +++ b/Transcendence/srcs/frontend/src/ajout du multiplayer @@ -1,7 +1,45 @@ -Je veux rendre le tetris en multijoueur, +Je veux faire un mode duel du tetris, il est fonctionnel est solo. Pour commencer, je vais devoir creer une div qui sera le rendu -du joueur second joueur. +du joueur second joueur. il sera a droite de la +div principale qui lui meme sera legerement decale vers la gauche, +cette div sera grossierement identitique en style qui la div principale +je ne vais pas voir en temps reelle +les pieces du joueur qui tombe. +A la place quand le joueur 2 a mis une piece, il envoie un signal, +le joueur 1 envoie un signal egalement quand la piece tombe. + +en mode duel, quand une ligne est clear, il envoie la ligne +moins la cellule qui a provoquer le clear (donc avec un trou pile +la ou la derniere piece est arrive) au joueur opposant. + +Ce qui a pour effet de decaler toute les lignes vers le haut +a l'opposant pour recevoir la ligne recu avec le trou. + +Pour se faire je vais devoir connecter les deux joueurs. + +Il me faudra : + +une class Duel il aura pour methode et membre: + +action_queue: c'est un tableau qui repertorie tous les +signaux a traiter, c'est un tableau qui sera partager +entre le joueur1 et le joueur2 + + +Syncronise_game: fonction qui traite les +actions de action_queue et qui verifie l'integrite du duel, +il va par exemple regarder l'etat du jeu de chaque joueur +pour voir s'il correspond bien a ce qui est attendu + +il y aura different type de signaux. +Le signal bloc pose avec le type de bloc, +sa rotation et as position + +le signal line cleared, avec le nombre de ligne +cleared et on ajoute le trou aussi + +Aucune idee de si je dois utiliser web socket ou autre diff --git a/Transcendence/srcs/frontend/src/duel.js b/Transcendence/srcs/frontend/src/duel.js new file mode 100644 index 0000000..303ebe2 --- /dev/null +++ b/Transcendence/srcs/frontend/src/duel.js @@ -0,0 +1,138 @@ +// ───────────────────────────────────────────── +// DUEL +// ───────────────────────────────────────────── + +class Duel { + constructor(socket, tetrisGame, onStatusChange) { + this.socket = socket; + this.tetrisGame = tetrisGame; + this.onStatusChange = onStatusChange; // (status, opponentName) => void + + this.action_queue = []; + this.opponentGrid = this._emptyGrid(); + this.opponentScore = 0; + this.roomCode = null; + this.isReady = false; + + this._bindSocketEvents(); + } + + // ─── Connexion ──────────────────────────── + + join(roomCode) { + this.roomCode = roomCode; + this.socket.emit('tetris:join', { roomCode }); + } + + leave() { + if (!this.roomCode) return; + this.socket.emit('tetris:leave'); + this.roomCode = null; + this.isReady = false; + this.opponentGrid = this._emptyGrid(); + this.opponentScore = 0; + } + + // ─── Hooks appelés par tetris.js ────────── + + onLocalBlockPlaced(grid, score) { + if (!this.isReady) return; + this.socket.emit('tetris:grid-update', { grid, score }); + } + + onLocalLinesCleared(count, holeCol) { + if (!this.isReady) return; + const garbageLines = []; + for (let i = 0; i < count; i++) + garbageLines.push(this._buildGarbageLine(holeCol)); + this.socket.emit('tetris:lines-cleared', { count, holeCol, garbageLines }); + } + + onLocalGameOver(score) { + if (!this.isReady) return; + this.socket.emit('tetris:game-over', { score }); + } + + // ─── Traitement de la queue ─────────────── + + synchronize_game() { + while (this.action_queue.length > 0) { + const action = this.action_queue.shift(); + this._processAction(action); + } + } + + _processAction(action) { + switch (action.type) { + case 'GRID_UPDATE': + this.opponentGrid = action.grid; + this.opponentScore = action.score; + document.getElementById('opponent-score').textContent = action.score; + renderOpponent(this.opponentGrid); + break; + + case 'LINES_CLEARED': + this.tetrisGame.addGarbageLines(action.garbageLines); + break; + + case 'OPPONENT_GAME_OVER': + this._showOpponentOverlay('YOU WIN', action.score); + break; + } + } + + // ─── Liaison socket ─────────────────────── + + _bindSocketEvents() { + this.socket.on('tetris:room-status', (data) => { + this.isReady = data.status === 'ready'; + const opponentName = data.players.find(p => p !== this.socket.username) || 'Adversaire'; + document.getElementById('opponent-name').textContent = opponentName; + this.onStatusChange(data.status, opponentName); + }); + + this.socket.on('tetris:opponent-joined', (data) => { + document.getElementById('opponent-name').textContent = data.username; + }); + + this.socket.on('tetris:opponent-left', () => { + this.isReady = false; + this.onStatusChange('waiting', null); + this._showOpponentOverlay('DÉCONNECTÉ'); + }); + + this.socket.on('tetris:grid-update', (data) => { + this.action_queue.push({ type: 'GRID_UPDATE', grid: data.grid, score: data.score }); + }); + + this.socket.on('tetris:lines-cleared', (data) => { + this.action_queue.push({ type: 'LINES_CLEARED', garbageLines: data.garbageLines }); + }); + + this.socket.on('tetris:opponent-game-over', (data) => { + this.action_queue.push({ type: 'OPPONENT_GAME_OVER', score: data.score }); + }); + } + + // ─── Utilitaires ───────────────────────── + + _buildGarbageLine(holeCol) { + return Array.from({ length: 10 }, (_, i) => i === holeCol ? 0 : 8); + } + + _emptyGrid() { + return Array.from({ length: 20 }, () => Array(10).fill(0)); + } + + _showOpponentOverlay(title, score) { + const overlayEl = document.getElementById('overlay-opponent'); + document.getElementById('overlay-opponent-title').textContent = title; + const scoreEl = document.getElementById('overlay-opponent-score'); + if (scoreEl) scoreEl.textContent = score !== undefined ? `Score : ${score}` : ''; + overlayEl.classList.add('visible'); + } + + hideOpponentOverlay() { + document.getElementById('overlay-opponent').classList.remove('visible'); + } +} diff --git a/Transcendence/srcs/frontend/src/renderer.js b/Transcendence/srcs/frontend/src/renderer.js index 597d41c..4473641 100644 --- a/Transcendence/srcs/frontend/src/renderer.js +++ b/Transcendence/srcs/frontend/src/renderer.js @@ -3,11 +3,12 @@ // ───────────────────────────────────────────── const CELL = 30; -const COLORS = ['#070712','#a855f7','#f97316','#3b82f6','#06b6d4','#ef4444','#22c55e','#eab308']; +const COLORS = ['#070712','#a855f7','#f97316','#3b82f6','#06b6d4','#ef4444','#22c55e','#eab308','#555577']; -const ctxMain = document.getElementById('canvas-main').getContext('2d'); -const ctxNext = document.getElementById('canvas-next').getContext('2d'); -const ctxHold = document.getElementById('canvas-hold').getContext('2d'); +const ctxMain = document.getElementById('canvas-main').getContext('2d'); +const ctxNext = document.getElementById('canvas-next').getContext('2d'); +const ctxHold = document.getElementById('canvas-hold').getContext('2d'); +const ctxOpponent = document.getElementById('canvas-opponent').getContext('2d'); function drawCell(ctx, x, y, colorIndex, size) { const p = 1; @@ -114,3 +115,12 @@ function render() { // Score document.getElementById('score-display').textContent = game.score; } + +function renderOpponent(opponentGrid) { + clearCanvas(ctxOpponent, 300, 600); + drawGridLines(ctxOpponent, 10, 20, CELL); + for (let y = 0; y < opponentGrid.length; y++) + for (let x = 0; x < opponentGrid[y].length; x++) + if (opponentGrid[y][x] !== 0) + drawCell(ctxOpponent, x, y, opponentGrid[y][x], CELL); +} diff --git a/Transcendence/srcs/frontend/src/tetris.css b/Transcendence/srcs/frontend/src/tetris.css index 1e88adc..5c028a7 100644 --- a/Transcendence/srcs/frontend/src/tetris.css +++ b/Transcendence/srcs/frontend/src/tetris.css @@ -46,14 +46,43 @@ h1 { z-index: 1; } -#app { +/* ── Zone de jeu globale ── */ +#game-area { display: flex; - gap: 16px; + gap: 32px; align-items: flex-start; position: relative; z-index: 1; } +/* ── Section locale (légèrement décalée à gauche par le flex naturel) ── */ +#local-section { + display: flex; + flex-direction: column; + align-items: center; +} + +#app { + display: flex; + gap: 16px; + align-items: flex-start; +} + +/* ── Section adversaire ── */ +#opponent-section { + display: none; /* masqué jusqu'à connexion duel */ + gap: 16px; + align-items: flex-start; +} +#opponent-section.visible { + display: flex; +} + +.opponent-info-panel { + width: 130px; +} + +/* ── Panneaux ── */ .panel { background: var(--panel); border: 1px solid var(--border); @@ -85,6 +114,13 @@ canvas { display: block; border-radius: 4px; } margin: 0 auto; } +/* ── Canvas adversaire ── */ +#canvas-opponent { + border: 1px solid var(--accent2); + box-shadow: 0 0 30px rgba(255,0,170,0.08), inset 0 0 30px rgba(0,0,0,0.5); +} + +/* ── Score ── */ .score-block { margin-top: 14px; text-align: center; @@ -106,6 +142,7 @@ canvas { display: block; border-radius: 4px; } text-shadow: 0 0 10px var(--accent); } +/* ── Boutons ── */ .btn-group { display: flex; flex-direction: column; @@ -139,6 +176,7 @@ button { button:disabled { opacity: 0.3; cursor: not-allowed; } +/* ── Contrôles ── */ .controls-list { margin-top: 14px; font-size: 0.6rem; @@ -147,9 +185,12 @@ button:disabled { opacity: 0.3; cursor: not-allowed; } } .controls-list span { color: var(--text); } -#main-wrapper { position: relative; } +/* ── Overlays ── */ +#main-wrapper, +#opponent-wrapper { position: relative; } -#overlay { +#overlay, +#overlay-opponent { display: none; position: absolute; top: 0; left: 0; @@ -164,7 +205,8 @@ button:disabled { opacity: 0.3; cursor: not-allowed; } z-index: 10; pointer-events: none; } -#overlay.visible { display: flex; } +#overlay.visible, +#overlay-opponent.visible { display: flex; } #overlay-title { font-family: 'Orbitron', monospace; @@ -181,14 +223,84 @@ button:disabled { opacity: 0.3; cursor: not-allowed; } color: var(--accent); } +#overlay-opponent-title { + font-family: 'Orbitron', monospace; + font-size: 1.4rem; + font-weight: 900; + letter-spacing: 0.2em; + color: var(--accent); + text-shadow: 0 0 20px var(--accent); +} + +#overlay-opponent-score { + font-family: 'Orbitron', monospace; + font-size: 0.9rem; + color: var(--accent2); +} + +/* ── Panneau duel ── */ +#duel-panel { + background: var(--panel); + border: 1px solid var(--border); + border-radius: 6px; + padding: 12px 20px; + margin-bottom: 14px; + position: relative; + z-index: 1; + display: flex; + align-items: center; + gap: 14px; + box-shadow: 0 0 20px rgba(255,0,170,0.04); +} + +.duel-row { + display: flex; + gap: 8px; + align-items: center; +} + +#input-room-code { + background: var(--bg); + border: 1px solid var(--border); + border-radius: 4px; + color: var(--accent2); + font-family: 'Orbitron', monospace; + font-size: 0.7rem; + letter-spacing: 0.15em; + padding: 6px 10px; + width: 120px; + text-transform: uppercase; + outline: none; + transition: border-color 0.2s; +} +#input-room-code:focus { + border-color: var(--accent2); + box-shadow: 0 0 8px rgba(255,0,170,0.2); +} + +#btn-join-duel { color: var(--accent2); border-color: var(--accent2); width: auto; padding: 6px 14px; } +#btn-join-duel:hover:not(:disabled) { background: var(--accent2); color: var(--bg); box-shadow: 0 0 12px var(--accent2); } + +#btn-leave-duel { color: #ef4444; border-color: #ef4444; width: auto; padding: 6px 14px; } +#btn-leave-duel:hover:not(:disabled) { background: #ef4444; color: var(--bg); box-shadow: 0 0 12px #ef4444; } + +#duel-status { + font-size: 0.6rem; + letter-spacing: 0.1em; + color: var(--dim); + min-width: 120px; +} +#duel-status.waiting { color: #f97316; } +#duel-status.ready { color: var(--accent); } + /* ── Settings Panel ── */ #settings-panel { background: var(--panel); border: 1px solid var(--border); border-radius: 6px; padding: 14px 20px; - margin-top: -250px; /* plus bas */ - margin-left: -600px; /* vers la gauche */ + margin-top: -250px; + margin-left: -600px; box-shadow: 0 0 20px rgba(0,255,231,0.05); position: relative; z-index: 1; diff --git a/Transcendence/srcs/frontend/src/tetris.html b/Transcendence/srcs/frontend/src/tetris.html index 20e0e6e..23bdb68 100644 --- a/Transcendence/srcs/frontend/src/tetris.html +++ b/Transcendence/srcs/frontend/src/tetris.html @@ -11,50 +11,89 @@

TETRIS

-
- - -
-
Hold
- - -
-
Score
-
0
-
- -
- - - -
+ +
+ Duel +
+ + +
- -
- -
-
GAME OVER
-
+
+
+ +
+ + +
+
+ + +
+
Hold
+ + +
+
Score
+
0
+
+ +
+ + + +
+
+ + +
+ +
+
GAME OVER
+
+
+
+ + +
+
Next
+ + +
+
← → Déplacer
+
Descendre
+
Q Rot. gauche
+
W Rot. droite
+
Espace Drop
+
C Hold
+
+
+
- -
-
Next
- + +
+
+
Adversaire
+
+
Score
+
+
+
-
-
← → Déplacer
-
Descendre
-
Q Rot. gauche
-
W Rot. droite
-
Espace Drop
-
C Hold
+
+ +
+
+
+
- + +
Paramètres
@@ -72,9 +111,11 @@
+ + diff --git a/Transcendence/srcs/frontend/src/tetris.js b/Transcendence/srcs/frontend/src/tetris.js index 428ff93..53e52ad 100644 --- a/Transcendence/srcs/frontend/src/tetris.js +++ b/Transcendence/srcs/frontend/src/tetris.js @@ -3,9 +3,11 @@ // ───────────────────────────────────────────── class Tetris { - constructor(onRender, onGameOver) { - this.onRender = onRender; - this.onGameOver = onGameOver; + constructor(onRender, onGameOver, onBlockPlaced = null, onLinesCleared = null) { + this.onRender = onRender; + this.onGameOver = onGameOver; + this.onBlockPlaced = onBlockPlaced; + this.onLinesCleared = onLinesCleared; this.grid = this._createGrid(10, 20); this.bufferGrid = this._createGrid(10, 5); @@ -20,6 +22,8 @@ class Tetris { this.count = 0; this.decrementTTD = 100; + this.lastLandingCol = 4; + this.isRunning = false; this.isPaused = false; this.canStore = true; @@ -266,6 +270,8 @@ class Tetris { const points = [0, 100, 300, 500, 800]; this.score += points[cleared]; this.count += points[cleared]; + if (this.onLinesCleared && cleared > 0) + this.onLinesCleared(cleared, this.lastLandingCol); } _makeHarder() { @@ -342,6 +348,15 @@ class Tetris { for (let col = 0; col < shape[row].length; col++) if (shape[row][col] !== 0) this.grid[y + row][x + col] = color; + this.lastLandingCol = x + Math.floor(shape[0].length / 2); + if (this.onBlockPlaced) this.onBlockPlaced(this.grid.map(r => [...r])); + } + + addGarbageLines(lines) { + if (!this.isRunning || !lines.length) return; + this.grid.splice(0, lines.length); + for (const line of lines) this.grid.push([...line]); + if (!this._isValidPosition()) this._gameOver(); } _gameOver() { diff --git a/Transcendence/srcs/frontend/src/ui.js b/Transcendence/srcs/frontend/src/ui.js index 488e57f..f5e75bb 100644 --- a/Transcendence/srcs/frontend/src/ui.js +++ b/Transcendence/srcs/frontend/src/ui.js @@ -10,6 +10,13 @@ const inputTTD = document.getElementById('input-ttd'); const inputHardening = document.getElementById('input-hardening'); const inputDecrement = document.getElementById('input-decrement'); +// Duel UI +const btnJoinDuel = document.getElementById('btn-join-duel'); +const btnLeaveDuel = document.getElementById('btn-leave-duel'); +const inputRoomCode = document.getElementById('input-room-code'); +const duelStatusEl = document.getElementById('duel-status'); +const opponentSection = document.getElementById('opponent-section'); + function updateButtons() { btnStart.disabled = game.isRunning; btnPause.disabled = !game.isRunning; @@ -30,13 +37,78 @@ function hideOverlay() { overlay.classList.remove('visible'); } +// ───────────────────────────────────────────── +// SOCKET + DUEL +// ───────────────────────────────────────────── + +const socket = io({ auth: { token: localStorage.getItem('token') } }); + +let duel = null; + +function updateDuelStatus(status, opponentName) { + duelStatusEl.className = ''; + if (status === 'waiting') { + duelStatusEl.textContent = 'En attente d\'un adversaire…'; + duelStatusEl.classList.add('waiting'); + opponentSection.classList.remove('visible'); + } else if (status === 'ready') { + duelStatusEl.textContent = `Prêt — ${opponentName}`; + duelStatusEl.classList.add('ready'); + opponentSection.classList.add('visible'); + if (duel) duel.hideOpponentOverlay(); + renderOpponent(duel ? duel.opponentGrid : Array.from({length:20}, () => Array(10).fill(0))); + } else { + duelStatusEl.textContent = '—'; + opponentSection.classList.remove('visible'); + } +} + +btnJoinDuel.addEventListener('click', () => { + const code = inputRoomCode.value.trim().toUpperCase(); + if (!code) return; + if (duel) { duel.leave(); } + duel = new Duel(socket, game, updateDuelStatus); + duel.join(code); + btnJoinDuel.disabled = true; + btnLeaveDuel.disabled = false; + inputRoomCode.disabled = true; + updateDuelStatus('waiting', null); +}); + +btnLeaveDuel.addEventListener('click', () => { + if (duel) { duel.leave(); duel = null; } + btnJoinDuel.disabled = false; + btnLeaveDuel.disabled = true; + inputRoomCode.disabled = false; + updateDuelStatus(null, null); +}); + // ───────────────────────────────────────────── // INIT // ───────────────────────────────────────────── const game = new Tetris( - () => { render(); updateButtons(); }, - (score) => { render(); updateButtons(); showOverlay('GAME OVER', score); } + // onRender + () => { + if (duel) duel.synchronize_game(); + render(); + updateButtons(); + }, + // onGameOver + (score) => { + if (duel) duel.onLocalGameOver(score); + render(); + updateButtons(); + showOverlay('GAME OVER', score); + }, + // onBlockPlaced — relay duel + (grid) => { + if (duel) duel.onLocalBlockPlaced(grid, game.score); + }, + // onLinesCleared — relay duel + (count, holeCol) => { + if (duel) duel.onLocalLinesCleared(count, holeCol); + } ); btnStart.addEventListener('click', () => {