Commit 049e06c9 authored by Mahmoud Aglan's avatar Mahmoud Aglan

feat(chess): Chess.com-style Game Review with full move analysis

New screen: Game Review (chess-review)
- Analyzes EVERY move with Stockfish (depth 12)
- Shows progress bar while analyzing
- Eval graph: canvas-rendered white/black area chart with mistake dots
- Accuracy comparison: player vs opponent percentage with colored bars
- Move classification breakdown table:
  !! Brilliant, ! Great, ★ Best, ✓ Good, 📖 Book,
  ?! Inaccuracy, ? Mistake, ?? Blunder
- Counts per player side-by-side (like Chess.com)
- Opening name displayed
- Links to detailed analysis (timeline scrubber)
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent 1b3088ab
......@@ -3,8 +3,10 @@ import { mountGame } from './scenes/game.js';
import { mountResult } from './scenes/result.js';
import { mountAnalysis } from './scenes/analysis.js';
import { mountHistory } from './scenes/history.js';
import { mountReview } from './scenes/review.js';
scene.register('chess-game', mountGame);
scene.register('chess-result', mountResult);
scene.register('chess-analysis', mountAnalysis);
scene.register('chess-history', mountHistory);
scene.register('chess-review', mountReview);
......@@ -127,7 +127,7 @@ export function mountResult(el, params) {
el.querySelector('#btn-analyze').addEventListener('click', () => {
audio.play('click');
juice.hapticLight();
scene.push('chess-analysis', { pgn, moveHistory, finalFen: params.finalFen });
scene.push('chess-review', { moveHistory, playerColor: params.playerColor || 'w', botId, result });
});
el.querySelector('#btn-back').addEventListener('click', () => {
......
import * as scene from '../../../core/scene.js';
import * as net from '../../../core/net.js';
import * as audio from '../../../core/audio.js';
import * as bus from '../../../core/bus.js';
import { t } from '../../../core/i18n.js';
import { createCanvas, clear } from '../../../core/canvas.js';
import * as engine from '../logic/engine.js';
import { classifyMove, calculateAccuracy, getMoveClassSummary, getAccuracyLabel } from '../logic/classifier.js';
import { getOpeningName } from '../logic/openings.js';
let reviewData = null;
export async function mountReview(el, params) {
const { moveHistory = [], playerColor = 'w', botId, result } = params;
el.innerHTML = `
<div style="display:flex;flex-direction:column;height:100%;background:#1a2e1a;overflow-y:auto;">
<div style="display:flex;align-items:center;justify-content:space-between;padding:10px 14px;background:#0f1f0f;border-bottom:1px solid rgba(255,255,255,0.06);">
<button class="btn btn-secondary" id="back-btn" style="min-height:30px;padding:4px 12px;font-size:12px;">← ${t('game.back')}</button>
<span style="font-size:15px;font-weight:700;color:#f8fafc;">⭐ مراجعة المباراة</span>
<div style="width:50px;"></div>
</div>
<!-- Progress -->
<div id="review-progress" style="padding:16px;text-align:center;">
<div style="font-size:14px;color:#94a3b8;margin-bottom:8px;">جاري تحليل كل نقلة...</div>
<div style="background:#1e3a1e;border-radius:6px;height:8px;overflow:hidden;">
<div id="progress-bar" style="height:100%;background:#4CAF50;width:0%;transition:width 0.3s;border-radius:6px;"></div>
</div>
<div id="progress-text" style="font-size:11px;color:#64748b;margin-top:4px;">0/${moveHistory.length}</div>
</div>
<!-- Review content (shown after analysis) -->
<div id="review-content" style="display:none;padding:0 14px 14px;"></div>
</div>
`;
el.querySelector('#back-btn').addEventListener('click', () => { audio.play('click'); scene.pop(); });
// Start analyzing
await analyzeAllMoves(el, moveHistory, playerColor);
}
async function analyzeAllMoves(el, moves, playerColor) {
const results = [];
const chess = new Chess();
let prevEval = 0.2; // Slight white advantage at start
const progressBar = el.querySelector('#progress-bar');
const progressText = el.querySelector('#progress-text');
for (let i = 0; i < moves.length; i++) {
const fen = chess.fen();
const isWhiteMove = chess.turn() === 'w';
const move = moves[i];
// Apply move
chess.move({ from: move.from, to: move.to, promotion: move.promotion });
// Get engine eval for position AFTER the move
let evalAfter = prevEval;
let bestEval = prevEval;
let bestMove = null;
try {
const analysis = await net.post('analysis.php', { fen: chess.fen(), depth: 12, lines: 1 });
if (analysis?.lines?.[0]) {
// Eval is from side-to-move perspective, flip it to white's perspective
evalAfter = analysis.lines[0].evaluation * (chess.turn() === 'w' ? 1 : -1);
}
} catch (e) {}
try {
const bestAnalysis = await net.post('analysis.php', { fen, depth: 12, lines: 1 });
if (bestAnalysis?.lines?.[0]) {
bestEval = bestAnalysis.lines[0].evaluation * (isWhiteMove ? 1 : -1);
bestMove = bestAnalysis.lines[0].move;
}
} catch (e) {
bestEval = evalAfter;
}
const moveNum = Math.floor(i / 2) + 1;
const classification = classifyMove(prevEval, evalAfter, bestEval, isWhiteMove, moveNum);
results.push({
san: move.san,
evalBefore: prevEval,
evalAfter,
bestEval,
bestMove,
classification,
isWhite: isWhiteMove
});
prevEval = evalAfter;
// Update progress
const pct = Math.round(((i + 1) / moves.length) * 100);
progressBar.style.width = pct + '%';
progressText.textContent = `${i + 1}/${moves.length}`;
}
reviewData = results;
renderReview(el, results, moves, playerColor);
}
function renderReview(el, results, moves, playerColor) {
el.querySelector('#review-progress').style.display = 'none';
const content = el.querySelector('#review-content');
content.style.display = 'block';
// Split by player
const whiteMoves = results.filter(r => r.isWhite);
const blackMoves = results.filter(r => !r.isWhite);
const whiteAccuracy = calculateAccuracy(whiteMoves.map(r => r.classification));
const blackAccuracy = calculateAccuracy(blackMoves.map(r => r.classification));
const whiteSummary = getMoveClassSummary(whiteMoves.map(r => r.classification));
const blackSummary = getMoveClassSummary(blackMoves.map(r => r.classification));
const playerAccuracy = playerColor === 'w' ? whiteAccuracy : blackAccuracy;
const opponentAccuracy = playerColor === 'w' ? blackAccuracy : whiteAccuracy;
const playerLabel = getAccuracyLabel(playerAccuracy);
const opponentLabel = getAccuracyLabel(opponentAccuracy);
const opening = getOpeningName(moves) || 'غير معروف';
content.innerHTML = `
<!-- Eval Graph -->
<div id="eval-graph-container" style="background:#0f1f0f;border-radius:10px;padding:8px;margin-bottom:12px;"></div>
<!-- Accuracy Comparison -->
<div style="background:#0f1f0f;border-radius:10px;padding:16px;margin-bottom:12px;">
<div style="text-align:center;font-size:15px;font-weight:700;color:#f8fafc;margin-bottom:12px;">⭐ مراجعة المباراة</div>
<div style="display:flex;align-items:center;gap:12px;margin-bottom:16px;">
<!-- Player accuracy -->
<div style="flex:1;text-align:center;">
<div style="font-size:28px;font-weight:800;color:${playerLabel.color};font-family:Inter,monospace;">${playerAccuracy}%</div>
<div style="font-size:11px;color:#94a3b8;">أنت</div>
<div style="height:6px;background:#1e3a1e;border-radius:3px;margin-top:6px;overflow:hidden;">
<div style="height:100%;width:${playerAccuracy}%;background:${playerLabel.color};border-radius:3px;"></div>
</div>
</div>
<div style="font-size:12px;color:#64748b;font-weight:600;">VS</div>
<!-- Opponent accuracy -->
<div style="flex:1;text-align:center;">
<div style="font-size:28px;font-weight:800;color:${opponentLabel.color};font-family:Inter,monospace;">${opponentAccuracy}%</div>
<div style="font-size:11px;color:#94a3b8;">الخصم</div>
<div style="height:6px;background:#1e3a1e;border-radius:3px;margin-top:6px;overflow:hidden;">
<div style="height:100%;width:${opponentAccuracy}%;background:${opponentLabel.color};border-radius:3px;"></div>
</div>
</div>
</div>
<!-- Opening -->
<div style="text-align:center;font-size:12px;color:#64748b;margin-bottom:12px;">📖 ${opening}</div>
</div>
<!-- Move Classification Breakdown -->
<div style="background:#0f1f0f;border-radius:10px;padding:14px;margin-bottom:12px;">
<div style="font-size:13px;font-weight:700;color:#f8fafc;margin-bottom:10px;text-align:center;">تصنيف النقلات</div>
<table style="width:100%;border-collapse:collapse;font-size:12px;">
<thead>
<tr style="color:#64748b;">
<th style="text-align:right;padding:3px 0;font-weight:600;">أنت</th>
<th style="text-align:center;padding:3px 8px;"></th>
<th style="text-align:left;padding:3px 0;font-weight:600;">الخصم</th>
</tr>
</thead>
<tbody>
${renderClassRow('brilliant', '!! رائعة', '#06B6D4', whiteSummary, blackSummary, playerColor)}
${renderClassRow('great', '! ممتازة', '#3B82F6', whiteSummary, blackSummary, playerColor)}
${renderClassRow('best', '★ أفضل نقلة', '#10B981', whiteSummary, blackSummary, playerColor)}
${renderClassRow('good', '✓ جيدة', '#94a3b8', whiteSummary, blackSummary, playerColor)}
${renderClassRow('book', '📖 نظرية', '#94a3b8', whiteSummary, blackSummary, playerColor)}
${renderClassRow('inaccuracy', '?! عدم دقة', '#FBBF24', whiteSummary, blackSummary, playerColor)}
${renderClassRow('mistake', '? خطأ', '#F97316', whiteSummary, blackSummary, playerColor)}
${renderClassRow('blunder', '?? خطأ فادح', '#EF4444', whiteSummary, blackSummary, playerColor)}
</tbody>
</table>
</div>
<!-- Back to analysis button -->
<button class="btn btn-secondary w-full" id="btn-full-analysis" style="font-size:13px;margin-bottom:12px;">📊 التحليل التفصيلي</button>
`;
// Render eval graph
renderEvalGraph(el.querySelector('#eval-graph-container'), results);
el.querySelector('#btn-full-analysis')?.addEventListener('click', () => {
audio.play('click');
scene.pop();
});
}
function renderClassRow(key, label, color, whiteSummary, blackSummary, playerColor) {
const playerCount = playerColor === 'w' ? (whiteSummary[key] || 0) : (blackSummary[key] || 0);
const opponentCount = playerColor === 'w' ? (blackSummary[key] || 0) : (whiteSummary[key] || 0);
return `
<tr style="border-top:1px solid rgba(255,255,255,0.04);">
<td style="text-align:right;padding:5px 0;color:${playerCount > 0 ? color : '#475569'};font-weight:${playerCount > 0 ? '700' : '400'};">${playerCount}</td>
<td style="text-align:center;padding:5px 8px;color:${color};font-size:11px;">${label}</td>
<td style="text-align:left;padding:5px 0;color:${opponentCount > 0 ? color : '#475569'};font-weight:${opponentCount > 0 ? '700' : '400'};">${opponentCount}</td>
</tr>
`;
}
function renderEvalGraph(container, results) {
if (!results || results.length < 2) return;
const width = container.clientWidth || 340;
const height = 80;
const { canvas, ctx } = createCanvas(container, width, height);
canvas.style.width = '100%';
canvas.style.height = height + 'px';
canvas.style.borderRadius = '6px';
const padding = 4;
const graphW = width - padding * 2;
const graphH = height - padding * 2;
// Background
ctx.fillStyle = '#1e3a1e';
ctx.fillRect(0, 0, width, height);
// Center line (0 eval)
ctx.strokeStyle = 'rgba(255,255,255,0.15)';
ctx.lineWidth = 1;
ctx.setLineDash([3, 3]);
ctx.beginPath();
ctx.moveTo(padding, height / 2);
ctx.lineTo(width - padding, height / 2);
ctx.stroke();
ctx.setLineDash([]);
// Fill areas
const evals = results.map(r => r.evalAfter);
const maxEval = 4;
function toY(ev) {
const clamped = Math.max(-maxEval, Math.min(maxEval, ev));
return padding + graphH / 2 - (clamped / maxEval) * (graphH / 2);
}
function toX(i) {
return padding + (i / (results.length - 1)) * graphW;
}
// White advantage fill (above center)
ctx.beginPath();
ctx.moveTo(toX(0), height / 2);
for (let i = 0; i < results.length; i++) {
const y = Math.min(toY(evals[i]), height / 2);
ctx.lineTo(toX(i), y);
}
ctx.lineTo(toX(results.length - 1), height / 2);
ctx.closePath();
ctx.fillStyle = 'rgba(255,255,255,0.7)';
ctx.fill();
// Black advantage fill (below center)
ctx.beginPath();
ctx.moveTo(toX(0), height / 2);
for (let i = 0; i < results.length; i++) {
const y = Math.max(toY(evals[i]), height / 2);
ctx.lineTo(toX(i), y);
}
ctx.lineTo(toX(results.length - 1), height / 2);
ctx.closePath();
ctx.fillStyle = 'rgba(50,50,50,0.7)';
ctx.fill();
// Eval line
ctx.beginPath();
ctx.moveTo(toX(0), toY(evals[0]));
for (let i = 1; i < results.length; i++) {
ctx.lineTo(toX(i), toY(evals[i]));
}
ctx.strokeStyle = '#fff';
ctx.lineWidth = 1.5;
ctx.stroke();
// Dots for mistakes/blunders
const dotColors = { blunder: '#EF4444', mistake: '#F97316', inaccuracy: '#FBBF24', brilliant: '#06B6D4' };
results.forEach((r, i) => {
const dotColor = dotColors[r.classification.class];
if (dotColor) {
ctx.beginPath();
ctx.arc(toX(i), toY(evals[i]), 3.5, 0, Math.PI * 2);
ctx.fillStyle = dotColor;
ctx.fill();
}
});
}
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