feat(SCOPONE-0013): PIMC AI rewrite + Gitea Android CI pipeline
Some checks failed
Android Build & Publish / android (push) Failing after 2m10s

- Replace minimax with PIMC (Perfect Information Monte Carlo) search
- Add PIMC_SCOPE_BOOST=150 → effective scopa value 540 (was 390)
  → Master win rate: 67.5% → 72.5% vs legacy AI (target ≥60%)
  → Advanced win rate: 97.5% vs beginner AI (target ≥55%)
  → Scope gap in losses: 6.54 → 3.00 scopa/match
- Add card inference engine for probabilistic hand tracking
- Add ai-strategy, ai-legacy evaluation bridge
- Add .gitea/workflows/android-build.yml: build debug + unsigned
  release APK and publish to Gitea generic package registry
This commit is contained in:
Giancarmine Salucci
2026-05-24 16:29:04 +02:00
parent 17f371d5ee
commit 3f74c57665
14 changed files with 6412 additions and 3938 deletions

View File

@@ -7,6 +7,7 @@ import {
import { AIMove, AIDecisionProgress } from '../game/ai';
import { AIWorkerClient, AIWorkerClientLike } from '../game/ai-worker-client';
import { CardTracker } from '../game/card-tracker';
import { CardInferenceEngine } from '../game/card-inference';
import {
DEFAULT_AUDIO_PREFERENCES,
GameSceneData,
@@ -71,6 +72,7 @@ export class GameScene extends Phaser.Scene {
// Difficulty & card tracker
private difficulty: Difficulty = 'advanced';
private tracker: CardTracker = new CardTracker();
private inference: CardInferenceEngine = new CardInferenceEngine(this.tracker);
private aiClient: AIWorkerClientLike | null = null;
// Active player highlight
@@ -133,6 +135,7 @@ export class GameScene extends Phaser.Scene {
? normalizeAudioPreferences(data.audioPreferences)
: loadAudioPreferences();
this.tracker = new CardTracker();
this.inference = new CardInferenceEngine(this.tracker);
this.aiClient?.dispose();
this.aiClient = new AIWorkerClient();
this.events.once(Phaser.Scenes.Events.SHUTDOWN, this.handleSceneShutdown, this);
@@ -793,7 +796,8 @@ export class GameScene extends Phaser.Scene {
this.updateThinkBar(playerIdx, progress);
if (progress.difficulty !== 'master') return;
finalProgress = progress;
}
},
{ inference: this.inference },
);
const remainingThinkMs = AI_MIN_THINK_MS - (Date.now() - thinkStartedAt);
@@ -1018,6 +1022,7 @@ export class GameScene extends Phaser.Scene {
// ---------------------------------------------------------------------------
private executeMove(playerIdx: PlayerIndex, card: Card, capture: Card[]): void {
const tableBeforeMove = [...this.state.table];
const { nextState, capture: captureResult, isScopa } = applyMove(
this.state, playerIdx, card, capture.length > 0 ? capture : undefined
);
@@ -1030,6 +1035,13 @@ export class GameScene extends Phaser.Scene {
this.tracker.trackCapture(captureResult.captured);
}
// Update inference engine
this.inference.onMove(
playerIdx,
{ card, capture: captureResult?.captured ?? [] },
tableBeforeMove,
);
const cardImg = this.cardImages.get(card.id)!;
cardImg.setDepth(15);
@@ -1650,6 +1662,7 @@ export class GameScene extends Phaser.Scene {
for (const img of this.cardImages.values()) img.destroy();
this.cardImages.clear();
this.tracker.reset();
this.inference.reset();
this.state = createInitialState(nextDealer);
this.state.matchStartingPlayer = matchStartingPlayer;
this.state.teamScores[0].totalPoints = totals[0];