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;
export function mountGame(el, 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);
scene.enterGameMode();
......@@ -51,7 +52,7 @@ export function mountGame(el, params) {
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);
endGame(isWin ? 'win' : data.result === 'draw' ? 'draw' : 'loss', 'resign');
endGame(isWin ? 'win' : data.result === 'draw' ? 'draw' : 'loss', 'resign', { serverAlreadyCompleted: true });
return;
}
......@@ -288,7 +289,18 @@ export function mountGame(el, params) {
handleLivePollData(el, 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') {
......@@ -296,16 +308,36 @@ export function mountGame(el, params) {
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;
// If no opponentId in params, get it from the match data
if (!opponentId && matchId) {
if (matchId) {
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');
// 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;
if (oppId) fetchAndRenderOpponent(el, oppId);
}
}).catch(() => {});
}
if (opponentId) {
......@@ -442,6 +474,11 @@ function executeMove(el, from, to, promotion) {
gameState.isPlayerTurn = false;
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());
return;
}
......@@ -753,7 +790,7 @@ function handleLivePollData(el, data) {
const isWin = (result === 'white_wins' && gameState.playerColor === 'w') ||
(result === 'black_wins' && gameState.playerColor === 'b');
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) {
action: 'draw',
match_id: gameState.matchId
});
endGame('draw', 'agreement');
endGame('draw', 'agreement', { serverAlreadyCompleted: true });
});
dialog.querySelector('#draw-reject').addEventListener('click', () => {
......@@ -877,7 +914,7 @@ function checkDrawResponse(el, rawGameState, myId) {
if (!gs) return;
if (gs.draw_accepted) {
endGame('draw', 'agreement');
endGame('draw', 'agreement', { serverAlreadyCompleted: true });
return;
}
......@@ -910,10 +947,10 @@ function fetchAndRenderOpponent(el, oppId) {
}).catch(() => {});
}
function endGame(result, reason) {
function endGame(result, reason, { serverAlreadyCompleted = false } = {}) {
if (gameState.gameOver) return;
mp.cleanup();
matchLive.session.destroy(); // Clear recovery so homepage doesn't try to rejoin
matchLive.session.destroy();
gameState.gameOver = true;
clock.stop();
board.interactive = false;
......@@ -958,6 +995,15 @@ function endGame(result, reason) {
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
const failsafeTimer = setTimeout(() => {
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