diff --git a/public/game.html b/public/game.html index 9c19d76..b9ccddb 100644 --- a/public/game.html +++ b/public/game.html @@ -147,6 +147,27 @@ }catch(_){ } } + // Robust card:play emitter with refresh fallback. + // Sometimes the client or network misses the room:update after playing a card — + // this helper emits card:play and requests a room refresh if no ack/update is received. + function emitCardPlay(payload, cb){ + try{ + if(!socket) return cb && cb({ error: 'no_socket' }); + let acked = false; + const timer = setTimeout(()=>{ + if(!acked){ + try{ socket.emit('room:refresh', { roomId }, ()=>{}); }catch(_){ } + } + }, 1500); + socket.emit('card:play', payload, (resp)=>{ + acked = true; try{ clearTimeout(timer); }catch(_){ } + // ask server to push a fresh room:update to ensure UIs are in sync + try{ socket.emit('room:refresh', { roomId }, ()=>{}); }catch(_){ } + if(cb) cb(resp); + }); + }catch(e){ if(cb) cb({ error: 'emit_failed' }); } + } + // play a short shuffle animation (shake + fade) when pieces are mixed function playShuffleAnimation(){ if(!boardEl) return; @@ -749,7 +770,7 @@ const occ = currentBoardState && Array.isArray(currentBoardState.pieces) && currentBoardState.pieces.find(p => p.square === square); if(!occ || occ.color === myShort){ log('select an enemy piece (second)'); return; } // emit swap payload: sourceSquare + targetSquare - socket.emit('card:play', { roomId, playerId: myPlayerId, cardId: pendingCard.cardId, payload: { sourceSquare: pendingCard.firstTarget, targetSquare: square } }, (resp)=>{ + emitCardPlay({ roomId, playerId: myPlayerId, cardId: pendingCard.cardId, payload: { sourceSquare: pendingCard.firstTarget, targetSquare: square } }, (resp)=>{ if(resp && resp.error){ log('card:play error', resp); showToast(resp.message || resp.error, { background: 'rgba(200,60,60,0.95)' }); } else { log('inversion played', resp); } }); @@ -761,7 +782,7 @@ const occupied = currentBoardState && Array.isArray(currentBoardState.pieces) && currentBoardState.pieces.find(p => p.square === square); if(occupied){ log('square not empty for mine placement'); return; } // send mine placement - socket.emit('card:play', { roomId, playerId: myPlayerId, cardId: pendingCard.cardId, payload: { targetSquare: square } }, (resp)=>{ + emitCardPlay({ roomId, playerId: myPlayerId, cardId: pendingCard.cardId, payload: { targetSquare: square } }, (resp)=>{ if(resp && resp.error){ log('card:play error', resp); showToast(resp.message || resp.error, { background: 'rgba(200,60,60,0.95)' }); } else { log('card played with target', resp); } }); @@ -770,10 +791,10 @@ const piece = currentBoardState && Array.isArray(currentBoardState.pieces) && currentBoardState.pieces.find(p => p.square === square); if(!piece || piece.color !== myShort){ log('select one of your pieces'); return; } if(piece.type && piece.type.toLowerCase() === 'k'){ log('cannot select king'); return; } - socket.emit('card:play', { roomId, playerId: myPlayerId, cardId: pendingCard.cardId, payload: { targetSquare: square } }, (resp)=>{ - if(resp && resp.error){ log('card:play error', resp); showToast(resp.message || resp.error, { background: 'rgba(200,60,60,0.95)' }); } - else { log('card played with target', resp); } - }); + emitCardPlay({ roomId, playerId: myPlayerId, cardId: pendingCard.cardId, payload: { targetSquare: square } }, (resp)=>{ + if(resp && resp.error){ log('card:play error', resp); showToast(resp.message || resp.error, { background: 'rgba(200,60,60,0.95)' }); } + else { log('card played with target', resp); } + }); pendingCard = null; clearTargetHighlights(); return; } else if(req === 'owned_pawn'){ // must be one of your pawns — show promotion choice modal before sending @@ -787,7 +808,7 @@ // canceled pendingCard = null; clearTargetHighlights(); return; } - socket.emit('card:play', { roomId, playerId: myPlayerId, cardId: pendingCard.cardId, payload: { targetSquare: square, promotion: promotionChoice } }, (resp)=>{ + emitCardPlay({ roomId, playerId: myPlayerId, cardId: pendingCard.cardId, payload: { targetSquare: square, promotion: promotionChoice } }, (resp)=>{ if(resp && resp.error){ log('card:play error', resp); showToast(resp.message || resp.error, { background: 'rgba(200,60,60,0.95)' }); } else { log('card played with promotion', resp); } }); @@ -798,7 +819,7 @@ const occ = currentBoardState && Array.isArray(currentBoardState.pieces) && currentBoardState.pieces.find(p => p.square === square); if(!occ || occ.color === myShort){ log('select an enemy piece'); return; } if(occ.type && occ.type.toLowerCase() === 'k'){ log('cannot select king'); return; } - socket.emit('card:play', { roomId, playerId: myPlayerId, cardId: pendingCard.cardId, payload: { targetSquare: square } }, (resp)=>{ + emitCardPlay({ roomId, playerId: myPlayerId, cardId: pendingCard.cardId, payload: { targetSquare: square } }, (resp)=>{ if(resp && resp.error){ log('card:play error', resp); showToast(resp.message || resp.error, { background: 'rgba(200,60,60,0.95)' }); } else { log('card played with target', resp); // after server applied the card effect, request legal moves for the selected piece so the client @@ -812,7 +833,7 @@ else if(req === 'enemy_queen'){ const occ = currentBoardState && Array.isArray(currentBoardState.pieces) && currentBoardState.pieces.find(p => p.square === square); if(!occ || occ.color === myShort || !occ.type || occ.type.toLowerCase() !== 'q'){ log('select an enemy queen'); return; } - socket.emit('card:play', { roomId, playerId: myPlayerId, cardId: pendingCard.cardId, payload: { targetSquare: square } }, (resp)=>{ + emitCardPlay({ roomId, playerId: myPlayerId, cardId: pendingCard.cardId, payload: { targetSquare: square } }, (resp)=>{ if(resp && resp.error){ log('card:play error', resp); showToast(resp.message || resp.error, { background: 'rgba(200,60,60,0.95)' }); } else { log('card played with target', resp); try{ setSelection(square); }catch(_){ } @@ -824,7 +845,7 @@ const occ = currentBoardState && Array.isArray(currentBoardState.pieces) && currentBoardState.pieces.find(p => p.square === square); if(!occ){ log('select a piece'); return; } if(occ.type && occ.type.toLowerCase() === 'k'){ log('cannot select king'); return; } - socket.emit('card:play', { roomId, playerId: myPlayerId, cardId: pendingCard.cardId, payload: { targetSquare: square } }, (resp)=>{ + emitCardPlay({ roomId, playerId: myPlayerId, cardId: pendingCard.cardId, payload: { targetSquare: square } }, (resp)=>{ if(resp && resp.error){ log('card:play error', resp); showToast(resp.message || resp.error, { background: 'rgba(200,60,60,0.95)' }); } else { log('card played with target', resp); } }); @@ -1189,16 +1210,16 @@ const opponents = Object.keys(handCounts || {}).filter(pid => pid !== myPlayerId); if(opponents.length === 1){ const targetPlayerId = opponents[0]; - socket.emit('card:play', { roomId, playerId: myPlayerId, cardId: cid, payload: { targetPlayerId } }, (resp)=>{ + emitCardPlay({ roomId, playerId: myPlayerId, cardId: cid, payload: { targetPlayerId } }, (resp)=>{ if(resp && resp.error){ log('card:play error', resp); showToast(resp.message || resp.error, { background: 'rgba(200,60,60,0.95)' }); } else { log('card played (steal card)', resp); } }); return; } // show modal to pick which player to steal from - showStealModal(handCounts, (targetPlayerId)=>{ + showStealModal(handCounts, (targetPlayerId)=>{ if(!targetPlayerId) return; // canceled - socket.emit('card:play', { roomId, playerId: myPlayerId, cardId: cid, payload: { targetPlayerId } }, (resp)=>{ + emitCardPlay({ roomId, playerId: myPlayerId, cardId: cid, payload: { targetPlayerId } }, (resp)=>{ if(resp && resp.error){ log('card:play error', resp); showToast(resp.message || resp.error, { background: 'rgba(200,60,60,0.95)' }); } else { log('card played (steal card)', resp); } }); @@ -1213,7 +1234,7 @@ } // immediate-play card (no target required) const payload = {}; - socket.emit('card:play', { roomId, playerId: myPlayerId, cardId: cid, payload }, (resp)=>{ + emitCardPlay({ roomId, playerId: myPlayerId, cardId: cid, payload }, (resp)=>{ if(resp && resp.error){ log('card:play error', resp); showToast(resp.message || resp.error, { background: 'rgba(200,60,60,0.95)' }); } else { log('card played', resp); } }); diff --git a/server.js b/server.js index 3ac43e0..6c8c26e 100644 --- a/server.js +++ b/server.js @@ -141,7 +141,12 @@ function maybeDrawAtTurnStart(room, playerId){ try{ if(!room) return; if(room.autoDraw){ - drawCardForPlayer(room, playerId); + const drawn = drawCardForPlayer(room, playerId); + // if nothing was drawn (deck empty, hand full, or noRemise), still push a room update + // so clients get the freshest state and don't remain out-of-sync. + if(!drawn){ + try{ sendRoomUpdate(room); }catch(_){ } + } } else { // ensure clients get the updated room state even if no draw happens sendRoomUpdate(room); @@ -1316,6 +1321,17 @@ io.on('connection', (socket) => { }catch(err){ console.error('room:deck:set error', err); return cb && cb({ error: 'server_error' }); } }); + // Client-requested refresh: allow client to ask server to resend the personalized room:update + socket.on('room:refresh', ({ roomId }, cb) => { + try{ + const room = rooms.get(roomId); + if(!room) return cb && cb({ error: 'room not found' }); + // only send personalized updates (sendRoomUpdate handles per-client privacy) + sendRoomUpdate(room); + return cb && cb({ ok: true }); + }catch(e){ console.error('room:refresh error', e); return cb && cb({ error: 'server_error' }); } + }); + socket.on('disconnect', () => { const roomId = socket.data.roomId; if (!roomId) return;