Commit 0aa09744 authored by Mahmoud Aglan's avatar Mahmoud Aglan

feat: add full Ludo game + theme editor + CSS variable unification

- Ludo: 4-player support with CSS-only board (15x15 grid), bot AI
  (easy/hard), local pass-and-play, multiplayer via Supabase Realtime,
  in-game chat, matchmaking, private rooms, mixed human+bot games
- Theme editor: admin panel at /admin/theme with 9 categories,
  color pickers, file uploads, DB persistence, global cache
- CSS vars: unified all hardcoded colors across the codebase
  (chess board, analysis, bots, overlays, arrows, eval bar)
- New files: ludo-constants, ludo-ui, ludo-bot, ludo-game, ludo-live,
  ludo-chat, api/ludo, api/theme, pages/ludo*, admin-theme
- DB: ludo_matches + ludo_queue tables with RLS policies
- Nav: added Ludo icon to sprite.svg and both nav menus
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent 9924c814
...@@ -15,3 +15,8 @@ Connections and docs / ...@@ -15,3 +15,8 @@ Connections and docs /
# Deps (none for now, but future-proofing) # Deps (none for now, but future-proofing)
vendor/ vendor/
node_modules/ node_modules/
# Theme cache & uploads
storage/theme-cache.json
public/uploads/theme/*
!public/uploads/theme/.gitkeep
...@@ -29,3 +29,7 @@ RewriteRule ^(.*)$ index.php?route=$1 [QSA,L] ...@@ -29,3 +29,7 @@ RewriteRule ^(.*)$ index.php?route=$1 [QSA,L]
<Files "config/*"> <Files "config/*">
Require all denied Require all denied
</Files> </Files>
<Files "storage/*">
Require all denied
</Files>
This diff is collapsed.
<?php
require_once __DIR__ . '/../config/database.php';
header('Content-Type: application/json');
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['error' => 'method not allowed']);
exit;
}
$adminUser = $_POST['admin_user'] ?? '';
$adminPass = $_POST['admin_pass'] ?? '';
if ($adminUser !== 'admin' || $adminPass !== 'Alarcade123#') {
http_response_code(403);
echo json_encode(['error' => 'forbidden']);
exit;
}
if (empty($_FILES['file'])) {
http_response_code(400);
echo json_encode(['error' => 'no file uploaded']);
exit;
}
$file = $_FILES['file'];
$key = $_POST['key'] ?? '';
$category = $_POST['category'] ?? 'assets';
if (!$key) {
http_response_code(400);
echo json_encode(['error' => 'key is required']);
exit;
}
$allowed = ['image/png', 'image/jpeg', 'image/svg+xml', 'image/webp', 'image/gif'];
if (!in_array($file['type'], $allowed)) {
http_response_code(400);
echo json_encode(['error' => 'invalid file type']);
exit;
}
$maxSize = 2 * 1024 * 1024;
if ($file['size'] > $maxSize) {
http_response_code(400);
echo json_encode(['error' => 'file too large (max 2MB)']);
exit;
}
$uploadDir = __DIR__ . '/../public/uploads/theme/';
if (!is_dir($uploadDir)) {
mkdir($uploadDir, 0755, true);
}
$ext = pathinfo($file['name'], PATHINFO_EXTENSION);
$safeKey = preg_replace('/[^a-zA-Z0-9_-]/', '_', $key);
$filename = $safeKey . '.' . $ext;
$destPath = $uploadDir . $filename;
if (!move_uploaded_file($file['tmp_name'], $destPath)) {
http_response_code(500);
echo json_encode(['error' => 'upload failed']);
exit;
}
$publicUrl = '/public/uploads/theme/' . $filename;
$existing = supabase_rest('GET', 'theme_settings?key=eq.' . urlencode($key) . '&select=id', [], SUPABASE_SERVICE_KEY);
if (!empty($existing['data']) && is_array($existing['data']) && !isset($existing['data']['code'])) {
supabase_rest('PATCH', 'theme_settings?key=eq.' . urlencode($key), [
'value' => $publicUrl,
'category' => $category,
'label' => $_POST['label'] ?? $key,
'updated_at' => date('c'),
], SUPABASE_SERVICE_KEY);
} else {
supabase_rest('POST', 'theme_settings', [
'key' => $key,
'value' => $publicUrl,
'category' => $category,
'label' => $_POST['label'] ?? $key,
], SUPABASE_SERVICE_KEY);
}
echo json_encode(['ok' => true, 'url' => $publicUrl]);
<?php
require_once __DIR__ . '/../config/database.php';
header('Content-Type: application/json');
$method = $_SERVER['REQUEST_METHOD'];
if ($method === 'GET') {
$res = supabase_rest('GET', 'theme_settings?select=key,value,category', [], SUPABASE_SERVICE_KEY);
$settings = ($res['status'] === 200 && is_array($res['data']) && !isset($res['data']['code'])) ? $res['data'] : [];
echo json_encode(['settings' => $settings]);
exit;
}
if ($method === 'POST') {
$input = json_decode(file_get_contents('php://input'), true);
$adminUser = $input['admin_user'] ?? '';
$adminPass = $input['admin_pass'] ?? '';
if ($adminUser !== 'admin' || $adminPass !== 'Alarcade123#') {
http_response_code(403);
echo json_encode(['error' => 'forbidden']);
exit;
}
$action = $input['action'] ?? '';
if ($action === 'save') {
$settings = $input['settings'] ?? [];
$errors = [];
foreach ($settings as $item) {
$key = $item['key'] ?? '';
$value = $item['value'] ?? '';
$category = $item['category'] ?? 'colors';
$label = $item['label'] ?? $key;
if (!$key || !$value) continue;
$existing = supabase_rest('GET', 'theme_settings?key=eq.' . urlencode($key) . '&select=id', [], SUPABASE_SERVICE_KEY);
if (!empty($existing['data']) && is_array($existing['data']) && !isset($existing['data']['code'])) {
$res = supabase_rest('PATCH', 'theme_settings?key=eq.' . urlencode($key), [
'value' => $value,
'category' => $category,
'label' => $label,
'updated_at' => date('c'),
], SUPABASE_SERVICE_KEY);
} else {
$res = supabase_rest('POST', 'theme_settings', [
'key' => $key,
'value' => $value,
'category' => $category,
'label' => $label,
], SUPABASE_SERVICE_KEY);
}
if ($res['status'] >= 400) {
$errors[] = $key;
}
}
echo json_encode(['ok' => true, 'errors' => $errors]);
} elseif ($action === 'delete') {
$key = $input['key'] ?? '';
if ($key) {
supabase_rest('DELETE', 'theme_settings?key=eq.' . urlencode($key), [], SUPABASE_SERVICE_KEY);
}
echo json_encode(['ok' => true]);
} elseif ($action === 'upload') {
echo json_encode(['error' => 'use /api/theme-upload for file uploads']);
} else {
echo json_encode(['error' => 'invalid action']);
}
}
...@@ -8,6 +8,37 @@ ...@@ -8,6 +8,37 @@
<div class="toast-container" id="toast-container"></div> <div class="toast-container" id="toast-container"></div>
<script src="/public/js/app.js"></script> <script src="/public/js/app.js"></script>
<script>
if (window.__themeAssets) {
document.addEventListener('DOMContentLoaded', () => {
const a = window.__themeAssets;
if (a['logo-text']) {
document.querySelectorAll('.header-logo, .nav-desktop-logo').forEach(el => { el.textContent = a['logo-text']; });
}
if (a['logo-image']) {
document.querySelectorAll('.header-logo').forEach(el => {
el.innerHTML = '<img src="' + a['logo-image'] + '" alt="Logo" style="height:28px;">';
});
}
if (a['favicon']) {
let link = document.querySelector('link[rel="icon"]') || document.createElement('link');
link.rel = 'icon'; link.href = a['favicon'];
document.head.appendChild(link);
}
if (a['sprite-svg']) {
document.querySelectorAll('use[href^="/public/icons/sprite.svg"]').forEach(el => {
el.setAttribute('href', el.getAttribute('href').replace('/public/icons/sprite.svg', a['sprite-svg']));
});
}
Object.keys(a).forEach(k => {
if (k.startsWith('piece-')) {
const piece = k.replace('piece-', '');
document.documentElement.style.setProperty('--piece-' + piece, 'url(' + a[k] + ')');
}
});
});
}
</script>
<?php if (isset($extraJs)): ?> <?php if (isset($extraJs)): ?>
<script src="<?= $extraJs ?>"></script> <script src="<?= $extraJs ?>"></script>
<?php endif; ?> <?php endif; ?>
......
...@@ -11,6 +11,7 @@ ...@@ -11,6 +11,7 @@
<?php if (isset($extraCss)): ?> <?php if (isset($extraCss)): ?>
<link rel="stylesheet" href="<?= $extraCss ?>"> <link rel="stylesheet" href="<?= $extraCss ?>">
<?php endif; ?> <?php endif; ?>
<?php require_once __DIR__ . '/theme-loader.php'; ?>
</head> </head>
<body> <body>
<div class="app"> <div class="app">
......
<?php
/**
* Loads theme overrides from DB and outputs a <style> block with CSS variable overrides.
* Caches in a local file for 60 seconds to avoid hitting Supabase on every request.
*/
require_once __DIR__ . '/../config/database.php';
function get_theme_overrides(): array {
$cacheFile = __DIR__ . '/../storage/theme-cache.json';
$cacheTTL = 60;
if (file_exists($cacheFile) && (time() - filemtime($cacheFile)) < $cacheTTL) {
$cached = json_decode(file_get_contents($cacheFile), true);
if (is_array($cached)) return $cached;
}
$res = supabase_rest('GET', 'theme_settings?select=key,value,category', [], SUPABASE_SERVICE_KEY);
$settings = ($res['status'] === 200 && is_array($res['data']) && !isset($res['data']['code'])) ? $res['data'] : [];
$cacheDir = dirname($cacheFile);
if (!is_dir($cacheDir)) mkdir($cacheDir, 0755, true);
file_put_contents($cacheFile, json_encode($settings));
return $settings;
}
function render_theme_style(): void {
$settings = get_theme_overrides();
if (empty($settings)) return;
$cssVars = [];
$assetOverrides = [];
foreach ($settings as $s) {
$key = $s['key'];
$value = $s['value'];
$category = $s['category'];
if ($category === 'assets' || $category === 'icons' || $category === 'pieces') {
$assetOverrides[$key] = $value;
} else {
$cssVars[] = ' --' . htmlspecialchars($key) . ': ' . htmlspecialchars($value) . ';';
}
}
if (!empty($cssVars)) {
echo "<style id=\"theme-overrides\">\n:root {\n" . implode("\n", $cssVars) . "\n}\n</style>\n";
}
if (!empty($assetOverrides)) {
echo "<script>window.__themeAssets = " . json_encode($assetOverrides) . ";</script>\n";
}
}
render_theme_style();
...@@ -45,6 +45,16 @@ if ($route === '' || $route === 'home') { ...@@ -45,6 +45,16 @@ if ($route === '' || $route === 'home') {
require 'pages/orgs.php'; require 'pages/orgs.php';
} elseif ($route === 'org') { } elseif ($route === 'org') {
require 'pages/org.php'; require 'pages/org.php';
} elseif ($route === 'ludo') {
require 'pages/ludo.php';
} elseif ($route === 'ludo-game') {
require 'pages/ludo-game.php';
} elseif ($route === 'ludo-live') {
require 'pages/ludo-live.php';
} elseif ($route === 'ludo-matchmaking') {
require 'pages/ludo-matchmaking.php';
} elseif ($route === 'admin/theme') {
require 'pages/admin-theme.php';
} elseif (str_starts_with($route, 'api/')) { } elseif (str_starts_with($route, 'api/')) {
$apiFile = str_replace('api/', '', $route); $apiFile = str_replace('api/', '', $route);
$apiPath = __DIR__ . '/api/' . basename($apiFile) . '.php'; $apiPath = __DIR__ . '/api/' . basename($apiFile) . '.php';
......
This diff is collapsed.
...@@ -189,7 +189,7 @@ $extraCss = '/public/css/chessboard.css'; ...@@ -189,7 +189,7 @@ $extraCss = '/public/css/chessboard.css';
.eval-bar { .eval-bar {
width: 24px; width: 24px;
min-height: 100%; min-height: 100%;
background: #333; background: var(--eval-bg);
border-radius: var(--radius-sm); border-radius: var(--radius-sm);
position: relative; position: relative;
overflow: hidden; overflow: hidden;
...@@ -203,7 +203,7 @@ $extraCss = '/public/css/chessboard.css'; ...@@ -203,7 +203,7 @@ $extraCss = '/public/css/chessboard.css';
left: 0; left: 0;
right: 0; right: 0;
height: 50%; height: 50%;
background: #f0f0f0; background: var(--eval-white);
transition: height 0.3s ease; transition: height 0.3s ease;
} }
...@@ -219,7 +219,7 @@ $extraCss = '/public/css/chessboard.css'; ...@@ -219,7 +219,7 @@ $extraCss = '/public/css/chessboard.css';
writing-mode: vertical-lr; writing-mode: vertical-lr;
text-orientation: mixed; text-orientation: mixed;
z-index: 1; z-index: 1;
text-shadow: 0 0 3px rgba(0,0,0,0.8); text-shadow: 0 0 3px var(--overlay-dark);
} }
.analysis-board-section .board-wrapper { .analysis-board-section .board-wrapper {
...@@ -279,13 +279,13 @@ $extraCss = '/public/css/chessboard.css'; ...@@ -279,13 +279,13 @@ $extraCss = '/public/css/chessboard.css';
font-weight: 700; font-weight: 700;
margin-right: 2px; margin-right: 2px;
} }
.move-class-brilliant { color: #00bcd4; } .move-class-brilliant { color: var(--move-text-brilliant); }
.move-class-great { color: #2196f3; } .move-class-great { color: var(--move-text-great); }
.move-class-good { color: #4caf50; } .move-class-good { color: var(--move-text-good); }
.move-class-book { color: #9e9e9e; } .move-class-book { color: var(--move-book); }
.move-class-inaccuracy { color: #ff9800; } .move-class-inaccuracy { color: var(--move-text-inaccuracy); }
.move-class-mistake { color: #f44336; } .move-class-mistake { color: var(--move-text-mistake); }
.move-class-blunder { color: #d32f2f; } .move-class-blunder { color: var(--move-text-blunder); }
/* Sections */ /* Sections */
.analysis-section { .analysis-section {
......
...@@ -12,13 +12,13 @@ ...@@ -12,13 +12,13 @@
<?php <?php
$bots = [ $bots = [
['id' => 'amina', 'name' => 'امينة', 'level' => 'مبتدئة', 'elo' => 800, 'letter' => 'A', 'gradient' => '#4ade80,#22c55e', 'bar' => 11, 'barColor' => 'var(--success)'], ['id' => 'amina', 'name' => 'امينة', 'level' => 'مبتدئة', 'elo' => 800, 'letter' => 'A', 'gradient' => 'var(--bot-amina)', 'bar' => 11, 'barColor' => 'var(--success)'],
['id' => 'tarek', 'name' => 'طارق', 'level' => 'هاوي', 'elo' => 1000, 'letter' => 'T', 'gradient' => '#38bdf8,#0ea5e9', 'bar' => 25, 'barColor' => 'var(--success)'], ['id' => 'tarek', 'name' => 'طارق', 'level' => 'هاوي', 'elo' => 1000, 'letter' => 'T', 'gradient' => 'var(--bot-tarek)', 'bar' => 25, 'barColor' => 'var(--success)'],
['id' => 'nour', 'name' => 'نور', 'level' => 'متوسطة', 'elo' => 1200, 'letter' => 'N', 'gradient' => '#a78bfa,#7c3aed', 'bar' => 40, 'barColor' => 'var(--warning)'], ['id' => 'nour', 'name' => 'نور', 'level' => 'متوسطة', 'elo' => 1200, 'letter' => 'N', 'gradient' => 'var(--bot-nour)', 'bar' => 40, 'barColor' => 'var(--warning)'],
['id' => 'omar', 'name' => 'عمر', 'level' => 'جيد', 'elo' => 1400, 'letter' => 'O', 'gradient' => '#fb923c,#ea580c', 'bar' => 55, 'barColor' => 'var(--warning)'], ['id' => 'omar', 'name' => 'عمر', 'level' => 'جيد', 'elo' => 1400, 'letter' => 'O', 'gradient' => 'var(--bot-omar)', 'bar' => 55, 'barColor' => 'var(--warning)'],
['id' => 'layla', 'name' => 'ليلى', 'level' => 'قوية', 'elo' => 1600, 'letter' => 'L', 'gradient' => '#f472b6,#db2777', 'bar' => 70, 'barColor' => 'var(--error)'], ['id' => 'layla', 'name' => 'ليلى', 'level' => 'قوية', 'elo' => 1600, 'letter' => 'L', 'gradient' => 'var(--bot-layla)', 'bar' => 70, 'barColor' => 'var(--error)'],
['id' => 'ziad', 'name' => 'زياد', 'level' => 'خبير', 'elo' => 1800, 'letter' => 'Z', 'gradient' => '#f87171,#dc2626', 'bar' => 85, 'barColor' => 'var(--error)'], ['id' => 'ziad', 'name' => 'زياد', 'level' => 'خبير', 'elo' => 1800, 'letter' => 'Z', 'gradient' => 'var(--bot-ziad)', 'bar' => 85, 'barColor' => 'var(--error)'],
['id' => 'grandmaster', 'name' => 'الاستاذ الكبير', 'level' => 'جراند ماستر', 'elo' => 2200, 'letter' => 'GM', 'gradient' => 'var(--gold),#b45309', 'bar' => 100, 'barColor' => 'var(--purple)'], ['id' => 'grandmaster', 'name' => 'الاستاذ الكبير', 'level' => 'جراند ماستر', 'elo' => 2200, 'letter' => 'GM', 'gradient' => 'var(--bot-gm)', 'bar' => 100, 'barColor' => 'var(--purple)'],
]; ];
foreach ($bots as $bot): ?> foreach ($bots as $bot): ?>
<div class="card card-hover bot-card" data-bot="<?= $bot['id'] ?>" data-elo="<?= $bot['elo'] ?>"> <div class="card card-hover bot-card" data-bot="<?= $bot['id'] ?>" data-elo="<?= $bot['elo'] ?>">
...@@ -26,7 +26,7 @@ foreach ($bots as $bot): ?> ...@@ -26,7 +26,7 @@ foreach ($bots as $bot): ?>
<img src="https://stockfishapi.caprover.al-arcade.com/portraits/<?= $bot['id'] ?>.jpg" <img src="https://stockfishapi.caprover.al-arcade.com/portraits/<?= $bot['id'] ?>.jpg"
class="avatar" style="object-fit:cover;" class="avatar" style="object-fit:cover;"
onerror="this.style.display='none';this.nextElementSibling.style.display='flex';"> onerror="this.style.display='none';this.nextElementSibling.style.display='flex';">
<div class="avatar" style="display:none;background:linear-gradient(135deg,<?= $bot['gradient'] ?>);color:#fff;font-weight:700;font-size:18px;align-items:center;justify-content:center;"><?= $bot['letter'] ?></div> <div class="avatar" style="display:none;background:linear-gradient(135deg,<?= $bot['gradient'] ?>);color:var(--text-1);font-weight:700;font-size:18px;align-items:center;justify-content:center;"><?= $bot['letter'] ?></div>
<div style="flex:1;"> <div style="flex:1;">
<p style="font-size:16px;font-weight:600;"><?= $bot['name'] ?></p> <p style="font-size:16px;font-weight:600;"><?= $bot['name'] ?></p>
<p class="text-muted text-xs"><?= $bot['level'] ?> - ELO <?= $bot['elo'] ?></p> <p class="text-muted text-xs"><?= $bot['level'] ?> - ELO <?= $bot['elo'] ?></p>
......
...@@ -37,7 +37,7 @@ ...@@ -37,7 +37,7 @@
ما عندك حساب؟ <a href="/register" style="color:var(--cyan);">سجل الان</a> ما عندك حساب؟ <a href="/register" style="color:var(--cyan);">سجل الان</a>
</p> </p>
<div id="login-error" style="display:none;margin-top:16px;padding:12px;background:rgba(239,68,68,0.1);border:1px solid rgba(239,68,68,0.2);border-radius:var(--radius-md);color:var(--error);font-size:13px;text-align:center;"></div> <div id="login-error" style="display:none;margin-top:16px;padding:12px;background:var(--overlay-error-bg);border:1px solid var(--overlay-error-border);border-radius:var(--radius-md);color:var(--error);font-size:13px;text-align:center;"></div>
</div> </div>
</div> </div>
......
<?php
$pageTitle = 'EL3AB - لودو';
$extraCss = ['/public/css/ludo.css'];
$extraJs = ['/public/js/ludo-constants.js', '/public/js/ludo-ui.js', '/public/js/ludo-bot.js', '/public/js/ludo-game.js'];
?>
<?php require __DIR__ . '/../includes/header.php'; ?>
<div class="ludo-layout">
<div class="ludo-board-column">
<div class="ludo-players-row" id="ludo-top-players"></div>
<div class="ludo-board-wrapper">
<div id="ludo-board"></div>
</div>
<div class="ludo-players-row" id="ludo-bottom-players"></div>
<div class="ludo-mobile-panel">
<div class="ludo-turn-indicator" id="ludo-turn-mobile"></div>
<div id="ludo-dice-container-mobile"></div>
</div>
</div>
<div class="ludo-side-panel">
<div class="ludo-turn-indicator" id="ludo-turn"></div>
<div id="ludo-dice-container"></div>
<div class="ludo-log" id="ludo-log"></div>
</div>
</div>
<script>
(function() {
var params = new URLSearchParams(window.location.search);
var mode = params.get('mode') || 'local';
var playerCount = parseInt(params.get('players') || '4', 10);
var botCount = parseInt(params.get('bots') || '1', 10);
var difficulty = params.get('difficulty') || 'easy';
var allPlayers = ['P1', 'P2', 'P3', 'P4'];
var activePlayers, bots, playerNames;
if (mode === 'bot') {
var totalPlayers = 1 + botCount;
if (totalPlayers > 4) totalPlayers = 4;
activePlayers = allPlayers.slice(0, totalPlayers);
bots = {};
playerNames = {};
playerNames['P1'] = 'انت';
for (var i = 1; i < totalPlayers; i++) {
bots[allPlayers[i]] = difficulty;
playerNames[allPlayers[i]] = 'بوت ' + i;
}
} else {
if (playerCount < 2) playerCount = 2;
if (playerCount > 4) playerCount = 4;
activePlayers = allPlayers.slice(0, playerCount);
bots = {};
playerNames = {};
activePlayers.forEach(function(p, idx) {
playerNames[p] = LudoConstants.PLAYER_LABELS[p];
});
}
document.addEventListener('DOMContentLoaded', function() {
LudoUI.renderBoard('#ludo-board');
var isMobile = window.innerWidth < 768;
var diceContainer = isMobile ? '#ludo-dice-container-mobile' : '#ludo-dice-container';
var turnEl = isMobile ? document.getElementById('ludo-turn-mobile') : document.getElementById('ludo-turn');
LudoUI.renderDice(diceContainer);
LudoUI.setTurnElement(turnEl);
LudoUI.setLogElement(document.getElementById('ludo-log'));
LudoUI.renderPlayerCards(
document.getElementById('ludo-top-players'),
document.getElementById('ludo-bottom-players'),
activePlayers,
playerNames
);
LudoGame.init({
players: activePlayers,
mode: mode,
bots: bots,
difficulty: difficulty,
playerNames: playerNames,
onGameEnd: function(winners) {
var playAgainBtn = document.getElementById('ludo-play-again');
if (playAgainBtn) {
playAgainBtn.addEventListener('click', function() {
LudoGame.restart({
players: activePlayers,
mode: mode,
bots: bots,
difficulty: difficulty,
playerNames: playerNames,
onGameEnd: arguments.callee
});
});
}
}
});
});
})();
</script>
<?php require __DIR__ . '/../includes/footer.php'; ?>
<?php
$pageTitle = 'EL3AB - لودو اونلاين';
$extraCss = ['/public/css/ludo.css'];
$extraJs = ['/public/js/ludo-constants.js', '/public/js/ludo-ui.js', '/public/js/ludo-bot.js', '/public/js/ludo-chat.js', '/public/js/ludo-live.js'];
?>
<?php require __DIR__ . '/../includes/header.php'; ?>
<div class="ludo-layout" id="ludo-page" style="display:none;">
<div class="ludo-board-column">
<div class="ludo-players-row" id="ludo-top-players"></div>
<div class="ludo-board-wrapper">
<div id="ludo-board"></div>
</div>
<div class="ludo-players-row" id="ludo-bottom-players"></div>
<div class="ludo-mobile-panel">
<div class="ludo-turn-indicator" id="ludo-turn-mobile"></div>
<div id="ludo-dice-container-mobile"></div>
</div>
</div>
<div class="ludo-side-panel">
<div class="ludo-turn-indicator" id="ludo-turn"></div>
<div id="ludo-dice-container"></div>
<div class="ludo-log" id="ludo-log"></div>
<div id="ludo-chat"></div>
</div>
</div>
<!-- Waiting Room -->
<div id="ludo-waiting" class="space-y-6" style="padding:24px;text-align:center;">
<h2 style="font-size:22px;font-weight:700;">غرفة لودو</h2>
<div class="card" id="waiting-card">
<div class="card-body space-y-4">
<div id="room-code-display" style="display:none;">
<p class="text-muted text-sm">كود الغرفة</p>
<p id="room-code-value" style="font-size:32px;font-weight:700;letter-spacing:6px;color:var(--gold);"></p>
<button class="btn btn-ghost btn-sm" onclick="copyRoomCode()">نسخ الكود</button>
</div>
<div id="waiting-players" class="space-y-2"></div>
<div id="waiting-status">
<div class="spinner" style="margin:0 auto;"></div>
<p class="text-muted text-sm" style="margin-top:8px;">في انتظار اللاعبين...</p>
</div>
<button class="btn btn-gold btn-block" id="start-game-btn" style="display:none;" onclick="startGame()">ابدأ اللعبة</button>
<button class="btn btn-ghost btn-block" onclick="leaveRoom()">مغادرة</button>
</div>
</div>
</div>
<script>
(function() {
var params = new URLSearchParams(window.location.search);
var action = params.get('action');
var code = params.get('code');
var matchId = params.get('id');
var session = App.getSession ? App.getSession() : null;
if (!session || !session.access_token) {
window.location.href = '/login';
return;
}
var token = session.access_token;
var userId = session.user ? session.user.id : null;
if (matchId) {
startLiveGame(matchId);
return;
}
if (action === 'create') {
LudoLive.createRoom(4, [], function(data) {
if (data.ok) {
showWaitingRoom(data.match, data.room_code);
initLiveConnection(data.match.id);
} else {
App.toast(data.error || 'خطأ', 'error');
}
});
} else if (action === 'join' && code) {
LudoLive.joinRoom(code, function(data) {
if (data.ok) {
showWaitingRoom(data.match, data.match.room_code);
initLiveConnection(data.match.id);
} else {
App.toast(data.error === 'room_not_found' ? 'الغرفة غير موجودة' : (data.error || 'خطأ'), 'error');
setTimeout(function() { window.location.href = '/ludo'; }, 1500);
}
});
}
// Expose token/userId for LudoLive
window.__ludoToken = token;
window.__ludoUserId = userId;
function initLiveConnection(id) {
LudoLive.init({
matchId: id,
userId: userId,
token: token
});
}
function showWaitingRoom(match, roomCode) {
var codeDisplay = document.getElementById('room-code-display');
var codeValue = document.getElementById('room-code-value');
codeDisplay.style.display = 'block';
codeValue.textContent = roomCode || match.room_code || '';
updateWaitingPlayers(match);
if (match.host_id === userId) {
document.getElementById('start-game-btn').style.display = 'block';
}
// Watch for status change
var checkInterval = setInterval(function() {
var st = LudoLive.getState();
if (st.match) {
var m = st.match;
var status = m.status;
updateWaitingPlayers(m);
if (status === 'in_progress') {
clearInterval(checkInterval);
startLiveGame(m.id);
}
}
}, 1000);
}
function updateWaitingPlayers(match) {
var players = typeof match.players === 'string' ? JSON.parse(match.players) : match.players;
var container = document.getElementById('waiting-players');
container.innerHTML = '';
players.forEach(function(p) {
var el = document.createElement('div');
el.style.cssText = 'display:flex;align-items:center;gap:8px;padding:8px;border-radius:8px;background:var(--bg-3);';
el.innerHTML = '<div style="width:12px;height:12px;border-radius:50%;background:var(--ludo-' + p.color.toLowerCase() + ');"></div>' +
'<span>' + p.name + '</span>' +
'<span class="text-muted text-sm" style="margin-right:auto;">' + (p.type === 'bot' ? 'بوت' : '') + '</span>';
container.appendChild(el);
});
}
window.startGame = function() {
LudoLive.startGame(function(data) {
if (data.ok) {
startLiveGame(data.match.id);
} else {
App.toast(data.error || 'خطأ', 'error');
}
});
};
window.startLiveGame = startLiveGame;
function startLiveGame(id) {
document.getElementById('ludo-waiting').style.display = 'none';
document.getElementById('ludo-page').style.display = '';
if (!LudoLive.getState().matchId) {
LudoLive.init({
matchId: id,
userId: userId,
token: token
});
}
LudoLive.bindUI();
LudoLive.fetchState();
}
window.leaveRoom = function() {
LudoLive.leave();
window.location.href = '/ludo';
};
window.copyRoomCode = function() {
var code = document.getElementById('room-code-value').textContent;
if (navigator.clipboard) {
navigator.clipboard.writeText(code);
App.toast('تم نسخ الكود', 'success');
}
};
})();
</script>
<?php require __DIR__ . '/../includes/footer.php'; ?>
<?php
$pageTitle = 'EL3AB - البحث عن خصم';
$extraCss = ['/public/css/ludo.css'];
?>
<?php require __DIR__ . '/../includes/header.php'; ?>
<div class="space-y-6" style="padding:24px;text-align:center;">
<h2 style="font-size:22px;font-weight:700;">البحث عن خصم - لودو</h2>
<div class="card">
<div class="card-body space-y-4" style="padding:32px;">
<div id="mm-searching">
<div style="width:80px;height:80px;margin:0 auto;border-radius:50%;background:linear-gradient(135deg, var(--ludo-p1), var(--ludo-p3));display:flex;align-items:center;justify-content:center;">
<svg class="icon-lg" style="color:var(--text-1);animation:spin 2s linear infinite;"><use href="/public/icons/sprite.svg#icon-search"></use></svg>
</div>
<p style="font-size:18px;font-weight:600;margin-top:16px;">جاري البحث...</p>
<p class="text-muted text-sm">نبحث عن لاعب بمستواك</p>
<div id="mm-timer" style="font-size:24px;font-weight:700;color:var(--gold);margin-top:12px;">0:00</div>
</div>
<button class="btn btn-ghost btn-block" onclick="cancelSearch()">الغاء</button>
</div>
</div>
</div>
<style>
@keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
</style>
<script>
(function() {
var session = App.getSession ? App.getSession() : null;
if (!session || !session.access_token) {
window.location.href = '/login';
return;
}
var token = session.access_token;
var timerEl = document.getElementById('mm-timer');
var startTime = Date.now();
var pollInterval = null;
var timerInterval = null;
var cancelled = false;
function updateTimer() {
var elapsed = Math.floor((Date.now() - startTime) / 1000);
var mins = Math.floor(elapsed / 60);
var secs = elapsed % 60;
timerEl.textContent = mins + ':' + (secs < 10 ? '0' : '') + secs;
}
function poll() {
if (cancelled) return;
var xhr = new XMLHttpRequest();
xhr.open('POST', '/api/ludo', true);
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.setRequestHeader('Authorization', 'Bearer ' + token);
xhr.onload = function() {
try {
var data = JSON.parse(xhr.responseText);
if (data.ok && data.status === 'matched' && data.match_id) {
clearInterval(pollInterval);
clearInterval(timerInterval);
window.location.href = '/ludo-live?id=' + data.match_id;
}
} catch(e) {}
};
xhr.send(JSON.stringify({ action: 'matchmake', sub_action: 'join' }));
}
timerInterval = setInterval(updateTimer, 1000);
poll();
pollInterval = setInterval(poll, 3000);
window.cancelSearch = function() {
cancelled = true;
clearInterval(pollInterval);
clearInterval(timerInterval);
var xhr = new XMLHttpRequest();
xhr.open('POST', '/api/ludo', true);
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.setRequestHeader('Authorization', 'Bearer ' + token);
xhr.onload = function() { window.location.href = '/ludo'; };
xhr.send(JSON.stringify({ action: 'matchmake', sub_action: 'leave' }));
};
})();
</script>
<?php require __DIR__ . '/../includes/footer.php'; ?>
<?php $pageTitle = 'EL3AB - لودو'; ?>
<?php require __DIR__ . '/../includes/header.php'; ?>
<div class="space-y-6">
<div class="text-center">
<h2 style="font-size:22px;font-weight:700;">لودو</h2>
<p class="text-muted text-sm">اختر نوع اللعب</p>
</div>
<div class="space-y-3">
<!-- Local (Pass & Play) -->
<div class="card card-hover" style="cursor:pointer;" onclick="startLocal()">
<div class="card-body" style="display:flex;align-items:center;gap:16px;">
<div class="avatar" style="background:linear-gradient(135deg, var(--ludo-p1), var(--ludo-p3));">
<svg class="icon-lg" style="color:var(--text-1)"><use href="/public/icons/sprite.svg#icon-users"></use></svg>
</div>
<div style="flex:1;">
<p style="font-size:16px;font-weight:600;">لعب محلي</p>
<p class="text-muted text-sm">2-4 لاعبين على نفس الجهاز</p>
</div>
<svg class="icon" style="color:var(--text-3);transform:scaleX(-1)"><use href="/public/icons/sprite.svg#icon-arrow-right"></use></svg>
</div>
</div>
<!-- VS Bot -->
<div class="card">
<div class="card-body space-y-3">
<div style="display:flex;align-items:center;gap:12px;">
<div class="avatar" style="background:var(--bg-3);">
<svg class="icon-lg" style="color:var(--cyan)"><use href="/public/icons/sprite.svg#icon-bot"></use></svg>
</div>
<div>
<p style="font-size:16px;font-weight:600;">ضد البوت</p>
<p class="text-muted text-sm">العب ضد 1-3 بوتات</p>
</div>
</div>
<div>
<label class="input-label">عدد البوتات</label>
<div class="tab-group" id="bot-count-tabs">
<button class="tab active" data-count="1">1</button>
<button class="tab" data-count="2">2</button>
<button class="tab" data-count="3">3</button>
</div>
</div>
<div>
<label class="input-label">الصعوبة</label>
<div class="tab-group" id="bot-diff-tabs">
<button class="tab active" data-diff="easy">سهل</button>
<button class="tab" data-diff="hard">صعب</button>
</div>
</div>
<button class="btn btn-gold btn-block" onclick="startBot()">
<svg class="icon"><use href="/public/icons/sprite.svg#icon-play"></use></svg>
ابدأ اللعب
</button>
</div>
</div>
<!-- Multiplayer -->
<div class="card" style="border-color:var(--gold);border-width:2px;">
<div class="card-body space-y-3">
<div style="display:flex;align-items:center;gap:12px;">
<div class="avatar" style="background:linear-gradient(135deg, var(--gold), var(--cyan));">
<svg class="icon-lg" style="color:var(--text-1)"><use href="/public/icons/sprite.svg#icon-sword"></use></svg>
</div>
<div>
<p style="font-size:18px;font-weight:700;">ضد لاعب حقيقي</p>
<p class="text-muted text-sm">العب اونلاين ضد لاعبين حقيقيين</p>
</div>
</div>
<button class="btn btn-gold btn-block btn-lg" onclick="startMatchmaking()">
<svg class="icon"><use href="/public/icons/sprite.svg#icon-search"></use></svg>
ابحث عن خصم
</button>
</div>
</div>
<!-- Private Room -->
<div class="card">
<div class="card-body space-y-3">
<div style="display:flex;align-items:center;gap:12px;">
<div class="avatar" style="background:var(--bg-3);">
<svg class="icon-lg" style="color:var(--ludo-p4)"><use href="/public/icons/sprite.svg#icon-lock"></use></svg>
</div>
<div>
<p style="font-size:16px;font-weight:600;">غرفة خاصة</p>
<p class="text-muted text-sm">انشئ غرفة وادعو اصحابك</p>
</div>
</div>
<div style="display:flex;gap:8px;">
<button class="btn btn-gold" style="flex:1;" onclick="createRoom()">انشئ غرفة</button>
<button class="btn btn-ghost" style="flex:1;" onclick="showJoinRoom()">انضم بكود</button>
</div>
<div id="join-room-form" style="display:none;">
<div style="display:flex;gap:8px;">
<input type="text" class="input" id="room-code-input" placeholder="ادخل كود الغرفة" maxlength="6" style="text-transform:uppercase;letter-spacing:4px;text-align:center;">
<button class="btn btn-gold" onclick="joinRoom()">دخول</button>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
document.querySelectorAll('.tab-group').forEach(function(group) {
group.querySelectorAll('.tab').forEach(function(tab) {
tab.addEventListener('click', function() {
group.querySelectorAll('.tab').forEach(function(t) { t.classList.remove('active'); });
tab.classList.add('active');
});
});
});
});
function startLocal() {
window.location.href = '/ludo-game?mode=local&players=4';
}
function startBot() {
var count = document.querySelector('#bot-count-tabs .tab.active').dataset.count;
var diff = document.querySelector('#bot-diff-tabs .tab.active').dataset.diff;
window.location.href = '/ludo-game?mode=bot&bots=' + count + '&difficulty=' + diff;
}
function startMatchmaking() {
if (!App.isLoggedIn()) { window.location.href = '/login'; return; }
window.location.href = '/ludo-matchmaking';
}
function createRoom() {
if (!App.isLoggedIn()) { window.location.href = '/login'; return; }
window.location.href = '/ludo-live?action=create';
}
function showJoinRoom() {
var form = document.getElementById('join-room-form');
form.style.display = form.style.display === 'none' ? 'block' : 'none';
if (form.style.display === 'block') {
document.getElementById('room-code-input').focus();
}
}
function joinRoom() {
if (!App.isLoggedIn()) { window.location.href = '/login'; return; }
var code = document.getElementById('room-code-input').value.trim().toUpperCase();
if (!code || code.length < 4) {
App.toast('ادخل كود صحيح', 'error');
return;
}
window.location.href = '/ludo-live?action=join&code=' + code;
}
</script>
<?php require __DIR__ . '/../includes/footer.php'; ?>
...@@ -172,7 +172,7 @@ $extraCss = '/public/css/chessboard.css'; ...@@ -172,7 +172,7 @@ $extraCss = '/public/css/chessboard.css';
.mm-found-icon { .mm-found-icon {
width: 40px; width: 40px;
height: 40px; height: 40px;
color: #fff; color: var(--text-1);
} }
</style> </style>
......
...@@ -44,7 +44,7 @@ ...@@ -44,7 +44,7 @@
</div> </div>
<!-- Create Modal (hidden) --> <!-- Create Modal (hidden) -->
<div id="create-org-modal" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,0.8);z-index:100;display:none;align-items:center;justify-content:center;padding:20px;"> <div id="create-org-modal" style="display:none;position:fixed;inset:0;background:var(--overlay-dark);z-index:100;display:none;align-items:center;justify-content:center;padding:20px;">
<div class="card" style="max-width:400px;width:100%;"> <div class="card" style="max-width:400px;width:100%;">
<div class="card-body space-y-4" style="padding:24px;"> <div class="card-body space-y-4" style="padding:24px;">
<p style="font-size:18px;font-weight:700;text-align:center;">انشاء منظمة</p> <p style="font-size:18px;font-weight:700;text-align:center;">انشاء منظمة</p>
......
...@@ -16,7 +16,7 @@ ...@@ -16,7 +16,7 @@
<div class="card-body space-y-3"> <div class="card-body space-y-3">
<div style="display:flex;align-items:center;gap:12px;"> <div style="display:flex;align-items:center;gap:12px;">
<div class="avatar" style="background:linear-gradient(135deg, var(--gold), var(--cyan));"> <div class="avatar" style="background:linear-gradient(135deg, var(--gold), var(--cyan));">
<svg class="icon-lg" style="color:#fff"><use href="/public/icons/sprite.svg#icon-sword"></use></svg> <svg class="icon-lg" style="color:var(--text-1)"><use href="/public/icons/sprite.svg#icon-sword"></use></svg>
</div> </div>
<div> <div>
<p style="font-size:18px;font-weight:700;">ضد لاعب حقيقي</p> <p style="font-size:18px;font-weight:700;">ضد لاعب حقيقي</p>
......
...@@ -41,7 +41,7 @@ ...@@ -41,7 +41,7 @@
عندك حساب؟ <a href="/login" style="color:var(--cyan);">سجل دخول</a> عندك حساب؟ <a href="/login" style="color:var(--cyan);">سجل دخول</a>
</p> </p>
<div id="reg-error" style="display:none;margin-top:16px;padding:12px;background:rgba(239,68,68,0.1);border:1px solid rgba(239,68,68,0.2);border-radius:var(--radius-md);color:var(--error);font-size:13px;text-align:center;"></div> <div id="reg-error" style="display:none;margin-top:16px;padding:12px;background:var(--overlay-error-bg);border:1px solid var(--overlay-error-border);border-radius:var(--radius-md);color:var(--error);font-size:13px;text-align:center;"></div>
</div> </div>
</div> </div>
......
...@@ -56,6 +56,84 @@ ...@@ -56,6 +56,84 @@
/* Transitions */ /* Transitions */
--ease: cubic-bezier(0.4, 0, 0.2, 1); --ease: cubic-bezier(0.4, 0, 0.2, 1);
/* Board Theme */
--board-light: #E8EDF9;
--board-dark: #7195D1;
--board-selected: rgba(21, 215, 255, 0.45);
--board-legal: rgba(0, 0, 0, 0.18);
--board-last-move: rgba(255, 199, 40, 0.35);
--board-check: rgba(239, 68, 68, 0.55);
--board-premove: rgba(21, 180, 240, 0.3);
--board-highlight-green: rgba(21, 180, 90, 0.4);
--board-highlight-red: rgba(220, 50, 50, 0.4);
--board-highlight-yellow: rgba(220, 180, 30, 0.4);
/* Eval Bar */
--eval-bg: #1a1a2e;
--eval-white: #f0f0f0;
--eval-label-light: #fff;
--eval-label-dark: #333;
/* Move Classifications */
--move-brilliant: #26c6da;
--move-great: #66bb6a;
--move-good: #81c784;
--move-book: #9e9e9e;
--move-inaccuracy: #fdd835;
--move-mistake: #ef6c00;
--move-blunder: #e53935;
/* Analysis Move Text (slightly different palette) */
--move-text-brilliant: #00bcd4;
--move-text-great: #2196f3;
--move-text-good: #4caf50;
--move-text-inaccuracy: #ff9800;
--move-text-mistake: #f44336;
--move-text-blunder: #d32f2f;
/* Arrows */
--arrow-green: rgba(21, 180, 90, 0.7);
--arrow-red: rgba(220, 50, 50, 0.7);
--arrow-yellow: rgba(220, 180, 30, 0.7);
/* Graph / Canvas */
--graph-bg: #0a1628;
--graph-grid: #333;
--graph-accent: #15d7ff;
--graph-error: #f44336;
/* Overlays */
--overlay-dark: rgba(0, 0, 0, 0.8);
--overlay-result: rgba(5, 13, 23, 0.92);
--overlay-error-bg: rgba(239, 68, 68, 0.1);
--overlay-error-border: rgba(239, 68, 68, 0.2);
/* Bot Avatars */
--bot-amina: #4ade80, #22c55e;
--bot-tarek: #38bdf8, #0ea5e9;
--bot-nour: #a78bfa, #7c3aed;
--bot-omar: #fb923c, #ea580c;
--bot-layla: #f472b6, #db2777;
--bot-ziad: #f87171, #dc2626;
--bot-gm: var(--gold), #b45309;
/* Ludo */
--ludo-p1: #E53935;
--ludo-p2: #43A047;
--ludo-p3: #FDD835;
--ludo-p4: #1E88E5;
--ludo-board-bg: #1a2332;
--ludo-path: #f5f5f5;
--ludo-path-border: rgba(0,0,0,0.1);
--ludo-safe: #FFD54F;
--ludo-home-p1: rgba(229,57,53,0.3);
--ludo-home-p2: rgba(67,160,71,0.3);
--ludo-home-p3: rgba(253,216,53,0.3);
--ludo-home-p4: rgba(30,136,229,0.3);
--ludo-dice-bg: var(--bg-2);
--ludo-dice-dot: var(--text-inverse);
--ludo-chat-bg: var(--bg-2);
} }
/* Reset */ /* Reset */
...@@ -206,7 +284,7 @@ img { ...@@ -206,7 +284,7 @@ img {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
color: #fff; color: var(--eval-label-light);
} }
/* Main content */ /* Main content */
...@@ -566,7 +644,7 @@ img { ...@@ -566,7 +644,7 @@ img {
width: 20px; width: 20px;
height: 20px; height: 20px;
border-radius: var(--radius-full); border-radius: var(--radius-full);
background: #fff; background: var(--eval-label-light);
transition: right 0.2s var(--ease); transition: right 0.2s var(--ease);
} }
......
...@@ -69,7 +69,7 @@ ...@@ -69,7 +69,7 @@
.game-clock.low { .game-clock.low {
background: var(--error); background: var(--error);
color: #fff; color: var(--eval-label-light);
animation: clock-pulse 1s ease infinite; animation: clock-pulse 1s ease infinite;
} }
...@@ -137,17 +137,17 @@ ...@@ -137,17 +137,17 @@
transition: background 0.1s; transition: background 0.1s;
} }
.square-light { background: #E8EDF9; } .square-light { background: var(--board-light); }
.square-dark { background: #7195D1; } .square-dark { background: var(--board-dark); }
.square.selected { background: rgba(21, 215, 255, 0.45) !important; } .square.selected { background: var(--board-selected) !important; }
.square.legal-move::after { .square.legal-move::after {
content: ''; content: '';
position: absolute; position: absolute;
width: 30%; width: 30%;
height: 30%; height: 30%;
border-radius: 50%; border-radius: 50%;
background: rgba(0, 0, 0, 0.18); background: var(--board-legal);
} }
.square.legal-capture::after { .square.legal-capture::after {
content: ''; content: '';
...@@ -155,16 +155,16 @@ ...@@ -155,16 +155,16 @@
width: 85%; width: 85%;
height: 85%; height: 85%;
border-radius: 50%; border-radius: 50%;
border: 3.5px solid rgba(0, 0, 0, 0.18); border: 3.5px solid var(--board-legal);
background: transparent; background: transparent;
} }
.square.last-move { background: rgba(255, 199, 40, 0.35) !important; } .square.last-move { background: var(--board-last-move) !important; }
.square.in-check { background: rgba(239, 68, 68, 0.55) !important; box-shadow: inset 0 0 8px rgba(239,68,68,0.6); } .square.in-check { background: var(--board-check) !important; box-shadow: inset 0 0 8px var(--board-check); }
.square.premove-from { background: rgba(21, 180, 240, 0.3) !important; } .square.premove-from { background: var(--board-premove) !important; }
.square.premove-to { background: rgba(21, 180, 240, 0.3) !important; } .square.premove-to { background: var(--board-premove) !important; }
.square.highlight-green { background: rgba(21, 180, 90, 0.4) !important; } .square.highlight-green { background: var(--board-highlight-green) !important; }
.square.highlight-red { background: rgba(220, 50, 50, 0.4) !important; } .square.highlight-red { background: var(--board-highlight-red) !important; }
.square.highlight-yellow { background: rgba(220, 180, 30, 0.4) !important; } .square.highlight-yellow { background: var(--board-highlight-yellow) !important; }
/* Pieces */ /* Pieces */
.piece { .piece {
...@@ -245,7 +245,7 @@ ...@@ -245,7 +245,7 @@
.eval-bar { .eval-bar {
width: 18px; width: 18px;
min-width: 18px; min-width: 18px;
background: #1a1a2e; background: var(--eval-bg);
border-radius: var(--radius-sm); border-radius: var(--radius-sm);
overflow: hidden; overflow: hidden;
position: relative; position: relative;
...@@ -258,7 +258,7 @@ ...@@ -258,7 +258,7 @@
bottom: 0; bottom: 0;
left: 0; left: 0;
right: 0; right: 0;
background: #fff; background: var(--eval-white);
transition: height 0.5s ease; transition: height 0.5s ease;
height: 50%; height: 50%;
} }
...@@ -275,8 +275,8 @@ ...@@ -275,8 +275,8 @@
z-index: 1; z-index: 1;
} }
.eval-bar-label-top { top: 3px; color: #fff; } .eval-bar-label-top { top: 3px; color: var(--eval-label-light); }
.eval-bar-label-bottom { bottom: 3px; color: #333; } .eval-bar-label-bottom { bottom: 3px; color: var(--eval-label-dark); }
/* ============================================ /* ============================================
SIDE PANEL SIDE PANEL
...@@ -486,7 +486,7 @@ ...@@ -486,7 +486,7 @@
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
background: rgba(5, 13, 23, 0.92); background: var(--overlay-result);
z-index: 10; z-index: 10;
border-radius: var(--radius-sm); border-radius: var(--radius-sm);
gap: 12px; gap: 12px;
...@@ -496,7 +496,7 @@ ...@@ -496,7 +496,7 @@
.game-result-title { .game-result-title {
font-size: 22px; font-size: 22px;
font-weight: 700; font-weight: 700;
color: #fff; color: var(--text-1);
} }
.game-result-subtitle { .game-result-subtitle {
...@@ -523,7 +523,7 @@ ...@@ -523,7 +523,7 @@
.game-result-stats .stat-row span:last-child { .game-result-stats .stat-row span:last-child {
font-weight: 600; font-weight: 600;
color: #fff; color: var(--text-1);
} }
/* ============================================ /* ============================================
...@@ -580,12 +580,12 @@ ...@@ -580,12 +580,12 @@
flex-shrink: 0; flex-shrink: 0;
} }
.move-brilliant { background: #26c6da; color: #fff; } .move-brilliant { background: var(--move-brilliant); color: var(--eval-label-light); }
.move-great { background: #66bb6a; color: #fff; } .move-great { background: var(--move-great); color: var(--eval-label-light); }
.move-good { background: #81c784; color: #fff; } .move-good { background: var(--move-good); color: var(--eval-label-light); }
.move-inaccuracy { background: #fdd835; color: #333; } .move-inaccuracy { background: var(--move-inaccuracy); color: var(--eval-label-dark); }
.move-mistake { background: #ef6c00; color: #fff; } .move-mistake { background: var(--move-mistake); color: var(--eval-label-light); }
.move-blunder { background: #e53935; color: #fff; } .move-blunder { background: var(--move-blunder); color: var(--eval-label-light); }
/* ============================================ /* ============================================
ANALYSIS PAGE ANALYSIS PAGE
......
This diff is collapsed.
...@@ -159,4 +159,33 @@ ...@@ -159,4 +159,33 @@
<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"/> <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> </symbol>
<symbol id="icon-ludo" viewBox="0 0 24 24">
<rect x="3" y="3" width="18" height="18" rx="2"/>
<circle cx="8" cy="8" r="2"/>
<circle cx="16" cy="8" r="2"/>
<circle cx="8" cy="16" r="2"/>
<circle cx="16" cy="16" r="2"/>
<path d="M12 3v18M3 12h18"/>
</symbol>
<symbol id="icon-dice" viewBox="0 0 24 24">
<rect x="3" y="3" width="18" height="18" rx="3"/>
<circle cx="8" cy="8" r="1.5" fill="currentColor" stroke="none"/>
<circle cx="16" cy="8" r="1.5" fill="currentColor" stroke="none"/>
<circle cx="8" cy="16" r="1.5" fill="currentColor" stroke="none"/>
<circle cx="16" cy="16" r="1.5" fill="currentColor" stroke="none"/>
<circle cx="12" cy="12" r="1.5" fill="currentColor" stroke="none"/>
</symbol>
<symbol id="icon-users" viewBox="0 0 24 24">
<path d="M17 21v-2a4 4 0 00-4-4H5a4 4 0 00-4 4v2"/>
<circle cx="9" cy="7" r="4"/>
<path d="M23 21v-2a4 4 0 00-3-3.87M16 3.13a4 4 0 010 7.75"/>
</symbol>
<symbol id="icon-lock" viewBox="0 0 24 24">
<rect x="3" y="11" width="18" height="11" rx="2"/>
<path d="M7 11V7a5 5 0 0110 0v4"/>
</symbol>
</svg> </svg>
...@@ -431,12 +431,14 @@ const Analysis = { ...@@ -431,12 +431,14 @@ const Analysis = {
const midY = H / 2; const midY = H / 2;
const maxEval = 5; const maxEval = 5;
const style = getComputedStyle(document.documentElement);
// Background // Background
ctx.fillStyle = '#0a1628'; ctx.fillStyle = style.getPropertyValue('--graph-bg').trim();
ctx.fillRect(0, 0, W, H); ctx.fillRect(0, 0, W, H);
// Center line (0 eval) // Center line (0 eval)
ctx.strokeStyle = '#333'; ctx.strokeStyle = style.getPropertyValue('--graph-grid').trim();
ctx.lineWidth = 1; ctx.lineWidth = 1;
ctx.beginPath(); ctx.beginPath();
ctx.moveTo(0, midY); ctx.moveTo(0, midY);
...@@ -473,7 +475,7 @@ const Analysis = { ...@@ -473,7 +475,7 @@ const Analysis = {
if (i === 0) ctx.moveTo(x, y); if (i === 0) ctx.moveTo(x, y);
else ctx.lineTo(x, y); else ctx.lineTo(x, y);
} }
ctx.strokeStyle = '#15d7ff'; ctx.strokeStyle = style.getPropertyValue('--graph-accent').trim();
ctx.lineWidth = 2; ctx.lineWidth = 2;
ctx.stroke(); ctx.stroke();
...@@ -485,7 +487,7 @@ const Analysis = { ...@@ -485,7 +487,7 @@ const Analysis = {
const y = midY - (clamped / maxEval) * (midY - 10); const y = midY - (clamped / maxEval) * (midY - 10);
ctx.beginPath(); ctx.beginPath();
ctx.arc(x, y, 4, 0, Math.PI * 2); ctx.arc(x, y, 4, 0, Math.PI * 2);
ctx.fillStyle = '#f44336'; ctx.fillStyle = style.getPropertyValue('--graph-error').trim();
ctx.fill(); ctx.fill();
} }
} }
...@@ -512,7 +514,8 @@ const Analysis = { ...@@ -512,7 +514,8 @@ const Analysis = {
const H = canvas.height; const H = canvas.height;
ctx.clearRect(0, 0, W, H); ctx.clearRect(0, 0, W, H);
ctx.fillStyle = '#0a1628'; const style2 = getComputedStyle(document.documentElement);
ctx.fillStyle = style2.getPropertyValue('--graph-bg').trim();
ctx.fillRect(0, 0, W, H); ctx.fillRect(0, 0, W, H);
// If no move time data available, show message // If no move time data available, show message
......
...@@ -156,6 +156,7 @@ const Board = { ...@@ -156,6 +156,7 @@ const Board = {
updatePieces() { updatePieces() {
if (!this.position) return; if (!this.position) return;
const board = this.position.board(); const board = this.position.board();
const assets = window.__themeAssets || {};
this.squares.forEach(sq => { this.squares.forEach(sq => {
const sqName = sq.dataset.square; const sqName = sq.dataset.square;
...@@ -163,7 +164,7 @@ const Board = { ...@@ -163,7 +164,7 @@ const Board = {
const rank = 8 - parseInt(sqName[1]); const rank = 8 - parseInt(sqName[1]);
const piece = board[rank][file]; const piece = board[rank][file];
let pieceEl = sq.querySelector('.piece'); let pieceEl = sq.querySelector('.piece:not(.piece-ghost)');
if (piece) { if (piece) {
const key = piece.color + piece.type.toUpperCase(); const key = piece.color + piece.type.toUpperCase();
if (!pieceEl) { if (!pieceEl) {
...@@ -171,7 +172,21 @@ const Board = { ...@@ -171,7 +172,21 @@ const Board = {
pieceEl.className = 'piece'; pieceEl.className = 'piece';
sq.appendChild(pieceEl); sq.appendChild(pieceEl);
} }
const customSrc = assets['piece-' + key];
if (customSrc) {
pieceEl.textContent = '';
let img = pieceEl.querySelector('img');
if (!img) {
img = document.createElement('img');
img.style.cssText = 'width:100%;height:100%;object-fit:contain;pointer-events:none;';
pieceEl.appendChild(img);
}
img.src = customSrc;
} else {
const img = pieceEl.querySelector('img');
if (img) img.remove();
pieceEl.textContent = this.PIECES[key]; pieceEl.textContent = this.PIECES[key];
}
pieceEl.dataset.piece = key; pieceEl.dataset.piece = key;
pieceEl.dataset.color = piece.color; pieceEl.dataset.color = piece.color;
} else { } else {
...@@ -270,7 +285,13 @@ const Board = { ...@@ -270,7 +285,13 @@ const Board = {
const key = piece.color + piece.type.toUpperCase(); const key = piece.color + piece.type.toUpperCase();
const ghost = document.createElement('div'); const ghost = document.createElement('div');
ghost.className = 'piece piece-ghost'; ghost.className = 'piece piece-ghost';
const assets = window.__themeAssets || {};
const customSrc = assets['piece-' + key];
if (customSrc) {
ghost.innerHTML = '<img src="' + customSrc + '" style="width:100%;height:100%;object-fit:contain;pointer-events:none;">';
} else {
ghost.textContent = this.PIECES[key]; ghost.textContent = this.PIECES[key];
}
ghost.style.opacity = '0.4'; ghost.style.opacity = '0.4';
ghost.style.pointerEvents = 'none'; ghost.style.pointerEvents = 'none';
toSq.appendChild(ghost); toSq.appendChild(ghost);
...@@ -572,10 +593,11 @@ const Board = { ...@@ -572,10 +593,11 @@ const Board = {
const h = this.arrowCanvas.height; const h = this.arrowCanvas.height;
ctx.clearRect(0, 0, w, h); ctx.clearRect(0, 0, w, h);
const style = getComputedStyle(document.documentElement);
const colorMap = { const colorMap = {
green: 'rgba(21, 180, 90, 0.7)', green: style.getPropertyValue('--arrow-green').trim() || 'rgba(21, 180, 90, 0.7)',
red: 'rgba(220, 50, 50, 0.7)', red: style.getPropertyValue('--arrow-red').trim() || 'rgba(220, 50, 50, 0.7)',
yellow: 'rgba(220, 180, 30, 0.7)' yellow: style.getPropertyValue('--arrow-yellow').trim() || 'rgba(220, 180, 30, 0.7)'
}; };
const allArrows = [...this.arrows]; const allArrows = [...this.arrows];
...@@ -695,9 +717,9 @@ const Board = { ...@@ -695,9 +717,9 @@ const Board = {
showPositionAt(index) { showPositionAt(index) {
const fen = this.positionHistory[index]; const fen = this.positionHistory[index];
if (!fen) return; if (!fen) return;
// Temporarily display this position without changing game state
const tempChess = new Chess(fen); const tempChess = new Chess(fen);
const board = tempChess.board(); const board = tempChess.board();
const assets = window.__themeAssets || {};
this.squares.forEach(sq => { this.squares.forEach(sq => {
const sqName = sq.dataset.square; const sqName = sq.dataset.square;
...@@ -713,7 +735,21 @@ const Board = { ...@@ -713,7 +735,21 @@ const Board = {
pieceEl.className = 'piece'; pieceEl.className = 'piece';
sq.appendChild(pieceEl); sq.appendChild(pieceEl);
} }
const customSrc = assets['piece-' + key];
if (customSrc) {
pieceEl.textContent = '';
let img = pieceEl.querySelector('img');
if (!img) {
img = document.createElement('img');
img.style.cssText = 'width:100%;height:100%;object-fit:contain;pointer-events:none;';
pieceEl.appendChild(img);
}
img.src = customSrc;
} else {
const img = pieceEl.querySelector('img');
if (img) img.remove();
pieceEl.textContent = this.PIECES[key]; pieceEl.textContent = this.PIECES[key];
}
pieceEl.dataset.piece = key; pieceEl.dataset.piece = key;
pieceEl.dataset.color = piece.color; pieceEl.dataset.color = piece.color;
} else { } else {
...@@ -721,7 +757,6 @@ const Board = { ...@@ -721,7 +757,6 @@ const Board = {
} }
}); });
// Disable interaction if not viewing latest position
this.enabled = (index === this.positionHistory.length - 1); this.enabled = (index === this.positionHistory.length - 1);
}, },
...@@ -757,10 +792,17 @@ const Board = { ...@@ -757,10 +792,17 @@ const Board = {
n: color === 'w' ? '♘' : '♞' n: color === 'w' ? '♘' : '♞'
}; };
const assets = window.__themeAssets || {};
pieces.forEach(p => { pieces.forEach(p => {
const btn = document.createElement('div'); const btn = document.createElement('div');
btn.className = 'promotion-piece'; btn.className = 'promotion-piece';
const key = color + p.toUpperCase();
const customSrc = assets['piece-' + key];
if (customSrc) {
btn.innerHTML = '<img src="' + customSrc + '" style="width:100%;height:100%;object-fit:contain;">';
} else {
btn.textContent = symbols[p]; btn.textContent = symbols[p];
}
btn.onclick = () => { btn.onclick = () => {
overlay.remove(); overlay.remove();
this.confirmMove({ from: move.from, to: move.to, promotion: p }); this.confirmMove({ from: move.from, to: move.to, promotion: p });
......
var LudoBot = (function() {
'use strict';
var C = LudoConstants;
function getEligiblePieces(player, diceValue, positions, activePlayers) {
var eligible = [];
var playerPositions = positions[player];
for (var i = 0; i < 4; i++) {
var pos = playerPositions[i];
var isInBase = C.BASE_POSITIONS[player].indexOf(pos) !== -1;
var isHome = pos === C.HOME_POSITIONS[player];
if (isHome) continue;
if (isInBase) {
if (diceValue === 6) eligible.push(i);
continue;
}
var newPos = calculateNewPosition(player, pos, diceValue);
if (newPos !== null) eligible.push(i);
}
return eligible;
}
function calculateNewPosition(player, currentPos, diceValue) {
var isInHomeEntrance = C.HOME_ENTRANCE[player].indexOf(currentPos) !== -1;
if (isInHomeEntrance) {
var homeIdx = C.HOME_ENTRANCE[player].indexOf(currentPos);
var stepsLeft = 5 - homeIdx;
if (diceValue > stepsLeft) return null;
if (diceValue === stepsLeft) return C.HOME_POSITIONS[player];
return C.HOME_ENTRANCE[player][homeIdx + diceValue];
}
var turningPoint = C.TURNING_POINTS[player];
var startPos = C.START_POSITIONS[player];
var distanceToTurn = (turningPoint - currentPos + 52) % 52;
if (distanceToTurn === 0 && diceValue <= 6) {
if (diceValue <= 5) return C.HOME_ENTRANCE[player][diceValue - 1];
return null;
}
if (diceValue > distanceToTurn && distanceToTurn > 0 && distanceToTurn < 6) {
var overflow = diceValue - distanceToTurn;
if (overflow <= 5) return C.HOME_ENTRANCE[player][overflow - 1];
return null;
}
var newPos = (currentPos + diceValue) % 52;
return newPos;
}
function choosePieceEasy(eligible) {
if (eligible.length === 0) return -1;
return eligible[Math.floor(Math.random() * eligible.length)];
}
function choosePieceHard(player, diceValue, positions, activePlayers, eligible) {
if (eligible.length === 0) return -1;
if (eligible.length === 1) return eligible[0];
var bestScore = -Infinity;
var bestPiece = eligible[0];
eligible.forEach(function(pieceIdx) {
var score = scorePieceMove(player, pieceIdx, diceValue, positions, activePlayers);
if (score > bestScore) {
bestScore = score;
bestPiece = pieceIdx;
}
});
return bestPiece;
}
function scorePieceMove(player, pieceIdx, diceValue, positions, activePlayers) {
var score = 0;
var currentPos = positions[player][pieceIdx];
var isInBase = C.BASE_POSITIONS[player].indexOf(currentPos) !== -1;
if (isInBase && diceValue === 6) {
score += 30;
var startPos = C.START_POSITIONS[player];
if (canKillAt(player, startPos, positions, activePlayers)) {
score += 50;
}
return score;
}
var newPos = calculateNewPosition(player, currentPos, diceValue);
if (newPos === null) return -Infinity;
if (newPos === C.HOME_POSITIONS[player]) {
score += 100;
return score;
}
var isEnteringHome = C.HOME_ENTRANCE[player].indexOf(newPos) !== -1;
if (isEnteringHome) {
score += 25;
}
if (newPos >= 0 && newPos <= 51) {
if (canKillAt(player, newPos, positions, activePlayers)) {
score += 50;
}
if (C.SAFE_POSITIONS.indexOf(newPos) !== -1) {
score += 20;
} else if (isInDanger(player, newPos, positions, activePlayers)) {
score -= 15;
}
if (isBlockingOpponentExit(player, newPos, positions, activePlayers)) {
score += 15;
}
}
var progress = getProgressDistance(player, currentPos);
var maxProgress = 0;
positions[player].forEach(function(pos, idx) {
if (idx === pieceIdx) return;
if (C.BASE_POSITIONS[player].indexOf(pos) !== -1) return;
if (pos === C.HOME_POSITIONS[player]) return;
var p = getProgressDistance(player, pos);
if (p > maxProgress) maxProgress = p;
});
if (progress < maxProgress) {
score += 10;
}
return score;
}
function canKillAt(player, position, positions, activePlayers) {
if (C.SAFE_POSITIONS.indexOf(position) !== -1) return false;
for (var i = 0; i < activePlayers.length; i++) {
var opponent = activePlayers[i];
if (opponent === player) continue;
for (var j = 0; j < 4; j++) {
if (positions[opponent][j] === position) return true;
}
}
return false;
}
function isInDanger(player, position, positions, activePlayers) {
for (var i = 0; i < activePlayers.length; i++) {
var opponent = activePlayers[i];
if (opponent === player) continue;
for (var j = 0; j < 4; j++) {
var oppPos = positions[opponent][j];
if (C.BASE_POSITIONS[opponent].indexOf(oppPos) !== -1) continue;
if (oppPos === C.HOME_POSITIONS[opponent]) continue;
if (C.HOME_ENTRANCE[opponent].indexOf(oppPos) !== -1) continue;
for (var dice = 1; dice <= 6; dice++) {
var oppNewPos = (oppPos + dice) % 52;
if (oppNewPos === position) return true;
}
}
}
return false;
}
function isBlockingOpponentExit(player, position, positions, activePlayers) {
for (var i = 0; i < activePlayers.length; i++) {
var opponent = activePlayers[i];
if (opponent === player) continue;
if (position === C.START_POSITIONS[opponent]) return true;
}
return false;
}
function getProgressDistance(player, position) {
if (C.BASE_POSITIONS[player].indexOf(position) !== -1) return 0;
if (position === C.HOME_POSITIONS[player]) return 57;
var homeIdx = C.HOME_ENTRANCE[player].indexOf(position);
if (homeIdx !== -1) return 52 + homeIdx;
var start = C.START_POSITIONS[player];
return (position - start + 52) % 52;
}
function takeTurn(player, diceValue, positions, activePlayers, difficulty) {
var eligible = getEligiblePieces(player, diceValue, positions, activePlayers);
if (eligible.length === 0) return -1;
if (difficulty === 'hard') {
return choosePieceHard(player, diceValue, positions, activePlayers, eligible);
}
return choosePieceEasy(eligible);
}
return {
takeTurn: takeTurn,
getEligiblePieces: getEligiblePieces,
calculateNewPosition: calculateNewPosition
};
})();
var LudoChat = (function() {
'use strict';
var C = LudoConstants;
var containerEl = null;
var messagesEl = null;
var inputEl = null;
var quickEl = null;
var matchId = null;
var lastSentAt = 0;
var COOLDOWN = 2000;
function init(container, matchIdVal, onSend) {
matchId = matchIdVal;
containerEl = typeof container === 'string' ? document.querySelector(container) : container;
if (!containerEl) return;
containerEl.innerHTML = '';
containerEl.className = 'ludo-chat';
messagesEl = document.createElement('div');
messagesEl.className = 'ludo-chat-messages';
quickEl = document.createElement('div');
quickEl.className = 'ludo-chat-quick';
C.QUICK_MESSAGES.forEach(function(msg) {
var btn = document.createElement('button');
btn.className = 'ludo-chat-quick-btn';
btn.textContent = msg;
btn.addEventListener('click', function() {
send(msg, onSend);
});
quickEl.appendChild(btn);
});
var inputRow = document.createElement('div');
inputRow.className = 'ludo-chat-input-row';
inputEl = document.createElement('input');
inputEl.type = 'text';
inputEl.className = 'ludo-chat-input';
inputEl.placeholder = 'اكتب رسالة...';
inputEl.maxLength = 100;
var sendBtn = document.createElement('button');
sendBtn.className = 'ludo-chat-send-btn';
sendBtn.textContent = 'ارسل';
sendBtn.addEventListener('click', function() {
var text = inputEl.value.trim();
if (text) {
send(text, onSend);
inputEl.value = '';
}
});
inputEl.addEventListener('keydown', function(e) {
if (e.key === 'Enter') {
var text = inputEl.value.trim();
if (text) {
send(text, onSend);
inputEl.value = '';
}
}
});
inputRow.appendChild(inputEl);
inputRow.appendChild(sendBtn);
containerEl.appendChild(messagesEl);
containerEl.appendChild(quickEl);
containerEl.appendChild(inputRow);
}
function send(text, onSend) {
var now = Date.now();
if (now - lastSentAt < COOLDOWN) return;
lastSentAt = now;
if (onSend) onSend(text);
}
function addMessage(senderName, text, isSystem) {
if (!messagesEl) return;
var msg = document.createElement('div');
msg.className = 'ludo-chat-msg';
if (isSystem) msg.classList.add('ludo-chat-msg--system');
if (!isSystem && senderName) {
var nameEl = document.createElement('span');
nameEl.className = 'ludo-chat-msg-name';
nameEl.textContent = senderName;
msg.appendChild(nameEl);
}
var textEl = document.createElement('span');
textEl.className = 'ludo-chat-msg-text';
textEl.textContent = text;
msg.appendChild(textEl);
messagesEl.appendChild(msg);
messagesEl.scrollTop = messagesEl.scrollHeight;
}
function loadMessages(chatArray) {
if (!messagesEl) return;
messagesEl.innerHTML = '';
chatArray.forEach(function(m) {
addMessage(m.sender_name, m.text, false);
});
}
function addSystemMessage(text) {
addMessage(null, text, true);
}
return {
init: init,
addMessage: addMessage,
addSystemMessage: addSystemMessage,
loadMessages: loadMessages
};
})();
var LudoConstants = (function() {
'use strict';
var STEP_LENGTH = 6.66;
var PLAYERS = ['P1', 'P2', 'P3', 'P4'];
var PLAYER_LABELS = { P1: 'اللاعب 1', P2: 'اللاعب 2', P3: 'اللاعب 3', P4: 'اللاعب 4' };
var PLAYER_COLORS = { P1: 'p1', P2: 'p2', P3: 'p3', P4: 'p4' };
// Main track: 52 positions (0-51) going clockwise
// Grid is 15x15, coordinates are [col, row] (0-indexed)
// Position 0 starts at bottom of left column (P1's start)
var COORDINATES_MAP = {
// Left column going UP (positions 0-5)
0: [6, 13],
1: [6, 12],
2: [6, 11],
3: [6, 10],
4: [6, 9],
// Top of left cross arm (positions 5-12)
5: [5, 8],
6: [4, 8],
7: [3, 8],
8: [2, 8],
9: [1, 8],
10: [0, 8],
11: [0, 7],
12: [0, 6],
// Top row going RIGHT (positions 13-18)
13: [1, 6],
14: [2, 6],
15: [3, 6],
16: [4, 6],
17: [5, 6],
// Top column going UP (positions 18-25)
18: [6, 5],
19: [6, 4],
20: [6, 3],
21: [6, 2],
22: [6, 1],
23: [6, 0],
24: [7, 0],
25: [8, 0],
// Right column going DOWN (positions 26-30)
26: [8, 1],
27: [8, 2],
28: [8, 3],
29: [8, 4],
30: [8, 5],
// Right cross arm (positions 31-38)
31: [9, 6],
32: [10, 6],
33: [11, 6],
34: [12, 6],
35: [13, 6],
36: [14, 6],
37: [14, 7],
38: [14, 8],
// Bottom row going LEFT (positions 39-43)
39: [13, 8],
40: [12, 8],
41: [11, 8],
42: [10, 8],
43: [9, 8],
// Bottom column going DOWN (positions 44-51)
44: [8, 9],
45: [8, 10],
46: [8, 11],
47: [8, 12],
48: [8, 13],
49: [8, 14],
50: [7, 14],
51: [6, 14],
// P1 Home Entrance (bottom, going UP along center column 7)
100: [7, 13],
101: [7, 12],
102: [7, 11],
103: [7, 10],
104: [7, 9],
105: [7, 8],
// P2 Home Entrance (left, going RIGHT along center row 7)
200: [1, 7],
201: [2, 7],
202: [3, 7],
203: [4, 7],
204: [5, 7],
205: [6, 7],
// P3 Home Entrance (top, going DOWN along center column 7)
300: [7, 1],
301: [7, 2],
302: [7, 3],
303: [7, 4],
304: [7, 5],
305: [7, 6],
// P4 Home Entrance (right, going LEFT along center row 7)
400: [13, 7],
401: [12, 7],
402: [11, 7],
403: [10, 7],
404: [9, 7],
405: [8, 7],
// P1 Base positions (bottom-left quadrant)
500: [1.5, 10.6],
501: [3.5, 10.6],
502: [1.5, 12.4],
503: [3.5, 12.4],
// P2 Base positions (top-left quadrant)
600: [1.5, 1.6],
601: [3.5, 1.6],
602: [1.5, 3.4],
603: [3.5, 3.4],
// P3 Base positions (top-right quadrant)
700: [10.5, 1.6],
701: [12.5, 1.6],
702: [10.5, 3.4],
703: [12.5, 3.4],
// P4 Base positions (bottom-right quadrant)
800: [10.5, 10.6],
801: [12.5, 10.6],
802: [10.5, 12.4],
803: [12.5, 12.4]
};
var BASE_POSITIONS = {
P1: [500, 501, 502, 503],
P2: [600, 601, 602, 603],
P3: [700, 701, 702, 703],
P4: [800, 801, 802, 803]
};
var START_POSITIONS = {
P1: 0,
P2: 13,
P3: 26,
P4: 39
};
var HOME_ENTRANCE = {
P1: [100, 101, 102, 103, 104],
P2: [200, 201, 202, 203, 204],
P3: [300, 301, 302, 303, 304],
P4: [400, 401, 402, 403, 404]
};
var HOME_POSITIONS = {
P1: 105,
P2: 205,
P3: 305,
P4: 405
};
// The last main-track position before entering home entrance
var TURNING_POINTS = {
P1: 50,
P2: 11,
P3: 24,
P4: 37
};
// Positions where pieces cannot be killed
var SAFE_POSITIONS = [0, 8, 13, 21, 26, 34, 39, 47];
var STATE = {
DICE_NOT_ROLLED: 'DICE_NOT_ROLLED',
DICE_ROLLED: 'DICE_ROLLED',
PIECE_MOVING: 'PIECE_MOVING'
};
// Dice face dot positions (3x3 grid indexed 0-8)
// 0 1 2
// 3 4 5
// 6 7 8
var DICE_FACES = {
1: [4],
2: [2, 6],
3: [2, 4, 6],
4: [0, 2, 6, 8],
5: [0, 2, 4, 6, 8],
6: [0, 2, 3, 5, 6, 8]
};
// Quick chat messages
var QUICK_MESSAGES = [
'حظ سعيد',
'يلا',
'هههه',
'اخخخ',
'GG',
'شكرا',
'يا سلام!',
'مبروك'
];
return {
STEP_LENGTH: STEP_LENGTH,
PLAYERS: PLAYERS,
PLAYER_LABELS: PLAYER_LABELS,
PLAYER_COLORS: PLAYER_COLORS,
COORDINATES_MAP: COORDINATES_MAP,
BASE_POSITIONS: BASE_POSITIONS,
START_POSITIONS: START_POSITIONS,
HOME_ENTRANCE: HOME_ENTRANCE,
HOME_POSITIONS: HOME_POSITIONS,
TURNING_POINTS: TURNING_POINTS,
SAFE_POSITIONS: SAFE_POSITIONS,
STATE: STATE,
DICE_FACES: DICE_FACES,
QUICK_MESSAGES: QUICK_MESSAGES
};
})();
var LudoGame = (function() {
'use strict';
var C = LudoConstants;
var UI = LudoUI;
var Bot = LudoBot;
var state = {
activePlayers: [],
positions: {},
currentPlayerIdx: 0,
diceValue: 0,
phase: C.STATE.DICE_NOT_ROLLED,
winners: [],
consecutiveSixes: 0,
isGameOver: false,
mode: 'local',
bots: {},
botDifficulty: 'easy',
playerNames: {},
onGameEnd: null
};
function init(options) {
state.activePlayers = options.players || ['P1', 'P2', 'P3', 'P4'];
state.mode = options.mode || 'local';
state.bots = options.bots || {};
state.botDifficulty = options.difficulty || 'easy';
state.playerNames = options.playerNames || {};
state.onGameEnd = options.onGameEnd || null;
state.currentPlayerIdx = 0;
state.diceValue = 0;
state.phase = C.STATE.DICE_NOT_ROLLED;
state.winners = [];
state.consecutiveSixes = 0;
state.isGameOver = false;
state.positions = {};
state.activePlayers.forEach(function(p) {
state.positions[p] = C.BASE_POSITIONS[p].slice();
});
UI.renderPieces(state.activePlayers);
state.activePlayers.forEach(function(p) {
for (var i = 0; i < 4; i++) {
UI.setPiecePosition(p, i, state.positions[p][i]);
}
});
UI.setTurn(currentPlayer(), state.playerNames[currentPlayer()]);
UI.enableDice();
UI.clearLog();
UI.hideResult();
UI.addLogEntry('بدأت اللعبة!');
UI.listenDiceClick(onDiceClick);
UI.listenPieceClick(onPieceClick);
if (isCurrentPlayerBot()) {
scheduleBotRoll();
}
}
function currentPlayer() {
return state.activePlayers[state.currentPlayerIdx];
}
function isCurrentPlayerBot() {
return !!state.bots[currentPlayer()];
}
function onDiceClick() {
if (state.isGameOver) return;
if (state.phase !== C.STATE.DICE_NOT_ROLLED) return;
if (isCurrentPlayerBot()) return;
rollDice();
}
function rollDice() {
state.phase = C.STATE.DICE_ROLLED;
UI.disableDice();
var value = 1 + Math.floor(Math.random() * 6);
state.diceValue = value;
UI.animateDiceRoll(value, function() {
afterRoll();
});
}
function afterRoll() {
var player = currentPlayer();
var eligible = Bot.getEligiblePieces(player, state.diceValue, state.positions, state.activePlayers);
UI.addLogEntry(getPlayerLabel(player) + ' رمى ' + state.diceValue);
if (eligible.length === 0) {
UI.addLogEntry(getPlayerLabel(player) + ' لا يوجد حركة متاحة');
state.consecutiveSixes = 0;
setTimeout(function() { nextTurn(); }, 600);
return;
}
if (eligible.length === 1 && isCurrentPlayerBot()) {
setTimeout(function() { movePiece(player, eligible[0]); }, 500);
return;
}
if (isCurrentPlayerBot()) {
var chosen = Bot.takeTurn(player, state.diceValue, state.positions, state.activePlayers,
state.bots[player] || state.botDifficulty);
setTimeout(function() { movePiece(player, chosen); }, 500);
return;
}
UI.highlightPieces(player, eligible);
state.phase = C.STATE.DICE_ROLLED;
}
function onPieceClick(e) {
if (state.isGameOver) return;
if (state.phase !== C.STATE.DICE_ROLLED) return;
if (isCurrentPlayerBot()) return;
var target = e.target.closest('.ludo-piece--highlight');
if (!target) return;
var player = target.dataset.player;
var pieceIdx = parseInt(target.dataset.piece, 10);
if (player !== currentPlayer()) return;
UI.unhighlightPieces();
movePiece(player, pieceIdx);
}
function movePiece(player, pieceIdx) {
state.phase = C.STATE.PIECE_MOVING;
UI.unhighlightPieces();
var currentPos = state.positions[player][pieceIdx];
var isInBase = C.BASE_POSITIONS[player].indexOf(currentPos) !== -1;
var newPos;
if (isInBase && state.diceValue === 6) {
newPos = C.START_POSITIONS[player];
} else {
newPos = Bot.calculateNewPosition(player, currentPos, state.diceValue);
}
if (newPos === null) {
nextTurn();
return;
}
state.positions[player][pieceIdx] = newPos;
UI.setPiecePosition(player, pieceIdx, newPos);
UI.updateStacking(state.positions, state.activePlayers);
var killed = checkKill(player, pieceIdx, newPos);
var reachedHome = newPos === C.HOME_POSITIONS[player];
var rolledSix = state.diceValue === 6;
var extraTurn = false;
if (killed) {
UI.addLogEntry(getPlayerLabel(player) + ' اكل قطعة ' + getPlayerLabel(killed.player));
extraTurn = true;
}
if (reachedHome) {
UI.addLogEntry(getPlayerLabel(player) + ' وصّل قطعة للبيت!');
extraTurn = true;
if (checkPlayerFinished(player)) {
state.winners.push(player);
UI.setPlayerFinished(player);
UI.addLogEntry(getPlayerLabel(player) + ' خلّص! المركز ' + state.winners.length);
if (checkGameOver()) {
endGame();
return;
}
}
}
if (rolledSix) {
state.consecutiveSixes++;
if (state.consecutiveSixes >= 3) {
UI.addLogEntry(getPlayerLabel(player) + ' 3 ستات متتالية - ضاع الدور');
state.consecutiveSixes = 0;
extraTurn = false;
} else {
extraTurn = true;
}
}
if (extraTurn && !checkPlayerFinished(player)) {
state.phase = C.STATE.DICE_NOT_ROLLED;
UI.enableDice();
if (isCurrentPlayerBot()) {
scheduleBotRoll();
}
} else {
state.consecutiveSixes = 0;
setTimeout(function() { nextTurn(); }, 300);
}
}
function checkKill(player, pieceIdx, position) {
if (C.SAFE_POSITIONS.indexOf(position) !== -1) return null;
if (position === C.START_POSITIONS[player]) {
// can still kill at start
}
if (C.HOME_ENTRANCE[player].indexOf(position) !== -1) return null;
if (position === C.HOME_POSITIONS[player]) return null;
for (var i = 0; i < state.activePlayers.length; i++) {
var opponent = state.activePlayers[i];
if (opponent === player) continue;
for (var j = 0; j < 4; j++) {
if (state.positions[opponent][j] === position) {
state.positions[opponent][j] = C.BASE_POSITIONS[opponent][j];
UI.setPiecePosition(opponent, j, C.BASE_POSITIONS[opponent][j]);
UI.updateStacking(state.positions, state.activePlayers);
return { player: opponent, piece: j };
}
}
}
return null;
}
function checkPlayerFinished(player) {
for (var i = 0; i < 4; i++) {
if (state.positions[player][i] !== C.HOME_POSITIONS[player]) return false;
}
return true;
}
function checkGameOver() {
var remaining = state.activePlayers.filter(function(p) {
return state.winners.indexOf(p) === -1;
});
return remaining.length <= 1;
}
function endGame() {
state.isGameOver = true;
UI.disableDice();
var remaining = state.activePlayers.filter(function(p) {
return state.winners.indexOf(p) === -1;
});
remaining.forEach(function(p) { state.winners.push(p); });
var winner = state.winners[0];
var title = getPlayerLabel(winner) + ' فاز!';
var subtitle = 'ترتيب: ' + state.winners.map(function(p, i) {
return (i + 1) + '. ' + getPlayerLabel(p);
}).join(' | ');
UI.showResult(title, subtitle);
UI.addLogEntry('انتهت اللعبة! الفائز: ' + getPlayerLabel(winner));
if (state.onGameEnd) {
state.onGameEnd(state.winners);
}
}
function nextTurn() {
var startIdx = state.currentPlayerIdx;
do {
state.currentPlayerIdx = (state.currentPlayerIdx + 1) % state.activePlayers.length;
} while (
state.winners.indexOf(state.activePlayers[state.currentPlayerIdx]) !== -1 &&
state.currentPlayerIdx !== startIdx
);
if (state.currentPlayerIdx === startIdx && state.winners.indexOf(currentPlayer()) !== -1) {
endGame();
return;
}
state.phase = C.STATE.DICE_NOT_ROLLED;
state.consecutiveSixes = 0;
UI.setTurn(currentPlayer(), state.playerNames[currentPlayer()]);
UI.enableDice();
if (isCurrentPlayerBot()) {
scheduleBotRoll();
}
}
function scheduleBotRoll() {
UI.disableDice();
setTimeout(function() {
if (state.isGameOver) return;
rollDice();
}, 800);
}
function getPlayerLabel(player) {
return state.playerNames[player] || C.PLAYER_LABELS[player];
}
function getState() {
return {
positions: state.positions,
currentPlayer: currentPlayer(),
diceValue: state.diceValue,
phase: state.phase,
winners: state.winners,
isGameOver: state.isGameOver
};
}
function restart(options) {
init(options);
}
return {
init: init,
restart: restart,
getState: getState
};
})();
This diff is collapsed.
This diff is collapsed.
...@@ -129,7 +129,7 @@ const Puzzles = { ...@@ -129,7 +129,7 @@ const Puzzles = {
dot.classList.remove('solved', 'current'); dot.classList.remove('solved', 'current');
if (i < this.dailySolved) { if (i < this.dailySolved) {
dot.classList.add('solved'); dot.classList.add('solved');
dot.innerHTML = '<svg class="icon-sm" style="color:#fff"><use href="/public/icons/sprite.svg#icon-check"></use></svg>'; dot.innerHTML = '<svg class="icon-sm" style="color:var(--text-1)"><use href="/public/icons/sprite.svg#icon-check"></use></svg>';
} else if (i === this.dailySolved) { } else if (i === this.dailySolved) {
dot.classList.add('current'); dot.classList.add('current');
} }
...@@ -336,8 +336,8 @@ const Puzzles = { ...@@ -336,8 +336,8 @@ const Puzzles = {
// Highlight the correct squares // Highlight the correct squares
const fromSq = Board.getSquareEl(from); const fromSq = Board.getSquareEl(from);
const toSq = Board.getSquareEl(to); const toSq = Board.getSquareEl(to);
if (fromSq) fromSq.style.background = 'rgba(76,175,80,0.4)'; if (fromSq) fromSq.style.background = 'var(--board-highlight-green)';
if (toSq) toSq.style.background = 'rgba(76,175,80,0.4)'; if (toSq) toSq.style.background = 'var(--board-highlight-green)';
}, },
async reportAttempt(solved, timeMs) { async reportAttempt(solved, timeMs) {
......
...@@ -3,9 +3,9 @@ ...@@ -3,9 +3,9 @@
$currentRoute = $_GET['route'] ?? ''; $currentRoute = $_GET['route'] ?? '';
$bottomItems = [ $bottomItems = [
['/', 'icon-home', 'الرئيسية'], ['/', 'icon-home', 'الرئيسية'],
['/play', 'icon-play', 'العب'], ['/play', 'icon-play', 'شطرنج'],
['/ludo', 'icon-ludo', 'لودو'],
['/tournaments', 'icon-trophy', 'بطولات'], ['/tournaments', 'icon-trophy', 'بطولات'],
['/friends', 'icon-friends', 'اجتماعي'],
['/profile', 'icon-profile', 'حسابي'], ['/profile', 'icon-profile', 'حسابي'],
]; ];
foreach ($bottomItems as $item): foreach ($bottomItems as $item):
......
...@@ -4,7 +4,8 @@ ...@@ -4,7 +4,8 @@
$currentRoute = $_GET['route'] ?? ''; $currentRoute = $_GET['route'] ?? '';
$navItems = [ $navItems = [
['/', 'icon-home', 'الرئيسية'], ['/', 'icon-home', 'الرئيسية'],
['/play', 'icon-play', 'العب'], ['/play', 'icon-play', 'شطرنج'],
['/ludo', 'icon-ludo', 'لودو'],
['/puzzles', 'icon-puzzle', 'تمارين'], ['/puzzles', 'icon-puzzle', 'تمارين'],
['/tournaments', 'icon-trophy', 'بطولات'], ['/tournaments', 'icon-trophy', 'بطولات'],
['/leaderboard', 'icon-leaderboard', 'متصدرون'], ['/leaderboard', 'icon-leaderboard', 'متصدرون'],
......
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