Commit 1c145579 authored by Mahmoud Aglan's avatar Mahmoud Aglan

fix: bulletproof matchmaking queue across all games

- Add dequeue handler to backgammon-match.php (was missing)
- Add 90-second stale entry cleanup to ludo/domino/backgammon queues
- Add atomic opponent claiming (conditional update) to prevent race conditions
- Fix client queue.js to route dequeue to correct game endpoint
- Register unmountQueue for proper cleanup when exiting search screen
- Remove json_encode on jsonb fields in backgammon (prevent double-encoding)

Fixes: self-matching, ghost queue entries, and race condition where two
players simultaneously claim the same opponent.
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent 6517bd2d
...@@ -17,6 +17,7 @@ $action = $input['action'] ?? ''; ...@@ -17,6 +17,7 @@ $action = $input['action'] ?? '';
switch ($action) { switch ($action) {
case 'queue': handleQueue($userId, $input); break; case 'queue': handleQueue($userId, $input); break;
case 'dequeue': handleBgDequeue($userId); break;
case 'status': handleStatus($userId); break; case 'status': handleStatus($userId); break;
case 'move': handleMove($userId, $input); break; case 'move': handleMove($userId, $input); break;
case 'get': handleGet($userId, $input); break; case 'get': handleGet($userId, $input); break;
...@@ -27,20 +28,34 @@ switch ($action) { ...@@ -27,20 +28,34 @@ switch ($action) {
default: jsonError('Invalid action'); default: jsonError('Invalid action');
} }
function handleBgDequeue(string $userId): void {
$sdb = supabaseService();
$sdb->delete('backgammon_queue', ['user_id' => 'eq.' . $userId]);
jsonResponse(['success' => true]);
}
function handleQueue(string $userId, array $input): void { function handleQueue(string $userId, array $input): void {
$sdb = supabaseService(); $sdb = supabaseService();
$variant = $input['variant'] ?? 'sheshbesh'; $variant = $input['variant'] ?? 'sheshbesh';
$matchLength = intval($input['match_length'] ?? 3); $matchLength = intval($input['match_length'] ?? 3);
// Clean old queue entries // STEP 1: Remove ALL existing entries for this player (prevents ghost/self-match)
$sdb->delete('backgammon_queue', ['user_id' => 'eq.' . $userId]); $sdb->delete('backgammon_queue', ['user_id' => 'eq.' . $userId]);
// Search for opponent // STEP 2: Clean stale entries from other players (older than 90 seconds)
$staleTime = gmdate('c', time() - 90);
$sdb->delete('backgammon_queue', [
'match_id' => 'is.null',
'created_at' => 'lt.' . $staleTime
]);
// STEP 3: Search for available opponent
$searchUrl = SUPABASE_REST . '/backgammon_queue' $searchUrl = SUPABASE_REST . '/backgammon_queue'
. '?user_id=neq.' . $userId . '?user_id=neq.' . $userId
. '&match_id=is.null' . '&match_id=is.null'
. '&variant=eq.' . $variant . '&variant=eq.' . $variant
. '&select=id,user_id' . '&select=id,user_id'
. '&order=created_at.asc'
. '&limit=1'; . '&limit=1';
$ch = curl_init($searchUrl); $ch = curl_init($searchUrl);
...@@ -48,7 +63,8 @@ function handleQueue(string $userId, array $input): void { ...@@ -48,7 +63,8 @@ function handleQueue(string $userId, array $input): void {
curl_setopt($ch, CURLOPT_HTTPHEADER, [ curl_setopt($ch, CURLOPT_HTTPHEADER, [
'apikey: ' . SUPABASE_SERVICE_KEY, 'apikey: ' . SUPABASE_SERVICE_KEY,
'Authorization: Bearer ' . SUPABASE_SERVICE_KEY, 'Authorization: Bearer ' . SUPABASE_SERVICE_KEY,
'Content-Type: application/json' 'Content-Type: application/json',
'Prefer: return=representation'
]); ]);
curl_setopt($ch, CURLOPT_TIMEOUT, 10); curl_setopt($ch, CURLOPT_TIMEOUT, 10);
$result = curl_exec($ch); $result = curl_exec($ch);
...@@ -57,28 +73,47 @@ function handleQueue(string $userId, array $input): void { ...@@ -57,28 +73,47 @@ function handleQueue(string $userId, array $input): void {
if (!empty($opponents) && isset($opponents[0])) { if (!empty($opponents) && isset($opponents[0])) {
$opponent = $opponents[0]; $opponent = $opponents[0];
$players = [$opponent['user_id'], $userId];
// Randomly assign colors // STEP 4: Atomically claim opponent — set match_id to a placeholder to prevent races
$claimResult = $sdb->update('backgammon_queue', [
'match_id' => '00000000-0000-0000-0000-000000000000'
], [
'id' => 'eq.' . $opponent['id'],
'match_id' => 'is.null'
]);
if (empty($claimResult) || isset($claimResult['error'])) {
// Opponent was grabbed by someone else — add self to queue
$sdb->insert('backgammon_queue', [
'user_id' => $userId,
'variant' => $variant,
'match_length' => $matchLength
]);
jsonResponse(['queued' => true]);
return;
}
// STEP 5: Create match
$players = [$opponent['user_id'], $userId];
$whiteIdx = rand(0, 1); $whiteIdx = rand(0, 1);
$colors = [$whiteIdx === 0 ? 'white' : 'black', $whiteIdx === 1 ? 'white' : 'black']; $colors = [$whiteIdx === 0 ? 'white' : 'black', $whiteIdx === 1 ? 'white' : 'black'];
$matchData = [ $matchData = [
'status' => 'in_progress', 'status' => 'in_progress',
'players' => json_encode($players), 'players' => $players,
'colors' => json_encode($colors), 'colors' => $colors,
'variant' => $variant, 'variant' => $variant,
'match_length' => $matchLength, 'match_length' => $matchLength,
'scores' => json_encode([0, 0]), 'scores' => [0, 0],
'current_turn' => $whiteIdx, 'current_turn' => $whiteIdx,
'game_state' => json_encode([ 'game_state' => [
'variant' => $variant, 'variant' => $variant,
'match_length' => $matchLength, 'match_length' => $matchLength,
'game_number' => 1, 'game_number' => 1,
'cube_value' => 1, 'cube_value' => 1,
'cube_owner' => null, 'cube_owner' => null,
'crawford' => false 'crawford' => false
]), ],
'host_id' => $opponent['user_id'], 'host_id' => $opponent['user_id'],
'created_by' => $userId 'created_by' => $userId
]; ];
......
...@@ -23,11 +23,18 @@ switch ($action) { ...@@ -23,11 +23,18 @@ switch ($action) {
case 'get': handleGet($userId, $input); break; case 'get': handleGet($userId, $input); break;
case 'resign': handleResign($userId, $input); break; case 'resign': handleResign($userId, $input); break;
case 'complete': handleComplete($userId, $input); break; case 'complete': handleComplete($userId, $input); break;
case 'dequeue': handleDequeue($userId); break;
case 'heartbeat': handleDominoHeartbeat($userId, $input); break; case 'heartbeat': handleDominoHeartbeat($userId, $input); break;
case 'leave': handleDominoLeave($userId, $input); break; case 'leave': handleDominoLeave($userId, $input); break;
default: jsonError('Invalid action'); default: jsonError('Invalid action');
} }
function handleDequeue(string $userId): void {
$sdb = supabaseService();
$sdb->delete('domino_queue', ['user_id' => 'eq.' . $userId]);
jsonResponse(['success' => true]);
}
function handleStart(string $userId, array $input): void { function handleStart(string $userId, array $input): void {
$sdb = supabaseService(); $sdb = supabaseService();
$mode = $input['mode'] ?? 'bot'; $mode = $input['mode'] ?? 'bot';
...@@ -69,12 +76,22 @@ function handleQueue(string $userId, array $input): void { ...@@ -69,12 +76,22 @@ function handleQueue(string $userId, array $input): void {
return; return;
} }
// STEP 1: Remove ALL existing entries for this player (prevents ghost/self-match)
$sdb->delete('domino_queue', ['user_id' => 'eq.' . $userId]); $sdb->delete('domino_queue', ['user_id' => 'eq.' . $userId]);
// STEP 2: Clean stale entries from other players (older than 90 seconds)
$staleTime = gmdate('c', time() - 90);
$sdb->delete('domino_queue', [
'match_id' => 'is.null',
'created_at' => 'lt.' . $staleTime
]);
// STEP 3: Search for available opponent
$searchUrl = SUPABASE_REST . '/domino_queue' $searchUrl = SUPABASE_REST . '/domino_queue'
. '?user_id=neq.' . $userId . '?user_id=neq.' . $userId
. '&match_id=is.null' . '&match_id=is.null'
. '&select=id,user_id' . '&select=id,user_id'
. '&order=created_at.asc'
. '&limit=1'; . '&limit=1';
$ch = curl_init($searchUrl); $ch = curl_init($searchUrl);
...@@ -91,6 +108,23 @@ function handleQueue(string $userId, array $input): void { ...@@ -91,6 +108,23 @@ function handleQueue(string $userId, array $input): void {
if (!empty($opponents) && isset($opponents[0])) { if (!empty($opponents) && isset($opponents[0])) {
$opponent = $opponents[0]; $opponent = $opponents[0];
// STEP 4: Atomically claim opponent — prevents race conditions
$claimResult = $sdb->update('domino_queue', [
'match_id' => '00000000-0000-0000-0000-000000000000'
], [
'id' => 'eq.' . $opponent['id'],
'match_id' => 'is.null'
]);
if (empty($claimResult) || isset($claimResult['error'])) {
// Opponent was grabbed by someone else — add self to queue
$sdb->insert('domino_queue', ['user_id' => $userId]);
jsonResponse(['queued' => true]);
return;
}
// STEP 5: Create match
$players = [$opponent['user_id'], $userId]; $players = [$opponent['user_id'], $userId];
$matchData = [ $matchData = [
...@@ -118,9 +152,11 @@ function handleQueue(string $userId, array $input): void { ...@@ -118,9 +152,11 @@ function handleQueue(string $userId, array $input): void {
'players' => $players, 'players' => $players,
'color' => 'player2' 'color' => 'player2'
]); ]);
return;
} }
} }
// No opponent found — add to queue
$sdb->insert('domino_queue', ['user_id' => $userId]); $sdb->insert('domino_queue', ['user_id' => $userId]);
jsonResponse(['queued' => true]); jsonResponse(['queued' => true]);
} }
......
...@@ -17,6 +17,7 @@ $action = $input['action'] ?? ''; ...@@ -17,6 +17,7 @@ $action = $input['action'] ?? '';
switch ($action) { switch ($action) {
case 'queue': handleLudoQueue($userId, $input); break; case 'queue': handleLudoQueue($userId, $input); break;
case 'dequeue': handleLudoDequeue($userId); break;
case 'status': handleLudoStatus($userId); break; case 'status': handleLudoStatus($userId); break;
case 'move': handleLudoMove($userId, $input); break; case 'move': handleLudoMove($userId, $input); break;
case 'get': handleLudoGet($userId, $input); break; case 'get': handleLudoGet($userId, $input); break;
...@@ -26,21 +27,35 @@ switch ($action) { ...@@ -26,21 +27,35 @@ switch ($action) {
default: jsonError('Invalid action'); default: jsonError('Invalid action');
} }
function handleLudoDequeue(string $userId): void {
$sdb = supabaseService();
$sdb->delete('ludo_queue', ['user_id' => 'eq.' . $userId]);
jsonResponse(['success' => true]);
}
function handleLudoQueue(string $userId, array $input): void { function handleLudoQueue(string $userId, array $input): void {
$sdb = supabaseService(); $sdb = supabaseService();
$requestedPlayers = intval($input['player_count'] ?? 4); $requestedPlayers = intval($input['player_count'] ?? 4);
if ($requestedPlayers < 2) $requestedPlayers = 2; if ($requestedPlayers < 2) $requestedPlayers = 2;
if ($requestedPlayers > 4) $requestedPlayers = 4; if ($requestedPlayers > 4) $requestedPlayers = 4;
// Clean old waiting entries for this player // STEP 1: Remove ALL existing entries for this player (prevents ghost/self-match)
$sdb->delete('ludo_queue', ['user_id' => 'eq.' . $userId]); $sdb->delete('ludo_queue', ['user_id' => 'eq.' . $userId]);
// Check for waiting opponents (need at least 1 to start) // STEP 2: Clean stale entries from other players (older than 90 seconds)
$staleTime = gmdate('c', time() - 90);
$sdb->delete('ludo_queue', [
'match_id' => 'is.null',
'created_at' => 'lt.' . $staleTime
]);
// STEP 3: Search for waiting opponents
$maxOpponents = $requestedPlayers - 1; $maxOpponents = $requestedPlayers - 1;
$searchUrl = SUPABASE_REST . '/ludo_queue' $searchUrl = SUPABASE_REST . '/ludo_queue'
. '?user_id=neq.' . $userId . '?user_id=neq.' . $userId
. '&match_id=is.null' . '&match_id=is.null'
. '&select=id,user_id' . '&select=id,user_id'
. '&order=created_at.asc'
. '&limit=' . $maxOpponents; . '&limit=' . $maxOpponents;
$ch = curl_init($searchUrl); $ch = curl_init($searchUrl);
...@@ -56,16 +71,36 @@ function handleLudoQueue(string $userId, array $input): void { ...@@ -56,16 +71,36 @@ function handleLudoQueue(string $userId, array $input): void {
$opponents = json_decode($result, true); $opponents = json_decode($result, true);
if (!empty($opponents) && isset($opponents[0])) { if (!empty($opponents) && isset($opponents[0])) {
// Found opponents — build player list, fill remaining with bots // STEP 4: Atomically claim opponents — prevents race conditions
$humanPlayers = [$opponents[0]['user_id'], $userId]; $claimedOpponents = [];
if (isset($opponents[1]) && $requestedPlayers >= 3) $humanPlayers[] = $opponents[1]['user_id']; foreach ($opponents as $opp) {
if (isset($opponents[2]) && $requestedPlayers >= 4) $humanPlayers[] = $opponents[2]['user_id']; $claimResult = $sdb->update('ludo_queue', [
'match_id' => '00000000-0000-0000-0000-000000000000'
], [
'id' => 'eq.' . $opp['id'],
'match_id' => 'is.null'
]);
if (!empty($claimResult) && !isset($claimResult['error'])) {
$claimedOpponents[] = $opp;
}
}
if (empty($claimedOpponents)) {
// All opponents were grabbed by others — add self to queue
$sdb->insert('ludo_queue', ['user_id' => $userId]);
jsonResponse(['queued' => true]);
return;
}
// STEP 5: Build player list from successfully claimed opponents
$humanPlayers = [$claimedOpponents[0]['user_id'], $userId];
if (isset($claimedOpponents[1]) && $requestedPlayers >= 3) $humanPlayers[] = $claimedOpponents[1]['user_id'];
if (isset($claimedOpponents[2]) && $requestedPlayers >= 4) $humanPlayers[] = $claimedOpponents[2]['user_id'];
$players = $humanPlayers; $players = $humanPlayers;
$botCount = $requestedPlayers - count($players); $botCount = $requestedPlayers - count($players);
for ($i = 1; $i <= $botCount; $i++) { $players[] = 'bot_' . $i; } for ($i = 1; $i <= $botCount; $i++) { $players[] = 'bot_' . $i; }
// Build positions array matching player count
$positions = []; $positions = [];
for ($i = 0; $i < $requestedPlayers; $i++) { for ($i = 0; $i < $requestedPlayers; $i++) {
$positions[] = ['pos' => [-1,-1,-1,-1]]; $positions[] = ['pos' => [-1,-1,-1,-1]];
...@@ -82,15 +117,14 @@ function handleLudoQueue(string $userId, array $input): void { ...@@ -82,15 +117,14 @@ function handleLudoQueue(string $userId, array $input): void {
'moves' => [], 'moves' => [],
'winners' => [], 'winners' => [],
'game_state' => ['turn_count' => 0], 'game_state' => ['turn_count' => 0],
'host_id' => $opponents[0]['user_id'] 'host_id' => $claimedOpponents[0]['user_id']
]; ];
$match = $sdb->insert('ludo_matches', $matchData); $match = $sdb->insert('ludo_matches', $matchData);
$matchId = $match[0]['id'] ?? $match['id'] ?? null; $matchId = $match[0]['id'] ?? $match['id'] ?? null;
if ($matchId) { if ($matchId) {
// Mark ALL opponents' queue entries with match_id foreach ($claimedOpponents as $opp) {
foreach ($opponents as $opp) {
$sdb->update('ludo_queue', ['match_id' => $matchId], ['id' => 'eq.' . $opp['id']]); $sdb->update('ludo_queue', ['match_id' => $matchId], ['id' => 'eq.' . $opp['id']]);
} }
...@@ -100,6 +134,7 @@ function handleLudoQueue(string $userId, array $input): void { ...@@ -100,6 +134,7 @@ function handleLudoQueue(string $userId, array $input): void {
'player_count' => $requestedPlayers, 'player_count' => $requestedPlayers,
'players' => $players 'players' => $players
]); ]);
return;
} }
} }
......
...@@ -34,41 +34,31 @@ function handleQueue($db, string $userId, array $input): void { ...@@ -34,41 +34,31 @@ function handleQueue($db, string $userId, array $input): void {
$gameKey = $input['game_key'] ?? 'chess'; $gameKey = $input['game_key'] ?? 'chess';
$timeControl = $input['time_control'] ?? 'rapid_10_0'; $timeControl = $input['time_control'] ?? 'rapid_10_0';
// Anti-abuse: check if player can queue (cooldown after repeated abandons) $sdb = supabaseService();
// Anti-abuse: check if player can queue
$canQueue = supabaseRpc('can_player_queue', ['p_player_id' => $userId]); $canQueue = supabaseRpc('can_player_queue', ['p_player_id' => $userId]);
if (is_array($canQueue) && isset($canQueue['allowed']) && !$canQueue['allowed']) { if (is_array($canQueue) && isset($canQueue['allowed']) && !$canQueue['allowed']) {
jsonResponse(['error' => 'cooldown', 'seconds_remaining' => $canQueue['seconds_remaining'] ?? 60]); jsonResponse(['error' => 'cooldown', 'seconds_remaining' => $canQueue['seconds_remaining'] ?? 60]);
return; return;
} }
// Use service key to bypass RLS for matchmaking operations // STEP 1: Remove ALL stale/existing entries for this player (prevents ghost entries)
$sdb = supabaseService(); $sdb->delete('matchmaking_queue', ['player_id' => 'eq.' . $userId]);
// STEP 2: Clean stale entries from other players (older than 90 seconds)
$staleTime = gmdate('c', time() - 90);
$sdb->delete('matchmaking_queue', [
'status' => 'eq.waiting',
'queued_at' => 'lt.' . $staleTime
]);
// Get player rating // STEP 3: Get player rating
$profiles = $sdb->get('profiles', ['id' => 'eq.' . $userId, 'select' => 'elo_rapid,elo_blitz,elo_bullet', 'limit' => 1]); $profiles = $sdb->get('profiles', ['id' => 'eq.' . $userId, 'select' => 'elo_rapid,elo_blitz,elo_bullet', 'limit' => 1]);
$profile = is_array($profiles) && !empty($profiles) ? $profiles[0] : []; $profile = is_array($profiles) && !empty($profiles) ? $profiles[0] : [];
$rating = $profile['elo_rapid'] ?? $profile['elo_blitz'] ?? 1200; $rating = $profile['elo_rapid'] ?? $profile['elo_blitz'] ?? 1200;
// Check if already in queue // STEP 4: Get block list
$existing = $sdb->get('matchmaking_queue', [
'player_id' => 'eq.' . $userId,
'status' => 'eq.waiting',
'select' => 'id'
]);
if (!empty($existing) && !isset($existing['error'])) {
// Already waiting — just check for opponent again
} else {
// Insert into queue first
$sdb->insert('matchmaking_queue', [
'player_id' => $userId,
'game_key' => $gameKey,
'time_control' => $timeControl,
'rating' => $rating,
'status' => 'waiting'
]);
}
// Get block list for this player (both directions)
$blockedIds = []; $blockedIds = [];
$blocks = $sdb->get('player_blocks', [ $blocks = $sdb->get('player_blocks', [
'or' => "(player_id.eq.{$userId},blocked_id.eq.{$userId})", 'or' => "(player_id.eq.{$userId},blocked_id.eq.{$userId})",
...@@ -81,7 +71,7 @@ function handleQueue($db, string $userId, array $input): void { ...@@ -81,7 +71,7 @@ function handleQueue($db, string $userId, array $input): void {
} }
} }
// Search for available opponent (service key bypasses RLS — can see ALL waiting players) // STEP 5: Search for available opponent
$excludeIds = array_merge([$userId], $blockedIds); $excludeIds = array_merge([$userId], $blockedIds);
$searchUrl = SUPABASE_REST . '/matchmaking_queue' $searchUrl = SUPABASE_REST . '/matchmaking_queue'
. '?game_key=eq.' . urlencode($gameKey) . '?game_key=eq.' . urlencode($gameKey)
...@@ -107,18 +97,41 @@ function handleQueue($db, string $userId, array $input): void { ...@@ -107,18 +97,41 @@ function handleQueue($db, string $userId, array $input): void {
if (!empty($opponents) && !isset($opponents['error']) && isset($opponents[0])) { if (!empty($opponents) && !isset($opponents['error']) && isset($opponents[0])) {
$opponent = $opponents[0]; $opponent = $opponents[0];
// Randomly assign colors // STEP 6: Atomically claim opponent — mark them as matched FIRST
// Use a conditional update: only succeeds if still 'waiting'
$claimResult = $sdb->update('matchmaking_queue', [
'status' => 'matched',
'matched_with' => $userId
], [
'id' => 'eq.' . $opponent['id'],
'status' => 'eq.waiting'
]);
// If claim failed (empty result = no rows matched), opponent was already taken
if (empty($claimResult) || isset($claimResult['error'])) {
// Opponent was grabbed by someone else — add self to queue and wait
$sdb->insert('matchmaking_queue', [
'player_id' => $userId,
'game_key' => $gameKey,
'time_control' => $timeControl,
'rating' => $rating,
'status' => 'waiting'
]);
jsonResponse(['queued' => true]);
return;
}
// STEP 7: Create match
$isWhite = rand(0, 1) === 0; $isWhite = rand(0, 1) === 0;
$whiteId = $isWhite ? $userId : $opponent['player_id']; $whiteId = $isWhite ? $userId : $opponent['player_id'];
$blackId = $isWhite ? $opponent['player_id'] : $userId; $blackId = $isWhite ? $opponent['player_id'] : $userId;
// Create match $initialTime = 600000;
$initialTime = 600000; // default 10 min
if (strpos($timeControl, 'bullet') !== false) $initialTime = 60000; if (strpos($timeControl, 'bullet') !== false) $initialTime = 60000;
elseif (strpos($timeControl, 'blitz') !== false) $initialTime = 300000; elseif (strpos($timeControl, 'blitz') !== false) $initialTime = 300000;
elseif (strpos($timeControl, 'classical') !== false) $initialTime = 3600000; elseif (strpos($timeControl, 'classical') !== false) $initialTime = 3600000;
$matchData = [ $match = $sdb->insert('matches', [
'game_key' => $gameKey, 'game_key' => $gameKey,
'match_type' => 'rated', 'match_type' => 'rated',
'white_player_id' => $whiteId, 'white_player_id' => $whiteId,
...@@ -129,35 +142,34 @@ function handleQueue($db, string $userId, array $input): void { ...@@ -129,35 +142,34 @@ function handleQueue($db, string $userId, array $input): void {
'white_time_remaining_ms' => $initialTime, 'white_time_remaining_ms' => $initialTime,
'black_time_remaining_ms' => $initialTime, 'black_time_remaining_ms' => $initialTime,
'current_fen' => 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1', 'current_fen' => 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1',
'moves' => '[]', 'moves' => [],
'move_count' => 0, 'move_count' => 0,
'game_state' => '{}', 'game_state' => (object)[],
'is_rated' => true, 'is_rated' => true,
'started_at' => date('c') 'started_at' => date('c')
]; ]);
$match = $sdb->insert('matches', $matchData);
$matchId = $match[0]['id'] ?? $match['id'] ?? null; $matchId = $match[0]['id'] ?? $match['id'] ?? null;
if ($matchId) { if ($matchId) {
// Mark OPPONENT as matched // Update opponent's queue entry with match_id
$sdb->update('matchmaking_queue', [ $sdb->update('matchmaking_queue', [
'status' => 'matched',
'matched_with' => $userId,
'match_id' => $matchId 'match_id' => $matchId
], ['id' => 'eq.' . $opponent['id']]); ], ['id' => 'eq.' . $opponent['id']]);
// Remove self from queue
$sdb->delete('matchmaking_queue', [
'player_id' => 'eq.' . $userId,
'status' => 'eq.waiting'
]);
$myColor = $isWhite ? 'w' : 'b'; $myColor = $isWhite ? 'w' : 'b';
jsonResponse(['match_id' => $matchId, 'color' => $myColor, 'opponent_id' => $opponent['player_id']]); jsonResponse(['match_id' => $matchId, 'color' => $myColor, 'opponent_id' => $opponent['player_id']]);
return;
} }
} }
// No opponent found — add self to queue
$sdb->insert('matchmaking_queue', [
'player_id' => $userId,
'game_key' => $gameKey,
'time_control' => $timeControl,
'rating' => $rating,
'status' => 'waiting'
]);
jsonResponse(['queued' => true]); jsonResponse(['queued' => true]);
} }
...@@ -173,7 +185,6 @@ function handleStatus($db, string $userId, array $input): void { ...@@ -173,7 +185,6 @@ function handleStatus($db, string $userId, array $input): void {
]); ]);
if (!empty($entry) && !isset($entry['error']) && isset($entry[0]['match_id'])) { if (!empty($entry) && !isset($entry['error']) && isset($entry[0]['match_id'])) {
// Found match! Get match details to know our color
$matchId = $entry[0]['match_id']; $matchId = $entry[0]['match_id'];
$match = $sdb->get('matches', ['id' => 'eq.' . $matchId, 'select' => 'white_player_id,black_player_id', 'limit' => 1]); $match = $sdb->get('matches', ['id' => 'eq.' . $matchId, 'select' => 'white_player_id,black_player_id', 'limit' => 1]);
$color = 'b'; $color = 'b';
...@@ -181,10 +192,11 @@ function handleStatus($db, string $userId, array $input): void { ...@@ -181,10 +192,11 @@ function handleStatus($db, string $userId, array $input): void {
$color = ($match[0]['white_player_id'] === $userId) ? 'w' : 'b'; $color = ($match[0]['white_player_id'] === $userId) ? 'w' : 'b';
} }
// Clean up queue // Clean up queue entry
$sdb->delete('matchmaking_queue', ['player_id' => 'eq.' . $userId]); $sdb->delete('matchmaking_queue', ['player_id' => 'eq.' . $userId]);
jsonResponse(['match_id' => $matchId, 'color' => $color]); jsonResponse(['match_id' => $matchId, 'color' => $color]);
return;
} }
jsonResponse(['waiting' => true]); jsonResponse(['waiting' => true]);
...@@ -192,9 +204,7 @@ function handleStatus($db, string $userId, array $input): void { ...@@ -192,9 +204,7 @@ function handleStatus($db, string $userId, array $input): void {
function handleDequeue($db, string $userId, array $input): void { function handleDequeue($db, string $userId, array $input): void {
$sdb = supabaseService(); $sdb = supabaseService();
$sdb->delete('matchmaking_queue', [ // Remove from chess matchmaking queue
'player_id' => 'eq.' . $userId, $sdb->delete('matchmaking_queue', ['player_id' => 'eq.' . $userId]);
'status' => 'eq.waiting'
]);
jsonResponse(['success' => true]); jsonResponse(['success' => true]);
} }
...@@ -2,13 +2,13 @@ import * as scene from '../../core/scene.js'; ...@@ -2,13 +2,13 @@ import * as scene from '../../core/scene.js';
import { mountTable } from './scenes/table.js'; import { mountTable } from './scenes/table.js';
import { mountBotSelect } from './scenes/bot-select.js'; import { mountBotSelect } from './scenes/bot-select.js';
import { mountTimeSelect } from './scenes/time-select.js'; import { mountTimeSelect } from './scenes/time-select.js';
import { mountQueue } from './scenes/queue.js'; import { mountQueue, unmountQueue } from './scenes/queue.js';
import { mountLobby } from './scenes/lobby.js'; import { mountLobby } from './scenes/lobby.js';
import { mountChallenge } from './scenes/challenge.js'; import { mountChallenge } from './scenes/challenge.js';
scene.register('play-table', mountTable); scene.register('play-table', mountTable);
scene.register('play-bot-select', mountBotSelect); scene.register('play-bot-select', mountBotSelect);
scene.register('play-time-select', mountTimeSelect); scene.register('play-time-select', mountTimeSelect);
scene.register('play-queue', mountQueue); scene.register('play-queue', mountQueue, unmountQueue);
scene.register('game-lobby', mountLobby); scene.register('game-lobby', mountLobby);
scene.register('challenge-friend', mountChallenge); scene.register('challenge-friend', mountChallenge);
...@@ -6,8 +6,17 @@ import { t } from '../../../core/i18n.js'; ...@@ -6,8 +6,17 @@ import { t } from '../../../core/i18n.js';
let unsub = null; let unsub = null;
let timer = null; let timer = null;
let activeParams = null;
export function unmountQueue() {
if (activeParams) {
leaveQueue(activeParams);
activeParams = null;
}
}
export function mountQueue(el, params) { export function mountQueue(el, params) {
activeParams = params;
let seconds = 0; let seconds = 0;
el.innerHTML = ` el.innerHTML = `
...@@ -40,6 +49,7 @@ export function mountQueue(el, params) { ...@@ -40,6 +49,7 @@ export function mountQueue(el, params) {
el.querySelector('#cancel-btn').addEventListener('click', () => { el.querySelector('#cancel-btn').addEventListener('click', () => {
audio.play('click'); audio.play('click');
leaveQueue(params); leaveQueue(params);
activeParams = null;
scene.pop(); scene.pop();
}); });
} }
...@@ -80,6 +90,7 @@ function pollForMatch(params) { ...@@ -80,6 +90,7 @@ function pollForMatch(params) {
function onMatchFound(data, params) { function onMatchFound(data, params) {
cleanup(); cleanup();
activeParams = null;
audio.play('notification'); audio.play('notification');
const gameScene = params.game === 'chess' ? 'chess-game' : params.game + '-game'; const gameScene = params.game === 'chess' ? 'chess-game' : params.game + '-game';
scene.replace(gameScene, { scene.replace(gameScene, {
...@@ -95,8 +106,9 @@ function onMatchFound(data, params) { ...@@ -95,8 +106,9 @@ function onMatchFound(data, params) {
async function leaveQueue(params) { async function leaveQueue(params) {
cleanup(); cleanup();
const endpoint = params.game === 'ludo' ? 'ludo-match.php' : params.game === 'domino' ? 'domino-match.php' : params.game === 'backgammon' ? 'backgammon-match.php' : 'matchmaking.php';
try { try {
await net.post('matchmaking.php', { action: 'dequeue', game_key: params.game }); await net.post(endpoint, { action: 'dequeue', game_key: params.game });
} catch (e) {} } catch (e) {}
} }
......
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