Commit 84398f94 authored by Mahmoud Aglan's avatar Mahmoud Aglan

feat: add domino + backgammon friend invite flow (Phase 2.7, 3.5, 4.7)

- friends.js: invite dialog now shows all 4 games in a 2x2 grid
- friends.php: create/check/accept/decline backgammon invites
- lobby.js: route to backgammon-game scene, show correct icon/label/color
- Time options hidden for non-chess games
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent 1325db19
...@@ -365,6 +365,31 @@ if ($method === 'POST') { ...@@ -365,6 +365,31 @@ if ($method === 'POST') {
'game_key' => $gameKey, 'game_key' => $gameKey,
'time_control' => $timeControl 'time_control' => $timeControl
]); ]);
} elseif ($gameKey === 'backgammon') {
$variant = $input['variant'] ?? 'sheshbesh';
$isWhite = rand(0, 1) === 0;
$whiteId = $isWhite ? $userId : $targetId;
$blackId = $isWhite ? $targetId : $userId;
$match = $sdb->insert('backgammon_matches', [
'status' => 'waiting',
'white_player_id' => $whiteId,
'black_player_id' => $blackId,
'variant' => $variant,
'game_state' => ['invite_from' => $userId, 'invite_to' => $targetId, 'invite_t' => time()],
'current_turn' => 'white',
'created_at' => gmdate('c')
]);
if (isset($match['error'])) jsonError($match['error']);
$matchId = $match[0]['id'] ?? $match['id'] ?? null;
jsonResponse([
'match_id' => $matchId,
'color' => $isWhite ? 'w' : 'b',
'game_key' => $gameKey,
'time_control' => $timeControl
]);
} else { } else {
// Chess // Chess
$initialTime = 600000; $initialTime = 600000;
...@@ -510,6 +535,34 @@ if ($method === 'POST') { ...@@ -510,6 +535,34 @@ if ($method === 'POST') {
} }
} }
// Check backgammon_matches table
$bgInvites = $sdb->get('backgammon_matches', [
'or' => "(white_player_id.eq.{$userId},black_player_id.eq.{$userId})",
'status' => 'eq.waiting',
'select' => 'id,white_player_id,black_player_id,game_state,variant,created_at',
'order' => 'created_at.desc',
'limit' => 5
]);
if (is_array($bgInvites) && !isset($bgInvites['error'])) {
foreach ($bgInvites as $inv) {
$gs = is_array($inv['game_state']) ? $inv['game_state'] : json_decode($inv['game_state'] ?? '{}', true);
$inviteTo = $gs['invite_to'] ?? null;
$inviteT = $gs['invite_t'] ?? 0;
if ($inviteTo !== $userId) continue;
if ($now - $inviteT > 120) continue;
$fromId = $gs['invite_from'] ?? null;
if ($fromId && in_array($fromId, $blockedIds)) continue;
$result[] = [
'match_id' => $inv['id'],
'game_key' => 'backgammon',
'time_control' => 'standard',
'from_id' => $fromId,
'created_at' => $inv['created_at']
];
}
}
jsonResponse(['invites' => $result]); jsonResponse(['invites' => $result]);
} }
...@@ -554,7 +607,6 @@ if ($method === 'POST') { ...@@ -554,7 +607,6 @@ if ($method === 'POST') {
if (!is_array($matches) || isset($matches['error']) || empty($matches)) { if (!is_array($matches) || isset($matches['error']) || empty($matches)) {
jsonError('Invite not found or expired'); jsonError('Invite not found or expired');
} }
// Verify current user is the invite target
$gs = is_array($matches[0]['game_state']) ? $matches[0]['game_state'] : json_decode($matches[0]['game_state'] ?? '{}', true); $gs = is_array($matches[0]['game_state']) ? $matches[0]['game_state'] : json_decode($matches[0]['game_state'] ?? '{}', true);
if (($gs['invite_to'] ?? '') !== $userId) { if (($gs['invite_to'] ?? '') !== $userId) {
jsonError('Not authorized to accept this invite', 403); jsonError('Not authorized to accept this invite', 403);
...@@ -570,6 +622,30 @@ if ($method === 'POST') { ...@@ -570,6 +622,30 @@ if ($method === 'POST') {
'player_index' => $playerIndex !== false ? $playerIndex : 1, 'player_index' => $playerIndex !== false ? $playerIndex : 1,
'started' => true 'started' => true
]); ]);
} elseif ($gameKey === 'backgammon') {
$matches = $sdb->get('backgammon_matches', [
'id' => 'eq.' . $matchId,
'status' => 'eq.waiting',
'or' => "(white_player_id.eq.{$userId},black_player_id.eq.{$userId})",
'select' => 'id,white_player_id,black_player_id,game_state',
'limit' => 1
]);
if (!is_array($matches) || isset($matches['error']) || empty($matches)) {
jsonError('Invite not found or expired');
}
$gs = is_array($matches[0]['game_state']) ? $matches[0]['game_state'] : json_decode($matches[0]['game_state'] ?? '{}', true);
if (($gs['invite_to'] ?? '') !== $userId) {
jsonError('Not authorized to accept this invite', 403);
}
$sdb->update('backgammon_matches', [
'status' => 'in_progress'
], ['id' => 'eq.' . $matchId]);
$color = ($matches[0]['white_player_id'] === $userId) ? 'w' : 'b';
jsonResponse([
'match_id' => $matchId,
'color' => $color,
'started' => true
]);
} else { } else {
// Chess (default) // Chess (default)
$matches = $sdb->get('matches', [ $matches = $sdb->get('matches', [
...@@ -641,6 +717,24 @@ if ($method === 'POST') { ...@@ -641,6 +717,24 @@ if ($method === 'POST') {
'status' => 'completed', 'status' => 'completed',
'result' => 'declined' 'result' => 'declined'
], ['id' => 'eq.' . $matchId, 'status' => 'eq.waiting']); ], ['id' => 'eq.' . $matchId, 'status' => 'eq.waiting']);
} elseif ($gameKey === 'backgammon') {
$matches = $sdb->get('backgammon_matches', [
'id' => 'eq.' . $matchId,
'status' => 'eq.waiting',
'select' => 'id,game_state',
'limit' => 1
]);
if (!is_array($matches) || isset($matches['error']) || empty($matches)) {
jsonError('Invite not found or expired');
}
$gs = is_array($matches[0]['game_state']) ? $matches[0]['game_state'] : json_decode($matches[0]['game_state'] ?? '{}', true);
if (($gs['invite_to'] ?? '') !== $userId) {
jsonError('Not authorized to decline this invite', 403);
}
$sdb->update('backgammon_matches', [
'status' => 'completed',
'result' => 'declined'
], ['id' => 'eq.' . $matchId, 'status' => 'eq.waiting']);
} else { } else {
$matches = $sdb->get('matches', [ $matches = $sdb->get('matches', [
'id' => 'eq.' . $matchId, 'id' => 'eq.' . $matchId,
......
...@@ -37,8 +37,8 @@ export function mountLobby(el, params = {}) { ...@@ -37,8 +37,8 @@ export function mountLobby(el, params = {}) {
const friendAvatar = friendProfile?.avatar_url; const friendAvatar = friendProfile?.avatar_url;
const tcLabel = formatTimeControl(timeControl); const tcLabel = formatTimeControl(timeControl);
const gameLabel = gameKey === 'ludo' ? t('game.ludo') : gameKey === 'domino' ? t('game.domino') : t('game.chess'); const gameLabel = gameKey === 'ludo' ? t('game.ludo') : gameKey === 'domino' ? t('game.domino') : gameKey === 'backgammon' ? t('game.backgammon') : t('game.chess');
const gameIcon = gameKey === 'ludo' ? emoji('dice', '🎲', 20) : gameKey === 'domino' ? emoji('domino_tile', '🁣', 20) : emoji('chess_pawn', '♟', 20); const gameIcon = gameKey === 'ludo' ? emoji('dice', '🎲', 20) : gameKey === 'domino' ? emoji('domino_tile', '🁣', 20) : gameKey === 'backgammon' ? emoji('backgammon', '🎯', 20) : emoji('chess_pawn', '♟', 20);
el.innerHTML = ` el.innerHTML = `
<div class="lobby-layout"> <div class="lobby-layout">
...@@ -50,7 +50,7 @@ export function mountLobby(el, params = {}) { ...@@ -50,7 +50,7 @@ export function mountLobby(el, params = {}) {
<!-- Match Info --> <!-- Match Info -->
<div class="lobby-match-info"> <div class="lobby-match-info">
<div class="lobby-game-badge" style="background:${gameKey === 'chess' ? 'var(--chess-primary)' : gameKey === 'ludo' ? 'var(--purple)' : 'var(--emerald)'};"> <div class="lobby-game-badge" style="background:${gameKey === 'chess' ? 'var(--chess-primary)' : gameKey === 'ludo' ? 'var(--purple)' : gameKey === 'backgammon' ? 'var(--amber)' : 'var(--emerald)'};">
<span style="font-size:20px;">${gameIcon}</span> <span style="font-size:20px;">${gameIcon}</span>
<span style="font-size:13px;font-weight:600;">${gameLabel}</span> <span style="font-size:13px;font-weight:600;">${gameLabel}</span>
</div> </div>
...@@ -152,7 +152,7 @@ async function pollMatchStatus(el, params) { ...@@ -152,7 +152,7 @@ async function pollMatchStatus(el, params) {
pollInFlight = true; pollInFlight = true;
const gameKey = params.gameKey || 'chess'; const gameKey = params.gameKey || 'chess';
const endpoint = gameKey === 'ludo' ? 'ludo-match.php' : gameKey === 'domino' ? 'domino-match.php' : 'game.php'; const endpoint = gameKey === 'ludo' ? 'ludo-match.php' : gameKey === 'domino' ? 'domino-match.php' : gameKey === 'backgammon' ? 'backgammon-match.php' : 'game.php';
try { try {
const res = await net.post(endpoint, { action: 'get', match_id: matchId }); const res = await net.post(endpoint, { action: 'get', match_id: matchId });
...@@ -232,6 +232,13 @@ function startGame(el, params) { ...@@ -232,6 +232,13 @@ function startGame(el, params) {
playerIndex: params.playerIndex ?? (params.isHost ? 0 : 1), playerIndex: params.playerIndex ?? (params.isHost ? 0 : 1),
isFriendly: true isFriendly: true
}); });
} else if (gameKey === 'backgammon') {
scene.replace('backgammon-game', {
mode: 'live',
matchId: params.matchId,
color: color,
isFriendly: true
});
} }
} }
......
...@@ -417,9 +417,11 @@ function showInviteDialog(content, targetId, targetName) { ...@@ -417,9 +417,11 @@ function showInviteDialog(content, targetId, targetName) {
<div style="font-size:16px;font-weight:700;color:var(--text-primary);margin-bottom:4px;">${t('challenge.send')}${targetName}</div> <div style="font-size:16px;font-weight:700;color:var(--text-primary);margin-bottom:4px;">${t('challenge.send')}${targetName}</div>
<div style="font-size:12px;color:var(--text-muted);margin-bottom:16px;">${t('challenge.select_game')}</div> <div style="font-size:12px;color:var(--text-muted);margin-bottom:16px;">${t('challenge.select_game')}</div>
<div style="display:flex;gap:8px;justify-content:center;margin-bottom:12px;"> <div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:12px;">
<button class="inv-game active" data-game="chess" style="flex:1;padding:10px;border-radius:10px;background:var(--chess-primary);border:2px solid var(--chess-primary);color:#fff;font-size:13px;font-weight:600;cursor:pointer;font-family:inherit;">${emoji('chess_pawn', '♟', 13)} ${t('game.chess')}</button> <button class="inv-game active" data-game="chess" style="padding:10px;border-radius:10px;background:var(--chess-primary);border:2px solid var(--chess-primary);color:#fff;font-size:13px;font-weight:600;cursor:pointer;font-family:inherit;">${emoji('chess_pawn', '♟', 13)} ${t('game.chess')}</button>
<button class="inv-game" data-game="ludo" style="flex:1;padding:10px;border-radius:10px;background:var(--bg-card);border:2px solid var(--border);color:var(--text-secondary);font-size:13px;font-weight:600;cursor:pointer;font-family:inherit;">${emoji('dice', '🎲', 13)} ${t('game.ludo')}</button> <button class="inv-game" data-game="ludo" style="padding:10px;border-radius:10px;background:var(--bg-card);border:2px solid var(--border);color:var(--text-secondary);font-size:13px;font-weight:600;cursor:pointer;font-family:inherit;">${emoji('dice', '🎲', 13)} ${t('game.ludo')}</button>
<button class="inv-game" data-game="domino" style="padding:10px;border-radius:10px;background:var(--bg-card);border:2px solid var(--border);color:var(--text-secondary);font-size:13px;font-weight:600;cursor:pointer;font-family:inherit;">${emoji('domino_tile', '🁣', 13)} ${t('game.domino')}</button>
<button class="inv-game" data-game="backgammon" style="padding:10px;border-radius:10px;background:var(--bg-card);border:2px solid var(--border);color:var(--text-secondary);font-size:13px;font-weight:600;cursor:pointer;font-family:inherit;">${emoji('backgammon', '🎯', 13)} ${t('game.backgammon')}</button>
</div> </div>
<div id="time-options" style="display:flex;gap:6px;justify-content:center;margin-bottom:16px;flex-wrap:wrap;"> <div id="time-options" style="display:flex;gap:6px;justify-content:center;margin-bottom:16px;flex-wrap:wrap;">
...@@ -452,9 +454,8 @@ function showInviteDialog(content, targetId, targetName) { ...@@ -452,9 +454,8 @@ function showInviteDialog(content, targetId, targetName) {
btn.style.color = '#fff'; btn.style.color = '#fff';
btn.classList.add('active'); btn.classList.add('active');
selectedGame = btn.dataset.game; selectedGame = btn.dataset.game;
// Hide time options for ludo
const timeDiv = dialog.querySelector('#time-options'); const timeDiv = dialog.querySelector('#time-options');
timeDiv.style.display = selectedGame === 'ludo' ? 'none' : 'flex'; timeDiv.style.display = selectedGame === 'chess' ? 'flex' : 'none';
}); });
}); });
......
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