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 = [];
|
||||
// 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);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
</style>
|
||||
</head>
|
||||
|
|
|
|||
|
|
@ -45,5 +45,6 @@
|
|||
</div>
|
||||
|
||||
<script src="src/main.js?v=2"></script>
|
||||
<script src="https://cdn.socket.io/4.5.4/socket.io.min.js"></script>
|
||||
</body>
|
||||
</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){
|
||||
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');
|
||||
|
|
|
|||
|
|
@ -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 = '<li>Aucun joueur</li>';
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue