fixed lobby synchronisation
This commit is contained in:
parent
e285783f9f
commit
7688954ab4
6 changed files with 101 additions and 30 deletions
|
|
@ -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);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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');
|
||||||
|
|
|
||||||
|
|
@ -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>';
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue