Card Battle — changelog
v1.1.5 — No-card bug fix + visual feedback
- Bug fix: both No cards disappeared when one was played. Reported by Benjamin (uplink 2026-06-27): "I had two No Cards in my hand, I used one but both of mine were gone not just one." Cause:
playNoCardcalledhand.splice(noIdx, 1), shifting all subsequent hand indexes by one. ~900 ms later,discardPlayedCardsthen ran with the now-stalestate.selected[side]index — which, depending on the position of the No card relative to the battle card, could point at the OTHER No card and remove it too. Both vanish in one play.
Fix: at battle-resolve time, snapshot the actual battle card OBJECTS by ID (state.battleCardIds = { p1, p2 }). discardPlayedCards now does a findIndex by ID instead of trusting a stale index. Stable across any mid-round hand mutation.
- New: No-card blocked animation. Reported by Benjamin via two uplinks (2026-06-27): wants a visual when either player plays a No card to stop a die roll. New brief full-screen overlay shows the ✕ glyph plus "<name> played a No card! · die roll blocked" — pop-in, hold, fade out over ~1.25 s. Pure opacity/transform animation, single composited layer, Pi-safe.
Trigger paths: (1) playNoCard fires the fx locally for vs-AI player + online host. (2) applyRemoteState on the guest detects a noPool length increase (host just recycled a No card → No was played) and fires the fx with the loser's name.
v1.1.4 — readable footer / setup links
- Same fix as MoM v1.74: in-game footer (
.status-strip .rules-link) and setup-screen version-line links now usevar(--ink)at 85% opacity by default, brightening tovar(--accent)+ underline on hover. Explicit:visitedrule prevents post-click purple. The previousvar(--muted)(#8a93a6) was hard to read on the dark panel.
v1.1.3 — bug fixes from Benjamin's smoke test
- Dice animation now plays in online mode. The v1.1 online refactor added a
runDieAnimation()function that gets fired from two places: directly via the host'sawait runDieAnimation(...)insidehostProceedToDieRoll, and indirectly viarenderOverlaysForPhase()when state.phase==='die'. Bug:hostProceedToDieRollwas callingrender()BEFORE itsawait—render()triggered the animation viarenderOverlaysForPhase, which claimed thedieAnimatingguard. The host's explicitawaitthen early-returned viaif (dieAnimating) return;, so the host raced past the animation todiscardPlayedCards()andhostNextRound(). Net effect: dice overlay flashed for one frame, no spin, immediately moved on. Fix: drop the redundantrender()fromhostProceedToDieRoll— the explicitawaitis now the only animation path. - Played No cards recycle. Previously a played No card went into
state.discard, which only reshuffles into the battle deck (where No cards are unplayable as regular cards). Net effect: the 2-card No pool drained to zero permanently after both were used, and 5/6 die rolls produced "pool empty, no card" forever after. Now: played No cards go straight back intonoPool, available for re-acquisition via future 5/6 rolls. Reported by Benjamin: "No cards get spent when you use them. They should go to a pile to be used again." - Dice numeric backup. The unicode dice glyphs (U+2680-2685) need a font that has them —
'Apple Color Emoji'on macOS,'Segoe UI Emoji'on Windows. On Chromebooks/Linux Chromium, neither is installed and the cascade throughsans-serifdoesn't always pick up a Symbols font. Belt-and-suspenders fix: render the numeric value (1-6) in monospace below the glyph, so even if the unicode glyph fails the player always sees the result. Cosmetic only; not the cause of the v1.1.2 dice bug. - Rules page updated: "Once both are spent, that's it" → "Played No cards recycle back into the pool".
v1.1.2 — quick play + more mobile fixes
- ⚡ Quick play matchmaking. New button on the Online setup tab: auto-matches you with anyone waiting in a Card Battle room, or hosts a new one if no one's waiting. Reuses MoM's existing
/api/online/quickjoinendpoint, now extended with agameTypefilter so CB players don't accidentally land in an MoM open room. Same sharedgamestable, just different rows. - Server-side merge for join + quickjoin. The shared
/api/online/{join,quickjoin}previously REPLACEDstate.players.p2with whatever the client sent. That clobbered CB's host-pre-dealt p2 hand. Now it MERGES ({ ...existingP2, ...playerObject }) so the host's deal is preserved. MoM behaviour unchanged because MoM's existing p2 is empty pre-join. - More mobile portrait fixes. The v1.1.1 fix should have worked at 430 px width but wasn't enough on Benjamin's iPhone. Belt-and-suspenders this round:
- Body uses
100dvh(dynamic viewport) with100vhfallback — handles iOS Safari URL bar. viewport-fit=cover+env(safe-area-inset-bottom/left/right)padding so the home indicator and notch don't overlap content.- Hand rows now have
overflow-x: autoas a last-resort scroll fallback if 5 cards still don't fit. - Status strip wraps to two rows on narrow viewports instead of clipping.
- New
(max-width: 380px)breakpoint for very small phones — cards drop to 52 × 72.
v1.1.1 — mobile responsive
- Phone layout fix. On iPhone Pro Max portrait, the 5×80 px hand cards + gaps overflowed the ~430 px viewport — cards got clipped on the right. In landscape, the stacked rows (header + opponent hand 110 + battle 160 + player hand 110 + status) totalled more than the ~430 px height, and
body { overflow: hidden }clipped the bottom off. Added two media-query breakpoints: (max-width: 500px)— phones in portrait. Cards shrink to 64×88, battle cards to 78×108, plus tighter spacing/padding throughout.(orientation: landscape) and (max-height: 500px)— phones in landscape. Cards shrink further to 50×70, battle cards 62×86, minimal gaps. Whole game fits in ~370 px tall.- Reported by Benjamin: portrait cuts off cards on the right; landscape cuts off the bottom.
v1.1 — online multiplayer
- "← home" link in setup-screen and in-game footer per Benjamin's uplinked feature request (2026-05-03): "there should be a button to send you back to the homepage". Sits next to the rules / changelog / backlog row.
- Online mode. Setup screen now has a vs AI / Online tab toggle. Host a private room (you get a 4-char code), share with a friend, they join with the code from another device. Reuses the existing Supabase backend that Myth-o-Magic uses (single shared
gamestable, distinguished by agameType: 'card-battle'field). - Host-orchestrated model. The host (room creator, p1) is the trusted server: they shuffle the deck, pre-deal both hands at create time, generate die rolls, and drive every phase transition. The guest (p2) only sends two kinds of input: card picks (during 'pick') and No-card decisions (during 'no-prompt' when they're the loser). Everything else flows back via state sync. This avoids races on the shared row.
- Room badge in the in-game footer shows the active room code (so you can re-share if your friend lost it).
- Forfeit on leave. Hitting "leave" mid-game in online mode pushes a
forfeitedByflag so the other side gets a definitive game-over instead of being stuck waiting forever. - Same dice animation, same No-card prompt as vs AI — just driven by remote state instead of local logic.
- Not yet: quick-play matchmaking, online scoreboard, resume-on-refresh, "play again" without re-hosting. Logged in BACKLOG.
v1.0 — first playable
- Initial scaffold from Benjamin's spec.
- Setup screen shows the per-game version ("Card Battle v1.0"), not the site build number. Setup + in-game footer have rules/changelog/backlog links.
- Changelog/backlog links resolve to locally-hosted HTML pages (the repo is private, so GitHub-rendered markdown isn't visible to visitors).
- 47-card deck: 15 Red, 15 Black, 15 Royal in a rock-paper-scissors beats cycle, plus 2 "No" cards in a separate pool.
- Single-player vs heuristic AI: first to 7 points wins.
- Round flow: both pick a card → reveal → winner rolls a 6-sided die → 1-4 = +1 point, 5-6 = pick up a No card (if any left in pool).
- Loser of a battle is prompted to play a No card to block the winner's roll. AI plays a No card at 70% probability when it has one and just lost.
- Same-suit ties: no roll, both cards discarded, next round.
- Auto-reshuffle of discard back into deck when deck is empty.
- Stalemate handling: if either side can't draw any battle cards, game ends; whoever has more points wins.
- Setup screen with player name + AI name (persisted in localStorage).