Commit c21d4618 authored by Mahmoud Aglan's avatar Mahmoud Aglan

feat: add full Backgammon game — bot AI, multiplayer, tournament rules

Complete backgammon (طاولة) implementation:
- 24-point board with bar, bearing off, doubling cube
- Bot AI: easy (random), medium (heuristic), hard (positional eval)
- Local pass-and-play + VS bot + online matchmaking + private rooms
- Server-side move validation, opening roll, gammon/backgammon detection
- Supabase Realtime WebSocket for multiplayer
- Responsive CSS with wood-grain board, triangle points, stacking checkers
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent 1f8615ee
This diff is collapsed.
......@@ -63,6 +63,14 @@ if ($route === '' || $route === 'home') {
require 'pages/domino-live.php';
} elseif ($route === 'domino-matchmaking') {
require 'pages/domino-matchmaking.php';
} elseif ($route === 'backgammon') {
require 'pages/backgammon.php';
} elseif ($route === 'backgammon-game') {
require 'pages/backgammon-game.php';
} elseif ($route === 'backgammon-live') {
require 'pages/backgammon-live.php';
} elseif ($route === 'backgammon-matchmaking') {
require 'pages/backgammon-matchmaking.php';
} elseif ($route === 'admin/theme') {
require 'pages/admin-theme.php';
} elseif (str_starts_with($route, 'api/')) {
......
<?php $pageTitle = 'EL3AB - طاولة'; $hideNav = true; ?>
<?php require __DIR__ . '/../includes/header.php'; ?>
<link rel="stylesheet" href="/public/css/backgammon.css">
<div class="bg-layout">
<div class="bg-board-column">
<!-- Player bar top (opponent) -->
<div class="bg-players-bar">
<div class="bg-player-info bg-player-info--black" id="bg-player-1">
<span class="bg-player-dot bg-player-dot--black"></span>
<span class="bg-player-name" id="bg-name-1">اللاعب 2</span>
<span class="bg-player-pip">نقاط: <span id="bg-pip-1">167</span></span>
</div>
</div>
<!-- Board -->
<div class="bg-board" id="bg-board"></div>
<!-- Player bar bottom (you) -->
<div class="bg-players-bar">
<div class="bg-player-info bg-player-info--white" id="bg-player-0">
<span class="bg-player-dot bg-player-dot--white"></span>
<span class="bg-player-name" id="bg-name-0">أنت</span>
<span class="bg-player-pip">نقاط: <span id="bg-pip-0">167</span></span>
</div>
</div>
</div>
<!-- Side panel -->
<div class="bg-side-panel">
<div id="bg-turn" class="bg-turn-indicator"></div>
<div id="bg-dice-container" class="bg-dice-area"></div>
<div class="bg-controls">
<button id="bg-roll-btn" class="bg-roll-btn">ارمِ النرد</button>
</div>
<div id="bg-log" class="bg-log"></div>
</div>
</div>
<!-- Result overlay -->
<div id="bg-result-overlay" class="bg-result-overlay" style="display:none;"></div>
<script src="/public/js/backgammon-constants.js"></script>
<script src="/public/js/backgammon-ui.js"></script>
<script src="/public/js/backgammon-bot.js"></script>
<script src="/public/js/backgammon-game.js"></script>
<script>
(function() {
var params = new URLSearchParams(window.location.search);
var mode = params.get('mode') || 'bot';
var difficulty = params.get('difficulty') || 'medium';
var players = [
{ id: 'p0', name: 'أنت', type: 'human' },
{ id: 'p1', name: 'اللاعب 2', type: 'human' }
];
var bots = [];
if (mode === 'bot') {
players[1] = { id: 'bot_0', name: 'بوت', type: 'bot', difficulty: difficulty };
bots.push({ index: 1, difficulty: difficulty });
}
BackgammonGame.init({
mode: mode,
players: players,
bots: bots,
matchLength: 1
});
})();
</script>
<?php require __DIR__ . '/../includes/footer.php'; ?>
<?php $pageTitle = 'EL3AB - طاولة'; $hideNav = true; ?>
<?php require __DIR__ . '/../includes/header.php'; ?>
<link rel="stylesheet" href="/public/css/backgammon.css">
<!-- Waiting Room -->
<div id="bg-waiting" class="lobby-page" style="max-width:500px;margin:0 auto;padding:24px;">
<div class="text-center" style="margin-bottom:24px;">
<h2 class="lobby-title">طاولة — غرفة خاصة</h2>
<p class="text-muted text-sm" id="bg-waiting-msg">جاري الإعداد...</p>
</div>
<div class="card" style="padding:24px;text-align:center;">
<p class="text-muted text-sm">كود الغرفة</p>
<h1 id="bg-room-code" style="font-size:2.5rem;letter-spacing:8px;margin:8px 0;font-family:monospace;">------</h1>
<p class="text-muted text-sm" id="bg-players-count">0 / 2 لاعبين</p>
</div>
<div id="bg-waiting-players" style="margin-top:16px;"></div>
<button id="bg-start-btn" class="btn btn-gold btn-block btn-lg" style="margin-top:16px;display:none;">
ابدأ اللعبة
</button>
</div>
<!-- Game Area (hidden until game starts) -->
<div id="bg-game-area" style="display:none;">
<div class="bg-layout">
<div class="bg-board-column">
<div class="bg-players-bar">
<div class="bg-player-info bg-player-info--black" id="bg-player-1">
<span class="bg-player-dot bg-player-dot--black"></span>
<span class="bg-player-name" id="bg-name-1">الخصم</span>
<span class="bg-player-pip">نقاط: <span id="bg-pip-1">167</span></span>
</div>
</div>
<div class="bg-board" id="bg-board"></div>
<div class="bg-players-bar">
<div class="bg-player-info bg-player-info--white" id="bg-player-0">
<span class="bg-player-dot bg-player-dot--white"></span>
<span class="bg-player-name" id="bg-name-0">أنت</span>
<span class="bg-player-pip">نقاط: <span id="bg-pip-0">167</span></span>
</div>
</div>
</div>
<div class="bg-side-panel">
<div id="bg-turn" class="bg-turn-indicator"></div>
<div id="bg-dice-container" class="bg-dice-area"></div>
<div class="bg-controls">
<button id="bg-roll-btn" class="bg-roll-btn">ارمِ النرد</button>
</div>
<div id="bg-log" class="bg-log"></div>
</div>
</div>
<div id="bg-result-overlay" class="bg-result-overlay" style="display:none;"></div>
</div>
<script src="/public/js/backgammon-constants.js"></script>
<script src="/public/js/backgammon-ui.js"></script>
<script src="/public/js/backgammon-live.js"></script>
<script>
(function() {
var params = new URLSearchParams(window.location.search);
var action = params.get('action');
var code = params.get('code');
var matchId = params.get('match_id');
BackgammonLive.init({
action: action,
code: code,
matchId: matchId
});
})();
</script>
<?php require __DIR__ . '/../includes/footer.php'; ?>
<?php $pageTitle = 'EL3AB - طاولة'; $hideNav = true; ?>
<?php require __DIR__ . '/../includes/header.php'; ?>
<link rel="stylesheet" href="/public/css/backgammon.css">
<div class="lobby-page" style="max-width:500px;margin:0 auto;padding:24px;text-align:center;">
<h2 class="lobby-title" style="margin-bottom:8px;">طاولة</h2>
<p class="text-muted text-sm" style="margin-bottom:32px;">البحث عن خصم...</p>
<div class="card" style="padding:32px;">
<div class="matchmaking-spinner"></div>
<p id="bg-mm-timer" style="font-size:1.5rem;margin-top:16px;font-weight:700;">0:00</p>
<p class="text-muted text-sm" style="margin-top:8px;">في الانتظار</p>
</div>
<button class="btn btn-outline btn-block" style="margin-top:24px;" onclick="cancelMatchmaking()">
إلغاء
</button>
</div>
<script>
(function() {
var token = localStorage.getItem('el3ab_token');
var user = JSON.parse(localStorage.getItem('el3ab_user') || 'null');
if (!token || !user) { window.location.href = '/login'; return; }
var startTime = Date.now();
var timerEl = document.getElementById('bg-mm-timer');
var polling = null;
var cancelled = false;
function updateTimer() {
var elapsed = Math.floor((Date.now() - startTime) / 1000);
var m = Math.floor(elapsed / 60);
var s = elapsed % 60;
timerEl.textContent = m + ':' + (s < 10 ? '0' : '') + s;
}
setInterval(updateTimer, 1000);
function joinQueue() {
fetch('/api/backgammon.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token },
body: JSON.stringify({ action: 'matchmake', sub_action: 'join' })
}).then(function(r) { return r.json(); }).then(function(data) {
if (data.match_id) {
window.location.href = '/backgammon-live?match_id=' + data.match_id;
} else {
polling = setInterval(pollQueue, 3000);
}
});
}
function pollQueue() {
if (cancelled) return;
fetch('/api/backgammon.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token },
body: JSON.stringify({ action: 'matchmake', sub_action: 'poll' })
}).then(function(r) { return r.json(); }).then(function(data) {
if (data.match_id) {
clearInterval(polling);
window.location.href = '/backgammon-live?match_id=' + data.match_id;
}
});
}
window.cancelMatchmaking = function() {
cancelled = true;
if (polling) clearInterval(polling);
fetch('/api/backgammon.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token },
body: JSON.stringify({ action: 'matchmake', sub_action: 'leave' })
}).then(function() {
window.location.href = '/backgammon';
});
};
joinQueue();
})();
</script>
<?php require __DIR__ . '/../includes/footer.php'; ?>
<?php $pageTitle = 'EL3AB - طاولة'; ?>
<?php require __DIR__ . '/../includes/header.php'; ?>
<div class="lobby-page">
<a href="/games" class="breadcrumb">
<svg class="icon" style="width:14px;height:14px;"><use href="/public/icons/sprite.svg#icon-arrow-right"></use></svg>
العاب
</a>
<div class="text-center" style="margin-top:16px;">
<h2 class="lobby-title">طاولة</h2>
<p class="text-muted text-sm">اختر نوع اللعب</p>
</div>
<div class="lobby-cards">
<!-- Local (Pass & Play) -->
<div class="card lobby-card">
<div class="card-body space-y-4">
<div class="lobby-card-header">
<div class="avatar avatar-game" style="background:linear-gradient(135deg, var(--cyan), var(--gold));">
<svg class="icon-lg" style="color:var(--text-1)"><use href="/public/icons/sprite.svg#icon-users"></use></svg>
</div>
<div>
<p class="lobby-card-title-sm">لعب محلي</p>
<p class="text-muted text-sm">لاعبين على نفس الجهاز</p>
</div>
</div>
<button class="btn btn-gold btn-block btn-lg" onclick="startLocal()">
<svg class="icon"><use href="/public/icons/sprite.svg#icon-play"></use></svg>
ابدأ اللعب
</button>
</div>
</div>
<!-- VS Bot -->
<div class="card lobby-card">
<div class="card-body space-y-4">
<div class="lobby-card-header">
<div class="avatar avatar-game" 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 class="lobby-card-title-sm">ضد البوت</p>
<p class="text-muted text-sm">العب ضد بوتات ذكية</p>
</div>
</div>
<div>
<label class="input-label">الصعوبة</label>
<div class="tab-group" id="bot-diff-tabs">
<button class="tab" data-diff="easy">سهل</button>
<button class="tab active" data-diff="medium">متوسط</button>
<button class="tab" data-diff="hard">صعب</button>
</div>
</div>
<button class="btn btn-gold btn-block btn-lg" onclick="startBot()">
<svg class="icon"><use href="/public/icons/sprite.svg#icon-play"></use></svg>
ابدأ اللعب
</button>
</div>
</div>
<!-- Online Multiplayer -->
<div class="card lobby-card card-featured">
<div class="card-body space-y-4">
<div class="lobby-card-header">
<div class="avatar avatar-game" style="background:linear-gradient(135deg, var(--gold), var(--gold-dark));">
<svg class="icon-lg" style="color:var(--text-1)"><use href="/public/icons/sprite.svg#icon-lightning"></use></svg>
</div>
<div>
<p class="lobby-card-title-sm">ضد لاعب حقيقي</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-play"></use></svg>
ابحث عن خصم
</button>
</div>
</div>
<!-- Private Room -->
<div class="card lobby-card">
<div class="card-body space-y-4">
<div class="lobby-card-header">
<div class="avatar avatar-game" style="background:var(--bg-3);">
<svg class="icon-lg" style="color:var(--gold)"><use href="/public/icons/sprite.svg#icon-key"></use></svg>
</div>
<div>
<p class="lobby-card-title-sm">غرفة خاصة</p>
<p class="text-muted text-sm">العب مع صديق بكود</p>
</div>
</div>
<div style="display:flex;gap:8px;">
<button class="btn btn-cyan flex-1" onclick="createRoom()">إنشاء غرفة</button>
<button class="btn btn-outline flex-1" onclick="joinRoom()">انضم بكود</button>
</div>
</div>
</div>
</div>
</div>
<script>
function getActiveDiff() {
var el = document.querySelector('#bot-diff-tabs .tab.active');
return el ? el.dataset.diff : 'medium';
}
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 = '/backgammon-game?mode=local';
}
function startBot() {
var diff = getActiveDiff();
window.location.href = '/backgammon-game?mode=bot&difficulty=' + diff;
}
function startMatchmaking() {
window.location.href = '/backgammon-matchmaking';
}
function createRoom() {
window.location.href = '/backgammon-live?action=create';
}
function joinRoom() {
var code = prompt('ادخل كود الغرفة:');
if (code && code.trim()) {
window.location.href = '/backgammon-live?action=join&code=' + code.trim().toUpperCase();
}
}
</script>
<?php require __DIR__ . '/../includes/footer.php'; ?>
......@@ -29,7 +29,8 @@ document.addEventListener('DOMContentLoaded', async () => {
const gameRoutes = {
chess: '/play',
ludo: '/ludo',
domino: '/domino'
domino: '/domino',
backgammon: '/backgammon'
};
const gameIcons = {
......
This diff is collapsed.
var BackgammonBot = (function() {
'use strict';
var C = BackgammonConstants;
function chooseMove(board, bar, borneOff, player, diceRemaining, difficulty) {
var sequences = C.getAllMovesForDice(board, bar, borneOff, player, diceRemaining);
if (!sequences || sequences.length === 0) return [];
if (difficulty === 'easy') {
return sequences[Math.floor(Math.random() * sequences.length)];
}
var best = null;
var bestScore = -Infinity;
for (var i = 0; i < sequences.length; i++) {
var score = scoreSequence(board, bar, borneOff, player, sequences[i], difficulty);
if (score > bestScore) {
bestScore = score;
best = sequences[i];
}
}
return best || sequences[0];
}
function scoreSequence(board, bar, borneOff, player, moves, difficulty) {
var state = { board: board.slice(), bar: bar.slice(), borneOff: borneOff.slice() };
var score = 0;
for (var i = 0; i < moves.length; i++) {
var move = moves[i];
if (move.hit) score += (difficulty === 'hard') ? 4 : 3;
if (move.to === 'off') score += 5;
state = C.applyMove(state.board, state.bar, state.borneOff, player, move);
}
if (difficulty === 'medium') {
score += scoreMedium(state.board, state.bar, state.borneOff, player);
} else if (difficulty === 'hard') {
score += scoreHard(state.board, state.bar, state.borneOff, player);
}
return score;
}
function scoreMedium(board, bar, borneOff, player) {
var score = 0;
var opp = 1 - player;
var homeStart = (player === 0) ? 0 : 18;
var homeEnd = (player === 0) ? 5 : 23;
for (var i = 0; i < 24; i++) {
var count = board[i];
var isOwn = (player === 0) ? count > 0 : count < 0;
var absCount = Math.abs(count);
if (isOwn) {
if (absCount === 1) {
score -= 4;
if (isInOpponentHome(i, player)) score -= 3;
}
if (absCount >= 2) {
score += 1;
}
}
}
score += borneOff[player] * 5;
score -= bar[player] * 8;
score += bar[opp] * 3;
return score;
}
function scoreHard(board, bar, borneOff, player) {
var score = 0;
var opp = 1 - player;
var myPips = C.pipCount(board, bar, player);
var oppPips = C.pipCount(board, bar, opp);
score -= myPips * 0.3;
score += (oppPips - myPips) * 0.2;
var primeLen = longestPrime(board, player);
score += primeLen * primeLen * 2;
var blots = countBlots(board, player);
for (var b = 0; b < blots.length; b++) {
score -= 3;
if (isInOpponentHome(blots[b], player)) score -= 5;
}
var anchors = countAnchors(board, player);
score += anchors * 4;
score += borneOff[player] * 6;
score -= bar[player] * 10;
score += bar[opp] * 4;
var phase = detectPhase(board, bar, borneOff, player);
if (phase === 'bearoff') {
score += borneOff[player] * 3;
} else if (phase === 'prime') {
score += primeLen * 3;
}
return score;
}
function detectPhase(board, bar, borneOff, player) {
if (C.isAllHome(board, bar, player)) return 'bearoff';
if (longestPrime(board, player) >= 4) return 'prime';
var myPips = C.pipCount(board, bar, player);
var oppPips = C.pipCount(board, bar, 1 - player);
if (Math.abs(myPips - oppPips) > 30) return 'race';
return 'positional';
}
function longestPrime(board, player) {
var max = 0;
var current = 0;
for (var i = 0; i < 24; i++) {
var count = board[i];
var isOwn = (player === 0) ? count >= 2 : count <= -2;
if (isOwn) {
current++;
if (current > max) max = current;
} else {
current = 0;
}
}
return max;
}
function countBlots(board, player) {
var blots = [];
for (var i = 0; i < 24; i++) {
var count = board[i];
var isBlot = (player === 0) ? count === 1 : count === -1;
if (isBlot) blots.push(i);
}
return blots;
}
function countAnchors(board, player) {
var count = 0;
var oppHomeStart = (player === 0) ? 18 : 0;
var oppHomeEnd = (player === 0) ? 23 : 5;
for (var i = oppHomeStart; i <= oppHomeEnd; i++) {
var val = board[i];
var isOwn = (player === 0) ? val >= 2 : val <= -2;
if (isOwn) count++;
}
return count;
}
function isInOpponentHome(point, player) {
if (player === 0) return point >= 18 && point <= 23;
return point >= 0 && point <= 5;
}
return {
chooseMove: chooseMove
};
})();
This diff is collapsed.
var BackgammonGame = (function() {
'use strict';
var C = BackgammonConstants;
var UI = BackgammonUI;
var Bot = BackgammonBot;
var state = null;
function init(opts) {
state = {
mode: opts.mode || 'local',
players: opts.players || [
{ id: 'p0', name: 'اللاعب 1', type: 'human' },
{ id: 'p1', name: 'اللاعب 2', type: 'human' }
],
board: C.INITIAL_BOARD.slice(),
bar: [0, 0],
borneOff: [0, 0],
currentTurn: 0,
dice: null,
diceRemaining: [],
phase: 'awaiting_roll',
scores: [0, 0],
matchLength: opts.matchLength || 1,
doublingCube: { value: 1, owner: null },
moves: [],
onGameEnd: opts.onGameEnd || null
};
if (opts.bots) {
for (var i = 0; i < opts.bots.length; i++) {
var botIdx = opts.bots[i].index;
state.players[botIdx].type = 'bot';
state.players[botIdx].difficulty = opts.bots[i].difficulty || 'medium';
state.players[botIdx].name = 'بوت (' + (opts.bots[i].difficulty === 'easy' ? 'سهل' : opts.bots[i].difficulty === 'hard' ? 'صعب' : 'متوسط') + ')';
}
}
UI.init(state);
doOpeningRoll();
}
function doOpeningRoll() {
var d1 = Math.ceil(Math.random() * 6);
var d2 = Math.ceil(Math.random() * 6);
while (d1 === d2) {
d1 = Math.ceil(Math.random() * 6);
d2 = Math.ceil(Math.random() * 6);
}
state.currentTurn = (d1 > d2) ? 0 : 1;
state.dice = [d1, d2];
state.diceRemaining = [d1, d2];
state.phase = 'moving';
UI.showLog('رمي الافتتاح: ' + d1 + ' و ' + d2 + ' — ' + state.players[state.currentTurn].name + ' يبدأ');
UI.render(state);
if (state.players[state.currentTurn].type === 'bot') {
setTimeout(function() { executeBotTurn(); }, 800);
}
}
function rollDice() {
if (state.phase !== 'awaiting_roll') return;
if (state.players[state.currentTurn].type !== 'human') return;
var d1 = Math.ceil(Math.random() * 6);
var d2 = Math.ceil(Math.random() * 6);
state.dice = [d1, d2];
state.diceRemaining = C.getDiceRemaining(d1, d2);
state.phase = 'moving';
UI.showLog(state.players[state.currentTurn].name + ' رمى ' + d1 + ' و ' + d2);
if (!C.canMove(state.board, state.bar, state.borneOff, state.currentTurn, state.diceRemaining)) {
UI.showLog('لا توجد حركة متاحة — تخطي الدور');
setTimeout(function() { endTurn(); }, 1200);
}
UI.render(state);
}
function handlePointClick(pointOrAction) {
if (state.phase !== 'moving') return;
if (state.players[state.currentTurn].type !== 'human') return;
if (pointOrAction === 'roll') {
rollDice();
return;
}
var selected = UI.getSelected();
if (selected === null) {
if (pointOrAction === 'bar') {
if (state.bar[state.currentTurn] > 0) {
UI.setSelected('bar');
highlightValidTargets('bar');
}
} else {
var point = parseInt(pointOrAction);
var count = state.board[point];
var isOwn = (state.currentTurn === 0) ? count > 0 : count < 0;
if (isOwn) {
UI.setSelected(point);
highlightValidTargets(point);
}
}
} else {
var from = selected;
var to = (pointOrAction === 'off') ? 'off' : parseInt(pointOrAction);
var validMoves = getValidMovesForFrom(from);
var validMove = null;
for (var i = 0; i < validMoves.length; i++) {
if (validMoves[i].to === to) {
validMove = validMoves[i];
break;
}
}
if (validMove) {
executeMove(validMove);
}
UI.clearSelected();
UI.clearHighlights();
}
}
function getValidMovesForFrom(from) {
var allMoves = [];
for (var i = 0; i < state.diceRemaining.length; i++) {
var die = state.diceRemaining[i];
var moves = C.getValidMoves(state.board, state.bar, state.borneOff, state.currentTurn, die);
for (var j = 0; j < moves.length; j++) {
if (moves[j].from === from) {
var isDup = false;
for (var k = 0; k < allMoves.length; k++) {
if (allMoves[k].to === moves[j].to) { isDup = true; break; }
}
if (!isDup) allMoves.push(moves[j]);
}
}
}
return allMoves;
}
function highlightValidTargets(from) {
var moves = getValidMovesForFrom(from);
var targets = [];
for (var i = 0; i < moves.length; i++) {
targets.push(moves[i].to);
}
UI.highlightPoints(targets);
}
function executeMove(move) {
var result = C.applyMove(state.board, state.bar, state.borneOff, state.currentTurn, move);
state.board = result.board;
state.bar = result.bar;
state.borneOff = result.borneOff;
var dieUsed = move.dieUsed;
var idx = state.diceRemaining.indexOf(dieUsed);
if (idx !== -1) state.diceRemaining.splice(idx, 1);
state.moves.push({
player: state.currentTurn,
from: move.from,
to: move.to,
die: dieUsed,
hit: move.hit
});
if (move.hit) {
UI.showLog(state.players[state.currentTurn].name + ' ضرب قطعة!');
}
if (C.checkWin(state.borneOff, state.currentTurn)) {
handleWin();
return;
}
if (state.diceRemaining.length === 0 || !C.canMove(state.board, state.bar, state.borneOff, state.currentTurn, state.diceRemaining)) {
if (state.diceRemaining.length > 0) {
UI.showLog('لا توجد حركة متاحة بالنرد المتبقي');
}
setTimeout(function() { endTurn(); }, 600);
}
UI.render(state);
}
function endTurn() {
state.currentTurn = 1 - state.currentTurn;
state.phase = 'awaiting_roll';
state.dice = null;
state.diceRemaining = [];
UI.render(state);
if (state.players[state.currentTurn].type === 'bot') {
setTimeout(function() { executeBotTurn(); }, 800);
}
}
function executeBotTurn() {
if (state.phase === 'awaiting_roll') {
var d1 = Math.ceil(Math.random() * 6);
var d2 = Math.ceil(Math.random() * 6);
state.dice = [d1, d2];
state.diceRemaining = C.getDiceRemaining(d1, d2);
state.phase = 'moving';
UI.showLog(state.players[state.currentTurn].name + ' رمى ' + d1 + ' و ' + d2);
UI.render(state);
}
if (!C.canMove(state.board, state.bar, state.borneOff, state.currentTurn, state.diceRemaining)) {
UI.showLog(state.players[state.currentTurn].name + ' — لا توجد حركة');
setTimeout(function() { endTurn(); }, 800);
return;
}
var difficulty = state.players[state.currentTurn].difficulty || 'medium';
var moves = Bot.chooseMove(state.board, state.bar, state.borneOff, state.currentTurn, state.diceRemaining, difficulty);
if (!moves || moves.length === 0) {
setTimeout(function() { endTurn(); }, 800);
return;
}
executeBotMoves(moves, 0);
}
function executeBotMoves(moves, idx) {
if (idx >= moves.length) {
setTimeout(function() { endTurn(); }, 600);
return;
}
var move = moves[idx];
var result = C.applyMove(state.board, state.bar, state.borneOff, state.currentTurn, move);
state.board = result.board;
state.bar = result.bar;
state.borneOff = result.borneOff;
var dieIdx = state.diceRemaining.indexOf(move.dieUsed);
if (dieIdx !== -1) state.diceRemaining.splice(dieIdx, 1);
state.moves.push({
player: state.currentTurn,
from: move.from,
to: move.to,
die: move.dieUsed,
hit: move.hit
});
if (move.hit) UI.showLog(state.players[state.currentTurn].name + ' ضرب قطعة!');
UI.render(state);
if (C.checkWin(state.borneOff, state.currentTurn)) {
handleWin();
return;
}
setTimeout(function() { executeBotMoves(moves, idx + 1); }, 500);
}
function handleWin() {
var winner = state.currentTurn;
var winType = C.getWinType(state.board, state.bar, state.borneOff, winner);
var points = (winType === 'backgammon') ? 3 : (winType === 'gammon') ? 2 : 1;
points *= state.doublingCube.value;
state.scores[winner] += points;
state.phase = 'game_over';
var winLabel = (winType === 'backgammon') ? 'باكغمّون!' : (winType === 'gammon') ? 'غمّون!' : 'فوز!';
UI.showLog(state.players[winner].name + ' — ' + winLabel + ' (+' + points + ')');
UI.showResult(state.players[winner].name, winType, points);
UI.render(state);
if (state.onGameEnd) {
state.onGameEnd({ winner: winner, winType: winType, points: points, scores: state.scores });
}
}
function getState() {
return state;
}
return {
init: init,
rollDice: rollDice,
handlePointClick: handlePointClick,
getState: getState
};
})();
var BackgammonLive = (function() {
'use strict';
var C = BackgammonConstants;
var UI = BackgammonUI;
var WS_URL = 'wss://safe-supabase-kong.caprover.al-arcade.com/realtime/v1/websocket';
var ANON_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwiaWF0IjoxNzM1Njg5NjAwLCJleHAiOjE4OTM0NTYwMDB9.31PF6PvP-pSrvRuQwLFptQoejR0W1A7o53lZhEbnz84';
var token = null;
var user = null;
var matchId = null;
var ws = null;
var heartbeatTimer = null;
var heartbeatRef = 0;
var myPlayerIdx = -1;
var topic = null;
var reconnectAttempts = 0;
function init(opts) {
token = localStorage.getItem('el3ab_token');
user = JSON.parse(localStorage.getItem('el3ab_user') || 'null');
if (!token || !user) { window.location.href = '/login'; return; }
if (opts.matchId) {
matchId = opts.matchId;
connectAndJoin();
} else if (opts.action === 'create') {
createRoom();
} else if (opts.action === 'join' && opts.code) {
joinRoom(opts.code);
}
}
function apiCall(body) {
return fetch('/api/backgammon.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token },
body: JSON.stringify(body)
}).then(function(r) { return r.json(); });
}
function createRoom() {
apiCall({ action: 'create' }).then(function(data) {
if (data.error) { alert(data.error); return; }
matchId = data.match_id;
showWaiting(data.room_code);
connectAndJoin();
});
}
function joinRoom(code) {
apiCall({ action: 'join', room_code: code }).then(function(data) {
if (data.error) { alert(data.error); return; }
matchId = data.match_id;
connectAndJoin();
});
}
function showWaiting(code) {
var codeEl = document.getElementById('bg-room-code');
var msgEl = document.getElementById('bg-waiting-msg');
if (codeEl) codeEl.textContent = code || '------';
if (msgEl) msgEl.textContent = 'في انتظار اللاعب الآخر...';
}
function connectAndJoin() {
topic = 'realtime:public:backgammon_matches:id=eq.' + matchId;
ws = new WebSocket(WS_URL + '?apikey=' + ANON_KEY + '&vsn=1.0.0');
ws.onopen = function() {
reconnectAttempts = 0;
startHeartbeat();
joinChannel();
fetchStatus();
};
ws.onmessage = function(evt) {
var msg = JSON.parse(evt.data);
if (msg.event === 'postgres_changes') {
var record = msg.payload && msg.payload.data && msg.payload.data.record;
if (record) handleUpdate(record);
}
};
ws.onclose = function() {
stopHeartbeat();
if (reconnectAttempts < 10) {
var delay = Math.min(1000 * Math.pow(2, reconnectAttempts), 30000);
reconnectAttempts++;
setTimeout(connectAndJoin, delay);
}
};
ws.onerror = function() { ws.close(); };
}
function joinChannel() {
send({
topic: topic,
event: 'phx_join',
payload: {
config: {
broadcast: { self: false },
postgres_changes: [{
event: 'UPDATE',
schema: 'public',
table: 'backgammon_matches',
filter: 'id=eq.' + matchId
}]
},
access_token: token
},
ref: String(++heartbeatRef)
});
}
function send(msg) {
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify(msg));
}
}
function startHeartbeat() {
heartbeatTimer = setInterval(function() {
send({ topic: 'phoenix', event: 'heartbeat', payload: {}, ref: String(++heartbeatRef) });
}, 30000);
}
function stopHeartbeat() {
if (heartbeatTimer) { clearInterval(heartbeatTimer); heartbeatTimer = null; }
}
function fetchStatus() {
fetch('/api/backgammon.php?action=status&match_id=' + matchId, {
headers: { 'Authorization': 'Bearer ' + token }
}).then(function(r) { return r.json(); }).then(function(data) {
if (data.match) handleUpdate(data.match);
});
}
function handleUpdate(match) {
var players = typeof match.players === 'string' ? JSON.parse(match.players) : match.players;
var board = typeof match.board === 'string' ? JSON.parse(match.board) : match.board;
var bar = typeof match.bar === 'string' ? JSON.parse(match.bar) : match.bar;
var borneOff = typeof match.borne_off === 'string' ? JSON.parse(match.borne_off) : match.borne_off;
var dice = match.dice ? (typeof match.dice === 'string' ? JSON.parse(match.dice) : match.dice) : null;
var diceRemaining = typeof match.dice_remaining === 'string' ? JSON.parse(match.dice_remaining) : (match.dice_remaining || []);
var gameState = typeof match.game_state === 'string' ? JSON.parse(match.game_state) : (match.game_state || {});
var scores = typeof match.scores === 'string' ? JSON.parse(match.scores) : (match.scores || [0,0]);
myPlayerIdx = -1;
for (var i = 0; i < players.length; i++) {
if (players[i].id === user.id) { myPlayerIdx = i; break; }
}
if (match.status === 'waiting') {
showWaiting(match.room_code);
var countEl = document.getElementById('bg-players-count');
if (countEl) countEl.textContent = players.length + ' / 2 لاعبين';
var startBtn = document.getElementById('bg-start-btn');
if (startBtn && players.length >= 2 && match.host_id === user.id) {
startBtn.style.display = '';
startBtn.onclick = function() {
apiCall({ action: 'start', match_id: matchId });
};
}
return;
}
if (match.status === 'in_progress' || match.status === 'completed') {
document.getElementById('bg-waiting').style.display = 'none';
document.getElementById('bg-game-area').style.display = '';
var state = {
players: players,
board: board,
bar: bar,
borneOff: borneOff,
currentTurn: match.current_turn,
dice: dice,
diceRemaining: diceRemaining,
phase: gameState.phase || 'awaiting_roll',
scores: scores,
doublingCube: typeof match.doubling_cube === 'string' ? JSON.parse(match.doubling_cube) : (match.doubling_cube || {value:1,owner:null}),
matchLength: match.match_length || 1
};
if (!UI.getSelected) UI.init(state);
UI.render(state);
var nameEl0 = document.getElementById('bg-name-0');
var nameEl1 = document.getElementById('bg-name-1');
if (nameEl0 && players[0]) nameEl0.textContent = players[0].name;
if (nameEl1 && players[1]) nameEl1.textContent = players[1].name;
setupControls(state);
if (match.status === 'completed' && gameState.winner !== undefined) {
var winType = gameState.win_type || 'normal';
var pts = gameState.points_awarded || 1;
UI.showResult(players[gameState.winner].name, winType, pts);
}
}
}
function setupControls(state) {
var rollBtn = document.getElementById('bg-roll-btn');
if (!rollBtn) return;
var isMyTurn = state.currentTurn === myPlayerIdx;
var canRoll = isMyTurn && state.phase === 'awaiting_roll';
rollBtn.style.display = canRoll ? '' : 'none';
rollBtn.onclick = function() {
apiCall({ action: 'roll', match_id: matchId });
};
var board = document.getElementById('bg-board');
if (board && isMyTurn && state.phase === 'moving') {
board.onclick = function(e) {
var point = e.target.closest('[data-point]');
var action = e.target.closest('[data-action]');
var from = point ? parseInt(point.dataset.point) : (action ? action.dataset.action : null);
if (from === null) return;
var sel = UI.getSelected();
if (sel === null) {
UI.setSelected(from);
UI.render(state);
} else {
var to = from;
apiCall({ action: 'move', match_id: matchId, from: sel, to: to }).then(function(data) {
if (data.error) { UI.showLog(data.error); }
UI.clearSelected();
UI.clearHighlights();
});
}
};
}
}
return { init: init };
})();
var BackgammonUI = (function() {
'use strict';
var C = BackgammonConstants;
var selected = null;
var highlights = [];
var gameState = null;
function init(state) {
gameState = state;
var board = document.getElementById('bg-board');
if (!board) return;
board.innerHTML = buildBoardHTML();
bindEvents();
}
function buildBoardHTML() {
var html = '';
html += '<div class="bg-board-inner">';
html += '<div class="bg-half bg-half--top">';
html += '<div class="bg-quadrant bg-quadrant--top-left">';
for (var i = 12; i <= 17; i++) {
html += '<div class="bg-point bg-point--top" data-point="' + i + '"></div>';
}
html += '</div>';
html += '<div class="bg-bar-area bg-bar--top" data-action="bar"></div>';
html += '<div class="bg-quadrant bg-quadrant--top-right">';
for (var i = 18; i <= 23; i++) {
html += '<div class="bg-point bg-point--top" data-point="' + i + '"></div>';
}
html += '</div>';
html += '</div>';
html += '<div class="bg-half bg-half--bottom">';
html += '<div class="bg-quadrant bg-quadrant--bottom-left">';
for (var i = 11; i >= 6; i--) {
html += '<div class="bg-point bg-point--bottom" data-point="' + i + '"></div>';
}
html += '</div>';
html += '<div class="bg-bar-area bg-bar--bottom" data-action="bar"></div>';
html += '<div class="bg-quadrant bg-quadrant--bottom-right">';
for (var i = 5; i >= 0; i--) {
html += '<div class="bg-point bg-point--bottom" data-point="' + i + '"></div>';
}
html += '</div>';
html += '</div>';
html += '</div>';
html += '<div class="bg-borne-off-area">';
html += '<div class="bg-borne-off bg-borne-off--black" data-action="off"></div>';
html += '<div class="bg-borne-off bg-borne-off--white" data-action="off"></div>';
html += '</div>';
return html;
}
function bindEvents() {
var board = document.getElementById('bg-board');
if (!board) return;
board.addEventListener('click', function(e) {
var point = e.target.closest('[data-point]');
var action = e.target.closest('[data-action]');
if (point) {
BackgammonGame.handlePointClick(point.dataset.point);
} else if (action) {
BackgammonGame.handlePointClick(action.dataset.action);
}
});
var rollBtn = document.getElementById('bg-roll-btn');
if (rollBtn) {
rollBtn.addEventListener('click', function() {
BackgammonGame.rollDice();
});
}
}
function render(state) {
gameState = state;
renderCheckers(state);
renderBar(state);
renderBorneOff(state);
renderDice(state);
renderTurnIndicator(state);
renderControls(state);
renderPipCount(state);
}
function renderCheckers(state) {
var points = document.querySelectorAll('.bg-point');
points.forEach(function(el) {
var idx = parseInt(el.dataset.point);
var count = state.board[idx];
var absCount = Math.abs(count);
var player = (count > 0) ? 0 : (count < 0) ? 1 : -1;
var checkersHTML = '';
var display = Math.min(absCount, 5);
for (var c = 0; c < display; c++) {
var cls = 'bg-checker bg-checker--' + (player === 0 ? 'white' : 'black');
if (selected === idx && c === display - 1) cls += ' bg-checker--selected';
checkersHTML += '<div class="' + cls + '"></div>';
}
if (absCount > 5) {
checkersHTML += '<span class="bg-checker-count">' + absCount + '</span>';
}
el.innerHTML = checkersHTML;
el.classList.remove('bg-point--valid', 'bg-point--selected');
if (selected === idx) el.classList.add('bg-point--selected');
if (highlights.indexOf(idx) !== -1) el.classList.add('bg-point--valid');
});
}
function renderBar(state) {
var barTop = document.querySelector('.bg-bar--top');
var barBottom = document.querySelector('.bg-bar--bottom');
if (!barTop || !barBottom) return;
var blackBar = state.bar[1];
var whiteBar = state.bar[0];
barTop.innerHTML = buildBarCheckers(blackBar, 'black', selected === 'bar' && state.currentTurn === 1);
barBottom.innerHTML = buildBarCheckers(whiteBar, 'white', selected === 'bar' && state.currentTurn === 0);
}
function buildBarCheckers(count, color, isSelected) {
var html = '';
var display = Math.min(count, 4);
for (var i = 0; i < display; i++) {
var cls = 'bg-checker bg-checker--' + color;
if (isSelected && i === display - 1) cls += ' bg-checker--selected';
html += '<div class="' + cls + '"></div>';
}
if (count > 4) html += '<span class="bg-checker-count">' + count + '</span>';
return html;
}
function renderBorneOff(state) {
var whiteOff = document.querySelector('.bg-borne-off--white');
var blackOff = document.querySelector('.bg-borne-off--black');
if (!whiteOff || !blackOff) return;
whiteOff.innerHTML = buildBorneOff(state.borneOff[0], 'white');
blackOff.innerHTML = buildBorneOff(state.borneOff[1], 'black');
whiteOff.classList.toggle('bg-point--valid', highlights.indexOf('off') !== -1 && state.currentTurn === 0);
blackOff.classList.toggle('bg-point--valid', highlights.indexOf('off') !== -1 && state.currentTurn === 1);
}
function buildBorneOff(count, color) {
var html = '<div class="bg-borne-off-stack">';
for (var i = 0; i < count; i++) {
html += '<div class="bg-borne-chip bg-borne-chip--' + color + '"></div>';
}
html += '</div>';
if (count > 0) html += '<span class="bg-borne-count">' + count + '/15</span>';
return html;
}
function renderDice(state) {
var container = document.getElementById('bg-dice-container');
if (!container) return;
if (!state.dice) {
container.innerHTML = '';
return;
}
var html = '<div class="bg-dice-pair">';
for (var d = 0; d < 2; d++) {
var val = state.dice[d];
var used = true;
for (var r = 0; r < state.diceRemaining.length; r++) {
if (state.diceRemaining[r] === val) { used = false; break; }
}
html += renderSingleDie(val, used);
}
html += '</div>';
container.innerHTML = html;
}
function renderSingleDie(value, used) {
var cls = 'bg-die' + (used ? ' bg-die--used' : '');
var dots = C.DICE_FACES[value] || [];
var html = '<div class="' + cls + '">';
for (var i = 0; i < 9; i++) {
var visible = dots.indexOf(i) !== -1;
html += '<span class="bg-die-dot' + (visible ? ' bg-die-dot--visible' : '') + '"></span>';
}
html += '</div>';
return html;
}
function renderTurnIndicator(state) {
var el = document.getElementById('bg-turn');
if (!el) return;
var player = state.players[state.currentTurn];
var color = state.currentTurn === 0 ? 'white' : 'black';
el.innerHTML = '<div class="bg-turn-card">' +
'<span class="bg-turn-chip bg-turn-chip--' + color + '"></span>' +
'<span class="bg-turn-name">' + player.name + '</span>' +
'</div>';
}
function renderControls(state) {
var rollBtn = document.getElementById('bg-roll-btn');
if (!rollBtn) return;
var isHumanTurn = state.players[state.currentTurn].type === 'human';
var canRoll = state.phase === 'awaiting_roll' && isHumanTurn;
rollBtn.style.display = canRoll ? '' : 'none';
rollBtn.disabled = !canRoll;
}
function renderPipCount(state) {
var el0 = document.getElementById('bg-pip-0');
var el1 = document.getElementById('bg-pip-1');
if (el0) el0.textContent = C.pipCount(state.board, state.bar, 0);
if (el1) el1.textContent = C.pipCount(state.board, state.bar, 1);
}
function setSelected(val) {
selected = val;
}
function getSelected() {
return selected;
}
function clearSelected() {
selected = null;
var points = document.querySelectorAll('.bg-point--selected');
points.forEach(function(el) { el.classList.remove('bg-point--selected'); });
}
function highlightPoints(targets) {
highlights = targets;
var points = document.querySelectorAll('.bg-point');
points.forEach(function(el) {
var idx = parseInt(el.dataset.point);
el.classList.toggle('bg-point--valid', targets.indexOf(idx) !== -1);
});
var borneOffs = document.querySelectorAll('.bg-borne-off');
borneOffs.forEach(function(el) {
el.classList.toggle('bg-point--valid', targets.indexOf('off') !== -1);
});
}
function clearHighlights() {
highlights = [];
var els = document.querySelectorAll('.bg-point--valid');
els.forEach(function(el) { el.classList.remove('bg-point--valid'); });
}
function showLog(msg) {
var log = document.getElementById('bg-log');
if (!log) return;
var entry = document.createElement('div');
entry.className = 'bg-log-entry';
entry.textContent = msg;
log.prepend(entry);
if (log.children.length > 20) log.removeChild(log.lastChild);
}
function showResult(winnerName, winType, points) {
var overlay = document.getElementById('bg-result-overlay');
if (!overlay) return;
var label = (winType === 'backgammon') ? 'باكغمّون!' : (winType === 'gammon') ? 'غمّون!' : 'فوز عادي';
overlay.innerHTML = '<div class="bg-result-card">' +
'<h2>' + winnerName + '</h2>' +
'<p class="bg-result-type">' + label + '</p>' +
'<p class="bg-result-points">+' + points + ' نقاط</p>' +
'</div>';
overlay.style.display = 'flex';
}
return {
init: init,
render: render,
setSelected: setSelected,
getSelected: getSelected,
clearSelected: clearSelected,
highlightPoints: highlightPoints,
clearHighlights: clearHighlights,
showLog: showLog,
showResult: showResult
};
})();
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