diff --git a/backend/index.js b/backend/index.js index 6f70e8e..ca75dc1 100644 --- a/backend/index.js +++ b/backend/index.js @@ -40,6 +40,10 @@ const PORT = process.env.PORT || 4000; const games = []; // map socket.id -> { gameId, playerId } 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) => { 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' }); // remove player (allowed only if game not started) 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(g.ownerId && leaving.id === g.ownerId){ games.splice(gIndex, 1); @@ -284,6 +291,18 @@ io.on('connection', (socket) => { if(gameId && playerId){ socketMap.set(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){ } }); @@ -348,33 +367,38 @@ io.on('connection', (socket) => { log('socket disconnect', { socketId: socket.id, reason, meta }); if(!meta) return; const { gameId, playerId } = meta; + // remove mapping for this socket immediately socketMap.delete(socket.id); - const gIndex = games.findIndex(x => x.id === gameId); - if(gIndex === -1) return; - const g = games[gIndex]; - const pIndex = g.players.findIndex(p => p.id === playerId); - if(pIndex === -1) return; - const player = g.players[pIndex]; - // mark as disconnected instead of removing when game started - if (g.started) { - player.connected = false; - console.log(`socket ${socket.id} disconnected (${reason}), marked player ${player.id} disconnected in ${gameId}`); + // schedule a delayed removal: if the player doesn't reconnect within 3s, consider them left + const key = pdKey(gameId, playerId); + if(pendingDisconnects.has(key)){ + clearTimeout(pendingDisconnects.get(key)); + pendingDisconnects.delete(key); + } + const t = setTimeout(() => { + // perform removal after grace period + const gIndex = games.findIndex(x => x.id === 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.emit('games-list', games); - return; - } - // if game not started, remove player as before - 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); + pendingDisconnects.delete(key); + }, 3000); + pendingDisconnects.set(key, t); }); }); diff --git a/frontend/public/game.html b/frontend/public/game.html index d0b5a5e..8d769f4 100644 --- a/frontend/public/game.html +++ b/frontend/public/game.html @@ -11,12 +11,15 @@ #board-wrapper{width:100%;max-width:900px;margin:0 auto} .board{display:grid;border:2px solid #333;box-sizing:border-box} .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.selected{outline:3px solid rgba(0,128,255,0.6);} /* make board square and responsive: use a wrapper with aspect-ratio */ #board-wrapper .board{width:100%;aspect-ratio:1/1} /* 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} diff --git a/frontend/public/index.html b/frontend/public/index.html index 97ae4fa..fef348b 100644 --- a/frontend/public/index.html +++ b/frontend/public/index.html @@ -45,5 +45,6 @@ + diff --git a/frontend/public/src/main.js b/frontend/public/src/main.js index 8216a6d..01a556c 100644 --- a/frontend/public/src/main.js +++ b/frontend/public/src/main.js @@ -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){ const li = document.createElement('li'); // 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) 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){ const span = document.createElement('span'); diff --git a/frontend/public/src/waiting.js b/frontend/public/src/waiting.js index 75b172d..e17b361 100644 --- a/frontend/public/src/waiting.js +++ b/frontend/public/src/waiting.js @@ -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; })(); if(stored) sock.emit('identify', { gameId, playerId: stored }); }); + sock.on('reconnect', (attempt) => { console.log('waiting socket reconnected', attempt); }); sock.on('game-deleted', (payload) => { if(payload && payload.gameId === gameId){ sessionStorage.removeItem(`playerId:${gameId}`); @@ -88,7 +89,10 @@ function tryConnectSocket(gameId){ }); sock.on('player-update', (g) => { if(!g) return; + console.log('[waiting.js] player-update received', g); 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) => { 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){ const ul = q('#wait-players'); ul.innerHTML = ''; if(!players || players.length === 0) ul.innerHTML = '
  • Aucun joueur
  • '; diff --git a/frontend/src/game.js b/frontend/src/game.js index 68e1944..ec0b0bc 100644 --- a/frontend/src/game.js +++ b/frontend/src/game.js @@ -12,12 +12,22 @@ function renderBoardFromState(state) { const boardEl = document.createElement('div'); 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 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++) { - const row = document.createElement('div'); - row.className = 'row'; for (let c = 0; c < w; c++) { const cell = document.createElement('div'); cell.className = 'cell'; @@ -44,7 +54,7 @@ function renderBoardFromState(state) { } cell.appendChild(piece); - row.appendChild(cell); + boardEl.appendChild(cell); // click to select/move cell.addEventListener('click', () => { // if no game/socket, ignore @@ -67,7 +77,6 @@ function renderBoardFromState(state) { } }); } - boardEl.appendChild(row); } container.appendChild(boardEl); }