Commit b27df183 authored by Mahmoud Aglan's avatar Mahmoud Aglan

feat(chess): full gameplay polish — move list, captured pieces, promotion...

feat(chess): full gameplay polish — move list, captured pieces, promotion dialog, bot thinking indicator, clock urgency, improved result screen

- Move list: scrollable horizontal move notation (SAN)
- Captured pieces: sorted display below each player name
- Promotion dialog: visual piece picker (Q/R/B/N) instead of auto-queen
- Bot thinking indicator: animated dots overlay during Stockfish response
- Clock urgency: red pulsing animation when time < 30s
- Resign confirmation: prevents accidental resign
- Result screen: proper rating change, coins, XP, move history, analysis button
- Sound: differentiated for check, checkmate, castle, capture, normal move
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent c1234161
......@@ -20,51 +20,82 @@ export function mountGame(el, params) {
gameState = {
mode, botId, matchId, playerColor,
isPlayerTurn: playerColor === 'w',
gameOver: false, moveCount: 0
gameOver: false, moveCount: 0,
capturedByPlayer: [], capturedByOpponent: [],
moveHistory: [], botThinking: false
};
engine.create();
clock = new ChessClock(tc.time, tc.increment);
el.innerHTML = `
<div style="display:flex;flex-direction:column;height:100%;background:var(--bg-deep);">
<div class="chess-opponent-bar" style="display:flex;align-items:center;justify-content:space-between;padding:var(--s-2) var(--s-3);background:var(--bg-base);">
<div style="display:flex;align-items:center;gap:var(--s-2);">
<div style="width:32px;height:32px;border-radius:50%;background:var(--bg-elevated);display:flex;align-items:center;justify-content:center;font-size:16px;">🤖</div>
<span style="font-size:14px;font-weight:600;" id="opponent-name">${mode === 'bot' ? (botId || 'Bot') : 'Opponent'}</span>
<div class="chess-layout" style="display:flex;flex-direction:column;height:100%;background:#1a1a2e;">
<!-- Opponent Bar -->
<div class="chess-bar" style="display:flex;align-items:center;justify-content:space-between;padding:6px 12px;background:#0f0f1e;">
<div style="display:flex;align-items:center;gap:8px;">
<div style="width:32px;height:32px;border-radius:50%;background:#2a2a4a;display:flex;align-items:center;justify-content:center;overflow:hidden;">
<img src="https://stockfishapi.caprover.al-arcade.com/portraits/${botId || 'amina'}.png" style="width:100%;height:100%;object-fit:cover;" onerror="this.style.display='none';this.parentNode.textContent='🤖'">
</div>
<div>
<div style="font-size:13px;font-weight:600;color:#f8fafc;" id="opponent-name">${mode === 'bot' ? (botId || 'Bot') : 'Opponent'}</div>
<div id="opponent-captured" style="font-size:11px;color:#94a3b8;min-height:14px;letter-spacing:1px;"></div>
</div>
</div>
<div id="clock-opponent" style="font-size:18px;font-weight:700;font-family:var(--font-lat);background:var(--bg-elevated);padding:var(--s-1) var(--s-3);border-radius:var(--r-sm);">${clock.format(tc.time)}</div>
<div id="clock-opponent" class="chess-clock" style="font-size:18px;font-weight:700;font-family:Inter,monospace;background:#1e1e3a;padding:4px 12px;border-radius:6px;color:#f8fafc;min-width:60px;text-align:center;">${clock.format(tc.time)}</div>
</div>
<div id="board-container" style="flex:1;display:flex;align-items:center;justify-content:center;padding:var(--s-2);"></div>
<!-- Board -->
<div id="board-container" style="flex:1;display:flex;align-items:center;justify-content:center;padding:4px;position:relative;">
<div id="bot-thinking" style="display:none;position:absolute;top:8px;left:50%;transform:translateX(-50%);background:rgba(0,0,0,0.8);color:#E4AC38;padding:4px 12px;border-radius:12px;font-size:12px;font-weight:600;z-index:10;">
${t('game.thinking')} <span class="pulse">●●●</span>
</div>
<div id="promo-dialog" style="display:none;position:absolute;z-index:20;background:#1e1e3a;border-radius:8px;padding:8px;box-shadow:0 8px 32px rgba(0,0,0,0.8);border:1px solid rgba(255,255,255,0.1);"></div>
</div>
<div class="chess-player-bar" style="display:flex;align-items:center;justify-content:space-between;padding:var(--s-2) var(--s-3);background:var(--bg-base);">
<div style="display:flex;align-items:center;gap:var(--s-2);">
<div style="width:32px;height:32px;border-radius:50%;background:var(--bg-elevated);display:flex;align-items:center;justify-content:center;font-size:14px;">👤</div>
<span style="font-size:14px;font-weight:600;">${store.get('player.username') || 'You'}</span>
<!-- Player Bar -->
<div class="chess-bar" style="display:flex;align-items:center;justify-content:space-between;padding:6px 12px;background:#0f0f1e;">
<div style="display:flex;align-items:center;gap:8px;">
<div style="width:32px;height:32px;border-radius:50%;background:#2a2a4a;display:flex;align-items:center;justify-content:center;font-size:14px;color:#f8fafc;">👤</div>
<div>
<div style="font-size:13px;font-weight:600;color:#f8fafc;">${store.get('player.username') || store.get('player.display_name') || 'You'}</div>
<div id="player-captured" style="font-size:11px;color:#94a3b8;min-height:14px;letter-spacing:1px;"></div>
</div>
</div>
<div id="clock-player" style="font-size:18px;font-weight:700;font-family:var(--font-lat);background:var(--bg-elevated);padding:var(--s-1) var(--s-3);border-radius:var(--r-sm);">${clock.format(tc.time)}</div>
<div id="clock-player" class="chess-clock" style="font-size:18px;font-weight:700;font-family:Inter,monospace;background:#1e1e3a;padding:4px 12px;border-radius:6px;color:#f8fafc;min-width:60px;text-align:center;">${clock.format(tc.time)}</div>
</div>
<div style="display:flex;gap:var(--s-2);padding:var(--s-2) var(--s-3);background:var(--bg-base);border-top:1px solid var(--border);">
<button class="btn btn-secondary" id="btn-resign" style="flex:1;font-size:13px;">${t('game.resign')}</button>
<button class="btn btn-secondary" id="btn-draw" style="flex:1;font-size:13px;">${t('game.draw')}</button>
<button class="btn btn-secondary" id="btn-flip" style="flex:1;font-size:13px;">${t('game.flip')}</button>
<!-- Move List -->
<div id="move-list" style="max-height:48px;overflow-x:auto;white-space:nowrap;padding:4px 12px;background:#0f0f1e;border-top:1px solid rgba(255,255,255,0.05);font-family:Inter,monospace;font-size:12px;color:#94a3b8;display:flex;gap:4px;align-items:center;">
<span style="color:#475569;">1.</span>
</div>
<!-- Controls -->
<div style="display:flex;gap:6px;padding:8px 12px;background:#0f0f1e;border-top:1px solid rgba(255,255,255,0.05);">
<button class="ctrl-btn" id="btn-resign">⚐ ${t('game.resign')}</button>
<button class="ctrl-btn" id="btn-draw">½ ${t('game.draw')}</button>
<button class="ctrl-btn" id="btn-flip">⟲ ${t('game.flip')}</button>
</div>
</div>
<style>
.ctrl-btn { flex:1;background:#1e1e3a;border:1px solid rgba(255,255,255,0.08);color:#94a3b8;font-size:12px;font-weight:600;padding:8px;border-radius:6px;cursor:pointer;font-family:inherit;transition:background 0.15s; }
.ctrl-btn:active { background:#2a2a5a;transform:scale(0.97); }
.chess-clock.low-time { color:#EF4444!important;animation:clockPulse 1s infinite; }
@keyframes clockPulse { 0%,100%{opacity:1}50%{opacity:0.5} }
</style>
`;
const boardContainer = el.querySelector('#board-container');
board = new ChessBoard(boardContainer, {
flipped: playerColor === 'b',
interactive: true,
onMove: (from, to) => handlePlayerMove(from, to)
onMove: (from, to) => handlePlayerMove(el, from, to)
});
board.canSelect = (piece) => {
if (gameState.gameOver) return false;
if (gameState.gameOver || gameState.botThinking) return false;
if (!gameState.isPlayerTurn) return false;
return piece[0] === playerColor;
const pieceColor = piece === piece.toUpperCase() ? 'w' : 'b';
return pieceColor === playerColor;
};
board.showLegalMoves = (square) => {
......@@ -73,83 +104,128 @@ export function mountGame(el, params) {
board.setPosition(engine.fen());
// Clock
clock.onTick = (w, b) => {
const playerClock = el.querySelector('#clock-player');
const opponentClock = el.querySelector('#clock-opponent');
const playerEl = el.querySelector('#clock-player');
const opponentEl = el.querySelector('#clock-opponent');
if (playerColor === 'w') {
playerClock.textContent = clock.format(w);
opponentClock.textContent = clock.format(b);
if (clock.isLowTime('w')) playerClock.style.color = 'var(--error)';
if (clock.isLowTime('b')) opponentClock.style.color = 'var(--error)';
playerEl.textContent = clock.format(w);
opponentEl.textContent = clock.format(b);
playerEl.classList.toggle('low-time', clock.isLowTime('w'));
opponentEl.classList.toggle('low-time', clock.isLowTime('b'));
} else {
playerClock.textContent = clock.format(b);
opponentClock.textContent = clock.format(w);
if (clock.isLowTime('b')) playerClock.style.color = 'var(--error)';
if (clock.isLowTime('w')) opponentClock.style.color = 'var(--error)';
playerEl.textContent = clock.format(b);
opponentEl.textContent = clock.format(w);
playerEl.classList.toggle('low-time', clock.isLowTime('b'));
opponentEl.classList.toggle('low-time', clock.isLowTime('w'));
}
};
clock.onFlag = (color) => {
endGame(color === playerColor ? 'loss' : 'win', 'timeout');
};
clock.onFlag = (color) => endGame(color === playerColor ? 'loss' : 'win', 'timeout');
// Controls
el.querySelector('#btn-resign').addEventListener('click', () => {
if (gameState.gameOver) return;
audio.play('click');
endGame('loss', 'resign');
});
el.querySelector('#btn-draw').addEventListener('click', () => {
audio.play('click');
});
el.querySelector('#btn-flip').addEventListener('click', () => {
audio.play('click');
board.flip();
if (confirm('هل أنت متأكد من الاستسلام؟')) {
audio.play('gameOver', 'game');
endGame('loss', 'resign');
}
});
el.querySelector('#btn-draw').addEventListener('click', () => audio.play('click'));
el.querySelector('#btn-flip').addEventListener('click', () => { audio.play('click'); board.flip(); });
// Start bot if playing as black
if (playerColor === 'b' && mode === 'bot') {
requestBotMove();
clock.start('w');
requestBotMove(el);
}
bus.emit('game:started', { gameKey: 'chess', matchId, opponent: botId, mode });
}
async function handlePlayerMove(from, to) {
function handlePlayerMove(el, from, to) {
const moves = engine.legalMoves(from);
const isLegal = moves.some(m => m.to === to);
if (!isLegal) return;
const legalMove = moves.find(m => m.to === to);
if (!legalMove) return;
let promotion = undefined;
// Check promotion
const piece = engine.get().get(from);
if (piece && piece.type === 'p') {
const rank = to[1];
if ((piece.color === 'w' && rank === '8') || (piece.color === 'b' && rank === '1')) {
promotion = 'q';
showPromotionDialog(el, from, to, piece.color);
return;
}
}
const m = engine.move(from, to, promotion);
executeMove(el, from, to);
}
function showPromotionDialog(el, from, to, color) {
const dialog = el.querySelector('#promo-dialog');
const pieces = ['q', 'r', 'b', 'n'];
const names = { q: '♛', r: '♜', b: '♝', n: '♞' };
if (color === 'w') { names.q = '♕'; names.r = '♖'; names.b = '♗'; names.n = '♘'; }
dialog.style.display = 'flex';
dialog.style.gap = '8px';
dialog.innerHTML = pieces.map(p => `
<div data-piece="${p}" style="width:44px;height:44px;display:flex;align-items:center;justify-content:center;font-size:28px;cursor:pointer;border-radius:6px;background:#2a2a5a;border:1px solid rgba(255,255,255,0.1);transition:background 0.15s;"
onmouseenter="this.style.background='#3a3a7a'"
onmouseleave="this.style.background='#2a2a5a'">
${names[p]}
</div>
`).join('');
dialog.querySelectorAll('[data-piece]').forEach(btn => {
btn.addEventListener('click', () => {
dialog.style.display = 'none';
executeMove(el, from, to, btn.dataset.piece);
});
});
}
function executeMove(el, from, to, promotion) {
const m = engine.move(from, to, promotion || undefined);
if (!m) return;
gameState.moveCount++;
board.setPosition(engine.fen());
board.setLastMove(from, to);
if (m.captured) audio.play('capture', 'game');
// Sound
if (m.san.includes('#')) audio.play('gameOver', 'game');
else if (m.san.includes('+')) audio.play('check', 'game');
else if (m.captured) audio.play('capture', 'game');
else if (m.san === 'O-O' || m.san === 'O-O-O') audio.play('castle', 'game');
else audio.play('move', 'game');
// Captured pieces
if (m.captured) {
const capturedPiece = m.color === gameState.playerColor
? m.captured : m.captured.toUpperCase ? m.captured : m.captured;
if (m.color === gameState.playerColor) {
gameState.capturedByPlayer.push(m.captured);
} else {
gameState.capturedByOpponent.push(m.captured);
}
updateCapturedDisplay(el);
}
// Check indicator
if (engine.isCheck()) {
const kingSquare = findKing(engine.turn());
board.setCheck(kingSquare);
board.setCheck(findKing(engine.turn()));
} else {
board.highlights.check = null;
board.draw();
}
// Move list
updateMoveList(el, m);
// Clock
if (gameState.moveCount === 1) {
clock.start(gameState.playerColor === 'w' ? 'b' : 'w');
clock.start(engine.turn());
} else {
clock.switch();
}
......@@ -157,17 +233,20 @@ async function handlePlayerMove(from, to) {
gameState.isPlayerTurn = false;
if (engine.isGameOver()) {
const result = engine.getResult(gameState.playerColor);
endGame(result, getEndReason());
endGame(engine.getResult(gameState.playerColor), getEndReason());
return;
}
if (gameState.mode === 'bot') {
requestBotMove();
requestBotMove(el);
}
}
async function requestBotMove() {
async function requestBotMove(el) {
gameState.botThinking = true;
const thinkingEl = el.querySelector('#bot-thinking');
thinkingEl.style.display = 'block';
try {
const data = await net.post('bots.php', {
action: 'move',
......@@ -175,43 +254,90 @@ async function requestBotMove() {
bot_id: gameState.botId
});
thinkingEl.style.display = 'none';
gameState.botThinking = false;
if (data.best_move && !gameState.gameOver) {
const m = engine.moveUci(data.best_move);
const from = data.best_move.substring(0, 2);
const to = data.best_move.substring(2, 4);
const promo = data.best_move.length > 4 ? data.best_move[4] : undefined;
const m = engine.move(from, to, promo);
if (m) {
board.setPosition(engine.fen());
board.setLastMove(m.from, m.to);
board.setLastMove(from, to);
if (m.captured) audio.play('capture', 'game');
if (m.san.includes('#')) audio.play('gameOver', 'game');
else if (m.san.includes('+')) audio.play('check', 'game');
else if (m.captured) audio.play('capture', 'game');
else if (m.san === 'O-O' || m.san === 'O-O-O') audio.play('castle', 'game');
else audio.play('move', 'game');
if (m.captured) {
gameState.capturedByOpponent.push(m.captured);
updateCapturedDisplay(el);
}
if (engine.isCheck()) {
const kingSquare = findKing(engine.turn());
board.setCheck(kingSquare);
board.setCheck(findKing(engine.turn()));
} else {
board.highlights.check = null;
board.draw();
}
updateMoveList(el, m);
clock.switch();
gameState.isPlayerTurn = true;
gameState.moveCount++;
if (engine.isGameOver()) {
const result = engine.getResult(gameState.playerColor);
endGame(result, getEndReason());
endGame(engine.getResult(gameState.playerColor), getEndReason());
}
}
}
} catch (e) {
setTimeout(requestBotMove, 2000);
thinkingEl.style.display = 'none';
gameState.botThinking = false;
setTimeout(() => requestBotMove(el), 2000);
}
}
function updateCapturedDisplay(el) {
const pieceSymbols = { p: '♟', n: '♞', b: '♝', r: '♜', q: '♛', k: '♚' };
const pieceOrder = ['q', 'r', 'b', 'n', 'p'];
const sortCaptures = (arr) => arr.sort((a, b) => pieceOrder.indexOf(a) - pieceOrder.indexOf(b));
const playerCaptures = sortCaptures([...gameState.capturedByPlayer]);
const opponentCaptures = sortCaptures([...gameState.capturedByOpponent]);
el.querySelector('#player-captured').textContent = playerCaptures.map(p => pieceSymbols[p] || p).join('');
el.querySelector('#opponent-captured').textContent = opponentCaptures.map(p => pieceSymbols[p] || p).join('');
}
function updateMoveList(el, m) {
gameState.moveHistory.push(m);
const moveList = el.querySelector('#move-list');
const history = gameState.moveHistory;
let html = '';
for (let i = 0; i < history.length; i += 2) {
const moveNum = Math.floor(i / 2) + 1;
const white = history[i]?.san || '';
const black = history[i + 1]?.san || '';
html += `<span style="color:#475569;">${moveNum}.</span>`;
html += `<span style="color:#f8fafc;font-weight:500;">${white}</span>`;
if (black) html += `<span style="color:#e2e8f0;">${black}</span>`;
}
moveList.innerHTML = html;
moveList.scrollLeft = moveList.scrollWidth;
}
function findKing(color) {
const board_state = engine.get().board();
const boardState = engine.get().board();
for (let r = 0; r < 8; r++) {
for (let c = 0; c < 8; c++) {
const p = board_state[r][c];
const p = boardState[r][c];
if (p && p.type === 'k' && p.color === color) {
return String.fromCharCode(97 + c) + String(8 - r);
}
......@@ -238,15 +364,28 @@ function endGame(result, reason) {
else if (result === 'loss') audio.play('lose', 'game');
else audio.play('gameOver', 'game');
// Calculate coins/XP
const coins = result === 'win' ? 50 : result === 'draw' ? 20 : 10;
const xp = gameState.moveCount * 2;
setTimeout(() => {
scene.exitGameMode();
scene.replace('chess-result', {
result, reason,
result, reason, coins, xp,
moves: gameState.moveCount,
pgn: engine.pgn(),
mode: gameState.mode,
botId: gameState.botId
botId: gameState.botId,
playerColor: gameState.playerColor,
capturedByPlayer: gameState.capturedByPlayer,
capturedByOpponent: gameState.capturedByOpponent,
moveHistory: gameState.moveHistory,
finalFen: engine.fen()
});
bus.emit('game:ended', { gameKey: 'chess', result, reason, mode: gameState.mode });
}, 1000);
if (result === 'win' || result === 'draw') {
bus.emit('coins:earned', { amount: coins });
bus.emit('xp:earned', { amount: xp });
}
}, 1500);
}
import * as scene from '../../../core/scene.js';
import * as audio from '../../../core/audio.js';
import * as bus from '../../../core/bus.js';
import * as store from '../../../core/store.js';
import { t } from '../../../core/i18n.js';
export function mountResult(el, params) {
const { result, reason, moves, mode, botId } = params;
const { result, reason, coins = 0, xp = 0, moves = 0, mode, botId, pgn, moveHistory = [] } = params;
const isWin = result === 'win';
const isDraw = result === 'draw';
const icon = isWin ? '🏆' : isDraw ? '🤝' : '💀';
const title = isWin ? t('game.you_win') : isDraw ? t('game.draw_result') : t('game.you_lose');
const color = isWin ? 'var(--win)' : isDraw ? 'var(--gold)' : 'var(--loss)';
const ratingChange = isWin ? '+12' : isDraw ? '+1' : '-8';
const coins = isWin ? 50 : isDraw ? 20 : 10;
const resultConfig = {
win: { icon: '🏆', title: t('game.you_win'), color: '#34D399', ratingChange: '+12' },
loss: { icon: '💀', title: t('game.you_lose'), color: '#F87171', ratingChange: '-8' },
draw: { icon: '🤝', title: t('game.draw_result'), color: '#E4AC38', ratingChange: '+1' }
};
const cfg = resultConfig[result] || resultConfig.loss;
const reasonText = {
checkmate: 'كش ملك',
stalemate: 'بات (جمود)',
resign: 'استسلام',
timeout: 'انتهاء الوقت',
threefold: 'تكرار ثلاثي',
insufficient: 'قطع غير كافية'
}[reason] || reason;
el.innerHTML = `
<div style="display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;gap:var(--s-6);padding:var(--s-6);">
<div style="font-size:64px;animation:float 2s ease-in-out infinite;">${icon}</div>
<div style="display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;gap:20px;padding:24px;background:#0a0a1a;">
<!-- Result Icon -->
<div style="font-size:72px;${isWin ? 'animation:float 2s ease-in-out infinite;' : ''}">${cfg.icon}</div>
<!-- Title & Reason -->
<div style="text-align:center;">
<div style="font-size:28px;font-weight:800;color:${color};">${title}</div>
<div style="font-size:14px;color:var(--text-secondary);margin-top:var(--s-1);">${reason}</div>
<div style="font-size:32px;font-weight:800;color:${cfg.color};">${cfg.title}</div>
<div style="font-size:14px;color:#94a3b8;margin-top:4px;">${reasonText}</div>
${moves ? `<div style="font-size:12px;color:#475569;margin-top:2px;">${moves} نقلة</div>` : ''}
</div>
<div style="display:flex;gap:var(--s-6);margin-top:var(--s-4);">
<!-- Stats Row -->
<div style="display:flex;gap:24px;margin-top:8px;">
<div style="text-align:center;">
<div style="font-size:24px;font-weight:700;color:${color};font-family:var(--font-lat);">${ratingChange}</div>
<div style="font-size:11px;color:var(--text-secondary);">Rating</div>
<div style="font-size:28px;font-weight:700;color:${cfg.color};font-family:Inter,monospace;">${cfg.ratingChange}</div>
<div style="font-size:11px;color:#64748b;">تصنيف</div>
</div>
<div style="width:1px;background:rgba(255,255,255,0.06);"></div>
<div style="text-align:center;">
<div style="font-size:24px;font-weight:700;color:var(--gold);font-family:var(--font-lat);">+${coins}</div>
<div style="font-size:11px;color:var(--text-secondary);">Coins</div>
<div style="font-size:28px;font-weight:700;color:#E4AC38;font-family:Inter,monospace;">+${coins}</div>
<div style="font-size:11px;color:#64748b;">عملات</div>
</div>
<div style="width:1px;background:rgba(255,255,255,0.06);"></div>
<div style="text-align:center;">
<div style="font-size:24px;font-weight:700;color:var(--cyan);font-family:var(--font-lat);">+${moves * 2}</div>
<div style="font-size:11px;color:var(--text-secondary);">XP</div>
<div style="font-size:28px;font-weight:700;color:#00FFFF;font-family:Inter,monospace;">+${xp}</div>
<div style="font-size:11px;color:#64748b;">خبرة</div>
</div>
</div>
<div style="display:flex;gap:var(--s-3);margin-top:var(--s-6);">
<button class="btn btn-primary" id="btn-rematch">${t('game.rematch')}</button>
<button class="btn btn-secondary" id="btn-back">${t('game.back')}</button>
<!-- Move Summary -->
${moveHistory.length > 0 ? `
<div style="background:#1a1a2e;border:1px solid rgba(255,255,255,0.06);border-radius:8px;padding:8px 12px;max-width:300px;width:100%;max-height:60px;overflow-x:auto;white-space:nowrap;">
<div style="font-family:Inter,monospace;font-size:11px;color:#94a3b8;">
${formatMoveHistory(moveHistory)}
</div>
</div>` : ''}
<!-- Actions -->
<div style="display:flex;flex-direction:column;gap:8px;width:100%;max-width:280px;margin-top:12px;">
<button class="btn btn-primary w-full" id="btn-rematch" style="font-size:15px;">${t('game.rematch')}</button>
<button class="btn btn-secondary w-full" id="btn-analyze" style="font-size:13px;">📊 تحليل المباراة</button>
<button class="btn btn-secondary w-full" id="btn-back" style="font-size:13px;">${t('game.back')}</button>
</div>
</div>
`;
if (isWin) {
bus.emit('coins:earned', { amount: coins });
bus.emit('xp:earned', { amount: moves * 2 });
}
el.querySelector('#btn-rematch').addEventListener('click', () => {
audio.play('click');
scene.replace('chess-game', { mode, botId, timeControl: 'rapid_10_0' });
});
el.querySelector('#btn-analyze').addEventListener('click', () => {
audio.play('click');
// TODO: open analysis scene
});
el.querySelector('#btn-back').addEventListener('click', () => {
audio.play('click');
bus.emit('navigate', { world: 'play', scene: 'play-table' });
});
// Update player coins in store
const player = store.get('player');
if (player && coins > 0) {
store.set('player', { ...player, coins: (player.coins || 0) + coins, xp: (player.xp || 0) + xp });
}
}
function formatMoveHistory(history) {
let html = '';
for (let i = 0; i < Math.min(history.length, 30); i += 2) {
const num = Math.floor(i / 2) + 1;
html += `${num}.${history[i]?.san || ''} ${history[i+1]?.san || ''} `;
}
if (history.length > 30) html += '...';
return html;
}
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