fix(SCOPONE-0011): complete iteration 0 - tune ai and ui
This commit is contained in:
@@ -143,3 +143,15 @@ Documentation refresh for the current repository state. `docs/ARCHITECTURE.md` a
|
|||||||
- `src/scenes/GameScene.ts` contains the current pacing and status behavior: `AI_MIN_THINK_MS = 1000`, `MOVE_OUTCOME_STATUS_MS = 2000`, timer-backed status updates, and shutdown cleanup.
|
- `src/scenes/GameScene.ts` contains the current pacing and status behavior: `AI_MIN_THINK_MS = 1000`, `MOVE_OUTCOME_STATUS_MS = 2000`, timer-backed status updates, and shutdown cleanup.
|
||||||
- `src/game/ai-benchmark.ts` enforces an iteration 5 contract with simulated timing, cross-seed aggregation, dual-loss reporting, and a regression watchlist intersection.
|
- `src/game/ai-benchmark.ts` enforces an iteration 5 contract with simulated timing, cross-seed aggregation, dual-loss reporting, and a regression watchlist intersection.
|
||||||
- `src/main.ts` imports and registers `SettingsScene` directly.
|
- `src/main.ts` imports and registers `SettingsScene` directly.
|
||||||
|
|
||||||
|
### SCOPONE-0011: AI opening, render config, and HUD layout notes (2026-04-10)
|
||||||
|
|
||||||
|
- Source: Context7 `/websites/phaser_io_api-documentation`, query `Phaser 3.87 game config antialias antialiasGL roundPixels pixelArt text resolution sharp text image smoothing camera round pixels texture filtering`.
|
||||||
|
- Phaser 3.87 render config keeps `antialias` and `antialiasGL` enabled by default, while `pixelArt: true` would forcibly disable antialiasing and enable `roundPixels`; `roundPixels` also snaps texture-based objects to integer positions.
|
||||||
|
- `src/main.ts` currently relies on Phaser defaults and does not declare an explicit `render` block, so card filtering behavior is not pinned in project code.
|
||||||
|
- `src/scenes/MenuScene.ts` and `src/scenes/SettingsScene.ts` already use `resolution: 2` text styles broadly. `src/scenes/GameScene.ts` uses `resolution: 2` in score-bar and some label text, but `buildStatusBar()` and several large overlay texts (`SCOPA!`, `7♦`, match-end copy) omit it.
|
||||||
|
- `src/scenes/GameScene.ts` currently drives card pacing with `PLAYED_CARD_TRAVEL_MS = 400`, `CAPTURE_COLLAPSE_MS = 480`, `NON_CAPTURE_TABLE_TWEEN_MS = 560`, and separate hand/table relayout tweens at `160 ms`.
|
||||||
|
- `src/scenes/GameScene.ts` places player names with `buildPlayerLabels()` at depth `2`; deal/gameplay cards render at depths `5-15`, and status/dialog overlays render at depths `9-50`, so names can be obscured by both cards and bottom-center status UI.
|
||||||
|
- `src/game/ai.ts` already concentrates opening-role and duplicate-rank logic in `scoreDumpAdv()`, `scoreCaptureAdv()`, `summarizeMoveTactics()`, `scoreControlOverrideCandidate()`, and `findStrategicControlOverride()`.
|
||||||
|
- `src/game/ai.ts` calls `findStrategicControlOverride()` in the master path before determinized search, so first-player opening and safe-scopa heuristics must be aligned across both advanced scoring and master root selection.
|
||||||
|
- `src/game/ai-benchmark-fixtures.ts` currently includes 13 fixtures and 6 critical concepts; it has coverage for `full-table-scopa`, `anti-scopa-safe-dump`, and `only-safe-release`, but no fixture isolates the new "safe scopa when there is no repercussion" rule or the new first-hand duplicate-rank opening preference on an empty/unknown table.
|
||||||
|
|||||||
@@ -245,24 +245,26 @@ const RAW_FIXTURES: RawFixture[] = [
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'safe-low-dump',
|
id: 'safe-scopa-conversion',
|
||||||
name: 'Safe Low Dump',
|
name: 'Safe Scopa Conversion',
|
||||||
description: 'The search should prefer the lone safe release over cards that either capture or create leverage for the next player.',
|
description: 'When a clean sweep is available, the search should take the safe scopa instead of settling for a smaller direct capture.',
|
||||||
tags: ['table-control'],
|
tags: ['safe-scopa', 'scopa-window'],
|
||||||
dealer: 2,
|
dealer: 3,
|
||||||
currentPlayer: 0,
|
currentPlayer: 0,
|
||||||
handSizes: [5, 5, 5, 5],
|
handSizes: [5, 5, 5, 5],
|
||||||
hands: [[
|
hands: [[
|
||||||
'coppe_10',
|
'bastoni_10',
|
||||||
'spade_8',
|
'denara_6',
|
||||||
'bastoni_6',
|
'coppe_9',
|
||||||
'denara_4',
|
'bastoni_3',
|
||||||
'coppe_3',
|
'spade_2',
|
||||||
], undefined, undefined, undefined],
|
], undefined, undefined, undefined],
|
||||||
table: ['denara_2', 'spade_9', 'bastoni_4', 'coppe_5'],
|
table: ['coppe_4', 'spade_6'],
|
||||||
piles: PILES_TEMPLATE_A,
|
pileCardCounts: [5, 4, 5, 4],
|
||||||
|
totalPoints: [8, 8],
|
||||||
expectedMove: {
|
expectedMove: {
|
||||||
cardId: 'coppe_3',
|
cardId: 'bastoni_10',
|
||||||
|
captureIds: ['coppe_4', 'spade_6'],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -337,25 +339,25 @@ const RAW_FIXTURES: RawFixture[] = [
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'direct-eight-conversion',
|
id: 'duplicate-rank-opening-release',
|
||||||
name: 'Direct Eight Conversion',
|
name: 'Duplicate Rank Opening Release',
|
||||||
description: 'The direct eight capture should be preferred when it removes the strongest immediate counter-card from the table.',
|
description: 'On an empty opening table, the search should release the non-denari duplicate high rank instead of opening with weaker denari or singleton alternatives.',
|
||||||
tags: ['material-swing'],
|
tags: ['opening-release', 'duplicate-rank'],
|
||||||
dealer: 2,
|
dealer: 3,
|
||||||
currentPlayer: 0,
|
currentPlayer: 0,
|
||||||
handSizes: [5, 5, 5, 5],
|
handSizes: [5, 5, 5, 5],
|
||||||
hands: [[
|
hands: [[
|
||||||
'coppe_10',
|
'coppe_8',
|
||||||
'spade_5',
|
|
||||||
'denara_8',
|
'denara_8',
|
||||||
'bastoni_9',
|
'denara_7',
|
||||||
'coppe_3',
|
'denara_6',
|
||||||
|
'bastoni_2',
|
||||||
], undefined, undefined, undefined],
|
], undefined, undefined, undefined],
|
||||||
table: ['denara_2', 'coppe_8', 'bastoni_4', 'spade_9'],
|
table: [],
|
||||||
piles: PILES_TEMPLATE_A,
|
pileCardCounts: [5, 5, 5, 5],
|
||||||
|
totalPoints: [7, 7],
|
||||||
expectedMove: {
|
expectedMove: {
|
||||||
cardId: 'denara_8',
|
cardId: 'coppe_8',
|
||||||
captureIds: ['coppe_8'],
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
154
src/game/ai.ts
154
src/game/ai.ts
@@ -721,6 +721,72 @@ function getPriorityThreatSummary(
|
|||||||
return countScopaThreats(afterTable, myHand, tracker, state, playerIdx);
|
return countScopaThreats(afterTable, myHand, tracker, state, playerIdx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isImmediateTacticalConcession(
|
||||||
|
afterTable: Card[],
|
||||||
|
nextIsOpp: boolean,
|
||||||
|
threats: ScopaThreatSummary | null,
|
||||||
|
): boolean {
|
||||||
|
if (!nextIsOpp || afterTable.length === 0) return false;
|
||||||
|
|
||||||
|
const tableSum = sumCardValues(afterTable);
|
||||||
|
if (afterTable.length === 1 && tableSum <= 10) return true;
|
||||||
|
if (afterTable.length === 2 && tableSum <= 10) return true;
|
||||||
|
|
||||||
|
return Boolean(threats?.nextOppCanScopa);
|
||||||
|
}
|
||||||
|
|
||||||
|
function evaluateSafeScopaPriority(
|
||||||
|
clearsTable: boolean,
|
||||||
|
afterTable: Card[],
|
||||||
|
lastPlay: boolean,
|
||||||
|
nextIsOpp: boolean,
|
||||||
|
threats: ScopaThreatSummary | null,
|
||||||
|
): number {
|
||||||
|
if (!clearsTable || lastPlay) return 0;
|
||||||
|
return isImmediateTacticalConcession(afterTable, nextIsOpp, threats) ? 1 : 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
function evaluateFirstHandOpeningReleasePriority(
|
||||||
|
card: Card,
|
||||||
|
myHand: Card[],
|
||||||
|
projectedHand: Card[],
|
||||||
|
afterTable: Card[],
|
||||||
|
state: GameState,
|
||||||
|
playerIdx: PlayerIndex,
|
||||||
|
tracker: CardTracker | undefined,
|
||||||
|
nextIsOpp: boolean,
|
||||||
|
roleContext: DealerRoleContext,
|
||||||
|
): number {
|
||||||
|
if (!nextIsOpp || roleContext.role !== 'first-hand' || afterTable.length !== 1) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextHandSize = state.players[nextPlayer(playerIdx)].hand.length;
|
||||||
|
if (nextHandSize <= 0) return 0;
|
||||||
|
|
||||||
|
const sameValueCount = countValueInHand(myHand, card.value);
|
||||||
|
const immediateScopaRisk = handLikelyHasValue(
|
||||||
|
card.value,
|
||||||
|
nextHandSize,
|
||||||
|
state,
|
||||||
|
playerIdx,
|
||||||
|
tracker,
|
||||||
|
projectedHand,
|
||||||
|
afterTable,
|
||||||
|
);
|
||||||
|
|
||||||
|
let score = 0;
|
||||||
|
score += Math.max(0, sameValueCount - 1) * 2;
|
||||||
|
if (sameValueCount >= 3) score += 2;
|
||||||
|
score += Math.round((0.32 - immediateScopaRisk) * 12);
|
||||||
|
|
||||||
|
if (sameValueCount >= 2 && card.value >= 8 && card.suit !== 'denara') score += 1;
|
||||||
|
if (card.suit === 'denara') score -= 1;
|
||||||
|
if (card.value === 7) score -= 1;
|
||||||
|
|
||||||
|
return clampPriorityBand(score, -8, 8);
|
||||||
|
}
|
||||||
|
|
||||||
function evaluateAntiScopaPriority(
|
function evaluateAntiScopaPriority(
|
||||||
afterTable: Card[],
|
afterTable: Card[],
|
||||||
nextIsOpp: boolean,
|
nextIsOpp: boolean,
|
||||||
@@ -1062,10 +1128,12 @@ function scoreCaptureAdv(
|
|||||||
): number {
|
): number {
|
||||||
const allCaptured = [played, ...captured];
|
const allCaptured = [played, ...captured];
|
||||||
const afterTable = table.filter(c => !captured.some(cc => cc.id === c.id));
|
const afterTable = table.filter(c => !captured.some(cc => cc.id === c.id));
|
||||||
|
const projectedHand = myHand.filter(card => card.id !== played.id);
|
||||||
const isScopa = afterTable.length === 0;
|
const isScopa = afterTable.length === 0;
|
||||||
const tableHasSettebello = table.some(c => c.suit === 'denara' && c.value === 7);
|
const tableHasSettebello = table.some(c => c.suit === 'denara' && c.value === 7);
|
||||||
const capturesSettebello = allCaptured.some(c => c.suit === 'denara' && c.value === 7);
|
const capturesSettebello = allCaptured.some(c => c.suit === 'denara' && c.value === 7);
|
||||||
const threats = getPriorityThreatSummary(afterTable, myHand, tracker, state, playerIdx);
|
const threats = getPriorityThreatSummary(afterTable, projectedHand, tracker, state, playerIdx);
|
||||||
|
const scopaPriority = evaluateSafeScopaPriority(isScopa, afterTable, lastPlay, nextIsOpp, threats);
|
||||||
let material = 30 + captured.length * (race.behindInCards ? 16 : 10) + phase * captured.length * 6;
|
let material = 30 + captured.length * (race.behindInCards ? 16 : 10) + phase * captured.length * 6;
|
||||||
|
|
||||||
material += allCaptured.filter(c => c.suit === 'denara').length * (race.behindInDenari ? 14 : 8);
|
material += allCaptured.filter(c => c.suit === 'denara').length * (race.behindInDenari ? 14 : 8);
|
||||||
@@ -1109,7 +1177,7 @@ function scoreCaptureAdv(
|
|||||||
if (roleContext.role === 'dealer' && !isScopa && sumCardValues(afterTable) >= 11) material += 10;
|
if (roleContext.role === 'dealer' && !isScopa && sumCardValues(afterTable) >= 11) material += 10;
|
||||||
|
|
||||||
return scoreTacticalPriorityLadder({
|
return scoreTacticalPriorityLadder({
|
||||||
scopa: isScopa && !lastPlay ? 2 : isScopa ? 0 : 0,
|
scopa: scopaPriority,
|
||||||
settebello: capturesSettebello ? 4 : tableHasSettebello && nextIsOpp ? -4 : tableHasSettebello ? -2 : 0,
|
settebello: capturesSettebello ? 4 : tableHasSettebello && nextIsOpp ? -4 : tableHasSettebello ? -2 : 0,
|
||||||
antiScopa: evaluateAntiScopaPriority(afterTable, nextIsOpp, threats),
|
antiScopa: evaluateAntiScopaPriority(afterTable, nextIsOpp, threats),
|
||||||
partnerSetup: isScopa ? 0 : evaluatePartnerSetupPriority(afterTable, nextIsOpp, partnerHandSize, threats),
|
partnerSetup: isScopa ? 0 : evaluatePartnerSetupPriority(afterTable, nextIsOpp, partnerHandSize, threats),
|
||||||
@@ -1126,10 +1194,22 @@ function scoreDumpAdv(
|
|||||||
lastPlay: boolean, roleContext: DealerRoleContext, rankResidue: RankResidueSnapshot | null,
|
lastPlay: boolean, roleContext: DealerRoleContext, rankResidue: RankResidueSnapshot | null,
|
||||||
): number {
|
): number {
|
||||||
const afterTable = [...table, card];
|
const afterTable = [...table, card];
|
||||||
|
const projectedHand = myHand.filter(held => held.id !== card.id);
|
||||||
|
|
||||||
// --- HARD RULES ---
|
// --- HARD RULES ---
|
||||||
if (card.suit === 'denara' && card.value === 7) return -10000;
|
if (card.suit === 'denara' && card.value === 7) return -10000;
|
||||||
const threats = getPriorityThreatSummary(afterTable, myHand, tracker, state, playerIdx);
|
const threats = getPriorityThreatSummary(afterTable, projectedHand, tracker, state, playerIdx);
|
||||||
|
const openingReleasePriority = evaluateFirstHandOpeningReleasePriority(
|
||||||
|
card,
|
||||||
|
myHand,
|
||||||
|
projectedHand,
|
||||||
|
afterTable,
|
||||||
|
state,
|
||||||
|
playerIdx,
|
||||||
|
tracker,
|
||||||
|
nextIsOpp,
|
||||||
|
roleContext,
|
||||||
|
);
|
||||||
let material = -20 + phase * 6;
|
let material = -20 + phase * 6;
|
||||||
|
|
||||||
if (card.suit === 'denara') material -= race.behindInDenari ? 28 : 16;
|
if (card.suit === 'denara') material -= race.behindInDenari ? 28 : 16;
|
||||||
@@ -1186,7 +1266,7 @@ function scoreDumpAdv(
|
|||||||
return scoreTacticalPriorityLadder({
|
return scoreTacticalPriorityLadder({
|
||||||
scopa: 0,
|
scopa: 0,
|
||||||
settebello: 0,
|
settebello: 0,
|
||||||
antiScopa: evaluateAntiScopaPriority(afterTable, nextIsOpp, threats),
|
antiScopa: evaluateAntiScopaPriority(afterTable, nextIsOpp, threats) + openingReleasePriority,
|
||||||
partnerSetup: evaluatePartnerSetupPriority(afterTable, nextIsOpp, partnerHandSize, threats),
|
partnerSetup: evaluatePartnerSetupPriority(afterTable, nextIsOpp, partnerHandSize, threats),
|
||||||
sevenDenial: evaluateSevenDenialPriority(afterTable, [], card, nextIsOpp, race.need7s),
|
sevenDenial: evaluateSevenDenialPriority(afterTable, [], card, nextIsOpp, race.need7s),
|
||||||
denariDenial: evaluateDenariDenialPriority(afterTable, [], card, nextIsOpp, race.behindInDenari),
|
denariDenial: evaluateDenariDenialPriority(afterTable, [], card, nextIsOpp, race.behindInDenari),
|
||||||
@@ -1947,18 +2027,37 @@ function scoreControlOverrideCandidate(
|
|||||||
playerIdx: PlayerIndex,
|
playerIdx: PlayerIndex,
|
||||||
race: RaceState,
|
race: RaceState,
|
||||||
roleContext: DealerRoleContext,
|
roleContext: DealerRoleContext,
|
||||||
|
tracker: CardTracker | undefined,
|
||||||
): number {
|
): number {
|
||||||
const hand = state.players[playerIdx].hand;
|
const hand = state.players[playerIdx].hand;
|
||||||
const nextIsOpp = isOpponent(playerIdx, nextPlayer(playerIdx));
|
const nextIsOpp = isOpponent(playerIdx, nextPlayer(playerIdx));
|
||||||
|
const lastPlay = isLastPlay(state, playerIdx);
|
||||||
const summary = summarizeMoveTactics(move, hand, state.table);
|
const summary = summarizeMoveTactics(move, hand, state.table);
|
||||||
const projectedHand = hand.filter(card => card.id !== move.card.id);
|
const projectedHand = hand.filter(card => card.id !== move.card.id);
|
||||||
|
const threats = getPriorityThreatSummary(summary.projectedTable, projectedHand, tracker, state, playerIdx);
|
||||||
|
const scopaPriority = evaluateSafeScopaPriority(summary.clearsTable, summary.projectedTable, lastPlay, nextIsOpp, threats);
|
||||||
|
const openingReleasePriority = move.capture.length === 0
|
||||||
|
? evaluateFirstHandOpeningReleasePriority(
|
||||||
|
move.card,
|
||||||
|
hand,
|
||||||
|
projectedHand,
|
||||||
|
summary.projectedTable,
|
||||||
|
state,
|
||||||
|
playerIdx,
|
||||||
|
tracker,
|
||||||
|
nextIsOpp,
|
||||||
|
roleContext,
|
||||||
|
)
|
||||||
|
: 0;
|
||||||
let score = Math.round(scoreHandStructure(projectedHand, summary.projectedTable, roleContext) * 0.55);
|
let score = Math.round(scoreHandStructure(projectedHand, summary.projectedTable, roleContext) * 0.55);
|
||||||
|
|
||||||
score += summary.projectedTable.length * 48;
|
score += summary.projectedTable.length * 48;
|
||||||
score += summary.tableSum >= 11 ? 90 + summary.tableSum * 8 : -260;
|
score += summary.tableSum >= 11 ? 90 + summary.tableSum * 8 : -260;
|
||||||
|
score += scopaPriority * 600;
|
||||||
|
|
||||||
if (move.capture.length === 0) {
|
if (move.capture.length === 0) {
|
||||||
if (summary.highQuietRelease) score += 220;
|
if (summary.highQuietRelease) score += 220;
|
||||||
|
score += openingReleasePriority * 180;
|
||||||
if (move.card.suit !== 'denara' && move.card.value <= 3) score += roleContext.defendingDealerAdvantage ? 260 : 70;
|
if (move.card.suit !== 'denara' && move.card.value <= 3) score += roleContext.defendingDealerAdvantage ? 260 : 70;
|
||||||
if (nextIsOpp && summary.projectedTable.length >= 5) score += 110;
|
if (nextIsOpp && summary.projectedTable.length >= 5) score += 110;
|
||||||
if (
|
if (
|
||||||
@@ -1972,6 +2071,9 @@ function scoreControlOverrideCandidate(
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (!isForcingSearchMove(summary, race)) score -= 200;
|
if (!isForcingSearchMove(summary, race)) score -= 200;
|
||||||
|
if (!summary.clearsTable && isImmediateTacticalConcession(summary.projectedTable, nextIsOpp, threats)) {
|
||||||
|
score -= 180;
|
||||||
|
}
|
||||||
if (nextIsOpp && summary.projectedTable.length <= 3) score -= 150;
|
if (nextIsOpp && summary.projectedTable.length <= 3) score -= 150;
|
||||||
if (nextIsOpp) score -= summary.exposedDenariCount * 90;
|
if (nextIsOpp) score -= summary.exposedDenariCount * 90;
|
||||||
if (nextIsOpp) score -= summary.exposedSevenCount * 70;
|
if (nextIsOpp) score -= summary.exposedSevenCount * 70;
|
||||||
@@ -2004,9 +2106,11 @@ function findStrategicControlOverride(
|
|||||||
playerIdx: PlayerIndex,
|
playerIdx: PlayerIndex,
|
||||||
race: RaceState,
|
race: RaceState,
|
||||||
roleContext: DealerRoleContext,
|
roleContext: DealerRoleContext,
|
||||||
|
tracker: CardTracker | undefined,
|
||||||
): AIMove | undefined {
|
): AIMove | undefined {
|
||||||
if (legalMoves.length <= 1) return undefined;
|
if (legalMoves.length <= 1) return undefined;
|
||||||
if (isLastPlay(state, playerIdx)) return undefined;
|
const lastPlay = isLastPlay(state, playerIdx);
|
||||||
|
if (lastPlay) return undefined;
|
||||||
if (!isOpponent(playerIdx, nextPlayer(playerIdx))) return undefined;
|
if (!isOpponent(playerIdx, nextPlayer(playerIdx))) return undefined;
|
||||||
|
|
||||||
let bestQuiet:
|
let bestQuiet:
|
||||||
@@ -2015,9 +2119,21 @@ function findStrategicControlOverride(
|
|||||||
let bestCapture:
|
let bestCapture:
|
||||||
| { move: AIMove; score: number }
|
| { move: AIMove; score: number }
|
||||||
| undefined;
|
| undefined;
|
||||||
|
let bestSafeScopa:
|
||||||
|
| { move: AIMove; score: number }
|
||||||
|
| undefined;
|
||||||
|
|
||||||
for (const move of legalMoves) {
|
for (const move of legalMoves) {
|
||||||
const score = scoreControlOverrideCandidate(move, state, playerIdx, race, roleContext);
|
const score = scoreControlOverrideCandidate(move, state, playerIdx, race, roleContext, tracker);
|
||||||
|
const summary = summarizeMoveTactics(move, state.players[playerIdx].hand, state.table);
|
||||||
|
const projectedHand = state.players[playerIdx].hand.filter(card => card.id !== move.card.id);
|
||||||
|
const threats = getPriorityThreatSummary(summary.projectedTable, projectedHand, tracker, state, playerIdx);
|
||||||
|
const scopaPriority = evaluateSafeScopaPriority(summary.clearsTable, summary.projectedTable, lastPlay, true, threats);
|
||||||
|
|
||||||
|
if (scopaPriority > 0) {
|
||||||
|
if (!bestSafeScopa || score > bestSafeScopa.score) bestSafeScopa = { move, score };
|
||||||
|
}
|
||||||
|
|
||||||
if (move.capture.length === 0) {
|
if (move.capture.length === 0) {
|
||||||
if (!bestQuiet || score > bestQuiet.score) bestQuiet = { move, score };
|
if (!bestQuiet || score > bestQuiet.score) bestQuiet = { move, score };
|
||||||
continue;
|
continue;
|
||||||
@@ -2026,6 +2142,8 @@ function findStrategicControlOverride(
|
|||||||
if (!bestCapture || score > bestCapture.score) bestCapture = { move, score };
|
if (!bestCapture || score > bestCapture.score) bestCapture = { move, score };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (bestSafeScopa) return bestSafeScopa.move;
|
||||||
|
|
||||||
if (!bestQuiet) return undefined;
|
if (!bestQuiet) return undefined;
|
||||||
|
|
||||||
const quietSummary = summarizeMoveTactics(bestQuiet.move, state.players[playerIdx].hand, state.table);
|
const quietSummary = summarizeMoveTactics(bestQuiet.move, state.players[playerIdx].hand, state.table);
|
||||||
@@ -2216,7 +2334,7 @@ async function masterMove(
|
|||||||
roleContext,
|
roleContext,
|
||||||
rankResidue,
|
rankResidue,
|
||||||
);
|
);
|
||||||
const controlOverride = findStrategicControlOverride(legalMoves, state, playerIdx, race, roleContext);
|
const controlOverride = findStrategicControlOverride(legalMoves, state, playerIdx, race, roleContext, tracker);
|
||||||
if (controlOverride) {
|
if (controlOverride) {
|
||||||
reportDecisionProgress(onProgress, 'master', startedAt, timing, profile.timeBudgetMs, 1, 1, {
|
reportDecisionProgress(onProgress, 'master', startedAt, timing, profile.timeBudgetMs, 1, 1, {
|
||||||
cardsRemaining,
|
cardsRemaining,
|
||||||
@@ -2395,13 +2513,28 @@ function quickEval(
|
|||||||
const projectedTableHasDenari = projectedTable.some(card => card.suit === 'denara');
|
const projectedTableHasDenari = projectedTable.some(card => card.suit === 'denara');
|
||||||
const projectedTableHasSeven = projectedTable.some(card => card.value === 7);
|
const projectedTableHasSeven = projectedTable.some(card => card.value === 7);
|
||||||
const nextIsOpp = isOpponent(playerIdx, nextPlayer(playerIdx));
|
const nextIsOpp = isOpponent(playerIdx, nextPlayer(playerIdx));
|
||||||
|
const threats = getPriorityThreatSummary(projectedTable, projectedHand, tracker, state, playerIdx);
|
||||||
|
const scopaPriority = evaluateSafeScopaPriority(moveSummary.clearsTable, projectedTable, lastPlay, nextIsOpp, threats);
|
||||||
|
const openingReleasePriority = move.capture.length === 0
|
||||||
|
? evaluateFirstHandOpeningReleasePriority(
|
||||||
|
move.card,
|
||||||
|
hand,
|
||||||
|
projectedHand,
|
||||||
|
projectedTable,
|
||||||
|
state,
|
||||||
|
playerIdx,
|
||||||
|
tracker,
|
||||||
|
nextIsOpp,
|
||||||
|
roleContext,
|
||||||
|
)
|
||||||
|
: 0;
|
||||||
const capturesSettebello = move.capture.some(card => card.suit === 'denara' && card.value === 7);
|
const capturesSettebello = move.capture.some(card => card.suit === 'denara' && card.value === 7);
|
||||||
const tableHasSettebello = table.some(card => card.suit === 'denara' && card.value === 7);
|
const tableHasSettebello = table.some(card => card.suit === 'denara' && card.value === 7);
|
||||||
const tableHasDenari = table.some(card => card.suit === 'denara');
|
const tableHasDenari = table.some(card => card.suit === 'denara');
|
||||||
|
|
||||||
// Scopa (not on last play!)
|
// Scopa (not on last play!)
|
||||||
if (move.capture.length > 0 && projectedTable.length === 0) {
|
if (move.capture.length > 0 && projectedTable.length === 0) {
|
||||||
score += lastPlay ? 50 : 1200;
|
score += lastPlay ? 50 : scopaPriority * 780;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Settebello
|
// Settebello
|
||||||
@@ -2430,6 +2563,7 @@ function quickEval(
|
|||||||
|
|
||||||
if (move.capture.length === 0) {
|
if (move.capture.length === 0) {
|
||||||
score -= 200;
|
score -= 200;
|
||||||
|
score += openingReleasePriority * 220;
|
||||||
if (move.card.value >= 8) score += 40;
|
if (move.card.value >= 8) score += 40;
|
||||||
if (move.card.suit === 'denara') score -= 130;
|
if (move.card.suit === 'denara') score -= 130;
|
||||||
if (move.card.value === 7) score -= 100;
|
if (move.card.value === 7) score -= 100;
|
||||||
@@ -2480,6 +2614,10 @@ function quickEval(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (move.capture.length > 0 && !moveSummary.clearsTable && isImmediateTacticalConcession(projectedTable, nextIsOpp, threats)) {
|
||||||
|
score -= 180;
|
||||||
|
}
|
||||||
|
|
||||||
// Partner awareness
|
// Partner awareness
|
||||||
const next = nextPlayer(playerIdx);
|
const next = nextPlayer(playerIdx);
|
||||||
if (!isOpponent(playerIdx, next) && projectedTable.length > 0) {
|
if (!isOpponent(playerIdx, next) && projectedTable.length > 0) {
|
||||||
|
|||||||
@@ -50,6 +50,12 @@ const config: Phaser.Types.Core.GameConfig = {
|
|||||||
backgroundColor: '#1a5c2a',
|
backgroundColor: '#1a5c2a',
|
||||||
parent: 'game',
|
parent: 'game',
|
||||||
scene: [BootScene, MenuScene, GameScene, SettingsScene],
|
scene: [BootScene, MenuScene, GameScene, SettingsScene],
|
||||||
|
render: {
|
||||||
|
antialias: true,
|
||||||
|
antialiasGL: true,
|
||||||
|
pixelArt: false,
|
||||||
|
roundPixels: false,
|
||||||
|
},
|
||||||
scale: {
|
scale: {
|
||||||
mode: Phaser.Scale.FIT,
|
mode: Phaser.Scale.FIT,
|
||||||
autoCenter: Phaser.Scale.CENTER_BOTH,
|
autoCenter: Phaser.Scale.CENTER_BOTH,
|
||||||
|
|||||||
@@ -37,10 +37,11 @@ const CH_A = 645 * CARD_SCALE_AI; // card height for AI ≈ 81
|
|||||||
const SCOREBAR_H = 54;
|
const SCOREBAR_H = 54;
|
||||||
const AI_MIN_THINK_MS = 1000;
|
const AI_MIN_THINK_MS = 1000;
|
||||||
const MOVE_OUTCOME_STATUS_MS = 2000;
|
const MOVE_OUTCOME_STATUS_MS = 2000;
|
||||||
const PLAYED_CARD_TRAVEL_MS = 400;
|
const PLAYED_CARD_TRAVEL_MS = 320;
|
||||||
const CAPTURE_COLLAPSE_MS = 480;
|
const CAPTURE_COLLAPSE_MS = 360;
|
||||||
const CAPTURE_COLLAPSE_DELAY_MS = 60;
|
const CAPTURE_COLLAPSE_DELAY_MS = 60;
|
||||||
const NON_CAPTURE_TABLE_TWEEN_MS = 560;
|
const NON_CAPTURE_TABLE_TWEEN_MS = 420;
|
||||||
|
const RELAYOUT_TWEEN_MS = 120;
|
||||||
|
|
||||||
// Player positions:
|
// Player positions:
|
||||||
// 0 = South (human, bottom), 1 = West (AI, left, rotated -90°)
|
// 0 = South (human, bottom), 1 = West (AI, left, rotated -90°)
|
||||||
@@ -93,7 +94,7 @@ export class GameScene extends Phaser.Scene {
|
|||||||
private thinkBar!: Phaser.GameObjects.Graphics;
|
private thinkBar!: Phaser.GameObjects.Graphics;
|
||||||
|
|
||||||
// Player label containers (pulsed on active turn)
|
// Player label containers (pulsed on active turn)
|
||||||
private playerLabels: Map<PlayerIndex, Phaser.GameObjects.Text> = new Map();
|
private playerLabels: Map<PlayerIndex, Phaser.GameObjects.Container> = new Map();
|
||||||
|
|
||||||
// Interaction state
|
// Interaction state
|
||||||
private selectedCard: Card | null = null;
|
private selectedCard: Card | null = null;
|
||||||
@@ -388,6 +389,7 @@ export class GameScene extends Phaser.Scene {
|
|||||||
this.statusText = this.add.text(W / 2, H - CH_H - 36, '', {
|
this.statusText = this.add.text(W / 2, H - CH_H - 36, '', {
|
||||||
fontFamily: 'serif', fontSize: '17px', color: '#ffffff',
|
fontFamily: 'serif', fontSize: '17px', color: '#ffffff',
|
||||||
stroke: '#000', strokeThickness: 2,
|
stroke: '#000', strokeThickness: 2,
|
||||||
|
resolution: 2,
|
||||||
}).setOrigin(0.5).setDepth(10);
|
}).setOrigin(0.5).setDepth(10);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -524,23 +526,59 @@ export class GameScene extends Phaser.Scene {
|
|||||||
return `Problema durante la mossa di ${this.state.players[playerIdx].name}.`;
|
return `Problema durante la mossa di ${this.state.players[playerIdx].name}.`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private withHiResText(
|
||||||
|
style: Phaser.Types.GameObjects.Text.TextStyle,
|
||||||
|
): Phaser.Types.GameObjects.Text.TextStyle {
|
||||||
|
return {
|
||||||
|
...style,
|
||||||
|
resolution: style.resolution ?? 2,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private createPlayerNameplate(
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
text: string,
|
||||||
|
color: string,
|
||||||
|
fillColor: number,
|
||||||
|
strokeColor: number,
|
||||||
|
): Phaser.GameObjects.Container {
|
||||||
|
const label = this.add.text(0, 0, text, this.withHiResText({
|
||||||
|
fontFamily: 'serif',
|
||||||
|
fontSize: '11px',
|
||||||
|
color,
|
||||||
|
stroke: '#000',
|
||||||
|
strokeThickness: 1,
|
||||||
|
align: 'center',
|
||||||
|
})).setOrigin(0.5);
|
||||||
|
|
||||||
|
const padX = 12;
|
||||||
|
const padY = 4;
|
||||||
|
const width = label.width + padX * 2;
|
||||||
|
const height = label.height + padY * 2;
|
||||||
|
const background = this.add.graphics();
|
||||||
|
background.fillStyle(fillColor, 0.92);
|
||||||
|
background.fillRoundedRect(-width / 2, -height / 2, width, height, 9);
|
||||||
|
background.lineStyle(1, strokeColor, 0.7);
|
||||||
|
background.strokeRoundedRect(-width / 2, -height / 2, width, height, 9);
|
||||||
|
|
||||||
|
return this.add.container(x, y, [background, label]).setDepth(18);
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Player labels (pulse on active turn)
|
// Player labels (pulse on active turn)
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
private buildPlayerLabels(W: number, H: number): void {
|
private buildPlayerLabels(W: number, H: number): void {
|
||||||
const defs: Array<{ idx: PlayerIndex; x: number; y: number; color: string;
|
const defs: Array<{ idx: PlayerIndex; x: number; y: number; color: string;
|
||||||
txt: string; originX: number; originY: number }> = [
|
fillColor: number; strokeColor: number; txt: string }> = [
|
||||||
{ idx: 0, x: W / 2, y: H - CH_H - 28, color: '#aaffaa', txt: 'Tu [Team A]', originX: 0.5, originY: 1 },
|
{ idx: 0, x: W / 2, y: H - 9, color: '#dfffe5', fillColor: 0x0b2410, strokeColor: 0x4aa86a, txt: 'Tu [Team A]' },
|
||||||
{ idx: 1, x: CH_A + 20, y: H / 2 + SCOREBAR_H / 2, color: '#ffaaaa', txt: 'AI\nOvest\n[B]', originX: 0, originY: 0.5 },
|
{ idx: 1, x: CH_A + 58, y: H / 2 + SCOREBAR_H / 2, color: '#ffe0e0', fillColor: 0x260d0d, strokeColor: 0xb36a6a, txt: 'AI Ovest [B]' },
|
||||||
{ idx: 2, x: W / 2, y: SCOREBAR_H + CH_A + 44, color: '#aaffaa', txt: 'Compagno [Team A]', originX: 0.5, originY: 0 },
|
{ idx: 2, x: W / 2, y: SCOREBAR_H + 11, color: '#dfffe5', fillColor: 0x0b2410, strokeColor: 0x4aa86a, txt: 'Compagno [Team A]' },
|
||||||
{ idx: 3, x: W - CH_A - 20, y: H / 2 + SCOREBAR_H / 2, color: '#ffaaaa', txt: 'AI\nEst\n[B]', originX: 1, originY: 0.5 },
|
{ idx: 3, x: W - CH_A - 58, y: H / 2 + SCOREBAR_H / 2, color: '#ffe0e0', fillColor: 0x260d0d, strokeColor: 0xb36a6a, txt: 'AI Est [B]' },
|
||||||
];
|
];
|
||||||
for (const d of defs) {
|
for (const d of defs) {
|
||||||
const lbl = this.add.text(d.x, d.y, d.txt, {
|
const lbl = this.createPlayerNameplate(d.x, d.y, d.txt, d.color, d.fillColor, d.strokeColor);
|
||||||
fontFamily: 'serif', fontSize: '12px', color: d.color,
|
|
||||||
stroke: '#000', strokeThickness: 1, align: 'center', resolution: 2,
|
|
||||||
}).setOrigin(d.originX, d.originY).setDepth(2);
|
|
||||||
this.playerLabels.set(d.idx, lbl);
|
this.playerLabels.set(d.idx, lbl);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -594,7 +632,7 @@ export class GameScene extends Phaser.Scene {
|
|||||||
const activeLbl = this.playerLabels.get(playerIdx)!;
|
const activeLbl = this.playerLabels.get(playerIdx)!;
|
||||||
this.tweens.add({
|
this.tweens.add({
|
||||||
targets: activeLbl,
|
targets: activeLbl,
|
||||||
scaleX: 1.2, scaleY: 1.2,
|
scaleX: 1.08, scaleY: 1.08,
|
||||||
duration: 300, yoyo: true, ease: 'Sine.InOut',
|
duration: 300, yoyo: true, ease: 'Sine.InOut',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -929,6 +967,7 @@ export class GameScene extends Phaser.Scene {
|
|||||||
const btn = this.add.zone(W / 2, y, 360, 28).setInteractive({ useHandCursor: true }).setDepth(21);
|
const btn = this.add.zone(W / 2, y, 360, 28).setInteractive({ useHandCursor: true }).setDepth(21);
|
||||||
const txt = this.add.text(W / 2, y, `Prendi: ${label}`, {
|
const txt = this.add.text(W / 2, y, `Prendi: ${label}`, {
|
||||||
fontFamily: 'serif', fontSize: '14px', color: color.text,
|
fontFamily: 'serif', fontSize: '14px', color: color.text,
|
||||||
|
resolution: 2,
|
||||||
}).setOrigin(0.5).setDepth(21);
|
}).setOrigin(0.5).setDepth(21);
|
||||||
btn.on('pointerdown', () => this.confirmMove(this.selectedCard!, cap));
|
btn.on('pointerdown', () => this.confirmMove(this.selectedCard!, cap));
|
||||||
(bg as any)._captureBtn = true;
|
(bg as any)._captureBtn = true;
|
||||||
@@ -1113,7 +1152,15 @@ export class GameScene extends Phaser.Scene {
|
|||||||
const positions = this.getHandPositions(playerIdx, hand.length);
|
const positions = this.getHandPositions(playerIdx, hand.length);
|
||||||
hand.forEach((card, i) => {
|
hand.forEach((card, i) => {
|
||||||
const img = this.cardImages.get(card.id);
|
const img = this.cardImages.get(card.id);
|
||||||
if (img) this.tweens.add({ targets: img, x: positions[i].x, y: positions[i].y, duration: 160 });
|
if (img) {
|
||||||
|
this.tweens.add({
|
||||||
|
targets: img,
|
||||||
|
x: positions[i].x,
|
||||||
|
y: positions[i].y,
|
||||||
|
duration: RELAYOUT_TWEEN_MS,
|
||||||
|
ease: 'Cubic.Out',
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1123,7 +1170,15 @@ export class GameScene extends Phaser.Scene {
|
|||||||
const positions = this.getTablePositions(table.length);
|
const positions = this.getTablePositions(table.length);
|
||||||
table.forEach((card, i) => {
|
table.forEach((card, i) => {
|
||||||
const img = this.cardImages.get(card.id);
|
const img = this.cardImages.get(card.id);
|
||||||
if (img?.visible) this.tweens.add({ targets: img, x: positions[i].x, y: positions[i].y, duration: 160 });
|
if (img?.visible) {
|
||||||
|
this.tweens.add({
|
||||||
|
targets: img,
|
||||||
|
x: positions[i].x,
|
||||||
|
y: positions[i].y,
|
||||||
|
duration: RELAYOUT_TWEEN_MS,
|
||||||
|
ease: 'Cubic.Out',
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1196,12 +1251,14 @@ export class GameScene extends Phaser.Scene {
|
|||||||
const txt = this.add.text(W / 2, H / 2, 'SCOPA!', {
|
const txt = this.add.text(W / 2, H / 2, 'SCOPA!', {
|
||||||
fontFamily: 'Georgia, serif', fontSize: '108px', color: '#ffd700',
|
fontFamily: 'Georgia, serif', fontSize: '108px', color: '#ffd700',
|
||||||
stroke: '#000000', strokeThickness: 10,
|
stroke: '#000000', strokeThickness: 10,
|
||||||
|
resolution: 2,
|
||||||
}).setOrigin(0.5).setDepth(50).setAlpha(0).setScale(0.2);
|
}).setOrigin(0.5).setDepth(50).setAlpha(0).setScale(0.2);
|
||||||
|
|
||||||
const sub = this.add.text(W / 2, H / 2 + 110, player.name, {
|
const sub = this.add.text(W / 2, H / 2 + 110, player.name, {
|
||||||
fontFamily: 'serif', fontSize: '32px',
|
fontFamily: 'serif', fontSize: '32px',
|
||||||
color: isTeamA ? '#aaffaa' : '#ffaaaa',
|
color: isTeamA ? '#aaffaa' : '#ffaaaa',
|
||||||
stroke: '#000', strokeThickness: 3,
|
stroke: '#000', strokeThickness: 3,
|
||||||
|
resolution: 2,
|
||||||
}).setOrigin(0.5).setDepth(50).setAlpha(0);
|
}).setOrigin(0.5).setDepth(50).setAlpha(0);
|
||||||
|
|
||||||
this.tweens.add({
|
this.tweens.add({
|
||||||
@@ -1257,6 +1314,7 @@ export class GameScene extends Phaser.Scene {
|
|||||||
const txt = this.add.text(W / 2, H / 2 - 60, '7♦', {
|
const txt = this.add.text(W / 2, H / 2 - 60, '7♦', {
|
||||||
fontFamily: 'Georgia, serif', fontSize: '64px', color: '#ffd700',
|
fontFamily: 'Georgia, serif', fontSize: '64px', color: '#ffd700',
|
||||||
stroke: '#000', strokeThickness: 7,
|
stroke: '#000', strokeThickness: 7,
|
||||||
|
resolution: 2,
|
||||||
}).setOrigin(0.5).setDepth(50).setAlpha(0).setScale(0.4);
|
}).setOrigin(0.5).setDepth(50).setAlpha(0).setScale(0.4);
|
||||||
|
|
||||||
this.tweens.add({
|
this.tweens.add({
|
||||||
@@ -1497,10 +1555,11 @@ export class GameScene extends Phaser.Scene {
|
|||||||
];
|
];
|
||||||
|
|
||||||
lines.forEach(([line, color], i) => {
|
lines.forEach(([line, color], i) => {
|
||||||
this.add.text(W / 2, H / 2 - 190 + i * 34, line, {
|
this.add.text(W / 2, H / 2 - 190 + i * 34, line, this.withHiResText({
|
||||||
fontFamily: i === 0 ? 'Georgia, serif' : 'monospace',
|
fontFamily: i === 0 ? 'Georgia, serif' : 'monospace',
|
||||||
fontSize: i === 0 ? '28px' : '16px', color,
|
fontSize: i === 0 ? '28px' : '16px',
|
||||||
}).setOrigin(0.5).setDepth(32);
|
color,
|
||||||
|
})).setOrigin(0.5).setDepth(32);
|
||||||
});
|
});
|
||||||
|
|
||||||
const outcome = getMatchOutcome(this.state.teamScores);
|
const outcome = getMatchOutcome(this.state.teamScores);
|
||||||
@@ -1514,6 +1573,7 @@ export class GameScene extends Phaser.Scene {
|
|||||||
.setInteractive({ useHandCursor: true }).setDepth(33);
|
.setInteractive({ useHandCursor: true }).setDepth(33);
|
||||||
this.add.text(W / 2, H / 2 + 207, btnLabel, {
|
this.add.text(W / 2, H / 2 + 207, btnLabel, {
|
||||||
fontFamily: 'Georgia, serif', fontSize: '20px', color: '#0a2e10',
|
fontFamily: 'Georgia, serif', fontSize: '20px', color: '#0a2e10',
|
||||||
|
resolution: 2,
|
||||||
}).setOrigin(0.5).setDepth(34);
|
}).setOrigin(0.5).setDepth(34);
|
||||||
|
|
||||||
btnG.on('pointerover', () => { btnG.clear(); btnG.fillStyle(0xffec6e, 1); btnG.fillRoundedRect(W / 2 - 110, H / 2 + 185, 220, 44, 10); });
|
btnG.on('pointerover', () => { btnG.clear(); btnG.fillStyle(0xffec6e, 1); btnG.fillRoundedRect(W / 2 - 110, H / 2 + 185, 220, 44, 10); });
|
||||||
@@ -1556,13 +1616,16 @@ export class GameScene extends Phaser.Scene {
|
|||||||
this.add.text(W / 2, H / 2 - 110, 'PARTITA CONCLUSA', {
|
this.add.text(W / 2, H / 2 - 110, 'PARTITA CONCLUSA', {
|
||||||
fontFamily: 'Georgia, serif', fontSize: '44px', color: '#ffd700',
|
fontFamily: 'Georgia, serif', fontSize: '44px', color: '#ffd700',
|
||||||
stroke: '#000', strokeThickness: 6,
|
stroke: '#000', strokeThickness: 6,
|
||||||
|
resolution: 2,
|
||||||
}).setOrigin(0.5).setDepth(42);
|
}).setOrigin(0.5).setDepth(42);
|
||||||
this.add.text(W / 2, H / 2 - 30, win ? 'Vince la tua squadra' : 'Vincono gli avversari', {
|
this.add.text(W / 2, H / 2 - 30, win ? 'Vince la tua squadra' : 'Vincono gli avversari', {
|
||||||
fontFamily: 'serif', fontSize: '26px',
|
fontFamily: 'serif', fontSize: '26px',
|
||||||
color: win ? '#aaffaa' : '#ffaaaa',
|
color: win ? '#aaffaa' : '#ffaaaa',
|
||||||
|
resolution: 2,
|
||||||
}).setOrigin(0.5).setDepth(42);
|
}).setOrigin(0.5).setDepth(42);
|
||||||
this.add.text(W / 2, H / 2 + 35, `${t0.totalPoints} — ${t1.totalPoints}`, {
|
this.add.text(W / 2, H / 2 + 35, `${t0.totalPoints} — ${t1.totalPoints}`, {
|
||||||
fontFamily: 'Georgia, serif', fontSize: '50px', color: '#ffd700',
|
fontFamily: 'Georgia, serif', fontSize: '50px', color: '#ffd700',
|
||||||
|
resolution: 2,
|
||||||
}).setOrigin(0.5).setDepth(42);
|
}).setOrigin(0.5).setDepth(42);
|
||||||
|
|
||||||
const bz = this.add.zone(W / 2, H / 2 + 115, 230, 48)
|
const bz = this.add.zone(W / 2, H / 2 + 115, 230, 48)
|
||||||
@@ -1572,6 +1635,7 @@ export class GameScene extends Phaser.Scene {
|
|||||||
drawBtn(0xffd700);
|
drawBtn(0xffd700);
|
||||||
this.add.text(W / 2, H / 2 + 115, 'NUOVA PARTITA', {
|
this.add.text(W / 2, H / 2 + 115, 'NUOVA PARTITA', {
|
||||||
fontFamily: 'Georgia, serif', fontSize: '21px', color: '#0a2e10',
|
fontFamily: 'Georgia, serif', fontSize: '21px', color: '#0a2e10',
|
||||||
|
resolution: 2,
|
||||||
}).setOrigin(0.5).setDepth(44);
|
}).setOrigin(0.5).setDepth(44);
|
||||||
bz.on('pointerover', () => drawBtn(0xffec6e));
|
bz.on('pointerover', () => drawBtn(0xffec6e));
|
||||||
bz.on('pointerout', () => drawBtn(0xffd700));
|
bz.on('pointerout', () => drawBtn(0xffd700));
|
||||||
|
|||||||
Reference in New Issue
Block a user