Commit 6790db43 authored by Mahmoud Aglan's avatar Mahmoud Aglan

fix: ensure result screens always show on win/loss in all games

Ludo:
- Add endingGame guard to prevent double endGame calls
- Parse server winners from live polling (fixes empty leaderboard
  when opponent wins remotely)
- Fix win/loss detection: last place = loss (was broken for 2-player)

Chess:
- Add 5s failsafe timer: result screen shows even if network hangs
- Deduplicate navigation logic with navigated flag
- Prevent double-navigation if both .then and failsafe fire

Backgammon:
- Add scene.exitGameMode() to both result navigation paths
  (was missing, could leave UI in game-mode state)
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent 439fd438
...@@ -572,6 +572,7 @@ function showGameResult(result) { ...@@ -572,6 +572,7 @@ function showGameResult(result) {
audio.play(result.winner === myColor ? 'sfx_bg_win_match' : 'lose', 'game'); audio.play(result.winner === myColor ? 'sfx_bg_win_match' : 'lose', 'game');
setTimeout(() => { setTimeout(() => {
if (params.mode === 'live') syncComplete(result); if (params.mode === 'live') syncComplete(result);
scene.exitGameMode();
scene.replace('backgammon-result', { scene.replace('backgammon-result', {
winner: result.winner === myColor ? 'you' : 'opponent', winner: result.winner === myColor ? 'you' : 'opponent',
scores: result.scores, matchLength: match.length, mode: params.mode scores: result.scores, matchLength: match.length, mode: params.mode
...@@ -585,6 +586,7 @@ function showGameResult(result) { ...@@ -585,6 +586,7 @@ function showGameResult(result) {
} }
function endMatchWithWin() { function endMatchWithWin() {
scene.exitGameMode();
scene.replace('backgammon-result', { scene.replace('backgammon-result', {
winner: 'you', scores: match.scores, matchLength: match.length, winner: 'you', scores: match.scores, matchLength: match.length,
mode: params.mode, reason: 'abandon' mode: params.mode, reason: 'abandon'
......
...@@ -904,6 +904,39 @@ function endGame(result, reason) { ...@@ -904,6 +904,39 @@ function endGame(result, reason) {
const botElos = { amina: 500, tarek: 900, nour: 1100, omar: 1300, layla: 1500, ziad: 1700, grandmaster: 2100 }; const botElos = { amina: 500, tarek: 900, nour: 1100, omar: 1300, layla: 1500, ziad: 1700, grandmaster: 2100 };
const opponentRating = botElos[gameState.botId] || 1200; const opponentRating = botElos[gameState.botId] || 1200;
let navigated = false;
function navigateToResult(ratingChange, ratingBefore, ratingAfter) {
if (navigated) return;
navigated = true;
scene.exitGameMode();
scene.replace('chess-result', {
result, reason, coins, xp, ratingChange,
ratingBefore, ratingAfter,
moves: gameState.moveCount,
pgn: engine.pgn(),
mode: gameState.mode,
botId: gameState.botId,
playerColor: gameState.playerColor,
capturedByPlayer: gameState.capturedByPlayer,
capturedByOpponent: gameState.capturedByOpponent,
moveHistory: gameState.moveHistory,
finalFen: engine.fen(),
tournamentId: gameState.tournamentId || null
});
bus.emit('game:ended', { gameKey: 'chess', result, reason, mode: gameState.mode });
if (gameState.tournamentId) {
bus.emit('tournament:match-ended', { tournamentId: gameState.tournamentId, result });
}
bus.emit('coins:earned', { amount: coins });
bus.emit('xp:earned', { amount: xp });
}
// Failsafe: always show result within 5s even if network hangs
const failsafeTimer = setTimeout(() => {
const fallbackRating = result === 'win' ? 12 : result === 'draw' ? 1 : -8;
navigateToResult(fallbackRating, null, null);
}, 5000);
net.post('game.php', { net.post('game.php', {
action: 'complete', action: 'complete',
match_id: gameState.matchId || 'local', match_id: gameState.matchId || 'local',
...@@ -913,62 +946,15 @@ function endGame(result, reason) { ...@@ -913,62 +946,15 @@ function endGame(result, reason) {
opponent_rating: opponentRating, opponent_rating: opponentRating,
time_control: gameState.timeControl || 'rapid_10_0' time_control: gameState.timeControl || 'rapid_10_0'
}).then(data => { }).then(data => {
clearTimeout(failsafeTimer);
const ratingChange = data?.rating_change || (result === 'win' ? 12 : result === 'draw' ? 1 : -8); const ratingChange = data?.rating_change || (result === 'win' ? 12 : result === 'draw' ? 1 : -8);
if (gameState.tournamentId) reportTournamentResult(result);
// Tournament mode: report result to tournament system setTimeout(() => navigateToResult(ratingChange, data?.rating_before, data?.rating_after), 1000);
if (gameState.tournamentId) {
reportTournamentResult(result);
}
setTimeout(() => {
scene.exitGameMode();
scene.replace('chess-result', {
result, reason, coins, xp, ratingChange,
ratingBefore: data?.rating_before,
ratingAfter: data?.rating_after,
moves: gameState.moveCount,
pgn: engine.pgn(),
mode: gameState.mode,
botId: gameState.botId,
playerColor: gameState.playerColor,
capturedByPlayer: gameState.capturedByPlayer,
capturedByOpponent: gameState.capturedByOpponent,
moveHistory: gameState.moveHistory,
finalFen: engine.fen(),
tournamentId: gameState.tournamentId || null
});
bus.emit('game:ended', { gameKey: 'chess', result, reason, mode: gameState.mode });
if (gameState.tournamentId) {
bus.emit('tournament:match-ended', { tournamentId: gameState.tournamentId, result });
}
bus.emit('coins:earned', { amount: coins });
bus.emit('xp:earned', { amount: xp });
}, 1000);
}).catch(() => { }).catch(() => {
if (gameState.tournamentId) { clearTimeout(failsafeTimer);
reportTournamentResult(result); if (gameState.tournamentId) reportTournamentResult(result);
} const fallbackRating = result === 'win' ? 12 : result === 'draw' ? 1 : -8;
setTimeout(() => { setTimeout(() => navigateToResult(fallbackRating, null, null), 1000);
scene.exitGameMode();
scene.replace('chess-result', {
result, reason, coins, xp,
ratingChange: result === 'win' ? 12 : result === 'draw' ? 1 : -8,
moves: gameState.moveCount,
pgn: engine.pgn(),
mode: gameState.mode,
botId: gameState.botId,
playerColor: gameState.playerColor,
capturedByPlayer: gameState.capturedByPlayer,
capturedByOpponent: gameState.capturedByOpponent,
moveHistory: gameState.moveHistory,
finalFen: engine.fen(),
tournamentId: gameState.tournamentId || null
});
bus.emit('game:ended', { gameKey: 'chess', result, reason, mode: gameState.mode });
if (gameState.tournamentId) {
bus.emit('tournament:match-ended', { tournamentId: gameState.tournamentId, result });
}
}, 1000);
}); });
} }
......
...@@ -104,6 +104,7 @@ export function mountGame(el, params) { ...@@ -104,6 +104,7 @@ export function mountGame(el, params) {
game.humanPlayers = PLAYER_NAMES.map((n, i) => n.startsWith('Bot') ? -1 : i).filter(i => i >= 0); game.humanPlayers = PLAYER_NAMES.map((n, i) => n.startsWith('Bot') ? -1 : i).filter(i => i >= 0);
validMoves = []; validMoves = [];
diceAnimating = false; diceAnimating = false;
endingGame = false;
lastSyncTurnCount = 0; lastSyncTurnCount = 0;
if (livePoller) { clearInterval(livePoller); livePoller = null; } if (livePoller) { clearInterval(livePoller); livePoller = null; }
...@@ -577,6 +578,21 @@ function startLudoPolling(el) { ...@@ -577,6 +578,21 @@ function startLudoPolling(el) {
// Check if game ended // Check if game ended
if (data.status === 'completed' && !game.gameOver) { if (data.status === 'completed' && !game.gameOver) {
// Parse server winners into local game.winners
if (data.winners) {
try {
const serverWinners = typeof data.winners === 'string' ? JSON.parse(data.winners) : data.winners;
if (Array.isArray(serverWinners) && serverWinners.length > 0) {
game.winners = serverWinners;
}
} catch (e) {}
}
// Fallback: if still no winners, derive from positions (who has all pieces finished)
if (!game.winners || game.winners.length === 0) {
game.winners = game.players
.map((p, i) => p.finished ? i : -1)
.filter(i => i >= 0);
}
game.gameOver = true; game.gameOver = true;
stopLudoPolling(); stopLudoPolling();
endGame(el); endGame(el);
...@@ -1382,7 +1398,10 @@ async function handleExit(el) { ...@@ -1382,7 +1398,10 @@ async function handleExit(el) {
bus.emit('game:ended', { gameKey: 'ludo', result: 'loss', resigned: true }); bus.emit('game:ended', { gameKey: 'ludo', result: 'loss', resigned: true });
} }
let endingGame = false;
function endGame(el) { function endGame(el) {
if (endingGame) return;
endingGame = true;
game.gameOver = true; game.gameOver = true;
clearTurnTimer(); clearTurnTimer();
stopRenderLoop(); stopRenderLoop();
...@@ -1391,9 +1410,9 @@ function endGame(el) { ...@@ -1391,9 +1410,9 @@ function endGame(el) {
if (matchId) localStorage.removeItem('el3ab_active_match'); if (matchId) localStorage.removeItem('el3ab_active_match');
const myRank = game.winners.indexOf(myPlayerIndex); const myRank = game.winners.indexOf(myPlayerIndex);
const isLoser = myRank === 3 || myRank === -1; const place = myRank >= 0 ? myRank + 1 : game.numPlayers;
const result = isLoser ? 'loss' : 'win'; const isLastPlace = place >= game.numPlayers || myRank === -1;
const place = myRank >= 0 ? myRank + 1 : 4; const result = isLastPlace ? 'loss' : 'win';
// Build ordered player IDs for rewards (1st → last) // Build ordered player IDs for rewards (1st → last)
const players = game.humanPlayers ? PLAYER_NAMES : ['bot_0','bot_1','bot_2','bot_3']; const players = game.humanPlayers ? PLAYER_NAMES : ['bot_0','bot_1','bot_2','bot_3'];
......
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