feat(SCOPONE-0008): refresh project docs
This commit is contained in:
@@ -1,147 +1,199 @@
|
||||
# Architecture
|
||||
|
||||
> Last Updated: 2026-03-31T00:00:00.000Z
|
||||
> Last Updated: 2026-04-02T18:12:18.000Z
|
||||
|
||||
## Overview
|
||||
|
||||
| Attribute | Value |
|
||||
|-----------------|--------------------------------------------------------|
|
||||
| **Language** | TypeScript (ES2020 target, strict mode) |
|
||||
| **Type** | 2D card game — Scopone Scientifico |
|
||||
| **Framework** | Phaser 3.87+ (scene-based game engine) |
|
||||
| **Bundler** | Vite 5 |
|
||||
| **Native** | Capacitor 8.3 (Android) |
|
||||
| **Resolution** | 1280 × 720, FIT scaling with auto-center |
|
||||
| Attribute | Value |
|
||||
|-----------|-------|
|
||||
| Primary language | TypeScript |
|
||||
| Secondary language | Java (Capacitor Android shell) |
|
||||
| Project type | Browser card game with Android packaging |
|
||||
| Framework | Phaser 3.87.0 |
|
||||
| Tooling | Vite 5, TypeScript 5.x, Capacitor 8.3 |
|
||||
| Runtime layout | 1280 x 720, `Phaser.Scale.FIT`, centered in `#game` |
|
||||
| Build command | `npm run build` |
|
||||
| Test command | `npx tsc --noEmit` |
|
||||
|
||||
The repository is a TypeScript-first Scopone Scientifico implementation. The web client contains the real game logic and presentation. The Android tree is a Capacitor wrapper with a custom immersive `MainActivity`.
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
```text
|
||||
scopone-phaser/
|
||||
├── src/
|
||||
│ ├── main.ts # Phaser.Game bootstrap + config
|
||||
│ ├── game/
|
||||
│ │ ├── types.ts # Card, Suit, Player, GameState, TeamScore, ScoreBreakdown, PRIMIERA_VALUES
|
||||
│ │ ├── engine.ts # Deck build, shuffle, capture logic, applyMove, scoring, primiera
|
||||
│ │ └── ai.ts # Heuristic AI: chooseMove, scoreCapture, scoreDump
|
||||
│ └── scenes/
|
||||
│ ├── BootScene.ts # Asset loading (atlas, card back image)
|
||||
│ ├── MenuScene.ts # Start menu with rules summary
|
||||
│ └── GameScene.ts # Main game: rendering, turn management, effects, audio
|
||||
├── index.html # Entry point, Italian locale, green felt background
|
||||
├── public/ # Static assets (atlas.json, atlas.png, retro.png)
|
||||
├── android/ # Capacitor Android native shell
|
||||
├── package.json
|
||||
├── tsconfig.json
|
||||
├── vite.config.ts
|
||||
└── capacitor.config.ts
|
||||
|- src/
|
||||
| |- main.ts
|
||||
| |- game/
|
||||
| | |- types.ts
|
||||
| | |- engine.ts
|
||||
| | |- card-tracker.ts
|
||||
| | `- ai.ts
|
||||
| `- scenes/
|
||||
| |- BootScene.ts
|
||||
| |- MenuScene.ts
|
||||
| `- GameScene.ts
|
||||
|- public/
|
||||
|- android/
|
||||
|- docs/
|
||||
|- prompts/
|
||||
|- package.json
|
||||
|- tsconfig.json
|
||||
|- vite.config.ts
|
||||
`- capacitor.config.ts
|
||||
```
|
||||
|
||||
## Key Directories
|
||||
|
||||
| Directory | Purpose |
|
||||
|--------------------|----------------------------------------------------------------|
|
||||
| `src/game/` | Domain logic — types, game engine, AI (no Phaser dependency) |
|
||||
| `src/scenes/` | Phaser scenes — rendering, input, effects, audio |
|
||||
| `public/` | Static assets served by Vite (card atlas, card back) |
|
||||
| `android/` | Capacitor-generated Android project (Gradle, Java) |
|
||||
| `prompts/` | JIRA agent pipeline artifacts |
|
||||
| Directory | Purpose |
|
||||
|-----------|---------|
|
||||
| `src/game/` | Framework-independent rules, scoring, imperfect-information tracking, and AI search |
|
||||
| `src/scenes/` | Phaser scene lifecycle, UI, animation, effects, and round orchestration |
|
||||
| `public/` | Static web assets consumed by Phaser loaders |
|
||||
| `android/` | Capacitor Android project, Gradle config, and immersive activity wrapper |
|
||||
| `docs/` | Architecture, code style, findings, and cache metadata |
|
||||
| `prompts/` | JIRA pipeline artifacts and iteration state |
|
||||
|
||||
## Design Patterns
|
||||
|
||||
| Pattern | Where |
|
||||
|-----------------------------|--------------------------------------------------------------------|
|
||||
| **Scene lifecycle** | BootScene → MenuScene → GameScene (Phaser scene graph) |
|
||||
| **Immutable state updates** | `applyMove()` deep-clones `GameState` before mutation |
|
||||
| **Heuristic scoring** | AI evaluates all legal moves with weighted feature scores |
|
||||
| **Separation of concerns** | `game/` has no Phaser imports; `scenes/` bridges game ↔ rendering |
|
||||
| **Procedural audio** | Web Audio API oscillators + delay reverb — no audio files |
|
||||
No explicit GoF patterns were detected in the source or by semantic search.
|
||||
|
||||
No explicit GoF patterns (singleton, factory, observer, DI) detected.
|
||||
Observed architectural patterns in the current codebase:
|
||||
|
||||
| Pattern | Where it appears |
|
||||
|---------|------------------|
|
||||
| Scene-based flow | `BootScene -> MenuScene -> GameScene` via Phaser scene registration |
|
||||
| Functional core / imperative shell | `src/game/` stays free of Phaser imports, while `src/scenes/` owns rendering and input |
|
||||
| Clone-before-mutate state transitions | `applyMove()` clones `GameState` before applying move effects |
|
||||
| Determinization search | Master AI samples hidden hands before alpha-beta evaluation |
|
||||
| Callback-driven progress reporting | `chooseMove()` reports `AIDecisionProgress` to `GameScene` for the think bar |
|
||||
|
||||
## Key Components
|
||||
|
||||
### `types.ts` — Domain Types
|
||||
### `src/main.ts`
|
||||
|
||||
- `Card { suit, value, id }` — 40-card Napoletane deck (suits: bastoni, coppe, denara, spade; values 1-10)
|
||||
- `GameState` — full round state: 4 players, table, current player, team scores, round tracking
|
||||
- `TeamScore` — per-team stats: cards, scope, denari, settebello, primiera, round/total points
|
||||
- `ScoreBreakdown` — which team won each scoring category
|
||||
- `PRIMIERA_VALUES` — lookup table for primiera card values
|
||||
- Bootstraps `Phaser.Game`.
|
||||
- Registers `BootScene`, `MenuScene`, and `GameScene`.
|
||||
- Installs a one-shot fullscreen request handler on first user interaction.
|
||||
|
||||
### `engine.ts` — Game Logic
|
||||
### `src/game/types.ts`
|
||||
|
||||
- `buildDeck()` / `shuffle()` — Fisher-Yates 40-card deck
|
||||
- `createInitialState()` — deals 10 cards per player, empty table (Scopone Scientifico rules)
|
||||
- `findCaptures(played, table)` — direct value match (mandatory) or subset-sum combinations
|
||||
- `applyMove(state, player, card, captureChoice)` — immutable state transition, scopa detection, end-of-round scoring
|
||||
- `calculateScores()` / `scoreRound()` — carte, denari, settebello, primiera, scope points
|
||||
- `calcPrimiera(pile)` — best card per suit using `PRIMIERA_VALUES`
|
||||
- `teamOf(playerIdx)` — team assignment: 0+2 = Team A, 1+3 = Team B
|
||||
- Defines the game model: `Card`, `Capture`, `Player`, `GameState`, `TeamScore`, `ScoreBreakdown`.
|
||||
- Encodes difficulty tiers as `'beginner' | 'advanced' | 'master'`.
|
||||
- Stores the `PRIMIERA_VALUES` lookup table.
|
||||
|
||||
### `ai.ts` — Heuristic AI
|
||||
### `src/game/engine.ts` (371 lines)
|
||||
|
||||
- `chooseMove(state, playerIdx)` — evaluates all legal moves (captures + dumps)
|
||||
- `scoreCapture()` — weighted: scopa (+500), settebello (+300), denari (+50 each), card count, primiera value, opponent threat
|
||||
- `scoreDump()` — avoids giving opponents scopa (-400), prefers low-value non-denari, penalises dumping 7s and aces
|
||||
- Builds and shuffles the 40-card deck.
|
||||
- Creates the initial round state for four players.
|
||||
- Implements capture selection rules: single direct matches take priority; subset sums are considered only when no direct match exists.
|
||||
- Applies moves immutably, detects scopas, assigns leftover table cards, and computes round scores.
|
||||
|
||||
### `GameScene.ts` — Main Scene (~1340 lines)
|
||||
### `src/game/card-tracker.ts` (89 lines)
|
||||
|
||||
- Four-player layout: South (human), West/East (AI, rotated ±90°), North (AI partner)
|
||||
- Deal animation with staggered tweens
|
||||
- Card selection with postFX glow pulse
|
||||
- Capture highlighting with multiple-choice UI
|
||||
- Particle effects: capture burst, scopa explosion, settebello flash, denari shimmer, primiera glow, card trails, victory confetti
|
||||
- Camera shake + flash on scopa and settebello
|
||||
- Live score bar with animated counter updates
|
||||
- Think bar progress indicator during AI turns
|
||||
- Procedural background music (oscillator drone + triangle melody + chord stabs)
|
||||
- Round-end summary panel and game-over screen
|
||||
- Tracks seen cards across a round without exposing hidden hands directly.
|
||||
- Computes unseen cards from `played + myHand + table`.
|
||||
- Supplies probability helpers used by the AI for value-based inference.
|
||||
|
||||
### `src/game/ai.ts` (1210 lines)
|
||||
|
||||
- Exposes `chooseMove()` as an async entry point.
|
||||
- Implements three difficulty levels:
|
||||
- `beginner`: noisy heuristic play.
|
||||
- `advanced`: stronger heuristics with race awareness, partner setup, and card-tracker inference.
|
||||
- `master`: determinization plus alpha-beta search with dynamic time budgets, batching, and progress callbacks.
|
||||
- Uses `yieldToBrowser()` between master-search batches so Phaser can repaint the think bar.
|
||||
|
||||
### `src/scenes/BootScene.ts` (47 lines)
|
||||
|
||||
- Loads the card atlas and card-back texture.
|
||||
- Shows a simple progress bar and transitions into the menu.
|
||||
|
||||
### `src/scenes/MenuScene.ts` (103 lines)
|
||||
|
||||
- Renders the title screen and rules summary.
|
||||
- Lets the player choose `beginner`, `advanced`, or `master` difficulty.
|
||||
- Starts `GameScene` with the selected difficulty in scene data.
|
||||
|
||||
### `src/scenes/GameScene.ts` (1446 lines)
|
||||
|
||||
- Owns the match loop, HUD, think bar, card interaction, animation, FX, audio, and round transitions.
|
||||
- Uses `CardTracker` to record played and captured cards after each move.
|
||||
- Bridges async AI progress into a visible top-of-screen think bar.
|
||||
- Handles end-of-round overlays and full-match restart flow.
|
||||
|
||||
### `android/app/src/main/java/com/phaser/scopa/MainActivity.java`
|
||||
|
||||
- Extends `BridgeActivity`.
|
||||
- Forces immersive mode by hiding status and navigation bars whenever the window gains focus.
|
||||
|
||||
## Dependencies
|
||||
|
||||
### Production
|
||||
### JavaScript production dependencies
|
||||
|
||||
| Package | Version | Purpose |
|
||||
|----------------------|----------|--------------------------------------|
|
||||
| `phaser` | ^3.87.0 | 2D game engine |
|
||||
| `@capacitor/core` | ^8.3.0 | Capacitor runtime |
|
||||
| `@capacitor/cli` | ^8.3.0 | Capacitor CLI |
|
||||
| `@capacitor/android` | ^8.3.0 | Android platform plugin |
|
||||
| Package | Version | Purpose |
|
||||
|---------|---------|---------|
|
||||
| `phaser` | `^3.87.0` | Game engine |
|
||||
| `@capacitor/core` | `^8.3.0` | Capacitor runtime |
|
||||
| `@capacitor/cli` | `^8.3.0` | Capacitor tooling |
|
||||
| `@capacitor/android` | `^8.3.0` | Android platform integration |
|
||||
|
||||
### Development
|
||||
### JavaScript development dependencies
|
||||
|
||||
| Package | Version | Purpose |
|
||||
|---------------|---------|--------------------------|
|
||||
| `typescript` | ^5.0.0 | TypeScript compiler |
|
||||
| `vite` | ^5.0.0 | Dev server and bundler |
|
||||
| Package | Version | Purpose |
|
||||
|---------|---------|---------|
|
||||
| `typescript` | `^5.0.0` | Type-checking and TS compilation for builds |
|
||||
| `vite` | `^5.0.0` | Dev server and bundling |
|
||||
|
||||
## Module Organisation
|
||||
### Android / Gradle dependencies
|
||||
|
||||
```
|
||||
main.ts ──→ BootScene ──→ MenuScene ──→ GameScene
|
||||
│
|
||||
├── game/engine (createInitialState, applyMove, findCaptures, ...)
|
||||
├── game/ai (chooseMove)
|
||||
└── game/types (Card, GameState, ...)
|
||||
| Dependency | Source | Purpose |
|
||||
|------------|--------|---------|
|
||||
| `com.android.tools.build:gradle:8.13.0` | `android/build.gradle` | Android build plugin |
|
||||
| `com.google.gms:google-services:4.4.4` | `android/build.gradle` | Optional Google services integration |
|
||||
| `androidx.appcompat:appcompat` | `android/app/build.gradle` | Android UI compatibility |
|
||||
| `androidx.coordinatorlayout:coordinatorlayout` | `android/app/build.gradle` | Android layout support |
|
||||
| `androidx.core:core-splashscreen` | `android/app/build.gradle` | Splash screen support |
|
||||
| `junit:junit` | `android/app/build.gradle` | JVM-side Android tests |
|
||||
| `androidx.test.ext:junit` | `android/app/build.gradle` | Instrumented Android testing |
|
||||
| `androidx.test.espresso:espresso-core` | `android/app/build.gradle` | Android UI testing |
|
||||
|
||||
## Module Organization
|
||||
|
||||
```text
|
||||
main.ts
|
||||
-> BootScene
|
||||
-> MenuScene
|
||||
-> GameScene
|
||||
-> engine.ts
|
||||
-> ai.ts
|
||||
-> card-tracker.ts
|
||||
-> types.ts
|
||||
```
|
||||
|
||||
`game/` modules are pure logic with no framework coupling. `scenes/` imports from `game/` but never vice versa.
|
||||
Dependencies are one-directional at the application level:
|
||||
|
||||
- `src/game/` imports only from sibling game modules.
|
||||
- `src/scenes/` imports from `src/game/`.
|
||||
- `src/game/` never imports Phaser.
|
||||
|
||||
## Data Flow
|
||||
|
||||
1. `createInitialState()` builds shuffled deck, deals 10 cards each, empty table
|
||||
2. `GameScene.nextTurn()` detects current player: human → enable input; AI → delay + `chooseMove()`
|
||||
3. `applyMove()` returns new `GameState` + capture result + scopa flag
|
||||
4. `GameScene.executeMove()` animates: card flight → capture burst → pile collection
|
||||
5. `updateScoreBar()` reflects live team stats with animated counter tweens
|
||||
6. When all hands empty → `calculateScores()` → round-end overlay
|
||||
7. First team to 11 points → game-over screen → optional restart
|
||||
1. `main.ts` creates the Phaser app and registers all scenes.
|
||||
2. `BootScene` loads assets, then starts `MenuScene`.
|
||||
3. `MenuScene` passes the chosen difficulty into `GameScene`.
|
||||
4. `GameScene.create()` initializes a new `CardTracker`, creates the initial `GameState`, and animates the opening deal.
|
||||
5. On each turn:
|
||||
- Human turns use click-driven selection and capture highlighting.
|
||||
- AI turns call `chooseMove(state, playerIdx, difficulty, tracker, onProgress)`.
|
||||
6. `chooseMove()` either returns immediately for heuristic tiers or performs batched master search while reporting `AIDecisionProgress`.
|
||||
7. `GameScene.executeMove()` applies the move, updates the tracker, animates the result, refreshes the HUD, and advances the round.
|
||||
8. When all hands are empty, `engine.ts` finalizes scoring and `GameScene` displays the round summary or final match screen.
|
||||
|
||||
## Build System
|
||||
|
||||
| Command | Action |
|
||||
|--------------------|--------------------------------------------|
|
||||
| `npm run dev` | `vite` — dev server on port 3000 |
|
||||
| `npm run build` | `tsc && vite build` — compile + bundle to `dist/` |
|
||||
| `npm run preview` | `vite preview` — preview production build |
|
||||
| `tsc --noEmit` | Type-check only (no test framework) |
|
||||
| Command | Source | Purpose |
|
||||
|---------|--------|---------|
|
||||
| `npm run dev` | `package.json` | Starts the Vite development server on port 3000 and opens the browser |
|
||||
| `npm run build` | user-provided build command | Runs `tsc && vite build` and writes web output to `dist/` |
|
||||
| `npm run preview` | `package.json` | Serves the built app locally via Vite preview |
|
||||
| `npx tsc --noEmit` | user-provided test command | Type-checks the TypeScript codebase without emitting files |
|
||||
|
||||
@@ -1,145 +1,149 @@
|
||||
# Code Style
|
||||
|
||||
> Last Updated: 2026-03-31T00:00:00.000Z
|
||||
> Last Updated: 2026-04-02T18:12:18.000Z
|
||||
|
||||
## Language & Version
|
||||
|
||||
- **TypeScript** 5.x, strict mode enabled
|
||||
- Target: **ES2020**, module: **ESNext**, module resolution: **bundler**
|
||||
- `noEmit: true` — Vite handles transpilation; `tsc` is used for type-checking only
|
||||
- Primary language: TypeScript 5.x.
|
||||
- Compiler settings from `tsconfig.json`: `target: ES2020`, `module: ESNext`, `moduleResolution: bundler`, `strict: true`, `noEmit: true`.
|
||||
- Secondary language: Java for the Capacitor Android wrapper.
|
||||
|
||||
## Naming Conventions
|
||||
|
||||
| Kind | Convention | Examples |
|
||||
|------------------|---------------|-------------------------------------------------------------------|
|
||||
| Types/Interfaces | PascalCase | `Card`, `GameState`, `PlayerIndex`, `TeamScore`, `ScoreBreakdown` |
|
||||
| Type aliases | PascalCase | `Suit`, `PlayerIndex` |
|
||||
| Classes | PascalCase | `BootScene`, `MenuScene`, `GameScene` |
|
||||
| Functions | camelCase | `buildDeck()`, `findCaptures()`, `chooseMove()`, `applyMove()` |
|
||||
| Constants | UPPER_SNAKE | `PRIMIERA_VALUES`, `SUITS`, `AI_DELAY`, `SCOREBAR_H` |
|
||||
| Local variables | camelCase | `bestMove`, `capturedCards`, `isScopa`, `afterTable` |
|
||||
| Private members | camelCase | `this.state`, `this.cardImages`, `this.aiThinking` |
|
||||
| Parameters | camelCase | `playerIdx`, `captureChoice`, `onComplete` |
|
||||
| Kind | Convention | Real examples |
|
||||
|------|------------|---------------|
|
||||
| Classes | PascalCase | `BootScene`, `MenuScene`, `GameScene`, `CardTracker` |
|
||||
| Interfaces | PascalCase | `Card`, `Capture`, `GameState`, `AIDecisionProgress`, `SearchProfile` |
|
||||
| Type aliases | PascalCase | `Suit`, `PlayerIndex`, `Difficulty` |
|
||||
| Functions | camelCase | `installFullscreenRequest`, `findCaptures`, `createInitialState`, `chooseMove`, `updateThinkBar` |
|
||||
| Constants | UPPER_SNAKE or descriptive `const` names | `SUITS`, `PRIMIERA_VALUES`, `SEARCH_PROFILES`, `SCOREBAR_H` |
|
||||
| Private fields | camelCase with `private` modifier | `state`, `tracker`, `thinkBar`, `selectedGlowTween` |
|
||||
| Parameters and locals | camelCase | `playerIdx`, `captureChoice`, `remainingRatio`, `partnerHandSize` |
|
||||
|
||||
## Class & Scene Patterns
|
||||
## Structural Conventions
|
||||
|
||||
Scenes extend `Phaser.Scene` and follow the Phaser lifecycle:
|
||||
|
||||
```ts
|
||||
export class BootScene extends Phaser.Scene {
|
||||
constructor() {
|
||||
super({ key: 'BootScene' });
|
||||
}
|
||||
preload(): void { /* asset loading */ }
|
||||
create(): void { /* scene setup */ }
|
||||
}
|
||||
```
|
||||
|
||||
Scene registration via `Phaser.Types.Core.GameConfig.scene` array in `main.ts`.
|
||||
- Phaser scenes extend `Phaser.Scene` and put setup in `create()` and asset loading in `preload()`.
|
||||
- Core rule modules use exported functions and interfaces rather than classes.
|
||||
- Stateful scene code uses `private` fields for long-lived UI and interaction state.
|
||||
- AI configuration is table-driven through `SEARCH_PROFILES` and helper interfaces rather than nested literals inside `chooseMove()`.
|
||||
|
||||
## Indentation & Formatting
|
||||
|
||||
- **2-space** indentation
|
||||
- **Single quotes** for string literals
|
||||
- No semicolons omission — **semicolons used consistently**
|
||||
- Trailing commas in multi-line objects/arrays
|
||||
- `const` preferred; `let` when reassignment is needed; no `var`
|
||||
- TypeScript files use 2-space indentation.
|
||||
- String literals use single quotes consistently.
|
||||
- Semicolons are used consistently.
|
||||
- Early returns are common for guard clauses.
|
||||
- Multi-line object literals and function calls keep trailing commas when the surrounding style already uses them.
|
||||
- `const` is the default; `let` is used only where reassignment is needed.
|
||||
|
||||
The Android Java wrapper follows the generated template style instead of the TypeScript style. `MainActivity.java` uses tab indentation and standard Android brace placement.
|
||||
|
||||
## Import Patterns
|
||||
|
||||
Named imports from local modules:
|
||||
Named imports are used for local modules:
|
||||
|
||||
```ts
|
||||
import { Card, GameState, PlayerIndex } from './types';
|
||||
import { findCaptures, canCapture, calcPrimiera, teamOf } from './engine';
|
||||
import { chooseMove } from '../game/ai';
|
||||
import { Card, GameState, PlayerIndex, Difficulty } from './types';
|
||||
import { findCaptures, canCapture, teamOf, applyMove } from './engine';
|
||||
import { CardTracker } from './card-tracker';
|
||||
```
|
||||
|
||||
Default import for Phaser:
|
||||
Framework imports use default or type-only imports where appropriate:
|
||||
|
||||
```ts
|
||||
import Phaser from 'phaser';
|
||||
```
|
||||
|
||||
Type-only import for Capacitor config:
|
||||
|
||||
```ts
|
||||
import type { CapacitorConfig } from '@capacitor/cli';
|
||||
```
|
||||
|
||||
Relative paths only (`./`, `../`). No path aliases configured.
|
||||
The project uses only relative import paths. No path aliases are configured.
|
||||
|
||||
## Export Patterns
|
||||
|
||||
- **Named exports** for all public symbols (`export function`, `export class`, `export interface`, `export type`, `export const`)
|
||||
- No default exports except `vite.config.ts` and `capacitor.config.ts` (framework convention)
|
||||
- Private/internal functions are not exported (`getSubsets`, `calculateScores`, `scoreRound`, `deepClone`)
|
||||
- Public APIs use named exports: `export function`, `export class`, `export interface`, `export type`, `export const`.
|
||||
- Default exports are reserved for configuration entry points: `vite.config.ts` and `capacitor.config.ts`.
|
||||
- Internal helpers remain file-local, for example `getSubsets`, `scoreRound`, `shuffleArray`, and `alphaBeta`.
|
||||
|
||||
## Type Annotations
|
||||
## Typing Patterns
|
||||
|
||||
- Explicit return types on exported functions: `(): Card[]`, `(): GameState`, `(): boolean`
|
||||
- Union types for constrained values: `PlayerIndex = 0 | 1 | 2 | 3`
|
||||
- String literal unions: `Suit = 'bastoni' | 'coppe' | 'denara' | 'spade'`
|
||||
- Tuple types for fixed-length arrays: `[Player, Player, Player, Player]`, `[TeamScore, TeamScore]`
|
||||
- `Record<K, V>` for maps: `PRIMIERA_VALUES: Record<number, number>`
|
||||
- Union types model constrained domains: `PlayerIndex = 0 | 1 | 2 | 3`, `Difficulty = 'beginner' | 'advanced' | 'master'`.
|
||||
- Fixed-size tuples represent stable game structures such as four players and two team scores.
|
||||
- `Record<number, number>` is used for lookup tables like `PRIMIERA_VALUES`.
|
||||
- Exported functions generally declare explicit return types.
|
||||
- Async behavior is typed directly in signatures such as `chooseMove(...): Promise<AIMove>`.
|
||||
|
||||
## Comments & Documentation
|
||||
|
||||
- **JSDoc** comments on key exported functions (`findCaptures`, `applyMove`)
|
||||
- Section separators using dashed lines:
|
||||
```ts
|
||||
// ---------------------------------------------------------------------------
|
||||
// Deck
|
||||
// ---------------------------------------------------------------------------
|
||||
```
|
||||
- `[trueref]` annotations documenting Phaser API provenance
|
||||
- Inline comments for non-obvious logic (capture rules, AI heuristic weights)
|
||||
- No auto-generated docs or separate documentation tooling
|
||||
|
||||
## Code Examples (from codebase)
|
||||
|
||||
### Immutable state update pattern
|
||||
- Section banners are used heavily in larger files:
|
||||
|
||||
```ts
|
||||
export function applyMove(
|
||||
// ---------------------------------------------------------------------------
|
||||
// Turn management
|
||||
// ---------------------------------------------------------------------------
|
||||
```
|
||||
|
||||
- JSDoc appears on rule-heavy or non-obvious APIs, for example `findCaptures()`, `applyMove()`, and `CardTracker` methods.
|
||||
- Scene files include `[trueref]` provenance notes near Phaser-specific APIs.
|
||||
- Inline comments explain rule edge cases, heuristics, and visual-effect intent rather than restating syntax.
|
||||
|
||||
## Code Examples
|
||||
|
||||
### Guarded DOM capability checks
|
||||
|
||||
```ts
|
||||
const installFullscreenRequest = (host: HTMLElement): void => {
|
||||
const canRequestFullscreen =
|
||||
typeof document.fullscreenEnabled === 'boolean'
|
||||
? document.fullscreenEnabled
|
||||
: typeof host.requestFullscreen === 'function';
|
||||
|
||||
if (!canRequestFullscreen || typeof host.requestFullscreen !== 'function') {
|
||||
return;
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### Rule-first capture selection
|
||||
|
||||
```ts
|
||||
export function findCaptures(played: Card, table: Card[]): Card[][] {
|
||||
const directMatches = table.filter(c => c.value === played.value);
|
||||
if (directMatches.length > 0) {
|
||||
return directMatches.map((directMatch): Card[] => [directMatch]);
|
||||
}
|
||||
|
||||
const results: Card[][] = [];
|
||||
const subsets = getSubsets(table);
|
||||
for (const subset of subsets) {
|
||||
if (subset.length >= 2) {
|
||||
const sum = subset.reduce((acc, c) => acc + c.value, 0);
|
||||
if (sum === played.value) {
|
||||
results.push(subset);
|
||||
}
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
```
|
||||
|
||||
### Async AI entry point with progress reporting
|
||||
|
||||
```ts
|
||||
export async function chooseMove(
|
||||
state: GameState,
|
||||
playerIdx: PlayerIndex,
|
||||
card: Card,
|
||||
captureChoice?: Card[]
|
||||
): { nextState: GameState; capture: Capture | null; isScopa: boolean } {
|
||||
const state2 = deepClone(state);
|
||||
// ... mutations on state2 ...
|
||||
return { nextState: state2, capture: ..., isScopa };
|
||||
}
|
||||
```
|
||||
|
||||
### AI heuristic weighted scoring
|
||||
|
||||
```ts
|
||||
function scoreCapture(...): number {
|
||||
let score = 100; // base for capturing anything
|
||||
if (isScopa) score += 500;
|
||||
if (settebello) score += 300;
|
||||
score += denariCount * 50;
|
||||
score += captured.length * 20;
|
||||
difficulty: Difficulty = 'advanced',
|
||||
tracker?: CardTracker,
|
||||
onProgress?: (progress: AIDecisionProgress) => void,
|
||||
): Promise<AIMove> {
|
||||
const startedAt = Date.now();
|
||||
const profile = getSearchProfile(state, difficulty);
|
||||
reportDecisionProgress(onProgress, difficulty, startedAt, profile.timeBudgetMs, 0, 0);
|
||||
// ...
|
||||
return score;
|
||||
}
|
||||
```
|
||||
|
||||
### Phaser particle effect
|
||||
|
||||
```ts
|
||||
const e1 = this.add.particles(x, y, 'particle_glow', {
|
||||
lifespan: { min: 350, max: 700 },
|
||||
speed: { min: 80, max: 280 },
|
||||
scale: { start: 0.9, end: 0 },
|
||||
tint: color, gravityY: 100, emitting: false,
|
||||
}).setDepth(25);
|
||||
e1.explode(count);
|
||||
```
|
||||
|
||||
## Linting & Formatting
|
||||
|
||||
No ESLint or Prettier configuration detected. Code style is maintained manually.
|
||||
TypeScript strict mode provides type-level linting (`strict: true` in `tsconfig.json`).
|
||||
No ESLint or Prettier configuration is present.
|
||||
|
||||
Formatting is maintained manually, with TypeScript compiler strictness acting as the main automated correctness gate. The only repository-wide verification command currently supplied is `npx tsc --noEmit`.
|
||||
|
||||
188
docs/FINDINGS.md
188
docs/FINDINGS.md
@@ -1,28 +1,54 @@
|
||||
# Findings
|
||||
|
||||
> Last Updated: 2026-03-31T00:00:00.000Z
|
||||
> Last Updated: 2026-04-02T18:12:18.000Z
|
||||
|
||||
## Summary
|
||||
|
||||
Initial analysis of the Scopone Scientifico Phaser 3 codebase. This document is populated by the Planner agent as research is performed.
|
||||
Initializer refresh for the current Scopone Scientifico codebase. The existing findings were stale relative to the latest AI and tracker implementation, so the observations below reflect the current source tree.
|
||||
|
||||
## 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
|
||||
- Primary gameplay code lives in 8 TypeScript source files under `src/`; the Android wrapper adds 3 Java files.
|
||||
- The largest modules are `src/scenes/GameScene.ts` (1446 lines) and `src/game/ai.ts` (1210 lines).
|
||||
- `src/game/` remains framework-independent and contains the rules engine, score calculation, card tracker, and AI logic.
|
||||
- The AI now has three distinct difficulty levels: `beginner`, `advanced`, and `master`.
|
||||
- The `advanced` and `master` tiers use `CardTracker` to reason about unseen cards instead of reading hidden hands directly.
|
||||
- The `master` tier performs determinization plus alpha-beta search and reports progress back to the scene.
|
||||
- `GameScene` displays AI progress through a top think bar and updates it from the `AIDecisionProgress` callback.
|
||||
- Audio remains fully procedural via Web Audio; no audio asset pipeline is present.
|
||||
- No ESLint or Prettier config is present.
|
||||
- The only repository-wide verification command supplied is `npx tsc --noEmit`.
|
||||
|
||||
## 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.
|
||||
- `GameScene.ts` still centralizes scene layout, input, effects, audio, HUD, and round transitions in one file, which raises maintenance cost.
|
||||
- `ai.ts` mixes heuristic tiers, inference helpers, determinization, and alpha-beta evaluation in one module.
|
||||
- The `master` profile allows up to 9800 ms of search budget, which may be expensive on slower devices even with batch yielding.
|
||||
- There is still no dedicated automated test suite for rules or AI behavior beyond type-checking.
|
||||
- Formatting rules are enforced socially rather than by a linter/formatter toolchain.
|
||||
|
||||
## Current Rule / Implementation Notes
|
||||
|
||||
### Capture behavior in `engine.ts`
|
||||
|
||||
- Direct-match capture has priority over subset-sum capture.
|
||||
- When multiple direct matches exist, `findCaptures()` returns one single-card option per matching card.
|
||||
- Subset-sum captures are explored only when no direct match exists.
|
||||
- `applyMove()` defaults to the first legal capture if no explicit capture choice is supplied.
|
||||
|
||||
### AI implementation snapshot
|
||||
|
||||
- `beginner` adds randomness around a basic heuristic to remain beatable.
|
||||
- `advanced` adds race awareness, anti-scopa logic, partner setup, anchor play, and tracker-based probability estimates.
|
||||
- `master` orders legal moves with a quick evaluator, samples hidden hands, then scores moves with alpha-beta search under a deadline.
|
||||
- `masterMove()` yields back to the browser between batches so Phaser can repaint the progress UI.
|
||||
|
||||
### Scene / UI implementation snapshot
|
||||
|
||||
- `MenuScene` exposes difficulty selection before match start.
|
||||
- `GameScene` records every played card and captured table card in `CardTracker`.
|
||||
- The HUD continuously displays cards, denari, settebello, primiera, scope, and total points for both teams.
|
||||
- Round-end and game-over flows are managed in-scene rather than through separate overlay components.
|
||||
|
||||
## Research Performed
|
||||
|
||||
@@ -31,112 +57,42 @@ Initial analysis of the Scopone Scientifico Phaser 3 codebase. This document is
|
||||
**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
|
||||
- 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 are dealt at the start of the round; the table begins empty.
|
||||
- Turns advance in the implementation as `0 -> 1 -> 2 -> 3`.
|
||||
|
||||
#### 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)
|
||||
1. If the played card matches one or more table cards by value, a direct match must be taken.
|
||||
2. If multiple direct matches exist, one matching table card is chosen.
|
||||
3. If no direct match exists, a subset of table cards may be captured when their values sum to the played value.
|
||||
4. A direct match has priority over any possible sum capture.
|
||||
5. Scopa awards a point only when the table is cleared before the final play 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. |
|
||||
#### Scoring
|
||||
| Category | Rule |
|
||||
|----------|------|
|
||||
| Carte | Majority of captured cards |
|
||||
| Denari | Majority of `denara` suit cards |
|
||||
| Settebello | Team that captures the 7 of `denara` |
|
||||
| Primiera | Highest best-of-each-suit prime value |
|
||||
| Scope | One point per scopa |
|
||||
|
||||
#### Primiera Values (confirmed matching codebase)
|
||||
#### Primiera values used in code
|
||||
| Card value | Primiera value |
|
||||
|------------|---------------|
|
||||
| 7 | 21 |
|
||||
| 6 | 18 |
|
||||
| 1 (Ace) | 16 |
|
||||
| 5 | 15 |
|
||||
| 4 | 14 |
|
||||
| 3 | 13 |
|
||||
| 2 | 12 |
|
||||
| 8,9,10 | 10 |
|
||||
|------------|----------------|
|
||||
| 7 | 21 |
|
||||
| 6 | 18 |
|
||||
| 1 | 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).
|
||||
### SCOPONE-0008: AI progress rendering notes (2026-04-02)
|
||||
|
||||
#### 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 (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
|
||||
- The current implementation does not use Phaser `TimerEvent` progress helpers.
|
||||
- Instead, `chooseMove()` emits its own normalized progress payload through `AIDecisionProgress`.
|
||||
- `GameScene.updateThinkBar()` renders remaining time from that callback.
|
||||
- The yielding behavior in `masterMove()` is necessary so the browser can repaint while search batches continue.
|
||||
|
||||
Reference in New Issue
Block a user