Commit 2dd53307 authored by Mahmoud Aglan's avatar Mahmoud Aglan

feat: chess fully playable — all modes, analysis, puzzles, matchmaking

/game — Full chess vs bot:
- Complete DOM (17 elements game engines need)
- Loads chess.min.js → openings.js → board.js → game.js in order
- Clocks, move list, opening display, thinking indicator
- Resign, draw, flip buttons
- Immersive mode (header/nav hide)

/game-live — Multiplayer chess:
- Realtime subscription for opponent moves
- Draw offer banner (accept/decline)
- Same full DOM as bot game

/analysis — Post-game analysis:
- Eval bar with animated fill
- Eval graph canvas
- Move navigation (first/prev/next/last)
- Player accuracy percentages
- Critical moments highlight
- Engine analysis button

/puzzles — Chess puzzles:
- Daily (5 per day with dot progress)
- Streak mode
- Rush mode (timed)
- Tab switching between modes

/matchmaking — Animated opponent search:
- Radar ring pulse animation
- Timer counting up
- Auto-poll API every 2s
- Cancel button returns to lobby

CSS additions:
- Chess game layout (flexbox centered, responsive board)
- Clock styles (active=gold, low=red+pulse)
- Move list compact style
- Matchmaking radar animation

Also: chessboard.css now loaded in <head> globally
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent d369c086
...@@ -255,9 +255,58 @@ img { display: block; max-width: 100%; } ...@@ -255,9 +255,58 @@ img { display: block; max-width: 100%; }
.mb-5 { margin-bottom: 20px; } .mb-5 { margin-bottom: 20px; }
.text-center { text-align: center; } .text-center { text-align: center; }
/* === CHESS GAME LAYOUT === */
.chess-game-wrap {
display: flex; flex-direction: column; align-items: center;
min-height: 100dvh; justify-content: center; padding: 8px 8px env(safe-area-inset-bottom);
}
.chess-top-bar, .chess-bottom-bar {
width: 100%; max-width: 400px; padding: 6px 4px;
}
.chess-player-bar {
display: flex; align-items: center; justify-content: space-between;
}
.chess-player-name { font-size: 13px; font-weight: 600; }
.chess-clock {
font-size: 16px; font-weight: 700; font-variant-numeric: tabular-nums;
padding: 4px 10px; background: var(--bg-2); border-radius: var(--radius-sm);
min-width: 60px; text-align: center;
}
.chess-clock.active { background: var(--gold); color: var(--text-inv); }
.chess-clock.low { background: var(--loss); color: white; animation: pulse 1s infinite; }
.chess-move-list-wrap {
width: 100%; max-width: 400px; max-height: 80px; overflow-y: auto;
margin: 4px 0; scrollbar-width: thin;
}
.chess-move-list {
display: flex; flex-wrap: wrap; gap: 4px; padding: 4px;
font-size: 12px; font-variant-numeric: tabular-nums;
}
.chess-actions {
display: flex; gap: 6px; flex-wrap: wrap; justify-content: center; margin-top: 8px;
}
/* === RESPONSIVE GAME BOARD === */ /* === RESPONSIVE GAME BOARD === */
.board-container { .board-container {
width: min(100vw - 32px, 400px); aspect-ratio: 1; margin: 0 auto; width: min(100vw - 24px, 400px); aspect-ratio: 1; margin: 0 auto;
}
/* === MATCHMAKING RADAR === */
.matchmaking-radar {
position: relative; width: 120px; height: 120px;
display: flex; align-items: center; justify-content: center;
}
.radar-ring {
position: absolute; inset: 0; border-radius: 50%;
border: 2px solid var(--gold); opacity: 0;
animation: radar-expand 2s ease-out infinite;
}
.radar-ring.r2 { animation-delay: 0.6s; }
.radar-ring.r3 { animation-delay: 1.2s; }
@keyframes radar-expand {
0% { transform: scale(0.3); opacity: 0.8; }
100% { transform: scale(1.5); opacity: 0; }
} }
/* === JUICE: BUTTON RIPPLE === */ /* === JUICE: BUTTON RIPPLE === */
......
...@@ -20,6 +20,7 @@ if (str_starts_with($route, 'api/')) { ...@@ -20,6 +20,7 @@ if (str_starts_with($route, 'api/')) {
<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans+Arabic:wght@400;500;600;700;800&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans+Arabic:wght@400;500;600;700;800&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/app/styles/el3ab.css"> <link rel="stylesheet" href="/app/styles/el3ab.css">
<link rel="stylesheet" href="/public/css/chessboard.css">
</head> </head>
<body> <body>
<div class="shell" id="shell"> <div class="shell" id="shell">
...@@ -345,34 +346,256 @@ defineScreen('/play', function chessLobby() { ...@@ -345,34 +346,256 @@ defineScreen('/play', function chessLobby() {
}); });
defineScreen('/game', function gameScreen() { defineScreen('/game', function gameScreen() {
// Immersive mode
document.getElementById('shell').classList.add('immersive'); document.getElementById('shell').classList.add('immersive');
const params = Object.fromEntries(new URLSearchParams(location.search));
main.innerHTML = ` main.innerHTML = `
<div style="display:flex;flex-direction:column;align-items:center;min-height:100dvh;justify-content:center;padding:8px;"> <div class="chess-game-wrap">
<div id="opponent-bar" style="width:100%;max-width:400px;display:flex;align-items:center;justify-content:space-between;padding:8px 4px;font-size:13px;font-weight:600;"> <div class="chess-top-bar">
<span id="opp-name">Bot</span><span id="clock-opp" style="font-variant-numeric:tabular-nums;">--:--</span> <div class="chess-player-bar">
<span class="chess-player-name" id="opp-name">🤖</span>
<span class="chess-clock" id="clock-top">--:--</span>
</div>
</div>
<div class="board-container">
<div id="board"></div>
</div>
<div class="chess-bottom-bar">
<div class="chess-player-bar">
<span class="chess-player-name" id="player-name">انت</span>
<span class="chess-clock" id="clock-bottom">--:--</span>
</div>
</div>
<div id="opening-display" style="text-align:center;font-size:12px;color:var(--text-2);min-height:18px;margin:4px 0;"></div>
<div id="opening-display-mobile" style="display:none;"></div>
<div id="thinking-indicator" style="text-align:center;font-size:12px;color:var(--gold);min-height:18px;"></div>
<div id="game-status" style="text-align:center;font-size:13px;font-weight:600;min-height:20px;margin:4px 0;"></div>
<div id="game-status-mobile" style="display:none;"></div>
<div class="chess-move-list-wrap">
<div id="move-list" class="chess-move-list"></div>
<div id="move-list-mobile" style="display:none;"></div>
</div>
<div class="chess-actions">
<button class="btn btn-ghost btn-sm" id="btn-resign" onclick="if(Game&&Game.resign)Game.resign()">استسلام</button>
<button class="btn btn-ghost btn-sm" id="btn-draw" onclick="if(Game&&Game.offerDraw)Game.offerDraw()">تعادل</button>
<button class="btn btn-ghost btn-sm" id="btn-flip" onclick="if(Board&&Board.flip)Board.flip()">🔄</button>
<button class="btn btn-ghost btn-sm" id="btn-pgn" style="display:none;">PGN</button>
<button class="btn btn-ghost btn-sm" id="btn-fen" style="display:none;">FEN</button>
<button class="btn btn-ghost btn-sm" id="btn-rematch" style="display:none;">اعادة</button>
</div> </div>
</div>
`;
function loadS(src){return new Promise((r,j)=>{if(document.querySelector('script[src="'+src+'"]')){r();return;}const s=document.createElement('script');s.src=src;s.onload=r;s.onerror=j;document.body.appendChild(s);});}
Promise.all([
loadS('/public/js/chess.min.js'),
loadS('/public/js/openings.js'),
loadS('/public/js/board.js'),
loadS('/public/js/game.js')
]).then(() => {
if (window.Board) Board.init('board', { flipped: params.color === 'b', playerColor: params.color || 'w' });
if (window.Game) Game.start({
color: params.color || 'w',
botId: params.bot || 'nour',
time: parseInt(params.time) || 300,
increment: parseInt(params.inc) || 0,
rated: params.rated !== 'false'
});
});
});
defineScreen('/game-live', function gameLiveScreen() {
document.getElementById('shell').classList.add('immersive');
const params = Object.fromEntries(new URLSearchParams(location.search));
main.innerHTML = `
<div class="chess-game-wrap">
<div class="chess-top-bar"><div class="chess-player-bar"><span class="chess-player-name" id="opp-name">خصم</span><span class="chess-clock" id="clock-top">--:--</span></div></div>
<div class="board-container"><div id="board"></div></div> <div class="board-container"><div id="board"></div></div>
<div id="player-bar" style="width:100%;max-width:400px;display:flex;align-items:center;justify-content:space-between;padding:8px 4px;font-size:13px;font-weight:600;"> <div class="chess-bottom-bar"><div class="chess-player-bar"><span class="chess-player-name" id="player-name">انت</span><span class="chess-clock" id="clock-bottom">--:--</span></div></div>
<span id="player-name">انت</span><span id="clock-player" style="font-variant-numeric:tabular-nums;">--:--</span> <div id="opening-display" style="text-align:center;font-size:12px;color:var(--text-2);min-height:18px;margin:4px 0;"></div>
<div id="opening-display-mobile" style="display:none;"></div>
<div id="thinking-indicator" style="text-align:center;font-size:12px;color:var(--gold);min-height:18px;"></div>
<div id="game-status" style="text-align:center;font-size:13px;font-weight:600;min-height:20px;margin:4px 0;"></div>
<div id="game-status-mobile" style="display:none;"></div>
<div class="chess-move-list-wrap"><div id="move-list" class="chess-move-list"></div><div id="move-list-mobile" style="display:none;"></div></div>
<div class="chess-actions">
<button class="btn btn-ghost btn-sm" id="btn-resign">استسلام</button>
<button class="btn btn-ghost btn-sm" id="btn-draw">عرض تعادل</button>
<button class="btn btn-ghost btn-sm" id="btn-flip" onclick="if(Board&&Board.flip)Board.flip()">🔄</button>
<button class="btn btn-ghost btn-sm" id="btn-rematch" style="display:none;">اعادة</button>
</div> </div>
<div style="display:flex;gap:8px;margin-top:12px;"> <div id="draw-offer-banner" style="display:none;text-align:center;padding:8px;background:var(--gold-dim);border-radius:var(--radius-sm);margin:8px 0;">
<button class="btn btn-ghost" onclick="if(confirm('متأكد؟'))Game.resign&&Game.resign()">استسلام</button> <span>خصمك يعرض تعادل</span>
<button class="btn btn-sm btn-gold" id="btn-accept-draw" style="margin-right:8px;">قبول</button>
<button class="btn btn-sm btn-ghost" id="btn-decline-draw">رفض</button>
</div> </div>
</div> </div>
`; `;
// Load chess engine and start function loadS(src){return new Promise((r,j)=>{if(document.querySelector('script[src="'+src+'"]')){r();return;}const s=document.createElement('script');s.src=src;s.onload=r;s.onerror=j;document.body.appendChild(s);});}
Promise.all([
loadS('/public/js/chess.min.js'),
loadS('/public/js/openings.js'),
loadS('/public/js/board.js'),
loadS('/public/js/realtime.js'),
loadS('/public/js/game-live.js')
]).then(() => {
if (window.LiveGame) LiveGame.init(params.match_id);
});
});
defineScreen('/analysis', function analysisScreen() {
const params = Object.fromEntries(new URLSearchParams(location.search)); const params = Object.fromEntries(new URLSearchParams(location.search));
function loadS(src) { return new Promise((r,j)=>{ if(document.querySelector('script[src="'+src+'"]')){r();return;} const s=document.createElement('script');s.src=src;s.onload=r;s.onerror=j;document.body.appendChild(s); }); }
const css = document.createElement('link'); css.rel='stylesheet'; css.href='/public/css/chessboard.css'; document.head.appendChild(css); main.innerHTML = `
<h2 class="text-center mb-4" style="font-size:18px;font-weight:700;">📊 تحليل المباراة</h2>
<div id="analysis-loading" class="text-center" style="color:var(--text-2);">جاري التحميل...</div>
<div id="analysis-content" style="display:none;">
<div style="display:flex;gap:12px;margin-bottom:12px;justify-content:center;">
<div style="text-align:center;"><span id="analysis-player-name" style="font-size:13px;font-weight:600;">انت</span><br><span id="analysis-player-accuracy" style="font-size:18px;font-weight:700;color:var(--win);">--%</span></div>
<div style="text-align:center;"><span id="analysis-opponent-name" style="font-size:13px;font-weight:600;">خصم</span><br><span id="analysis-opponent-accuracy" style="font-size:18px;font-weight:700;color:var(--loss);">--%</span></div>
</div>
<div id="eval-bar" style="height:24px;background:var(--bg-3);border-radius:var(--radius-sm);overflow:hidden;margin-bottom:8px;position:relative;">
<div id="eval-bar-fill" style="height:100%;width:50%;background:var(--text-1);transition:width 0.3s;"></div>
<span id="eval-bar-value" style="position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);font-size:11px;font-weight:700;mix-blend-mode:difference;">0.0</span>
</div>
<div class="board-container" style="margin-bottom:12px;"><div id="board"></div></div>
<canvas id="eval-graph" width="320" height="60" style="width:100%;height:60px;background:var(--bg-2);border-radius:var(--radius-sm);margin-bottom:8px;"></canvas>
<div id="analysis-opening" style="text-align:center;font-size:12px;color:var(--text-2);margin-bottom:8px;"></div>
<div style="display:flex;gap:8px;justify-content:center;margin-bottom:12px;">
<button class="btn btn-ghost btn-sm" id="btn-first">⏮</button>
<button class="btn btn-ghost btn-sm" id="btn-prev">◀</button>
<button class="btn btn-ghost btn-sm" id="btn-next">▶</button>
<button class="btn btn-ghost btn-sm" id="btn-last">⏭</button>
</div>
<div id="analysis-moves" class="chess-move-list" style="max-height:120px;overflow-y:auto;margin-bottom:12px;"></div>
<div id="critical-moments" style="margin-bottom:12px;"></div>
<button class="btn btn-gold btn-block" id="btn-analyze">🔍 تحليل بالمحرك</button>
<canvas id="time-graph" width="320" height="60" style="width:100%;height:60px;background:var(--bg-2);border-radius:var(--radius-sm);margin-top:8px;display:none;"></canvas>
</div>
`;
Promise.all([loadS('/public/js/chess.min.js'), loadS('/public/js/board.js'), loadS('/public/js/game.js')]).then(() => { function loadS(src){return new Promise((r,j)=>{if(document.querySelector('script[src="'+src+'"]')){r();return;}const s=document.createElement('script');s.src=src;s.onload=r;s.onerror=j;document.body.appendChild(s);});}
if (window.Board) Board.init('board', { flipped: params.color === 'b', playerColor: params.color || 'w' });
if (window.Game) Game.start({ color: params.color||'w', botId: params.bot||'nour', time: parseInt(params.time)||300, increment: parseInt(params.inc)||0, rated: params.rated !== 'false' }); Promise.all([
loadS('/public/js/chess.min.js'),
loadS('/public/js/openings.js'),
loadS('/public/js/board.js'),
loadS('/public/js/analysis.js')
]).then(() => {
if (window.Analysis) Analysis.init(params.id);
});
});
defineScreen('/puzzles', function puzzlesScreen() {
main.innerHTML = `
<h2 class="text-center mb-4" style="font-size:20px;font-weight:700;">🧩 تمارين شطرنج</h2>
<div id="puzzle-tabs" class="chips mb-4" style="justify-content:center;">
<button class="chip on" data-tab="daily">اليومية</button>
<button class="chip" data-tab="streak">سلسلة</button>
<button class="chip" data-tab="rush">سباق</button>
</div>
<div id="tab-daily">
<div class="daily-dots" style="display:flex;gap:8px;justify-content:center;margin-bottom:12px;">
<span class="daily-dot" style="width:12px;height:12px;border-radius:50%;background:var(--bg-3);border:2px solid var(--border);"></span>
<span class="daily-dot" style="width:12px;height:12px;border-radius:50%;background:var(--bg-3);border:2px solid var(--border);"></span>
<span class="daily-dot" style="width:12px;height:12px;border-radius:50%;background:var(--bg-3);border:2px solid var(--border);"></span>
<span class="daily-dot" style="width:12px;height:12px;border-radius:50%;background:var(--bg-3);border:2px solid var(--border);"></span>
<span class="daily-dot" style="width:12px;height:12px;border-radius:50%;background:var(--bg-3);border:2px solid var(--border);"></span>
</div>
</div>
<div id="puzzle-board-area">
<div style="text-align:center;margin-bottom:8px;">
<span id="current-puzzle-rating" style="font-size:12px;color:var(--text-2);"></span>
<span id="puzzle-turn" style="font-size:12px;color:var(--gold);margin-right:12px;"></span>
</div>
<div class="board-container"><div id="board"></div></div>
<div id="puzzle-status" style="text-align:center;font-size:14px;font-weight:600;margin:12px 0;min-height:20px;"></div>
<div id="puzzle-result" style="display:none;text-align:center;margin:12px 0;">
<span id="puzzle-result-icon" style="font-size:32px;"></span>
<p id="puzzle-result-text" style="font-size:14px;font-weight:600;margin-top:4px;"></p>
</div>
<button class="btn btn-gold btn-block" onclick="if(Puzzles&&Puzzles.nextPuzzle)Puzzles.nextPuzzle()">التالي ▶</button>
</div>
<div id="rush-timer" style="display:none;text-align:center;margin:12px 0;">
<span id="rush-time" style="font-size:24px;font-weight:700;font-variant-numeric:tabular-nums;">3:00</span>
<span id="rush-count" style="display:block;font-size:13px;color:var(--text-2);">0 حل</span>
</div>
<button class="btn btn-ghost btn-block" id="btn-start-rush" style="display:none;" onclick="if(Puzzles&&Puzzles.startRush)Puzzles.startRush()">ابدأ السباق</button>
`;
// Chip tabs
document.querySelectorAll('#puzzle-tabs .chip').forEach(c => {
c.onclick = () => {
document.querySelectorAll('#puzzle-tabs .chip').forEach(x => x.classList.remove('on'));
c.classList.add('on');
};
}); });
function loadS(src){return new Promise((r,j)=>{if(document.querySelector('script[src="'+src+'"]')){r();return;}const s=document.createElement('script');s.src=src;s.onload=r;s.onerror=j;document.body.appendChild(s);});}
Promise.all([
loadS('/public/js/chess.min.js'),
loadS('/public/js/board.js'),
loadS('/public/js/puzzles.js')
]).then(() => {
if (window.Puzzles) Puzzles.init();
});
});
defineScreen('/matchmaking', function matchmakingScreen() {
const params = Object.fromEntries(new URLSearchParams(location.search));
main.innerHTML = `
<div style="display:flex;flex-direction:column;align-items:center;justify-content:center;min-height:60vh;text-align:center;">
<div class="matchmaking-radar">
<div class="radar-ring"></div>
<div class="radar-ring r2"></div>
<div class="radar-ring r3"></div>
<span style="font-size:32px;position:relative;z-index:1;">⚔️</span>
</div>
<p style="font-size:16px;font-weight:700;margin-top:20px;">جاري البحث عن خصم...</p>
<p style="font-size:13px;color:var(--text-2);margin-top:8px;" id="mm-status">الانتظار: <span id="mm-timer">0</span> ثانية</p>
<button class="btn btn-ghost" style="margin-top:24px;" id="mm-cancel">الغاء</button>
</div>
`;
let seconds = 0;
let polling = null;
const timerEl = document.getElementById('mm-timer');
const timerInterval = setInterval(() => { seconds++; if(timerEl) timerEl.textContent = seconds; }, 1000);
// Join queue
App.fetch('/api/matchmaking', { method:'POST', body: JSON.stringify({ action:'join', game_key:'chess', time_control: params.tc, rating: 1200 }) });
// Poll for match
polling = setInterval(async () => {
const res = await App.fetch('/api/matchmaking?action=status');
if (res && res.status === 'matched' && res.match_id) {
clearInterval(timerInterval);
clearInterval(polling);
SFX.win();
H.success();
location.href = '/game-live?match_id=' + res.match_id;
}
}, 2000);
document.getElementById('mm-cancel').onclick = () => {
clearInterval(timerInterval);
clearInterval(polling);
App.fetch('/api/matchmaking', { method:'POST', body: JSON.stringify({ action:'cancel' }) });
route(null, '/play');
};
}); });
defineScreen('/ludo', function ludoLobby() { defineScreen('/ludo', function ludoLobby() {
......
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