feat: add Freecell card game and improve Triominoes tile visibility
- Register Freecell game in server registry with card game configuration - Import and configure FreecellGame in main.js and GameRoomScene - Update game-icons sprite sheet with new icon frame for Freecell - Fix Triominoes to dim all tiles when no legal moves are available
This commit is contained in:
parent
74d5470d11
commit
4fbc868305
Binary file not shown.
|
Before Width: | Height: | Size: 184 KiB After Width: | Height: | Size: 189 KiB |
Binary file not shown.
|
|
@ -0,0 +1,609 @@
|
||||||
|
import * as Phaser from 'phaser';
|
||||||
|
import { GAME_WIDTH, GAME_HEIGHT, COLORS } from '../../config.js';
|
||||||
|
import { Button } from '../../ui/Button.js';
|
||||||
|
import { MusicPlayer } from '../../ui/MusicPlayer.js';
|
||||||
|
import { playSound, SFX } from '../../ui/Sounds.js';
|
||||||
|
import { api } from '../../services/api.js';
|
||||||
|
import { FreecellEngine, freshDeck } from './FreecellLogic.js';
|
||||||
|
|
||||||
|
const CARD_W = 100;
|
||||||
|
const CARD_H = 140;
|
||||||
|
const CARD_R = 10;
|
||||||
|
const COL_GAP = 132;
|
||||||
|
const START_X = 448;
|
||||||
|
const colX = (c) => START_X + c * COL_GAP;
|
||||||
|
|
||||||
|
const FC_Y = 100; // freecell row centre-y
|
||||||
|
const HOME_Y = 100; // foundation row centre-y
|
||||||
|
const TAB_Y = 280; // tableau top card centre-y
|
||||||
|
const FAN = 28; // px per fanned card in a column
|
||||||
|
|
||||||
|
const FELT = 0x0e3b2a;
|
||||||
|
const SEL = 0x2ec28a;
|
||||||
|
const HINT = 0xc8a84b;
|
||||||
|
|
||||||
|
const SUITS_ORDER = ['s', 'h', 'd', 'c'];
|
||||||
|
const SUIT_GLYPH = { s: '♠', h: '♥', d: '♦', c: '♣' };
|
||||||
|
|
||||||
|
const D = { felt: -2, table: -1, card: 10, ui: 30, banner: 34, overlay: 60, overlayUI: 62 };
|
||||||
|
|
||||||
|
export default class FreecellGame extends Phaser.Scene {
|
||||||
|
constructor() { super('FreecellGame'); }
|
||||||
|
|
||||||
|
init(data) {
|
||||||
|
this.gameDef = data.game;
|
||||||
|
this.engine = null;
|
||||||
|
this.sel = null; // { kind:'tableau', col, fromIdx } | { kind:'freecell', fcIdx }
|
||||||
|
this.animating = false;
|
||||||
|
this.autoRunning = false;
|
||||||
|
this.recorded = false;
|
||||||
|
this.overlayObjs = [];
|
||||||
|
this.overlayUp = false;
|
||||||
|
this.pulse = null;
|
||||||
|
|
||||||
|
this.board = null;
|
||||||
|
this.cardSprites = new Map();
|
||||||
|
this.dropZones = [];
|
||||||
|
this.potentialDrag = null;
|
||||||
|
this.dragState = null;
|
||||||
|
this.dropHighlight = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
create() {
|
||||||
|
try {
|
||||||
|
const music = this.cache.json.get('music');
|
||||||
|
if (music?.tracks) new MusicPlayer(this, music.tracks);
|
||||||
|
} catch (_) { /* optional */ }
|
||||||
|
|
||||||
|
this.add.rectangle(GAME_WIDTH / 2, GAME_HEIGHT / 2, GAME_WIDTH, GAME_HEIGHT, FELT).setDepth(D.felt);
|
||||||
|
this.add.rectangle(GAME_WIDTH / 2, GAME_HEIGHT / 2, GAME_WIDTH - 24, GAME_HEIGHT - 24, FELT)
|
||||||
|
.setStrokeStyle(3, COLORS.accent, 0.35).setDepth(D.table);
|
||||||
|
|
||||||
|
this.add.text(40, 28, 'FREECELL', {
|
||||||
|
fontFamily: 'Righteous', fontSize: '40px', color: COLORS.goldHex,
|
||||||
|
}).setDepth(D.ui);
|
||||||
|
|
||||||
|
this.movesText = this.add.text(24, GAME_HEIGHT - 24, 'Moves: 0', {
|
||||||
|
fontFamily: 'Righteous', fontSize: '28px', color: COLORS.textHex,
|
||||||
|
}).setOrigin(0, 1).setDepth(D.ui);
|
||||||
|
|
||||||
|
this.stuckBanner = this.add.text(GAME_WIDTH / 2, 200, 'No more moves!', {
|
||||||
|
fontFamily: '"Julius Sans One"', fontSize: '26px', color: COLORS.dangerHex,
|
||||||
|
}).setOrigin(0.5).setDepth(D.banner).setVisible(false);
|
||||||
|
|
||||||
|
this.board = this.add.container(0, 0).setDepth(D.card);
|
||||||
|
|
||||||
|
this.autoCompleteBtn = new Button(this, GAME_WIDTH / 2, 1018, 'Auto-Complete', () => this.onAutoComplete(),
|
||||||
|
{ width: 280, height: 60, fontSize: 24 });
|
||||||
|
this.autoCompleteBtn.setDepth(D.ui).setVisible(false);
|
||||||
|
|
||||||
|
this.newGameBtn = new Button(this, GAME_WIDTH / 2 - 180, 1018, 'New Game', () => this.startGame(),
|
||||||
|
{ width: 240, height: 60, fontSize: 24 });
|
||||||
|
this.newGameBtn.setDepth(D.ui).setVisible(false);
|
||||||
|
|
||||||
|
this.leaveBtn = new Button(this, GAME_WIDTH - 110, 1042, 'Leave', () => this.scene.start('GameMenu'),
|
||||||
|
{ variant: 'ghost', width: 160, height: 54, fontSize: 22 });
|
||||||
|
this.leaveBtn.setDepth(D.ui);
|
||||||
|
|
||||||
|
this.setupDrag();
|
||||||
|
this.startGame();
|
||||||
|
}
|
||||||
|
|
||||||
|
startGame() {
|
||||||
|
this.sel = null;
|
||||||
|
this.animating = false;
|
||||||
|
this.autoRunning = false;
|
||||||
|
this.recorded = false;
|
||||||
|
this.overlayUp = false;
|
||||||
|
this.clearOverlay();
|
||||||
|
this.stuckBanner.setVisible(false);
|
||||||
|
this.newGameBtn.setVisible(false);
|
||||||
|
if (this.pulse) { this.pulse.stop(); this.pulse = null; }
|
||||||
|
this.engine = new FreecellEngine(freshDeck());
|
||||||
|
playSound(this, SFX.CARD_SHUFFLE);
|
||||||
|
this.refresh();
|
||||||
|
this.dealAnimation();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── card rendering helpers (same as SolitaireTourGame) ────────────────────
|
||||||
|
|
||||||
|
drawFace(container, card, faceUp, selected, hint) {
|
||||||
|
const x = -CARD_W / 2, y = -CARD_H / 2;
|
||||||
|
const g = this.add.graphics();
|
||||||
|
if (!faceUp) {
|
||||||
|
g.fillStyle(0x16324f, 1); g.fillRoundedRect(x, y, CARD_W, CARD_H, CARD_R);
|
||||||
|
g.lineStyle(3, COLORS.accent, 0.7); g.strokeRoundedRect(x + 5, y + 5, CARD_W - 10, CARD_H - 10, CARD_R - 2);
|
||||||
|
container.add(g);
|
||||||
|
container.add(this.add.text(0, 0, '✦', { fontFamily: 'serif', fontSize: '40px', color: '#caa84b' }).setOrigin(0.5).setAlpha(0.55));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
g.fillStyle(0xfbf6e7, 1); g.fillRoundedRect(x, y, CARD_W, CARD_H, CARD_R);
|
||||||
|
const rimColor = selected ? SEL : hint ? HINT : 0xcc803a;
|
||||||
|
const rimW = selected ? 5 : hint ? 4 : 2;
|
||||||
|
g.lineStyle(rimW, rimColor, selected || hint ? 1 : 0.5);
|
||||||
|
g.strokeRoundedRect(x + 2, y + 2, CARD_W - 4, CARD_H - 4, CARD_R - 1);
|
||||||
|
container.add(g);
|
||||||
|
|
||||||
|
const col = card.isRed ? '#c0392b' : '#1a1208';
|
||||||
|
container.add(this.add.text(x + 9, y + 6, card.label, { fontFamily: 'Righteous', fontSize: '24px', color: col }));
|
||||||
|
container.add(this.add.text(x + 11, y + 35, card.suitSymbol, { fontFamily: 'sans-serif', fontSize: '22px', color: col }));
|
||||||
|
container.add(this.add.text(0, 6, card.suitSymbol, { fontFamily: 'sans-serif', fontSize: '48px', color: col }).setOrigin(0.5));
|
||||||
|
container.add(this.add.text(x + CARD_W - 9, y + CARD_H - 6, card.label, { fontFamily: 'Righteous', fontSize: '24px', color: col }).setOrigin(1, 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
card(cardObj, x, y, opts = {}) {
|
||||||
|
const { faceUp = true, selected = false, hint = false, dim = false, onClick = null, onDown = null } = opts;
|
||||||
|
const c = this.add.container(x, selected ? y - 10 : y);
|
||||||
|
c.isCard = true;
|
||||||
|
this.drawFace(c, cardObj, faceUp, selected, hint);
|
||||||
|
if (dim) c.setAlpha(0.4);
|
||||||
|
if (onClick || onDown) {
|
||||||
|
c.setInteractive(new Phaser.Geom.Rectangle(-CARD_W / 2, -CARD_H / 2, CARD_W, CARD_H), Phaser.Geom.Rectangle.Contains);
|
||||||
|
c.input.cursor = onDown ? 'grab' : 'pointer';
|
||||||
|
if (onClick) c.on('pointerdown', onClick);
|
||||||
|
if (onDown) c.on('pointerdown', (pointer) => onDown(c, pointer));
|
||||||
|
}
|
||||||
|
this.board.add(c);
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
|
||||||
|
slot(x, y, label, onClick = null) {
|
||||||
|
const c = this.add.container(x, y);
|
||||||
|
const g = this.add.graphics();
|
||||||
|
g.lineStyle(2, COLORS.accent, 0.4);
|
||||||
|
g.strokeRoundedRect(-CARD_W / 2, -CARD_H / 2, CARD_W, CARD_H, CARD_R);
|
||||||
|
c.add(g);
|
||||||
|
if (label) c.add(this.add.text(0, 0, label, { fontFamily: '"Julius Sans One"', fontSize: '15px', color: COLORS.mutedHex, align: 'center' }).setOrigin(0.5));
|
||||||
|
if (onClick) {
|
||||||
|
c.setInteractive(new Phaser.Geom.Rectangle(-CARD_W / 2, -CARD_H / 2, CARD_W, CARD_H), Phaser.Geom.Rectangle.Contains);
|
||||||
|
c.input.cursor = 'pointer';
|
||||||
|
c.on('pointerdown', onClick);
|
||||||
|
}
|
||||||
|
this.board.add(c);
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── board rendering ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
renderBoard() {
|
||||||
|
this.board.removeAll(true);
|
||||||
|
this.cardSprites.clear();
|
||||||
|
this.dropZones = [];
|
||||||
|
|
||||||
|
const e = this.engine;
|
||||||
|
|
||||||
|
// Labels
|
||||||
|
this.board.add(this.add.text(colX(0), FC_Y - CARD_H / 2 - 18, 'Free Cells', {
|
||||||
|
fontFamily: '"Julius Sans One"', fontSize: '15px', color: COLORS.mutedHex,
|
||||||
|
}).setOrigin(0.5, 1));
|
||||||
|
this.board.add(this.add.text(colX(4) + (colX(7) - colX(4)) / 2, HOME_Y - CARD_H / 2 - 18, 'Foundations', {
|
||||||
|
fontFamily: '"Julius Sans One"', fontSize: '15px', color: COLORS.mutedHex,
|
||||||
|
}).setOrigin(0.5, 1));
|
||||||
|
|
||||||
|
// Freecells (cols 0-3)
|
||||||
|
for (let fc = 0; fc < 4; fc++) {
|
||||||
|
const x = colX(fc);
|
||||||
|
const card = e.freecells[fc];
|
||||||
|
const isSel = this.sel?.kind === 'freecell' && this.sel.fcIdx === fc;
|
||||||
|
this.dropZones.push({ cx: x, cy: FC_Y, hw: CARD_W * 0.7, hh: CARD_H * 0.8, hlx: x, hly: FC_Y, kind: 'freecell', idx: fc });
|
||||||
|
if (card) {
|
||||||
|
const sprite = this.card(card, x, FC_Y, {
|
||||||
|
selected: isSel,
|
||||||
|
onDown: (_c, pointer) => this.onCardDown({ kind: 'freecell', fcIdx: fc }, pointer),
|
||||||
|
});
|
||||||
|
this.cardSprites.set(card.id, sprite);
|
||||||
|
} else {
|
||||||
|
this.slot(x, FC_Y, 'Free', () => this.onSlotClick('freecell', fc));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Foundations (cols 4-7)
|
||||||
|
for (let i = 0; i < 4; i++) {
|
||||||
|
const x = colX(4 + i);
|
||||||
|
const suit = SUITS_ORDER[i];
|
||||||
|
const pile = e.foundations[suit];
|
||||||
|
const top = pile[pile.length - 1];
|
||||||
|
this.dropZones.push({ cx: x, cy: HOME_Y, hw: CARD_W * 0.7, hh: CARD_H * 0.8, hlx: x, hly: HOME_Y, kind: 'foundation', suit });
|
||||||
|
if (top) {
|
||||||
|
this.card(top, x, HOME_Y, { onClick: () => this.onSlotClick('foundation', i) });
|
||||||
|
} else {
|
||||||
|
this.slot(x, HOME_Y, SUIT_GLYPH[suit], () => this.onSlotClick('foundation', i));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tableau (cols 0-7)
|
||||||
|
for (let c = 0; c < 8; c++) {
|
||||||
|
const pile = e.tableau[c];
|
||||||
|
const x = colX(c);
|
||||||
|
const colH = Math.max(CARD_H, pile.length * FAN + CARD_H);
|
||||||
|
const midY = TAB_Y + (pile.length * FAN) / 2;
|
||||||
|
this.dropZones.push({ cx: x, cy: midY, hw: CARD_W * 0.7, hh: colH / 2 + 20, hlx: x, hly: TAB_Y + pile.length * FAN, kind: 'column', idx: c });
|
||||||
|
if (!pile.length) {
|
||||||
|
this.slot(x, TAB_Y, '', () => this.onSlotClick('column', c));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
pile.forEach((cardObj, idx) => {
|
||||||
|
const isTop = idx === pile.length - 1;
|
||||||
|
const inSel = this.sel?.kind === 'tableau' && this.sel.col === c && idx >= this.sel.fromIdx;
|
||||||
|
const isHint = isTop && e.canMoveToFoundation(cardObj);
|
||||||
|
// A card is draggable if the entire run from here passes _validRun
|
||||||
|
const draggable = e._validRun(c, idx) !== null;
|
||||||
|
const sprite = this.card(cardObj, x, TAB_Y + idx * FAN, {
|
||||||
|
selected: inSel,
|
||||||
|
hint: isHint && !inSel,
|
||||||
|
onDown: draggable ? (_co, pointer) => this.onCardDown({ kind: 'tableau', col: c, fromIdx: idx }, pointer) : null,
|
||||||
|
onClick: draggable ? null : null, // non-draggable interior cards are inaccessible
|
||||||
|
});
|
||||||
|
this.cardSprites.set(cardObj.id, sprite);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── interaction ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
interactive() { return !this.animating && !this.autoRunning && !this.overlayUp; }
|
||||||
|
|
||||||
|
onCardDown(descriptor, pointer) {
|
||||||
|
if (!this.interactive()) return;
|
||||||
|
this.beginDrag(
|
||||||
|
this.dragSpritesFor(descriptor),
|
||||||
|
pointer,
|
||||||
|
() => this.onTap(descriptor),
|
||||||
|
(x, y) => this.resolveDrop(descriptor, x, y),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
dragSpritesFor(descriptor) {
|
||||||
|
if (descriptor.kind === 'freecell') {
|
||||||
|
const card = this.engine.freecells[descriptor.fcIdx];
|
||||||
|
const o = card && this.cardSprites.get(card.id);
|
||||||
|
return o ? [o] : [];
|
||||||
|
}
|
||||||
|
const run = this.engine._validRun(descriptor.col, descriptor.fromIdx);
|
||||||
|
if (!run) return [];
|
||||||
|
return run.map((c) => this.cardSprites.get(c.id)).filter(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
|
onTap(descriptor) {
|
||||||
|
if (!this.interactive()) return;
|
||||||
|
if (this.sel) {
|
||||||
|
const sameSpot = this.sel.kind === descriptor.kind &&
|
||||||
|
(descriptor.kind === 'freecell' ? this.sel.fcIdx === descriptor.fcIdx
|
||||||
|
: this.sel.col === descriptor.col && this.sel.fromIdx === descriptor.fromIdx);
|
||||||
|
if (sameSpot) { this.sel = null; this.refresh(); return; }
|
||||||
|
// Try to complete a move from sel → descriptor location
|
||||||
|
if (this.commitMove(descriptor)) return;
|
||||||
|
}
|
||||||
|
this.sel = descriptor;
|
||||||
|
this.refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
onSlotClick(kind, idx) {
|
||||||
|
if (!this.interactive() || !this.sel) return;
|
||||||
|
if (kind === 'foundation') {
|
||||||
|
this.commitToFoundation(this.sel);
|
||||||
|
} else if (kind === 'freecell') {
|
||||||
|
if (this.sel.kind === 'tableau') {
|
||||||
|
const col = this.sel.col;
|
||||||
|
const pile = this.engine.tableau[col];
|
||||||
|
const isTop = this.sel.fromIdx === pile.length - 1;
|
||||||
|
if (isTop && this.applyMove(this.engine.moveToFreecell(col))) return;
|
||||||
|
}
|
||||||
|
} else if (kind === 'column') {
|
||||||
|
this.commitToColumn(this.sel, idx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to move sel → the tapped card's location (used when tapping a card while another is selected)
|
||||||
|
commitMove(descriptor) {
|
||||||
|
const e = this.engine;
|
||||||
|
// Try foundation
|
||||||
|
if (this.commitToFoundation(this.sel)) return true;
|
||||||
|
// Try freecell (only top tableau card)
|
||||||
|
if (descriptor.kind === 'freecell' && this.sel.kind === 'tableau') {
|
||||||
|
const isTop = this.sel.fromIdx === e.tableau[this.sel.col].length - 1;
|
||||||
|
if (isTop && this.applyMove(e.moveToFreecell(this.sel.col))) return true;
|
||||||
|
}
|
||||||
|
// Try column (tapped on a card in destCol → use that col as destination)
|
||||||
|
if (descriptor.kind === 'tableau' && descriptor.col !== (this.sel.col ?? -1)) {
|
||||||
|
if (this.commitToColumn(this.sel, descriptor.col)) return true;
|
||||||
|
}
|
||||||
|
if (descriptor.kind === 'freecell' && this.sel.kind === 'freecell') {
|
||||||
|
// freecell → freecell not a standard move; ignore
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
commitToFoundation(sel) {
|
||||||
|
let ok = false;
|
||||||
|
if (sel.kind === 'tableau') {
|
||||||
|
const pile = this.engine.tableau[sel.col];
|
||||||
|
if (sel.fromIdx === pile.length - 1) ok = this.engine.moveToFoundation('tableau', sel.col);
|
||||||
|
} else {
|
||||||
|
ok = this.engine.moveFromFreecellToFoundation(sel.fcIdx);
|
||||||
|
}
|
||||||
|
if (ok) { this.sel = null; this.applyMove(true); return true; }
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
commitToColumn(sel, destCol) {
|
||||||
|
let ok = false;
|
||||||
|
if (sel.kind === 'freecell') {
|
||||||
|
ok = this.engine.moveFromFreecellToColumn(sel.fcIdx, destCol);
|
||||||
|
} else {
|
||||||
|
ok = this.engine.moveRun(sel.col, sel.fromIdx, destCol);
|
||||||
|
}
|
||||||
|
if (ok) { this.sel = null; this.applyMove(true); return true; }
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
resolveDrop(descriptor, x, y) {
|
||||||
|
const z = this.dropZones.find((zone) => Math.abs(x - zone.cx) < zone.hw && Math.abs(y - zone.cy) < zone.hh);
|
||||||
|
if (!z) return null;
|
||||||
|
const pos = { x: z.hlx, y: z.hly };
|
||||||
|
const e = this.engine;
|
||||||
|
|
||||||
|
if (z.kind === 'foundation') {
|
||||||
|
if (descriptor.kind === 'tableau') {
|
||||||
|
const pile = e.tableau[descriptor.col];
|
||||||
|
if (descriptor.fromIdx !== pile.length - 1) return null;
|
||||||
|
if (!e.canMoveToFoundation(pile[descriptor.fromIdx])) return null;
|
||||||
|
} else {
|
||||||
|
const card = e.freecells[descriptor.fcIdx];
|
||||||
|
if (!card || !e.canMoveToFoundation(card)) return null;
|
||||||
|
}
|
||||||
|
const commit = () => {
|
||||||
|
if (descriptor.kind === 'tableau') return e.moveToFoundation('tableau', descriptor.col);
|
||||||
|
return e.moveFromFreecellToFoundation(descriptor.fcIdx);
|
||||||
|
};
|
||||||
|
return { pos, color: 0xffd700, commit };
|
||||||
|
}
|
||||||
|
if (z.kind === 'freecell') {
|
||||||
|
if (descriptor.kind !== 'tableau') return null;
|
||||||
|
const pile = e.tableau[descriptor.col];
|
||||||
|
if (descriptor.fromIdx !== pile.length - 1) return null;
|
||||||
|
if (e._emptyFreecells() === 0) return null;
|
||||||
|
const commit = () => e.moveToFreecell(descriptor.col);
|
||||||
|
return { pos, color: HINT, commit };
|
||||||
|
}
|
||||||
|
if (z.kind === 'column') {
|
||||||
|
if (descriptor.kind === 'tableau') {
|
||||||
|
if (!e.canMoveRun(descriptor.col, descriptor.fromIdx, z.idx)) return null;
|
||||||
|
} else {
|
||||||
|
const card = e.freecells[descriptor.fcIdx];
|
||||||
|
if (!card || !e.canMoveToColumn(card, z.idx)) return null;
|
||||||
|
}
|
||||||
|
const commit = () => {
|
||||||
|
if (descriptor.kind === 'freecell') return e.moveFromFreecellToColumn(descriptor.fcIdx, z.idx);
|
||||||
|
return e.moveRun(descriptor.col, descriptor.fromIdx, z.idx);
|
||||||
|
};
|
||||||
|
return { pos, color: SEL, commit };
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
applyMove(ok, sfx = SFX.CARD_PLACE) {
|
||||||
|
if (ok) { playSound(this, sfx); this.refresh(); }
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
refresh() {
|
||||||
|
this.renderBoard();
|
||||||
|
this.updateHud();
|
||||||
|
if (this.engine.isWon()) { this.endGame(); return; }
|
||||||
|
this.updateAutoComplete();
|
||||||
|
this.updateStuck();
|
||||||
|
}
|
||||||
|
|
||||||
|
updateHud() {
|
||||||
|
this.movesText.setText(`Moves: ${this.engine.moveCount}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateAutoComplete() {
|
||||||
|
const show = !this.animating && !this.autoRunning && !this.overlayUp && this.engine.canAutoComplete() && !this.engine.isWon();
|
||||||
|
this.autoCompleteBtn.setVisible(show);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateStuck() {
|
||||||
|
const stuck = this.interactive() && !this.engine.hasMoves() && !this.engine.isWon();
|
||||||
|
this.stuckBanner.setVisible(stuck);
|
||||||
|
this.newGameBtn.setVisible(stuck);
|
||||||
|
if (stuck && !this.pulse) {
|
||||||
|
this.pulse = this.tweens.add({ targets: this.newGameBtn, scaleX: 1.06, scaleY: 1.06, yoyo: true, repeat: -1, duration: 520, ease: 'Sine.easeInOut' });
|
||||||
|
} else if (!stuck && this.pulse) {
|
||||||
|
this.pulse.stop(); this.pulse = null; this.newGameBtn.setScale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── auto-complete ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
onAutoComplete() {
|
||||||
|
if (!this.interactive()) return;
|
||||||
|
this.autoRunning = true;
|
||||||
|
this.animating = true;
|
||||||
|
this.sel = null;
|
||||||
|
this.autoCompleteBtn.setVisible(false);
|
||||||
|
this.doAutoStep();
|
||||||
|
}
|
||||||
|
|
||||||
|
doAutoStep() {
|
||||||
|
const next = this.engine.autoCompleteStep();
|
||||||
|
if (!next) {
|
||||||
|
this.autoRunning = false;
|
||||||
|
this.animating = false;
|
||||||
|
this.refresh();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const suitIdx = SUITS_ORDER.indexOf(next.card.suit);
|
||||||
|
const toX = colX(4 + suitIdx);
|
||||||
|
const toY = HOME_Y;
|
||||||
|
|
||||||
|
const src = this.cardSprites.get(next.card.id);
|
||||||
|
const fromX = src ? src.x : GAME_WIDTH / 2;
|
||||||
|
const fromY = src ? src.y : GAME_HEIGHT / 2;
|
||||||
|
if (src) src.setVisible(false);
|
||||||
|
|
||||||
|
this.engine.moveToFoundation(next.srcKind, next.srcIdx);
|
||||||
|
|
||||||
|
const fly = this.add.container(fromX, fromY).setDepth(D.banner);
|
||||||
|
this.drawFace(fly, next.card, true, false, false);
|
||||||
|
this.tweens.add({
|
||||||
|
targets: fly, x: toX, y: toY, duration: 110, ease: 'Quad.easeIn',
|
||||||
|
onComplete: () => {
|
||||||
|
fly.destroy();
|
||||||
|
playSound(this, SFX.CARD_PLACE);
|
||||||
|
this.renderBoard();
|
||||||
|
this.updateHud();
|
||||||
|
if (this.engine.isWon()) { this.autoRunning = false; this.animating = false; this.endGame(); return; }
|
||||||
|
this.doAutoStep();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── deal animation (same as SolitaireTourGame) ────────────────────────────
|
||||||
|
|
||||||
|
dealAnimation() {
|
||||||
|
const cards = this.board.list.filter((o) => o.isCard);
|
||||||
|
if (!cards.length) return;
|
||||||
|
const deckX = GAME_WIDTH / 2, deckY = GAME_HEIGHT + 90;
|
||||||
|
const homes = cards.map((c) => ({ c, x: c.x, y: c.y, alpha: c.alpha }));
|
||||||
|
this.animating = true;
|
||||||
|
for (const { c } of homes) { c.setPosition(deckX, deckY).setScale(0.7).setAlpha(0); }
|
||||||
|
homes.forEach(({ c, x, y, alpha }, i) => {
|
||||||
|
const last = i === homes.length - 1;
|
||||||
|
this.tweens.add({
|
||||||
|
targets: c, x, y, scaleX: 1, scaleY: 1, alpha,
|
||||||
|
delay: i * 12, duration: 200, ease: 'Quad.easeOut',
|
||||||
|
onComplete: last ? () => { this.animating = false; this.updateStuck(); } : undefined,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── drag-and-drop (verbatim from SolitaireTourGame) ───────────────────────
|
||||||
|
|
||||||
|
setupDrag() {
|
||||||
|
this.input.on('pointermove', (pointer) => {
|
||||||
|
if (!pointer.isDown) return;
|
||||||
|
if (this.dragState) {
|
||||||
|
this.dragUpdate(pointer);
|
||||||
|
} else if (this.potentialDrag) {
|
||||||
|
const dx = pointer.x - this.potentialDrag.startX;
|
||||||
|
const dy = pointer.y - this.potentialDrag.startY;
|
||||||
|
if (dx * dx + dy * dy > 80) this.dragPromote();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.input.on('pointerup', () => {
|
||||||
|
if (this.dragState) this.dragEnd();
|
||||||
|
else if (this.potentialDrag) {
|
||||||
|
const pd = this.potentialDrag;
|
||||||
|
this.potentialDrag = null;
|
||||||
|
pd.tap();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
beginDrag(objs, pointer, tap, resolve) {
|
||||||
|
if (!this.interactive() || this.dragState || !objs.length) return;
|
||||||
|
this.potentialDrag = {
|
||||||
|
startX: pointer.x, startY: pointer.y, tap, resolve,
|
||||||
|
sprites: objs.map((obj) => ({ obj, offX: obj.x - pointer.x, offY: obj.y - pointer.y, homeX: obj.x, homeY: obj.y })),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
dragPromote() {
|
||||||
|
const pd = this.potentialDrag;
|
||||||
|
this.potentialDrag = null;
|
||||||
|
pd.sprites.forEach(({ obj }) => {
|
||||||
|
this.board.bringToTop(obj);
|
||||||
|
this.tweens.add({ targets: obj, scaleX: 1.05, scaleY: 1.05, duration: 90 });
|
||||||
|
});
|
||||||
|
this.dragState = pd;
|
||||||
|
}
|
||||||
|
|
||||||
|
dragUpdate(pointer) {
|
||||||
|
for (const { obj, offX, offY } of this.dragState.sprites) {
|
||||||
|
obj.x = pointer.x + offX;
|
||||||
|
obj.y = pointer.y + offY;
|
||||||
|
}
|
||||||
|
const primary = this.dragState.sprites[0].obj;
|
||||||
|
this.highlightDrop(this.dragState.resolve(primary.x, primary.y));
|
||||||
|
}
|
||||||
|
|
||||||
|
highlightDrop(target) {
|
||||||
|
if (this.dropHighlight) { this.dropHighlight.destroy(); this.dropHighlight = null; }
|
||||||
|
if (!target) return;
|
||||||
|
this.dropHighlight = this.add.rectangle(target.pos.x, target.pos.y, CARD_W + 16, CARD_H + 16, target.color, 0.18)
|
||||||
|
.setStrokeStyle(3, target.color, 0.9).setDepth(D.card - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
dragEnd() {
|
||||||
|
const ds = this.dragState;
|
||||||
|
this.dragState = null;
|
||||||
|
if (this.dropHighlight) { this.dropHighlight.destroy(); this.dropHighlight = null; }
|
||||||
|
|
||||||
|
const primary = ds.sprites[0].obj;
|
||||||
|
const target = ds.resolve(primary.x, primary.y);
|
||||||
|
if (target && target.commit()) { this.sel = null; this.applyMove(true); return; }
|
||||||
|
ds.sprites.forEach(({ obj, homeX, homeY }) => {
|
||||||
|
this.tweens.add({ targets: obj, x: homeX, y: homeY, scaleX: 1, scaleY: 1, duration: 220, ease: 'Back.easeOut' });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── win / end ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
endGame() {
|
||||||
|
this.animating = false;
|
||||||
|
this.autoRunning = false;
|
||||||
|
this.sel = null;
|
||||||
|
this.stuckBanner.setVisible(false);
|
||||||
|
this.newGameBtn.setVisible(false);
|
||||||
|
this.autoCompleteBtn.setVisible(false);
|
||||||
|
if (this.pulse) { this.pulse.stop(); this.pulse = null; }
|
||||||
|
playSound(this, SFX.CASINO_WIN);
|
||||||
|
this.recordResult();
|
||||||
|
this.showWinOverlay();
|
||||||
|
}
|
||||||
|
|
||||||
|
showWinOverlay() {
|
||||||
|
this.overlayUp = true;
|
||||||
|
const cx = GAME_WIDTH / 2, cy = GAME_HEIGHT / 2;
|
||||||
|
const dim = this.add.rectangle(cx, cy, GAME_WIDTH, GAME_HEIGHT, 0x000000, 0.62).setDepth(D.overlay).setInteractive();
|
||||||
|
const g = this.add.graphics().setDepth(D.overlay);
|
||||||
|
g.fillStyle(COLORS.panel, 1); g.fillRoundedRect(cx - 380, cy - 200, 760, 400, 20);
|
||||||
|
g.lineStyle(3, COLORS.accent, 1); g.strokeRoundedRect(cx - 380, cy - 200, 760, 400, 20);
|
||||||
|
this.overlayObjs.push(dim, g);
|
||||||
|
|
||||||
|
this.overlayObjs.push(this.add.text(cx, cy - 130, 'You Win!', {
|
||||||
|
fontFamily: 'Righteous', fontSize: '62px', color: '#5fd29a',
|
||||||
|
}).setOrigin(0.5).setDepth(D.overlayUI));
|
||||||
|
|
||||||
|
this.overlayObjs.push(this.add.text(cx, cy - 30, `Completed in ${this.engine.moveCount} moves`, {
|
||||||
|
fontFamily: '"Julius Sans One"', fontSize: '28px', color: COLORS.textHex,
|
||||||
|
}).setOrigin(0.5).setDepth(D.overlayUI));
|
||||||
|
|
||||||
|
const again = new Button(this, cx - 130, cy + 110, 'Play Again', () => {
|
||||||
|
this.clearOverlay(); this.overlayUp = false; this.startGame();
|
||||||
|
}, { width: 220, height: 60, fontSize: 24 });
|
||||||
|
again.setDepth(D.overlayUI);
|
||||||
|
|
||||||
|
const leave = new Button(this, cx + 130, cy + 110, 'Leave', () => this.scene.start('GameMenu'),
|
||||||
|
{ variant: 'ghost', width: 220, height: 60, fontSize: 24 });
|
||||||
|
leave.setDepth(D.overlayUI);
|
||||||
|
|
||||||
|
this.overlayObjs.push(again, leave);
|
||||||
|
}
|
||||||
|
|
||||||
|
clearOverlay() {
|
||||||
|
for (const o of this.overlayObjs) o.destroy();
|
||||||
|
this.overlayObjs = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
recordResult() {
|
||||||
|
if (this.recorded) return;
|
||||||
|
this.recorded = true;
|
||||||
|
api.post('/history/single-player', {
|
||||||
|
slug: 'freecell', score: this.engine.moveCount, opponentScores: [], result: 'win',
|
||||||
|
}).catch(() => { /* best effort */ });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,218 @@
|
||||||
|
import { Card, SUITS, RANKS } from '../cards/Deck.js';
|
||||||
|
|
||||||
|
const PVAL = { A: 1, T: 10, J: 11, Q: 12, K: 13 };
|
||||||
|
for (let n = 2; n <= 9; n++) PVAL[String(n)] = n;
|
||||||
|
|
||||||
|
export function freshDeck() {
|
||||||
|
const cards = [];
|
||||||
|
let id = 0;
|
||||||
|
for (const suit of SUITS) {
|
||||||
|
for (const rank of RANKS) {
|
||||||
|
const c = new Card(rank, suit);
|
||||||
|
c.id = id++;
|
||||||
|
c.pval = PVAL[rank];
|
||||||
|
cards.push(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (let i = cards.length - 1; i > 0; i--) {
|
||||||
|
const j = Math.floor(Math.random() * (i + 1));
|
||||||
|
[cards[i], cards[j]] = [cards[j], cards[i]];
|
||||||
|
}
|
||||||
|
return cards;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class FreecellEngine {
|
||||||
|
constructor(deck) {
|
||||||
|
this.tableau = Array.from({ length: 8 }, () => []);
|
||||||
|
this.freecells = [null, null, null, null];
|
||||||
|
this.foundations = { s: [], h: [], d: [], c: [] };
|
||||||
|
this.moveCount = 0;
|
||||||
|
|
||||||
|
// Deal: cols 0-3 get 7 cards, cols 4-7 get 6 cards (round-robin)
|
||||||
|
let k = 0;
|
||||||
|
for (let row = 0; row < 7; row++) {
|
||||||
|
const cols = row < 6 ? 8 : 4; // first 6 rows fill all 8; row 6 fills only cols 0-3
|
||||||
|
for (let c = 0; c < cols; c++) this.tableau[c].push(deck[k++]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── internal helpers ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
_foundationNext(suit) {
|
||||||
|
const pile = this.foundations[suit];
|
||||||
|
return pile.length === 0 ? 1 : pile[pile.length - 1].pval + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
_emptyFreecells() {
|
||||||
|
return this.freecells.filter((s) => s === null).length;
|
||||||
|
}
|
||||||
|
|
||||||
|
_emptyColumns() {
|
||||||
|
return this.tableau.filter((col) => col.length === 0).length;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the valid alternating-color descending run from fromIdx to bottom,
|
||||||
|
// or null if it doesn't form one.
|
||||||
|
_validRun(col, fromIdx) {
|
||||||
|
const pile = this.tableau[col];
|
||||||
|
if (fromIdx < 0 || fromIdx >= pile.length) return null;
|
||||||
|
const run = [pile[fromIdx]];
|
||||||
|
for (let i = fromIdx + 1; i < pile.length; i++) {
|
||||||
|
const prev = pile[i - 1], cur = pile[i];
|
||||||
|
if (cur.isRed === prev.isRed || cur.pval !== prev.pval - 1) return null;
|
||||||
|
run.push(cur);
|
||||||
|
}
|
||||||
|
return run;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── move validation ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
canMoveToFoundation(card) {
|
||||||
|
return card.pval === this._foundationNext(card.suit);
|
||||||
|
}
|
||||||
|
|
||||||
|
canMoveToColumn(card, destCol) {
|
||||||
|
const dest = this.tableau[destCol];
|
||||||
|
if (dest.length === 0) return true;
|
||||||
|
const top = dest[dest.length - 1];
|
||||||
|
return card.isRed !== top.isRed && card.pval === top.pval - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
canMoveRun(srcCol, fromIdx, destCol) {
|
||||||
|
if (srcCol === destCol) return false;
|
||||||
|
const run = this._validRun(srcCol, fromIdx);
|
||||||
|
if (!run) return false;
|
||||||
|
return this.canMoveToColumn(run[0], destCol);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── mutations (all return boolean) ────────────────────────────────────────
|
||||||
|
|
||||||
|
moveToFoundation(srcKind, srcIdx) {
|
||||||
|
let card;
|
||||||
|
if (srcKind === 'tableau') {
|
||||||
|
const pile = this.tableau[srcIdx];
|
||||||
|
if (!pile.length) return false;
|
||||||
|
card = pile[pile.length - 1];
|
||||||
|
} else {
|
||||||
|
card = this.freecells[srcIdx];
|
||||||
|
if (!card) return false;
|
||||||
|
}
|
||||||
|
if (!this.canMoveToFoundation(card)) return false;
|
||||||
|
this.foundations[card.suit].push(card);
|
||||||
|
if (srcKind === 'tableau') this.tableau[srcIdx].pop();
|
||||||
|
else this.freecells[srcIdx] = null;
|
||||||
|
this.moveCount++;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
moveToFreecell(srcCol) {
|
||||||
|
const pile = this.tableau[srcCol];
|
||||||
|
if (!pile.length) return false;
|
||||||
|
const fcIdx = this.freecells.indexOf(null);
|
||||||
|
if (fcIdx === -1) return false;
|
||||||
|
this.freecells[fcIdx] = pile.pop();
|
||||||
|
this.moveCount++;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
moveFromFreecellToFoundation(fcIdx) {
|
||||||
|
return this.moveToFoundation('freecell', fcIdx);
|
||||||
|
}
|
||||||
|
|
||||||
|
moveFromFreecellToColumn(fcIdx, destCol) {
|
||||||
|
const card = this.freecells[fcIdx];
|
||||||
|
if (!card) return false;
|
||||||
|
if (!this.canMoveToColumn(card, destCol)) return false;
|
||||||
|
this.tableau[destCol].push(card);
|
||||||
|
this.freecells[fcIdx] = null;
|
||||||
|
this.moveCount++;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
moveRun(srcCol, fromIdx, destCol) {
|
||||||
|
if (!this.canMoveRun(srcCol, fromIdx, destCol)) return false;
|
||||||
|
const run = this.tableau[srcCol].splice(fromIdx);
|
||||||
|
this.tableau[destCol].push(...run);
|
||||||
|
this.moveCount++;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── queries ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
hasMoves() {
|
||||||
|
const allCards = [
|
||||||
|
...this.freecells.filter(Boolean),
|
||||||
|
...this.tableau.map((col) => col[col.length - 1]).filter(Boolean),
|
||||||
|
];
|
||||||
|
// Any card to foundation?
|
||||||
|
if (allCards.some((c) => this.canMoveToFoundation(c))) return true;
|
||||||
|
// Any card to freecell?
|
||||||
|
if (this._emptyFreecells() > 0 && this.tableau.some((col) => col.length > 0)) return true;
|
||||||
|
// Any card/run to any column?
|
||||||
|
for (let dest = 0; dest < 8; dest++) {
|
||||||
|
for (const card of allCards) {
|
||||||
|
if (this.canMoveToColumn(card, dest)) return true;
|
||||||
|
}
|
||||||
|
// Runs from tableau to any column
|
||||||
|
for (let src = 0; src < 8; src++) {
|
||||||
|
if (src === dest) continue;
|
||||||
|
const pile = this.tableau[src];
|
||||||
|
for (let idx = 0; idx < pile.length; idx++) {
|
||||||
|
if (this.canMoveRun(src, idx, dest)) return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
isWon() {
|
||||||
|
return SUITS.every((s) => this.foundations[s].length === 13);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simulate foundation-only play; returns true if all 52 cards would clear.
|
||||||
|
canAutoComplete() {
|
||||||
|
const need = {};
|
||||||
|
for (const s of SUITS) need[s] = this._foundationNext(s);
|
||||||
|
const cols = this.tableau.map((col) => col.slice());
|
||||||
|
const fcs = this.freecells.slice();
|
||||||
|
let remaining = cols.reduce((t, c) => t + c.length, 0) + fcs.filter(Boolean).length;
|
||||||
|
if (remaining === 0) return true;
|
||||||
|
let moved = true;
|
||||||
|
while (remaining > 0 && moved) {
|
||||||
|
moved = false;
|
||||||
|
for (let i = 0; i < 4; i++) {
|
||||||
|
if (fcs[i] && fcs[i].pval === need[fcs[i].suit]) {
|
||||||
|
need[fcs[i].suit]++;
|
||||||
|
fcs[i] = null;
|
||||||
|
remaining--;
|
||||||
|
moved = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const col of cols) {
|
||||||
|
const top = col[col.length - 1];
|
||||||
|
if (top && top.pval === need[top.suit]) {
|
||||||
|
col.pop();
|
||||||
|
need[top.suit]++;
|
||||||
|
remaining--;
|
||||||
|
moved = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return remaining === 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the next card that can go to a foundation, or null.
|
||||||
|
// Checks freecells first, then tableau tops.
|
||||||
|
autoCompleteStep() {
|
||||||
|
for (let i = 0; i < 4; i++) {
|
||||||
|
const card = this.freecells[i];
|
||||||
|
if (card && this.canMoveToFoundation(card)) return { card, srcKind: 'freecell', srcIdx: i };
|
||||||
|
}
|
||||||
|
for (let c = 0; c < 8; c++) {
|
||||||
|
const pile = this.tableau[c];
|
||||||
|
const top = pile[pile.length - 1];
|
||||||
|
if (top && this.canMoveToFoundation(top)) return { card: top, srcKind: 'tableau', srcIdx: c };
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -351,13 +351,14 @@ export default class TriominoesGame extends Phaser.Scene {
|
||||||
const startX = CX - ((hand.length - 1) * pitch) / 2;
|
const startX = CX - ((hand.length - 1) * pitch) / 2;
|
||||||
const pts = this.handPts();
|
const pts = this.handPts();
|
||||||
|
|
||||||
|
const anyPlayable = legalTiles.size > 0;
|
||||||
hand.forEach((tile, idx) => {
|
hand.forEach((tile, idx) => {
|
||||||
const x = startX + idx * pitch;
|
const x = startX + idx * pitch;
|
||||||
const playable = humanTurn && legalTiles.has(idx);
|
const playable = humanTurn && legalTiles.has(idx);
|
||||||
const container = this.add.container(x, HAND_Y).setDepth(DEPTH.hand);
|
const container = this.add.container(x, HAND_Y).setDepth(DEPTH.hand);
|
||||||
const border = playable ? COLORS.accent : COLORS.muted;
|
const border = playable ? COLORS.accent : COLORS.muted;
|
||||||
this.paintTriangle(container, pts, tile.v, { fill: COLORS.text, border, lineW: 3, fontSize: 30 });
|
this.paintTriangle(container, pts, tile.v, { fill: COLORS.text, border, lineW: 3, fontSize: 30 });
|
||||||
container.setAlpha(humanTurn ? (playable ? 1 : 0.42) : 0.7);
|
container.setAlpha(humanTurn ? (anyPlayable ? 1 : 0.42) : 0.7);
|
||||||
|
|
||||||
container._tileIndex = idx;
|
container._tileIndex = idx;
|
||||||
container._homeX = x;
|
container._homeX = x;
|
||||||
|
|
|
||||||
|
|
@ -60,6 +60,7 @@ import StrategoGame from './games/stratego/StrategoGame.js';
|
||||||
import KiitosGame from './games/kiitos/KiitosGame.js';
|
import KiitosGame from './games/kiitos/KiitosGame.js';
|
||||||
import MonopolyGame from './games/monopoly/MonopolyGame.js';
|
import MonopolyGame from './games/monopoly/MonopolyGame.js';
|
||||||
import TriominoesGame from './games/triominoes/TriominoesGame.js';
|
import TriominoesGame from './games/triominoes/TriominoesGame.js';
|
||||||
|
import FreecellGame from './games/freecell/FreecellGame.js';
|
||||||
|
|
||||||
const config = {
|
const config = {
|
||||||
type: Phaser.AUTO,
|
type: Phaser.AUTO,
|
||||||
|
|
@ -133,6 +134,7 @@ const config = {
|
||||||
KiitosGame,
|
KiitosGame,
|
||||||
MonopolyGame,
|
MonopolyGame,
|
||||||
TriominoesGame,
|
TriominoesGame,
|
||||||
|
FreecellGame,
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ export default class GameRoomScene extends Phaser.Scene {
|
||||||
}
|
}
|
||||||
|
|
||||||
create() {
|
create() {
|
||||||
const slugDispatch = { backgammon: 'Backgammon', holdem: 'HoldemGame', blackjack: 'BlackjackGame', parchisi: 'ParchisiGame', yatzi: 'YatziGame', skipbo: 'SkipBoGame', phase10: 'Phase10Game', chinesecheckers: 'ChineseCheckersGame', gofish: 'GoFishGame', uno: 'UnoGame', craps: 'CrapsGame', roulette: 'RouletteGame', mexicantrain: 'MexicanTrainGame', hearts: 'HeartsGame', catan: 'CatanGame', tickettoride: 'TicketToRideGame', nerts: 'NertsGame', bingo: 'BingoGame', baccarat: 'BaccaratGame', dominion: 'DominionGame', checkers: 'CheckersGame', chess: 'ChessGame', wordle: 'WordleGame', scrabble: 'ScrabbleGame', ghost: 'GhostGame', wordladder: 'WordLadderGame', wordsearch: 'WordSearchGame', hangman: 'HangmanGame', sudoku: 'SudokuGame', othello: 'OthelloGame', go: 'GoGame', battleship: 'BattleshipGame', mastermind: 'MastermindGame', connect4: 'Connect4Game', boggle: 'BoggleGame', oldmaid: 'OldMaidGame', blokus: 'BlokusGame', spellingbee: 'SpellingBeeGame', minicrossword: 'MiniCrosswordGame', forbiddenisland: 'ForbiddenIslandGame', solitairetour: 'SolitaireTourGame', splendor: 'SplendorGame', tectonic: 'TectonicGame', labyrinth: 'LabyrinthGame', videopoker: 'VideoPokerGame', farkel: 'FarkelGame', stratego: 'StrategoGame', kiitos: 'KiitosGame', monopoly: 'MonopolyGame', triominoes: 'TriominoesGame' };
|
const slugDispatch = { backgammon: 'Backgammon', holdem: 'HoldemGame', blackjack: 'BlackjackGame', parchisi: 'ParchisiGame', yatzi: 'YatziGame', skipbo: 'SkipBoGame', phase10: 'Phase10Game', chinesecheckers: 'ChineseCheckersGame', gofish: 'GoFishGame', uno: 'UnoGame', craps: 'CrapsGame', roulette: 'RouletteGame', mexicantrain: 'MexicanTrainGame', hearts: 'HeartsGame', catan: 'CatanGame', tickettoride: 'TicketToRideGame', nerts: 'NertsGame', bingo: 'BingoGame', baccarat: 'BaccaratGame', dominion: 'DominionGame', checkers: 'CheckersGame', chess: 'ChessGame', wordle: 'WordleGame', scrabble: 'ScrabbleGame', ghost: 'GhostGame', wordladder: 'WordLadderGame', wordsearch: 'WordSearchGame', hangman: 'HangmanGame', sudoku: 'SudokuGame', othello: 'OthelloGame', go: 'GoGame', battleship: 'BattleshipGame', mastermind: 'MastermindGame', connect4: 'Connect4Game', boggle: 'BoggleGame', oldmaid: 'OldMaidGame', blokus: 'BlokusGame', spellingbee: 'SpellingBeeGame', minicrossword: 'MiniCrosswordGame', forbiddenisland: 'ForbiddenIslandGame', solitairetour: 'SolitaireTourGame', splendor: 'SplendorGame', tectonic: 'TectonicGame', labyrinth: 'LabyrinthGame', videopoker: 'VideoPokerGame', farkel: 'FarkelGame', stratego: 'StrategoGame', kiitos: 'KiitosGame', monopoly: 'MonopolyGame', triominoes: 'TriominoesGame', freecell: 'FreecellGame' };
|
||||||
if (slugDispatch[this.game.slug]) {
|
if (slugDispatch[this.game.slug]) {
|
||||||
this.scene.start(slugDispatch[this.game.slug], {
|
this.scene.start(slugDispatch[this.game.slug], {
|
||||||
game: this.game,
|
game: this.game,
|
||||||
|
|
|
||||||
|
|
@ -75,3 +75,4 @@ registerGame({ slug: 'stratego', name: 'Stratego', category: '
|
||||||
registerGame({ slug: 'kiitos', name: 'Kiitos', category: 'word', minPlayers: 2, maxPlayers: 4, minOpponents: 1, maxOpponents: 3, iconFrame: 47 });
|
registerGame({ slug: 'kiitos', name: 'Kiitos', category: 'word', minPlayers: 2, maxPlayers: 4, minOpponents: 1, maxOpponents: 3, iconFrame: 47 });
|
||||||
registerGame({ slug: 'monopoly', name: 'Monopoly', category: 'tabletop', minPlayers: 2, maxPlayers: 4, minOpponents: 1, maxOpponents: 3, iconFrame: 48 });
|
registerGame({ slug: 'monopoly', name: 'Monopoly', category: 'tabletop', minPlayers: 2, maxPlayers: 4, minOpponents: 1, maxOpponents: 3, iconFrame: 48 });
|
||||||
registerGame({ slug: 'triominoes', name: 'Tri-Ominoes', category: 'word', minPlayers: 2, maxPlayers: 4, minOpponents: 1, maxOpponents: 3, iconFrame: 49 });
|
registerGame({ slug: 'triominoes', name: 'Tri-Ominoes', category: 'word', minPlayers: 2, maxPlayers: 4, minOpponents: 1, maxOpponents: 3, iconFrame: 49 });
|
||||||
|
registerGame({ slug: 'freecell', name: 'Freecell', category: 'cards', cardGame: true, minPlayers: 1, maxPlayers: 1, minOpponents: 0, maxOpponents: 0, iconFrame: 50 });
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue