diff --git a/docs/FINDINGS.md b/docs/FINDINGS.md index b3cf0a5..0b4fc19 100644 --- a/docs/FINDINGS.md +++ b/docs/FINDINGS.md @@ -83,10 +83,29 @@ A team missing an entire suit **cannot win primiera** (even 3×21=63 loses to 21 ### 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. This is the **existing behavior** and must not be altered per success criteria.) +- 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 diff --git a/public/atlas.json b/public/atlas.json index 7dad1b5..9e8491e 100644 --- a/public/atlas.json +++ b/public/atlas.json @@ -805,4 +805,4 @@ "image": "atlas.png", "scale": "1" } -} \ No newline at end of file +}