Files
scopone/docs/FINDINGS.md
2026-04-02 20:10:55 +02:00

9.2 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-0004: Phaser Text Resolution for Crisp Rendering (2026-03-31)

Source: Phaser 3 API docs (Context7 — /websites/phaser_io_api-documentation)

Phaser Text objects render to an internal Canvas. When the game uses Scale.FIT (scaling 1280×720 to a larger display), the text canvas is rasterized at 1× and then upscaled by CSS, causing blur.

Fix: Set the resolution property on each TextStyle or call text.setResolution(value). A value of 2 doubles the internal canvas size, producing sharper text on high-DPI and scaled displays at the cost of more memory. The style property resolution: 0 (default) uses the game's resolution (defaults to 1).

Recommended: Use resolution: 2 in all text style objects for critical UI text (menu, score bar, labels). Avoid higher values to limit memory impact.

Available since: Phaser 3.12.0 (current project uses ^3.87.0).

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.

SCOPONE-0008: Phaser AI Turn Progress Timing (2026-04-02)

Source: Phaser 3 API docs (Context7 — /websites/phaser_io_api-documentation)

  • Phaser.Time.TimerEvent#getProgress() returns the current iteration progress as a normalized value between 0 and 1.
  • Phaser.Time.TimerEvent#getOverallProgress() returns normalized overall progress when repeats are involved.
  • TimerEvent instances are managed by the scene clock, so they are suitable for rendering an AI decision countdown bar that reflects elapsed scene time.

Planning impact:

  • A visible AI countdown can be driven from elapsed timer progress instead of a blind tween.
  • If the AI search budget increases beyond the current short synchronous window, the search loop must yield back to the browser between batches or Phaser will not repaint the progress bar while the AI is thinking.

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