119 lines
7.5 KiB
Markdown
119 lines
7.5 KiB
Markdown
# 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 1–10
|
||
- **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 (4–6 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
|
||
- 10–20 samples × 625 nodes = ~6,000–12,500 evaluations — runs in <100ms on modern hardware
|