feat(SCOPONE-0005): complete iteration 0 — AI mastery levels, score bar fix, difficulty selection
This commit is contained in:
@@ -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];
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import Phaser from 'phaser';
|
||||
import { Difficulty } from '../game/types';
|
||||
|
||||
export class MenuScene extends Phaser.Scene {
|
||||
constructor() {
|
||||
@@ -13,7 +14,7 @@ export class MenuScene extends Phaser.Scene {
|
||||
this.add.rectangle(0, 0, W, H, 0x1a5c2a).setOrigin(0);
|
||||
|
||||
// Title
|
||||
this.add.text(W / 2, H * 0.2, 'Scopone Scientifico', {
|
||||
this.add.text(W / 2, H * 0.18, 'Scopone Scientifico', {
|
||||
fontFamily: 'Georgia, serif',
|
||||
fontSize: '52px',
|
||||
color: '#ffd700',
|
||||
@@ -22,7 +23,7 @@ export class MenuScene extends Phaser.Scene {
|
||||
resolution: 2,
|
||||
}).setOrigin(0.5);
|
||||
|
||||
this.add.text(W / 2, H * 0.32, '2 vs 2 · Tu + Compagno vs 2 AI', {
|
||||
this.add.text(W / 2, H * 0.30, '2 vs 2 · Tu + Compagno vs 2 AI', {
|
||||
fontFamily: 'serif',
|
||||
fontSize: '22px',
|
||||
color: '#ccffcc',
|
||||
@@ -37,36 +38,63 @@ export class MenuScene extends Phaser.Scene {
|
||||
'Prima squadra a 11 punti vince',
|
||||
];
|
||||
rules.forEach((line, i) => {
|
||||
this.add.text(W / 2, H * 0.44 + i * 28, line, {
|
||||
this.add.text(W / 2, H * 0.40 + i * 26, line, {
|
||||
fontFamily: 'serif',
|
||||
fontSize: '18px',
|
||||
fontSize: '17px',
|
||||
color: '#ffffff',
|
||||
resolution: 2,
|
||||
}).setOrigin(0.5);
|
||||
});
|
||||
|
||||
// Start button
|
||||
const btn = this.add.rectangle(W / 2, H * 0.72, 220, 60, 0xffd700, 1)
|
||||
.setInteractive({ useHandCursor: true });
|
||||
const btnText = this.add.text(W / 2, H * 0.72, 'INIZIA PARTITA', {
|
||||
// Difficulty selection label
|
||||
this.add.text(W / 2, H * 0.60, 'Scegli la difficoltà:', {
|
||||
fontFamily: 'Georgia, serif',
|
||||
fontSize: '22px',
|
||||
color: '#1a5c2a',
|
||||
fontSize: '20px',
|
||||
color: '#ffd700',
|
||||
resolution: 2,
|
||||
}).setOrigin(0.5);
|
||||
|
||||
btn.on('pointerover', () => btn.setFillStyle(0xffec6e));
|
||||
btn.on('pointerout', () => btn.setFillStyle(0xffd700));
|
||||
btn.on('pointerdown', () => {
|
||||
this.cameras.main.fadeOut(300, 0, 30, 0);
|
||||
this.cameras.main.once('camerafadeoutcomplete', () => {
|
||||
this.scene.start('GameScene');
|
||||
// Difficulty buttons
|
||||
const difficulties: Array<{ label: string; value: Difficulty; color: number; hoverColor: number }> = [
|
||||
{ label: 'Principiante', value: 'beginner', color: 0x4caf50, hoverColor: 0x66bb6a },
|
||||
{ label: 'Avanzato', value: 'advanced', color: 0xff9800, hoverColor: 0xffb74d },
|
||||
{ label: 'Maestro', value: 'master', color: 0xf44336, hoverColor: 0xef5350 },
|
||||
];
|
||||
|
||||
const btnWidth = 200;
|
||||
const btnHeight = 50;
|
||||
const totalWidth = difficulties.length * btnWidth + (difficulties.length - 1) * 20;
|
||||
const startX = (W - totalWidth) / 2 + btnWidth / 2;
|
||||
|
||||
difficulties.forEach((d, i) => {
|
||||
const x = startX + i * (btnWidth + 20);
|
||||
const y = H * 0.70;
|
||||
|
||||
const btn = this.add.rectangle(x, y, btnWidth, btnHeight, d.color, 1)
|
||||
.setInteractive({ useHandCursor: true });
|
||||
|
||||
this.add.text(x, y, d.label, {
|
||||
fontFamily: 'Georgia, serif',
|
||||
fontSize: '20px',
|
||||
color: '#ffffff',
|
||||
stroke: '#000000',
|
||||
strokeThickness: 2,
|
||||
resolution: 2,
|
||||
}).setOrigin(0.5);
|
||||
|
||||
btn.on('pointerover', () => btn.setFillStyle(d.hoverColor));
|
||||
btn.on('pointerout', () => btn.setFillStyle(d.color));
|
||||
btn.on('pointerdown', () => {
|
||||
this.cameras.main.fadeOut(300, 0, 30, 0);
|
||||
this.cameras.main.once('camerafadeoutcomplete', () => {
|
||||
this.scene.start('GameScene', { difficulty: d.value });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Show some face-down cards decoratively
|
||||
const positions = [
|
||||
[W * 0.1, H * 0.5], [W * 0.15, H * 0.52], [W * 0.9, H * 0.5], [W * 0.85, H * 0.52],
|
||||
[W * 0.08, H * 0.85], [W * 0.14, H * 0.87], [W * 0.92, H * 0.85], [W * 0.86, H * 0.87],
|
||||
];
|
||||
for (const [x, y] of positions) {
|
||||
this.add.image(x, y, 'retro').setScale(0.08).setAngle(Phaser.Math.Between(-15, 15));
|
||||
|
||||
Reference in New Issue
Block a user