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

119 lines
7.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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