Merge branch 'feature/SCOPONE-0003_choose_capture_target' into feature/SCOPONE-0002_rotate_round_starter
This commit is contained in:
@@ -83,10 +83,29 @@ A team missing an entire suit **cannot win primiera** (even 3×21=63 loses to 21
|
|||||||
### Codebase Capture Rule Validation
|
### Codebase Capture Rule Validation
|
||||||
The `findCaptures()` in `engine.ts` correctly implements:
|
The `findCaptures()` in `engine.ts` correctly implements:
|
||||||
- Direct match priority over sum captures ✓
|
- 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 ✓
|
- Sum subsets via power set enumeration ✓
|
||||||
- `applyMove()` auto-captures when possible ✓
|
- `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
|
### Minimax Feasibility Analysis
|
||||||
- 10 cards per player × 4 players = 40 total moves per round
|
- 10 cards per player × 4 players = 40 total moves per round
|
||||||
- Full game tree: ~10^12 nodes — infeasible for exhaustive search
|
- Full game tree: ~10^12 nodes — infeasible for exhaustive search
|
||||||
|
|||||||
@@ -805,4 +805,4 @@
|
|||||||
"image": "atlas.png",
|
"image": "atlas.png",
|
||||||
"scale": "1"
|
"scale": "1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,17 +41,13 @@ export function shuffle<T>(arr: T[]): T[] {
|
|||||||
export function findCaptures(played: Card, table: Card[]): Card[][] {
|
export function findCaptures(played: Card, table: Card[]): Card[][] {
|
||||||
const results: Card[][] = [];
|
const results: Card[][] = [];
|
||||||
|
|
||||||
// Check for direct value matches
|
// Each direct-match card is a separate single-card capture option
|
||||||
const directMatches = table.filter(c => c.value === played.value);
|
const directMatches = table.filter(c => c.value === played.value);
|
||||||
if (directMatches.length > 0) {
|
for (const dm of directMatches) {
|
||||||
// Must capture exactly one matching card (Italian rules: take one direct match)
|
results.push([dm]);
|
||||||
// Actually: if there's one direct match take it; if multiple, still take all that match
|
|
||||||
// Standard Italian rule: take ALL cards of matching value
|
|
||||||
results.push([...directMatches]);
|
|
||||||
return results; // direct match takes priority, no sum captures allowed
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// No direct matches — find subsets that sum to played.value
|
// Also find multi-card subsets that sum to played.value
|
||||||
const subsets = getSubsets(table);
|
const subsets = getSubsets(table);
|
||||||
for (const subset of subsets) {
|
for (const subset of subsets) {
|
||||||
if (subset.length >= 2) {
|
if (subset.length >= 2) {
|
||||||
|
|||||||
@@ -674,32 +674,45 @@ export class GameScene extends Phaser.Scene {
|
|||||||
private highlightMultipleCaptures(captures: Card[][]): void {
|
private highlightMultipleCaptures(captures: Card[][]): void {
|
||||||
this.clearHighlights();
|
this.clearHighlights();
|
||||||
const W = this.scale.width;
|
const W = this.scale.width;
|
||||||
|
|
||||||
|
// Distinct color palette for each capture option
|
||||||
|
const palette = [
|
||||||
|
{ fill: 0x00cc66, stroke: 0x00ff88, text: '#00ffaa', bg: 0x001a0a },
|
||||||
|
{ fill: 0x3399ff, stroke: 0x66bbff, text: '#88ccff', bg: 0x001020 },
|
||||||
|
{ fill: 0xff8833, stroke: 0xffaa55, text: '#ffcc88', bg: 0x1a0d00 },
|
||||||
|
{ fill: 0xcc44cc, stroke: 0xff66ff, text: '#ff88ff', bg: 0x1a001a },
|
||||||
|
{ fill: 0x00cccc, stroke: 0x44ffff, text: '#88ffff', bg: 0x001a1a },
|
||||||
|
];
|
||||||
|
|
||||||
captures.forEach((cap, i) => {
|
captures.forEach((cap, i) => {
|
||||||
|
const color = palette[i % palette.length];
|
||||||
const label = cap.map(cardName).join(' + ');
|
const label = cap.map(cardName).join(' + ');
|
||||||
const y = SCOREBAR_H + 70 + i * 36;
|
const y = SCOREBAR_H + 70 + i * 36;
|
||||||
const bg = this.add.graphics().setDepth(20);
|
const bg = this.add.graphics().setDepth(20);
|
||||||
bg.fillStyle(0x001a0a, 0.9);
|
bg.fillStyle(color.bg, 0.9);
|
||||||
bg.fillRoundedRect(W / 2 - 180, y - 14, 360, 28, 7);
|
bg.fillRoundedRect(W / 2 - 180, y - 14, 360, 28, 7);
|
||||||
bg.lineStyle(1, 0x00ff88, 0.7);
|
bg.lineStyle(2, color.stroke, 0.8);
|
||||||
bg.strokeRoundedRect(W / 2 - 180, y - 14, 360, 28, 7);
|
bg.strokeRoundedRect(W / 2 - 180, y - 14, 360, 28, 7);
|
||||||
const btn = this.add.zone(W / 2, y, 360, 28).setInteractive({ useHandCursor: true }).setDepth(21);
|
const btn = this.add.zone(W / 2, y, 360, 28).setInteractive({ useHandCursor: true }).setDepth(21);
|
||||||
const txt = this.add.text(W / 2, y, `Cattura: ${label}`, {
|
const txt = this.add.text(W / 2, y, `Cattura: ${label}`, {
|
||||||
fontFamily: 'serif', fontSize: '14px', color: '#00ffaa',
|
fontFamily: 'serif', fontSize: '14px', color: color.text,
|
||||||
}).setOrigin(0.5).setDepth(21);
|
}).setOrigin(0.5).setDepth(21);
|
||||||
btn.on('pointerdown', () => this.confirmMove(this.selectedCard!, cap));
|
btn.on('pointerdown', () => this.confirmMove(this.selectedCard!, cap));
|
||||||
(bg as any)._captureBtn = true;
|
(bg as any)._captureBtn = true;
|
||||||
this.tableHighlights.push(bg, btn, txt);
|
this.tableHighlights.push(bg, btn, txt);
|
||||||
});
|
|
||||||
for (const cap of captures) {
|
// Highlight table cards belonging to this option with the matching color
|
||||||
for (const c of cap) {
|
for (const c of cap) {
|
||||||
const img = this.cardImages.get(c.id);
|
const img = this.cardImages.get(c.id);
|
||||||
if (img) {
|
if (img) {
|
||||||
const hl = this.add.rectangle(img.x, img.y, CW_H + 8, CH_H + 8, 0xffff00, 0.15)
|
const hl = this.add.rectangle(img.x, img.y, CW_H + 8, CH_H + 8, color.fill, 0.2)
|
||||||
.setStrokeStyle(2, 0xffff00, 0.8).setDepth(4);
|
.setStrokeStyle(2, color.stroke, 0.9).setDepth(4);
|
||||||
this.tableHighlights.push(hl);
|
this.tableHighlights.push(hl);
|
||||||
|
img.setInteractive({ useHandCursor: true });
|
||||||
|
img.once('pointerdown', () => this.confirmMove(this.selectedCard!, cap));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private highlightTableForDump(card: Card): void {
|
private highlightTableForDump(card: Card): void {
|
||||||
|
|||||||
Reference in New Issue
Block a user