feat(SCOPONE-0008): complete iteration 0 improve ai rules

This commit is contained in:
Giancarmine Salucci
2026-04-02 20:10:55 +02:00
parent e4edc4d660
commit 747da35190
6 changed files with 397 additions and 122 deletions

View File

@@ -1,9 +1,9 @@
import Phaser from 'phaser';
import { Card, PlayerIndex, GameState, Difficulty } from '../game/types';
import {
createInitialState, applyMove, findCaptures, getScoreBreakdown, teamOf, calcPrimiera
createInitialState, applyMove, findCaptures, getScoreBreakdown, teamOf, calcPrimiera, getMatchOutcome
} from '../game/engine';
import { chooseMove } from '../game/ai';
import { chooseMove, AIDecisionProgress } from '../game/ai';
import { CardTracker } from '../game/card-tracker';
// ---------------------------------------------------------------------------
@@ -25,8 +25,6 @@ const CH_H = 645 * CARD_SCALE_HUMAN; // card height for human ≈ 106
const CW_A = 402 * CARD_SCALE_AI; // card width for AI ≈ 50
const CH_A = 645 * CARD_SCALE_AI; // card height for AI ≈ 81
const AI_DELAY = 1100; // ms — think bar fills over this time
// Scorebar height at top
const SCOREBAR_H = 54;
@@ -76,8 +74,6 @@ export class GameScene extends Phaser.Scene {
// Think bar
private thinkBar!: Phaser.GameObjects.Graphics;
private thinkTween: Phaser.Tweens.Tween | null = null;
private thinkProgress = 0;
// Player label containers (pulsed on active turn)
private playerLabels: Map<PlayerIndex, Phaser.GameObjects.Text> = new Map();
@@ -128,7 +124,8 @@ export class GameScene extends Phaser.Scene {
this.input.once('pointerdown', () => this.startMusic());
this.state = createInitialState();
const startingPlayer = Phaser.Math.Between(0, 3) as PlayerIndex;
this.state = createInitialState(startingPlayer);
this.dealAnimation(() => {
this.updateScoreBar();
this.nextTurn();
@@ -374,39 +371,34 @@ export class GameScene extends Phaser.Scene {
this.thinkBar = this.add.graphics().setDepth(11).setVisible(false);
}
private showThinkBar(playerIdx: PlayerIndex): void {
this.thinkProgress = 0;
private showThinkBar(playerIdx: PlayerIndex, remainingRatio = 1): void {
this.thinkBar.setVisible(true);
this.thinkTween?.stop();
this.drawThinkBar(playerIdx, remainingRatio);
}
private updateThinkBar(playerIdx: PlayerIndex, progress: AIDecisionProgress): void {
this.drawThinkBar(playerIdx, 1 - progress.progress);
}
private drawThinkBar(playerIdx: PlayerIndex, remainingRatio: number): void {
const W = this.scale.width;
const tg = this.thinkBar;
const color = (playerIdx === 0 || playerIdx === 2) ? 0x44ff88 : 0xff5555;
const clampedRatio = Phaser.Math.Clamp(remainingRatio, 0, 1);
const width = clampedRatio * W;
const tweenTarget = { v: 0 };
this.thinkTween = this.tweens.add({
targets: tweenTarget,
v: 1,
duration: AI_DELAY - 80,
ease: 'Linear',
onUpdate: () => {
tg.clear();
const w = tweenTarget.v * W;
tg.fillStyle(0x000000, 0.4);
tg.fillRect(0, SCOREBAR_H, W, 4);
tg.fillStyle(color, 0.85);
tg.fillRect(0, SCOREBAR_H, w, 4);
// Glow tip
tg.fillStyle(0xffffff, 0.6);
tg.fillRect(w - 6, SCOREBAR_H, 6, 4);
},
onComplete: () => { tg.clear(); tg.setVisible(false); },
});
tg.clear();
tg.fillStyle(0x000000, 0.4);
tg.fillRect(0, SCOREBAR_H, W, 4);
if (width <= 0) return;
tg.fillStyle(color, 0.85);
tg.fillRect(0, SCOREBAR_H, width, 4);
tg.fillStyle(0xffffff, 0.6);
tg.fillRect(Math.max(0, width - 6), SCOREBAR_H, Math.min(6, width), 4);
}
private hideThinkBar(): void {
this.thinkTween?.stop();
this.thinkTween = null;
this.thinkBar.clear();
this.thinkBar.setVisible(false);
}
@@ -602,21 +594,47 @@ export class GameScene extends Phaser.Scene {
this.pulseLabel(cur);
if (player.isHuman) {
this.hideThinkBar();
this.enableHumanInteraction();
} else {
this.aiThinking = true;
this.showThinkBar(cur);
this.time.delayedCall(AI_DELAY, () => {
this.hideThinkBar();
this.doAIMove(cur);
});
this.showThinkBar(cur, 1);
void this.doAIMove(cur);
}
}
private doAIMove(playerIdx: PlayerIndex): void {
const move = chooseMove(this.state, playerIdx, this.difficulty, this.tracker);
this.aiThinking = false;
this.executeMove(playerIdx, move.card, move.capture);
private async doAIMove(playerIdx: PlayerIndex): Promise<void> {
const turnState = this.state;
try {
const move = await chooseMove(
this.state,
playerIdx,
this.difficulty,
this.tracker,
(progress) => {
if (!this.scene.isActive('GameScene') || this.state !== turnState) return;
this.updateThinkBar(playerIdx, progress);
}
);
if (!this.scene.isActive('GameScene')) return;
if (this.state !== turnState || this.state.currentPlayer !== playerIdx || this.state.roundOver) return;
this.hideThinkBar();
this.aiThinking = false;
this.executeMove(playerIdx, move.card, move.capture);
} catch (error) {
console.error('AI move failed', error);
if (this.scene.isActive('GameScene') && this.state === turnState) {
this.setStatus('Errore durante la mossa AI');
}
} finally {
if (this.scene.isActive('GameScene') && this.state === turnState) {
this.hideThinkBar();
this.aiThinking = false;
}
}
}
// ---------------------------------------------------------------------------
@@ -1316,7 +1334,8 @@ export class GameScene extends Phaser.Scene {
}).setOrigin(0.5).setDepth(32);
});
const gameOver = t0.totalPoints >= 11 || t1.totalPoints >= 11;
const outcome = getMatchOutcome(this.state.teamScores);
const gameOver = !outcome.continueMatch;
const btnLabel = gameOver ? 'Fine Partita' : 'Prossima Mano';
const btnG = this.add.graphics().setDepth(32);
@@ -1344,7 +1363,8 @@ export class GameScene extends Phaser.Scene {
const H = this.scale.height;
const t0 = this.state.teamScores[0];
const t1 = this.state.teamScores[1];
const win = t0.totalPoints >= t1.totalPoints;
const outcome = getMatchOutcome(this.state.teamScores);
const win = outcome.winner === 0;
this.stopMusic();
// Victory confetti
@@ -1392,11 +1412,13 @@ export class GameScene extends Phaser.Scene {
private startNewRound(): void {
const totals = this.state.teamScores.map(t => t.totalPoints);
const nextRound = (this.state.roundNumber ?? 1) + 1;
const startingPlayer = ((nextRound - 1) % 4) as PlayerIndex;
const matchStartingPlayer = this.state.matchStartingPlayer;
const startingPlayer = ((matchStartingPlayer + nextRound - 1) % 4) as PlayerIndex;
for (const img of this.cardImages.values()) img.destroy();
this.cardImages.clear();
this.tracker.reset();
this.state = createInitialState(startingPlayer);
this.state.matchStartingPlayer = matchStartingPlayer;
this.state.teamScores[0].totalPoints = totals[0];
this.state.teamScores[1].totalPoints = totals[1];
this.state.roundNumber = nextRound;