Files
scopone/docs/FINDINGS.md
2026-03-31 21:07:28 +02:00

7.5 KiB
Raw Blame History

Findings

Last Updated: 2026-03-31T00:00:00.000Z

Summary

Initial analysis of the Scopone Scientifico Phaser 3 codebase. This document is populated by the Planner agent as research is performed.

Codebase Observations

  • Total source files: 9 TypeScript (6 in src/), 3 Java (Capacitor boilerplate)
  • Largest file: GameScene.ts (~1340 lines) — rendering, input, effects, audio, UI
  • Game logic is framework-independent: game/ modules have zero Phaser imports
  • No test framework: only tsc --noEmit for type-checking
  • No linter/formatter: code style enforced manually
  • AI plays all 3 non-human seats using the same heuristic
  • Procedural audio: all sound is Web Audio oscillators — no audio asset files

Potential Improvement Areas

  • AI cheats with perfect information: scoreDump() and opponentThreatScore() in ai.ts iterate opp.hand directly — bots can see all opponent cards. Must be replaced with imperfect-information card tracking.
  • No mastery/difficulty levels: All 3 AI seats use the same heuristic at the same strength.
  • No card tracking: No module tracks which cards have been played or remains in the deck.
  • No minimax: Pure heuristic scoring, no look-ahead or game tree search.
  • Allied bot is selfish: Compagno (player 2) plays identically to opponents — no cooperative strategy.

Research Performed

Web Research: Scopone Scientifico Rules (2026-03-31)

Sources: Wikipedia (Scopa article, Scopone section), Pagat.com (Scopone page by John McLeod)

Core Rules (Scopone Scientifico variant)

  • 4 players, 2 fixed teams of 2 (sit opposite): Team A = players 0+2, Team B = players 1+3
  • 40-card Napoletane deck: 4 suits (bastoni, coppe, denara, spade), values 110
  • All 40 cards dealt (10 each), no initial table cards — the "scientifico" variant
  • Play passes around the table (counter-clockwise in Italian tradition; this game uses 0→1→2→3)
  • Each turn: play one card face-up to the table

Capture Rules

  1. If the played card's value matches a table card, the table card must be captured (single card, not a sum)
  2. If multiple table cards match the played value, exactly one is captured (player chooses)
  3. If no direct match, the player may capture a subset of table cards summing to the played value
  4. If the played card matches both a single card and a sum, the single card must be captured (not the sum)
  5. There is no obligation to play a capturing card — a player may choose to play a non-capturing card instead. But if the played card CAN capture, it MUST capture.
  6. Scopa: capturing ALL remaining table cards awards +1 point (except on the very last card of the round)

Scoring (per round, 4 fixed points + scope)

Category Rule
Carte Team with majority of captured cards (20+ of 40). Tie = no point.
Denari Team with majority of coins/denara suit cards (6+ of 10). Tie = no point.
Settebello Team capturing the 7 of denara. Always awarded.
Primiera Team with highest prime value. Prime = best card per suit using special scale. Tie = no point. Must have all 4 suits.
Scope +1 per scopa achieved during play.

Primiera Values (confirmed matching codebase)

Card value Primiera value
7 21
6 18
1 (Ace) 16
5 15
4 14
3 13
2 12
8,9,10 10

A team missing an entire suit cannot win primiera (even 3×21=63 loses to 21+16+16+16=69 with all 4 suits).

Winning

  • First team to 11+ points at the end of a round wins
  • If both reach 11 in the same round, higher total wins; if tied, play continues

Strategy Notes (from Pagat.com)

  • 7 of coins (settebello) is the single most valuable card — contributes to all 4 fixed scoring categories
  • Avoid giving scope: leave table total ≥ 11 when possible
  • Anchor strategy: leave a card on table that your team controls (you hold duplicates of that value)
  • Whirlwind: consecutive scope — clearing the table forces opponent to play, partner captures, repeat
  • Sevens > sixes > aces in priority for primiera control
  • Paired/unpaired tracking: if all captures are single-card matches, the last card matches the last table card. Sum captures disrupt this pattern, important for end-game planning.

Codebase Capture Rule Validation

The findCaptures() in engine.ts correctly implements:

  • Direct match priority over sum captures ✓
  • Multiple direct matches: takes ALL matching cards (slight deviation — pagat.com says choose ONE, but Wikipedia says take all. The codebase takes all direct matches.)
  • Sum subsets via power set enumeration ✓
  • applyMove() auto-captures when possible ✓

SCOPONE-0003: findCaptures() Change Analysis (2026-03-31)

Current behavior (engine.ts lines 43-62): When direct matches exist, findCaptures() bundles ALL into one Card[] and returns immediately (results.push([...directMatches]); return results). Sum captures are never computed when direct matches exist.

Required behavior:

  1. Each direct match returned as a separate single-card option: [match_a], [match_b], etc.
  2. Sum captures computed alongside direct matches (removing the early return).
  3. Both direct-match and sum options presented to the player for choice.

Mathematical proof that sums never include direct-match cards: A direct-match card has value V = played.value. Any subset of size ≥ 2 containing this card sums to at least V + 1 (minimum card value is 1). Since we're looking for subsets summing to exactly V, no valid sum can ever include a direct-match card. Therefore, computing sums from the entire table is safe — no filtering needed.

Callers impacted:

  • canCapture() (engine.ts:77): Uses .length > 0 — unaffected.
  • applyMove() (engine.ts:139): Uses captures[0] as default — now defaults to the first direct-match card (single card) instead of all bundled matches. Correct behavior.
  • chooseMove() (ai.ts:23): Iterates and scores all capture sets — now scores each direct match individually. Improvement: AI can now prefer the higher-value direct match (e.g., 7♦ over 7♣).
  • opponentThreatScore() (ai.ts:151): Uses caps[0].length — changes from N to 1 for multi-match scenarios. More accurate (opponent captures one card, not all).
  • scoreDump() (ai.ts:103): Uses caps for threat checking — unaffected.
  • GameScene.onCardClick(): Already routes captures.length > 1 to highlightMultipleCaptures() — works correctly with new behavior.

Minimax Feasibility Analysis

  • 10 cards per player × 4 players = 40 total moves per round
  • Full game tree: ~10^12 nodes — infeasible for exhaustive search
  • Approach: Depth-limited alpha-beta with determinization for imperfect information
    • Sample N possible opponent hand assignments consistent with card tracking
    • Run minimax on each sample to limited depth (46 plies)
    • Average/vote across samples for best move
    • Alpha-beta pruning reduces effective branching factor significantly
    • Depth 4 (one full rotation) with ~5 moves per player = ~625 nodes per sample — very manageable
    • 1020 samples × 625 nodes = ~6,00012,500 evaluations — runs in <100ms on modern hardware