fixed lobby synchronisation

This commit is contained in:
Didictateur 2025-10-16 09:07:54 +02:00
parent e285783f9f
commit 7688954ab4
6 changed files with 101 additions and 30 deletions

View file

@ -40,6 +40,10 @@ const PORT = process.env.PORT || 4000;
const games = []; const games = [];
// map socket.id -> { gameId, playerId } // map socket.id -> { gameId, playerId }
const socketMap = new Map(); const socketMap = new Map();
// map `${gameId}:${playerId}` -> timeoutId for pending disconnects
const pendingDisconnects = new Map();
function pdKey(gameId, playerId){ return `${gameId}:${playerId}`; }
app.post('/api/create', (req, res) => { app.post('/api/create', (req, res) => {
console.log('[backend] POST /api/create received', req.body && { name: req.body.name, owner: req.body.ownerNickname }); console.log('[backend] POST /api/create received', req.body && { name: req.body.name, owner: req.body.ownerNickname });
@ -171,6 +175,9 @@ app.post('/api/leave', (req, res) => {
if (g.started) return res.status(403).json({ error: 'game in progress' }); if (g.started) return res.status(403).json({ error: 'game in progress' });
// remove player (allowed only if game not started) // remove player (allowed only if game not started)
const leaving = g.players.splice(pIndex, 1)[0]; const leaving = g.players.splice(pIndex, 1)[0];
// clear any pending disconnect timer for this player
const key = pdKey(gameId, playerId);
if(pendingDisconnects.has(key)){ clearTimeout(pendingDisconnects.get(key)); pendingDisconnects.delete(key); }
// if the leaving player was the owner, remove the entire game // if the leaving player was the owner, remove the entire game
if(g.ownerId && leaving.id === g.ownerId){ if(g.ownerId && leaving.id === g.ownerId){
games.splice(gIndex, 1); games.splice(gIndex, 1);
@ -284,6 +291,18 @@ io.on('connection', (socket) => {
if(gameId && playerId){ if(gameId && playerId){
socketMap.set(socket.id, { gameId, playerId }); socketMap.set(socket.id, { gameId, playerId });
log('identify', { socketId: socket.id, gameId, playerId }); log('identify', { socketId: socket.id, gameId, playerId });
// if this player had a pending disconnect timer, cancel it and mark reconnected
const key = pdKey(gameId, playerId);
if(pendingDisconnects.has(key)){
clearTimeout(pendingDisconnects.get(key));
pendingDisconnects.delete(key);
// mark player as connected again and notify others
const g = games.find(x => x.id === gameId);
if(g){
const p = g.players.find(p => p.id === playerId);
if(p){ p.connected = true; io.to(gameId).emit('player-update', g); io.emit('games-list', games); }
}
}
} }
}catch(e){ } }catch(e){ }
}); });
@ -348,33 +367,38 @@ io.on('connection', (socket) => {
log('socket disconnect', { socketId: socket.id, reason, meta }); log('socket disconnect', { socketId: socket.id, reason, meta });
if(!meta) return; if(!meta) return;
const { gameId, playerId } = meta; const { gameId, playerId } = meta;
// remove mapping for this socket immediately
socketMap.delete(socket.id); socketMap.delete(socket.id);
const gIndex = games.findIndex(x => x.id === gameId); // schedule a delayed removal: if the player doesn't reconnect within 3s, consider them left
if(gIndex === -1) return; const key = pdKey(gameId, playerId);
const g = games[gIndex]; if(pendingDisconnects.has(key)){
const pIndex = g.players.findIndex(p => p.id === playerId); clearTimeout(pendingDisconnects.get(key));
if(pIndex === -1) return; pendingDisconnects.delete(key);
const player = g.players[pIndex]; }
// mark as disconnected instead of removing when game started const t = setTimeout(() => {
if (g.started) { // perform removal after grace period
player.connected = false; const gIndex = games.findIndex(x => x.id === gameId);
console.log(`socket ${socket.id} disconnected (${reason}), marked player ${player.id} disconnected in ${gameId}`); if(gIndex === -1){ pendingDisconnects.delete(key); return; }
const g = games[gIndex];
const pIndex = g.players.findIndex(p => p.id === playerId);
if(pIndex === -1){ pendingDisconnects.delete(key); return; }
const player = g.players[pIndex];
// remove player regardless of started state (user asked players away >3s are considered left)
const leaving = g.players.splice(pIndex, 1)[0];
log('delayed remove player', { gameId, playerId: leaving.id, owner: g.ownerId });
// if owner left, remove entire game immediately
if(g.ownerId && leaving.id === g.ownerId){
games.splice(gIndex, 1);
io.emit('games-list', games);
io.to(gameId).emit('game-deleted', { gameId });
pendingDisconnects.delete(key);
return;
}
io.to(gameId).emit('player-update', g); io.to(gameId).emit('player-update', g);
io.emit('games-list', games); io.emit('games-list', games);
return; pendingDisconnects.delete(key);
} }, 3000);
// if game not started, remove player as before pendingDisconnects.set(key, t);
const leaving = g.players.splice(pIndex, 1)[0];
console.log(`socket ${socket.id} disconnected (${reason}), removed player ${leaving.id} from ${gameId}`);
// if owner left and game not started, delete the game
if(g.ownerId && leaving.id === g.ownerId){
games.splice(gIndex, 1);
io.emit('games-list', games);
io.to(gameId).emit('game-deleted', { gameId });
return;
}
io.to(gameId).emit('player-update', g);
io.emit('games-list', games);
}); });
}); });

View file

@ -11,12 +11,15 @@
#board-wrapper{width:100%;max-width:900px;margin:0 auto} #board-wrapper{width:100%;max-width:900px;margin:0 auto}
.board{display:grid;border:2px solid #333;box-sizing:border-box} .board{display:grid;border:2px solid #333;box-sizing:border-box}
.row{display:contents} .row{display:contents}
.cell{position:relative;padding:0;overflow:hidden} .cell{position:relative;padding:0;overflow:hidden;display:flex;align-items:center;justify-content:center}
.cell .piece{width:100%;height:100%;object-fit:contain;display:block} .cell .piece{width:100%;height:100%;object-fit:contain;display:block}
.cell.selected{outline:3px solid rgba(0,128,255,0.6);} .cell.selected{outline:3px solid rgba(0,128,255,0.6);}
/* make board square and responsive: use a wrapper with aspect-ratio */ /* make board square and responsive: use a wrapper with aspect-ratio */
#board-wrapper .board{width:100%;aspect-ratio:1/1} #board-wrapper .board{width:100%;aspect-ratio:1/1}
/* grid cells will be created dynamically via JS using grid-template */ /* grid cells will be created dynamically via JS using grid-template */
/* checkerboard colors */
.board .cell:nth-child(odd){ background: #f0d9b5; }
.board .cell:nth-child(even){ background: #b58863; }
#meta{margin-top:8px} #meta{margin-top:8px}
</style> </style>
</head> </head>

View file

@ -45,5 +45,6 @@
</div> </div>
<script src="src/main.js?v=2"></script> <script src="src/main.js?v=2"></script>
<script src="https://cdn.socket.io/4.5.4/socket.io.min.js"></script>
</body> </body>
</html> </html>

View file

@ -75,6 +75,16 @@ function renderGameList(){
}); });
} }
// Socket: listen for global games-list updates from backend to refresh lobby list live
if(typeof io !== 'undefined'){
try{
const sock = io('http://localhost:4000', { transports: ['websocket', 'polling'] });
sock.on('connect', () => { console.log('lobby socket connected', sock.id); });
sock.on('games-list', (list) => { console.log('games-list event received', list); try { renderGameList(); } catch(e){} });
sock.on('connect_error', (e) => { console.warn('lobby socket connect error', e); });
}catch(e){ console.warn('failed to init lobby socket', e); }
}
function renderGameListItem(ul, g){ function renderGameListItem(ul, g){
const li = document.createElement('li'); const li = document.createElement('li');
// display only human-friendly name; avoid showing internal ids // display only human-friendly name; avoid showing internal ids
@ -82,7 +92,18 @@ function renderGameListItem(ul, g){
// detect if this client already joined this game (we store playerId in sessionStorage) // detect if this client already joined this game (we store playerId in sessionStorage)
const storedPlayerId = sessionStorage.getItem(`playerId:${g.id}`); const storedPlayerId = sessionStorage.getItem(`playerId:${g.id}`);
const alreadyJoined = storedPlayerId ? (g.players || []).some(p => p.id === storedPlayerId) : false; // If we have a stored player id but the server-side game no longer contains that player,
// cleanup the sessionStorage entry (it means the player was removed / left).
let alreadyJoined = false;
if (storedPlayerId) {
const playerStillInGame = (g.players || []).some(p => p.id === storedPlayerId);
if (!playerStillInGame) {
// stale stored id: remove it
sessionStorage.removeItem(`playerId:${g.id}`);
} else {
alreadyJoined = true;
}
}
if(alreadyJoined){ if(alreadyJoined){
const span = document.createElement('span'); const span = document.createElement('span');

View file

@ -79,6 +79,7 @@ function tryConnectSocket(gameId){
const stored = sessionStorage.getItem(`playerId:${gameId}`) || (()=>{ const all = JSON.parse(localStorage.getItem('games') || '[]'); const g = all.find(x=>x.id===gameId); return g && g.players && g.players[0] && g.players[0].id; })(); const stored = sessionStorage.getItem(`playerId:${gameId}`) || (()=>{ const all = JSON.parse(localStorage.getItem('games') || '[]'); const g = all.find(x=>x.id===gameId); return g && g.players && g.players[0] && g.players[0].id; })();
if(stored) sock.emit('identify', { gameId, playerId: stored }); if(stored) sock.emit('identify', { gameId, playerId: stored });
}); });
sock.on('reconnect', (attempt) => { console.log('waiting socket reconnected', attempt); });
sock.on('game-deleted', (payload) => { sock.on('game-deleted', (payload) => {
if(payload && payload.gameId === gameId){ if(payload && payload.gameId === gameId){
sessionStorage.removeItem(`playerId:${gameId}`); sessionStorage.removeItem(`playerId:${gameId}`);
@ -88,7 +89,10 @@ function tryConnectSocket(gameId){
}); });
sock.on('player-update', (g) => { sock.on('player-update', (g) => {
if(!g) return; if(!g) return;
console.log('[waiting.js] player-update received', g);
renderWaitingPlayers(g.players||[]); renderWaitingPlayers(g.players||[]);
// update start button visibility/disabled state live
try { console.log('[waiting.js] updating start button'); updateStartButton(g); } catch(e){ console.error('updateStartButton error', e); }
}); });
sock.on('game-start', (g) => { sock.on('game-start', (g) => {
if(!g) return; if(!g) return;
@ -101,6 +105,15 @@ function tryConnectSocket(gameId){
}); });
} }
function updateStartButton(g){
const stored = sessionStorage.getItem(`playerId:${gameId}`);
const ownerId = g.players && g.players[0] && g.players[0].id;
const startBtn = q('#start-btn');
console.log('[waiting.js] updateStartButton values', { ownerId, stored, playersLength: (g.players||[]).length, startBtnExists: !!startBtn });
if(ownerId && stored && ownerId === stored){ startBtn?.classList.remove('hidden'); } else { startBtn?.classList.add('hidden'); }
if(startBtn){ startBtn.disabled = ((g.players||[]).length < 2); }
}
function renderWaitingPlayers(players){ function renderWaitingPlayers(players){
const ul = q('#wait-players'); ul.innerHTML = ''; const ul = q('#wait-players'); ul.innerHTML = '';
if(!players || players.length === 0) ul.innerHTML = '<li>Aucun joueur</li>'; if(!players || players.length === 0) ul.innerHTML = '<li>Aucun joueur</li>';

View file

@ -12,12 +12,22 @@ function renderBoardFromState(state) {
const boardEl = document.createElement('div'); const boardEl = document.createElement('div');
boardEl.className = 'board'; boardEl.className = 'board';
// if no state provided, create an empty 8x8 board
if(!state || !state.board){
const w = 8, h = 8;
const board = Array.from({ length: h }, () => Array.from({ length: w }, () => null));
state = { board, width: w, height: h };
}
const h = state.height || state.board.length; const h = state.height || state.board.length;
const w = state.width || (state.board[0] && state.board[0].length) || 8; const w = state.width || (state.board[0] && state.board[0].length) || 8;
// Use CSS grid so cells distribute evenly and keep the board square via wrapper's aspect-ratio
boardEl.style.display = 'grid';
boardEl.style.gridTemplateColumns = `repeat(${w}, 1fr)`;
boardEl.style.gridTemplateRows = `repeat(${h}, 1fr)`;
for (let r = 0; r < h; r++) { for (let r = 0; r < h; r++) {
const row = document.createElement('div');
row.className = 'row';
for (let c = 0; c < w; c++) { for (let c = 0; c < w; c++) {
const cell = document.createElement('div'); const cell = document.createElement('div');
cell.className = 'cell'; cell.className = 'cell';
@ -44,7 +54,7 @@ function renderBoardFromState(state) {
} }
cell.appendChild(piece); cell.appendChild(piece);
row.appendChild(cell); boardEl.appendChild(cell);
// click to select/move // click to select/move
cell.addEventListener('click', () => { cell.addEventListener('click', () => {
// if no game/socket, ignore // if no game/socket, ignore
@ -67,7 +77,6 @@ function renderBoardFromState(state) {
} }
}); });
} }
boardEl.appendChild(row);
} }
container.appendChild(boardEl); container.appendChild(boardEl);
} }