• Mahmoud Aglan's avatar
    fix: repair chess multiplayer and build the missing tournament engine · 33fd4d40
    Mahmoud Aglan authored
    Four independent defects each broke competitive chess on their own, and they
    compounded. Verified against live production (Supabase, the Swiss API and the
    CapRover deployment), not inferred from code alone. Full diagnosis in FIX_PLAN.md.
    
    A. Match completion was rejected for every decisive game.
       The client sends result 'win'|'loss'|'draw'; game.php's $validResults did not
       contain 'win' or 'loss', so every won or lost game got a 400. That list was
       also wrong in its own right — timeout_white, abandon_white, resign_white,
       checkmate_* and bot_* are not in the public.match_result enum. And
       handleComplete ignored the write's return value, so a Postgres rejection was
       invisible and the endpoint still reported success. Consequence: no rating, no
       coins, no XP, matches stuck in_progress forever, tournament results lost, and
       the winner shown a fabricated +12.
    
       The client now reports only *how* the game ended. The server derives the
       winner from the final position (the mated side is the side to move), from who
       resigned, or from whose clock expired, validates against the real enum, and
       verifies the write landed. A losing player can no longer report a win.
    
    B. Both players could be assigned White — "playing against yourself".
       `playerColor = color || 'w'` defaulted both players to White whenever a caller
       supplied no colour, and the poll only ingests the opponent's move while it is
       *not* your turn, so two Whites never saw each other and each played a private
       board. The async correction also wrote gameState.playerColor while the clock
       closures still read a stale local copy.
    
       Colour now has one source of truth. game.php returns my_color computed
       server-side; the board stays inert until the server confirms it. Fixed the
       callers that passed no colour (refresh recovery, group invites — which also
       lacked mode:'live' and so started a bot game) and stopped lobby.js guessing
       from isHost.
    
    C. Finished matches were never released. unmountGame never tore down the match
       session, so localStorage kept the recovery key; combined with A, every app
       start dragged players back into a dead game with no colour, straight into B.
    
    D. Hand-offs were destroyed. matchmaking.php deleted all of a player's queue
       rows including one already matched and carrying a match_id, so the waiting
       player never learned their match and their opponent sat alone. Tournament
       create-or-join did a read-then-insert with no lock, so both paired players
       could create their own match. Match ids are now derived from
       (tournament, round, board): both clients compute the same id and the primary
       key decides, so exactly one match exists per pairing.
    
    E. There was no tournament engine at all. Nothing created a Swiss tournament,
       consumed auto_start, generated pairings, advanced a round or computed
       standings. api/swiss.php sent no Authorization header, so every call to the
       external pairing service returned 401 and each getter swallowed it and
       returned an empty array — which is why standings and pairings have always
       rendered blank.
    
       includes/tournament-engine.php is a native Dutch-system Swiss engine with no
       external dependency: backtracking pairing that never repeats an opponent,
       FIDE colour allocation, byes, Buchholz cut-1 / Buchholz / Sonneborn-Berger
       tiebreaks, automatic round advance and final placings. api/tournament-admin.php
       creates and starts tournaments; api/cron.php is the scheduler that acts on
       auto_start and forfeits no-shows.
    
    Also fixed
    - Server authority: turn ownership, move-count monotonicity, FEN transition
      sanity and a server-side clock, so a client can no longer overwrite the
      position or report its own remaining time.
    - Disconnect detection was dead: match-live marked the opponent active on every
      poll, and match-session's abandon branch was unreachable behind the disconnect
      branch's latch. Heartbeats are now recorded per player in game_state.
    - Draw acceptance required no offer from the opponent — a way out of a lost game.
    - Removed the in-process HTTP call back into our own Apache for tournament
      reporting (deadlock risk, and it lost results silently when it failed).
    - Three time controls in the picker (blitz_5_5, rapid_20_0, classical_45_45) are
      not in the time_control enum; choosing one created no match.
    - Auth did an upstream GoTrue call plus a ban query on every request, including
      the 2s poll. Now verifies HS256 locally when the secret is set, otherwise
      memoises briefly.
    - curl_close() removed repo-wide: deprecated in 8.5, where its notice lands in
      the middle of every JSON body. Dockerfile now turns display_errors off.
    
    Tests: tests/run.sh — 5 suites against a disposable local Postgres carrying the
    production schema. Covers 2-100 player tournaments, 167 real master-game move
    transitions (no legal move is ever rejected), the pairing race, JWT verification,
    and a full two-client game and tournament over HTTP.
    Co-Authored-By: 's avatarClaude Opus 5 (1M context) <noreply@anthropic.com>
    33fd4d40
constants.php 1.65 KB