- 19 Oct, 2026 25 commits
-
-
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:Claude Opus 5.5 <noreply@anthropic.com>
-
Mahmoud Aglan authored
The owner rejected the walnut and leather look as dated. The new world: a quiet violet-black ink stage with a faint star lattice; flat ink tiles for lists (each row its own tile); white game cards on a lip; flat colour buttons on a solid lip the finger pushes down; a floating HUD and a floating dock with the green play key standing out of it; Rubik, self- hosted, with the display face pinned to ExtraBold. Stickers are rebuilt flat on a lip, the game art is redrawn flat, each game owns one hue. Home, HUD, rewards, auth, shop, settings, profile and the leaderboard are converted; game screens follow. Co-Authored-By:Claude Opus 5.5 <noreply@anthropic.com>
-
Mahmoud Aglan authored
Chess: a status poll that landed between the claim and the match id write deleted the claim, so the waiting player searched forever while the other sat alone. A claim in progress is now left alone for 3 minutes, dequeue reports it, a failed match insert releases it, and a waiting player's status poll claims an older waiting row. Domino gets the same status-side match. Raced 4/4 (chess) and 3/3 (domino) with simultaneous queue calls. Co-Authored-By:Claude Opus 5.5 <noreply@anthropic.com>
-
Mahmoud Aglan authored
justify-content centred the register form past the top of the screen; auto margins centre it without overflowing. The age checkbox is a carved slot that pops a green tick, and checkboxes squash and click like buttons. Co-Authored-By:Claude Opus 5.5 <noreply@anthropic.com>
-
Mahmoud Aglan authored
Each challenge shows its game's sticker (ludo_played, chess_played...) and a ready one glows gold until collected. The all-done panel was never shown; it now appears once every challenge is claimed. The API's fallback icons and the stored templates use sticker names instead of emoji. Co-Authored-By:Claude Opus 5.5 <noreply@anthropic.com>
-
Mahmoud Aglan authored
Both missed each other in the queue call and both inserted; status never tried to match, so they waited for the 60 s timeout. A waiting player's status poll now claims fresh rows queued before its own, after locking its own row, so two pollers can never claim each other. ludo_queue stores the requested table size for that path. Co-Authored-By:Claude Opus 5.5 <noreply@anthropic.com>
-
Mahmoud Aglan authored
decline-invite only accepted the invited player, so a host backing out of the lobby got 403 and the friend could still accept into an empty game for two minutes. The host now cancels (result 'cancelled'); the target still declines. Co-Authored-By:Claude Opus 5.5 <noreply@anthropic.com>
-
Mahmoud Aglan authored
Co-Authored-By:Claude Opus 5.5 <noreply@anthropic.com>
-
Mahmoud Aglan authored
Bot game scenes create domino/ludo/backgammon match rows and, now that payouts work, economy and rating rows; removeAccount left them behind. Twelve orphaned domino matches from earlier runs were deleted from production. Co-Authored-By:Claude Opus 5.5 <noreply@anthropic.com>
-
Mahmoud Aglan authored
Haptics (feel.haptic) are off when the player turns vibration off; the setting persists with the rest of the player's preferences. Co-Authored-By:Claude Opus 5.5 <noreply@anthropic.com>
-
Mahmoud Aglan authored
bounce-easing is the owner's direction (El3ab is a game: pops and overshoot); side-tab flags the 3D bevel under buttons and cards. The XP bar used to transition width (layout thrash); it now scales. Co-Authored-By:Claude Opus 5.5 <noreply@anthropic.com>
-
Mahmoud Aglan authored
Co-Authored-By:Claude Opus 5.5 <noreply@anthropic.com>
-
Mahmoud Aglan authored
Co-Authored-By:Claude Opus 5.5 <noreply@anthropic.com>
-
Mahmoud Aglan authored
Achievements were only checked when the achievements screen was opened, so a new one was never celebrated. After every game the HUD now refreshes the player from the server (the purse counts up, a level-up fires if earned), runs the achievement check and drops a gold banner for each new one with its coin reward, the reward jingle and a haptic. Co-Authored-By:Claude Opus 5.5 <noreply@anthropic.com>
-
Mahmoud Aglan authored
When the server raises your level, the table dims and the crown slams in on spinning rays with the new level in a gold star, confetti, the level-up jingle and a heavy haptic. shoot.mjs --eval runs a snippet before the shot. Co-Authored-By:Claude Opus 5.5 <noreply@anthropic.com>
-
Mahmoud Aglan authored
Nothing listened for bus.emit('toast'), so sync errors, login-required notes and group messages silently vanished. engine.js now shows them with a new ui.toast (parchment note with a sticker and a sound). Friend rows: Challenge is a green game button, chat a sticker button. shoot.mjs --with-friend makes a befriended second throwaway account. Co-Authored-By:Claude Opus 5.5 <noreply@anthropic.com>
-
Mahmoud Aglan authored
- The splash is a game loading screen: the crown dealt onto spinning light rays, the wordmark, a chunky gold loading bar and a rotating tip. - index.php shows the crown and a loading bar before any script runs, so a slow phone never sees a blank page. - Tiebreak names (Buchholz, Sonneborn-Berger, ...) were raw keys on the tournament page. Co-Authored-By:Claude Opus 5.5 <noreply@anthropic.com>
-
Mahmoud Aglan authored
Found by the gameplay pass, verified against production with throwaway accounts (31/31 checks): - ludo: the queue filtered and ordered on created_at, a column ludo_queue does not have, so no two players were ever matched; five stale rows from July to September were also deleted. - domino and ludo status/get decoded jsonb columns that already arrive as arrays: a PHP 8 TypeError, so the waiting player never learned of the match and every live poll failed. - all three: the zero-UUID claim placeholder was reported as a found match and its row deleted; failed inserts were reported as success; domino hands were renumbered by array_merge; complete passed p_winners as a string (the RPC refused it) and could pay twice. Complete now pays once, to seated players. - engine.js loads each game module independently, so one module failing to load can no longer stop the whole app from booting. Co-Authored-By:Claude Opus 5.5 <noreply@anthropic.com>
-
Mahmoud Aglan authored
- Daily reward is a moment: a treasure chest on spinning light rays that wiggles when ready, a gold "open" button, the chest bursts with the reward jingle and coins fly to the purse; the week is a grid of reward cards with the chest on day seven. - Shop: loot cards framed and ribboned by rarity (grey, green, blue, purple, gold), drawn previews per item type (mini boards in their colours, piece sets, framed avatars, effects), coin and gem price tags; Arabic item names (api/shop.php now sends name_ar). - Achievements as sticker badges, grey with a lock until earned, gold when done; chunky gold progress bars. Leaderboard podium medals with ribbons. Settings: sticker tiles, a green game switch, the delete button is red, and a credits sheet for the CC BY artists. Profile: framed avatar, crown level, sticker stats and game marks; the Challenge button pushed a scene that does not exist and did nothing. - Signed out there is no HUD or tab bar; login sits in a framed tray with a big gold "play as guest" button. - HUD/tab crown uses the optimised crown.webp; wood and crown are preloaded. - Server: no game had paid coins or XP since 2026-06-18 because award_coins wrote a transaction type the table's check constraint rejects; fixed in the database (migrations/2026-10-19-award-coins-type.sql). Online backgammon had no tables at all; created (migrations/2026-10-19-backgammon-online.sql), and the backgammon friend invite now writes the same shape as matchmaking. Co-Authored-By:Claude Opus 5.5 <noreply@anthropic.com>
-
Mahmoud Aglan authored
The owner's direction: El3ab is a game, not an app; the indigo pass read as a "big blue blob". The world moves onto a warm walnut cafe table under a lamp. - New game UI kit (tokens.css, core.css): leather trays for lists, ivory card stock in gold frames, chunky glossy 3D buttons (green play, blue, gold, red, purple, wood) that push down, ribbon screen titles, round back buttons, gold tab chips. Lalezar for display type, Baloo Bhaijaan 2 for reading. - Game HUD: avatar frame with level badge and XP bar (500 XP a level, as the server computes it), coin and gem purses with green plus buttons (reward, shop), wooden bell. Tab bar: sticker icons that lift when active, a big round play button with the crown standing out of the bar. - 60 glossy sticker icons baked from game-icons.net (CC BY 3.0, credited in public/assets/game/CREDITS.md) by tools/build-gicons.mjs; gicon()/crown() in icons.js; legacy emoji keys and icon slots now render stickers. - Illustrated SVG scene per game (core/game-art.js), used on the home hand and each game room. - Home is a game lobby: side buttons (reward chest that wiggles when ready, achievements, leaderboard, challenge) around the crown, the hand of game cards, a huge green Play online button, a live-tournament banner. - Rooms use shared game-mode buttons and a framed art hero (ui.js). - Sound: recorded card-table sounds (Kenney, CC0) through Web Audio with variants and pitch drift; synth fallback. Global press feel (feel.js), card-move scene transitions, counting purse, ringing bell. - Fix: the shop module was never loaded, so the shop could not open. - Restyled screens from the parallel pass (play flow, chess and puzzles, rank and tournaments, social, profile, shop, rewards, auth) and ~200 recovered or missing Arabic/English strings; t() now warns once per missing key. Ludo, backgammon and domino game screens and the shared in-game panels ship separately, after their gameplay pass. Co-Authored-By:Claude Opus 5.5 <noreply@anthropic.com>
-
Mahmoud Aglan authored
- Chess bots are court cards: the bot's initial printed large in red or black ink by strength, name plate, a one-line personality, the rating band as the corner index; the strongest carries a crown. - Time controls are printed cards grouped by bullet, blitz, rapid, classical. - Matchmaking shows a face-down deck riffling while the clock counts. - Challenge-a-friend rows, a card-stock options sheet; the pre-game lobby shows you and your opponent as two player cards, the opponent face down until they accept. The lobby's clock chip now shows the real control; every classical control used to read 30 minutes. - Ludo, domino and backgammon rooms: chip groups printed on card stock when chosen, ludo seats in each player's ink, one red start button. Domino bot setup is one screen (level and target, then start) instead of two steps. - PRODUCT.md: El3ab is a game, not an app; binding for every screen. Co-Authored-By:Claude Opus 5.5 <noreply@anthropic.com>
-
Mahmoud Aglan authored
Co-Authored-By:Claude Opus 5.5 <noreply@anthropic.com>
-
Mahmoud Aglan authored
Co-Authored-By:Claude Opus 5.5 <noreply@anthropic.com>
-
Mahmoud Aglan authored
- The colour section now edits the deck tokens (card back, card stock and inks, foil and signals, text) with tokens.css values as defaults; the top and tab bar section only lists values theme.js still applies. - Retired sections (home tiles, quick actions, game menu sheet, menu buttons, chips, bot cards, game gradients, pill radii) are gone; saving them did nothing after the redesign, and their old defaults could have overridden it. - Emoji replacement slots become icon overrides (icon_<name>); slots that are now drawn icons are dropped from the asset list. Board defaults match the printed boards. - POST /api/branding.php required no login at all: anyone could rewrite the theme for every player. It now requires the branding admin session. Co-Authored-By:Claude Opus 5.5 <noreply@anthropic.com>
-
Mahmoud Aglan authored
- core/ui.js: screenHead/wireBack, emptyState, loadingRows, errorState, avatar, esc, so every redesigned screen is built from the same furniture. - i18n: 44 strings carried emoji or check-mark glyphs; they now read as words. - Chess board dark squares move to a mid-tone printed indigo so both piece colours read on them. - tools/shoot.mjs can push any scene with params. Co-Authored-By:Claude Opus 5.5 <noreply@anthropic.com>
-
- 18 Oct, 2026 1 commit
-
-
Mahmoud Aglan authored
El3ab is redesigned as the deck on a cafe table: an indigo card-back ground printed with a fine lattice, white card-stock faces in black and red ink, gold foil kept for the crown, coins and first place. One face, Alexandria, for every role, with tabular figures so clocks and ratings never jitter. - tokens.css and core.css rewritten. The old token names still resolve, into the new palette, so every screen picks up the world before its own pass. - New icon family (public/js/core/icons.js): Lucide for utility icons plus El3ab's own crown, coin, gem, suits and four game marks. emoji() and the old PNG icon slots now render these, so the 300 emoji call sites stop printing emoji. Admins can still override any icon via an "icon_<name>" asset. - theme.js applies only the new keys; the retired palette's stored values can no longer repaint the app. getColor() falls back to the matching token. - HUD: avatar, name and level, coin and gem purses, bell. Tab bar marks the active world with a red diamond pip. - Home is now a fanned hand of the four games with your rating in each card's corner; the chosen card is pulled up and its lobby sits below it. - tools/shoot.mjs screenshots scenes as a throwaway signed-in player. - docs/branding-backup-2026-10-19.json: the stored theme before this change. Co-Authored-By:Claude Opus 5.5 <noreply@anthropic.com>
-
- 01 Sep, 2026 14 commits
-
-
Mahmoud Aglan authored
When neither player opened their board, the sweep called tournamentForfeitPairing with the white player's id as the absentee. That helper awards the point to whichever seat is NOT the one named, so every game nobody turned up for was recorded as black_wins — a full point, in a rated event, for a player who also did not appear. It fired on all four boards of a round during testing. An unnamed absentee now records the pairing as 'aborted', which chessResultPoints already scores 0-0, and the standings count it as a loss for both rather than falling through to the draw branch. Co-Authored-By:Claude Opus 5 (1M context) <noreply@anthropic.com>
-
Mahmoud Aglan authored
api/cron.php now accepts the Supabase service key as an alternative credential, so the management app can run the engine without a second shared secret having to be provisioned on two CapRover apps first, and takes an optional tournament_id so an organiser pressing Start gets the answer now instead of at the next sweep. The sweep also stops skipping tournaments with no round. Flipping a tournament to in_progress without pairing round one left it registered, running and permanently unplayable; the sweep now opens the round. Co-Authored-By:Claude Opus 5 (1M context) <noreply@anthropic.com>
-
Mahmoud Aglan authored
The manager's Start button flips el3ab_tournaments.status to in_progress directly. For a tournament this engine owns, nothing else was ever going to pair round 1 — so the tournament sat registered, started, and permanently unplayable, with current_round 0 and no round row. tournamentTick now opens the round when it finds that state, which also repairs any tournament already stuck in it. Covered by e2e section 21. Co-Authored-By:Claude Opus 5 (1M context) <noreply@anthropic.com>
-
Mahmoud Aglan authored
The audit signs in as a guest per device, which creates a real account. A single long-lived browser was crashing partway through the third device, and the crash took the teardown with it — leaving throwaway accounts in production. Now: one browser per device, and teardown in a finally block so it runs whichever way the run ends. Co-Authored-By:Claude Opus 5 (1M context) <noreply@anthropic.com>
-
Mahmoud Aglan authored
ChessBoard sized itself from container.clientWidth, which INCLUDES padding, so inside a padded container the board came out exactly the padding too wide. On the puzzle screen that pushed the whole page 4px sideways and hung the board off the edge. Sizing now measures the container's content box. resize() had a second bug: it set canvas.width/height (the backing store) but never the CSS width/height, so after any resize the element laid out at its attribute width — devicePixelRatio times too large. It also now resets the transform before scaling rather than relying on the implicit reset. Both paths share one availableSize(), so they can no longer disagree. Local audit is clean across all scenes at 360/390/430px. Co-Authored-By:Claude Opus 5 (1M context) <noreply@anthropic.com>
-
Mahmoud Aglan authored
An audit of the live app at 360/390/430px found 355 measurable layout defects. tools/ui-audit.mjs drives the real app at those three widths and reports what is measurable rather than a matter of taste: horizontal overflow, tap targets below 44px, text below a legible size, and content clipped by its own container. before 355 (tiny-text 216, tap-target 133, clipped 6) after 0 The root cause of the size problems was not the stylesheet. /api/branding.php writes 181 stored values inline onto :root at runtime, overriding every token in tokens.css — including quick_btn_label_font 10 and hud_badge_font 9. Editing tokens.css alone changed nothing. The floors are now enforced in theme.js where the override happens, so no stored value (or future admin edit) can reintroduce illegible text or an unreachable control: - any *_font token is floored at 11px - tap-target tokens are floored at 44px - home_max_width, stored as 340px for a 360px phone, is treated as a floor and allowed to grow (min(100%, max(340px, 92vw))) instead of pinning every phone to the smallest one's layout Alongside that: a fluid type scale (--fs-xs … --fs-xl) and --tap-min in tokens.css, a tap-target floor on every control in core.css, and 53 sub-11px inline font sizes and 41 undersized inline button heights raised at the source. Layout fixes visible on screen: - the daily-gift button carried breathe-glow on the whole flex column, drawing a large dark rectangle across the quick-actions row. The glow belongs on the icon. - play home dumped its spare height as dead space above the tab bar; it now centres via auto margins, which does not clip when content overflows the way justify-content:center does. - the chess board left a stray "1." floating in a gap above the controls. The opening/material/move-list block is now hidden until there is something in it. Also adds tools/dev-server.php, which serves the app locally against the real Supabase so this work does not need a deploy per iteration. Multiplayer regression: tests/run.sh 5/5, and the production rehearsal passes. Co-Authored-By:Claude Opus 5 (1M context) <noreply@anthropic.com>
-
Mahmoud Aglan authored
Reactions did not sync, and when something appeared it floated in the middle of the screen with no sign of who sent it. Four games had four implementations, three broken in a different way: - chess kept ONE shared `emote` slot in the match's game_state, so two players reacting in the same second overwrote each other, and it carried no sender; - ludo posted reactions through the MOVE endpoint, which replaces game_state wholesale — sending an emote wiped turn_count and broke the sync loop. Its receive side registered with mp.onEmoteReceived, which only stores a callback that nothing ever invokes, so ludo players never saw a reaction at all; - domino had a third variant, again a single shared slot; - backgammon wrote a `last_emote` column no client ever read, so the opponent saw nothing. (That table does not exist in the database either — see below.) Reactions are chat, not game state. They now live in `chat_messages` on a `match` channel via the new api/match-chat.php, so nothing about a reaction can reach a board, a clock or a turn — a social feature must not be able to corrupt a game in progress. The multiplayer sync path is untouched. core/reactions.js is the single client implementation. Its central idea is SEATS: a game registers which element belongs to which player, and every bubble is then drawn attached to that player, carrying their name and avatar. That is the actual fix for "you can't tell who is talking". Seat resolution falls back to "the seat that isn't mine" in a two-player game, so a game can register its opponent seat before it knows the opponent's id. Only preset emotes and preset phrases are accepted, validated server-side — free text between strangers in a live match is a moderation surface nobody is staffing. The cooldown is enforced on the server, not just in the UI. Also fixed, found on the way: - ChessBoard has draw(), not render(). board.js called render() whenever a themed piece image finished loading, so it threw every time and the board never repainted with custom piece art. - chess/scenes/game.js built an inline onerror="" whose body embedded the result of emoji() — which returns <img src="..."> when a themed asset exists. Its double quotes closed the attribute, breaking the markup and throwing a SyntaxError on every bot game; the stray '"> rendered as text inside the avatar. The fallback is wired in JS now. Adds tools/ui-audit.mjs (drives the real app at three phone widths and reports measurable layout defects) and tools/test-reactions.mjs (end-to-end reaction check against a live deployment, with teardown). Noted, not fixed here: backgammon_matches and backgammon_queue do not exist in the database, so backgammon multiplayer has never worked. Out of scope for this change and untouched. Co-Authored-By:Claude Opus 5 (1M context) <noreply@anthropic.com>
-
Mahmoud Aglan authored
Deployed to el3ab-player and verified end to end against production with tests/rehearsal.mjs, which now also covers spectating and the "watch this player" lookup. Production is left byte-for-byte as it was found: row counts across matches, tournaments, profiles, rating_history and the queue are unchanged. Remaining for a human: play one game on a real device (the interface changes are the part I could not verify), and optionally set CRON_SECRET and SUPABASE_JWT_SECRET, which need CapRover app configuration the push webhook does not grant. Co-Authored-By:Claude Opus 5 (1M context) <noreply@anthropic.com>
-
Mahmoud Aglan authored
Two problems, one of them mine. Mine: locking api/game.php's `get` to the two participants broke spectating outright. Watching a live board is a normal part of a chess event — the profile "watch" button and the tournament live board both rely on it — and on championship day it is what most people will be doing. Non-participants now get a spectator projection: position, clocks, move list, players and result, but no `my_color` and none of the private game_state (draw offers, heartbeats). Pre-existing: handleFindActiveMatch ignored the player_id it was given and always looked up the *caller's* match, so clicking "watch" on someone else silently opened your own game. It now honours the requested player. The board itself still goes through `get`, which decides what a spectator may see. Both lookups are also bounded to the last 12 hours. Production carries 27 matches stuck in a non-final state since May and July; without that bound, "watch" would open a game that ended three months ago. Test-harness hardening in the same commit: pgshim silently ignored any PostgREST filter it did not implement, so a query with `gte.` matched every row and the test passed while production filtered correctly. It now implements gte/lte and throws on anything unrecognised, rather than quietly matching everything. Co-Authored-By:Claude Opus 5 (1M context) <noreply@anthropic.com>
-
Mahmoud Aglan authored
el3ab_tournaments.starts_at is NOT NULL in the live schema. tournament-admin.php allowed it to be null, so every create call that did not supply one was rejected by Postgres. The local test schema had the column nullable, so the suite passed and the defect only appeared when the rehearsal ran against production. starts_at now defaults to now() — which also gives auto_start something to act on — and tests/schema.sql marks the column NOT NULL so the next one is caught locally. Adds tests/rehearsal.mjs, the script that found it: four throwaway accounts driven through matchmaking, a 33-move game to checkmate and a full Swiss round against the live deployment, with teardown in a finally block. Run it after any deploy. Co-Authored-By:Claude Opus 5 (1M context) <noreply@anthropic.com>
-
Mahmoud Aglan authored
Co-Authored-By:Claude Opus 5 (1M context) <noreply@anthropic.com>
-
Mahmoud Aglan authored
requestBotMove retried forever on failure and did nothing at all when the engine answered without a move, so a Stockfish hiccup left the player watching the thinking indicator with no way to continue. Now: four attempts with exponential backoff, then a dialog offering to end the game. Also guards against gameState having been torn down mid-request. The engine itself is healthy — verified live: GET /api/chess/bots returns the seven bots the client expects, and POST /api/chess/move answers the start position with d2d4 at depth 3. api/bots.php's paths are correct. Arabic and English strings added for the new message. Co-Authored-By:Claude Opus 5 (1M context) <noreply@anthropic.com>
-
Mahmoud Aglan authored
No completed match in production has ever had white_rating_after or rating_change_white written, and 272 of the 290 profiles were still sitting on the default 1200. The complete_match database function does not touch the rating columns, and game.php had calculateElo() and getRatingColumn() helpers that nothing ever called. Elo is now computed in applyRatings(), called from finaliseMatch() after its conditional write has succeeded — so a game can never be rated twice. It writes both ratings before and after and the deltas onto the match, updates the correct per-time-control column on each profile along with the game/win/loss/draw and streak counters, and records a rating_history row per player. This matters beyond the leaderboard: Swiss seeding orders players by rating, so with every rating pinned at 1200 the top-half/bottom-half split in round 1 was arbitrary. Bot games and aborted games are never rated, and a match with the same player on both sides is refused outright. Covered by e2e sections 18 and 19. Co-Authored-By:Claude Opus 5 (1M context) <noreply@anthropic.com>
-
Mahmoud Aglan authored
Production carries seven matchmaking_queue rows marked `matched` from May and June, each pointing at a game that finished months ago. The previous commit started honouring `matched` rows so the hand-off survives a re-queue — which, without this, would have sent those players straight into a three-month-old finished match. liveClaimedMatch() now verifies the referenced match is real, still playable, and actually belongs to the caller before acting on it, and deletes the row on the spot when it is not. Self-healing, so the existing rows need no manual cleanup. handleStatus also deletes only the row it consumed rather than every row for that player, and returns opponent_id alongside the colour. Covered by e2e section 17: a stale claim is refused and cleared, queueing past one works normally, and a genuinely live claim is still delivered with the correct opposite colour. Co-Authored-By:Claude Opus 5 (1M context) <noreply@anthropic.com>
-