Skip to main content

Multiplayer System

Multiplayer System

Status: PARTIAL — Duel is PLAYABLE as of 2026-07-07: routes a duelist's flips through the turn-validated flipCardDuel path (checked BEFORE the solo session, since only one player's account owns the s

Last updated: July 16, 2026

Flip Match -- Multiplayer System

Status: PARTIAL — Duel is PLAYABLE as of 2026-07-07: routes a duelist's flips through the turn-validated flipCardDuel path (checked BEFORE the solo session, since only one player's account owns the shared board), start duel resumes an active duel, the opponent gets a pending chat notification on every turn pass and at completion, and a completed duel closes the shared session WITHOUT crediting solo stats/leaderboard/achievements to the session-owning account. Guarded by (JUnit). NOT yet built from the Duel design: the 5-minute per-turn timeout + 3-timeout forfeit, the quit-forfeits command, the full duel scoring formula (base 1000, victory +500, margin bonus — v1 decides winner by pair count via determineWinner), tie-breakers, and per-player win/loss stats (recordDuelResult is still a logging stub). Team Snap remains gated off in the voice flow with no play handler.

Doc coverage:: PARTIAL — Backend support for Speed Race and Duel is implemented in (startSeededGame, getOpponentRaceSession, recordSpeedRaceResult, createDuelGame, flipCardDuel, recordDuelResult), MemorySnapMultiplayerGame model, , and session fields (gameMode, boardSeed, challengeId, partyRef). ships the Speed Race status surface. Player-facing handlers for the rest of the flow (, , ) and Team Snap end-to-end (party scaling, turn rotation, pass command, team scoring) are still TODO.

Prerequisites: Read index.md for core game mechanics, board.md for board/card system, scoring.md for scoring formulas.

Infrastructure: Builds on the existing party system (chat.chat_parties, PartyGameDetails) and mini-game challenge system (mini_games.mini_game_challenges, ). See engine/minigames.md for challenge architecture.


Design Philosophy

Flip Match multiplayer extends the core single-player experience with three distinct competitive and cooperative modes. Each mode serves a different player motivation:

Mode Motivation Social Dynamic Complexity
Duel Direct competition Head-to-head rivalry Medium
Speed Race Personal mastery Async comparison Low
Team Snap Cooperation Shared achievement Medium

Key Principles:

  1. Leverage existing infrastructure -- Party system for groups, challenge system for 1v1, chatRef for player identity. No new networking or real-time sync layers.
  2. Single-player remains first-class -- Multiplayer is additive, never gates single-player content. All achievements, leaderboards, and scoring work identically in solo play.
  3. Async-friendly -- All modes work in the text-based turn-by-turn engine. No real-time clock synchronization required. Players take turns via the same input system used for chess and warship.
  4. Fair competition -- Seed-based board generation ensures identical card layouts when fairness matters (Speed Race). Duel uses a shared board where both players see the same state.

Mode 1: Duel (Head-to-Head)

Overview

Two players share one board. They alternate turns flipping cards. Each matched pair scores for the player who found it. The player with more pairs at the end wins.

Why This Mode Exists

Duel is the most natural multiplayer translation of memory games. It transforms memorization from a solo optimization problem into a strategic one -- you must remember not just where cards are, but also what your opponent has seen. Watching your opponent flip cards gives you free information, creating a tension between playing safe (flipping new cards) and exploiting what you've learned from their turns.

Game Flow

Challenge Phase:
  Player A challenges Player B to Flip Match Duel
    → Challenge created (PENDING, 24-hour expiry)
    → Player B sees pending challenge via accept/decline system
    → Player B accepts → game created

Setup Phase:
  → Difficulty and theme determined:
    - Challenger's preferred difficulty (or specified in challenge)
    - Random theme (or challenger's preferred theme)
  → Board generated with standard algorithm (no seed needed -- shared board)
  → Challenger goes first (like white in chess)

Play Phase (strictly alternating turns):
  Current player's turn:
    → Player flips first card (coordinate input, e.g., "A1")
      → Card revealed to BOTH players
    → Player flips second card
      → MATCH: Pair scored for current player. Turn passes to opponent.
      → NO MATCH: Cards flip back. Turn passes to opponent.
    → Turn ALWAYS passes regardless of match/mismatch (strictly alternating)
    → Optional: "status" shows board + both players' pair counts
    → Optional: "quit" forfeits (opponent wins)

  Not-your-turn:
    → Any flip attempt shows "Waiting for {opponent} to play."
    → "status" still works (shows current board state)

End Phase:
  → All pairs found
  → Player with more pairs wins
  → Tie: player who took fewer total turns wins (tiebreaker)
  → Both players get score calculated from their individual performance
  → Winner gets +500 victory bonus + margin bonus applied to their score
  → Leaderboard entries created for both players (tagged as DUEL)

Turn Rules

  • Strictly alternating turns: Unlike physical memory games, Duel uses strictly alternating turns regardless of match/mismatch. This prevents snowball effects where a lucky early streak gives one player overwhelming information and turn advantage. Both players always get equal turns.
  • Per-turn timeout: 5 minutes. If the current player doesn't flip within 5 minutes, their turn is auto-passed. 3 consecutive timeouts = forfeit.
  • Overall time limit: The difficulty time limit still applies (MEDIUM: 4 min, HARD: 5 min, EASY: no limit). If time expires, the player with more pairs wins.
  • Forfeit: Either player can quit. The remaining player wins with all unclaimed pairs awarded to them. Forfeit counts as a loss for the quitting player's stats.

Information Visibility

Both players see the full board state at all times:

  • Matched pairs are visible (with which player claimed them, color-coded)
  • Hidden cards show the standard card-back
  • When opponent flips a card, you see what it is (this is the strategic core -- free information)
  • First flip of a turn is visible to both players before second flip

This mirrors physical memory card games where both players sit at the same table and see all flips.

Scoring (Duel)

Each player gets an individual score using a modified version of the solo formula:

Duel Score Calculation (per player):

Base Score = 1000

Deductions:
  Per turn where YOU didn't find a match: -10 points
  Per second over par time (shared clock): -1 point
  (No hint deduction -- hints are disabled in Duel)

Bonuses:
  Victory bonus (winner only): +500 points
  Pairs found bonus: +25 per pair found
  Streak bonus (YOUR longest streak): +10 per consecutive match
  Domination bonus (found 75%+ of pairs): +300 points
  Margin bonus (winner): +(pairsFound - opponentPairs) × 50

Final Score = max(0, (int)(rawScore * difficultyMultiplier))

Why modify the solo formula? In Duel, the total turns depend on both players. A player shouldn't be heavily penalized for turns their opponent took. The per-pair bonus (+25) replaces the solo perfect game bonus (+500) since perfection isn't achievable against an opponent. The victory bonus (+500) and margin bonus make winning meaningfully more rewarding than losing -- a close 5-3 win earns +600 (500 + 2×50), while a dominant 7-1 win earns +800 (500 + 6×50) plus the domination bonus (+300).

Hints

Hints are disabled in Duel mode. Using hints against a human opponent would be unfair and undermine the core skill of memorization. The hint button is hidden/disabled in the UI.

Duel Leaderboard

Duel games create leaderboard entries tagged with gameMode = "DUEL". The standard leaderboard shows solo games by default. A separate "Duel Rankings" view filters by mode, showing:

  • Win/loss record
  • Average pairs per game
  • Best duel score
  • Longest duel win streak

Mode 2: Speed Race (Async Challenge)

Overview

Two players each play their own copy of an identical board (same card positions). They play independently and their scores are compared afterward. The player with the higher score wins.

Why This Mode Exists

Speed Race is the simplest multiplayer mode and the most async-friendly. It works like a golf scramble -- both players face the same course, play at their own pace, and compare scores. This mode:

  • Requires zero turn synchronization (both play independently)
  • Works across time zones (Player A can finish Monday, Player B on Tuesday)
  • Tests pure skill since both boards are identical
  • Creates natural "can you beat my score?" social dynamics

Game Flow

Challenge Phase:
  Player A completes a solo game (or starts a Speed Race from menu)
    → Option appears: "Challenge a friend to beat your score?"
    → Player A selects opponent
    → Challenge created with board seed + Player A's score as benchmark

  OR:
  Player A sends a Speed Race challenge directly
    → Difficulty and theme selected
    → Board seed generated (but Player A hasn't played yet)
    → Both players play when ready

Play Phase:
  Each player plays their board independently:
    → Board generated from seed (identical layout)
    → Standard solo gameplay (flip, hint, status, quit)
    → Standard solo scoring (no modifications from single-player formula)
    → Game saved normally with session tagged as SPEED_RACE

Comparison Phase (after both complete):
  → Scores compared
  → Higher score wins
  → If scores tied: faster time wins
  → If time also tied: fewer turns wins
  → Results shown to both players via pending notification

Seed-Based Board Generation

Critical for fairness: Both players must get the exact same card layout.

Board Generation with Seed:

1. Create a java.util.Random with the seed value
2. Use seeded Random for item selection:
   - Query all items for theme (deterministic -- same items every time)
   - Shuffle items using seeded Random (same order every time)
   - Take first N items for required pairs
3. Create card pairs (deterministic from items)
4. Shuffle card list using seeded Random (same positions every time)
5. Place on grid (row-major, deterministic)

Result: Given the same seed + theme + difficulty, the board is identical.

Seed lifecycle:

  1. Generated once at challenge creation time: System.nanoTime() ^ challengeId.hashCode()
  2. Stored on the challenge record (MiniGameChallenge.metadata JSON or new column) -- this is the source of truth
  3. Copied to each player's MemorySnapGameSession.boardSeed when they start their game
  4. Copied to MemorySnapPartyDetails.boardSeed if the race uses the party system
  5. Never regenerated -- both players MUST use the seed from the challenge record to ensure identical boards

Seed storage summary:

Location When Written Purpose
MiniGameChallenge record Challenge creation Source of truth
MemorySnapGameSession.boardSeed Player starts their game Per-session record for verification/replay
MemorySnapPartyDetails.boardSeed Party race starts Party-level tracking (optional, for party races)

Theme and Difficulty

  • Difficulty: Set by challenger. Both players play the same difficulty.
  • Theme: Set by challenger. Both players play the same theme.
  • These are stored on the challenge record and enforced when the challengee starts their game.

Time Limit

The standard difficulty time limit applies independently to each player:

  • EASY: No limit (both play at leisure)
  • MEDIUM: 4 minutes each
  • HARD: 5 minutes each

If one player times out (abandons), the other player wins by default when they complete their game.

Scoring (Speed Race)

Standard solo scoring formula -- no modifications. Both players are scored identically using the same formula from scoring.md. The higher score wins.

This is intentional: Speed Race is a direct skill comparison. Using the same scoring formula as solo play means players can compare Speed Race scores against their personal bests and the global leaderboard.

Speed Race Leaderboard

Speed Race completions create standard leaderboard entries (same as solo). Additionally, a "Race Record" is tracked:

  • Win/loss record in Speed Races
  • Average score differential (how much you beat opponents by)

Mode 3: Team Snap (Party Co-op)

Overview

2-4 players in a party work together to complete a large board. They take rotational turns. The team shares a score and tries to beat par together.

Why This Mode Exists

Team Snap provides the cooperative social experience that Duel and Speed Race don't. It transforms Flip Match from an individual pursuit into a team effort where communication and shared memory matter. The party system already supports 2-4 players, and this mode gives parties a casual game to play between more serious content.

Game Flow

Setup Phase:
  Party leader starts Team Snap:
    → "start team snap" / "start coop" / "team game"
    → All party members must be present (online and in party)
    → Board size scales with party size:
      - 2 players: 6x6 (18 pairs, 9 per player average)
      - 3 players: 8x8 (32 pairs, ~11 per player average)
      - 4 players: 8x8 (32 pairs, 8 per player average)
    → Difficulty set by party leader's preference
    → Theme set by party leader's preference
    → Turn order determined by party join order

Play Phase (rotational turns):
  Current player's turn:
    → Player flips first card → revealed to all party members
    → Player flips second card
      → MATCH: Pair counted for team. Player gets another turn.
      → NO MATCH: Cards flip back. Turn passes to next player in rotation.
    → "status" shows board + team progress + whose turn it is
    → "pass" skips your turn (strategic -- if you can't remember any matches, let a teammate try)

  Not-your-turn:
    → Flip attempts show "It's {playerName}'s turn."
    → "status" still works
    → Cannot use hints on other players' turns

  Communication:
    → Players can use chat ("say ...") to discuss what they've seen
    → This is the cooperative core -- sharing information via chat

End Phase:
  → All pairs found (team success) or time expires (team failure)
  → Team score calculated using modified formula
  → All players receive the same team score
  → Individual contribution tracked (pairs found per player)
  → Leaderboard entry created tagged as TEAM_SNAP with party size

Turn Order

Turn rotation: Player 1 → Player 2 → Player 3 → Player 4 → Player 1 → ...

Exception: On match, current player goes again (bonus turn).

If a player disconnects (no input for 2 minutes):
  → Their turn is auto-passed
  → They remain in the game but skip turns until they return
  → Team can still complete the game without them

Turn order is stored in MemorySnapPartyDetails.turnOrder (list of chatRefs) and currentTurnChatRef tracks whose turn it is. advanceTurn() cycles to the next player.

"Pass" mechanic: In Team Snap (unlike Duel), players have the option to pass their turn. This is useful when you genuinely don't remember any matching pairs -- rather than wasting a turn on a random guess that reveals information to no one, you let a teammate who might remember try. Passing does NOT count as a mismatch turn for scoring purposes, but it does consume clock time. The pass action is only available during your turn and only in Team Snap mode.

Hints in Team Snap

  • Shared hint pool: Team gets hints based on difficulty (EASY: 3, MEDIUM: 2, HARD: 1)
  • Any player can use a hint on their turn
  • Hint reveals to all players: The hint result (card location + identity) is shown to everyone
  • Score penalty applies to team score: -50 per hint used

Scoring (Team Snap)

Team Snap Score Calculation:

Base Score = 1000

Par Time Adjustment:
  Team par time = solo par time × 1.5 (teams get 50% more time before penalty)
  EASY:   90s par (no time limit)
  MEDIUM: 180s par (240s limit)
  HARD:   225s par (300s limit)

Deductions:
  Per extra turn beyond minimum: -10 points (same as solo)
  Per second over team par time: -1 point
  Per hint used: -50 points

Bonuses:
  Perfect game (minimum turns): +500 points (very hard with multiple players)
  Under par time: +100 points
  No hints used: +100 points
  Team streak bonus: +10 per consecutive team match
  Full participation bonus: +100 if every player found at least 1 pair

Final Score = max(0, (int)(rawScore * difficultyMultiplier))

Why adjust par time? With multiple players taking turns, there's communication overhead and turn-passing delay. The 1.5x multiplier keeps par achievable without making it trivial.

Full participation bonus encourages teams where everyone contributes rather than one expert doing everything while others watch.

Team Snap Leaderboard

Team Snap creates leaderboard entries tagged with gameMode = "TEAM_SNAP" and partySize = N. Leaderboards are filtered by party size so 2-player teams don't compete against 4-player teams.


Challenge System Integration

MiniGameType Extension

Add MEMORY_SNAP to the MiniGameType enum:

public enum MiniGameType {
   CHESS("Chess"),
   WARSHIP("Warship"),
   BLACKJACK("Blackjack"),
   ROCK_PAPER_SCISSORS("Rock Paper Scissors"),
   MEMORY_SNAP("Flip Match");  // Published as Flip Match
}

Challenge Creation (Duel & Speed Race)

Both Duel and Speed Race use the existing mini-game challenge menu flow (same as chess/warship):

Menu Flow (PRIORITY_COMMANDS tier):
  Player navigates: Mini-Games → Challenge a Friend
  → Game type selection shows: Chess, Warship, Blackjack, RPS, Flip Match
  → Player selects "Flip Match"
  → Sub-menu: "Duel" or "Speed Race"
  → Player selects opponent from friends list
  → .createChallenge() with gameType = MEMORY_SNAP
  → Challenge stored with PENDING status + metadata (mode: DUEL or SPEED_RACE)

For Speed Race:
  → Board seed generated at challenge creation time
  → Seed stored on challenge record metadata
  → Difficulty + theme stored on challenge record

Why menu-based, not one-shot? Chess and warship challenges use PRIORITY_COMMANDS → menu navigation. Flip Match follows the same pattern for consistency. One-shot entries in CHAT_WARBAND_GUILD_FRIENDS context would require users to be in chat, which is unnecessary for starting a game challenge.

Challenge Acceptance

// In .acceptChallenge():
case MEMORY_SNAP -> {
   if (isSpeedRace(challenge)) {
      // Speed Race: create independent game with same seed
      final long seed = generateSeed(challenge);
      final MemorySnapGameSession session = memorySnapService.startSeededGame(
         challengeeChatRef, difficulty, theme, seed);
      yield session.getSessionId().toString();
   } else {
      // Duel: create shared game
      final MemorySnapMultiplayerGame game = memorySnapService.createDuelGame(
         challenge.getChallengerChatRef(),
         challenge.getChallengeeChatRef(),
         difficulty, theme);
      yield game.getGameId();
   }
}

Team Snap (Party-Based, No Challenge)

Team Snap doesn't use the challenge system. It's initiated by the party leader through the existing MEMORY_SNAP_START_GAME handler:

Party leader says: "start team snap" / "start coop" / "team game"
  → Routed to MEMORY_SNAP_START_GAME handler (existing one-shot: "start")
  → Handler detects "team snap", "coop", or "team game" in free-form value
  → Handler checks party exists and has 2-4 members
  → Creates team game with party member turn order
  → All party members notified via pending system
  → Game begins immediately

If player says "start team snap" but is NOT in a party:
  → Handler responds: "You need to be in a party with 2-4 players to start Team Snap."

Why not a separate action? The existing start one-shot in MEMORY_SNAP context routes to MEMORY_SNAP_START_GAME. Adding start team snap as a separate one-shot would conflict with the start prefix. Instead, the START_GAME handler checks the free-form value to determine solo vs team mode.


Party System Integration

MemorySnapPartyDetails

Already added to :

public static class MemorySnapPartyDetails implements Serializable {
   String activeGameId;        // Active multiplayer game ID
   Long boardSeed;             // Seed for deterministic board generation
   String gameMode;            // DUEL, SPEED_RACE, TEAM_SNAP
   Integer boardSize;          // 4, 6, or 8
   List<String> turnOrder;     // chatRefs in play order
   String currentTurnChatRef;  // Whose turn it is
   Map<String, Integer> playerScores;  // Per-player pair counts
   Map<String, Long> playerTimes;      // Per-player completion times
   Long startTime;             // Game start timestamp
}

Party Details Usage by Mode

Field Duel Speed Race Team Snap
activeGameId Shared game ID Challenge ID Team game ID
boardSeed Not used (shared board) Seed for identical boards Not used
gameMode "DUEL" "SPEED_RACE" "TEAM_SNAP"
boardSize From difficulty From difficulty Scaled by party size
turnOrder [player1, player2] Not used (async) [p1, p2, p3, p4]
currentTurnChatRef Active player Not used Active player
playerScores Pairs per player Final scores Pairs per player
playerTimes Not used Completion times Not used
startTime Game start Challenge start Game start

Data Model Extensions

MemorySnapGameSession (Extended)

Add multiplayer fields to the existing session model:

New Field Type Purpose
gameMode String "SOLO", "DUEL", "SPEED_RACE", "TEAM_SNAP"
boardSeed Long Seed used for board generation (null for non-seeded)
challengeId String Link to MiniGameChallenge (Duel/Speed Race)
partyRef String Link to party (Team Snap)

Default gameMode to "SOLO" for all existing sessions (backward compatible).

MemorySnapMultiplayerGame (New Model)

For Duel and Team Snap modes where players share a board:

Field Type Purpose
gameId String Unique game identifier
gameMode String "DUEL" or "TEAM_SNAP"
sessionId Long Link to MemorySnapGameSession (shared board)
players List chatRefs of all players
turnOrder List chatRefs in turn order
currentTurnChatRef String Whose turn it is
playerPairs Map<String, Integer> Pairs found per player
playerStreaks Map<String, Integer> Current streak per player
gameStatus String ACTIVE, COMPLETED, ABANDONED
winnerChatRef String Winner's chatRef (null until complete)
createdAt LocalDateTime Creation timestamp

Session Column Additions

ALTER TABLE memory_snap.game_session
  ADD COLUMN game_mode VARCHAR(20) DEFAULT 'SOLO' AFTER game_state,
  ADD COLUMN board_seed BIGINT NULL AFTER game_mode,
  ADD COLUMN challenge_id VARCHAR(50) NULL AFTER board_seed,
  ADD COLUMN party_ref VARCHAR(50) NULL AFTER challenge_id;

Commands

New Multiplayer Commands

Command Action Context Routing
challenge {name} to memory snap Create Duel challenge Mini-games menu → "Challenge a Friend" PRIORITY_COMMANDS menu flow
challenge {name} to memory snap race Create Speed Race challenge Mini-games menu → "Challenge a Friend" PRIORITY_COMMANDS menu flow
start team snap / start coop Start Team Snap with party Flip Match context, party exists Handled inside MEMORY_SNAP_START_GAME via free-form value
pass Skip your turn (Team Snap only) During Team Snap, your turn MEMORY_SNAP context one-shot
race status Check Speed Race progress During active Speed Race MEMORY_SNAP context one-shot

NLU Registration

Challenge commands use the existing mini-game challenge menu flow (PRIORITY_COMMANDS tier, like chess/warship). Players navigate: Mini-Games menu → "Challenge a Friend" → select game type → select opponent. The routes based on MiniGameType.MEMORY_SNAP.

Team Snap "start" commands are handled inside the existing MEMORY_SNAP_START_GAME handler. When the handler detects start team snap, start coop, or team game as the free-form input value, it checks for party membership and starts Team Snap instead of solo. This avoids a start prefix collision with the existing start one-shot entry in the MEMORY_SNAP context.

New one-shot keys in process_one_shot_values (MEMORY_SNAP context only):

Key Context Action
pass MEMORY_SNAP MEMORY_SNAP_PASS_TURN
race status MEMORY_SNAP MEMORY_SNAP_RACE_STATUS

FreeTextDeclarationAction Extensions

// New actions for Flip Match multiplayer (reduced from original 5 -- challenge routing uses existing mini-game flow)
MEMORY_SNAP_PASS_TURN,       // Pass turn in Team Snap
MEMORY_SNAP_RACE_STATUS,     // Check Speed Race progress

Note: Challenge creation (Duel/Speed Race) uses the existing MINI_GAME_CHALLENGE_FRIEND action flow routed through with gameType = MEMORY_SNAP. Team Snap start is handled inside the existing MEMORY_SNAP_START_GAME handler. Only 2 new actions are needed, not 5.


Statistics & Achievements

Multiplayer Stats

Track separately from solo stats on MemorySnapAccount:

New Field Type Purpose
duelWins int Duel victories
duelLosses int Duel defeats
duelBestScore Integer Highest Duel score
raceWins int Speed Race victories
raceLosses int Speed Race defeats
teamGamesPlayed int Team Snap games completed
teamBestScore Integer Highest Team Snap score

Multiplayer Achievements

Achievement Requirement Points Emoji
"First Duel" Win 1 Duel 50 :crossed_swords:
"Duel Master" Win 10 Duels 200 :trophy:
"Unbeatable" Win 5 Duels in a row 300 :fire:
"Speed Demon" Win a Speed Race with score > 1500 150 :dash:
"Photo Finish" Win a Speed Race by < 50 points 100 :camera_flash:
"Team Player" Complete 5 Team Snap games 100 :handshake:
"Dominator" Find 75%+ of pairs in a Duel 200 :muscle:
"Full Participation" Team Snap where everyone found 2+ pairs 100 :star2:

New Requirement Types

Add to MemorySnapAchievement.RequirementType:

DUEL_WINS,      // Win N duel games
RACE_WINS,      // Win N speed races
TEAM_GAMES,     // Complete N team snap games
DUEL_STREAK,    // Win N duels in a row

UI Considerations

Multiplayer Menu

From the Flip Match main menu (no active game):

Flip Match - Multiplayer

1. Duel a Friend - Challenge someone to a head-to-head match
2. Speed Race - Race to beat a friend's score on the same board
3. Team Snap - Play cooperatively with your party
4. Back to Menu

Say "challenge {name} to memory snap" to start a duel.

Duel Display

When it's your turn in a Duel:

Flip Match Duel vs {OpponentName}
Board: 4x4 EASY (Animals)

    A    B    C    D
  +----+----+----+----+
1 | :game_die: | :lion: | :game_die: | :game_die: |
  +----+----+----+----+
2 | :game_die: | :game_die: | :dog: | :game_die: |
  +----+----+----+----+
3 | :lion: | :game_die: | :game_die: | :game_die: |
  +----+----+----+----+
4 | :game_die: | :game_die: | :game_die: | :dog: |
  +----+----+----+----+

You: 3 pairs  |  {Opponent}: 2 pairs  |  Remaining: 3 pairs
YOUR TURN - Flip a card!

When it's opponent's turn:

Waiting for {OpponentName} to play...
You: 3 pairs  |  {Opponent}: 2 pairs  |  Remaining: 3 pairs

Speed Race Status

Speed Race vs {OpponentName}
Board: 6x6 MEDIUM (Flowers)

Your Score: 1,455 (Completed in 1:52)
{Opponent}: Still playing...

Check back later or play another game while you wait!

Team Snap Display

Team Snap (3 players)
Board: 8x8 HARD (Transport)

    A    B    C    D    E    F    G    H
  +----+----+----+----+----+----+----+----+
  ... (board) ...

Team Progress: 12/32 pairs found
  Ziggy: 5 pairs  |  Archie: 4 pairs  |  Nova: 3 pairs
{Archie}'s turn  |  Time: 2:15 / 5:00

Say a coordinate to flip, or "pass" to skip.

Edge Cases

Duel Edge Cases

Scenario Behavior
Player disconnects mid-duel Game remains ACTIVE. Opponent can keep playing on their turns. If disconnected player doesn't return within 24 hours, opponent wins by forfeit.
Both players quit Game marked ABANDONED. No winner.
Tie in pairs Tiebreaker: fewer total turns wins. If still tied: fastest total time. If still tied: true draw (both get draw bonus +100).
Time expires (MEDIUM/HARD) Game ends immediately. Player with more pairs wins. Counts as time-out loss for the player with fewer pairs.
Player challenges themselves Blocked by existing challenge validation.
Challenge while in active game New game starts, previous active session auto-abandoned (consistent with solo behavior).

Speed Race Edge Cases

Scenario Behavior
One player times out, other completes Completing player wins automatically.
Both players time out Both get ABANDONED. No winner. Challenge marked EXPIRED.
Player starts race but never finishes Challenge expires after 24 hours. Other player's completed game still counts as a solo entry for leaderboard.
Theme doesn't have enough items for difficulty Same fallback as solo: duplicate items. Both players get the same duplicates (seeded).
Seed collision (two challenges same seed) Acceptable -- seeds are per-challenge, not globally unique. Same board layout is fine between different challenges.

Team Snap Edge Cases

Scenario Behavior
Player leaves party mid-game Their turns are auto-skipped. Team can still finish. Their pairs count toward team score.
Party leader quits Game continues with remaining players. Leadership transfers per party rules.
All players leave Game ABANDONED.
Party has 1 player Cannot start Team Snap (minimum 2 required).
Party has 5+ players Cannot start Team Snap (maximum 4). Use a smaller party.
Player joins party mid-game Cannot join active Team Snap game. They wait for the next game.

Leaderboard Filtering Rules

Solo and multiplayer leaderboard entries do NOT mix. Each mode has its own leaderboard view:

Leaderboard View Filters Entries
Standard (default) gameMode = 'SOLO' Solo games only (all existing entries)
Duel Rankings gameMode = 'DUEL' Duel games only, shows win/loss + best score
Speed Race gameMode = 'SPEED_RACE' Speed Race completions, shows win/loss + avg differential
Team Snap gameMode = 'TEAM_SNAP', grouped by partySize Team games, separated by 2/3/4-player

Why separate? Solo scoring, Duel scoring, and Team Snap scoring use different formulas. Comparing a solo score of 1500 against a Duel score of 1500 is meaningless -- they measure different things. Speed Race uses the same solo formula but still tracks separately because the competitive context (beating a specific opponent) is distinct from optimizing a solo run.

Implementation: The existing adds a gameMode filter parameter. Default is "SOLO" for backward compatibility. The leaderboard query adds WHERE game_mode = ? to the existing query.


Migration & Backward Compatibility

  • All existing sessions remain gameMode = "SOLO" (default value)
  • All existing leaderboard entries are implicitly SOLO mode
  • No changes to existing solo gameplay, scoring, or achievements
  • boardSeed is NULL for all non-seeded games (solo, duel)
  • MiniGameType.MEMORY_SNAP addition is backward compatible (enum extension)
  • MemorySnapPartyDetails in PartyGameDetails is null when unused (JSON NON_NULL)