Commit f91d9222 authored by Mahmoud Aglan's avatar Mahmoud Aglan

feat(chess): analysis screen + improved board sizing

- Post-game analysis with Stockfish multi-line eval (3 lines, depth 18)
- Move navigator: step through game with  buttons
- Eval bar: visual white/black advantage indicator
- Move chips: click any move to jump to that position
- Board sizing: fills available vertical space better (up to 480px)
- Analysis accessible from result screen 'تحليل المباراة' button
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent b27df183
...@@ -229,13 +229,15 @@ export class ChessBoard { ...@@ -229,13 +229,15 @@ export class ChessBoard {
} }
setupCanvas() { setupCanvas() {
const size = Math.min(this.wrapper.clientWidth || 360, 400); const containerH = this.container.clientHeight || 500;
const containerW = this.wrapper.clientWidth || 360;
const size = Math.min(containerW, containerH - 8, 480);
this.squareSize = size / 8; this.squareSize = size / 8;
const { canvas, ctx } = createCanvas(this.wrapper, size, size); const { canvas, ctx } = createCanvas(this.wrapper, size, size);
this.canvas = canvas; this.canvas = canvas;
this.ctx = ctx; this.ctx = ctx;
this.size = size; this.size = size;
this.canvas.style.cssText = 'width:100%;height:100%;display:block;border-radius:4px;box-shadow:0 4px 20px rgba(0,0,0,0.5);'; this.canvas.style.cssText = 'display:block;border-radius:4px;box-shadow:0 4px 20px rgba(0,0,0,0.5);';
} }
bindEvents() { bindEvents() {
......
import * as scene from '../../core/scene.js'; import * as scene from '../../core/scene.js';
import { mountGame } from './scenes/game.js'; import { mountGame } from './scenes/game.js';
import { mountResult } from './scenes/result.js'; import { mountResult } from './scenes/result.js';
import { mountAnalysis } from './scenes/analysis.js';
scene.register('chess-game', mountGame); scene.register('chess-game', mountGame);
scene.register('chess-result', mountResult); scene.register('chess-result', mountResult);
scene.register('chess-analysis', mountAnalysis);
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 { ChessBoard } from '../canvas/board.js';
import * as engine from '../logic/engine.js';
let board, analysisData, currentMoveIdx;
export function mountAnalysis(el, params) {
const { pgn, moveHistory = [], finalFen } = params;
currentMoveIdx = 0;
analysisData = null;
engine.create();
el.innerHTML = `
<div style="display:flex;flex-direction:column;height:100%;background:#1a1a2e;">
<div style="display:flex;align-items:center;justify-content:space-between;padding:8px 12px;background:#0f0f1e;border-bottom:1px solid rgba(255,255,255,0.06);">
<button class="btn btn-secondary" id="back-btn" style="min-height:32px;padding:4px 12px;font-size:12px;">← ${t('game.back')}</button>
<span style="font-size:14px;font-weight:700;color:#f8fafc;">📊 تحليل المباراة</span>
<div style="width:60px;"></div>
</div>
<div id="analysis-board" style="flex:1;display:flex;align-items:center;justify-content:center;padding:4px;min-height:0;"></div>
<!-- Eval bar -->
<div id="eval-bar" style="height:24px;background:#333;margin:0 12px;border-radius:4px;overflow:hidden;position:relative;">
<div id="eval-fill" style="height:100%;background:#f8fafc;width:50%;transition:width 0.3s;"></div>
<div id="eval-text" style="position:absolute;inset:0;display:flex;align-items:center;justify-content:center;font-size:11px;font-weight:700;color:#000;mix-blend-mode:difference;">0.0</div>
</div>
<!-- Move navigator -->
<div style="display:flex;align-items:center;gap:4px;padding:8px 12px;background:#0f0f1e;">
<button class="nav-btn" id="nav-start">⏮</button>
<button class="nav-btn" id="nav-prev">◀</button>
<div id="move-display" style="flex:1;text-align:center;font-family:Inter,monospace;font-size:13px;color:#94a3b8;">بداية</div>
<button class="nav-btn" id="nav-next">▶</button>
<button class="nav-btn" id="nav-end">⏭</button>
</div>
<!-- Analysis lines -->
<div id="analysis-lines" style="padding:8px 12px;background:#0f0f1e;border-top:1px solid rgba(255,255,255,0.06);min-height:60px;max-height:100px;overflow-y:auto;">
<div style="color:#64748b;font-size:12px;text-align:center;">اضغط على أي نقلة لتحليلها</div>
</div>
<!-- Full move list -->
<div id="analysis-moves" style="padding:8px 12px;background:#0f0f1e;border-top:1px solid rgba(255,255,255,0.06);max-height:60px;overflow-x:auto;white-space:nowrap;font-family:Inter,monospace;font-size:11px;display:flex;gap:3px;align-items:center;flex-wrap:nowrap;">
</div>
</div>
<style>
.nav-btn { width:36px;height:32px;background:#1e1e3a;border:1px solid rgba(255,255,255,0.08);color:#94a3b8;border-radius:6px;cursor:pointer;font-size:14px;display:flex;align-items:center;justify-content:center; }
.nav-btn:active { background:#2a2a5a; }
.move-chip { padding:2px 6px;border-radius:4px;cursor:pointer;color:#94a3b8; }
.move-chip:hover { background:rgba(255,255,255,0.05); }
.move-chip.active { background:#2563EB;color:#fff; }
</style>
`;
const boardContainer = el.querySelector('#analysis-board');
board = new ChessBoard(boardContainer, { interactive: false });
board.setPosition(engine.fen());
// Build move list
const moves = [];
if (moveHistory.length > 0) {
moveHistory.forEach(m => moves.push(m));
}
renderMoveChips(el, moves);
// Navigation
el.querySelector('#back-btn').addEventListener('click', () => { audio.play('click'); scene.pop(); });
el.querySelector('#nav-start').addEventListener('click', () => goToMove(el, moves, 0));
el.querySelector('#nav-prev').addEventListener('click', () => goToMove(el, moves, Math.max(0, currentMoveIdx - 1)));
el.querySelector('#nav-next').addEventListener('click', () => goToMove(el, moves, Math.min(moves.length, currentMoveIdx + 1)));
el.querySelector('#nav-end').addEventListener('click', () => goToMove(el, moves, moves.length));
// Go to final position
goToMove(el, moves, moves.length);
}
function renderMoveChips(el, moves) {
const container = el.querySelector('#analysis-moves');
let html = '<span class="move-chip active" data-idx="0">بداية</span>';
for (let i = 0; i < moves.length; i += 2) {
const num = Math.floor(i / 2) + 1;
html += `<span style="color:#475569;font-size:10px;">${num}.</span>`;
html += `<span class="move-chip" data-idx="${i + 1}">${moves[i]?.san || ''}</span>`;
if (moves[i + 1]) {
html += `<span class="move-chip" data-idx="${i + 2}">${moves[i + 1]?.san || ''}</span>`;
}
}
container.innerHTML = html;
container.querySelectorAll('.move-chip').forEach(chip => {
chip.addEventListener('click', () => {
const idx = parseInt(chip.dataset.idx);
goToMove(el, moves, idx);
});
});
}
function goToMove(el, moves, idx) {
currentMoveIdx = idx;
engine.create();
for (let i = 0; i < idx; i++) {
const m = moves[i];
if (m) engine.move(m.from, m.to, m.promotion);
}
board.setPosition(engine.fen());
if (idx > 0) {
const lastMove = moves[idx - 1];
board.setLastMove(lastMove.from, lastMove.to);
}
// Update display
const display = el.querySelector('#move-display');
if (idx === 0) display.textContent = 'بداية';
else display.textContent = `${Math.ceil(idx / 2)}. ${moves[idx - 1]?.san || ''}`;
// Update active chip
el.querySelectorAll('.move-chip').forEach(c => c.classList.remove('active'));
const activeChip = el.querySelector(`.move-chip[data-idx="${idx}"]`);
if (activeChip) {
activeChip.classList.add('active');
activeChip.scrollIntoView({ inline: 'center', behavior: 'smooth' });
}
// Request analysis for this position
analyzePosition(el, engine.fen());
}
async function analyzePosition(el, fen) {
const linesContainer = el.querySelector('#analysis-lines');
linesContainer.innerHTML = '<div style="color:#64748b;font-size:12px;text-align:center;">جاري التحليل...</div>';
try {
const data = await net.post('analysis.php', { fen, depth: 18, lines: 3 });
if (data.lines && data.lines.length > 0) {
renderAnalysisLines(el, data.lines, fen);
updateEvalBar(el, data.lines[0].evaluation);
}
} catch (e) {
linesContainer.innerHTML = '<div style="color:#ef4444;font-size:12px;text-align:center;">فشل التحليل</div>';
}
}
function renderAnalysisLines(el, lines, fen) {
const container = el.querySelector('#analysis-lines');
container.innerHTML = lines.map((line, i) => {
const evalStr = formatEval(line.evaluation);
const isPositive = line.evaluation >= 0;
const color = isPositive ? '#34D399' : '#F87171';
const pv = (line.pv || '').split(' ').slice(0, 6).join(' ');
return `
<div style="display:flex;align-items:center;gap:8px;padding:2px 0;${i === 0 ? 'font-weight:600;' : ''}">
<span style="font-size:12px;font-weight:700;color:${color};min-width:40px;font-family:Inter,monospace;">${evalStr}</span>
<span style="font-size:11px;color:#94a3b8;font-family:Inter,monospace;overflow:hidden;text-overflow:ellipsis;">${line.move} ${pv}</span>
</div>
`;
}).join('');
}
function updateEvalBar(el, evaluation) {
const fill = el.querySelector('#eval-fill');
const text = el.querySelector('#eval-text');
const clamped = Math.max(-10, Math.min(10, evaluation));
const percent = 50 + (clamped / 10) * 50;
fill.style.width = percent + '%';
text.textContent = formatEval(evaluation);
}
function formatEval(ev) {
if (ev >= 999) return 'M+';
if (ev <= -999) return 'M-';
const sign = ev >= 0 ? '+' : '';
return sign + ev.toFixed(1);
}
...@@ -45,7 +45,7 @@ export function mountGame(el, params) { ...@@ -45,7 +45,7 @@ export function mountGame(el, params) {
</div> </div>
<!-- Board --> <!-- Board -->
<div id="board-container" style="flex:1;display:flex;align-items:center;justify-content:center;padding:4px;position:relative;"> <div id="board-container" style="flex:1;display:flex;align-items:center;justify-content:center;padding:2px 4px;position:relative;min-height:0;">
<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;"> <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> ${t('game.thinking')} <span class="pulse">●●●</span>
</div> </div>
......
...@@ -80,7 +80,7 @@ export function mountResult(el, params) { ...@@ -80,7 +80,7 @@ export function mountResult(el, params) {
el.querySelector('#btn-analyze').addEventListener('click', () => { el.querySelector('#btn-analyze').addEventListener('click', () => {
audio.play('click'); audio.play('click');
// TODO: open analysis scene scene.push('chess-analysis', { pgn, moveHistory, finalFen: params.finalFen });
}); });
el.querySelector('#btn-back').addEventListener('click', () => { el.querySelector('#btn-back').addEventListener('click', () => {
......
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