Commit 7df3c582 authored by Mahmoud Aglan's avatar Mahmoud Aglan

fix: domino sync bugs + backgammon logic bugs + HUD token vars

Domino:
- dealAndSyncToServer: retry once on failure, abort to room on 2nd fail (#100)
- syncPassToServer: only increment moveCount after success (#41)
- drawFromBoneyard: debounce with drawInFlight flag (#42)
- endMatch: guard empty players array, fallback to match IDs (#209)

Backgammon:
- createGame: fallback to sheshbesh if variant key missing (#26)
- handleServerState: selective merge instead of Object.assign (#56)

Core:
- multiplayer.js: mark startDisconnectWatch as deprecated (#229)
- core.css: replace hardcoded HUD values with token variables
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent 8c593ef8
......@@ -53,7 +53,7 @@ html, body {
display: flex;
align-items: center;
justify-content: space-between;
padding-inline: var(--s-4);
padding-inline: var(--hud-padding-x);
background: var(--bg-base);
border-bottom: 1px solid var(--border);
transition: transform var(--dur-normal) var(--ease-out);
......@@ -72,11 +72,11 @@ html, body {
}
.hud-avatar {
width: 34px;
height: 34px;
width: var(--hud-avatar-size);
height: var(--hud-avatar-size);
border-radius: 50%;
background: var(--bg-elevated);
border: 2px solid var(--gold);
border: var(--hud-avatar-border) solid var(--gold);
display: flex;
align-items: center;
justify-content: center;
......@@ -107,7 +107,7 @@ html, body {
.hud-stats {
display: flex;
align-items: center;
gap: var(--s-4);
gap: var(--hud-stat-gap);
}
.hud-stat {
......@@ -115,7 +115,7 @@ html, body {
align-items: center;
gap: var(--s-1);
font-family: var(--font-lat);
font-size: 14px;
font-size: var(--hud-stat-font);
font-weight: 700;
}
......@@ -130,8 +130,8 @@ html, body {
}
.hud-btn {
width: 44px;
height: 44px;
width: var(--hud-btn-size);
height: var(--hud-btn-size);
border-radius: 50%;
background: var(--bg-elevated);
display: flex;
......@@ -148,12 +148,12 @@ html, body {
position: absolute;
top: -2px;
right: -2px;
min-width: 16px;
height: 16px;
line-height: 16px;
min-width: var(--hud-badge-size);
height: var(--hud-badge-size);
line-height: var(--hud-badge-size);
background: var(--orange);
color: white;
font-size: 9px;
font-size: var(--hud-badge-font);
font-weight: 700;
font-family: var(--font-lat);
border-radius: var(--r-full);
......@@ -228,16 +228,16 @@ html, body {
display: flex;
flex-direction: column;
align-items: center;
gap: 3px;
padding: var(--s-2) var(--s-3);
gap: var(--tab-item-gap);
padding: var(--tab-item-padding-y) var(--tab-item-padding-x);
border-radius: var(--r-md);
color: var(--text-muted);
font-size: 11px;
font-size: var(--tab-label-font);
font-weight: 600;
cursor: pointer;
transition: color var(--dur-fast), transform var(--dur-fast);
min-width: 48px;
min-height: 48px;
min-width: var(--tab-item-min-size);
min-height: var(--tab-item-min-size);
justify-content: center;
}
......@@ -246,9 +246,10 @@ html, body {
.tab-item.active .tab-icon { filter: drop-shadow(0 0 6px var(--gold)); }
.tab-icon {
width: 24px; height: 24px;
width: var(--tab-icon-size); height: var(--tab-icon-size);
fill: currentColor;
}
.tab-icon img { width: 100%; height: 100%; object-fit: contain; }
/* Scene transitions - game feel */
.scene {
......
......@@ -179,6 +179,7 @@ export function updateConnectionStatus(isConnected) {
lastOpponentPing = Date.now();
}
// Deprecated: use match-session.js startDisconnectWatch instead
export function startDisconnectWatch(matchId, matchType, timeoutMs = 60000) {
currentMatchId = matchId;
currentMatchType = matchType;
......@@ -188,11 +189,9 @@ export function startDisconnectWatch(matchId, matchType, timeoutMs = 60000) {
const status = document.getElementById('mp-opponent-status');
if (elapsed > timeoutMs) {
// Opponent disconnected too long — claim win
if (dot) dot.style.background = 'var(--error)';
if (status) status.textContent = t('game.opponent_disconnected');
clearInterval(disconnectTimer);
// Could auto-claim win here
} else if (elapsed > 15000) {
if (dot) dot.style.background = 'var(--amber)';
if (status) status.textContent = t('game.weak_connection');
......
......@@ -154,7 +154,11 @@ export function getVariants() {
// ===== GAME =====
export function createGame(variant = 'sheshbesh') {
const rule = VARIANTS[variant];
const rule = VARIANTS[variant] || VARIANTS.sheshbesh;
if (!VARIANTS[variant]) {
console.warn(`[backgammon] Unknown variant "${variant}", falling back to sheshbesh`);
variant = 'sheshbesh';
}
const state = createState();
rule.resetState(state);
......@@ -462,7 +466,7 @@ function countAtHigherPos(state, normPosition, type, rule) {
}
function calculatePlayableMoves(state, moves, type, variant) {
const rule = VARIANTS[variant];
const rule = VARIANTS[variant] || VARIANTS.sheshbesh;
const weight = calcMoveWeights(state, moves, type, rule, null, true);
return weight.playableMoves.length > 0 ? weight.playableMoves : [];
}
......
......@@ -634,13 +634,28 @@ function handleServerState(data) {
matchSession.markOpponentActive();
if (data.game_state) {
const gs = data.game_state;
// Only merge authoritative server fields — preserve local-only state
// (selectedPiece, highlights, isRolling, isAnimating, animatingChecker, particles)
if (gs.state) game.state = gs.state;
if (gs.dice) game.dice = gs.dice;
if (gs.movesLeft) game.movesLeft = gs.movesLeft;
if (gs.movesPlayed) game.movesPlayed = gs.movesPlayed;
if (gs.turn !== undefined) game.turn = gs.turn;
if (gs.gameOver !== undefined) game.gameOver = gs.gameOver;
if (gs.turnNumber !== undefined) game.turnNumber = gs.turnNumber;
if (gs.isOver !== undefined) game.isOver = gs.isOver;
if (gs.winner !== undefined) game.winner = gs.winner;
if (gs.score !== undefined) game.score = gs.score;
if (data.match_state) match.scores = data.match_state.scores || match.scores;
if (data.cube) match.cube = data.cube;
clearSelection();
updateUI();
if (game.isOver) { handleGameOver(); return; }
if (game.turn === myColor && game.movesLeft && game.movesLeft.length === 0 && game.dice) {
// Opponent finished their turn, now it's ours
game.dice = null;
}
}
}
......
......@@ -18,6 +18,7 @@ import { DominoDrag } from '../components/drag.js';
let state, board, hand, drag, liveSession;
let botTimeout = null;
let autoPassTimeout = null;
let drawInFlight = false;
export function mountGame(el, params) {
const { mode = 'bot', matchId, playerIndex = 0, players, botLevel = 'intermediate', targetScore } = params;
......@@ -369,7 +370,7 @@ function startHeartbeat() {
}, 10000);
}
async function dealAndSyncToServer(el, matchId) {
async function dealAndSyncToServer(el, matchId, retryCount = 0) {
dealNewRound();
board.setChain(state.chain);
updateUI(el);
......@@ -394,8 +395,16 @@ async function dealAndSyncToServer(el, matchId) {
});
} catch (e) {
console.error('[domino] deal sync failed:', e);
bus.emit('toast', { text: 'Sync error — retrying...', duration: 2000 });
setTimeout(() => dealAndSyncToServer(el, matchId), 2000);
if (retryCount < 1) {
bus.emit('toast', { text: t('game.sync_error_retrying') || 'Sync error — retrying...', duration: 2000 });
setTimeout(() => dealAndSyncToServer(el, matchId, retryCount + 1), 2000);
} else {
bus.emit('toast', { text: t('game.sync_failed') || 'Connection failed — returning to room.', duration: 3000 });
setTimeout(() => {
scene.exitGameMode();
scene.replace('domino-room', { mode: 'menu' });
}, 1500);
}
}
}
......@@ -611,21 +620,24 @@ async function syncDrawToServer() {
async function syncPassToServer() {
if (state.mode !== 'live' || !state.matchId) return;
const moveNum = state.moveCount + 1;
const pendingMoveCount = state.moveCount + 1;
try {
await net.post('domino-match.php', {
action: 'move',
match_id: state.matchId,
current_turn: state.currentPlayer,
game_state: JSON.stringify({
move_count: moveNum,
move_count: pendingMoveCount,
last_move: { player: state.myPlayerIndex, action: 'pass', t: Date.now() },
left_end: state.leftEnd,
right_end: state.rightEnd,
})
});
state.moveCount = moveNum;
} catch (e) {}
// Only increment after successful server response
state.moveCount = pendingMoveCount;
} catch (e) {
console.warn('[domino] syncPassToServer failed, moveCount not incremented:', e);
}
}
async function syncRoundEndToServer(winnerIdx, roundPoints) {
......@@ -831,13 +843,14 @@ function executePlacement(el, tile, end) {
}
async function drawFromBoneyard(el) {
if (state.drawing) return;
if (state.drawing || drawInFlight) return;
if (state.currentPlayer !== state.myPlayerIndex) return;
if (state.boneyard.length === 0) return;
if (state.gameOver) return;
if (rules.hasValidMove(state.hands[state.myPlayerIndex], state.leftEnd, state.rightEnd)) return;
state.drawing = true;
drawInFlight = true;
while (state.boneyard.length > 0 && !rules.hasValidMove(state.hands[state.myPlayerIndex], state.leftEnd, state.rightEnd)) {
const tile = state.boneyard.pop();
......@@ -849,7 +862,11 @@ async function drawFromBoneyard(el) {
await new Promise(r => setTimeout(r, 400));
}
syncDrawToServer();
try {
await syncDrawToServer();
} finally {
drawInFlight = false;
}
state.drawing = false;
if (!rules.hasValidMove(state.hands[state.myPlayerIndex], state.leftEnd, state.rightEnd) && state.boneyard.length === 0) {
......@@ -1025,8 +1042,11 @@ function endMatch(el, result, reason) {
if (state.matchId && state.mode === 'live') {
const myId = store.get('auth.userId');
const players = Array.isArray(state.players) ? state.players : [];
const oppId = players.find(p => p !== myId) || state.opponentId || '';
const players = Array.isArray(state.players) && state.players.length > 0 ? state.players : null;
const oppId = (players && players.find(p => p !== myId))
|| state.opponentId
|| store.get(`match.${state.matchId}.opponentId`)
|| '';
if (myId && oppId) {
const winners = result === 'win' ? [myId, oppId] : [oppId, myId];
net.post('domino-match.php', {
......@@ -1035,6 +1055,8 @@ function endMatch(el, result, reason) {
winners,
reason
}).catch(() => {});
} else {
console.warn('[domino] endMatch: cannot determine opponent ID, skipping complete call');
}
}
......@@ -1313,4 +1335,5 @@ export function unmountGame() {
drag?.destroy();
state = null;
liveSession = null;
drawInFlight = false;
}
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