feat: improve Go Fish AI with card sprites and turn exclusions
- Add custom card texture spritesheet and render cards using sprite frames - Prevent AI from repeatedly asking for the same rank after a failed catch - Track and exclude previously attempted (target, rank) pairs per turn to reduce redundant moves - Load gofish-cards assets in preload scene
This commit is contained in:
parent
618d3d31c4
commit
b6d85e30be
Binary file not shown.
|
After Width: | Height: | Size: 2.3 MiB |
Binary file not shown.
|
|
@ -71,7 +71,7 @@ export function observeLog(memory, state, selfSeat) {
|
|||
* Pick (targetSeat, rank) for the AI. Falls back gracefully if memory is
|
||||
* empty (e.g. very first turn).
|
||||
*/
|
||||
export function chooseAction(state, seat, memory) {
|
||||
export function chooseAction(state, seat, memory, exclude = new Set()) {
|
||||
const me = state.players[seat];
|
||||
if (!me || me.hand.length === 0) return null;
|
||||
|
||||
|
|
@ -94,6 +94,7 @@ export function chooseAction(state, seat, memory) {
|
|||
if (target === seat) continue;
|
||||
const opp = state.players[target];
|
||||
if (opp.sittingOut || opp.hand.length === 0) continue;
|
||||
if (exclude.has(`${target}:${rank}`)) continue;
|
||||
|
||||
let score = 0;
|
||||
const belief = memory.beliefs[target][rank] || 0;
|
||||
|
|
@ -129,6 +130,7 @@ export function chooseAction(state, seat, memory) {
|
|||
for (const rank of myRanks) {
|
||||
for (let t = 0; t < state.players.length; t++) {
|
||||
if (t === seat) continue;
|
||||
if (exclude.has(`${t}:${rank}`)) continue;
|
||||
const opp = state.players[t];
|
||||
if (!opp.sittingOut && opp.hand.length > 0) {
|
||||
return { targetSeat: t, rank };
|
||||
|
|
|
|||
|
|
@ -116,6 +116,8 @@ const SUIT_COLORS = {
|
|||
d: { fill: 0xfbe7e2, stroke: 0xc92a2a, glyph: '#c92a2a' },
|
||||
};
|
||||
|
||||
const GOFISH_CARD_FRAME = { A: 0, '2': 1, '3': 2, '4': 3, '5': 4, '6': 5, '7': 6, '8': 7, '9': 8, T: 9, J: 10, Q: 11, K: 12 };
|
||||
|
||||
// ── Scene ───────────────────────────────────────────────────────────────────
|
||||
export default class GoFishGame extends Phaser.Scene {
|
||||
constructor() { super('GoFishGame'); }
|
||||
|
|
@ -357,6 +359,7 @@ export default class GoFishGame extends Phaser.Scene {
|
|||
const playerCount = this.slotForSeat.length;
|
||||
const finalState = createInitialState({ playerCount, matchSize: this.matchVariant });
|
||||
this.aiMemory = [];
|
||||
this.aiTurnExclusions = new Map();
|
||||
for (let s = 0; s < playerCount; s++) {
|
||||
this.aiMemory[s] = createMemory(playerCount);
|
||||
observeLog(this.aiMemory[s], finalState, s);
|
||||
|
|
@ -496,6 +499,17 @@ export default class GoFishGame extends Phaser.Scene {
|
|||
return;
|
||||
}
|
||||
|
||||
const frame = GOFISH_CARD_FRAME[card.rank];
|
||||
if (frame !== undefined && this.textures.exists('gofish-cards')) {
|
||||
g.destroy();
|
||||
container.add(
|
||||
this.add.image(0, 0, 'gofish-cards', frame)
|
||||
.setDisplaySize(CARD_W, CARD_H)
|
||||
.setOrigin(0.5)
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const suit = SUIT_COLORS[card.suit] ?? SUIT_COLORS.s;
|
||||
g.fillStyle(suit.fill, 1);
|
||||
g.fillRoundedRect(x, y, CARD_W, CARD_H, CARD_R);
|
||||
|
|
@ -763,6 +777,10 @@ export default class GoFishGame extends Phaser.Scene {
|
|||
}
|
||||
|
||||
// Catch, or fish with empty pool — normal completion.
|
||||
if (last.result === 'catch' && askerSeat !== 0) {
|
||||
if (!this.aiTurnExclusions.has(askerSeat)) this.aiTurnExclusions.set(askerSeat, new Set());
|
||||
this.aiTurnExclusions.get(askerSeat).add(`${targetSeat}:${rank}`);
|
||||
}
|
||||
if (last.result === 'catch' && targetSeat !== 0) {
|
||||
this.opponentPortraits[targetSeat]?.playEmotion('upset');
|
||||
}
|
||||
|
|
@ -1335,7 +1353,12 @@ export default class GoFishGame extends Phaser.Scene {
|
|||
if (this.gameOver || this.animating) return;
|
||||
const seat = this.gs.currentPlayer;
|
||||
if (seat === 0) return;
|
||||
const action = chooseAction(this.gs, seat, this.aiMemory[seat]);
|
||||
// Clear exclusions when starting a fresh turn (not continuing after a catch).
|
||||
if (this.gs.lastAsk?.askerSeat !== seat || this.gs.lastAsk?.result !== 'catch') {
|
||||
this.aiTurnExclusions.set(seat, new Set());
|
||||
}
|
||||
const exclude = this.aiTurnExclusions.get(seat) ?? new Set();
|
||||
const action = chooseAction(this.gs, seat, this.aiMemory[seat], exclude);
|
||||
if (!action) {
|
||||
// Shouldn't happen — fail-safe: end the game if no action available.
|
||||
this.endGame();
|
||||
|
|
|
|||
|
|
@ -76,6 +76,7 @@ export default class PreloadScene extends Phaser.Scene {
|
|||
// Prosperity token sprites (1 VP, 5 VP, Gold) — 150×150 each.
|
||||
this.load.spritesheet('dominion-tokens', '/assets/images/dominion-tokens.png', { frameWidth: 150, frameHeight: 150 });
|
||||
this.load.spritesheet('ttr-cards', '/assets/images/tickettoride-cards.png', { frameWidth: 270, frameHeight: 390 });
|
||||
this.load.spritesheet('gofish-cards', '/assets/images/gofish-cards.png', { frameWidth: 270, frameHeight: 390 });
|
||||
this.load.spritesheet('tab-icons', '/assets/images/tab-icons.png', { frameWidth: 128, frameHeight: 128 });
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue