tetris duel bugged

This commit is contained in:
2026-02-19 16:28:22 +01:00
parent 1879203ac8
commit 0f69f4fb6f
8 changed files with 577 additions and 53 deletions
@@ -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<socketId, socket> }
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;
@@ -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
+138
View File
@@ -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');
}
}
+14 -4
View File
@@ -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);
}
+119 -7
View File
@@ -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;
+76 -35
View File
@@ -11,50 +11,89 @@
<h1>TETRIS</h1>
<div id="app">
<!-- Panneau gauche : Hold + Score + Boutons -->
<div class="panel">
<div class="panel-title">Hold</div>
<canvas id="canvas-hold" width="100" height="80"></canvas>
<div class="score-block">
<div class="score-label">Score</div>
<div class="score-value" id="score-display">0</div>
</div>
<div class="btn-group">
<button id="btn-start">Start</button>
<button id="btn-pause" disabled>Pause</button>
<button id="btn-stop" disabled>Stop</button>
</div>
<!-- Panneau de connexion duel -->
<div id="duel-panel">
<span class="settings-title">Duel</span>
<div class="duel-row">
<input id="input-room-code" placeholder="Code de salle" maxlength="8" spellcheck="false">
<button id="btn-join-duel">Rejoindre</button>
<button id="btn-leave-duel" disabled>Quitter</button>
</div>
<!-- Grille principale -->
<div id="main-wrapper">
<canvas id="canvas-main" width="300" height="600"></canvas>
<div id="overlay">
<div id="overlay-title">GAME OVER</div>
<div id="overlay-score"></div>
<div id="duel-status"></div>
</div>
<div id="game-area">
<!-- ── JOUEUR LOCAL ── -->
<div id="local-section">
<div id="app">
<!-- Panneau gauche : Hold + Score + Boutons -->
<div class="panel">
<div class="panel-title">Hold</div>
<canvas id="canvas-hold" width="100" height="80"></canvas>
<div class="score-block">
<div class="score-label">Score</div>
<div class="score-value" id="score-display">0</div>
</div>
<div class="btn-group">
<button id="btn-start">Start</button>
<button id="btn-pause" disabled>Pause</button>
<button id="btn-stop" disabled>Stop</button>
</div>
</div>
<!-- Grille principale -->
<div id="main-wrapper">
<canvas id="canvas-main" width="300" height="600"></canvas>
<div id="overlay">
<div id="overlay-title">GAME OVER</div>
<div id="overlay-score"></div>
</div>
</div>
<!-- Panneau droit : Next + Contrôles -->
<div class="panel">
<div class="panel-title">Next</div>
<canvas id="canvas-next" width="100" height="80"></canvas>
<div class="controls-list">
<div><span>← →</span> Déplacer</div>
<div><span></span> Descendre</div>
<div><span>Q</span> Rot. gauche</div>
<div><span>W</span> Rot. droite</div>
<div><span>Espace</span> Drop</div>
<div><span>C</span> Hold</div>
</div>
</div>
</div>
</div>
<!-- Panneau droit : Next + Contrôles -->
<div class="panel">
<div class="panel-title">Next</div>
<canvas id="canvas-next" width="100" height="80"></canvas>
<!-- ── JOUEUR ADVERSAIRE ── -->
<div id="opponent-section">
<div class="panel opponent-info-panel">
<div class="panel-title" id="opponent-name">Adversaire</div>
<div class="score-block">
<div class="score-label">Score</div>
<div class="score-value" id="opponent-score"></div>
</div>
</div>
<div class="controls-list">
<div><span>← →</span> Déplacer</div>
<div><span></span> Descendre</div>
<div><span>Q</span> Rot. gauche</div>
<div><span>W</span> Rot. droite</div>
<div><span>Espace</span> Drop</div>
<div><span>C</span> Hold</div>
<div id="opponent-wrapper">
<canvas id="canvas-opponent" width="300" height="600"></canvas>
<div id="overlay-opponent">
<div id="overlay-opponent-title"></div>
<div id="overlay-opponent-score"></div>
</div>
</div>
</div>
</div>
<!-- Panneau de configuration -->
<!-- Panneau de configuration -->
<div id="settings-panel">
<div class="settings-title">Paramètres</div>
<div class="settings-row">
@@ -72,9 +111,11 @@
</div>
<script src="/socket.io/socket.io.js"></script>
<script src="pieces.js"></script>
<script src="tetris.js"></script>
<script src="renderer.js"></script>
<script src="duel.js"></script>
<script src="ui.js"></script>
</body>
</html>
+18 -3
View File
@@ -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() {
+74 -2
View File
@@ -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', () => {