1. 27 Oct, 2026 11 commits
    • Mahmoud Aglan's avatar
    • Mahmoud Aglan's avatar
      feat(native): app link verification files for the native app · 78dc09f8
      Mahmoud Aglan authored
      /.well-known/apple-app-site-association (team HXAUF476A7, bundle
      com.al-arcade.el3ab: /t/ and /r/ links open the app) and assetlinks.json
      (package com.alarcade.el3ab; the debug key for now, the release and Play
      signing keys join once known).
      Co-Authored-By: 's avatarClaude Opus 5.5 <noreply@anthropic.com>
      78dc09f8
    • Mahmoud Aglan's avatar
      feat(native): sign-in with the phone's account, and real push notifications · 41052e7e
      Mahmoud Aglan authored
      For the rebuilt native app (root/el3ab-mobile):
      - api/auth.php id_token: sign in with a Google or Apple ID token from the
        phone (GoTrue grant_type=id_token), instead of a web redirect Google
        refuses inside a WebView.
      - Push through FCM HTTP v1 (includes/push.php): every notifications row is
        queued in push_outbox by a trigger, whoever wrote it (this app, the
        manager, SQL functions), and sent to the player's phones; challenges and
        chat messages queue a push of their own. Sent right after the request
        that made them (a fire-and-forget call to api/push.php dispatch) and by
        the scheduler every minute; dead tokens are dropped. api/push.php keeps
        the phones (register / unregister) and sends a test for the manager.
        The Firebase service account lives in platform_secrets (service only),
        set from the manager.
      - The web bridge reports every refreshed session to the shell, asks for the
        native sign-in when the login screen opens inside the app, routes tapped
        pushes (tournament, chat, challenge), and answers the back button. The
        CSS that hid the web tab bar for a native bar is gone.
      Co-Authored-By: 's avatarClaude Opus 5.5 <noreply@anthropic.com>
      41052e7e
    • Mahmoud Aglan's avatar
      fix(tournaments): banners in the tournament list loaded without the storage key · 1a4bd1e5
      Mahmoud Aglan authored
      The list built a tournament-media URL without ?apikey= when the public
      helpers were not loaded; Kong refused it and every banner in the hub was a
      broken image.
      Co-Authored-By: 's avatarClaude Opus 5.5 <noreply@anthropic.com>
      1a4bd1e5
    • Mahmoud Aglan's avatar
      feat(analytics): every day a player opens the app, for the manager · d9aedb66
      Mahmoud Aglan authored
      player_daily_activity (one row per player per Cairo day: first and last
      seen, time in the app, platform) is written by the presence pulse the app
      already sends, inside el3ab_social_pulse (no extra call); the app now sends
      its platform (android, ios, web). Backfilled from the games, coins and
      messages already on record. manager_insights(p_days) computes the
      dashboard's numbers in one call. Service role only.
      Co-Authored-By: 's avatarClaude Opus 5.5 <noreply@anthropic.com>
      d9aedb66
    • Mahmoud Aglan's avatar
      feat(play): a bot plays for a player who drops; tournaments wait, then rule · 7267057e
      Mahmoud Aglan authored
      Casual online games: a player gone 30 s gets a ring on the other screen
      (no claim button); at 60 s a bot plays their seat until they are back, and
      the game goes on to its natural end. Nobody is handed a win for a drop.
      - Chess: the server plays the absent side (game.php standin_move: the
        engine near their rating; the caller never picks the move), through the
        normal move path, so clock and position checks apply. includes/chess.php
        chessApplyUci() applies the engine's move (castling, en passant,
        promotion, counters).
      - Backgammon: the client still at the table plays the seat with the bot
        and publishes each action tagged with the seat it was for; a client that
        comes back to actions played for it rebuilds from the snapshot.
      - Ludo: three silent turns no longer end a casual game; the table's bot
        keeps the seat. Domino already played gone seats; it now never ends a
        casual game on a drop.
      - A player still away when the game ends is not paid (no AFK farming).
      - A "they left" claim while the other player is plainly at the table is
        refused by the server in every game.
      - Casual tables everyone left 15+ minutes ago are closed by the scheduler
        (unpaid), and bot games idle a day; 184 rows had been stuck in progress.
      
      Tournament games: no bot. The tournament sets how long to wait and what
      then: a forfeit, or a draw (chess). The ring counts the tournament's wait,
      the server checks the absence itself (match_presence), and the scheduler
      applies the rule when no client is left to claim it.
      
      Tests: tests/play/standin-api.mjs (server rules, chess), a backgammon
      stand-in section; resume, online and tournament suites pass.
      Co-Authored-By: 's avatarClaude Opus 5.5 <noreply@anthropic.com>
      7267057e
    • Mahmoud Aglan's avatar
      copy: the main dictionary in Egyptian, short; fixes the suites turned up · 62d54dbf
      Mahmoud Aglan authored
      All 690 stiff or wordy Arabic lines of i18n.js now read the way an Egyptian
      game talks: ماتش، كسبت، البوت، ليفل، أونلاين، لايف، الباسورد، إنت;
      بنـ… for "loading", اتـ… for "done", مفيش for "none". Chess speaks like
      Egyptian players (العسكري، كش مات، بولت/بليتز); domino too (دقّ، الدُش).
      Meaning fixes: checkmate read "check"; a domino tip gave the opposite
      advice; the master rank shared the legendary rarity's word; a bye read
      like a promotion.
      
      Found by the suites:
      - Eight sign-in errors had no strings (auth.err_*): a failed login showed
        the raw key. Added in both languages.
      - Domino: when the other player reported a too-short match first, the
        winner saw 0 coins with no reason. The server now says too_short to the
        later reporter as well.
      - Ludo: the knocked-over pawn on a last place sank under the finishing
        order; it rests inside its box now, at every phone height.
      - Tests: the ludo sticker check accepts the fallen pawn; the chess puzzle
        failure step copes with a position that has no wrong, non-mating move.
      Co-Authored-By: 's avatarClaude Opus 5.5 <noreply@anthropic.com>
      62d54dbf
    • Mahmoud Aglan's avatar
    • Mahmoud Aglan's avatar
      copy: Egyptian, short, human: server messages and module strings · 097add47
      Mahmoud Aglan authored
      The server's error, notification and share text and the module string
      tables (orgs, codes, tournaments, stages, hosting) read the way an Egyptian
      game talks: fewer words, no MSA, the loanwords players use (ماتش، ليفل،
      كود، لينك، لايف). Server and app now say the same thing for the same error.
      A bye is راحة everywhere; the queen is الوزير in the opening names too.
      
      Fixes found on the way:
      - Tournament refusals lost their numbers: the hosting catalog registers the
        plain err_* lines first, so "your rating is 1200, this starts at 1500"
        showed as "rating too low". refusalText() has its own refuse_* keys now.
      - "wait N minutes" uses دقيقة، دقيقتين، دقايق by count.
      - The certificate check page told people to scan a QR the PNG does not have.
      - The double-elimination error said the stage was not a knockout.
      Co-Authored-By: 's avatarClaude Opus 5.5 <noreply@anthropic.com>
      097add47
    • Mahmoud Aglan's avatar
      chore(server): the once-a-minute scheduler for the tournament engine and campaigns · be626bdc
      Mahmoud Aglan authored
      tools/server/el3ab-cron.sh runs api/cron.php (player app) and the manager's
      api/cron/notifications under flock, logging only failures. Installed on
      169.58.165.0 as /usr/local/bin/el3ab-cron.sh with /etc/cron.d/el3ab-cron;
      the service key lives in /etc/el3ab-cron.headers (root, 600).
      Co-Authored-By: 's avatarClaude Opus 5.5 <noreply@anthropic.com>
      be626bdc
    • Mahmoud Aglan's avatar
      feat(tournaments): big-tournament hosting (P1-P13) and club chat · 89610750
      Mahmoud Aglan authored
      Stages and mixed formats: Swiss, round robin and knockout chained in any
      order (Swiss then a top-8 bracket, groups then knockout), with atomic
      pairing, byes, tie-breaks and advancement between stages. Knockout brackets
      with multi-game ties, third place and reopen. Teams and inter-club events,
      with team match scoring.
      
      Registration: extra fields per event (phone, country across the Arab world
      with its governorates, uploads), review and waitlist, check-in, members-only
      and late join, fee waivers. Presentation: media, sponsors and ad slots in
      the app and on the public sign-up page, link previews, certificates and a
      calendar feed. Prizes settle and release with the results; disputes get
      their own desk.
      
      Ludo, domino and backgammon play in tournaments too (heats for ludo), and the
      tournament screens open each game's own scene. Clubs and tournaments get
      their own chat, kept in step with membership from the app, the manager
      (api/org-feed.php) and the cron tick. Only tournament staff can create
      events.
      
      Fixes: match chat lines were readable by any signed-in player; deleting an
      org with a club chat failed; heat reports in the same second ranked wrong;
      started events showed a relative date. Two unused scenes removed.
      
      Tests: hosting logic suites, admin, registration, bracket and detail UI,
      games, links and org feed, all green on a rehearsal of this tree.
      Co-Authored-By: 's avatarClaude Opus 5.5 <noreply@anthropic.com>
      89610750
  2. 26 Oct, 2026 9 commits
    • Mahmoud Aglan's avatar
      fix(db): clients keep only SELECT on el3ab_tournaments · 6340e392
      Mahmoud Aglan authored
      Write grants (TRUNCATE included) were still open to anon and authenticated;
      nothing on the client writes tournaments. Applied.
      Co-Authored-By: 's avatarClaude Opus 5.5 <noreply@anthropic.com>
      6340e392
    • Mahmoud Aglan's avatar
      docs: the hosting contract every package codes against · 47593d10
      Mahmoud Aglan authored
      The schema as applied (read back from production), the allowed values and the
      conflict targets, the decisions taken where the spec left room, the JSON
      shapes, the SQL functions and PHP hooks each package exposes, the API
      contracts with one error shape and one Arabic error catalog, the manager
      routes with their permission keys and the org-scope guard, file ownership,
      the test harness, and notes for each package.
      Co-Authored-By: 's avatarClaude Opus 5.5 <noreply@anthropic.com>
      47593d10
    • Mahmoud Aglan's avatar
      test: a production replica, a PostgREST-shaped shim, and the hosting migrations in the suite · 41180847
      Mahmoud Aglan authored
      tests/schema.sql is now generated from read-only dumps of production: the 35
      tables the tournament system touches, with their checks, keys, indexes, RLS,
      policies and grants, and the production functions the hosting packages build
      on (tournament_register, _withdraw, _refund_entry, _refund_due, _settle,
      _redeem_tournament, redeem_fold, complete_match, award_coins). It used to be a
      hand-written subset without any of those functions.
      
      tests/run.sh loads it strictly, applies every 2026-10-27-hosting-*.sql (the
      schema file first), proves a second run leaves the schema identical and that
      the hosting tables stay locked to the service role, then runs every
      tests/test_hosting_*.php after the existing suites.
      
      PgShim now behaves like PostgREST where the engine relies on it: rpc() runs the
      real SQL function with named arguments and the client's return shapes, upsert()
      exists, jsonb, arrays and numerics come back typed, and a PHP list written to
      an array column becomes a Postgres array. The test router's supabaseRpc() goes
      through it.
      
      The suite was red before this (3 passed, 2 failed): registration and
      settlement are SQL functions now, and the shim answered every rpc with []. The
      end-to-end test also crashed at section 9, so sections 9 to 21 never ran; its
      heartbeat checks still looked in game_state, while heartbeats moved to
      match_presence. Now: 8 passed, 0 failed.
      Co-Authored-By: 's avatarClaude Opus 5.5 <noreply@anthropic.com>
      41180847
    • Mahmoud Aglan's avatar
      feat(db): the big-tournament hosting schema (P0) · 873278e4
      Mahmoud Aglan authored
      One idempotent migration for every hosting package: stages carry their own
      rounds, teams, team matches and stage entries, playable brackets, check-in,
      waitlist, registration forms and answers, prize categories and a prize hold,
      presentation (media, sponsors, ad slots, announcements), disputes,
      certificates, notices, forbidden pairs, tournament columns on the backgammon,
      domino and ludo tables, and club and lobby groups. Two storage buckets:
      tournament-media (public) and tournament-uploads (private).
      
      The 21 tournament tables keep RLS on with the service role only: anon and
      authenticated hold no grant at all (they read nothing there today; RLS had
      no policy for them). el3ab_tournaments and el3ab_tournament_rounds keep the
      read policies Realtime needs.
      
      Applied to production on 2026-10-26 after a backup in
      /root/backups/2026-10-27-hosting-schema/, then run a second time: the schema
      dumps before and after the second run are identical, and the 9 tournaments,
      16 registrations and 39 rounds are unchanged (the 4 legacy string pairings
      hold objects, so none was unwrapped). A commented rollback block is at the end
      of the file, tested inside BEGIN/ROLLBACK on production.
      
      Departures from the spec, each recorded in docs/tournament-hosting-contract.md:
      'unpaired' rounds stay legal only for the old Swiss service, a team event must
      have a team size, dispute_window_minutes gets a column, groups get kind and
      tournament_id.
      Co-Authored-By: 's avatarClaude Opus 5.5 <noreply@anthropic.com>
      873278e4
    • Mahmoud Aglan's avatar
      test: mark every test account, and prove the pools never mix · 9ce197dd
      Mahmoud Aglan authored
      Every account helper in tests/play sets profiles.is_test right after the
      account is made (guest or GoTrue admin), before it can queue, and fails
      loudly when the marker did not stick.
      
      tests/play/match-pool.mjs: two test accounts and two unmarked stand-ins for
      real players, made with the admin API (no guest signups). In every public
      queue, in a bucket the app never offers a real player where the game has
      one, a test account is not seated with a real player waiting longer, by the
      queue call or the status poll; two test accounts are seated together on both
      paths; a real player searching passes a waiting test account; two real
      players still pair; domino's public tables keep apart the same way; and a
      player's own token cannot set is_test either way. Against the code before
      the fix it fails 42 checks; after, 63 of 63 pass. Teardown is verified.
      Co-Authored-By: 's avatarClaude Opus 5.5 <noreply@anthropic.com>
      9ce197dd
    • Mahmoud Aglan's avatar
      fix(matchmaking): test accounts and real players never meet in a public queue · f5207cc4
      Mahmoud Aglan authored
      The play tests run against production, and a test account waiting in the
      public chess queue was seated across a real player (karimelshafei22), who
      then played an account that vanished with the test's teardown. Nothing in
      any queue knew a test account from a person.
      
      profiles.is_test (migrations/2026-10-27-test-accounts.sql, applied) marks
      an account the tests made. It is a column, not a name: usernames and
      display names are the player's own choice, so a name marker could drop a
      real player into the test pool. The profiles guard keeps the column out of
      a player's token, both ways. The migration also marks the 23 ZZZ-* test
      accounts earlier runs left behind.
      
      includes/match-pool.php keeps every search to the searcher's pool: the
      queue call and the status pickup of an earlier queuer in chess, ludo,
      domino and backgammon, and domino's public tables of three and four.
      Unknown means no: a profile that cannot be read is seated with nobody. The
      bot fill only ever holds the player's own row, after that search, so it
      needs no change. Real players among themselves pair exactly as before.
      Co-Authored-By: 's avatarClaude Opus 5.5 <noreply@anthropic.com>
      f5207cc4
    • Mahmoud Aglan's avatar
      feat: invite codes and links, player-side orgs, guest upgrade, and the game honouring the manager · 90ab18f7
      Mahmoud Aglan authored
      Invite codes and links (one system for org, tournament, group and promo codes)
      - Redeem screen, invitation card, reveal and history; a code box at signup and
        login; links (/r/CODE, ?org=, ?code=, ?join=) survive reloads, a closed tab
        and the Google redirect; WhatsApp/Telegram previews with a shaped Arabic share
        card. Canonical codes match the SQL exactly (tests share the vectors).
      - A guest who registers is upgraded in place: same account, coins, history and
        held invites.
      - Organisations, player side only (staff manage them in the manager): browse,
        read-only org page with announcements, join, apply with a private proof,
        leave, primary org on the profile. Proofs moved to a private bucket.
      - The groups join box resolves any code through the same rate-limited path.
      
      The game honours the manager
      - Feature flags and maintenance mode, which games are on and their order,
        daily reward and Elo K from reward_config, a ban screen with the reason,
        ads on the table with counted impressions and clicks.
      - Tournaments: drafts hidden, registration window, rating limits, members
        only, entry fee charged with the seat and refunded on withdraw or cancel,
        prizes paid once at the end, announcements and the live page link, a
        paused state, withdrawn players keep their opponents' points.
      - Draws and losses are paid; the battle pass counts per season; the shop
        respects availability; the game keeps its own art (the manager cannot
        change logos, tab icons or bot portraits).
      
      Fixes
      - Realtime channels get the refreshed token and rejoin when dropped; a chat
        that failed its first load keeps trying; no vibration before the first tap;
        org and redeem notices in the bell; Dockerfile adds intl and gd.
      
      Data: Arab countries and regions dataset for tournament forms. Docs: redeem
      API, tournament hosting spec.
      Co-Authored-By: 's avatarClaude Opus 5.5 <noreply@anthropic.com>
      90ab18f7
    • Mahmoud Aglan's avatar
      fix(chess): bot games crashed on resign and finish · 6ce8f229
      Mahmoud Aglan authored
      A top-level const in api/game.php was declared below the dispatch, so it was
      not defined yet when finishing a bot game called chessBotTooShort(): every
      bot game ended in "Undefined constant", was never closed and never paid.
      Declared above the dispatch now.
      
      Also the applied follow-up migration: account deletion no longer fails for
      code creators and reviewers, legacy group codes are unique by canonical form,
      tournament codes register through tournament_register (no drafts, window and
      fee honoured), the table keeps its game order, the test ad is paused.
      Co-Authored-By: 's avatarClaude Opus 5.5 <noreply@anthropic.com>
      6ce8f229
    • Mahmoud Aglan's avatar
      fix(security): close coin minting and open write policies · f4eec81f
      Mahmoud Aglan authored
      Found while building the org/redeem layer; all exploitable with the public
      anon key that ships in realtime.js, or with a player's own session.
      
      - complete_match, award_coins and the match/queue functions were executable
        by anon and players: one REST call paid win coins to anyone. Service role
        only now; every caller is PHP with the service key.
      - battlepass.php credited whatever "coins" the request carried, to anyone,
        as often as asked. The server now decides the reward, needs an active
        season and an unlocked tier, and pays each tier once.
      - The legacy api/ludo.php let any player end any ludo match with any winners
        and get paid. It, api/domino.php and api/multiplayer.php were unused by the
        app and are removed.
      - Shop, challenges and daily reward wrote balances with the player's token,
        which only worked because players could PATCH their own coins over REST.
        They write with the service key now; the shop deducts only if the balance
        is unchanged (a double tap cannot pay twice), refunds if the grant fails,
        and records its transaction (the old insert used a missing column).
      - Policies: anyone could delete any ludo/domino/backgammon match, rewrite
        queues, groups and the theme, insert an "accepted" friendship with anyone
        and post into any chat. Writes are service-role only; every read and
        Realtime feed is unchanged. Group members read only their own memberships.
      - profiles-guard.sql (applied after this deploy): a player's token may change
        only cosmetic profile fields.
      
      Also the org-unify / redeem SQL layer and its contract (docs/redeem-contract.md).
      Co-Authored-By: 's avatarClaude Opus 5.5 <noreply@anthropic.com>
      f4eec81f
  3. 25 Oct, 2026 2 commits
    • Mahmoud Aglan's avatar
      feat: player feedback round across all four games, plus sessions that survive bad lines · 0b076f00
      Mahmoud Aglan authored
      Domino
      - Public tables for 2, 3 and 4 seats, including the four-player free-for-all
        (domino_table_join/leave, domino_merge_state); bot matches pay real coins.
      - The Egyptian lock rule: a blocked game is scored, the opponent no longer
        draws the boneyard.
      
      Backgammon (tawla)
      - 31: the first runner must reach the home board before the rest may move.
      - New board colours so the white checkers read clearly (--bg-* tokens,
        DESIGN.md, branding admin).
      
      Matchmaking
      - After 60 s with no opponent, a procedurally generated player-like bot takes
        the seat (ghost_profiles); unrated, paid once, 90 s anti-farm floor.
      
      Chess and ludo
      - Premoves merged; draw on flag when the side left cannot mate; bot payout
        guard; ludo seat guard and rules fixes.
      
      Social and sessions
      - Chat: the sender on the right, the other person on the left; a thread whose
        first load failed keeps retrying instead of going silent; creating a group
        survives a dropped answer without making a second group.
      - Group chat messages readable by members only (RLS).
      - A slow auth service no longer signs players out: the server answers 503
        instead of a cached 401, refresh only ends a session the server refused,
        and realtime channels receive the refreshed token.
      - complete_match raises levels with XP.
      
      Docs: the manager/game integration audit and the org invites + events plan.
      Co-Authored-By: 's avatarClaude Opus 5.5 <noreply@anthropic.com>
      0b076f00
    • Mahmoud Aglan's avatar
      feat(social, multiplayer): working chat and groups, reliable challenges, true... · d951ad46
      Mahmoud Aglan authored
      feat(social, multiplayer): working chat and groups, reliable challenges, true VS, resume sync, emotes from the sender
      
      Social: chat sides came from a stored user id that Google sign-in never set,
      so every message looked like the other player's; the server now marks each
      message mine and the client reads its id from the token (core/me.js). Chat
      rebuilt (my bubbles inline-end, grouping, times, seen, typing, reactions,
      quick replies), live delivery fixed, groups fully working (join by code or
      link, admins, unread, owner hand-over), presence, dock/bell/friend badges,
      friend notifications, news feed, invite links, profile actions.
      tests/play/social.mjs 106/106.
      
      Challenges: the invite was only seen on the Friends screen; a global prompt
      (play/invite-watch.js) now shows it everywhere with a countdown. Chess
      declines failed on the result enum; the handshake lives in match_invites
      with conditional updates, expiry and challenger presence. VS: self vs self
      came from the stored id and profile.php answering with the caller when the
      id was missing; every player's card now travels with the hand-off.
      Backgammon simultaneous searches and ludo table sizes fixed.
      tests/play/challenge.mjs 72/72 (4 games x 6 scenarios x 3 rounds).
      
      Resume: leaving the screen dropped the saved match; your own sleep was
      blamed on the opponent; heartbeats raced moves inside game_state (now
      match_presence); moves on a bad line were dropped (outbox) and late retries
      rolled boards back (stale steps refused); frozen loops restart on return.
      Emotes appear at the sender's plate on both screens, per seat in ludo.
      Resume and emote scenarios pass in all four suites.
      
      Migrations applied: social, match-invites, match-presence.
      Co-Authored-By: 's avatarClaude Opus 5.5 <noreply@anthropic.com>
      d951ad46
  4. 19 Oct, 2026 18 commits
    • Mahmoud Aglan's avatar
    • Mahmoud Aglan's avatar
      feat(ludo): bot games pay; one roll key; start key always on screen · f56437fc
      Mahmoud Aglan authored
      A bot game gets its own ludo_matches row (action start, mode bot), and
      complete pays its creator once through complete_match with the bot seats
      as null: nothing under 90 s, nothing for a walked-away game, 403 for
      anyone else. The result card shows the real coins and XP.
      
      From the design review: the die is the roll key (#roll-btn), an idle full
      face on a lip in the roller's colour, instead of a die beside a button
      with a red pip in a glowing ring; plates use the shared avatar (purple
      rounded square for people, ink with the robot for bots); the room's start
      key is pinned above the dock with compact seat cards, checked uncovered at
      360x640, 390x844 and 430x932. tests/play/ludo.mjs 83/84 then 37/37 for
      the local games after fixing the opening roll in the one lucky check.
      Co-Authored-By: 's avatarClaude Opus 5.5 <noreply@anthropic.com>
      f56437fc
    • Mahmoud Aglan's avatar
      docs(design): DESIGN.md for Night Arcade; flat faces, no gloss rims · 41dbece4
      Mahmoud Aglan authored
      DESIGN.md (and .impeccable/design.json) replace the June v3 spec with the
      shipped Night Arcade system, derived from tokens.css and core.css: the
      stage, ink tiles, white cards, keys on a lip, colour rationing, the board
      palettes, Rubik with the pinned display face, motion and feel, RTL, and
      the theming contract.
      
      The documenter's audit found the code breaking its own rules: a white rim
      on top of every key and the play key read as gloss (removed), the green
      and orange faces had a lighter top that dropped white text under 3:1
      (both are one flat colour now, orange deepened to 3.4:1), and small green
      keys carried 13-14px text at 3.1:1 (they use a deeper green, 5:1). The
      CTA glint stays, recorded as motion.
      Co-Authored-By: 's avatarClaude Opus 5.5 <noreply@anthropic.com>
      41dbece4
    • Mahmoud Aglan's avatar
      fix(backgammon): the table takes the whole screen; one avatar system · 3be7e45a
      Mahmoud Aglan authored
      The backgammon game never entered game mode, so the HUD and the dock
      stayed over the board (screenshots only hid it when an earlier game had
      left game mode on). It now enters on mount and exits on unmount like the
      other games. Avatars follow one rule across games: a person is a purple
      rounded square on a lip, a bot an ink rounded square with the robot
      (chess's player tile was blue, backgammon's were circles).
      backgammon.mjs --only result,layout: 27/27.
      Co-Authored-By: 's avatarClaude Opus 5.5 <noreply@anthropic.com>
      3be7e45a
    • Mahmoud Aglan's avatar
      fix(design): finish-review fixes for Night Arcade · bf86ce99
      Mahmoud Aglan authored
      From the finish review:
      - Contrast: white on the green face was 2.0:1; the green (and orange)
        button faces are deepened so white text clears 3:1. Green stickers on
        a green key render white instead of vanishing. The shop's common and
        legendary headers print in ink, on tokens.
      - HUD: the purses keep their plus keys on phones; the avatar with its
        level badge stands for the player and the name moves to the profile.
      - Domino: the table was purple-tinted (purple flooded the screen); it is
        plain ink, purple stays on the tile backs.
      - Home: the game cards overlap less so every name shows; the duplicate
        leaderboard entries (a side key and a chip, besides the dock tab) give
        way to a shop key; a live tournament is an ink tile with a red live
        bead, not a red slab.
      - Leaderboard rows stay inside the gutter; the player's own row is lit.
      - index.php FORM records that this world had no concept roll or comp.
      Co-Authored-By: 's avatarClaude Opus 5.5 <noreply@anthropic.com>
      bf86ce99
    • Mahmoud Aglan's avatar
      fix(api): an RPC that returns a number no longer crashes the request · 790663e2
      Mahmoud Aglan authored
      award_coins answers the new balance; json_decode gave an int and
      SupabaseClient::request(), typed to return an array, threw a TypeError.
      Every achievement that paid coins answered 500 after the coins were
      already granted (seen in production on achievements.php). Scalars now
      come back as ['result' => value]; error bodies that are not objects no
      longer index into a scalar.
      Co-Authored-By: 's avatarClaude Opus 5.5 <noreply@anthropic.com>
      790663e2
    • Mahmoud Aglan's avatar
      chore(design): remove the walnut texture and its leftovers · fee8349c
      Mahmoud Aglan authored
      Nothing paints wood any more; the texture, its credit and the legacy
      --wood-* tokens go.
      Co-Authored-By: 's avatarClaude Opus 5.5 <noreply@anthropic.com>
      fee8349c
    • Mahmoud Aglan's avatar
      fix(backgammon): forced moves, variant rules, online sync, bot payouts · a855527a
      Mahmoud Aglan authored
      Rules: move generation is rewritten so both dice must be played when they
      can be, otherwise the larger. 31 used a long-backgammon setup and allowed
      pins; it now starts like mahbousa, a single checker closes a point and the
      winner scores the loser's remaining checkers (lengths 1/11/21/31).
      Mahbousa could freeze forever when both starting stacks were pinned; the
      plakoto mother rule now settles it. Turning the cube off lasted one game,
      taking the bot's double froze the game, and a player on black saw the
      board upside down with a swapped score.
      
      Online: a poll could wipe unsent moves, the cube never synced, 'opponent'
      was sent as the winner id, and a refreshed tab rejoined as white. Online
      is now a numbered move log plus a snapshot; the other side replays the log
      with animation and checks it matches. Timers stop when the scene closes.
      
      Bot games get a server match row (action start) and complete pays the
      creator once; a row under 90 s old closes with no payout.
      
      Look: flat teal board, red and white points, checkers on a lip, flat dice
      and cube. backgammon-rules 659/659 (brute-force cross-check); browser
      suite 122 + online 39/39 + layout 20/20, zero console errors.
      Co-Authored-By: 's avatarClaude Opus 5.5 <noreply@anthropic.com>
      a855527a
    • Mahmoud Aglan's avatar
      fix(leaderboard): test accounts never rank; shoot.mjs cleans up late rows · 8a0227c2
      Mahmoud Aglan authored
      Accounts named ZZZ-... are test accounts (every script in tests/ and
      tools/ tags them so); the leaderboard skips them in every game instead of
      showing eight ZZZ-CYCLE rows at the top. shoot.mjs waits for requests the
      closing page had in flight and repeats its cleanup until the profile is
      gone: a bot match created at the last moment had kept one alive.
      Co-Authored-By: 's avatarClaude Opus 5.5 <noreply@anthropic.com>
      8a0227c2
    • Mahmoud Aglan's avatar
      chore(puzzles): back up three puzzles corrected in production · 003f4625
      Mahmoud Aglan authored
      p004 (tagged mate) let the defender take the rook; the black rook is gone
      so Ra8 is mate. p009 was tagged promotion with a rook move; it is a back-
      rank mate. p019 (tagged mate) left the queen hanging; it is now Qh8 mate
      with the king guarding. Rows before the change are in this file; all three
      answers were checked as mate with chess.js.
      Co-Authored-By: 's avatarClaude Opus 5.5 <noreply@anthropic.com>
      003f4625
    • Mahmoud Aglan's avatar
      fix(hud): the purse counts up to the balance after achievement coins · 6f4dbe11
      Mahmoud Aglan authored
      Achievement coins were paid by the check after the HUD had already pulled
      the player, so the purse showed the old balance until the next refresh.
      Co-Authored-By: 's avatarClaude Opus 5.5 <noreply@anthropic.com>
      6f4dbe11
    • Mahmoud Aglan's avatar
      fix(admin): a branding save no longer pins every default over tokens.css · 5c00e8d4
      Mahmoud Aglan authored
      The form posted every field, pre-filled with the Cafe Deck defaults, and
      the save stored each one in platform_theme, where stored values beat
      tokens.css: one save would have turned the app indigo again. It now sends
      only the fields that changed, and its defaults are the Night Arcade
      values. Pawn uploads go to the ludo_token_* slots the board now reads.
      Co-Authored-By: 's avatarClaude Opus 5.5 <noreply@anthropic.com>
      5c00e8d4
    • Mahmoud Aglan's avatar
      feat(chess): flat drawn pieces everywhere; white pieces get a readable edge · 32bfa830
      Mahmoud Aglan authored
      The uploaded piece images were heavily outlined old-style art; they are
      backed up to docs/branding-backup-2026-10-19-chess-pieces.json and cleared
      in platform_assets, so every board draws the flat Night Arcade pieces.
      White pieces get a thin mid-grey edge and a silver lip to read on the
      light squares. tools/shoot.mjs --drop-assets previews the built-in art.
      Co-Authored-By: 's avatarClaude Opus 5.5 <noreply@anthropic.com>
      32bfa830
    • Mahmoud Aglan's avatar
      fix(ludo): online boards stay in sync; rules, bots, rewards; flat board · 4ce3d90f
      Mahmoud Aglan authored
      Online: at speed a client's writes trail its own board, and a stale-row
      check read that lag as a stale row, reset the client and let it play again
      from the older board: two writers with one sequence number. The check now
      fires only with nothing in flight, every write is dropped while a resync
      is pending, and a resync takes the row's sequence. The quiet-player notice
      shows for each new silence, not once per match.
      
      Rules: in 3-4 player games the first finisher kept a turn nobody could
      take and the game stalled; three sixes is enforced and no longer carries
      into the next turn. Seats come from the match, not the room. Exactly one
      client settles a match, with real ids. Bots score their moves by
      difficulty. A leaver is announced and in a duel the one who stays wins.
      Names in the roll button are escaped.
      
      Look: flat quadrants on white, pawns on a lip, a flat tumbling die.
      tests/play/ludo.mjs 73/73 (local 4, duel, online), ludo-rules 60/60.
      Co-Authored-By: 's avatarClaude Opus 5.5 <noreply@anthropic.com>
      4ce3d90f
    • Mahmoud Aglan's avatar
      fix(chess): live games, clocks, refresh, puzzles; Night Arcade board · 95acecec
      Mahmoud Aglan authored
      Live games lost their move history (each opponent move arrived as a bare
      FEN), so threefold repetition never triggered and the PGN broke; moves now
      replay from the server's list. The mating move was sent after the game
      closed and never reached the opponent. A refreshed tab overwrote the
      server's move list with a short one; it now rebuilds from the server.
      The opponent got the increment twice, Black's clock started before the
      first move, and a flag is now confirmed with the server. Refreshing a bot
      game lost it; the bot accepted half of all draw offers at random and froze
      on an illegal reply. Promotion has a cancellable picker. The result card
      no longer invents rating changes for bot games, rematch after a live game
      searches at the same clock, and back to table no longer stacks a home.
      Analysis kept the evaluation sign and orientation for Black; review runs
      one engine call per position and caches. Puzzles accept an alternative
      mate and offer retry / show answer.
      
      Board: flat lavender in an ink frame, pieces on a lip, tokens only.
      tests/play/chess.mjs: 183/183 (bot, online with two accounts, puzzles,
      three phone sizes), zero console errors.
      Co-Authored-By: 's avatarClaude Opus 5.5 <noreply@anthropic.com>
      95acecec
    • Mahmoud Aglan's avatar
      feat(game): in-game shell and domino in Night Arcade; invite fixes · e4ccd12e
      Mahmoud Aglan authored
      Shell (match UI, player plates, dialogs, reactions, queue, VS, challenge):
      flat Night Arcade surfaces, turn timers on the plates, banners for your
      turn / time low / waiting, a claimable disconnect ring, and a cancel that
      waits for an in-flight join so it never leaves a ghost queue row.
      tests/play/shell.mjs: 144/147 (the 3 were the invite bugs below).
      
      Domino: flat white tiles on a lip, a purple-tinted table, playable tiles
      lifted, the hand fanned with translate. tests/play/domino.mjs: 785/785
      against the bot, scripted positions and online.
      
      friends.php: a ludo invite wrote a column ludo_matches does not have (400)
      and opened a four-seat table; domino and ludo invites stored players as a
      JSON string, so check-invites never showed them to the friend.
      Co-Authored-By: 's avatarClaude Opus 5.5 <noreply@anthropic.com>
      e4ccd12e
    • Mahmoud Aglan's avatar
      fix(reactions): backgammon players can send reactions in online games · b789888e
      Mahmoud Aglan authored
      isMatchParticipant() knew only ludo and domino matches, so every reaction
      poll in an online backgammon game answered 403 and nothing reached the
      opponent. Backgammon seats come from players or the colour columns.
      Co-Authored-By: 's avatarClaude Opus 5.5 <noreply@anthropic.com>
      b789888e
    • Mahmoud Aglan's avatar
      fix(hud): fits 360px phones; FIDE title strings · 0fa3571d
      Mahmoud Aglan authored
      On narrow phones the purse keeps its tap but drops its plus key and the
      name tightens, so the bell no longer falls off the screen edge. The eight
      FIDE title labels on the profile editor were missing.
      Co-Authored-By: 's avatarClaude Opus 5.5 <noreply@anthropic.com>
      0fa3571d