From 4fbc868305922c96fe84f656fc5250cb83a5cca5 Mon Sep 17 00:00:00 2001 From: Brian Fertig Date: Sun, 7 Jun 2026 20:31:46 -0600 Subject: [PATCH] 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 --- public/assets/images/game-icons.png | Bin 188888 -> 193267 bytes public/assets/images/game-icons.psd | Bin 500630 -> 510204 bytes public/src/games/freecell/FreecellGame.js | 609 ++++++++++++++++++ public/src/games/freecell/FreecellLogic.js | 218 +++++++ public/src/games/triominoes/TriominoesGame.js | 3 +- public/src/main.js | 2 + public/src/scenes/GameRoomScene.js | 2 +- server/games/registry.js | 1 + 8 files changed, 833 insertions(+), 2 deletions(-) create mode 100644 public/src/games/freecell/FreecellGame.js create mode 100644 public/src/games/freecell/FreecellLogic.js diff --git a/public/assets/images/game-icons.png b/public/assets/images/game-icons.png index 56c1268a0eb83d2e240358d31341fd5be0c8bd4b..10b956824fca1b15b90346994b8ee163a90e6bce 100644 GIT binary patch delta 32821 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

vgf6|HGM{(;pnaIa*A`85iXz=M(PB;IFb5P zCLuc(@5`&0Rdb}&q#xObLR@tc6P2Y3=EYe5L$ z%?iK|l*nNbIr|O%rU#ByeP|K$aS2|Hy#n9`ktE&o$k7%l4^^TH zIht76OO)+yo^VJ2r9CwzjBcCPSwSF8PJrcrlhK5!q9OWD@{=AOqy$-XrgBHNS46cw zW47LaM(npg{8{4qiO@0i1c*x~mJ2ZjpmzJBJw3he5pGA;JA&sVVY98I9h8wGhE73>eDB5k`2Yo7qdKpDJ-8l&04nU%3X z;AW;9)z@XI<4h(<_qh_Dw;Fb&0!+pW)f;D5q@^u*^*epQwB10eyhEQ6?Lc>obgD;7?DwP zO`pNey9Rb3}baU477>%(34!QekgtgUr$nn6>~sZyE_RW6kn8Ajd(!?C>v`I9PtKpIa;8$P2}b_K6Q1SCT9B7WNx6mnMyb zx7VB4K()8ng%WX%n)3{vg2NutCXy8pA)h9Salwdz^MRjk#^kHPI9I`;uRYi82XhIm zVuVpmlmQhjLG_;bFw5#azL&4`gl>UHt_%i#v)$0&=h}lS_F^05;lX>N`9@lls)kj? zoou2)*#U$@=@{brX+~n;7Th>09Pr~PKjeq{RQLiTB^>2I63F{ApBszT{RBQLJ&1UTMZUc z`Db(zo8y=$9AcqCtCka7G6pU&WJ|}Ptpg;h{vT;ZfV!de>q~uE#V3PdRSQ-ZW|how zvQJ~Qjf_r#(Um>YxD0BoTj96dfe+3q?VdQW7yk1vs;Z-B=kOpNur@<9 zf57jiAq`kg|4X&MZ$>gJ-SZS-;z^mNG`ZHJh`GMP1!0D>(1vgp4;4Z)Y=mCTlq1H) zmte*Olb3~NyNY>BnrT2g_>9t z*1R{gJp$Kqy?2RBPNIm9ha`XbsTfVf!4jBLTPyN?9>aCDebf8Ph|biBBq>c zOPCnW*o$K$mo?0Vbl6h{baj=5V?t|vUPHr|b3Dga1dzck2KT>tF07wv?ANzPeXop} zJidNLPQiAF_V+I1Q|0v_QFS0`Fa+G;6Q70(s3Fo&Ib;T!{(+j%blVFt!60Ep4*J(u z$#wKXV~f>5x{xBy!5u9FfS`C`v|g#zn~g?k_zfqJpW=#Ih&Z|%Oa^;}+P?WD`OT&z&!N@rMAhv$2%vXXY5q5~^Ef^L+W9YnS~{BT

#XZ>*QhEq2qLh&#?8a`Opxufgy&CuJyDj6k!rjJ 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⪼K&p^aK9U=zo7O@J|)~msJ4%*>e5M(*L>y{No+_cU1uXp}qXO(hvOgKl}T) zSM;CF_doml?>gZB@bAA%0RO|k|1JUixB1uo-*f-Xwu7q!N3X?4PADGO|IBVbxP>-; H^5_2n_>HZ4 diff --git a/public/assets/images/game-icons.psd b/public/assets/images/game-icons.psd index 24bb99414b2ac613553f9007b7e41736680f7473..c4374131f12e404de7f9090edeff32064dd29ec7 100644 GIT binary patch delta 11867 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 RC_=8LSlY&44klUk{69|rP*wl{ delta 2811 zcmaKtd2rK36vsC-yQ@}gLt9GkgY-%Zy#)q2TIt%tPzSET(N4qtA}7KC|*KU!S!# z=T`iB_xtIZ^WUA*t9D=7bO+w-@%ivTFv{Jq_FD46w58v7KKpR=zN;lu%n?y}_TG;l zUDDFu|JikY)dz_&D;;A?iyGG6SJ-BkYYSDcYxk;h*;EHc4fLUjGjirD1KB|$q+~O_ z`GG1yp-9)N1EL>N=#Wyed*IvVc>$$cwbqzfsuWM&(pR_S6>Qp=&wdtbH&5Q)-0(>; z-C@vvWY8+u9APwQeU`s&+!&!;HS+06XfApdtwaT=2<<|LP(7+eN7!d0YGU8bY?_b- zIgtakBO4ob)P`Ewv*V}%RicBa0)4@Tj@8aHh9)a7DF35aGN@S3RE`WP9z`vzvK3iT z7wTcUR@BLqErXiJ*@~5He92TjiX}seXMBtySxTb#U)^KKz;(L^bv^rZu-$t0YG%56 z^bJ!z#I$v&ll9i_*{pGzCpXbI?rm6k3dO&}=jd%|Mgc^gNl1ma(&0!6Keh zdJg3>UWQ&qFQFx95gV&mbQQ{D(P_r;WaSAT<4aSOV@}he0naeNE$`&=U|5709?hP6>VLWe98u!Fzdz1tx}Kz3)(lWgz9;E#U2Ve1Jm_WGCSL99-~Q0xlO&k%&Lz;D&ig!{DDJyhlV1 zXoM|p$V|qwd8AxloQ$t=Fu)gqMA)wjBGUcBAR^tr6-1=_?~W$Y{g`0#Ar~{j>0lz= z-#3Ox_t%FEgC|0Xbl)#*7+ek`(tUF{k?xmAkXpV@2NXt<2Hpym0nKfwS1ZGzFpBII z?rZ+I1ZO=@mqZgSud_l(3^8%Afln;42)GkVY#ch_@>t?+5<9rmqQ(x5aio=}IRN5` zw_}{JJwensVRa&rtuQ@_cw3gVm#;#M>8c z*gc-eR+yYhr27|Bg)MH_mPUm8Mt2&?koNnBQei)A4W-imtD#i-zb1@I|L=rR>AxV}MTdXPtY;AT3l;NXIq4C-~-1utfblXXKt z7WFoS8ys0w+Mb_Hi@AUs2C}KN{h7yU)<#;y{z8ioyY-bcNx;=gnkFEliayT42t`$N zf`C(1bQ%W}#8=ZN1iV>Ir*kmF$!a>6Lo3AT=?npH=;>4rZP2Z!Qv?Lm(8(OyVRa47 z63`$c9q^z=ENX$ATCu1F_Sbsac<|+{aHEzk;$VXXhdpgP7_ourFnx|gCq&eVjoRUj zIw4?(-a5}c7_4zXYCT=R!3q27>0$w-L9FA1*Cceo`39QH)4E~q5xPvku_JUohaT{6 zq&Xa1uzol=8tDr>;)aZ);uYNR)zM**kxg{P!$TW&qf-#o=@Sn7n`j&_EjRWy(X`-y E0gFfkcmMzZ diff --git a/public/src/games/freecell/FreecellGame.js b/public/src/games/freecell/FreecellGame.js new file mode 100644 index 0000000..f24733d --- /dev/null +++ b/public/src/games/freecell/FreecellGame.js @@ -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 */ }); + } +} diff --git a/public/src/games/freecell/FreecellLogic.js b/public/src/games/freecell/FreecellLogic.js new file mode 100644 index 0000000..2a7f9f1 --- /dev/null +++ b/public/src/games/freecell/FreecellLogic.js @@ -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; + } +} diff --git a/public/src/games/triominoes/TriominoesGame.js b/public/src/games/triominoes/TriominoesGame.js index 66ba21f..d937cf3 100644 --- a/public/src/games/triominoes/TriominoesGame.js +++ b/public/src/games/triominoes/TriominoesGame.js @@ -351,13 +351,14 @@ export default class TriominoesGame extends Phaser.Scene { const startX = CX - ((hand.length - 1) * pitch) / 2; const pts = this.handPts(); + const anyPlayable = legalTiles.size > 0; hand.forEach((tile, idx) => { const x = startX + idx * pitch; const playable = humanTurn && legalTiles.has(idx); const container = this.add.container(x, HAND_Y).setDepth(DEPTH.hand); const border = playable ? COLORS.accent : COLORS.muted; 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._homeX = x; diff --git a/public/src/main.js b/public/src/main.js index ef452c0..642e2a3 100644 --- a/public/src/main.js +++ b/public/src/main.js @@ -60,6 +60,7 @@ import StrategoGame from './games/stratego/StrategoGame.js'; import KiitosGame from './games/kiitos/KiitosGame.js'; import MonopolyGame from './games/monopoly/MonopolyGame.js'; import TriominoesGame from './games/triominoes/TriominoesGame.js'; +import FreecellGame from './games/freecell/FreecellGame.js'; const config = { type: Phaser.AUTO, @@ -133,6 +134,7 @@ const config = { KiitosGame, MonopolyGame, TriominoesGame, + FreecellGame, ], }; diff --git a/public/src/scenes/GameRoomScene.js b/public/src/scenes/GameRoomScene.js index 3c2d278..d67d66b 100644 --- a/public/src/scenes/GameRoomScene.js +++ b/public/src/scenes/GameRoomScene.js @@ -22,7 +22,7 @@ export default class GameRoomScene extends Phaser.Scene { } 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]) { this.scene.start(slugDispatch[this.game.slug], { game: this.game, diff --git a/server/games/registry.js b/server/games/registry.js index 88efabb..e366196 100644 --- a/server/games/registry.js +++ b/server/games/registry.js @@ -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: '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: 'freecell', name: 'Freecell', category: 'cards', cardGame: true, minPlayers: 1, maxPlayers: 1, minOpponents: 0, maxOpponents: 0, iconFrame: 50 });