feat(SCOPONE-0005): complete iteration 0 — AI mastery levels, score bar fix, difficulty selection

This commit is contained in:
Giancarmine Salucci
2026-03-31 22:22:24 +02:00
parent 6c01044c71
commit 0a030d0f01
5 changed files with 580 additions and 136 deletions

View File

@@ -1,9 +1,10 @@
import Phaser from 'phaser';
import { Card, PlayerIndex, GameState } from '../game/types';
import { Card, PlayerIndex, GameState, Difficulty } from '../game/types';
import {
createInitialState, applyMove, findCaptures, getScoreBreakdown, teamOf, calcPrimiera
} from '../game/engine';
import { chooseMove } from '../game/ai';
import { CardTracker } from '../game/card-tracker';
// ---------------------------------------------------------------------------
// Layout constants
@@ -46,13 +47,20 @@ export class GameScene extends Phaser.Scene {
private state!: GameState;
private cardImages: Map<string, Phaser.GameObjects.Image> = new Map();
// Difficulty & card tracker
private difficulty: Difficulty = 'advanced';
private tracker: CardTracker = new CardTracker();
// Active player highlight
private activeHighlightRect: Phaser.GameObjects.Graphics | null = null;
// Live score bar texts
private hudA!: { scope: Phaser.GameObjects.Text; cards: Phaser.GameObjects.Text;
denari: Phaser.GameObjects.Text; prim: Phaser.GameObjects.Text;
total: Phaser.GameObjects.Text };
denari: Phaser.GameObjects.Text; sette: Phaser.GameObjects.Text;
prim: Phaser.GameObjects.Text; total: Phaser.GameObjects.Text };
private hudB!: { scope: Phaser.GameObjects.Text; cards: Phaser.GameObjects.Text;
denari: Phaser.GameObjects.Text; prim: Phaser.GameObjects.Text;
total: Phaser.GameObjects.Text };
denari: Phaser.GameObjects.Text; sette: Phaser.GameObjects.Text;
prim: Phaser.GameObjects.Text; total: Phaser.GameObjects.Text };
private roundText!: Phaser.GameObjects.Text;
// Status bar
@@ -90,11 +98,15 @@ export class GameScene extends Phaser.Scene {
// Create
// ---------------------------------------------------------------------------
create(): void {
create(data?: { difficulty?: Difficulty }): void {
const W = this.scale.width;
const H = this.scale.height;
this.tableCenter = { x: W / 2, y: (H + SCOREBAR_H) / 2 + 10 };
// Read difficulty from scene data (MenuScene passes it)
this.difficulty = data?.difficulty ?? 'advanced';
this.tracker = new CardTracker();
this.generateParticleTextures();
this.drawBackground(W, H);
this.buildScoreBar(W);
@@ -245,12 +257,12 @@ export class GameScene extends Phaser.Scene {
this.roundText = mkTxt(W / 2, SCOREBAR_H / 2, 'Mano 1', '#ffd700', '16px');
// Column headers (shared, centered-ish)
const cols = ['Scope', 'Carte', 'Denari', 'Primiera', 'TOTALE'];
const xA = [240, 320, 410, 510, 620];
const xB = [W - 240, W - 320, W - 410, W - 510, W - 620];
const cols = ['Scope', 'Carte', 'Denari', '7Bello', 'Primiera', 'TOTALE'];
const xA = [230, 295, 370, 445, 520, 610];
const xB = [W - 230, W - 295, W - 370, W - 445, W - 520, W - 610];
cols.forEach((_, i) => {
const label = ['Sc', 'Ca', 'De', 'Pr', 'Pt'][i];
const label = ['Sc', 'Ca', 'De', '7B', 'Pr', 'Pt'][i];
this.add.text(xA[i], SCOREBAR_H * 0.28, label, {
fontFamily: 'monospace', fontSize: '10px', color: '#999999', resolution: 2,
}).setOrigin(0.5).setDepth(9);
@@ -264,15 +276,15 @@ export class GameScene extends Phaser.Scene {
const mkB = (xi: number) => mkTxt(xB[xi], SCOREBAR_H * 0.72, '0', '#ffaaaa', '17px');
this.hudA = {
scope: mkA(0), cards: mkA(1), denari: mkA(2), prim: mkA(3),
total: this.add.text(xA[4], SCOREBAR_H * 0.72, '0', {
scope: mkA(0), cards: mkA(1), denari: mkA(2), sette: mkA(3), prim: mkA(4),
total: this.add.text(xA[5], SCOREBAR_H * 0.72, '0', {
fontFamily: 'Georgia, serif', fontSize: '20px', color: '#aaffaa',
stroke: '#000', strokeThickness: 2, resolution: 2,
}).setOrigin(0.5).setDepth(9),
};
this.hudB = {
scope: mkB(0), cards: mkB(1), denari: mkB(2), prim: mkB(3),
total: this.add.text(xB[4], SCOREBAR_H * 0.72, '0', {
scope: mkB(0), cards: mkB(1), denari: mkB(2), sette: mkB(3), prim: mkB(4),
total: this.add.text(xB[5], SCOREBAR_H * 0.72, '0', {
fontFamily: 'Georgia, serif', fontSize: '20px', color: '#ffaaaa',
stroke: '#000', strokeThickness: 2, resolution: 2,
}).setOrigin(0.5).setDepth(9),
@@ -294,6 +306,8 @@ export class GameScene extends Phaser.Scene {
const den1 = pile1.filter(c => c.suit === 'denara').length;
const prim0 = calcPrimiera(pile0);
const prim1 = calcPrimiera(pile1);
const sette0 = pile0.some(c => c.suit === 'denara' && c.value === 7);
const sette1 = pile1.some(c => c.suit === 'denara' && c.value === 7);
const setAnim = (txt: Phaser.GameObjects.Text, val: string | number) => {
const v = String(val);
@@ -305,12 +319,14 @@ export class GameScene extends Phaser.Scene {
setAnim(this.hudA.scope, scope0);
setAnim(this.hudA.cards, pile0.length);
setAnim(this.hudA.denari, den0);
setAnim(this.hudA.sette, sette0 ? '✓' : '');
setAnim(this.hudA.prim, prim0 > 0 ? prim0 : '-');
setAnim(this.hudA.total, t0.totalPoints);
setAnim(this.hudB.scope, scope1);
setAnim(this.hudB.cards, pile1.length);
setAnim(this.hudB.denari, den1);
setAnim(this.hudB.sette, sette1 ? '✓' : '');
setAnim(this.hudB.prim, prim1 > 0 ? prim1 : '-');
setAnim(this.hudB.total, t1.totalPoints);
@@ -408,12 +424,37 @@ export class GameScene extends Phaser.Scene {
}
private pulseLabel(playerIdx: PlayerIndex): void {
// Reset all
// Reset all labels
for (const [idx, lbl] of this.playerLabels) {
lbl.setAlpha(idx === playerIdx ? 1 : 0.5);
lbl.setAlpha(idx === playerIdx ? 1 : 0.4);
}
// Pulse active
// Remove old highlight
if (this.activeHighlightRect) {
this.activeHighlightRect.destroy();
this.activeHighlightRect = null;
}
// Draw glow rectangle behind active player label
const lbl = this.playerLabels.get(playerIdx)!;
const bounds = lbl.getBounds();
const pad = 6;
const color = teamOf(playerIdx) === 0 ? 0x00ff44 : 0xff4444;
const gfx = this.add.graphics().setDepth(1);
gfx.fillStyle(color, 0.25);
gfx.fillRoundedRect(bounds.x - pad, bounds.y - pad, bounds.width + pad * 2, bounds.height + pad * 2, 6);
gfx.lineStyle(2, color, 0.8);
gfx.strokeRoundedRect(bounds.x - pad, bounds.y - pad, bounds.width + pad * 2, bounds.height + pad * 2, 6);
this.activeHighlightRect = gfx;
// Pulse the glow
this.tweens.add({
targets: gfx,
alpha: { from: 1, to: 0.4 },
duration: 600, yoyo: true, repeat: -1, ease: 'Sine.InOut',
});
// Pulse the label
this.tweens.add({
targets: lbl,
scaleX: 1.2, scaleY: 1.2,
@@ -548,7 +589,7 @@ export class GameScene extends Phaser.Scene {
}
private doAIMove(playerIdx: PlayerIndex): void {
const move = chooseMove(this.state, playerIdx);
const move = chooseMove(this.state, playerIdx, this.difficulty, this.tracker);
this.aiThinking = false;
this.executeMove(playerIdx, move.card, move.capture);
}
@@ -750,6 +791,12 @@ export class GameScene extends Phaser.Scene {
const oldState = this.state;
this.state = nextState;
// Update card tracker
this.tracker.trackPlay(card);
if (captureResult) {
this.tracker.trackCapture(captureResult.captured);
}
const cardImg = this.cardImages.get(card.id)!;
cardImg.setDepth(15);
@@ -1318,6 +1365,7 @@ export class GameScene extends Phaser.Scene {
const startingPlayer = ((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.teamScores[0].totalPoints = totals[0];
this.state.teamScores[1].totalPoints = totals[1];