feat(SCOPONE-0009) improve ai, dealer, apparigliare e sparigliare
This commit is contained in:
@@ -4,7 +4,7 @@ import {
|
||||
createInitialState, applyMove, findCaptures, getScoreBreakdown, teamOf, calcPrimiera, getMatchOutcome,
|
||||
nextPlayer
|
||||
} from '../game/engine';
|
||||
import { AIDecisionProgress } from '../game/ai';
|
||||
import { AIMove, AIDecisionProgress } from '../game/ai';
|
||||
import { AIWorkerClient, AIWorkerClientLike } from '../game/ai-worker-client';
|
||||
import { CardTracker } from '../game/card-tracker';
|
||||
|
||||
@@ -29,6 +29,8 @@ const CH_A = 645 * CARD_SCALE_AI; // card height for AI ≈ 81
|
||||
|
||||
// Scorebar height at top
|
||||
const SCOREBAR_H = 54;
|
||||
const AI_MIN_THINK_MS = 1000;
|
||||
const MOVE_OUTCOME_STATUS_MS = 2000;
|
||||
|
||||
// Player positions:
|
||||
// 0 = South (human, bottom), 1 = West (AI, left, rotated -90°)
|
||||
@@ -74,6 +76,8 @@ export class GameScene extends Phaser.Scene {
|
||||
|
||||
// Status bar
|
||||
private statusText!: Phaser.GameObjects.Text;
|
||||
private statusTimer: Phaser.Time.TimerEvent | null = null;
|
||||
private persistentStatusText = '';
|
||||
|
||||
// Think bar
|
||||
private thinkBar!: Phaser.GameObjects.Graphics;
|
||||
@@ -387,6 +391,38 @@ export class GameScene extends Phaser.Scene {
|
||||
this.drawThinkBar(playerIdx, 1 - progress.progress);
|
||||
}
|
||||
|
||||
private logMasterAIDiagnostics(
|
||||
playerIdx: PlayerIndex,
|
||||
move: AIMove,
|
||||
progress: AIDecisionProgress | null,
|
||||
): void {
|
||||
if (this.difficulty !== 'master' || !progress) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.info('[AI master diagnostics]', {
|
||||
playerIdx,
|
||||
roundNumber: this.state.roundNumber ?? 1,
|
||||
moveCardId: move.card.id,
|
||||
captureCount: move.capture.length,
|
||||
timing: {
|
||||
elapsedMs: progress.elapsedMs,
|
||||
budgetMs: progress.budgetMs,
|
||||
progress: progress.progress,
|
||||
timedOut: progress.timedOut ?? false,
|
||||
},
|
||||
search: {
|
||||
batchesCompleted: progress.batchesCompleted,
|
||||
cardsRemaining: progress.cardsRemaining ?? null,
|
||||
sampleCount: progress.sampleCount ?? null,
|
||||
maxDepth: progress.maxDepth ?? null,
|
||||
completedDepth: progress.completedDepth ?? null,
|
||||
rootMoveCount: progress.rootMoveCount ?? null,
|
||||
aspirationExpansions: progress.aspirationExpansions ?? 0,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private drawThinkBar(playerIdx: PlayerIndex, remainingRatio: number): void {
|
||||
const W = this.scale.width;
|
||||
const tg = this.thinkBar;
|
||||
@@ -410,6 +446,35 @@ export class GameScene extends Phaser.Scene {
|
||||
this.thinkBar.setVisible(false);
|
||||
}
|
||||
|
||||
private clearStatusTimer(): void {
|
||||
if (!this.statusTimer) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.statusTimer.remove(false);
|
||||
this.statusTimer.destroy();
|
||||
this.statusTimer = null;
|
||||
}
|
||||
|
||||
private waitForDelay(delayMs: number): Promise<void> {
|
||||
if (delayMs <= 0 || !this.scene.isActive('GameScene')) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
this.time.delayedCall(delayMs, () => resolve());
|
||||
});
|
||||
}
|
||||
|
||||
private buildMoveOutcomeStatus(playerIdx: PlayerIndex, card: Card, capture: Card[] | null): string {
|
||||
const player = this.state.players[playerIdx];
|
||||
if (!capture || capture.length === 0) {
|
||||
return `${player.name} gioca ${cardName(card)}`;
|
||||
}
|
||||
|
||||
return `${player.name} cattura ${capture.map(cardName).join(', ')} con ${cardName(card)}`;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Player labels (pulse on active turn)
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -597,7 +662,7 @@ export class GameScene extends Phaser.Scene {
|
||||
if (this.state.roundOver) { this.showRoundEnd(); return; }
|
||||
const cur = this.state.currentPlayer;
|
||||
const player = this.state.players[cur];
|
||||
this.setStatus(`Turno di ${player.name}`);
|
||||
this.setStatus(`Turno di ${player.name}`, { persist: true });
|
||||
this.pulseLabel(cur);
|
||||
|
||||
if (player.isHuman) {
|
||||
@@ -611,6 +676,7 @@ export class GameScene extends Phaser.Scene {
|
||||
}
|
||||
|
||||
private handleSceneShutdown(): void {
|
||||
this.clearStatusTimer();
|
||||
this.aiClient?.dispose();
|
||||
this.aiClient = null;
|
||||
this.aiThinking = false;
|
||||
@@ -622,6 +688,8 @@ export class GameScene extends Phaser.Scene {
|
||||
private async doAIMove(playerIdx: PlayerIndex): Promise<void> {
|
||||
const turnState = this.state;
|
||||
const aiClient = this.aiClient;
|
||||
let finalProgress: AIDecisionProgress | null = null;
|
||||
const thinkStartedAt = Date.now();
|
||||
|
||||
if (!aiClient) {
|
||||
return;
|
||||
@@ -636,13 +704,21 @@ export class GameScene extends Phaser.Scene {
|
||||
(progress) => {
|
||||
if (this.aiClient !== aiClient || !this.scene.isActive('GameScene') || this.state !== turnState) return;
|
||||
this.updateThinkBar(playerIdx, progress);
|
||||
if (progress.difficulty !== 'master') return;
|
||||
finalProgress = progress;
|
||||
}
|
||||
);
|
||||
|
||||
const remainingThinkMs = AI_MIN_THINK_MS - (Date.now() - thinkStartedAt);
|
||||
if (remainingThinkMs > 0) {
|
||||
await this.waitForDelay(remainingThinkMs);
|
||||
}
|
||||
|
||||
if (this.aiClient !== aiClient) return;
|
||||
if (!this.scene.isActive('GameScene')) return;
|
||||
if (this.state !== turnState || this.state.currentPlayer !== playerIdx || this.state.roundOver) return;
|
||||
|
||||
this.logMasterAIDiagnostics(playerIdx, move, finalProgress);
|
||||
this.hideThinkBar();
|
||||
this.aiThinking = false;
|
||||
this.executeMove(playerIdx, move.card, move.capture);
|
||||
@@ -923,10 +999,10 @@ export class GameScene extends Phaser.Scene {
|
||||
if (isScopa) {
|
||||
this.playSfx('scopa');
|
||||
this.doScopaEffect(playerIdx, () =>
|
||||
this.afterMove(nextState, oldState)
|
||||
this.afterMove(playerIdx, card, captureResult?.captured ?? null, nextState, oldState)
|
||||
);
|
||||
} else {
|
||||
this.afterMove(nextState, oldState);
|
||||
this.afterMove(playerIdx, card, captureResult?.captured ?? null, nextState, oldState);
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -947,20 +1023,39 @@ export class GameScene extends Phaser.Scene {
|
||||
targets: cardImg,
|
||||
x: tablePos.x, y: tablePos.y, angle: randomAngle,
|
||||
duration: 280, ease: 'Back.Out',
|
||||
onComplete: () => this.afterMove(nextState, oldState),
|
||||
onComplete: () => this.afterMove(playerIdx, card, null, nextState, oldState),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private afterMove(nextState: GameState, _old: GameState): void {
|
||||
private afterMove(
|
||||
playerIdx: PlayerIndex,
|
||||
card: Card,
|
||||
capture: Card[] | null,
|
||||
nextState: GameState,
|
||||
_old: GameState,
|
||||
): void {
|
||||
this.updateScoreBar();
|
||||
this.relayoutTable();
|
||||
if (nextState.roundOver) {
|
||||
this.time.delayedCall(500, () => this.showRoundEnd());
|
||||
} else {
|
||||
if (!nextState.roundOver) {
|
||||
this.relayoutHand(0);
|
||||
this.nextTurn();
|
||||
}
|
||||
|
||||
this.setStatus(this.buildMoveOutcomeStatus(playerIdx, card, capture), {
|
||||
durationMs: MOVE_OUTCOME_STATUS_MS,
|
||||
onExpire: () => {
|
||||
if (!this.scene.isActive('GameScene')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (nextState.roundOver) {
|
||||
this.showRoundEnd();
|
||||
return;
|
||||
}
|
||||
|
||||
this.nextTurn();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private relayoutHand(playerIdx: PlayerIndex): void {
|
||||
@@ -1450,7 +1545,32 @@ export class GameScene extends Phaser.Scene {
|
||||
});
|
||||
}
|
||||
|
||||
private setStatus(msg: string): void { this.statusText.setText(msg); }
|
||||
private setStatus(
|
||||
msg: string,
|
||||
options: {
|
||||
persist?: boolean;
|
||||
durationMs?: number;
|
||||
onExpire?: () => void;
|
||||
} = {},
|
||||
): void {
|
||||
const { persist = false, durationMs, onExpire } = options;
|
||||
|
||||
this.clearStatusTimer();
|
||||
this.statusText.setText(msg);
|
||||
|
||||
if (persist) {
|
||||
this.persistentStatusText = msg;
|
||||
}
|
||||
|
||||
if (durationMs === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.statusTimer = this.time.delayedCall(durationMs, () => {
|
||||
this.statusTimer = null;
|
||||
onExpire?.();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user