Commit 4f3da099 authored by Mahmoud Aglan's avatar Mahmoud Aglan

feat: Pro Chess Player Overhaul - complete rewrite with 23 pro features

- Pro board: drag/drop, pre-moves, arrows, highlights, keyboard nav, coords
- Pro timer: Fischer/Bronstein/delay, tenths/hundredths, low-time warnings
- Pro move stack: clickable, scrollable, opening detection (100+ openings)
- Post-game analysis: eval bar, eval graph, move classification, accuracy %
- Puzzle trainer: daily puzzles, streak mode, rush mode, themed puzzles
- Sound system: move/capture/check/castle/game-end via Web Audio API
- Eval bar: real-time position evaluation display
- PGN export, FEN copy, analyze button
- 50 seeded puzzles in database with ELO rating system
- New icons: eye, download, copy, chart, puzzle, brain
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent 4f492cea
<?php
require_once __DIR__ . '/../config/database.php';
header('Content-Type: application/json');
$token = get_auth_token();
if (!$token) {
http_response_code(401);
echo json_encode(['error' => 'unauthorized']);
exit;
}
$method = $_SERVER['REQUEST_METHOD'];
if ($method === 'GET') {
$gameId = $_GET['id'] ?? '';
if (!$gameId) {
http_response_code(400);
echo json_encode(['error' => 'missing game id']);
exit;
}
$res = supabase_rest('GET', "games?id=eq.{$gameId}&select=id,bot_id,player_color,pgn,fen,final_fen,result,reason,time_control,increment,rated,status,started_at,ended_at,game_state,clock_white,clock_black", [], $token);
if ($res['status'] >= 200 && $res['status'] < 300 && !empty($res['data'])) {
$game = $res['data'][0];
// If analysis already cached in game_state, return it
$gameState = $game['game_state'] ?? null;
if (is_string($gameState)) {
$gameState = json_decode($gameState, true);
}
$hasAnalysis = isset($gameState['analysis']) && !empty($gameState['analysis']);
echo json_encode([
'game' => $game,
'has_analysis' => $hasAnalysis,
'analysis' => $hasAnalysis ? $gameState['analysis'] : null
]);
} else {
http_response_code(404);
echo json_encode(['error' => 'game not found']);
}
exit;
}
// POST requests
$input = json_decode(file_get_contents('php://input'), true);
$action = $input['action'] ?? '';
switch ($action) {
case 'analyze':
$gameId = $input['game_id'] ?? '';
$positions = $input['positions'] ?? []; // Array of FEN strings
if (!$gameId || empty($positions)) {
http_response_code(400);
echo json_encode(['error' => 'missing game_id or positions']);
exit;
}
$evaluations = [];
$apiUrl = STOCKFISH_API . '/api/chess/move';
foreach ($positions as $index => $fen) {
$payload = json_encode([
'bot_id' => 'grandmaster',
'fen' => $fen,
'wtime' => 120000,
'btime' => 120000
]);
$ch = curl_init($apiUrl);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/json',
'X-API-Key: ' . STOCKFISH_MGMT_KEY
]);
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode === 200) {
$data = json_decode($response, true);
$evaluations[] = [
'move_index' => $index,
'fen' => $fen,
'best_move' => $data['best_move'] ?? null,
'evaluation' => $data['evaluation'] ?? 0,
'depth' => $data['depth'] ?? 20
];
} else {
$evaluations[] = [
'move_index' => $index,
'fen' => $fen,
'best_move' => null,
'evaluation' => 0,
'depth' => 0,
'error' => true
];
}
// Small delay to avoid overwhelming the API
if ($index < count($positions) - 1) {
usleep(100000); // 100ms
}
}
// Cache analysis in game_state
$analysisData = [
'evaluations' => $evaluations,
'analyzed_at' => date('c')
];
// Get existing game_state
$gameRes = supabase_rest('GET', "games?id=eq.{$gameId}&select=game_state", [], $token);
$existingState = [];
if (!empty($gameRes['data'][0]['game_state'])) {
$existingState = $gameRes['data'][0]['game_state'];
if (is_string($existingState)) {
$existingState = json_decode($existingState, true) ?? [];
}
}
$existingState['analysis'] = $analysisData;
supabase_rest('PATCH', "games?id=eq.{$gameId}", [
'game_state' => json_encode($existingState)
], $token);
echo json_encode([
'ok' => true,
'analysis' => $analysisData
]);
break;
case 'analyze_single':
// Analyze a single position (used for real-time partial analysis)
$fen = $input['fen'] ?? '';
if (!$fen) {
http_response_code(400);
echo json_encode(['error' => 'missing fen']);
exit;
}
$apiUrl = STOCKFISH_API . '/api/chess/move';
$payload = json_encode([
'bot_id' => 'grandmaster',
'fen' => $fen,
'wtime' => 120000,
'btime' => 120000
]);
$ch = curl_init($apiUrl);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/json',
'X-API-Key: ' . STOCKFISH_MGMT_KEY
]);
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode === 200) {
$data = json_decode($response, true);
echo json_encode([
'best_move' => $data['best_move'] ?? null,
'evaluation' => $data['evaluation'] ?? 0,
'depth' => $data['depth'] ?? 20
]);
} else {
http_response_code(502);
echo json_encode(['error' => 'engine unavailable']);
}
break;
default:
http_response_code(400);
echo json_encode(['error' => 'invalid action']);
}
<?php
require_once __DIR__ . '/../config/database.php';
header('Content-Type: application/json');
$token = get_auth_token();
if (!$token) {
http_response_code(401);
echo json_encode(['error' => 'unauthorized']);
exit;
}
$method = $_SERVER['REQUEST_METHOD'];
if ($method === 'GET') {
$action = $_GET['action'] ?? '';
if ($action === 'daily') {
// Get today's 3 daily puzzles (seeded by date)
$today = date('Y-m-d');
$seed = crc32($today);
// Fetch puzzles and use the seed to deterministically select 3
$offset = abs($seed) % 100;
$res = supabase_rest('GET', "puzzles?select=id,fen,moves,rating,themes,title&order=id.asc&offset={$offset}&limit=3", [], $token);
if ($res['status'] >= 200 && $res['status'] < 300) {
echo json_encode([
'puzzles' => $res['data'] ?? [],
'date' => $today
]);
} else {
echo json_encode(['puzzles' => [], 'date' => $today]);
}
} elseif ($action === 'streak') {
// Get player's current streak
$profileRes = supabase_rest('GET', 'profiles?select=puzzle_streak,best_puzzle_streak,puzzle_rating', [], $token);
if (!empty($profileRes['data'])) {
echo json_encode($profileRes['data'][0]);
} else {
echo json_encode(['puzzle_streak' => 0, 'best_puzzle_streak' => 0, 'puzzle_rating' => 1200]);
}
} else {
// List puzzles with optional filters
$theme = $_GET['theme'] ?? '';
$rating = intval($_GET['rating'] ?? 1200);
$limit = min(intval($_GET['limit'] ?? 20), 50);
$query = 'puzzles?select=id,fen,moves,rating,themes,title';
if ($theme) {
$query .= "&themes=cs.{{$theme}}";
}
// Get puzzles close to player's rating (within 200 range)
$ratingMin = $rating - 200;
$ratingMax = $rating + 200;
$query .= "&rating=gte.{$ratingMin}&rating=lte.{$ratingMax}";
$query .= "&order=rating.asc&limit={$limit}";
$res = supabase_rest('GET', $query, [], $token);
if ($res['status'] >= 200 && $res['status'] < 300) {
echo json_encode(['puzzles' => $res['data'] ?? []]);
} else {
echo json_encode(['puzzles' => []]);
}
}
exit;
}
// POST requests
$input = json_decode(file_get_contents('php://input'), true);
$action = $input['action'] ?? '';
switch ($action) {
case 'attempt':
$puzzleId = $input['puzzle_id'] ?? '';
$solved = $input['solved'] ?? false;
$timeMs = intval($input['time_ms'] ?? 0);
if (!$puzzleId) {
http_response_code(400);
echo json_encode(['error' => 'missing puzzle_id']);
exit;
}
// Get player's current puzzle_rating
$profileRes = supabase_rest('GET', 'profiles?select=id,puzzle_rating,puzzle_streak,best_puzzle_streak', [], $token);
$profile = $profileRes['data'][0] ?? null;
if (!$profile) {
http_response_code(404);
echo json_encode(['error' => 'profile not found']);
exit;
}
$playerRating = $profile['puzzle_rating'] ?? 1200;
$currentStreak = $profile['puzzle_streak'] ?? 0;
$bestStreak = $profile['best_puzzle_streak'] ?? 0;
// Get puzzle rating
$puzzleRes = supabase_rest('GET', "puzzles?id=eq.{$puzzleId}&select=rating", [], $token);
$puzzleRating = $puzzleRes['data'][0]['rating'] ?? 1200;
// ELO calculation (K-factor = 32 for puzzles)
$K = 32;
$expected = 1 / (1 + pow(10, ($puzzleRating - $playerRating) / 400));
$score = $solved ? 1 : 0;
$newRating = round($playerRating + $K * ($score - $expected));
// Update streak
$newStreak = $solved ? $currentStreak + 1 : 0;
$newBestStreak = max($bestStreak, $newStreak);
// Record attempt
$attemptData = [
'puzzle_id' => $puzzleId,
'solved' => $solved,
'time_ms' => $timeMs,
'rating_before' => $playerRating,
'rating_after' => $newRating
];
supabase_rest('POST', 'puzzle_attempts', $attemptData, $token);
// Update profile
supabase_rest('PATCH', "profiles?id=eq.{$profile['id']}", [
'puzzle_rating' => $newRating,
'puzzle_streak' => $newStreak,
'best_puzzle_streak' => $newBestStreak
], $token);
// Award coins for solving
$coinsAwarded = 0;
if ($solved) {
$coinsAwarded = 10;
if ($newStreak >= 5) $coinsAwarded = 20;
if ($newStreak >= 10) $coinsAwarded = 30;
supabase_rest('POST', 'rpc/add_coins', ['amount' => $coinsAwarded], $token);
}
echo json_encode([
'ok' => true,
'rating_before' => $playerRating,
'rating_after' => $newRating,
'rating_change' => $newRating - $playerRating,
'streak' => $newStreak,
'best_streak' => $newBestStreak,
'coins_awarded' => $coinsAwarded
]);
break;
case 'streak':
// Reset/get streak info
$profileRes = supabase_rest('GET', 'profiles?select=puzzle_streak,best_puzzle_streak,puzzle_rating', [], $token);
if (!empty($profileRes['data'])) {
echo json_encode($profileRes['data'][0]);
} else {
echo json_encode(['puzzle_streak' => 0, 'best_puzzle_streak' => 0, 'puzzle_rating' => 1200]);
}
break;
default:
http_response_code(400);
echo json_encode(['error' => 'invalid action']);
}
......@@ -29,6 +29,10 @@ if ($route === '' || $route === 'home') {
require 'pages/shop.php';
} elseif ($route === 'achievements') {
require 'pages/achievements.php';
} elseif ($route === 'analysis') {
require 'pages/analysis.php';
} elseif ($route === 'puzzles') {
require 'pages/puzzles.php';
} elseif ($route === 'notifications') {
require 'pages/notifications.php';
} elseif ($route === 'settings') {
......
This diff is collapsed.
......@@ -4,71 +4,185 @@ $extraCss = '/public/css/chessboard.css';
?>
<?php require __DIR__ . '/../includes/header.php'; ?>
<div class="game-container" id="game-container">
<div class="game-layout" id="game-container">
<!-- Board Column (eval bar + opponent/player info + board) -->
<div class="game-board-column">
<!-- Opponent info + clock -->
<div class="game-header">
<div class="game-player">
<div class="avatar avatar-sm" id="opponent-avatar">
<svg class="icon"><use href="/public/icons/sprite.svg#icon-bot"></use></svg>
</div>
<div>
<div class="game-player-name" id="opponent-name">Bot</div>
<div class="game-player-rating" id="opponent-rating">1200</div>
</div>
</div>
<div class="game-clock" id="clock-top">10:00</div>
</div>
<!-- Opponent info -->
<div class="game-header">
<div class="game-player">
<div class="avatar avatar-sm" id="opponent-avatar">
<svg class="icon"><use href="/public/icons/sprite.svg#icon-bot"></use></svg>
<!-- Board + Eval Bar -->
<div class="game-board-section">
<!-- Eval Bar -->
<div class="eval-bar" id="eval-bar">
<span class="eval-bar-label eval-bar-label-top" id="eval-label-top"></span>
<div class="eval-bar-fill" id="eval-bar-fill" style="height:50%;"></div>
<span class="eval-bar-label eval-bar-label-bottom" id="eval-label-bottom"></span>
</div>
<div>
<div class="game-player-name" id="opponent-name">Bot</div>
<div class="game-player-rating" id="opponent-rating">1200</div>
<!-- Board -->
<div class="board-wrapper">
<div class="board" id="board"></div>
<!-- Coordinate labels -->
<div class="board-coords-file" id="coords-file">
<span>a</span><span>b</span><span>c</span><span>d</span>
<span>e</span><span>f</span><span>g</span><span>h</span>
</div>
<div class="board-coords-rank" id="coords-rank">
<span>8</span><span>7</span><span>6</span><span>5</span>
<span>4</span><span>3</span><span>2</span><span>1</span>
</div>
<!-- Arrow overlay -->
<div class="board-arrows" id="board-arrows">
<svg viewBox="0 0 800 800" preserveAspectRatio="xMidYMid meet"></svg>
</div>
</div>
</div>
<div class="game-clock" id="clock-top">10:00</div>
</div>
<!-- Board -->
<div class="board-wrapper">
<div class="board" id="board"></div>
</div>
<!-- Thinking indicator -->
<div class="thinking" id="thinking-indicator" style="display:none;">
<div class="thinking-dots">
<span></span><span></span><span></span>
</div>
<span>يفكر...</span>
</div>
<!-- Thinking indicator -->
<div class="thinking" id="thinking-indicator" style="display:none;">
<div class="thinking-dots">
<span></span><span></span><span></span>
<!-- Pre-move indicator -->
<div class="premove-indicator" id="premove-indicator" style="display:none;">
نقلة مسبقة محجوزة
</div>
<!-- Player info + clock -->
<div class="game-header">
<div class="game-player">
<div class="avatar avatar-sm">
<svg class="icon"><use href="/public/icons/sprite.svg#icon-profile"></use></svg>
</div>
<div>
<div class="game-player-name" id="player-name">انت</div>
<div class="game-player-rating" id="player-rating">1200</div>
</div>
</div>
<div class="game-clock" id="clock-bottom">10:00</div>
</div>
<span>يفكر...</span>
</div>
<!-- Player info -->
<div class="game-header">
<div class="game-player">
<div class="avatar avatar-sm">
<svg class="icon"><use href="/public/icons/sprite.svg#icon-profile"></use></svg>
<!-- Side Panel (Desktop) -->
<div class="game-side-panel" id="side-panel">
<div class="analysis-panel">
<!-- Opening name -->
<div class="opening-display" id="opening-display" style="display:none;">
<span class="opening-eco" id="opening-eco"></span>
<span class="opening-name" id="opening-name"></span>
</div>
<div>
<div class="game-player-name" id="player-name">انت</div>
<div class="game-player-rating" id="player-rating">1200</div>
<!-- Move list -->
<div class="move-list-pro" id="move-list"></div>
<!-- Game status -->
<div class="panel-status" id="game-status">دورك</div>
<!-- Controls -->
<div class="panel-controls" id="game-controls">
<button class="btn btn-ghost btn-sm" onclick="Game.resign()" id="btn-resign">
<svg class="icon"><use href="/public/icons/sprite.svg#icon-flag"></use></svg>
استسلام
</button>
<button class="btn btn-ghost btn-sm" onclick="Board.flip()">
<svg class="icon"><use href="/public/icons/sprite.svg#icon-arrows"></use></svg>
اقلب
</button>
<button class="btn btn-ghost btn-sm" onclick="Game.toggleArrows()" id="btn-arrows">
<svg class="icon"><use href="/public/icons/sprite.svg#icon-eye"></use></svg>
اسهم
</button>
</div>
<!-- Post-game controls (hidden initially) -->
<div class="postgame-controls" id="postgame-controls" style="display:none;">
<button class="btn btn-ghost btn-sm" onclick="Game.exportPGN()">
<svg class="icon"><use href="/public/icons/sprite.svg#icon-download"></use></svg>
PGN
</button>
<button class="btn btn-ghost btn-sm" onclick="Game.copyFEN()">
<svg class="icon"><use href="/public/icons/sprite.svg#icon-copy"></use></svg>
FEN
</button>
<button class="btn btn-ghost btn-sm" onclick="Game.analyze()">
<svg class="icon"><use href="/public/icons/sprite.svg#icon-chart"></use></svg>
تحليل
</button>
</div>
</div>
<div class="game-clock" id="clock-bottom">10:00</div>
</div>
<!-- Status -->
<div class="text-center text-sm text-muted" id="game-status">دورك</div>
<!-- Move list -->
<div class="move-list" id="move-list"></div>
<!-- Controls -->
<div class="game-controls">
<button class="btn btn-ghost btn-sm" onclick="Game.resign()">
<svg class="icon"><use href="/public/icons/sprite.svg#icon-flag"></use></svg>
استسلام
</button>
<button class="btn btn-ghost btn-sm" onclick="Board.flip()">
<svg class="icon"><use href="/public/icons/sprite.svg#icon-arrows"></use></svg>
اقلب
</button>
<!-- Mobile Panel (shown only on mobile) -->
<div class="game-mobile-panel" id="mobile-panel">
<div class="analysis-panel">
<!-- Opening name (mobile) -->
<div class="opening-display" id="opening-display-mobile" style="display:none;">
<span class="opening-eco" id="opening-eco-mobile"></span>
<span class="opening-name" id="opening-name-mobile"></span>
</div>
<!-- Move list (mobile) -->
<div class="move-list-pro" id="move-list-mobile"></div>
<!-- Game status (mobile) -->
<div class="panel-status" id="game-status-mobile">دورك</div>
<!-- Controls (mobile) -->
<div class="panel-controls" id="game-controls-mobile">
<button class="btn btn-ghost btn-sm" onclick="Game.resign()">
<svg class="icon"><use href="/public/icons/sprite.svg#icon-flag"></use></svg>
استسلام
</button>
<button class="btn btn-ghost btn-sm" onclick="Board.flip()">
<svg class="icon"><use href="/public/icons/sprite.svg#icon-arrows"></use></svg>
اقلب
</button>
<button class="btn btn-ghost btn-sm" onclick="Game.toggleArrows()">
<svg class="icon"><use href="/public/icons/sprite.svg#icon-eye"></use></svg>
اسهم
</button>
</div>
<!-- Post-game controls (mobile, hidden initially) -->
<div class="postgame-controls" id="postgame-controls-mobile" style="display:none;">
<button class="btn btn-ghost btn-sm" onclick="Game.exportPGN()">
<svg class="icon"><use href="/public/icons/sprite.svg#icon-download"></use></svg>
PGN
</button>
<button class="btn btn-ghost btn-sm" onclick="Game.copyFEN()">
<svg class="icon"><use href="/public/icons/sprite.svg#icon-copy"></use></svg>
FEN
</button>
<button class="btn btn-ghost btn-sm" onclick="Game.analyze()">
<svg class="icon"><use href="/public/icons/sprite.svg#icon-chart"></use></svg>
تحليل
</button>
</div>
</div>
</div>
</div>
<script src="/public/js/chess.min.js"></script>
<script src="/public/js/board.js"></script>
<script src="/public/js/openings.js"></script>
<script src="/public/js/game.js"></script>
<script>
document.addEventListener('DOMContentLoaded', () => {
......
This diff is collapsed.
......@@ -114,6 +114,11 @@
}
.square.last-move { background: rgba(231, 168, 50, 0.3) !important; }
.square.in-check { background: rgba(239, 68, 68, 0.5) !important; }
.square.premove-from { background: rgba(21, 180, 240, 0.35) !important; }
.square.premove-to { background: rgba(21, 180, 240, 0.35) !important; }
.square.highlight-green { background: rgba(21, 180, 90, 0.45) !important; }
.square.highlight-red { background: rgba(220, 50, 50, 0.45) !important; }
.square.highlight-yellow { background: rgba(220, 180, 30, 0.45) !important; }
/* Pieces */
.piece {
......@@ -236,6 +241,28 @@
color: var(--text-2);
}
.game-result-stats {
display: flex;
flex-direction: column;
gap: 4px;
margin-top: 12px;
font-family: var(--font-en);
font-size: 13px;
width: 180px;
}
.game-result-stats .stat-row {
display: flex;
justify-content: space-between;
padding: 3px 0;
color: var(--text-2);
}
.game-result-stats .stat-row span:last-child {
font-weight: 600;
color: var(--text-1, #fff);
}
/* Thinking indicator */
.thinking {
display: flex;
......@@ -298,3 +325,419 @@
}
.promotion-piece:hover { background: var(--cyan); }
/* ============================================
PRO FEATURES
============================================ */
/* Eval Bar */
.eval-bar {
width: 24px;
min-width: 24px;
height: 100%;
background: #333;
border-radius: var(--radius-sm);
overflow: hidden;
position: relative;
border: 1px solid var(--border);
}
.eval-bar-fill {
position: absolute;
bottom: 0;
left: 0;
right: 0;
background: #fff;
transition: height 0.5s ease;
height: 50%;
}
.eval-bar-label {
position: absolute;
left: 0;
right: 0;
text-align: center;
font-family: var(--font-en);
font-size: 9px;
font-weight: 700;
pointer-events: none;
z-index: 1;
}
.eval-bar-label-top {
top: 4px;
color: #fff;
}
.eval-bar-label-bottom {
bottom: 4px;
color: #333;
}
/* Arrow Overlay */
.board-arrows {
position: absolute;
inset: 0;
pointer-events: none;
z-index: 5;
}
.board-arrows svg {
width: 100%;
height: 100%;
position: absolute;
inset: 0;
}
.board-arrows svg line,
.board-arrows svg polygon {
opacity: 0.7;
}
/* Square Highlights */
.square-highlight-green {
background: rgba(21, 215, 100, 0.4) !important;
}
.square-highlight-red {
background: rgba(239, 68, 68, 0.45) !important;
}
.square-highlight-yellow {
background: rgba(255, 200, 0, 0.4) !important;
}
/* Pre-move Ghost Piece */
.piece-ghost {
opacity: 0.45;
pointer-events: none;
filter: grayscale(0.3);
}
/* Move Classification Badges */
.move-badge {
display: inline-flex;
align-items: center;
justify-content: center;
width: 16px;
height: 16px;
border-radius: 50%;
font-size: 9px;
font-weight: 800;
font-family: var(--font-en);
margin-inline-start: 4px;
vertical-align: middle;
flex-shrink: 0;
}
.move-brilliant {
background: #26c6da;
color: #fff;
}
.move-great {
background: #66bb6a;
color: #fff;
}
.move-good {
background: #81c784;
color: #fff;
}
.move-inaccuracy {
background: #fdd835;
color: #333;
}
.move-mistake {
background: #ef6c00;
color: #fff;
}
.move-blunder {
background: #e53935;
color: #fff;
}
/* Analysis Panel */
.analysis-panel {
background: var(--bg-2);
border: 1px solid var(--border);
border-radius: var(--radius-md);
display: flex;
flex-direction: column;
overflow: hidden;
min-height: 0;
}
.analysis-panel-header {
padding: 10px 14px;
border-bottom: 1px solid var(--border);
font-size: 13px;
font-weight: 600;
display: flex;
align-items: center;
gap: 8px;
}
.analysis-eval-graph {
width: 100%;
height: 80px;
min-height: 60px;
background: var(--bg-3);
border-bottom: 1px solid var(--border);
position: relative;
}
.analysis-eval-graph canvas {
width: 100%;
height: 100%;
display: block;
}
/* Puzzle UI */
.puzzle-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 16px;
background: var(--bg-2);
border: 1px solid var(--border);
border-radius: var(--radius-md);
}
.puzzle-streak {
display: flex;
align-items: center;
gap: 6px;
font-family: var(--font-en);
font-size: 18px;
font-weight: 700;
color: var(--gold);
}
.puzzle-timer {
font-family: var(--font-en);
font-size: 16px;
font-weight: 600;
color: var(--text-2);
padding: 4px 10px;
background: var(--bg-3);
border-radius: var(--radius-sm);
}
/* Pro Move List */
.move-list-pro {
flex: 1;
overflow-y: auto;
font-family: var(--font-en);
font-size: 13px;
direction: ltr;
text-align: left;
padding: 8px 0;
min-height: 0;
max-height: 320px;
}
.move-list-pro .move-pair {
display: flex;
align-items: center;
padding: 3px 12px;
gap: 2px;
}
.move-list-pro .move-pair:nth-child(odd) {
background: var(--bg-3);
}
.move-list-pro .move-number {
color: var(--text-3);
min-width: 30px;
font-size: 12px;
}
.move-list-pro .move {
padding: 3px 8px;
border-radius: 4px;
cursor: pointer;
min-width: 54px;
text-align: center;
}
.move-list-pro .move:hover {
background: var(--bg-hover, rgba(255,255,255,0.08));
}
.move-list-pro .move.current {
background: var(--cyan);
color: var(--text-inverse);
}
/* Opening Display */
.opening-display {
padding: 8px 14px;
border-bottom: 1px solid var(--border);
font-size: 12px;
color: var(--text-2);
display: flex;
align-items: center;
gap: 6px;
}
.opening-eco {
font-family: var(--font-en);
font-weight: 700;
color: var(--cyan);
font-size: 11px;
}
.opening-name {
font-weight: 500;
}
/* Game Status in Panel */
.panel-status {
padding: 10px 14px;
border-top: 1px solid var(--border);
font-size: 13px;
text-align: center;
color: var(--text-2);
}
/* Post-game Controls */
.postgame-controls {
display: flex;
gap: 6px;
padding: 10px 14px;
border-top: 1px solid var(--border);
flex-wrap: wrap;
}
.postgame-controls .btn {
flex: 1;
min-width: 0;
font-size: 12px;
padding: 6px 8px;
}
/* Panel Controls Row */
.panel-controls {
display: flex;
gap: 6px;
padding: 10px 14px;
border-top: 1px solid var(--border);
}
.panel-controls .btn {
flex: 1;
font-size: 12px;
padding: 6px 8px;
}
/* Pre-move indicator */
.premove-indicator {
font-size: 11px;
color: var(--cyan);
padding: 4px 14px;
border-top: 1px solid var(--border);
}
/* ============================================
PRO GAME LAYOUT (Desktop + Mobile)
============================================ */
.game-layout {
display: flex;
flex-direction: column;
gap: 12px;
max-width: 100%;
}
.game-board-section {
display: flex;
gap: 8px;
align-items: stretch;
}
.game-side-panel {
display: none;
flex-direction: column;
width: 280px;
min-width: 260px;
max-height: calc(100vw - 60px);
}
/* Eval graph canvas container */
.eval-graph-container {
position: relative;
width: 100%;
height: 80px;
background: var(--bg-3);
}
.eval-graph-container canvas {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
}
/* ============================================
RESPONSIVE
============================================ */
@media (min-width: 768px) {
.game-layout {
flex-direction: row;
align-items: flex-start;
gap: 16px;
}
.game-board-column {
flex: 0 0 auto;
}
.game-side-panel {
display: flex;
flex: 1;
max-width: 320px;
max-height: 560px;
}
.game-mobile-panel {
display: none;
}
.board-wrapper {
max-width: 560px;
}
}
@media (max-width: 767px) {
.game-layout {
flex-direction: column;
}
.game-side-panel {
display: none;
}
.game-mobile-panel {
display: flex;
flex-direction: column;
}
.game-mobile-panel .analysis-panel {
max-height: 260px;
}
.game-mobile-panel .move-list-pro {
max-height: 160px;
}
.eval-bar {
width: 18px;
min-width: 18px;
}
}
......@@ -133,4 +133,30 @@
<path d="M9.879 16.121A3 3 0 1012.015 11L11 14H9c0 .768.293 1.536.879 2.121z"/>
</symbol>
<symbol id="icon-eye" viewBox="0 0 24 24">
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/>
<circle cx="12" cy="12" r="3"/>
</symbol>
<symbol id="icon-download" viewBox="0 0 24 24">
<path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4M7 10l5 5 5-5M12 15V3"/>
</symbol>
<symbol id="icon-copy" viewBox="0 0 24 24">
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"/>
<path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"/>
</symbol>
<symbol id="icon-chart" viewBox="0 0 24 24">
<path d="M18 20V10M12 20V4M6 20v-6"/>
</symbol>
<symbol id="icon-puzzle" viewBox="0 0 24 24">
<path d="M14.5 2.5a2.5 2.5 0 015 0v1h1a2 2 0 012 2v3.5h-1a2.5 2.5 0 000 5h1V17a2 2 0 01-2 2h-3.5v-1a2.5 2.5 0 00-5 0v1H7.5a2 2 0 01-2-2v-3.5h1a2.5 2.5 0 000-5h-1V5a2 2 0 012-2h3.5v-.5z"/>
</symbol>
<symbol id="icon-brain" viewBox="0 0 24 24">
<path d="M12 2C9.5 2 7.5 4 7.5 6.5c0 1-.5 2-1.5 2.5-1.5 1-2.5 2.5-2.5 4.5C3.5 16 5.5 18 8 18h1v3h6v-3h1c2.5 0 4.5-2 4.5-4.5 0-2-1-3.5-2.5-4.5-1-.5-1.5-1.5-1.5-2.5C16.5 4 14.5 2 12 2z"/>
</symbol>
</svg>
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
......@@ -5,6 +5,7 @@
$navItems = [
['/', 'icon-home', 'الرئيسية'],
['/play', 'icon-play', 'العب'],
['/puzzles', 'icon-puzzle', 'تمارين'],
['/tournaments', 'icon-trophy', 'بطولات'],
['/leaderboard', 'icon-leaderboard', 'متصدرون'],
['/friends', 'icon-friends', 'اجتماعي'],
......
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