Commit 7372dc29 authored by Mahmoud Aglan's avatar Mahmoud Aglan

fix: chess resign/draw sync — opponent now sees both actions

- match-live.js: always forward poll data to game (not only on move_count change)
  This was the root cause — resign/draw don't increment move_count, so the
  game never processed the status change or game_state update
- game.php: all match operations now use supabaseService() to bypass RLS
  (handleGameMove, handleResign, handleComplete all failed silently with user token)
- Add dedicated 'draw' action in game.php that atomically marks match completed
- Draw acceptance now calls action:'draw' instead of writing game_state only
- handleResign validates match isn't already completed (prevents double-resign)
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent d63a4517
...@@ -27,6 +27,9 @@ switch ($action) { ...@@ -27,6 +27,9 @@ switch ($action) {
case 'resign': case 'resign':
handleResign($db, $userId, $input); handleResign($db, $userId, $input);
break; break;
case 'draw':
handleDraw($db, $userId, $input);
break;
case 'complete': case 'complete':
handleComplete($db, $userId, $input); handleComplete($db, $userId, $input);
break; break;
...@@ -96,6 +99,7 @@ function handleGameMove($db, string $userId, array $input): void { ...@@ -96,6 +99,7 @@ function handleGameMove($db, string $userId, array $input): void {
$matchId = $input['match_id'] ?? ''; $matchId = $input['match_id'] ?? '';
if (!$matchId) jsonError('match_id required'); if (!$matchId) jsonError('match_id required');
$sdb = supabaseService();
$update = ['updated_at' => date('c')]; $update = ['updated_at' => date('c')];
if (!empty($input['fen'])) $update['current_fen'] = $input['fen']; if (!empty($input['fen'])) $update['current_fen'] = $input['fen'];
...@@ -107,7 +111,6 @@ function handleGameMove($db, string $userId, array $input): void { ...@@ -107,7 +111,6 @@ function handleGameMove($db, string $userId, array $input): void {
// Merge game_state instead of overwriting — preserves emotes, draw offers, etc. // Merge game_state instead of overwriting — preserves emotes, draw offers, etc.
if (!empty($input['game_state'])) { if (!empty($input['game_state'])) {
$newState = json_decode($input['game_state'], true) ?: []; $newState = json_decode($input['game_state'], true) ?: [];
$sdb = supabaseService();
$matches = $sdb->get('matches', ['id' => 'eq.' . $matchId, 'select' => 'game_state', 'limit' => 1]); $matches = $sdb->get('matches', ['id' => 'eq.' . $matchId, 'select' => 'game_state', 'limit' => 1]);
$existing = []; $existing = [];
if (is_array($matches) && !empty($matches) && !isset($matches['error'])) { if (is_array($matches) && !empty($matches) && !isset($matches['error'])) {
...@@ -118,7 +121,7 @@ function handleGameMove($db, string $userId, array $input): void { ...@@ -118,7 +121,7 @@ function handleGameMove($db, string $userId, array $input): void {
$update['game_state'] = json_encode($merged); $update['game_state'] = json_encode($merged);
} }
$result = $db->update('matches', $update, ['id' => 'eq.' . $matchId]); $result = $sdb->update('matches', $update, ['id' => 'eq.' . $matchId]);
if (isset($result['error'])) jsonError($result['error']); if (isset($result['error'])) jsonError($result['error']);
jsonResponse(['success' => true]); jsonResponse(['success' => true]);
...@@ -128,13 +131,16 @@ function handleResign($db, string $userId, array $input): void { ...@@ -128,13 +131,16 @@ function handleResign($db, string $userId, array $input): void {
$matchId = $input['match_id'] ?? ''; $matchId = $input['match_id'] ?? '';
if (!$matchId) jsonError('match_id required'); if (!$matchId) jsonError('match_id required');
$match = $db->getOne('matches', ['id' => 'eq.' . $matchId]); $sdb = supabaseService();
$matches = $sdb->get('matches', ['id' => 'eq.' . $matchId, 'select' => 'id,white_player_id,black_player_id,status', 'limit' => 1]);
$match = (is_array($matches) && !empty($matches) && !isset($matches['error'])) ? $matches[0] : null;
if (!$match) jsonError('Match not found', 404); if (!$match) jsonError('Match not found', 404);
if ($match['status'] === 'completed') jsonError('Match already ended');
$isWhite = $match['white_player_id'] === $userId; $isWhite = $match['white_player_id'] === $userId;
$result = $isWhite ? 'black_wins' : 'white_wins'; $result = $isWhite ? 'black_wins' : 'white_wins';
$db->update('matches', [ $sdb->update('matches', [
'status' => 'completed', 'status' => 'completed',
'result' => $result, 'result' => $result,
'ended_at' => date('c') 'ended_at' => date('c')
...@@ -143,6 +149,26 @@ function handleResign($db, string $userId, array $input): void { ...@@ -143,6 +149,26 @@ function handleResign($db, string $userId, array $input): void {
jsonResponse(['result' => $result]); jsonResponse(['result' => $result]);
} }
function handleDraw($db, string $userId, array $input): void {
$matchId = $input['match_id'] ?? '';
if (!$matchId) jsonError('match_id required');
$sdb = supabaseService();
$matches = $sdb->get('matches', ['id' => 'eq.' . $matchId, 'select' => 'id,status', 'limit' => 1]);
$match = (is_array($matches) && !empty($matches) && !isset($matches['error'])) ? $matches[0] : null;
if (!$match) jsonError('Match not found', 404);
if ($match['status'] === 'completed') jsonError('Match already ended');
$sdb->update('matches', [
'status' => 'completed',
'result' => 'draw',
'ended_at' => date('c'),
'game_state' => json_encode(['draw_accepted' => true])
], ['id' => 'eq.' . $matchId]);
jsonResponse(['result' => 'draw']);
}
function handleComplete($db, string $userId, array $input): void { function handleComplete($db, string $userId, array $input): void {
$matchId = $input['match_id'] ?? ''; $matchId = $input['match_id'] ?? '';
$result = $input['result'] ?? ''; $result = $input['result'] ?? '';
...@@ -155,7 +181,8 @@ function handleComplete($db, string $userId, array $input): void { ...@@ -155,7 +181,8 @@ function handleComplete($db, string $userId, array $input): void {
jsonError('match_id and result are required'); jsonError('match_id and result are required');
} }
$db->update('matches', [ $sdb = supabaseService();
$sdb->update('matches', [
'status' => 'completed', 'status' => 'completed',
'result' => $result, 'result' => $result,
'current_fen' => $fen, 'current_fen' => $fen,
...@@ -165,7 +192,7 @@ function handleComplete($db, string $userId, array $input): void { ...@@ -165,7 +192,7 @@ function handleComplete($db, string $userId, array $input): void {
// Calculate Elo rating change // Calculate Elo rating change
$ratingCol = getRatingColumn($timeControl); $ratingCol = getRatingColumn($timeControl);
$profiles = $db->get('profiles', ['id' => 'eq.' . $userId, 'select' => $ratingCol . ',games_played,total_wins,total_draws,total_losses,win_streak,coins,xp,level', 'limit' => 1]); $profiles = $sdb->get('profiles', ['id' => 'eq.' . $userId, 'select' => $ratingCol . ',games_played,total_wins,total_draws,total_losses,win_streak,coins,xp,level', 'limit' => 1]);
$profile = is_array($profiles) && !empty($profiles) ? $profiles[0] : null; $profile = is_array($profiles) && !empty($profiles) ? $profiles[0] : null;
if ($profile) { if ($profile) {
...@@ -186,10 +213,9 @@ function handleComplete($db, string $userId, array $input): void { ...@@ -186,10 +213,9 @@ function handleComplete($db, string $userId, array $input): void {
$updates['win_streak'] = 0; $updates['win_streak'] = 0;
} }
$db->update('profiles', $updates, ['id' => 'eq.' . $userId]); $sdb->update('profiles', $updates, ['id' => 'eq.' . $userId]);
// Record rating history // Record rating history
$sdb = supabaseService();
$sdb->insert('rating_history', [ $sdb->insert('rating_history', [
'player_id' => $userId, 'player_id' => $userId,
'game_key' => 'chess', 'game_key' => 'chess',
......
...@@ -21,12 +21,12 @@ export function start(matchId, gameType, options = {}) { ...@@ -21,12 +21,12 @@ export function start(matchId, gameType, options = {}) {
ui.showConnectionRestored(); ui.showConnectionRestored();
} }
// Detect new move // Always forward data so game can process resign/draw/state changes
const moveCount = data.move_count || data.current_turn || 0; const moveCount = data.move_count || data.current_turn || 0;
if (moveCount > lastMoveCount) { if (moveCount > lastMoveCount) {
lastMoveCount = moveCount; lastMoveCount = moveCount;
onMove?.(data);
} }
onMove?.(data);
// Detect game ended externally // Detect game ended externally
if (data.status === 'completed') { if (data.status === 'completed') {
......
...@@ -815,9 +815,8 @@ function checkDrawOffer(el, rawGameState, myId) { ...@@ -815,9 +815,8 @@ function checkDrawOffer(el, rawGameState, myId) {
dialog.querySelector('#draw-accept').addEventListener('click', () => { dialog.querySelector('#draw-accept').addEventListener('click', () => {
dialog.remove(); dialog.remove();
net.post('game.php', { net.post('game.php', {
action: 'move', action: 'draw',
match_id: gameState.matchId, match_id: gameState.matchId
game_state: JSON.stringify({ draw_accepted: true, draw_offer: null, draw_offer_t: null })
}); });
endGame('draw', 'agreement'); endGame('draw', 'agreement');
}); });
......
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