Commit 1a19a25c authored by Mahmoud Aglan's avatar Mahmoud Aglan

fix: 3 critical chess multiplayer bugs (resign detection, color identity, checkmate sync)

Bug 1: Resign not detected - endGame now skips redundant complete API call
when server already completed the match (resign/draw/abandon via poll).
Bug 2: Players seeing themselves as opponent - verify and correct color
from authoritative server match data on game start.
Bug 3: Checkmate not appearing - send final move to server BEFORE calling
endGame so opponent receives the checkmate FEN position.
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent 7505311c
...@@ -21,7 +21,8 @@ let board, clock, gameState; ...@@ -21,7 +21,8 @@ let board, clock, gameState;
export function mountGame(el, params) { export function mountGame(el, params) {
const { mode = 'bot', botId = 'amina', timeControl = 'rapid_10_0', matchId, color } = params; const { mode = 'bot', botId = 'amina', timeControl = 'rapid_10_0', matchId, color } = params;
const playerColor = color || 'w'; // For live games without color, resolve from match data (prevents both players seeing white)
let playerColor = color || 'w';
const tc = parseTimeControl(timeControl); const tc = parseTimeControl(timeControl);
scene.enterGameMode(); scene.enterGameMode();
...@@ -51,7 +52,7 @@ export function mountGame(el, params) { ...@@ -51,7 +52,7 @@ export function mountGame(el, params) {
const myId = store.get('auth.userId'); const myId = store.get('auth.userId');
const isWhite = data.white_player_id === myId; const isWhite = data.white_player_id === myId;
const isWin = (data.result === 'white_wins' && isWhite) || (data.result === 'black_wins' && !isWhite); const isWin = (data.result === 'white_wins' && isWhite) || (data.result === 'black_wins' && !isWhite);
endGame(isWin ? 'win' : data.result === 'draw' ? 'draw' : 'loss', 'resign'); endGame(isWin ? 'win' : data.result === 'draw' ? 'draw' : 'loss', 'resign', { serverAlreadyCompleted: true });
return; return;
} }
...@@ -288,7 +289,18 @@ export function mountGame(el, params) { ...@@ -288,7 +289,18 @@ export function mountGame(el, params) {
handleLivePollData(el, data); handleLivePollData(el, data);
}, },
onGameEnd: (data) => { onGameEnd: (data) => {
if (!gameState.gameOver) endGame('loss', 'abandon'); // Already handled by handleLivePollData's status===completed check
// This is a fallback for edge cases (e.g., abandon detection)
if (gameState.gameOver) return;
if (data && data.result) {
const myId = store.get('auth.userId');
const isWhite = data.white_player_id === myId;
const isWin = (data.result === 'white_wins' && isWhite) || (data.result === 'black_wins' && !isWhite);
const isDraw = data.result === 'draw' || data.result === 'aborted';
endGame(isWin ? 'win' : isDraw ? 'draw' : 'loss', data.result.includes('abandon') ? 'abandon' : 'resign', { serverAlreadyCompleted: true });
} else {
endGame('loss', 'abandon', { serverAlreadyCompleted: true });
}
} }
}); });
if (playerColor === 'b') { if (playerColor === 'b') {
...@@ -296,16 +308,36 @@ export function mountGame(el, params) { ...@@ -296,16 +308,36 @@ export function mountGame(el, params) {
clock.start('w'); clock.start('w');
} }
// Fetch and render opponent profile bar (photo, name, level) // Fetch match data to verify color and render opponent profile
let opponentId = params.opponentId; let opponentId = params.opponentId;
// If no opponentId in params, get it from the match data if (matchId) {
if (!opponentId && matchId) {
net.post('game.php', { action: 'get', match_id: matchId }).then(matchData => { net.post('game.php', { action: 'get', match_id: matchId }).then(matchData => {
if (matchData && !matchData.error) { if (!matchData || matchData.error) return;
const myId = store.get('auth.userId'); const myId = store.get('auth.userId');
// Bug 2 fix: verify and correct player color from authoritative server data
const serverColor = matchData.white_player_id === myId ? 'w' : 'b';
if (gameState.playerColor !== serverColor) {
gameState.playerColor = serverColor;
gameState.isPlayerTurn = engine.turn() === serverColor;
if (board) {
board.flipped = serverColor === 'b';
board.canSelect = (piece) => {
if (gameState.gameOver || gameState.botThinking) return false;
if (!gameState.isPlayerTurn) return false;
const pieceColor = piece === piece.toUpperCase() ? 'w' : 'b';
return pieceColor === gameState.playerColor;
};
board.draw();
}
if (serverColor === 'b' && gameState.moveCount === 0) {
clock.start('w');
}
}
// Render opponent profile
const oppId = matchData.white_player_id === myId ? matchData.black_player_id : matchData.white_player_id; const oppId = matchData.white_player_id === myId ? matchData.black_player_id : matchData.white_player_id;
if (oppId) fetchAndRenderOpponent(el, oppId); if (oppId) fetchAndRenderOpponent(el, oppId);
}
}).catch(() => {}); }).catch(() => {});
} }
if (opponentId) { if (opponentId) {
...@@ -442,6 +474,11 @@ function executeMove(el, from, to, promotion) { ...@@ -442,6 +474,11 @@ function executeMove(el, from, to, promotion) {
gameState.isPlayerTurn = false; gameState.isPlayerTurn = false;
if (engine.isGameOver()) { if (engine.isGameOver()) {
// In live mode, send the final move BEFORE ending the game
// so the opponent receives the checkmate/stalemate position
if (gameState.mode === 'live') {
sendLiveMove(el);
}
endGame(engine.getResult(gameState.playerColor), getEndReason()); endGame(engine.getResult(gameState.playerColor), getEndReason());
return; return;
} }
...@@ -753,7 +790,7 @@ function handleLivePollData(el, data) { ...@@ -753,7 +790,7 @@ function handleLivePollData(el, data) {
const isWin = (result === 'white_wins' && gameState.playerColor === 'w') || const isWin = (result === 'white_wins' && gameState.playerColor === 'w') ||
(result === 'black_wins' && gameState.playerColor === 'b'); (result === 'black_wins' && gameState.playerColor === 'b');
const isDraw = result === 'draw' || result === 'aborted'; const isDraw = result === 'draw' || result === 'aborted';
endGame(isWin ? 'win' : isDraw ? 'draw' : 'loss', result === 'aborted' ? 'abandon' : 'resign'); endGame(isWin ? 'win' : isDraw ? 'draw' : 'loss', result === 'aborted' ? 'abandon' : 'resign', { serverAlreadyCompleted: true });
} }
} }
} }
...@@ -848,7 +885,7 @@ function checkDrawOffer(el, rawGameState, myId) { ...@@ -848,7 +885,7 @@ function checkDrawOffer(el, rawGameState, myId) {
action: 'draw', action: 'draw',
match_id: gameState.matchId match_id: gameState.matchId
}); });
endGame('draw', 'agreement'); endGame('draw', 'agreement', { serverAlreadyCompleted: true });
}); });
dialog.querySelector('#draw-reject').addEventListener('click', () => { dialog.querySelector('#draw-reject').addEventListener('click', () => {
...@@ -877,7 +914,7 @@ function checkDrawResponse(el, rawGameState, myId) { ...@@ -877,7 +914,7 @@ function checkDrawResponse(el, rawGameState, myId) {
if (!gs) return; if (!gs) return;
if (gs.draw_accepted) { if (gs.draw_accepted) {
endGame('draw', 'agreement'); endGame('draw', 'agreement', { serverAlreadyCompleted: true });
return; return;
} }
...@@ -910,10 +947,10 @@ function fetchAndRenderOpponent(el, oppId) { ...@@ -910,10 +947,10 @@ function fetchAndRenderOpponent(el, oppId) {
}).catch(() => {}); }).catch(() => {});
} }
function endGame(result, reason) { function endGame(result, reason, { serverAlreadyCompleted = false } = {}) {
if (gameState.gameOver) return; if (gameState.gameOver) return;
mp.cleanup(); mp.cleanup();
matchLive.session.destroy(); // Clear recovery so homepage doesn't try to rejoin matchLive.session.destroy();
gameState.gameOver = true; gameState.gameOver = true;
clock.stop(); clock.stop();
board.interactive = false; board.interactive = false;
...@@ -958,6 +995,15 @@ function endGame(result, reason) { ...@@ -958,6 +995,15 @@ function endGame(result, reason) {
bus.emit('xp:earned', { amount: xp }); bus.emit('xp:earned', { amount: xp });
} }
// If the server already completed this match (opponent resigned/drew/abandoned),
// skip redundant complete call — just show the result
if (serverAlreadyCompleted) {
const fallbackRating = result === 'win' ? 12 : result === 'draw' ? 1 : -8;
if (gameState.tournamentId) reportTournamentResult(result);
setTimeout(() => navigateToResult(fallbackRating, null, null), 1000);
return;
}
// Failsafe: always show result within 5s even if network hangs // Failsafe: always show result within 5s even if network hangs
const failsafeTimer = setTimeout(() => { const failsafeTimer = setTimeout(() => {
const fallbackRating = result === 'win' ? 12 : result === 'draw' ? 1 : -8; const fallbackRating = result === 'win' ? 12 : result === 'draw' ? 1 : -8;
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment