1. 01 Sep, 2026 1 commit
    • 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
  2. 15 Aug, 2026 1 commit
  3. 10 Jul, 2026 1 commit
  4. 03 Jul, 2026 34 commits
  5. 25 Jun, 2026 3 commits