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
<?php
require_once __DIR__ . '/../config/database.php';
header('Content-Type: application/json');
if (!check_feature_flag('backgammon_enabled')) {
http_response_code(403);
echo json_encode(['error' => 'feature_disabled', 'message' => 'لعبة الطاولة معطلة حالياً']);
exit;
}
$token = get_auth_token();
if (!$token) {
http_response_code(401);
echo json_encode(['error' => 'unauthorized']);
exit;
}
$userRes = supabase_auth('user', [], $token, 'GET');
$userId = $userRes['data']['id'] ?? null;
$userName = $userRes['data']['user_metadata']['display_name'] ?? 'لاعب';
if (!$userId) {
http_response_code(401);
echo json_encode(['error' => 'invalid_token']);
exit;
}
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
$action = $_GET['action'] ?? '';
handleGet($action, $userId, $token);
} elseif ($_SERVER['REQUEST_METHOD'] === 'POST') {
$input = json_decode(file_get_contents('php://input'), true);
$action = $input['action'] ?? '';
handlePost($action, $input, $userId, $userName, $token);
} else {
http_response_code(405);
echo json_encode(['error' => 'method_not_allowed']);
}
// ─── GET Handlers ───────────────────────────────────────────────────────────
function handleGet($action, $userId, $token) {
switch ($action) {
case 'status':
$matchId = $_GET['match_id'] ?? '';
if (!$matchId) {
http_response_code(400);
echo json_encode(['error' => 'match_id_required']);
return;
}
$res = supabase_rest('GET', "backgammon_matches?id=eq.{$matchId}&select=*", [], SUPABASE_SERVICE_KEY);
if (empty($res['data'])) {
http_response_code(404);
echo json_encode(['error' => 'not_found']);
return;
}
$match = $res['data'][0];
echo json_encode(['ok' => true, 'match' => $match]);
break;
default:
http_response_code(400);
echo json_encode(['error' => 'invalid_action']);
}
}
// ─── POST Handlers ──────────────────────────────────────────────────────────
function handlePost($action, $input, $userId, $userName, $token) {
switch ($action) {
case 'create':
createRoom($input, $userId, $userName);
break;
case 'join':
joinRoom($input, $userId, $userName);
break;
case 'start':
startGame($input, $userId);
break;
case 'roll':
rollDice($input, $userId);
break;
case 'move':
makeMove($input, $userId);
break;
case 'double':
proposeDouble($input, $userId);
break;
case 'accept_double':
acceptDouble($input, $userId);
break;
case 'decline_double':
declineDouble($input, $userId);
break;
case 'leave':
leaveGame($input, $userId);
break;
case 'chat':
sendChat($input, $userId, $userName);
break;
case 'matchmake':
matchmake($input, $userId, $userName);
break;
default:
http_response_code(400);
echo json_encode(['error' => 'invalid_action']);
}
}
// ─── Constants ──────────────────────────────────────────────────────────────
function getInitialBoard() {
return [2,0,0,0,0,-5, 0,-3,0,0,0,5, -5,0,0,0,3,0, 5,0,0,0,0,-2];
}
// ─── Room Management ────────────────────────────────────────────────────────
function generateRoomCode() {
$chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
$code = '';
for ($i = 0; $i < 6; $i++) {
$code .= $chars[random_int(0, strlen($chars) - 1)];
}
return $code;
}
function createRoom($input, $userId, $userName) {
$matchLength = (int)($input['match_length'] ?? 1);
if ($matchLength < 1) $matchLength = 1;
$bots = $input['bots'] ?? [];
$roomCode = generateRoomCode();
$players = [
['id' => $userId, 'name' => $userName, 'type' => 'human']
];
foreach ($bots as $bot) {
if (count($players) >= 2) break;
$players[] = [
'id' => 'bot_0',
'name' => $bot['name'] ?? 'بوت',
'type' => 'bot',
'difficulty' => $bot['difficulty'] ?? 'medium'
];
}
$status = count($players) >= 2 ? 'ready' : 'waiting';
$data = [
'room_code' => $roomCode,
'status' => $status,
'players' => json_encode($players),
'current_turn' => 0,
'board' => json_encode(getInitialBoard()),
'dice' => null,
'dice_remaining' => json_encode([]),
'bar' => json_encode([0, 0]),
'borne_off' => json_encode([0, 0]),
'doubling_cube' => json_encode(['value' => 1, 'owner' => null]),
'game_state' => json_encode(['phase' => 'waiting']),
'moves' => json_encode([]),
'scores' => json_encode([0, 0]),
'match_length' => $matchLength,
'chat' => json_encode([]),
'host_id' => $userId,
'result' => null
];
$res = supabase_rest('POST', 'backgammon_matches', $data, SUPABASE_SERVICE_KEY);
if ($res['status'] >= 400) {
http_response_code(500);
echo json_encode(['error' => 'create_failed', 'details' => $res['data']]);
return;
}
echo json_encode(['ok' => true, 'match' => $res['data'][0], 'room_code' => $roomCode]);
}
function joinRoom($input, $userId, $userName) {
$roomCode = strtoupper(trim($input['room_code'] ?? ''));
if (!$roomCode) {
http_response_code(400);
echo json_encode(['error' => 'room_code_required']);
return;
}
$res = supabase_rest('GET', "backgammon_matches?room_code=eq.{$roomCode}&status=eq.waiting&select=*", [], SUPABASE_SERVICE_KEY);
if (empty($res['data'])) {
http_response_code(404);
echo json_encode(['error' => 'room_not_found']);
return;
}
$match = $res['data'][0];
$players = json_decode($match['players'], true);
foreach ($players as $p) {
if ($p['id'] === $userId) {
echo json_encode(['ok' => true, 'match' => $match, 'already_joined' => true]);
return;
}
}
if (count($players) >= 2) {
http_response_code(400);
echo json_encode(['error' => 'room_full']);
return;
}
$players[] = [
'id' => $userId,
'name' => $userName,
'type' => 'human'
];
$update = [
'players' => json_encode($players),
'status' => 'ready'
];
$patchRes = supabase_rest('PATCH', "backgammon_matches?id=eq.{$match['id']}", $update, SUPABASE_SERVICE_KEY);
echo json_encode(['ok' => true, 'match' => $patchRes['data'][0]]);
}
// ─── Start Game (Opening Roll) ──────────────────────────────────────────────
function startGame($input, $userId) {
$matchId = $input['match_id'] ?? '';
if (!$matchId) {
http_response_code(400);
echo json_encode(['error' => 'match_id_required']);
return;
}
$res = supabase_rest('GET', "backgammon_matches?id=eq.{$matchId}&select=*", [], SUPABASE_SERVICE_KEY);
if (empty($res['data'])) {
http_response_code(404);
echo json_encode(['error' => 'not_found']);
return;
}
$match = $res['data'][0];
if ($match['host_id'] !== $userId) {
http_response_code(403);
echo json_encode(['error' => 'not_host']);
return;
}
if ($match['status'] !== 'ready' && $match['status'] !== 'waiting') {
http_response_code(400);
echo json_encode(['error' => 'already_started']);
return;
}
$players = json_decode($match['players'], true);
// Fill remaining slot with bot if needed
if (count($players) < 2) {
$players[] = [
'id' => 'bot_0',
'name' => 'بوت',
'type' => 'bot',
'difficulty' => 'medium'
];
}
// Opening roll: each player rolls 1 die, higher goes first
$d1 = 0;
$d2 = 0;
while ($d1 === $d2) {
$d1 = random_int(1, 6);
$d2 = random_int(1, 6);
}
$firstPlayer = ($d1 > $d2) ? 0 : 1;
$dice = [$d1, $d2];
$diceRemaining = [$d1, $d2];
$update = [
'status' => 'in_progress',
'players' => json_encode($players),
'board' => json_encode(getInitialBoard()),
'bar' => json_encode([0, 0]),
'borne_off' => json_encode([0, 0]),
'dice' => json_encode($dice),
'dice_remaining' => json_encode($diceRemaining),
'current_turn' => $firstPlayer,
'game_state' => json_encode(['phase' => 'moving', 'started_at' => date('c')]),
'doubling_cube' => json_encode(['value' => 1, 'owner' => null])
];
$patchRes = supabase_rest('PATCH', "backgammon_matches?id=eq.{$matchId}", $update, SUPABASE_SERVICE_KEY);
echo json_encode(['ok' => true, 'match' => $patchRes['data'][0], 'opening_roll' => ['d1' => $d1, 'd2' => $d2, 'first_player' => $firstPlayer]]);
// If first player is a bot, execute bot turns
if ($players[$firstPlayer]['type'] === 'bot') {
executeBotTurns($matchId);
}
}
// ─── Roll Dice ──────────────────────────────────────────────────────────────
function rollDice($input, $userId) {
$matchId = $input['match_id'] ?? '';
$match = getAndValidateMatch($matchId, $userId);
if (!$match) return;
$players = json_decode($match['players'], true);
$currentTurn = $match['current_turn'];
$gameState = json_decode($match['game_state'], true);
if ($players[$currentTurn]['id'] !== $userId) {
http_response_code(400);
echo json_encode(['error' => 'not_your_turn']);
return;
}
if ($gameState['phase'] !== 'awaiting_roll') {
http_response_code(400);
echo json_encode(['error' => 'not_roll_phase', 'phase' => $gameState['phase']]);
return;
}
$d1 = random_int(1, 6);
$d2 = random_int(1, 6);
$dice = [$d1, $d2];
// Doubles = 4 moves
if ($d1 === $d2) {
$diceRemaining = [$d1, $d1, $d1, $d1];
} else {
$diceRemaining = [$d1, $d2];
}
$board = json_decode($match['board'], true);
$bar = json_decode($match['bar'], true);
$borneOff = json_decode($match['borne_off'], true);
// Check if any valid moves exist
$hasValidMoves = playerHasAnyValidMove($board, $bar, $borneOff, $currentTurn, $diceRemaining);
if (!$hasValidMoves) {
// Auto-pass: no valid moves
$nextTurn = 1 - $currentTurn;
$gameState['phase'] = 'awaiting_roll';
$moves = json_decode($match['moves'], true);
$moves[] = [
'player' => $currentTurn,
'action' => 'roll',
'dice' => $dice,
'at' => date('c')
];
$moves[] = [
'player' => $currentTurn,
'action' => 'no_moves',
'at' => date('c')
];
$update = [
'dice' => json_encode($dice),
'dice_remaining' => json_encode([]),
'current_turn' => $nextTurn,
'game_state' => json_encode($gameState),
'moves' => json_encode($moves)
];
$patchRes = supabase_rest('PATCH', "backgammon_matches?id=eq.{$matchId}", $update, SUPABASE_SERVICE_KEY);
echo json_encode(['ok' => true, 'match' => $patchRes['data'][0], 'no_moves' => true]);
if ($players[$nextTurn]['type'] === 'bot') {
executeBotTurns($matchId);
}
return;
}
$gameState['phase'] = 'moving';
$moves = json_decode($match['moves'], true);
$moves[] = [
'player' => $currentTurn,
'action' => 'roll',
'dice' => $dice,
'at' => date('c')
];
$update = [
'dice' => json_encode($dice),
'dice_remaining' => json_encode($diceRemaining),
'game_state' => json_encode($gameState),
'moves' => json_encode($moves)
];
$patchRes = supabase_rest('PATCH', "backgammon_matches?id=eq.{$matchId}", $update, SUPABASE_SERVICE_KEY);
echo json_encode(['ok' => true, 'match' => $patchRes['data'][0]]);
}
// ─── Make Move ──────────────────────────────────────────────────────────────
function makeMove($input, $userId) {
$matchId = $input['match_id'] ?? '';
$from = $input['from'] ?? null;
$to = $input['to'] ?? null;
$match = getAndValidateMatch($matchId, $userId);
if (!$match) return;
$players = json_decode($match['players'], true);
$currentTurn = $match['current_turn'];
$gameState = json_decode($match['game_state'], true);
if ($players[$currentTurn]['id'] !== $userId) {
http_response_code(400);
echo json_encode(['error' => 'not_your_turn']);
return;
}
if ($gameState['phase'] !== 'moving') {
http_response_code(400);
echo json_encode(['error' => 'not_moving_phase', 'phase' => $gameState['phase']]);
return;
}
$board = json_decode($match['board'], true);
$bar = json_decode($match['bar'], true);
$borneOff = json_decode($match['borne_off'], true);
$diceRemaining = json_decode($match['dice_remaining'], true);
$moves = json_decode($match['moves'], true);
if (empty($diceRemaining)) {
http_response_code(400);
echo json_encode(['error' => 'no_dice_remaining']);
return;
}
// Validate and execute the move
$moveResult = validateAndExecuteMove($board, $bar, $borneOff, $diceRemaining, $currentTurn, $from, $to);
if (isset($moveResult['error'])) {
http_response_code(400);
echo json_encode(['error' => $moveResult['error']]);
return;
}
$board = $moveResult['board'];
$bar = $moveResult['bar'];
$borneOff = $moveResult['borne_off'];
$diceRemaining = $moveResult['dice_remaining'];
$moves[] = [
'player' => $currentTurn,
'action' => 'move',
'from' => $from,
'to' => $to,
'die_used' => $moveResult['die_used'],
'hit' => $moveResult['hit'],
'at' => date('c')
];
// Check for win
if ($borneOff[$currentTurn] >= 15) {
return handleWin($match, $matchId, $board, $bar, $borneOff, $diceRemaining, $moves, $gameState, $players, $currentTurn, $userId);
}
// Check if dice_remaining empty or no more valid moves → switch turn
$turnOver = false;
if (empty($diceRemaining)) {
$turnOver = true;
} else {
$hasMore = playerHasAnyValidMove($board, $bar, $borneOff, $currentTurn, $diceRemaining);
if (!$hasMore) {
$turnOver = true;
}
}
if ($turnOver) {
$nextTurn = 1 - $currentTurn;
$gameState['phase'] = 'awaiting_roll';
$update = [
'board' => json_encode($board),
'bar' => json_encode($bar),
'borne_off' => json_encode($borneOff),
'dice_remaining' => json_encode([]),
'current_turn' => $nextTurn,
'game_state' => json_encode($gameState),
'moves' => json_encode($moves)
];
$patchRes = supabase_rest('PATCH', "backgammon_matches?id=eq.{$matchId}", $update, SUPABASE_SERVICE_KEY);
echo json_encode(['ok' => true, 'match' => $patchRes['data'][0], 'turn_over' => true]);
if ($players[$nextTurn]['type'] === 'bot') {
executeBotTurns($matchId);
}
} else {
$update = [
'board' => json_encode($board),
'bar' => json_encode($bar),
'borne_off' => json_encode($borneOff),
'dice_remaining' => json_encode($diceRemaining),
'moves' => json_encode($moves)
];
$patchRes = supabase_rest('PATCH', "backgammon_matches?id=eq.{$matchId}", $update, SUPABASE_SERVICE_KEY);
echo json_encode(['ok' => true, 'match' => $patchRes['data'][0], 'turn_over' => false]);
}
}
// ─── Doubling Cube ──────────────────────────────────────────────────────────
function proposeDouble($input, $userId) {
$matchId = $input['match_id'] ?? '';
$match = getAndValidateMatch($matchId, $userId);
if (!$match) return;
$players = json_decode($match['players'], true);
$currentTurn = $match['current_turn'];
$gameState = json_decode($match['game_state'], true);
$doublingCube = json_decode($match['doubling_cube'], true);
if ($players[$currentTurn]['id'] !== $userId) {
http_response_code(400);
echo json_encode(['error' => 'not_your_turn']);
return;
}
if ($gameState['phase'] !== 'awaiting_roll') {
http_response_code(400);
echo json_encode(['error' => 'can_only_double_before_rolling']);
return;
}
// Check cube ownership: only the player who doesn't own the cube (or if centered) can double
if ($doublingCube['owner'] !== null && $doublingCube['owner'] !== $currentTurn) {
http_response_code(400);
echo json_encode(['error' => 'not_cube_owner']);
return;
}
$gameState['phase'] = 'doubling';
$gameState['doubler'] = $currentTurn;
$moves = json_decode($match['moves'], true);
$moves[] = [
'player' => $currentTurn,
'action' => 'double',
'cube_value' => $doublingCube['value'] * 2,
'at' => date('c')
];
$update = [
'game_state' => json_encode($gameState),
'moves' => json_encode($moves)
];
$patchRes = supabase_rest('PATCH', "backgammon_matches?id=eq.{$matchId}", $update, SUPABASE_SERVICE_KEY);
echo json_encode(['ok' => true, 'match' => $patchRes['data'][0]]);
// If opponent is bot, auto-decide
$opponent = 1 - $currentTurn;
if ($players[$opponent]['type'] === 'bot') {
botRespondToDouble($matchId, $players[$opponent]['difficulty'] ?? 'medium');
}
}
function acceptDouble($input, $userId) {
$matchId = $input['match_id'] ?? '';
$match = getAndValidateMatch($matchId, $userId);
if (!$match) return;
$players = json_decode($match['players'], true);
$currentTurn = $match['current_turn'];
$gameState = json_decode($match['game_state'], true);
$doublingCube = json_decode($match['doubling_cube'], true);
if ($gameState['phase'] !== 'doubling') {
http_response_code(400);
echo json_encode(['error' => 'not_doubling_phase']);
return;
}
$opponent = 1 - $currentTurn;
if ($players[$opponent]['id'] !== $userId) {
http_response_code(400);
echo json_encode(['error' => 'not_the_responder']);
return;
}
$doublingCube['value'] *= 2;
$doublingCube['owner'] = $opponent;
$gameState['phase'] = 'awaiting_roll';
unset($gameState['doubler']);
$moves = json_decode($match['moves'], true);
$moves[] = [
'player' => $opponent,
'action' => 'accept_double',
'cube_value' => $doublingCube['value'],
'at' => date('c')
];
$update = [
'doubling_cube' => json_encode($doublingCube),
'game_state' => json_encode($gameState),
'moves' => json_encode($moves)
];
$patchRes = supabase_rest('PATCH', "backgammon_matches?id=eq.{$matchId}", $update, SUPABASE_SERVICE_KEY);
echo json_encode(['ok' => true, 'match' => $patchRes['data'][0]]);
}
function declineDouble($input, $userId) {
$matchId = $input['match_id'] ?? '';
$match = getAndValidateMatch($matchId, $userId);
if (!$match) return;
$players = json_decode($match['players'], true);
$currentTurn = $match['current_turn'];
$gameState = json_decode($match['game_state'], true);
$doublingCube = json_decode($match['doubling_cube'], true);
if ($gameState['phase'] !== 'doubling') {
http_response_code(400);
echo json_encode(['error' => 'not_doubling_phase']);
return;
}
$opponent = 1 - $currentTurn;
if ($players[$opponent]['id'] !== $userId) {
http_response_code(400);
echo json_encode(['error' => 'not_the_responder']);
return;
}
// Decliner loses current cube value
$scores = json_decode($match['scores'], true);
$scores[$currentTurn] += $doublingCube['value'];
$moves = json_decode($match['moves'], true);
$moves[] = [
'player' => $opponent,
'action' => 'decline_double',
'at' => date('c')
];
$matchLength = $match['match_length'];
// Check if match is over
if ($scores[$currentTurn] >= $matchLength) {
$gameState['phase'] = 'match_over';
$update = [
'scores' => json_encode($scores),
'game_state' => json_encode($gameState),
'moves' => json_encode($moves),
'status' => 'completed',
'completed_at' => date('c'),
'result' => "player_{$currentTurn}_wins"
];
$patchRes = supabase_rest('PATCH', "backgammon_matches?id=eq.{$matchId}", $update, SUPABASE_SERVICE_KEY);
echo json_encode(['ok' => true, 'match' => $patchRes['data'][0], 'match_over' => true, 'winner' => $currentTurn]);
return;
}
// Start new game in match
startNewGame($matchId, $scores, $moves, $players, $gameState);
}
// ─── Move Validation & Execution ────────────────────────────────────────────
function validateAndExecuteMove(&$board, &$bar, &$borneOff, &$diceRemaining, $player, $from, $to) {
$direction = ($player === 0) ? -1 : 1; // Player 0: high→low, Player 1: low→high
// Determine which die is used
$dieUsed = null;
if ($from === 'bar') {
// Bar entry
if ($bar[$player] <= 0) {
return ['error' => 'no_checker_on_bar'];
}
if ($to === 'off') {
return ['error' => 'cannot_bear_off_from_bar'];
}
$toPoint = (int)$to;
// Calculate required die
if ($player === 0) {
// Enters at points 18-23, die d → point 24-d
$dieNeeded = 24 - $toPoint;
} else {
// Enters at points 0-5, die d → point d-1
$dieNeeded = $toPoint + 1;
}
if (!in_array($dieNeeded, $diceRemaining)) {
return ['error' => 'invalid_die_for_bar_entry'];
}
// Check target not blocked
if (isPointBlocked($board, $toPoint, $player)) {
return ['error' => 'point_blocked'];
}
$dieUsed = $dieNeeded;
$hit = false;
// Execute: remove from bar
$bar[$player]--;
// Check for hit
if (isOpponentBlot($board, $toPoint, $player)) {
$hit = true;
$opponent = 1 - $player;
$bar[$opponent]++;
$board[$toPoint] = 0;
}
// Place checker
if ($player === 0) {
$board[$toPoint]++;
} else {
$board[$toPoint]--;
}
// Remove die from remaining
$idx = array_search($dieUsed, $diceRemaining);
array_splice($diceRemaining, $idx, 1);
return ['board' => $board, 'bar' => $bar, 'borne_off' => $borneOff, 'dice_remaining' => $diceRemaining, 'die_used' => $dieUsed, 'hit' => $hit];
}
// Must enter from bar first
if ($bar[$player] > 0) {
return ['error' => 'must_enter_from_bar_first'];
}
$fromPoint = (int)$from;
// Check player has checker at from
if (!hasCheckerAt($board, $fromPoint, $player)) {
return ['error' => 'no_checker_at_point'];
}
if ($to === 'off') {
// Bearing off
if (!canBearOff($board, $bar, $player)) {
return ['error' => 'cannot_bear_off_yet'];
}
// Calculate die needed for bearing off
if ($player === 0) {
$dieNeeded = $fromPoint + 1; // Point 0 needs die 1, point 5 needs die 6
} else {
$dieNeeded = 24 - $fromPoint; // Point 23 needs die 1, point 18 needs die 6
}
// Exact die or higher die if no checker on higher point
if (in_array($dieNeeded, $diceRemaining)) {
$dieUsed = $dieNeeded;
} else {
// Try higher die — only valid if no checker on a higher point
$higherDice = array_filter($diceRemaining, fn($d) => $d > $dieNeeded);
if (!empty($higherDice) && !hasCheckerOnHigherPoint($board, $fromPoint, $player)) {
sort($higherDice);
$dieUsed = $higherDice[0];
} else {
return ['error' => 'no_valid_die_for_bear_off'];
}
}
// Execute bear off
if ($player === 0) {
$board[$fromPoint]--;
} else {
$board[$fromPoint]++;
}
$borneOff[$player]++;
$idx = array_search($dieUsed, $diceRemaining);
array_splice($diceRemaining, $idx, 1);
return ['board' => $board, 'bar' => $bar, 'borne_off' => $borneOff, 'dice_remaining' => $diceRemaining, 'die_used' => $dieUsed, 'hit' => false];
}
// Normal move
$toPoint = (int)$to;
// Calculate die needed
if ($player === 0) {
$dieNeeded = $fromPoint - $toPoint;
} else {
$dieNeeded = $toPoint - $fromPoint;
}
if ($dieNeeded <= 0) {
return ['error' => 'invalid_direction'];
}
if (!in_array($dieNeeded, $diceRemaining)) {
return ['error' => 'no_matching_die'];
}
// Check target not blocked
if (isPointBlocked($board, $toPoint, $player)) {
return ['error' => 'point_blocked'];
}
$dieUsed = $dieNeeded;
$hit = false;
// Execute
if ($player === 0) {
$board[$fromPoint]--;
} else {
$board[$fromPoint]++;
}
// Check for hit
if (isOpponentBlot($board, $toPoint, $player)) {
$hit = true;
$opponent = 1 - $player;
$bar[$opponent]++;
$board[$toPoint] = 0;
}
if ($player === 0) {
$board[$toPoint]++;
} else {
$board[$toPoint]--;
}
$idx = array_search($dieUsed, $diceRemaining);
array_splice($diceRemaining, $idx, 1);
return ['board' => $board, 'bar' => $bar, 'borne_off' => $borneOff, 'dice_remaining' => $diceRemaining, 'die_used' => $dieUsed, 'hit' => $hit];
}
// ─── Board Helper Functions ─────────────────────────────────────────────────
function hasCheckerAt($board, $point, $player) {
if ($player === 0) {
return $board[$point] > 0;
} else {
return $board[$point] < 0;
}
}
function isPointBlocked($board, $point, $player) {
// A point is blocked if opponent has 2+ checkers
if ($player === 0) {
return $board[$point] <= -2;
} else {
return $board[$point] >= 2;
}
}
function isOpponentBlot($board, $point, $player) {
// Exactly 1 opponent checker
if ($player === 0) {
return $board[$point] === -1;
} else {
return $board[$point] === 1;
}
}
function canBearOff($board, $bar, $player) {
// All 15 checkers must be in home board
if ($bar[$player] > 0) return false;
if ($player === 0) {
// Home = points 0-5
for ($i = 6; $i <= 23; $i++) {
if ($board[$i] > 0) return false;
}
} else {
// Home = points 18-23
for ($i = 0; $i <= 17; $i++) {
if ($board[$i] < 0) return false;
}
}
return true;
}
function hasCheckerOnHigherPoint($board, $fromPoint, $player) {
// "Higher" means farther from bearing off
if ($player === 0) {
// Higher point index = farther from off for player 0
for ($i = $fromPoint + 1; $i <= 5; $i++) {
if ($board[$i] > 0) return true;
}
} else {
// Lower point index = farther from off for player 1
for ($i = $fromPoint - 1; $i >= 18; $i--) {
if ($board[$i] < 0) return true;
}
}
return false;
}
// ─── Valid Move Generation ──────────────────────────────────────────────────
function getValidMoves($board, $bar, $borneOff, $player, $dieValue) {
$moves = [];
// If player has checkers on bar, must enter first
if ($bar[$player] > 0) {
if ($player === 0) {
$target = 24 - $dieValue; // Points 18-23
} else {
$target = $dieValue - 1; // Points 0-5
}
if ($target >= 0 && $target <= 23 && !isPointBlocked($board, $target, $player)) {
$moves[] = ['from' => 'bar', 'to' => $target];
}
return $moves;
}
$canBear = canBearOff($board, $bar, $player);
for ($i = 0; $i <= 23; $i++) {
if (!hasCheckerAt($board, $i, $player)) continue;
// Normal move
if ($player === 0) {
$target = $i - $dieValue;
} else {
$target = $i + $dieValue;
}
if ($target >= 0 && $target <= 23) {
if (!isPointBlocked($board, $target, $player)) {
$moves[] = ['from' => $i, 'to' => $target];
}
}
// Bearing off
if ($canBear) {
if ($player === 0 && $i <= 5) {
$dieNeeded = $i + 1;
if ($dieValue === $dieNeeded) {
$moves[] = ['from' => $i, 'to' => 'off'];
} elseif ($dieValue > $dieNeeded && !hasCheckerOnHigherPoint($board, $i, $player)) {
$moves[] = ['from' => $i, 'to' => 'off'];
}
} elseif ($player === 1 && $i >= 18) {
$dieNeeded = 24 - $i;
if ($dieValue === $dieNeeded) {
$moves[] = ['from' => $i, 'to' => 'off'];
} elseif ($dieValue > $dieNeeded && !hasCheckerOnHigherPoint($board, $i, $player)) {
$moves[] = ['from' => $i, 'to' => 'off'];
}
}
}
}
return $moves;
}
function playerHasAnyValidMove($board, $bar, $borneOff, $player, $diceRemaining) {
$uniqueDice = array_unique($diceRemaining);
foreach ($uniqueDice as $die) {
$moves = getValidMoves($board, $bar, $borneOff, $player, $die);
if (!empty($moves)) return true;
}
return false;
}
// ─── Win Handling ───────────────────────────────────────────────────────────
function handleWin($match, $matchId, $board, $bar, $borneOff, $diceRemaining, $moves, $gameState, $players, $winner, $userId) {
$loser = 1 - $winner;
$doublingCube = json_decode($match['doubling_cube'], true);
$cubeValue = $doublingCube['value'];
// Determine win type
$winType = 'normal'; // 1 point
$multiplier = 1;
if ($borneOff[$loser] === 0) {
// Check for backgammon: opponent has checker on bar or in winner's home
$hasBackgammon = false;
if ($bar[$loser] > 0) {
$hasBackgammon = true;
} else {
// Check winner's home board
if ($winner === 0) {
// Winner's home is 0-5, check if loser (negative) has checkers there
for ($i = 0; $i <= 5; $i++) {
if ($board[$i] < 0) { $hasBackgammon = true; break; }
}
} else {
// Winner's home is 18-23, check if loser (positive) has checkers there
for ($i = 18; $i <= 23; $i++) {
if ($board[$i] > 0) { $hasBackgammon = true; break; }
}
}
}
if ($hasBackgammon) {
$winType = 'backgammon';
$multiplier = 3;
} else {
$winType = 'gammon';
$multiplier = 2;
}
}
$pointsWon = $cubeValue * $multiplier;
$scores = json_decode($match['scores'], true);
$scores[$winner] += $pointsWon;
$moves[] = [
'player' => $winner,
'action' => 'win',
'win_type' => $winType,
'points' => $pointsWon,
'at' => date('c')
];
$matchLength = $match['match_length'];
// Check if match is over
if ($scores[$winner] >= $matchLength) {
$gameState['phase'] = 'match_over';
$update = [
'board' => json_encode($board),
'bar' => json_encode($bar),
'borne_off' => json_encode($borneOff),
'dice_remaining' => json_encode([]),
'scores' => json_encode($scores),
'moves' => json_encode($moves),
'game_state' => json_encode($gameState),
'status' => 'completed',
'completed_at' => date('c'),
'result' => "player_{$winner}_wins_{$winType}"
];
$patchRes = supabase_rest('PATCH', "backgammon_matches?id=eq.{$matchId}", $update, SUPABASE_SERVICE_KEY);
echo json_encode(['ok' => true, 'match' => $patchRes['data'][0], 'game_over' => true, 'match_over' => true, 'winner' => $winner, 'win_type' => $winType, 'points' => $pointsWon]);
return;
}
// Start new game in match
startNewGame($matchId, $scores, $moves, $players, $gameState);
}
function startNewGame($matchId, $scores, $moves, $players, $gameState) {
// Opening roll for new game
$d1 = 0;
$d2 = 0;
while ($d1 === $d2) {
$d1 = random_int(1, 6);
$d2 = random_int(1, 6);
}
$firstPlayer = ($d1 > $d2) ? 0 : 1;
$gameState['phase'] = 'moving';
unset($gameState['doubler']);
$update = [
'board' => json_encode(getInitialBoard()),
'bar' => json_encode([0, 0]),
'borne_off' => json_encode([0, 0]),
'dice' => json_encode([$d1, $d2]),
'dice_remaining' => json_encode([$d1, $d2]),
'doubling_cube' => json_encode(['value' => 1, 'owner' => null]),
'current_turn' => $firstPlayer,
'scores' => json_encode($scores),
'moves' => json_encode($moves),
'game_state' => json_encode($gameState)
];
$patchRes = supabase_rest('PATCH', "backgammon_matches?id=eq.{$matchId}", $update, SUPABASE_SERVICE_KEY);
echo json_encode(['ok' => true, 'match' => $patchRes['data'][0], 'game_over' => true, 'match_over' => false, 'new_game' => true]);
if ($players[$firstPlayer]['type'] === 'bot') {
executeBotTurns($matchId);
}
}
// ─── Bot AI ─────────────────────────────────────────────────────────────────
function executeBotTurns($matchId) {
$maxIterations = 100;
$iteration = 0;
while ($iteration < $maxIterations) {
$iteration++;
$res = supabase_rest('GET', "backgammon_matches?id=eq.{$matchId}&select=*", [], SUPABASE_SERVICE_KEY);
if (empty($res['data'])) break;
$match = $res['data'][0];
if ($match['status'] !== 'in_progress') break;
$players = json_decode($match['players'], true);
$currentTurn = $match['current_turn'];
$currentPlayer = $players[$currentTurn];
if ($currentPlayer['type'] !== 'bot') break;
$board = json_decode($match['board'], true);
$bar = json_decode($match['bar'], true);
$borneOff = json_decode($match['borne_off'], true);
$gameState = json_decode($match['game_state'], true);
$moves = json_decode($match['moves'], true);
$diceRemaining = json_decode($match['dice_remaining'], true);
$difficulty = $currentPlayer['difficulty'] ?? 'medium';
// Phase: awaiting_roll → roll dice
if ($gameState['phase'] === 'awaiting_roll') {
$d1 = random_int(1, 6);
$d2 = random_int(1, 6);
$dice = [$d1, $d2];
if ($d1 === $d2) {
$diceRemaining = [$d1, $d1, $d1, $d1];
} else {
$diceRemaining = [$d1, $d2];
}
$moves[] = [
'player' => $currentTurn,
'action' => 'roll',
'dice' => $dice,
'at' => date('c'),
'bot' => true
];
// Check if any valid moves
$hasValidMoves = playerHasAnyValidMove($board, $bar, $borneOff, $currentTurn, $diceRemaining);
if (!$hasValidMoves) {
// No moves — pass
$nextTurn = 1 - $currentTurn;
$gameState['phase'] = 'awaiting_roll';
$moves[] = ['player' => $currentTurn, 'action' => 'no_moves', 'at' => date('c'), 'bot' => true];
$update = [
'dice' => json_encode($dice),
'dice_remaining' => json_encode([]),
'current_turn' => $nextTurn,
'game_state' => json_encode($gameState),
'moves' => json_encode($moves)
];
supabase_rest('PATCH', "backgammon_matches?id=eq.{$matchId}", $update, SUPABASE_SERVICE_KEY);
if ($players[$nextTurn]['type'] !== 'bot') break;
continue;
}
$gameState['phase'] = 'moving';
// Update dice in match, then continue to move phase
$update = [
'dice' => json_encode($dice),
'dice_remaining' => json_encode($diceRemaining),
'game_state' => json_encode($gameState),
'moves' => json_encode($moves)
];
supabase_rest('PATCH', "backgammon_matches?id=eq.{$matchId}", $update, SUPABASE_SERVICE_KEY);
continue;
}
// Phase: moving → execute moves until dice exhausted
if ($gameState['phase'] === 'moving') {
$moveMade = false;
// Try to use each remaining die
while (!empty($diceRemaining)) {
$uniqueDice = array_unique($diceRemaining);
$allValidMoves = [];
foreach ($uniqueDice as $die) {
$validForDie = getValidMoves($board, $bar, $borneOff, $currentTurn, $die);
foreach ($validForDie as $vm) {
$vm['die'] = $die;
$allValidMoves[] = $vm;
}
}
if (empty($allValidMoves)) break;
// Bot picks a move
$chosen = botPickMove($allValidMoves, $board, $bar, $borneOff, $currentTurn, $difficulty, $diceRemaining);
// Execute the chosen move
$from = $chosen['from'];
$to = $chosen['to'];
$moveResult = validateAndExecuteMove($board, $bar, $borneOff, $diceRemaining, $currentTurn, $from, $to);
if (isset($moveResult['error'])) {
// Shouldn't happen with valid move generation, but safety break
break;
}
$board = $moveResult['board'];
$bar = $moveResult['bar'];
$borneOff = $moveResult['borne_off'];
$diceRemaining = $moveResult['dice_remaining'];
$moves[] = [
'player' => $currentTurn,
'action' => 'move',
'from' => $from,
'to' => $to,
'die_used' => $moveResult['die_used'],
'hit' => $moveResult['hit'],
'at' => date('c'),
'bot' => true
];
$moveMade = true;
// Check win
if ($borneOff[$currentTurn] >= 15) {
$doublingCube = json_decode($match['doubling_cube'], true);
$cubeValue = $doublingCube['value'];
$loser = 1 - $currentTurn;
$winType = 'normal';
$multiplier = 1;
if ($borneOff[$loser] === 0) {
$hasBackgammon = false;
if ($bar[$loser] > 0) {
$hasBackgammon = true;
} else {
if ($currentTurn === 0) {
for ($i = 0; $i <= 5; $i++) { if ($board[$i] < 0) { $hasBackgammon = true; break; } }
} else {
for ($i = 18; $i <= 23; $i++) { if ($board[$i] > 0) { $hasBackgammon = true; break; } }
}
}
if ($hasBackgammon) { $winType = 'backgammon'; $multiplier = 3; }
else { $winType = 'gammon'; $multiplier = 2; }
}
$pointsWon = $cubeValue * $multiplier;
$scores = json_decode($match['scores'], true);
$scores[$currentTurn] += $pointsWon;
$matchLength = $match['match_length'];
$moves[] = ['player' => $currentTurn, 'action' => 'win', 'win_type' => $winType, 'points' => $pointsWon, 'at' => date('c'), 'bot' => true];
if ($scores[$currentTurn] >= $matchLength) {
$gameState['phase'] = 'match_over';
$update = [
'board' => json_encode($board),
'bar' => json_encode($bar),
'borne_off' => json_encode($borneOff),
'dice_remaining' => json_encode([]),
'scores' => json_encode($scores),
'moves' => json_encode($moves),
'game_state' => json_encode($gameState),
'status' => 'completed',
'completed_at' => date('c'),
'result' => "player_{$currentTurn}_wins_{$winType}"
];
supabase_rest('PATCH', "backgammon_matches?id=eq.{$matchId}", $update, SUPABASE_SERVICE_KEY);
return;
}
// New game in match
$nd1 = 0; $nd2 = 0;
while ($nd1 === $nd2) { $nd1 = random_int(1, 6); $nd2 = random_int(1, 6); }
$firstPlayer = ($nd1 > $nd2) ? 0 : 1;
$gameState['phase'] = 'moving';
$update = [
'board' => json_encode(getInitialBoard()),
'bar' => json_encode([0, 0]),
'borne_off' => json_encode([0, 0]),
'dice' => json_encode([$nd1, $nd2]),
'dice_remaining' => json_encode([$nd1, $nd2]),
'doubling_cube' => json_encode(['value' => 1, 'owner' => null]),
'current_turn' => $firstPlayer,
'scores' => json_encode($scores),
'moves' => json_encode($moves),
'game_state' => json_encode($gameState)
];
supabase_rest('PATCH', "backgammon_matches?id=eq.{$matchId}", $update, SUPABASE_SERVICE_KEY);
if ($players[$firstPlayer]['type'] !== 'bot') return;
break 2; // Break inner while and will re-enter outer while
}
}
// Turn over — switch to opponent
$nextTurn = 1 - $currentTurn;
$gameState['phase'] = 'awaiting_roll';
$update = [
'board' => json_encode($board),
'bar' => json_encode($bar),
'borne_off' => json_encode($borneOff),
'dice_remaining' => json_encode([]),
'current_turn' => $nextTurn,
'game_state' => json_encode($gameState),
'moves' => json_encode($moves)
];
supabase_rest('PATCH', "backgammon_matches?id=eq.{$matchId}", $update, SUPABASE_SERVICE_KEY);
if ($players[$nextTurn]['type'] !== 'bot') break;
continue;
}
// Unknown phase for bot
break;
}
}
function botPickMove($allValidMoves, $board, $bar, $borneOff, $player, $difficulty, $diceRemaining) {
if ($difficulty === 'easy') {
return $allValidMoves[array_rand($allValidMoves)];
}
if ($difficulty === 'medium') {
// Heuristic scoring per move
$bestScore = -9999;
$bestMove = $allValidMoves[0];
foreach ($allValidMoves as $move) {
$score = 0;
$from = $move['from'];
$to = $move['to'];
// Bear off bonus
if ($to === 'off') {
$score += 5;
} else {
$toPoint = (int)$to;
// Hit bonus
if (isOpponentBlot($board, $toPoint, $player)) {
$score += 3;
}
// Make point (land where you already have 1 checker)
if ($player === 0 && $board[$toPoint] === 1) {
$score += 2;
} elseif ($player === 1 && $board[$toPoint] === -1) {
$score += 2;
}
// Leaving a blot penalty
if ($from !== 'bar') {
$fromPoint = (int)$from;
if ($player === 0 && $board[$fromPoint] === 2) {
$score -= 4; // Leaving one behind = blot
} elseif ($player === 1 && $board[$fromPoint] === -2) {
$score -= 4;
}
}
// Advance toward home bonus
$score += 1;
}
if ($score > $bestScore) {
$bestScore = $score;
$bestMove = $move;
}
}
return $bestMove;
}
// Hard: positional evaluation
$bestScore = -99999;
$bestMove = $allValidMoves[0];
foreach ($allValidMoves as $move) {
// Simulate the move
$simBoard = $board;
$simBar = $bar;
$simBorneOff = $borneOff;
$simDice = $diceRemaining;
$from = $move['from'];
$to = $move['to'];
$result = validateAndExecuteMove($simBoard, $simBar, $simBorneOff, $simDice, $player, $from, $to);
if (isset($result['error'])) continue;
$simBoard = $result['board'];
$simBar = $result['bar'];
$simBorneOff = $result['borne_off'];
$score = evaluatePosition($simBoard, $simBar, $simBorneOff, $player);
if ($score > $bestScore) {
$bestScore = $score;
$bestMove = $move;
}
}
return $bestMove;
}
function evaluatePosition($board, $bar, $borneOff, $player) {
$score = 0;
$opponent = 1 - $player;
// Borne off count (high priority)
$score += $borneOff[$player] * 10;
$score -= $borneOff[$opponent] * 10;
// Bar pressure
$score -= $bar[$player] * 15;
$score += $bar[$opponent] * 15;
// Pip count (lower is better for current player)
$myPips = 0;
$oppPips = 0;
for ($i = 0; $i <= 23; $i++) {
if ($player === 0) {
if ($board[$i] > 0) $myPips += $board[$i] * ($i + 1);
if ($board[$i] < 0) $oppPips += abs($board[$i]) * (24 - $i);
} else {
if ($board[$i] < 0) $myPips += abs($board[$i]) * (24 - $i);
if ($board[$i] > 0) $oppPips += $board[$i] * ($i + 1);
}
}
$score -= $myPips * 0.5;
$score += $oppPips * 0.5;
// Blot exposure (penalty for single checkers)
for ($i = 0; $i <= 23; $i++) {
if ($player === 0 && $board[$i] === 1) {
$score -= 3;
} elseif ($player === 1 && $board[$i] === -1) {
$score -= 3;
}
}
// Prime detection (consecutive blocked points)
$primeLength = 0;
$maxPrime = 0;
for ($i = 0; $i <= 23; $i++) {
if (($player === 0 && $board[$i] >= 2) || ($player === 1 && $board[$i] <= -2)) {
$primeLength++;
if ($primeLength > $maxPrime) $maxPrime = $primeLength;
} else {
$primeLength = 0;
}
}
$score += $maxPrime * 5;
// Anchors in opponent's home (good for player)
if ($player === 0) {
for ($i = 18; $i <= 23; $i++) {
if ($board[$i] >= 2) $score += 3;
}
} else {
for ($i = 0; $i <= 5; $i++) {
if ($board[$i] <= -2) $score += 3;
}
}
return $score;
}
function botRespondToDouble($matchId, $difficulty) {
$res = supabase_rest('GET', "backgammon_matches?id=eq.{$matchId}&select=*", [], SUPABASE_SERVICE_KEY);
if (empty($res['data'])) return;
$match = $res['data'][0];
$players = json_decode($match['players'], true);
$currentTurn = $match['current_turn'];
$opponent = 1 - $currentTurn;
$board = json_decode($match['board'], true);
$bar = json_decode($match['bar'], true);
$borneOff = json_decode($match['borne_off'], true);
$doublingCube = json_decode($match['doubling_cube'], true);
$gameState = json_decode($match['game_state'], true);
$moves = json_decode($match['moves'], true);
// Bot decision: evaluate position
$posScore = evaluatePosition($board, $bar, $borneOff, $opponent);
$accept = true;
if ($difficulty === 'easy') {
$accept = (random_int(0, 100) > 40);
} elseif ($difficulty === 'medium') {
$accept = ($posScore > -20);
} else {
$accept = ($posScore > -30);
}
if ($accept) {
$doublingCube['value'] *= 2;
$doublingCube['owner'] = $opponent;
$gameState['phase'] = 'awaiting_roll';
unset($gameState['doubler']);
$moves[] = ['player' => $opponent, 'action' => 'accept_double', 'cube_value' => $doublingCube['value'], 'at' => date('c'), 'bot' => true];
$update = [
'doubling_cube' => json_encode($doublingCube),
'game_state' => json_encode($gameState),
'moves' => json_encode($moves)
];
supabase_rest('PATCH', "backgammon_matches?id=eq.{$matchId}", $update, SUPABASE_SERVICE_KEY);
} else {
// Decline — current player wins
$scores = json_decode($match['scores'], true);
$scores[$currentTurn] += $doublingCube['value'];
$matchLength = $match['match_length'];
$moves[] = ['player' => $opponent, 'action' => 'decline_double', 'at' => date('c'), 'bot' => true];
if ($scores[$currentTurn] >= $matchLength) {
$gameState['phase'] = 'match_over';
$update = [
'scores' => json_encode($scores),
'game_state' => json_encode($gameState),
'moves' => json_encode($moves),
'status' => 'completed',
'completed_at' => date('c'),
'result' => "player_{$currentTurn}_wins"
];
supabase_rest('PATCH', "backgammon_matches?id=eq.{$matchId}", $update, SUPABASE_SERVICE_KEY);
} else {
// New game
$nd1 = 0; $nd2 = 0;
while ($nd1 === $nd2) { $nd1 = random_int(1, 6); $nd2 = random_int(1, 6); }
$firstPlayer = ($nd1 > $nd2) ? 0 : 1;
$gameState['phase'] = 'moving';
unset($gameState['doubler']);
$update = [
'board' => json_encode(getInitialBoard()),
'bar' => json_encode([0, 0]),
'borne_off' => json_encode([0, 0]),
'dice' => json_encode([$nd1, $nd2]),
'dice_remaining' => json_encode([$nd1, $nd2]),
'doubling_cube' => json_encode(['value' => 1, 'owner' => null]),
'current_turn' => $firstPlayer,
'scores' => json_encode($scores),
'moves' => json_encode($moves),
'game_state' => json_encode($gameState)
];
supabase_rest('PATCH', "backgammon_matches?id=eq.{$matchId}", $update, SUPABASE_SERVICE_KEY);
if ($players[$firstPlayer]['type'] === 'bot') {
executeBotTurns($matchId);
}
}
}
}
// ─── Leave/Chat/Matchmaking ─────────────────────────────────────────────────
function leaveGame($input, $userId) {
$matchId = $input['match_id'] ?? '';
$match = getAndValidateMatch($matchId, $userId);
if (!$match) return;
$players = json_decode($match['players'], true);
$playerIdx = -1;
foreach ($players as $idx => $p) {
if ($p['id'] === $userId) { $playerIdx = $idx; break; }
}
if ($playerIdx === -1) {
http_response_code(400);
echo json_encode(['error' => 'not_in_match']);
return;
}
$players[$playerIdx]['type'] = 'bot';
$players[$playerIdx]['difficulty'] = 'medium';
$players[$playerIdx]['name'] = 'بوت (غادر)';
$update = ['players' => json_encode($players)];
supabase_rest('PATCH', "backgammon_matches?id=eq.{$matchId}", $update, SUPABASE_SERVICE_KEY);
echo json_encode(['ok' => true]);
// If it's now this bot's turn, execute
if ($match['current_turn'] === $playerIdx) {
executeBotTurns($matchId);
}
}
function sendChat($input, $userId, $userName) {
$matchId = $input['match_id'] ?? '';
$text = trim($input['text'] ?? '');
if (!$text || strlen($text) > 200) {
http_response_code(400);
echo json_encode(['error' => 'invalid_message']);
return;
}
$match = getAndValidateMatch($matchId, $userId);
if (!$match) return;
$chat = json_decode($match['chat'], true);
$chat[] = [
'sender_id' => $userId,
'sender_name' => $userName,
'text' => htmlspecialchars($text, ENT_QUOTES, 'UTF-8'),
'at' => date('c')
];
supabase_rest('PATCH', "backgammon_matches?id=eq.{$matchId}", ['chat' => json_encode($chat)], SUPABASE_SERVICE_KEY);
echo json_encode(['ok' => true]);
}
function matchmake($input, $userId, $userName) {
$subAction = $input['sub_action'] ?? 'join';
$matchLength = (int)($input['match_length'] ?? 1);
if ($matchLength < 1) $matchLength = 1;
if ($subAction === 'leave') {
supabase_rest('DELETE', "backgammon_queue?user_id=eq.{$userId}", [], SUPABASE_SERVICE_KEY);
echo json_encode(['ok' => true, 'status' => 'left']);
return;
}
if ($subAction === 'poll') {
$existing = supabase_rest('GET', "backgammon_queue?user_id=eq.{$userId}&select=*", [], SUPABASE_SERVICE_KEY);
if (!empty($existing['data']) && !empty($existing['data'][0]['match_id'])) {
$matchId = $existing['data'][0]['match_id'];
supabase_rest('DELETE', "backgammon_queue?user_id=eq.{$userId}", [], SUPABASE_SERVICE_KEY);
echo json_encode(['ok' => true, 'status' => 'matched', 'match_id' => $matchId]);
return;
}
echo json_encode(['ok' => true, 'status' => 'waiting']);
return;
}
// sub_action = 'join'
$existing = supabase_rest('GET', "backgammon_queue?user_id=eq.{$userId}&select=*", [], SUPABASE_SERVICE_KEY);
if (!empty($existing['data'])) {
$entry = $existing['data'][0];
if (!empty($entry['match_id'])) {
supabase_rest('DELETE', "backgammon_queue?user_id=eq.{$userId}", [], SUPABASE_SERVICE_KEY);
echo json_encode(['ok' => true, 'status' => 'matched', 'match_id' => $entry['match_id']]);
return;
}
// Try to find an opponent
$opponents = supabase_rest('GET', "backgammon_queue?user_id=neq.{$userId}&match_id=is.null&match_length=eq.{$matchLength}&select=*&limit=1", [], SUPABASE_SERVICE_KEY);
if (!empty($opponents['data'])) {
$opp = $opponents['data'][0];
$allPlayers = [
['id' => $userId, 'name' => $userName, 'type' => 'human'],
['id' => $opp['user_id'], 'name' => $opp['user_name'] ?? 'لاعب', 'type' => 'human']
];
// Opening roll
$d1 = 0; $d2 = 0;
while ($d1 === $d2) { $d1 = random_int(1, 6); $d2 = random_int(1, 6); }
$firstPlayer = ($d1 > $d2) ? 0 : 1;
$matchData = [
'room_code' => generateRoomCode(),
'status' => 'in_progress',
'players' => json_encode($allPlayers),
'current_turn' => $firstPlayer,
'board' => json_encode(getInitialBoard()),
'dice' => json_encode([$d1, $d2]),
'dice_remaining' => json_encode([$d1, $d2]),
'bar' => json_encode([0, 0]),
'borne_off' => json_encode([0, 0]),
'doubling_cube' => json_encode(['value' => 1, 'owner' => null]),
'game_state' => json_encode(['phase' => 'moving', 'started_at' => date('c')]),
'moves' => json_encode([]),
'scores' => json_encode([0, 0]),
'match_length' => $matchLength,
'chat' => json_encode([]),
'host_id' => $userId,
'result' => null
];
$matchRes = supabase_rest('POST', 'backgammon_matches', $matchData, SUPABASE_SERVICE_KEY);
if (!empty($matchRes['data'])) {
$newMatchId = $matchRes['data'][0]['id'];
supabase_rest('PATCH', "backgammon_queue?user_id=eq.{$opp['user_id']}", ['match_id' => $newMatchId], SUPABASE_SERVICE_KEY);
supabase_rest('DELETE', "backgammon_queue?user_id=eq.{$userId}", [], SUPABASE_SERVICE_KEY);
echo json_encode(['ok' => true, 'status' => 'matched', 'match_id' => $newMatchId]);
return;
}
}
echo json_encode(['ok' => true, 'status' => 'waiting']);
return;
}
// Join queue
supabase_rest('POST', 'backgammon_queue', [
'user_id' => $userId,
'user_name' => $userName,
'match_length' => $matchLength
], SUPABASE_SERVICE_KEY);
echo json_encode(['ok' => true, 'status' => 'queued']);
}
// ─── Helpers ────────────────────────────────────────────────────────────────
function getAndValidateMatch($matchId, $userId) {
if (!$matchId) {
http_response_code(400);
echo json_encode(['error' => 'match_id_required']);
return null;
}
$res = supabase_rest('GET', "backgammon_matches?id=eq.{$matchId}&select=*", [], SUPABASE_SERVICE_KEY);
if (empty($res['data'])) {
http_response_code(404);
echo json_encode(['error' => 'not_found']);
return null;
}
$match = $res['data'][0];
if ($match['status'] === 'completed' || $match['status'] === 'aborted') {
http_response_code(400);
echo json_encode(['error' => 'game_ended']);
return null;
}
$players = json_decode($match['players'], true);
$found = false;
foreach ($players as $p) {
if ($p['id'] === $userId) { $found = true; break; }
}
if (!$found) {
http_response_code(403);
echo json_encode(['error' => 'not_in_match']);
return null;
}
return $match;
}
...@@ -63,6 +63,14 @@ if ($route === '' || $route === 'home') { ...@@ -63,6 +63,14 @@ if ($route === '' || $route === 'home') {
require 'pages/domino-live.php'; require 'pages/domino-live.php';
} elseif ($route === 'domino-matchmaking') { } elseif ($route === 'domino-matchmaking') {
require 'pages/domino-matchmaking.php'; 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') { } elseif ($route === 'admin/theme') {
require 'pages/admin-theme.php'; require 'pages/admin-theme.php';
} elseif (str_starts_with($route, 'api/')) { } 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 () => { ...@@ -29,7 +29,8 @@ document.addEventListener('DOMContentLoaded', async () => {
const gameRoutes = { const gameRoutes = {
chess: '/play', chess: '/play',
ludo: '/ludo', ludo: '/ludo',
domino: '/domino' domino: '/domino',
backgammon: '/backgammon'
}; };
const gameIcons = { const gameIcons = {
......
/* ─── Backgammon CSS Variables ────────────────────────────────────────────── */
:root {
--bg-board-wood: #5c3d2e;
--bg-felt: #1a5c32;
--bg-frame: #3a2418;
--bg-point-light: #d4a76a;
--bg-point-dark: #8b4513;
--bg-bar: #3a2418;
--bg-checker-white: #f0ebe0;
--bg-checker-black: #1a1a1a;
--bg-checker-size: 36px;
--bg-dice-bg: #f8f4ec;
--bg-dice-pip: #1a1a1a;
--bg-valid: rgba(76, 175, 80, 0.5);
--bg-selected: rgba(255, 200, 50, 0.6);
--bg-hit: rgba(244, 67, 54, 0.4);
}
/* ─── Game Layout ────────────────────────────────────────────────────────── */
.bg-layout {
display: flex;
gap: 16px;
justify-content: center;
align-items: flex-start;
max-width: 900px;
margin: 0 auto;
padding: 12px;
direction: ltr;
}
.bg-board-column {
display: flex;
flex-direction: column;
gap: 8px;
align-items: center;
flex: 1;
min-width: 0;
}
/* ─── Board Frame ────────────────────────────────────────────────────────── */
.bg-board {
position: relative;
width: 100%;
max-width: 600px;
aspect-ratio: 1.4 / 1;
background: var(--bg-board-wood);
border: 8px solid var(--bg-frame);
border-radius: 8px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4), inset 0 2px 8px rgba(0, 0, 0, 0.3);
overflow: hidden;
}
.bg-board-inner {
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
background: var(--bg-felt);
}
/* ─── Board Halves ───────────────────────────────────────────────────────── */
.bg-half {
display: flex;
flex: 1;
position: relative;
}
.bg-half--top {
align-items: flex-start;
}
.bg-half--bottom {
align-items: flex-end;
}
/* ─── Quadrants ──────────────────────────────────────────────────────────── */
.bg-quadrant {
display: flex;
flex: 1;
height: 100%;
padding: 4px 2px;
}
.bg-quadrant--top-left,
.bg-quadrant--top-right {
align-items: flex-start;
}
.bg-quadrant--bottom-left,
.bg-quadrant--bottom-right {
align-items: flex-end;
}
/* ─── Points (Triangles) ─────────────────────────────────────────────────── */
.bg-point {
position: relative;
display: flex;
align-items: center;
width: calc(100% / 6);
height: 100%;
cursor: pointer;
transition: background 0.15s ease;
z-index: 1;
}
.bg-half--top .bg-point {
flex-direction: column;
justify-content: flex-start;
}
.bg-half--bottom .bg-point {
flex-direction: column-reverse;
justify-content: flex-start;
}
/* Triangle shape via pseudo-element */
.bg-point::before {
content: '';
position: absolute;
left: 10%;
right: 10%;
width: 80%;
height: 42%;
z-index: 0;
}
.bg-half--top .bg-point::before {
top: 0;
clip-path: polygon(50% 100%, 0% 0%, 100% 0%);
}
.bg-half--bottom .bg-point::before {
bottom: 0;
clip-path: polygon(50% 0%, 0% 100%, 100% 100%);
}
/* Alternating point colors */
.bg-point:nth-child(odd)::before {
background: var(--bg-point-light);
}
.bg-point:nth-child(even)::before {
background: var(--bg-point-dark);
}
/* Point highlights */
.bg-point--valid {
background: var(--bg-valid);
border-radius: 4px;
}
.bg-point--selected {
background: var(--bg-selected);
border-radius: 4px;
}
.bg-point--hit {
background: var(--bg-hit);
border-radius: 4px;
}
/* Point number labels (optional, shown on hover or always) */
.bg-point-label {
position: absolute;
font-size: 9px;
color: rgba(255, 255, 255, 0.5);
font-weight: 600;
z-index: 5;
}
.bg-half--top .bg-point-label {
top: -14px;
}
.bg-half--bottom .bg-point-label {
bottom: -14px;
}
/* ─── Checkers ───────────────────────────────────────────────────────────── */
.bg-checker {
width: var(--bg-checker-size);
height: var(--bg-checker-size);
border-radius: 50%;
position: relative;
z-index: 2;
flex-shrink: 0;
transition: transform 0.2s ease, box-shadow 0.2s ease;
cursor: pointer;
}
.bg-checker--white {
background: radial-gradient(circle at 35% 35%, #fffef8, var(--bg-checker-white) 60%, #c8c0b0);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3), inset 0 1px 2px rgba(255, 255, 255, 0.6);
border: 1px solid #b0a890;
}
.bg-checker--black {
background: radial-gradient(circle at 35% 35%, #4a4a4a, var(--bg-checker-black) 60%, #000);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.5), inset 0 1px 2px rgba(255, 255, 255, 0.1);
border: 1px solid #333;
}
.bg-checker--selected {
box-shadow: 0 0 0 3px var(--bg-selected), 0 0 12px rgba(255, 200, 50, 0.6);
transform: scale(1.08);
z-index: 10;
}
.bg-checker--valid-target {
box-shadow: 0 0 0 3px var(--bg-valid), 0 0 10px rgba(76, 175, 80, 0.5);
animation: bg-pulse 1.2s ease-in-out infinite;
}
.bg-checker--movable {
animation: bg-pulse 1.5s ease-in-out infinite;
cursor: pointer;
}
/* Checker count badge (when > 5 stacked) */
.bg-checker-count {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 11px;
font-weight: 700;
color: #fff;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.8);
pointer-events: none;
z-index: 3;
}
.bg-checker--white .bg-checker-count {
color: #333;
text-shadow: 0 1px 1px rgba(255, 255, 255, 0.5);
}
/* Checker stacking within points */
.bg-point .bg-checkers-stack {
display: flex;
position: relative;
z-index: 2;
gap: 0;
}
.bg-half--top .bg-checkers-stack {
flex-direction: column;
margin-top: 2px;
}
.bg-half--bottom .bg-checkers-stack {
flex-direction: column-reverse;
margin-bottom: 2px;
}
/* Overlap checkers slightly when many */
.bg-checkers-stack--compact .bg-checker + .bg-checker {
margin-top: -8px;
}
.bg-half--bottom .bg-checkers-stack--compact .bg-checker + .bg-checker {
margin-top: 0;
margin-bottom: -8px;
}
/* ─── Bar Area ───────────────────────────────────────────────────────────── */
.bg-bar-area {
width: 8%;
min-width: 28px;
background: var(--bg-bar);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 4px;
padding: 4px 0;
box-shadow: inset 2px 0 4px rgba(0, 0, 0, 0.3), inset -2px 0 4px rgba(0, 0, 0, 0.3);
z-index: 3;
}
.bg-bar--top {
justify-content: flex-end;
padding-bottom: 8px;
}
.bg-bar--bottom {
justify-content: flex-start;
padding-top: 8px;
}
.bg-bar-checkers {
display: flex;
flex-direction: column;
align-items: center;
gap: 2px;
}
/* ─── Borne Off Trays ────────────────────────────────────────────────────── */
.bg-borne-off {
position: absolute;
top: 4px;
bottom: 4px;
right: -48px;
width: 40px;
background: var(--bg-frame);
border-radius: 4px;
display: flex;
flex-direction: column;
align-items: center;
padding: 6px 2px;
gap: 1px;
overflow: hidden;
box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.4);
}
.bg-borne-off--white {
justify-content: flex-end;
}
.bg-borne-off--black {
justify-content: flex-start;
}
.bg-borne-off .bg-checker {
width: calc(var(--bg-checker-size) - 4px);
height: 8px;
border-radius: 3px;
cursor: default;
}
.bg-borne-off .bg-checker--white {
background: var(--bg-checker-white);
border: 1px solid #b0a890;
box-shadow: none;
}
.bg-borne-off .bg-checker--black {
background: var(--bg-checker-black);
border: 1px solid #333;
box-shadow: none;
}
.bg-borne-off-count {
font-size: 11px;
color: rgba(255, 255, 255, 0.7);
font-weight: 600;
margin-top: 4px;
}
/* ─── Dice ───────────────────────────────────────────────────────────────── */
.bg-dice-area {
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
padding: 12px;
}
.bg-dice {
width: 48px;
height: 48px;
background: var(--bg-dice-bg);
border-radius: 8px;
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-template-rows: repeat(3, 1fr);
padding: 6px;
gap: 2px;
box-shadow: 0 3px 8px rgba(0, 0, 0, 0.3), inset 0 1px 1px rgba(255, 255, 255, 0.4);
border: 2px solid #ddd;
transition: transform 0.3s ease;
}
.bg-dice-dot {
width: 100%;
height: 100%;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
.bg-dice-dot--filled {
background: var(--bg-dice-pip);
box-shadow: inset 0 1px 1px rgba(255, 255, 255, 0.1);
}
.bg-dice--used {
opacity: 0.4;
transform: scale(0.9);
}
.bg-dice--rolling {
animation: bg-dice-roll 0.6s ease-in-out;
}
.bg-dice--doubles {
border-color: var(--gold);
box-shadow: 0 0 8px rgba(231, 168, 50, 0.4), 0 3px 8px rgba(0, 0, 0, 0.3);
}
/* ─── Doubling Cube ──────────────────────────────────────────────────────── */
.bg-cube {
width: 40px;
height: 40px;
background: linear-gradient(135deg, #f5f5f5, #e0e0e0);
border: 2px solid #999;
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
font-weight: 700;
color: #333;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3);
cursor: pointer;
transition: transform 0.2s ease;
}
.bg-cube:hover {
transform: scale(1.05);
}
.bg-cube--offered {
border-color: var(--gold);
box-shadow: 0 0 10px rgba(231, 168, 50, 0.5);
animation: bg-pulse 1s ease-in-out infinite;
}
/* ─── Side Panel ─────────────────────────────────────────────────────────── */
.bg-side-panel {
display: flex;
flex-direction: column;
gap: 12px;
width: 220px;
flex-shrink: 0;
}
.bg-turn-indicator {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 14px;
background: var(--bg-2, rgba(0, 0, 0, 0.3));
border-radius: 8px;
border: 1px solid rgba(255, 255, 255, 0.08);
}
.bg-turn-indicator__dot {
width: 12px;
height: 12px;
border-radius: 50%;
flex-shrink: 0;
}
.bg-turn-indicator__text {
font-size: 13px;
font-weight: 600;
color: var(--text-1, #fff);
}
.bg-turn-indicator--active {
border-color: var(--gold, #e7a832);
box-shadow: 0 0 8px rgba(231, 168, 50, 0.25);
}
/* ─── Controls ───────────────────────────────────────────────────────────── */
.bg-controls {
display: flex;
flex-direction: column;
gap: 8px;
padding: 12px;
background: var(--bg-2, rgba(0, 0, 0, 0.3));
border-radius: 8px;
border: 1px solid rgba(255, 255, 255, 0.08);
}
.bg-roll-btn {
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
padding: 12px 20px;
min-height: 44px;
background: linear-gradient(135deg, #e7a832, #d4922a);
color: #fff;
font-size: 15px;
font-weight: 700;
border: none;
border-radius: 8px;
cursor: pointer;
box-shadow: 0 3px 10px rgba(231, 168, 50, 0.4);
transition: transform 0.15s ease, box-shadow 0.15s ease;
}
.bg-roll-btn:hover {
transform: translateY(-1px);
box-shadow: 0 5px 14px rgba(231, 168, 50, 0.5);
}
.bg-roll-btn:active {
transform: translateY(1px);
box-shadow: 0 1px 4px rgba(231, 168, 50, 0.3);
}
.bg-roll-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
transform: none;
box-shadow: none;
}
.bg-undo-btn,
.bg-double-btn {
padding: 8px 14px;
min-height: 36px;
font-size: 13px;
font-weight: 600;
border: 1px solid rgba(255, 255, 255, 0.15);
border-radius: 6px;
background: rgba(255, 255, 255, 0.06);
color: var(--text-2, #ccc);
cursor: pointer;
transition: background 0.15s ease, border-color 0.15s ease;
}
.bg-undo-btn:hover,
.bg-double-btn:hover {
background: rgba(255, 255, 255, 0.12);
border-color: rgba(255, 255, 255, 0.25);
}
/* ─── Game Log ───────────────────────────────────────────────────────────── */
.bg-log {
display: flex;
flex-direction: column;
gap: 4px;
padding: 10px 12px;
background: var(--bg-2, rgba(0, 0, 0, 0.3));
border-radius: 8px;
border: 1px solid rgba(255, 255, 255, 0.08);
max-height: 180px;
overflow-y: auto;
font-size: 12px;
color: var(--text-3, #888);
}
.bg-log-entry {
padding: 2px 0;
border-bottom: 1px solid rgba(255, 255, 255, 0.04);
}
.bg-log-entry:last-child {
border-bottom: none;
}
.bg-log-entry--move {
color: var(--text-2, #ccc);
}
.bg-log-entry--roll {
color: var(--gold, #e7a832);
font-weight: 600;
}
.bg-log-entry--hit {
color: var(--red, #f44336);
}
/* ─── Players Bar ────────────────────────────────────────────────────────── */
.bg-players-bar {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
max-width: 600px;
gap: 12px;
}
.bg-player-info {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 14px;
background: var(--bg-2, rgba(0, 0, 0, 0.3));
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 8px;
flex: 1;
min-width: 0;
transition: border-color 0.3s ease, box-shadow 0.3s ease;
}
.bg-player-info--active {
border-color: var(--gold, #e7a832);
box-shadow: 0 0 10px rgba(231, 168, 50, 0.25);
}
.bg-player-info__color {
width: 14px;
height: 14px;
border-radius: 50%;
flex-shrink: 0;
border: 1px solid rgba(255, 255, 255, 0.2);
}
.bg-player-info__color--white {
background: var(--bg-checker-white);
}
.bg-player-info__color--black {
background: var(--bg-checker-black);
}
.bg-player-info__name {
font-size: 13px;
font-weight: 600;
color: var(--text-1, #fff);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.bg-player-info__pip {
font-size: 11px;
color: var(--text-3, #888);
margin-left: auto;
white-space: nowrap;
}
/* ─── Pip Count Badge ────────────────────────────────────────────────────── */
.bg-pip-count {
display: inline-flex;
align-items: center;
gap: 3px;
padding: 2px 6px;
background: rgba(255, 255, 255, 0.08);
border-radius: 10px;
font-size: 11px;
color: var(--text-3, #888);
}
/* ─── Animations ─────────────────────────────────────────────────────────── */
@keyframes bg-checker-move {
0% { transform: translate(var(--move-x, 0), var(--move-y, 0)); }
100% { transform: translate(0, 0); }
}
@keyframes bg-dice-roll {
0% { transform: rotateX(0deg) rotateY(0deg); }
25% { transform: rotateX(180deg) rotateY(90deg) scale(0.9); }
50% { transform: rotateX(360deg) rotateY(180deg) scale(1.05); }
75% { transform: rotateX(540deg) rotateY(270deg) scale(0.95); }
100% { transform: rotateX(720deg) rotateY(360deg) scale(1); }
}
@keyframes bg-pulse {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.06); }
}
@keyframes bg-hit {
0% { background: transparent; }
25% { background: var(--bg-hit); }
50% { background: transparent; }
75% { background: var(--bg-hit); }
100% { background: transparent; }
}
.bg-checker--moving {
animation: bg-checker-move 0.35s ease-out;
}
.bg-point--hit-flash {
animation: bg-hit 0.5s ease-in-out;
}
/* ─── Status / Overlay ───────────────────────────────────────────────────── */
.bg-status {
text-align: center;
padding: 6px 12px;
font-size: 14px;
font-weight: 600;
border-radius: 6px;
}
.bg-status--info { color: var(--cyan, #00bcd4); }
.bg-status--success { color: var(--green, #4caf50); }
.bg-status--warning { color: var(--gold, #e7a832); }
.bg-game-over-overlay {
display: none;
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.85);
z-index: 1000;
align-items: center;
justify-content: center;
}
.bg-game-over-content {
text-align: center;
padding: 32px;
border-radius: 16px;
background: var(--bg-2, #1a1a2e);
border: 1px solid rgba(255, 255, 255, 0.1);
max-width: 320px;
}
.bg-game-over-content h3 {
font-size: 22px;
color: var(--text-1, #fff);
margin-bottom: 12px;
}
.bg-game-over-result {
color: var(--gold, #e7a832);
font-size: 28px;
font-weight: 700;
margin-bottom: 20px;
}
/* ─── Responsive: Mobile < 768px ─────────────────────────────────────────── */
@media (max-width: 768px) {
:root {
--bg-checker-size: 28px;
}
.bg-layout {
flex-direction: column;
align-items: center;
gap: 12px;
padding: 8px;
}
.bg-board {
max-width: 100%;
border-width: 5px;
}
.bg-borne-off {
right: -36px;
width: 30px;
}
.bg-side-panel {
width: 100%;
flex-direction: row;
flex-wrap: wrap;
gap: 8px;
}
.bg-dice-area {
padding: 8px;
gap: 8px;
}
.bg-dice {
width: 40px;
height: 40px;
padding: 5px;
}
.bg-cube {
width: 32px;
height: 32px;
font-size: 13px;
}
.bg-controls {
flex-direction: row;
flex-wrap: wrap;
justify-content: center;
width: 100%;
}
.bg-roll-btn {
flex: 1;
min-width: 120px;
}
.bg-log {
max-height: 100px;
width: 100%;
}
.bg-players-bar {
flex-direction: column;
gap: 6px;
}
.bg-player-info {
width: 100%;
}
.bg-bar-area {
min-width: 22px;
}
}
/* ─── Responsive: Large screens ──────────────────────────────────────────── */
@media (min-width: 1024px) {
.bg-board {
max-width: 640px;
}
.bg-dice {
width: 56px;
height: 56px;
padding: 8px;
}
.bg-side-panel {
width: 240px;
}
}
/* ─── Reduced Motion ─────────────────────────────────────────────────────── */
@media (prefers-reduced-motion: reduce) {
.bg-checker,
.bg-dice,
.bg-cube,
.bg-roll-btn {
transition: none;
}
.bg-checker--moving,
.bg-checker--movable,
.bg-checker--valid-target,
.bg-dice--rolling,
.bg-point--hit-flash,
.bg-cube--offered {
animation: none;
}
}
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
};
})();
var BackgammonConstants = (function() {
'use strict';
// Starting position: positive = player 0 (white), negative = player 1 (black)
// Points numbered 0-23
var INITIAL_BOARD = [2, 0, 0, 0, 0, -5, 0, -3, 0, 0, 0, 5, -5, 0, 0, 0, 3, 0, 5, 0, 0, 0, 0, -2];
// Player 0 moves high→low (direction -1), Player 1 moves low→high (direction +1)
var PLAYER_DIRECTION = { 0: -1, 1: 1 };
// Home board ranges
var HOME_RANGE = { 0: [0, 5], 1: [18, 23] };
// Bar entry target points per die value
// Player 0 enters at 24 - die, Player 1 enters at die - 1
var BAR_ENTRY_POINTS = { 0: function(die) { return 24 - die; }, 1: function(die) { return die - 1; } };
var TOTAL_CHECKERS = 15;
// Pip positions for rendering dice faces 1-6 (3x3 grid)
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 (Arabic)
var QUICK_MESSAGES = ['حظ سعيد', 'يلا', 'هههه', 'اخخخ', 'GG', 'شكرا', 'يا سلام!', 'مبروك'];
/**
* Check if all of a player's checkers are in their home board (none on bar).
*/
function isAllHome(board, bar, player) {
if (bar[player] > 0) return false;
var homeStart = HOME_RANGE[player][0];
var homeEnd = HOME_RANGE[player][1];
var count = 0;
for (var i = 0; i < 24; i++) {
if (player === 0 && board[i] > 0) {
if (i < homeStart || i > homeEnd) return false;
count += board[i];
} else if (player === 1 && board[i] < 0) {
if (i < homeStart || i > homeEnd) return false;
count += Math.abs(board[i]);
}
}
return true;
}
/**
* Get valid moves for a single die value.
* Returns array of {from, to, hit} objects.
* from: point index or 'bar'; to: point index or 'off'
*/
function getValidMoves(board, bar, borneOff, player, dieValue) {
var moves = [];
var sign = (player === 0) ? 1 : -1; // positive for player 0, negative for player 1
// If player has checkers on the bar, must enter first
if (bar[player] > 0) {
var target = (player === 0) ? 24 - dieValue : dieValue - 1;
// Check if target is not blocked by opponent
var occupant = board[target] * sign;
if (occupant >= 0 || Math.abs(board[target]) <= 1) {
var hit = (board[target] * sign < 0) && Math.abs(board[target]) === 1;
moves.push({ from: 'bar', to: target, hit: hit });
}
return moves; // Must enter from bar first
}
var allHome = isAllHome(board, bar, player);
// Normal moves and bearing off
for (var i = 0; i < 24; i++) {
var checkers = board[i] * sign;
if (checkers <= 0) continue; // No player checkers here
var dest;
if (player === 0) {
dest = i - dieValue;
} else {
dest = i + dieValue;
}
// Bearing off
if (allHome) {
var canBearOff = false;
if (player === 0) {
// Player 0 home: points 0-5, bear off when dest < 0
if (dest < 0) {
if (dest === -1) {
// Exact roll
canBearOff = true;
} else {
// Die is higher than needed — only allowed from highest occupied point
var highest = -1;
for (var h = 5; h >= 0; h--) {
if (board[h] > 0) { highest = h; break; }
}
if (i === highest) canBearOff = true;
}
}
} else {
// Player 1 home: points 18-23, bear off when dest > 23
if (dest > 23) {
if (dest === 24) {
// Exact roll
canBearOff = true;
} else {
// Die is higher than needed — only allowed from highest occupied point
var highestP1 = -1;
for (var h2 = 18; h2 <= 23; h2++) {
if (board[h2] < 0) { highestP1 = h2; break; }
}
if (i === highestP1) canBearOff = true;
}
}
}
if (canBearOff) {
moves.push({ from: i, to: 'off', hit: false });
}
}
// Normal move (dest within board)
if (dest >= 0 && dest <= 23) {
var destOccupant = board[dest] * sign;
// Not blocked: blocked means 2+ opponent checkers
if (destOccupant >= -1) {
var isHit = (destOccupant === -1);
moves.push({ from: i, to: dest, hit: isHit });
}
}
}
return moves;
}
/**
* Apply a single move and return new game state.
*/
function applyMove(board, bar, borneOff, player, move) {
var newBoard = board.slice();
var newBar = { 0: bar[0], 1: bar[1] };
var newBorneOff = { 0: borneOff[0], 1: borneOff[1] };
var sign = (player === 0) ? 1 : -1;
var opponent = 1 - player;
// Remove checker from source
if (move.from === 'bar') {
newBar[player]--;
} else {
newBoard[move.from] -= sign;
}
// Place checker at destination
if (move.to === 'off') {
newBorneOff[player]++;
} else {
// Handle hit
if (move.hit) {
newBoard[move.to] += sign; // remove opponent's single checker
newBar[opponent]++;
}
newBoard[move.to] += sign;
}
return { board: newBoard, bar: newBar, borneOff: newBorneOff };
}
/**
* Check if a player can make any move with any of the remaining dice.
*/
function canMove(board, bar, borneOff, player, diceRemaining) {
for (var i = 0; i < diceRemaining.length; i++) {
var moves = getValidMoves(board, bar, borneOff, player, diceRemaining[i]);
if (moves.length > 0) return true;
}
return false;
}
/**
* Generate all possible complete move sequences for given dice.
* Returns array of move-sequence arrays.
* Enforces: must use maximum number of dice possible.
* If only one die is usable, prefer the higher one.
*/
function getAllMovesForDice(board, bar, borneOff, player, diceRemaining) {
var allSequences = [];
var maxLength = 0;
function dfs(curBoard, curBar, curBorneOff, remainingDice, sequence) {
var foundMove = false;
for (var i = 0; i < remainingDice.length; i++) {
var die = remainingDice[i];
var moves = getValidMoves(curBoard, curBar, curBorneOff, player, die);
if (moves.length > 0) {
foundMove = true;
var nextDice = remainingDice.slice();
nextDice.splice(i, 1);
for (var m = 0; m < moves.length; m++) {
var result = applyMove(curBoard, curBar, curBorneOff, player, moves[m]);
var newSeq = sequence.concat([moves[m]]);
dfs(result.board, result.bar, result.borneOff, nextDice, newSeq);
}
}
}
if (!foundMove || remainingDice.length === 0) {
if (sequence.length > maxLength) {
maxLength = sequence.length;
}
allSequences.push(sequence);
}
}
dfs(board, bar, borneOff, diceRemaining, []);
// Filter: keep only sequences that use the maximum number of dice
var filtered = [];
for (var i = 0; i < allSequences.length; i++) {
if (allSequences[i].length === maxLength) {
filtered.push(allSequences[i]);
}
}
// If only one die is usable (maxLength === 1) and we had 2 different dice,
// prefer the higher die
if (maxLength === 1 && diceRemaining.length === 2 && diceRemaining[0] !== diceRemaining[1]) {
var higherDie = Math.max(diceRemaining[0], diceRemaining[1]);
var higherMoves = [];
var lowerMoves = [];
for (var j = 0; j < filtered.length; j++) {
var move = filtered[j][0];
// Determine which die was used by checking distance
var dist;
if (move.from === 'bar') {
if (player === 0) dist = 24 - move.to;
else dist = move.to + 1;
} else if (move.to === 'off') {
if (player === 0) dist = move.from + 1;
else dist = 24 - move.from;
} else {
dist = Math.abs(move.to - move.from);
}
if (dist === higherDie) {
higherMoves.push(filtered[j]);
} else {
lowerMoves.push(filtered[j]);
}
}
// Only prefer higher if higher moves exist; otherwise keep lower
if (higherMoves.length > 0) {
filtered = higherMoves;
}
}
return filtered;
}
/**
* Check if player has won (all 15 checkers borne off).
*/
function checkWin(borneOff, player) {
return borneOff[player] === TOTAL_CHECKERS;
}
/**
* Determine win type: 'normal', 'gammon', or 'backgammon'.
*/
function getWinType(board, bar, borneOff, winner) {
var loser = 1 - winner;
// If opponent has borne off at least 1 checker, it's a normal win
if (borneOff[loser] > 0) return 'normal';
// Gammon: opponent has 0 borne off
// Backgammon: opponent has 0 borne off AND (has checker on bar OR in winner's home)
var hasOnBar = bar[loser] > 0;
var hasInWinnerHome = false;
var winnerHomeStart = HOME_RANGE[winner][0];
var winnerHomeEnd = HOME_RANGE[winner][1];
var loserSign = (loser === 0) ? 1 : -1;
for (var i = winnerHomeStart; i <= winnerHomeEnd; i++) {
if (board[i] * loserSign > 0) {
hasInWinnerHome = true;
break;
}
}
if (hasOnBar || hasInWinnerHome) return 'backgammon';
return 'gammon';
}
/**
* Calculate pip count for a player (total distance to bear off).
*/
function pipCount(board, bar, player) {
var total = 0;
// Bar checkers: 25 pips each
total += bar[player] * 25;
for (var i = 0; i < 24; i++) {
if (player === 0 && board[i] > 0) {
// Player 0: checker at point X contributes X+1 pips
total += board[i] * (i + 1);
} else if (player === 1 && board[i] < 0) {
// Player 1: checker at point X contributes 24-X pips
total += Math.abs(board[i]) * (24 - i);
}
}
return total;
}
/**
* Roll two dice.
*/
function rollDice() {
var d1 = Math.floor(Math.random() * 6) + 1;
var d2 = Math.floor(Math.random() * 6) + 1;
return [d1, d2];
}
/**
* Get remaining dice to play. Doubles give 4 moves.
*/
function getDiceRemaining(d1, d2) {
if (d1 === d2) {
return [d1, d1, d1, d1];
}
return [d1, d2];
}
return {
INITIAL_BOARD: INITIAL_BOARD,
PLAYER_DIRECTION: PLAYER_DIRECTION,
HOME_RANGE: HOME_RANGE,
BAR_ENTRY_POINTS: BAR_ENTRY_POINTS,
TOTAL_CHECKERS: TOTAL_CHECKERS,
DICE_FACES: DICE_FACES,
QUICK_MESSAGES: QUICK_MESSAGES,
isAllHome: isAllHome,
getValidMoves: getValidMoves,
applyMove: applyMove,
canMove: canMove,
getAllMovesForDice: getAllMovesForDice,
checkWin: checkWin,
getWinType: getWinType,
pipCount: pipCount,
rollDice: rollDice,
getDiceRemaining: getDiceRemaining
};
})();
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