From bbb9c329c7ed4220fec047f7f32699706dcbd8d2 Mon Sep 17 00:00:00 2001 From: Brian Fertig Date: Thu, 11 Jun 2026 18:18:45 -0600 Subject: [PATCH] feat: add 4-player Hong Kong style Mahjong with AI opponents - Implement pure logic engine for tile management, shanten calculation, claim resolution, and faan scoring. - Add heuristic AI (5 skill levels) using shanten minimization, ukeire tiebreaking, and adaptive defense. - Build Phaser UI with tile rendering, dynamic scoreboard, claim prompts, hand-end modals, and a scoring reference panel. - Integrate into frontend routing, opponent selection, and backend registry. - Include headless verification script for tile catalog, scoring fixtures, and AI self-play invariant checks. - Update game icon assets and add in-game tutorial. --- public/assets/images/game-icons.png | Bin 214472 -> 221982 bytes public/assets/images/game-icons.psd | Bin 574441 -> 594190 bytes public/src/games/mahjong/MahjongAI.js | 215 +++++++ public/src/games/mahjong/MahjongData.js | 122 ++++ public/src/games/mahjong/MahjongGame.js | 780 +++++++++++++++++++++++ public/src/games/mahjong/MahjongLogic.js | 602 +++++++++++++++++ public/src/games/mahjong/tutorial.md | 91 +++ public/src/main.js | 2 + public/src/scenes/GameRoomScene.js | 2 +- public/src/scenes/OpponentSelectScene.js | 2 +- server/games/registry.js | 1 + server/scripts/verifyMahjong.js | 292 +++++++++ 12 files changed, 2107 insertions(+), 2 deletions(-) create mode 100644 public/src/games/mahjong/MahjongAI.js create mode 100644 public/src/games/mahjong/MahjongData.js create mode 100644 public/src/games/mahjong/MahjongGame.js create mode 100644 public/src/games/mahjong/MahjongLogic.js create mode 100644 public/src/games/mahjong/tutorial.md create mode 100644 server/scripts/verifyMahjong.js diff --git a/public/assets/images/game-icons.png b/public/assets/images/game-icons.png index bb56cf1c603f3db8e6a0499b9f30fb636edbcb93..2de7ec3d58a9623292c123f368817a7bc01918de 100644 GIT binary patch delta 61765 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È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 VbIvyV>I;89c^Q>=#cx0O{|7U%mLdQE delta 54196 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 Ab^rhX diff --git a/public/assets/images/game-icons.psd b/public/assets/images/game-icons.psd index e994999a372e20222b069787ccecf102467a9eda..d79acfe18a5c953972cd38aaf26d8a702a4b4990 100644 GIT binary patch delta 27345 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 delta 8222 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 2 ? 3 : 0); + const r = rankOf(kind); + if (r >= 2 && counts[kind - 1] > 0) u += 2; + if (r <= 8 && counts[kind + 1] > 0) u += 2; + if (r >= 3 && counts[kind - 2] > 0) u += 1; + if (r <= 7 && counts[kind + 2] > 0) u += 1; + if (r === 1 || r === 9) u -= 1; + return u; +} + +// Draws that could improve a hand: pairing an existing tile or extending a +// suited shape. A brand-new isolated kind never lowers standard shanten. +function improverKinds(counts) { + const out = new Set(); + for (let k = 0; k < 34; k++) { + if (counts[k] === 0) continue; + out.add(k); + if (!isSuited(k)) continue; + const r = rankOf(k); + for (const d of [-2, -1, 1, 2]) { + const t = k + d; + if (r + d >= 1 && r + d <= 9) out.add(t); + } + } + return [...out]; +} + +// Pick a discard for the player on turn (hand holds 14 − 3·melds tiles). +export function chooseDiscard(state, seat, skill = 3) { + const prof = profileFor(skill); + const p = state.players[seat]; + const counts = counts34(p.hand); + const kinds = []; + for (let k = 0; k < 34; k++) if (counts[k] > 0) kinds.push(k); + + if (prof.claimAlways) { // skills 1-2: discard the least useful tile + let best = kinds[0], bestScore = Infinity; + for (const k of kinds) { + const s = usefulness(counts, k) + Math.random() * prof.noise * 2; + if (s < bestScore) { bestScore = s; best = k; } + } + return best; + } + + const m = p.melds.length; + let minSh = Infinity; + const perKind = new Map(); + for (const k of kinds) { + counts[k]--; + const s = shanten(counts, m); + counts[k]++; + perKind.set(k, s); + if (s < minSh) minSh = s; + } + const candidates = kinds.filter((k) => perKind.get(k) === minSh); + + const seen = prof.seenAware ? seenCounts(state) : null; + const threat = prof.defense > 0 && minSh >= 2 + && state.players.some((q) => q.seat !== seat && q.melds.length >= 3); + + let best = candidates[0], bestScore = -Infinity; + for (const k of candidates) { + counts[k]--; + let ukeire = 0; + for (const t of improverKinds(counts)) { + counts[t]++; + if (shanten(counts, m) < minSh) { + let left = 4 - counts[t] + 1; + if (seen) left -= seen[t]; + if (left > 0) ukeire += left; + } + counts[t]--; + } + counts[k]++; + let score = ukeire + Math.random() * prof.noise * 2; + // someone looks close to winning — favour tiles already seen on the table + if (threat && seen && seen[k] >= 1) score += 4 * prof.defense; + if (score > bestScore) { bestScore = score; best = k; } + } + return best; +} + +// React to another player's discard. `options` comes from claimOptionsFor. +// Returns a claim ({ type, tiles? }) or null to pass. +export function chooseClaim(state, seat, options, skill = 3) { + if (!options) return null; + if (options.win) return { type: 'win' }; + const prof = profileFor(skill); + const p = state.players[seat]; + const kind = state.lastDiscard.kind; + const m = p.melds.length; + const counts = counts34(p.hand); + + if (prof.claimAlways) { + if (options.kong) return { type: 'kong' }; + if (options.pung) return { type: 'pung' }; + if (options.chows.length) return { type: 'chow', tiles: options.chows[0] }; + return null; + } + + const s0 = shanten(counts, m); + if (options.kong) { // free replacement draw — accept when not a step back + counts[kind] -= 3; + const s = shanten(counts, m + 1); + counts[kind] += 3; + if (s <= s0) return { type: 'kong' }; + } + if (options.pung) { + counts[kind] -= 2; + const s = shanten(counts, m + 1); + counts[kind] += 2; + if (s < s0) return { type: 'pung' }; + } + for (const tiles of options.chows) { + counts[tiles[0]]--; counts[tiles[1]]--; + const s = shanten(counts, m + 1); + counts[tiles[0]]++; counts[tiles[1]]++; + if (s < s0) return { type: 'chow', tiles }; + } + return null; +} + +// Decide on a self-drawn win or a kong declaration while awaiting discard. +// `actions` comes from selfActions. Returns { type: 'win' }, +// { type: 'kong', spec } or null to just discard. +export function chooseSelfAction(state, seat, actions, skill = 3) { + if (actions.canWin) return { type: 'win' }; + const prof = profileFor(skill); + if (!actions.concealedKongs.length && !actions.addedKongs.length) return null; + if (prof.claimAlways) { + if (actions.concealedKongs.length) return { type: 'kong', spec: { type: 'concealed', kind: actions.concealedKongs[0] } }; + return { type: 'kong', spec: { type: 'added', kind: actions.addedKongs[0] } }; + } + + const p = state.players[seat]; + const m = p.melds.length; + const counts = counts34(p.hand); + let bestDiscardSh = Infinity; + for (let k = 0; k < 34; k++) { + if (counts[k] === 0) continue; + counts[k]--; + const s = shanten(counts, m); + counts[k]++; + if (s < bestDiscardSh) bestDiscardSh = s; + } + for (const k of actions.concealedKongs) { + counts[k] -= 4; + const s = shanten(counts, m + 1); + counts[k] += 4; + if (s <= bestDiscardSh) return { type: 'kong', spec: { type: 'concealed', kind: k } }; + } + for (const k of actions.addedKongs) { + counts[k]--; + const s = shanten(counts, m); // pung→kong: still one meld, one tile fewer + counts[k]++; + if (s <= bestDiscardSh) return { type: 'kong', spec: { type: 'added', kind: k } }; + } + return null; +} + +// Collect claim intents from every AI seat for the current discard. The +// scene merges these with the human's choice before calling resolveClaims. +export function collectAIClaims(state, humanSeat = 0) { + const intents = []; + for (let seat = 0; seat < 4; seat++) { + if (seat === humanSeat) continue; + const p = state.players[seat]; + if (!p.isAI) continue; + const options = claimOptionsFor(state, seat); + if (!options) continue; + const claim = chooseClaim(state, seat, options, p.skill); + if (claim) intents.push({ seat, claim }); + } + return intents; +} diff --git a/public/src/games/mahjong/MahjongData.js b/public/src/games/mahjong/MahjongData.js new file mode 100644 index 0000000..00fc515 --- /dev/null +++ b/public/src/games/mahjong/MahjongData.js @@ -0,0 +1,122 @@ +// Mahjong (Hong Kong style) — static catalog. No Phaser, no game state. +// Tile kinds, label-art mapping, the faan scoring table (single source of +// truth for both the engine and the in-game reference panel), and the +// faan → base-points ladder. +// +// Tiles are encoded as small integers ("kinds") so the engine can count and +// decompose hands with flat arrays: +// 0..8 bamboo 1-9 9..17 circle 1-9 18..26 character 1-9 +// 27..30 winds E S W N 31..33 dragons R G W +// 34..37 flowers (seat E S W N) 38..41 seasons (seat E S W N) + +// pinyin sheet index for character (萬) tiles 1..9 — mirrors Mahjong Match. +const CHAR_LABEL = [13, 14, 15, 7, 8, 9, 10, 11, 12]; + +export const BAMBOO = 0, CIRCLE = 9, CHAR = 18; +export const WIND_KINDS = [27, 28, 29, 30]; // E S W N +export const DRAGON_KINDS = [31, 32, 33]; // red green white +export const FIRST_BONUS = 34; // flowers then seasons +export const KIND_COUNT = 42; // 34 playing + 8 bonus + +export const isSuited = (k) => k < 27; +export const isHonor = (k) => k >= 27 && k < 34; +export const isBonus = (k) => k >= FIRST_BONUS; +export const suitOf = (k) => (k < 27 ? Math.floor(k / 9) : -1); // 0 bam, 1 cir, 2 char +export const rankOf = (k) => (k % 9) + 1; // suited kinds only +export const isTerminal = (k) => isSuited(k) && (rankOf(k) === 1 || rankOf(k) === 9); + +export const WIND_NAMES = ['East', 'South', 'West', 'North']; +export const SUIT_NAMES = ['Bamboo', 'Circle', 'Character']; + +// One entry per kind: { kind, id, label (texture key | null), name }. +export const TILES = (() => { + const t = []; + const suits = [['bamboo', 'Bamboo'], ['circle', 'Circle'], ['char', 'Character']]; + suits.forEach(([id, name], s) => { + for (let n = 1; n <= 9; n++) { + const label = id === 'char' ? `mahjong-pinyin${CHAR_LABEL[n - 1]}` : `mahjong-${id}${n}`; + t.push({ kind: s * 9 + n - 1, id: `${id}${n}`, label, name: `${name} ${n}` }); + } + }); + const winds = [['east', 4], ['south', 3], ['west', 6], ['north', 5]]; + winds.forEach(([w, pinyin], i) => { + t.push({ kind: 27 + i, id: `wind-${w}`, label: `mahjong-pinyin${pinyin}`, name: `${WIND_NAMES[i]} Wind` }); + }); + t.push({ kind: 31, id: 'dragon-red', label: 'mahjong-pinyin1', name: 'Red Dragon' }); + t.push({ kind: 32, id: 'dragon-green', label: 'mahjong-pinyin2', name: 'Green Dragon' }); + t.push({ kind: 33, id: 'dragon-white', label: null, name: 'White Dragon' }); // drawn procedurally + const flowers = ['orchid', 'peony', 'chrysanthemum', 'lotus']; + flowers.forEach((f, i) => { + t.push({ kind: 34 + i, id: f, label: `mahjong-${f}`, name: `Flower (${WIND_NAMES[i]})` }); + }); + const seasons = ['spring', 'summer', 'fall', 'winter']; + seasons.forEach((s, i) => { + t.push({ kind: 38 + i, id: s, label: `mahjong-${s}`, name: `Season (${WIND_NAMES[i]})` }); + }); + return t; +})(); + +// Full 144-tile wall: 4 copies of each playing tile, 1 of each bonus tile. +export function buildWall() { + const wall = []; + for (let k = 0; k < FIRST_BONUS; k++) wall.push(k, k, k, k); + for (let k = FIRST_BONUS; k < KIND_COUNT; k++) wall.push(k); + return wall; +} + +// ── Scoring ────────────────────────────────────────────────────────────────── +// Classic HK Old Style faan values with a 1-faan minimum: a "chicken hand" +// (no faan at all) may not declare a win, but bonus-tile and contextual faan +// count toward the minimum, so hands stay fast. `excludes` lists faan ids the +// engine must NOT also award when this row applies (prevents double counting, +// e.g. Great Dragons already contains its three dragon pungs). + +export const MIN_FAAN = 1; +export const LIMIT_FAAN = 13; + +export const FAAN_TABLE = [ + // hand patterns + { id: 'common-hand', label: 'Common Hand', faan: 1, desc: 'Four chows and a pair of suited tiles' }, + { id: 'all-pungs', label: 'All Pungs', faan: 3, desc: 'Four pungs or kongs and a pair' }, + { id: 'mixed-one-suit', label: 'Mixed One Suit', faan: 3, desc: 'One suit plus honor tiles only' }, + { id: 'pure-one-suit', label: 'Pure One Suit', faan: 6, desc: 'One suit only, no honors', + excludes: ['mixed-one-suit'] }, + { id: 'small-dragons', label: 'Small Dragons', faan: 5, desc: 'Two dragon pungs and a dragon pair', + excludes: ['dragon-pung'] }, + { id: 'great-dragons', label: 'Great Dragons', faan: 8, desc: 'All three dragon pungs', + excludes: ['dragon-pung', 'small-dragons'] }, + { id: 'small-winds', label: 'Small Winds', faan: 6, desc: 'Three wind pungs and a wind pair', + excludes: ['seat-wind', 'round-wind'] }, + { id: 'great-winds', label: 'Great Winds', faan: 13, desc: 'All four wind pungs', + excludes: ['seat-wind', 'round-wind', 'small-winds', 'all-pungs'] }, + { id: 'all-honors', label: 'All Honors', faan: 13, desc: 'Winds and dragons only', + excludes: ['all-pungs', 'mixed-one-suit'] }, + { id: 'thirteen-orphans', label: 'Thirteen Orphans', faan: 13, desc: 'One of every 1, 9 and honor, plus a duplicate' }, + // per-meld faan + { id: 'dragon-pung', label: 'Dragon Pung', faan: 1, desc: 'Each pung or kong of a dragon' }, + { id: 'seat-wind', label: 'Seat Wind Pung', faan: 1, desc: 'Pung or kong of your seat wind' }, + { id: 'round-wind', label: 'Round Wind Pung', faan: 1, desc: 'Pung or kong of East, the round wind' }, + // win context + { id: 'self-draw', label: 'Self Draw', faan: 1, desc: 'Winning tile drawn from the wall' }, + { id: 'concealed', label: 'Concealed Hand', faan: 1, desc: 'No melds claimed from discards' }, + { id: 'last-tile', label: 'Last Wall Tile', faan: 1, desc: 'Win on the very last wall tile' }, + { id: 'kong-draw', label: 'Kong Replacement', faan: 1, desc: 'Win on the tile drawn after a kong' }, + // bonus tiles + { id: 'seat-flower', label: 'Own Flower / Season', faan: 1, desc: 'Each flower or season matching your seat' }, + { id: 'flower-set', label: 'All Four Flowers', faan: 2, desc: 'The complete flower set' }, + { id: 'season-set', label: 'All Four Seasons', faan: 2, desc: 'The complete season set' }, + { id: 'no-bonus', label: 'No Bonus Tiles', faan: 1, desc: 'Hand finished with no flowers or seasons' }, +]; + +export const FAAN_BY_ID = Object.fromEntries(FAAN_TABLE.map((r) => [r.id, r])); + +// Base points per faan (half-doubling ladder, capped at the 13-faan limit). +export const BASE_POINTS = [1, 2, 4, 8, 16, 24, 32, 48, 64, 96, 128, 192, 256, 384]; +export const basePoints = (faan) => BASE_POINTS[Math.min(faan, LIMIT_FAAN)]; + +// Payments: winner by discard collects 2× base from the discarder alone; +// a self-drawn winner collects 1× base from each of the three others. + +// Seat colours (shared visual language with the other tabletop games). +export const PLAYER_COLORS = [0xd0473a, 0x4a90d9, 0x49a25a, 0xe2b53c]; +export const PLAYER_COLOR_HEX = ['#d0473a', '#4a90d9', '#49a25a', '#e2b53c']; diff --git a/public/src/games/mahjong/MahjongGame.js b/public/src/games/mahjong/MahjongGame.js new file mode 100644 index 0000000..9a0e17a --- /dev/null +++ b/public/src/games/mahjong/MahjongGame.js @@ -0,0 +1,780 @@ +import * as Phaser from 'phaser'; +import { GAME_WIDTH, GAME_HEIGHT, COLORS } from '../../config.js'; +import { Button } from '../../ui/Button.js'; +import { auth } from '../../services/auth.js'; +import { api } from '../../services/api.js'; +import { createOpponentPortrait, createPlayerPortrait } from '../../ui/Portrait.js'; +import { playSound, SFX } from '../../ui/Sounds.js'; +import { MusicPlayer } from '../../ui/MusicPlayer.js'; +import { + TILES, SUIT_NAMES, WIND_NAMES, rankOf, isSuited, + FAAN_TABLE, MIN_FAAN, PLAYER_COLOR_HEX, +} from './MahjongData.js'; +import { + createInitialState, discardTile, claimOptionsFor, resolveClaims, + selfActions, declareKong, declareWin, startNextHand, + isGameOver, getWinners, seatWindOf, +} from './MahjongLogic.js'; +import { + chooseDiscard, chooseSelfAction, collectAIClaims, nextThinkDelay, +} from './MahjongAI.js'; + +// Deep-green felt with ivory tiles — same table look as Mahjong Match. +const FELT = 0x0e2a1c; +const FACE = 0xf6efdb; +const FACE_EDGE = 0x8d7c52; +const SIDE = 0xb59c66; +const DRAGON_BLUE = 0x3f6bb5; +const BACK_FACE = 0x2e6e4e; +const BACK_INSET = 0x3f8a63; + +// Label art is 128×178; keep its aspect when fitting it onto a tile face. +const LABEL_W = 128; +const LABEL_H = 178; + +// ── layout ──────────────────────────────────────────────────────────────────── +const HAND_Y = 975, HAND_TW = 76, HAND_TH = 104, HAND_STEP = 80; +const SM_W = 44, SM_H = 60; // opponent backs, melds, modal tiles +const RIV_W = 42, RIV_H = 58; // discard rivers + +// per-seat river grids: 8 per row, growing toward the table centre +const RIVERS = [ + { cx: 960, y0: 652, dy: 64 }, // seat 0 — bottom + { cx: 1320, y0: 412, dy: 64 }, // seat 1 — right + { cx: 960, y0: 198, dy: 64 }, // seat 2 — top + { cx: 600, y0: 412, dy: 64 }, // seat 3 — left +]; + +const CLAIM_Y = 820; +const REF_W = 430; + +const DEPTH = { + bg: -2, panel: 0, text: 2, tiles: 5, ui: 20, claims: 30, toast: 55, ref: 60, modal: 70, +}; + +const tileName = (k) => TILES[k].name; + +export default class MahjongGame extends Phaser.Scene { + constructor() { super('MahjongGame'); } + + init(data) { + this.gameDef = data.game; + this.opponents = data.opponents ?? []; + this.playfield = data.playfield ?? null; + + this.humanSeat = 0; + this.driving = false; + this.gameOverShown = false; + this.humanCanDiscard = false; + this.sel = null; // selected hand tile { c, baseY } + this.handTiles = []; + this.claimResolve = null; + this.kongSpecs = []; + this.chowOptions = []; + this.portraitCtrls = []; + this.refOpen = false; + } + + create() { + try { new MusicPlayer(this, this.cache.json.get('music')?.tracks ?? []); } catch { /* optional */ } + this.add.rectangle(GAME_WIDTH / 2, GAME_HEIGHT / 2, GAME_WIDTH, GAME_HEIGHT, FELT).setDepth(DEPTH.bg); + + const names = [auth.user?.username ?? 'You']; + const skills = { 0: 5 }; + for (let seat = 1; seat < 4; seat++) { + const opp = this.opponents[seat - 1]; + names.push(opp?.name ?? `Player ${seat + 1}`); + skills[seat] = Math.max(1, Math.min(5, opp?.skill ?? 3)); + } + this.gs = createInitialState({ names, skills }); + + this.dyn = this.add.container(0, 0).setDepth(DEPTH.tiles); + this.buildScoreboard(); + this.buildPortraits(); + this.buildCentreTexts(); + this.buildActionButtons(); + this.buildReferencePanel(); + + new Button(this, 80, GAME_HEIGHT - 40, 'Leave', () => this.scene.start('GameMenu'), { + variant: 'ghost', width: 140, height: 52, fontSize: 20, + }).setDepth(DEPTH.ui); + + this.refresh(); + this.advance(); + } + + // ── static chrome ─────────────────────────────────────────────────────────── + buildScoreboard() { + this.add.rectangle(180, 132, 320, 224, COLORS.panel, 0.92) + .setStrokeStyle(2, COLORS.accent).setDepth(DEPTH.panel); + this.sbHeader = this.add.text(180, 48, '', { + fontFamily: 'Righteous', fontSize: '22px', color: COLORS.goldHex, + }).setOrigin(0.5).setDepth(DEPTH.text); + this.sbRows = []; + for (let seat = 0; seat < 4; seat++) { + const y = 88 + seat * 40; + const dot = this.add.circle(48, y, 8, Phaser.Display.Color.HexStringToColor(PLAYER_COLOR_HEX[seat]).color) + .setDepth(DEPTH.text); + const name = this.add.text(66, y, '', { + fontFamily: '"Julius Sans One"', fontSize: '20px', color: COLORS.textHex, + }).setOrigin(0, 0.5).setDepth(DEPTH.text); + const score = this.add.text(312, y, '', { + fontFamily: 'Righteous', fontSize: '20px', color: COLORS.accentHex, + }).setOrigin(1, 0.5).setDepth(DEPTH.text); + this.sbRows.push({ dot, name, score }); + } + } + + buildPortraits() { + const spots = [ + { x: 90, y: 870, r: 46 }, // human — bottom left + { x: 1830, y: 170, r: 40 }, // seat 1 — right + { x: 560, y: 95, r: 40 }, // seat 2 — top + { x: 90, y: 310, r: 40 }, // seat 3 — left + ]; + for (let seat = 0; seat < 4; seat++) { + const { x, y, r } = spots[seat]; + const ring = this.add.graphics().setDepth(DEPTH.ui); + let ctrl; + if (seat === this.humanSeat) { + ctrl = createPlayerPortrait(this, x, y, r, DEPTH.ui, 'MahjongGame'); + } else { + const opp = this.opponents[seat - 1] ?? { id: 'bot', spriteIndex: 0 }; + ctrl = createOpponentPortrait(this, opp, x, y, r, DEPTH.ui); + } + this.add.text(x, y + r + 14, this.gs.players[seat].name, { + fontFamily: '"Julius Sans One"', fontSize: '15px', color: COLORS.textHex, + }).setOrigin(0.5).setDepth(DEPTH.ui); + this.portraitCtrls.push({ ring, controller: ctrl, x, y, r }); + } + } + + buildCentreTexts() { + this.wallText = this.add.text(960, 448, '', { + fontFamily: 'Righteous', fontSize: '30px', color: COLORS.goldHex, + }).setOrigin(0.5).setDepth(DEPTH.text); + this.statusText = this.add.text(960, 505, '', { + fontFamily: '"Julius Sans One"', fontSize: '24px', color: COLORS.accentHex, + }).setOrigin(0.5).setDepth(DEPTH.text); + } + + buildActionButtons() { + const mk = (label, cb, w = 180) => + new Button(this, 0, CLAIM_Y, label, cb, { width: w, height: 60, fontSize: 24 }) + .setDepth(DEPTH.claims).setVisible(false); + + this.claimBtns = { + win: mk('Mahjong!', () => this.resolveClaim({ type: 'win' })), + kong: mk('Kong', () => this.resolveClaim({ type: 'kong' })), + pung: mk('Pung', () => this.resolveClaim({ type: 'pung' })), + chows: [0, 1, 2].map((i) => mk('Chow', () => this.resolveClaim({ type: 'chow', tiles: this.chowOptions[i] }), 210)), + pass: mk('Pass', () => this.resolveClaim(null)), + }; + + this.selfWinBtn = new Button(this, 1770, CLAIM_Y, 'Mahjong!', () => this.onSelfWin(), { + width: 170, height: 60, fontSize: 24, + }).setDepth(DEPTH.claims).setVisible(false); + this.selfKongBtns = [0, 1].map((i) => + new Button(this, 1770, CLAIM_Y + 70 + i * 70, 'Kong', () => this.onSelfKong(i), { + width: 170, height: 60, fontSize: 22, + }).setDepth(DEPTH.claims).setVisible(false)); + } + + // Toggleable list of every scoring hand — the same table the engine scores + // with, so the panel can never drift from the rules. + buildReferencePanel() { + const panel = this.add.container(GAME_WIDTH, 0).setDepth(DEPTH.ref); + const g = this.add.graphics(); + g.fillStyle(COLORS.panel, 0.97); + g.fillRect(0, 0, REF_W, GAME_HEIGHT); + g.lineStyle(2, COLORS.accent, 1); + g.lineBetween(0, 0, 0, GAME_HEIGHT); + panel.add(g); + panel.add(this.add.text(REF_W / 2, 34, 'Winning Hands', { + fontFamily: 'Righteous', fontSize: '28px', color: COLORS.goldHex, + }).setOrigin(0.5)); + + let y = 76; + for (const r of FAAN_TABLE) { + panel.add(this.add.text(24, y, r.label, { + fontFamily: '"Julius Sans One"', fontSize: '19px', color: COLORS.textHex, + }).setOrigin(0, 0.5)); + panel.add(this.add.text(REF_W - 24, y, `${r.faan}`, { + fontFamily: 'Righteous', fontSize: '20px', color: COLORS.accentHex, + }).setOrigin(1, 0.5)); + panel.add(this.add.text(24, y + 18, r.desc, { + fontFamily: '"Julius Sans One"', fontSize: '13px', color: COLORS.mutedHex, + }).setOrigin(0, 0.5)); + y += 44; + } + panel.add(this.add.text(REF_W / 2, y + 14, `A hand needs at least ${MIN_FAAN} faan to win.`, { + fontFamily: '"Julius Sans One"', fontSize: '15px', color: COLORS.goldHex, + }).setOrigin(0.5)); + panel.add(this.add.text(REF_W / 2, y + 38, 'Points double with faan · discarder pays 2×, everyone pays on self-draw', { + fontFamily: '"Julius Sans One"', fontSize: '13px', color: COLORS.mutedHex, + }).setOrigin(0.5)); + this.refPanel = panel; + + this.refBtn = new Button(this, 1810, 44, 'Hands', () => this.toggleReference(), { + width: 150, height: 52, fontSize: 22, variant: 'ghost', + }).setDepth(DEPTH.ref + 1); + } + + toggleReference() { + this.refOpen = !this.refOpen; + this.tweens.add({ + targets: this.refPanel, + x: this.refOpen ? GAME_WIDTH - REF_W : GAME_WIDTH, + duration: 200, + ease: 'Quad.Out', + }); + this.refBtn.setLabel(this.refOpen ? 'Close' : 'Hands'); + } + + // ── tile drawing ────────────────────────────────────────────────────────────── + makeTileFace(kind, w, h, { highlight = false } = {}) { + const t = Math.max(2, Math.round(w * 0.08)); + const r = Math.max(3, Math.round(w * 0.09)); + const g = this.add.graphics(); + g.fillStyle(SIDE, 1); + g.fillRoundedRect(-w / 2 + t, -h / 2 + t, w, h, r); + g.fillStyle(FACE, 1); + g.fillRoundedRect(-w / 2, -h / 2, w, h, r); + g.lineStyle(highlight ? 3 : 2, highlight ? COLORS.gold : FACE_EDGE, 1); + g.strokeRoundedRect(-w / 2, -h / 2, w, h, r); + const kids = [g]; + const tile = TILES[kind]; + if (tile.label && this.textures.exists(tile.label)) { + const img = this.add.image(0, 0, tile.label); + img.setScale(Math.min((w * 0.80) / LABEL_W, (h * 0.82) / LABEL_H)); + kids.push(img); + } else if (tile.id === 'dragon-white') { + g.lineStyle(Math.max(2, Math.round(w * 0.06)), DRAGON_BLUE, 0.95); + g.strokeRoundedRect(-w * 0.30, -h * 0.32, w * 0.60, h * 0.64, 5); + } + return this.add.container(0, 0, kids); + } + + makeTileBack(w, h) { + const t = Math.max(2, Math.round(w * 0.08)); + const r = Math.max(3, Math.round(w * 0.09)); + const g = this.add.graphics(); + g.fillStyle(SIDE, 1); + g.fillRoundedRect(-w / 2 + t, -h / 2 + t, w, h, r); + g.fillStyle(BACK_FACE, 1); + g.fillRoundedRect(-w / 2, -h / 2, w, h, r); + g.lineStyle(2, FACE_EDGE, 1); + g.strokeRoundedRect(-w / 2, -h / 2, w, h, r); + g.fillStyle(BACK_INSET, 1); + g.fillRoundedRect(-w * 0.34, -h * 0.34, w * 0.68, h * 0.68, 4); + return this.add.container(0, 0, [g]); + } + + // A meld drawn as a tight row of small tiles. Concealed kongs show their + // backs (other players shouldn't read them at a glance). + makeMeldRow(meld, w, h, faceUp = true) { + const c = this.add.container(0, 0); + const step = w + 2; + const n = meld.kinds.length; + meld.kinds.forEach((k, i) => { + const showFace = faceUp || !meld.concealed; + const tile = showFace ? this.makeTileFace(k, w, h) : this.makeTileBack(w, h); + tile.setPosition((i - (n - 1) / 2) * step, 0); + c.add(tile); + }); + return c; + } + + // ── dynamic rendering ───────────────────────────────────────────────────────── + refresh() { + this.dyn.removeAll(true); + this.handTiles = []; + this.sel = null; + + const gs = this.gs; + this.humanCanDiscard = gs.phase === 'awaitDiscard' && gs.turn === 0 && !this.gameOverShown; + + this.renderHumanHand(); + this.renderHumanMeldsAndBonus(); + for (let seat = 1; seat < 4; seat++) this.renderOpponent(seat); + this.renderRivers(); + this.renderScoreboard(); + this.renderCentre(); + this.updatePortraitRings(); + this.updateSelfButtons(); + } + + renderHumanHand() { + const p = this.gs.players[0]; + const tiles = [...p.hand].sort((a, b) => a - b); + let drawn = null; + if (this.humanCanDiscard && this.gs.drawnTile !== null) { + const i = tiles.indexOf(this.gs.drawnTile); + if (i >= 0) { tiles.splice(i, 1); drawn = this.gs.drawnTile; } + } + const totalW = tiles.length * HAND_STEP + (drawn !== null ? HAND_STEP + 22 : 0); + let x = 960 - totalW / 2 + HAND_STEP / 2; + const place = (kind, hx) => { + const c = this.makeTileFace(kind, HAND_TW, HAND_TH); + c.setPosition(hx, HAND_Y); + this.dyn.add(c); + if (this.humanCanDiscard) { + c.setSize(HAND_TW, HAND_TH); + c.setInteractive({ useHandCursor: true }); + c.on('pointerup', () => this.onHandTileClick(c, kind)); + } + this.handTiles.push({ c, kind }); + return c; + }; + for (const k of tiles) { place(k, x); x += HAND_STEP; } + if (drawn !== null) place(drawn, x + 22).y -= 10; + } + + renderHumanMeldsAndBonus() { + const p = this.gs.players[0]; + let x = 220; + for (const m of p.melds) { + const row = this.makeMeldRow(m, SM_W, SM_H, true); + const w = m.kinds.length * (SM_W + 2); + row.setPosition(x + w / 2, 878); + this.dyn.add(row); + x += w + 18; + } + let bx = 1680; + for (const b of [...p.bonus].reverse()) { + const tile = this.makeTileFace(b, SM_W, SM_H); + tile.setPosition(bx, 878); + this.dyn.add(tile); + bx -= SM_W + 4; + } + } + + renderOpponent(seat) { + const p = this.gs.players[seat]; + const n = p.hand.length; + const step = SM_W + 2; + + if (seat === 2) { // top — horizontal row of backs + let x = 960 - ((n - 1) * step) / 2; + for (let i = 0; i < n; i++) { + const back = this.makeTileBack(SM_W, SM_H); + back.setPosition(x, 70); + this.dyn.add(back); + x += step; + } + let mx = 1330; + for (const m of p.melds) { + const row = this.makeMeldRow(m, 40, 56, false); + const w = m.kinds.length * 42; + row.setPosition(mx + w / 2, 70); + this.dyn.add(row); + mx += w + 14; + } + let bx = 1330; + for (const b of p.bonus) { + const tile = this.makeTileFace(b, 36, 50); + tile.setPosition(bx, 132); + this.dyn.add(tile); + bx += 40; + } + } else { // sides — vertical column of rotated backs + const x = seat === 1 ? 1858 : 62; + let y = 580 - ((n - 1) * step) / 2; + for (let i = 0; i < n; i++) { + const back = this.makeTileBack(SM_W, SM_H); + back.setPosition(x, y).setAngle(seat === 1 ? -90 : 90); + this.dyn.add(back); + y += step; + } + const mx = seat === 1 ? 1745 : 175; + let my = 330; + for (const m of p.melds) { + const row = this.makeMeldRow(m, 38, 52, false); + row.setPosition(mx, my); + this.dyn.add(row); + my += 60; + } + p.bonus.forEach((b, i) => { + const tile = this.makeTileFace(b, 36, 50); + tile.setPosition(mx + ((i % 4) - 1.5) * 40, my + 8 + Math.floor(i / 4) * 56); + this.dyn.add(tile); + }); + } + } + + renderRivers() { + const gs = this.gs; + for (let seat = 0; seat < 4; seat++) { + const river = gs.discards[seat]; + const cfg = RIVERS[seat]; + const dir = seat === 0 ? -1 : 1; // extra rows grow toward the centre + river.forEach((k, i) => { + const row = Math.floor(i / 8); + const col = i % 8; + const tile = this.makeTileFace(k, RIV_W, RIV_H); + tile.setPosition(cfg.cx + (col - 3.5) * (RIV_W + 4), cfg.y0 + dir * row * cfg.dy); + this.dyn.add(tile); + // pulse the live discard + if (gs.phase === 'awaitClaims' && gs.lastDiscard + && gs.lastDiscard.from === seat && i === river.length - 1) { + const halo = this.add.graphics(); + halo.lineStyle(3, COLORS.gold, 1); + halo.strokeRoundedRect(-RIV_W / 2 - 3, -RIV_H / 2 - 3, RIV_W + 6, RIV_H + 6, 6); + tile.add(halo); + this.tweens.add({ targets: tile, scale: { from: 1.18, to: 1 }, duration: 240, ease: 'Quad.Out' }); + } + }); + } + } + + renderScoreboard() { + const gs = this.gs; + this.sbHeader.setText(`Hand ${gs.handNumber} · East round`); + for (let seat = 0; seat < 4; seat++) { + const p = gs.players[seat]; + const wind = WIND_NAMES[seatWindOf(gs, seat)][0]; + const dealer = gs.dealer === seat ? ' ◆' : ''; + this.sbRows[seat].name.setText(`${wind} · ${p.name}${dealer}`); + this.sbRows[seat].score.setText(String(p.score)); + } + } + + renderCentre() { + const gs = this.gs; + this.wallText.setText(`Wall: ${Math.max(0, gs.wallEnd - gs.wallPos + 1)}`); + if (this.gameOverShown || gs.phase === 'gameOver') { this.statusText.setText(''); return; } + if (gs.phase === 'handOver') { this.statusText.setText(''); return; } + if (gs.phase === 'awaitDiscard') { + const p = gs.players[gs.turn]; + this.statusText.setText(gs.turn === 0 ? 'Your turn — choose a tile to discard' : `${p.name} is thinking…`); + } else if (gs.phase === 'awaitClaims' && gs.lastDiscard) { + this.statusText.setText(`${gs.players[gs.lastDiscard.from].name} discards ${tileName(gs.lastDiscard.kind)}`); + } + } + + updatePortraitRings() { + for (let seat = 0; seat < 4; seat++) { + const { ring, x, y, r } = this.portraitCtrls[seat]; + ring.clear(); + if (seat === this.gs.turn && !isGameOver(this.gs) && this.gs.phase !== 'handOver') { + ring.lineStyle(4, COLORS.gold, 1); + ring.strokeCircle(x, y, r + 6); + } + } + } + + updateSelfButtons() { + const show = this.humanCanDiscard; + const acts = show ? selfActions(this.gs) : null; + this.selfWinBtn.setVisible(!!acts?.canWin); + this.kongSpecs = []; + if (acts) { + for (const k of acts.concealedKongs) this.kongSpecs.push({ type: 'concealed', kind: k }); + for (const k of acts.addedKongs) this.kongSpecs.push({ type: 'added', kind: k }); + } + this.selfKongBtns.forEach((btn, i) => { + const spec = this.kongSpecs[i]; + btn.setVisible(!!spec); + if (spec) btn.setLabel(`Kong ${shortName(spec.kind)}`); + }); + } + + // ── human input ─────────────────────────────────────────────────────────────── + onHandTileClick(c, kind) { + if (!this.humanCanDiscard || this.driving === 'modal') return; + if (this.sel && this.sel.c === c) { + playSound(this, SFX.PIECE_CLICK); + this.humanCanDiscard = false; + discardTile(this.gs, kind); + this.refresh(); + this.advance(); + return; + } + playSound(this, SFX.PIECE_CLICK); + if (this.sel) this.sel.c.y = this.sel.baseY; + this.sel = { c, baseY: c.y }; + c.y -= 16; + } + + onSelfWin() { + if (!this.humanCanDiscard) return; + this.humanCanDiscard = false; + declareWin(this.gs, 0, { byDiscard: false }); + this.refresh(); + this.advance(); + } + + onSelfKong(i) { + if (!this.humanCanDiscard || !this.kongSpecs[i]) return; + playSound(this, SFX.PIECE_CLICK); + declareKong(this.gs, this.kongSpecs[i]); + this.refresh(); + // still our discard (with the replacement tile) — or the wall just ran out + if (this.gs.phase !== 'awaitDiscard') this.advance(); + } + + promptHumanClaim(opts) { + return new Promise((resolve) => { + this.claimResolve = resolve; + this.chowOptions = opts.chows; + const visible = []; + if (opts.win) visible.push(this.claimBtns.win); + if (opts.kong) visible.push(this.claimBtns.kong); + if (opts.pung) visible.push(this.claimBtns.pung); + opts.chows.forEach((tiles, i) => { + const kind = this.gs.lastDiscard.kind; + const run = [...tiles, kind].sort((a, b) => a - b); + this.claimBtns.chows[i].setLabel(`Chow ${run.map((k) => rankOf(k)).join('-')}`); + visible.push(this.claimBtns.chows[i]); + }); + visible.push(this.claimBtns.pass); + const step = 226; + const left = 960 - ((visible.length - 1) * step) / 2; + visible.forEach((btn, i) => btn.setPosition(left + i * step, CLAIM_Y).setVisible(true)); + this.statusText.setText(`Claim the ${tileName(this.gs.lastDiscard.kind)}?`); + }); + } + + resolveClaim(claim) { + if (!this.claimResolve) return; + playSound(this, SFX.PIECE_CLICK); + const done = this.claimResolve; + this.claimResolve = null; + this.hideClaimButtons(); + done(claim); + } + + hideClaimButtons() { + this.claimBtns.win.setVisible(false); + this.claimBtns.kong.setVisible(false); + this.claimBtns.pung.setVisible(false); + this.claimBtns.chows.forEach((b) => b.setVisible(false)); + this.claimBtns.pass.setVisible(false); + } + + // ── turn driver ─────────────────────────────────────────────────────────────── + async advance() { + if (this.driving) return; + this.driving = true; + const gs = this.gs; + try { + for (;;) { + if (this.gameOverShown) return; + this.refresh(); + if (gs.phase === 'gameOver') { this.showGameOver(); return; } + if (gs.phase === 'handOver') { + await this.showHandEnd(); + startNextHand(gs); + continue; + } + if (gs.phase === 'awaitDiscard') { + if (gs.turn === 0) return; // wait for the human (buttons + hand clicks live) + const seat = gs.turn; + const skill = gs.players[seat].skill; + await this.delay(nextThinkDelay(skill)); + if (this.gameOverShown) return; + const acts = selfActions(gs); + const sa = chooseSelfAction(gs, seat, acts, skill); + if (sa?.type === 'win') { declareWin(gs, seat, { byDiscard: false }); continue; } + if (sa?.type === 'kong') { + declareKong(gs, sa.spec); + await this.toast(`${gs.players[seat].name}: Kong!`); + continue; + } + discardTile(gs, chooseDiscard(gs, seat, skill)); + playSound(this, SFX.PIECE_CLICK); + continue; + } + if (gs.phase === 'awaitClaims') { + const intents = collectAIClaims(gs, 0); + const humanOpts = claimOptionsFor(gs, 0); + if (humanOpts) { + const claim = await this.promptHumanClaim(humanOpts); + if (claim) intents.push({ seat: 0, claim }); + } else { + await this.delay(420); // beat so the discard registers + } + const { applied } = resolveClaims(gs, intents); + if (applied && applied.claim.type !== 'win' && applied.seat !== 0) { + const label = applied.claim.type === 'chow' ? 'Chow' : applied.claim.type === 'pung' ? 'Pung' : 'Kong'; + await this.toast(`${gs.players[applied.seat].name}: ${label}!`); + } + continue; + } + return; // unknown phase — bail rather than spin + } + } finally { + this.driving = false; + } + } + + // ── hand end / game over ────────────────────────────────────────────────────── + showHandEnd() { + return new Promise((resolve) => { + const gs = this.gs; + const r = gs.result; + const modal = this.add.container(0, 0).setDepth(DEPTH.modal); + modal.add(this.add.rectangle(960, 540, GAME_WIDTH, GAME_HEIGHT, 0x000000, 0.65).setInteractive()); + + const isDraw = r.type === 'draw'; + const faanRows = isDraw ? [] : r.faanList; + const h = isDraw ? 280 : Math.min(940, 360 + faanRows.length * 32 + 130); + const top = 540 - h / 2; + modal.add(this.add.rectangle(960, 540, 980, h, COLORS.panel, 1).setStrokeStyle(2, COLORS.accent)); + + if (isDraw) { + modal.add(this.add.text(960, top + 70, 'Wall exhausted', { + fontFamily: 'Righteous', fontSize: '44px', color: COLORS.goldHex, + }).setOrigin(0.5)); + modal.add(this.add.text(960, top + 130, 'Nobody wins — the dealer redeals.', { + fontFamily: '"Julius Sans One"', fontSize: '24px', color: COLORS.textHex, + }).setOrigin(0.5)); + } else { + const winner = gs.players[r.winner]; + playSound(this, SFX.CASINO_WIN); + this.portraitCtrls[r.winner]?.controller?.playEmotion?.('happy'); + if (r.byDiscard) this.portraitCtrls[r.discarder]?.controller?.playEmotion?.('upset'); + modal.add(this.add.text(960, top + 52, `${winner.name} wins!`, { + fontFamily: 'Righteous', fontSize: '44px', color: COLORS.goldHex, + }).setOrigin(0.5)); + modal.add(this.add.text(960, top + 100, r.byDiscard + ? `off ${gs.players[r.discarder].name}'s ${tileName(r.winTile)}` + : `self-drawn ${tileName(r.winTile)}`, { + fontFamily: '"Julius Sans One"', fontSize: '22px', color: COLORS.mutedHex, + }).setOrigin(0.5)); + + // the winning hand: melds, then concealed tiles, winning tile last + const concealed = [...winner.hand].sort((a, b) => a - b); + if (!r.byDiscard) { + const i = concealed.indexOf(r.winTile); + if (i >= 0) concealed.splice(i, 1); + } + const groups = []; + for (const m of winner.melds) groups.push(m.kinds); + groups.push(concealed); + const flatW = groups.reduce((a, g2) => a + g2.length * (SM_W + 2) + 14, 0) + SM_W + 24; + let x = 960 - flatW / 2; + const tileY = top + 170; + for (const g2 of groups) { + for (const k of g2) { + const tile = this.makeTileFace(k, SM_W, SM_H); + tile.setPosition(x + SM_W / 2, tileY); + modal.add(tile); + x += SM_W + 2; + } + x += 14; + } + const winTileC = this.makeTileFace(r.winTile, SM_W, SM_H, { highlight: true }); + winTileC.setPosition(x + 10 + SM_W / 2, tileY); + modal.add(winTileC); + + let y = top + 240; + for (const fr of faanRows) { + modal.add(this.add.text(620, y, fr.label, { + fontFamily: '"Julius Sans One"', fontSize: '20px', color: COLORS.textHex, + }).setOrigin(0, 0.5)); + modal.add(this.add.text(1300, y, `${fr.faan} faan`, { + fontFamily: '"Julius Sans One"', fontSize: '20px', color: COLORS.accentHex, + }).setOrigin(1, 0.5)); + y += 32; + } + y += 8; + modal.add(this.add.text(620, y, `Total: ${r.faan} faan → ${r.base} base points`, { + fontFamily: 'Righteous', fontSize: '24px', color: COLORS.goldHex, + }).setOrigin(0, 0.5)); + y += 44; + for (let seat = 0; seat < 4; seat++) { + const d = r.payments[seat]; + if (d === 0) continue; + modal.add(this.add.text(620, y, gs.players[seat].name, { + fontFamily: '"Julius Sans One"', fontSize: '20px', color: PLAYER_COLOR_HEX[seat], + }).setOrigin(0, 0.5)); + modal.add(this.add.text(1300, y, (d > 0 ? '+' : '') + d, { + fontFamily: 'Righteous', fontSize: '22px', color: d > 0 ? COLORS.goldHex : COLORS.dangerHex, + }).setOrigin(1, 0.5)); + y += 32; + } + } + + const repeat = isDraw || r.winner === gs.dealer; + const ending = !repeat && gs.dealer === 3; + const btn = new Button(this, 960, top + h - 56, ending ? 'Final Scores' : 'Next Hand', () => { + modal.destroy(true); + resolve(); + }, { width: 280, fontSize: 24 }); + btn.setDepth(DEPTH.modal); + modal.add(btn); + }); + } + + showGameOver() { + if (this.gameOverShown) return; + this.gameOverShown = true; + playSound(this, SFX.VICTORY_SHORT); + this.postHistory().catch(() => {}); + + this.add.rectangle(960, 540, GAME_WIDTH, GAME_HEIGHT, 0x000000, 0.65) + .setInteractive().setDepth(DEPTH.modal); + const panelW = 720, panelH = 440; + this.add.rectangle(960, 540, panelW, panelH, COLORS.panel, 1) + .setStrokeStyle(2, COLORS.accent).setDepth(DEPTH.modal); + this.add.text(960, 540 - panelH / 2 + 48, 'Final Scores', { + fontFamily: 'Righteous', fontSize: '42px', color: COLORS.goldHex, + }).setOrigin(0.5).setDepth(DEPTH.modal); + + const winners = new Set(getWinners(this.gs)); + const order = [...this.gs.players].sort((a, b) => b.score - a.score); + let rowY = 540 - panelH / 2 + 110; + for (const p of order) { + const isWin = winners.has(p.seat); + const color = isWin ? COLORS.goldHex : COLORS.textHex; + this.add.text(960 - panelW / 2 + 40, rowY, `${isWin ? '★ ' : ' '}${p.name}`, { + fontFamily: 'Righteous', fontSize: '24px', color, + }).setOrigin(0, 0.5).setDepth(DEPTH.modal); + this.add.text(960 + panelW / 2 - 40, rowY, String(p.score), { + fontFamily: 'Righteous', fontSize: '28px', color, + }).setOrigin(1, 0.5).setDepth(DEPTH.modal); + rowY += 52; + } + new Button(this, 960, 540 + panelH / 2 - 48, 'Back to Menu', + () => this.scene.start('GameMenu'), { width: 280, fontSize: 24 }, + ).setDepth(DEPTH.modal); + } + + async postHistory() { + const totals = this.gs.players.map((p) => p.score); + const winners = new Set(getWinners(this.gs)); + let result; + if (winners.has(this.humanSeat) && winners.size === 1) result = 'win'; + else if (winners.has(this.humanSeat)) result = 'draw'; + else result = 'loss'; + await api.post('/history/single-player', { + slug: 'mahjong', + score: totals[this.humanSeat], + opponentScores: totals.filter((_, i) => i !== this.humanSeat), + result, + }); + } + + // ── small fx ────────────────────────────────────────────────────────────────── + toast(msg) { + playSound(this, SFX.PIECE_CLICK); + const t = this.add.text(960, 560, msg, { + fontFamily: 'Righteous', fontSize: '46px', color: COLORS.goldHex, + }).setOrigin(0.5).setDepth(DEPTH.toast).setAngle(-4); + this.tweens.add({ targets: t, scale: { from: 0.7, to: 1.05 }, duration: 200, ease: 'Back.Out' }); + return new Promise((resolve) => { + this.time.delayedCall(850, () => { + this.tweens.add({ targets: t, alpha: 0, duration: 200, onComplete: () => { t.destroy(); resolve(); } }); + }); + }); + } + + delay(ms) { return new Promise((resolve) => this.time.delayedCall(ms, resolve)); } +} + +// Short tile tag for button labels, e.g. "5 Circle" or "East". +function shortName(kind) { + if (isSuited(kind)) return `${rankOf(kind)} ${SUIT_NAMES[Math.floor(kind / 9)]}`; + return TILES[kind].name.replace(' Wind', '').replace(' Dragon', ''); +} diff --git a/public/src/games/mahjong/MahjongLogic.js b/public/src/games/mahjong/MahjongLogic.js new file mode 100644 index 0000000..6851102 --- /dev/null +++ b/public/src/games/mahjong/MahjongLogic.js @@ -0,0 +1,602 @@ +// Mahjong (Hong Kong style) — pure game engine. No Phaser, no timers. +// Deterministic given a seed so AI self-play is reproducible. +// +// One session is one full East round: every player deals at least once; the +// dealer repeats after a dealer win or a wall-exhausted draw (goulash). Chow +// claims come only from the player to the left; pung/kong/win from anyone. +// Flowers and seasons are revealed and replaced immediately. Kong and bonus +// replacement tiles draw from the back of the live wall (no reserved dead +// wall). Deliberately out of scope: robbing the kong, rare limit hands other +// than Thirteen Orphans, and sacred-discard etiquette rules. +// +// Hands are stored merged: while a player is awaiting discard their hand +// holds 14 − 3·melds tiles (the drawn tile already in it, with state.drawnTile +// recording which kind arrived); otherwise 13 − 3·melds. + +import { + buildWall, isSuited, isHonor, isBonus, rankOf, + WIND_KINDS, DRAGON_KINDS, FIRST_BONUS, + FAAN_BY_ID, MIN_FAAN, LIMIT_FAAN, basePoints, +} from './MahjongData.js'; + +// ── seeded RNG (mulberry32) ────────────────────────────────────────────────── +function mulberry32(seed) { + let a = seed >>> 0; + return function () { + a |= 0; a = (a + 0x6d2b79f5) | 0; + let t = Math.imul(a ^ (a >>> 15), 1 | a); + t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; + return ((t ^ (t >>> 14)) >>> 0) / 4294967296; + }; +} + +// ── small helpers ──────────────────────────────────────────────────────────── +export function counts34(tiles) { + const c = new Array(34).fill(0); + for (const k of tiles) c[k]++; + return c; +} + +const sortHand = (hand) => hand.sort((a, b) => a - b); + +export const seatWindOf = (state, seat) => (seat - state.dealer + 4) % 4; + +// ── state ──────────────────────────────────────────────────────────────────── +export function createInitialState({ names = [], skills = {}, seed } = {}) { + const rng = mulberry32((seed ?? (Date.now() & 0x7fffffff)) | 0); + const players = []; + for (let seat = 0; seat < 4; seat++) { + players.push({ + name: names[seat] ?? `Player ${seat + 1}`, + seat, + score: 0, + skill: skills[seat] ?? 3, + isAI: seat !== 0, + hand: [], + melds: [], // { type: 'chow'|'pung'|'kong', kinds, concealed, from } + bonus: [], // revealed flowers / seasons + }); + } + const state = { + players, + wall: [], wallPos: 0, wallEnd: 143, + dealer: 0, + roundWind: 0, // East round only — one full round per session + handNumber: 1, + turn: 0, + phase: 'awaitDiscard', // awaitDiscard | awaitClaims | handOver | gameOver + drawnTile: null, + lastDrawWasKongReplacement: false, + lastDiscard: null, // { kind, from } + discards: [[], [], [], []], + result: null, + winners: [], + _rng: rng, + }; + dealHand(state); + return state; +} + +export function cloneState(s) { + const rngState = s._rng; // functions don't clone; lookahead must not draw tiles + const copy = JSON.parse(JSON.stringify({ ...s, _rng: undefined })); + copy._rng = rngState; + return copy; +} + +export function dealHand(state) { + const rng = state._rng; + const wall = buildWall(); + for (let i = wall.length - 1; i > 0; i--) { + const j = Math.floor(rng() * (i + 1)); + [wall[i], wall[j]] = [wall[j], wall[i]]; + } + state.wall = wall; + state.wallPos = 0; + state.wallEnd = wall.length - 1; + state.discards = [[], [], [], []]; + state.result = null; + state.lastDiscard = null; + state.drawnTile = null; + state.lastDrawWasKongReplacement = false; + for (const p of state.players) { p.hand = []; p.melds = []; p.bonus = []; } + + // 13 tiles each (dealer first), then reveal and replace bonus tiles in the + // same order. The wall cannot run out during the deal. + for (let i = 0; i < 4; i++) { + const p = state.players[(state.dealer + i) % 4]; + for (let n = 0; n < 13; n++) p.hand.push(state.wall[state.wallPos++]); + } + for (let i = 0; i < 4; i++) { + const p = state.players[(state.dealer + i) % 4]; + for (let h = 0; h < p.hand.length; h++) { + while (isBonus(p.hand[h])) { + p.bonus.push(p.hand[h]); + p.hand[h] = state.wall[state.wallEnd--]; + } + } + sortHand(p.hand); + } + + state.turn = state.dealer; + drawForCurrent(state); // dealer's 14th tile + return state; +} + +// Draw from the front of the wall for the player on turn. Bonus tiles are +// revealed and replaced from the back. Ends the hand in a draw when the wall +// is exhausted. +export function drawForCurrent(state) { + if (state.wallPos > state.wallEnd) return endInDraw(state); + const p = state.players[state.turn]; + let k = state.wall[state.wallPos++]; + while (isBonus(k)) { + p.bonus.push(k); + if (state.wallPos > state.wallEnd) return endInDraw(state); + k = state.wall[state.wallEnd--]; + } + p.hand.push(k); + sortHand(p.hand); + state.drawnTile = k; + state.lastDrawWasKongReplacement = false; + state.phase = 'awaitDiscard'; + return state; +} + +// Replacement draw after a kong — from the back of the wall. +function drawReplacement(state) { + const p = state.players[state.turn]; + for (;;) { + if (state.wallPos > state.wallEnd) return endInDraw(state); + const k = state.wall[state.wallEnd--]; + if (isBonus(k)) { p.bonus.push(k); continue; } + p.hand.push(k); + sortHand(p.hand); + state.drawnTile = k; + state.lastDrawWasKongReplacement = true; + state.phase = 'awaitDiscard'; + return state; + } +} + +function endInDraw(state) { + state.drawnTile = null; + state.result = { type: 'draw' }; + state.phase = 'handOver'; + return state; +} + +const removeFromHand = (hand, kind, n) => { + for (let i = 0; i < n; i++) hand.splice(hand.indexOf(kind), 1); +}; + +// Discard `kind` from the hand of the player on turn. Returns false if the +// tile isn't held. +export function discardTile(state, kind) { + if (state.phase !== 'awaitDiscard') return false; + const p = state.players[state.turn]; + if (!p.hand.includes(kind)) return false; + removeFromHand(p.hand, kind, 1); + state.discards[state.turn].push(kind); + state.lastDiscard = { kind, from: state.turn }; + state.drawnTile = null; + state.lastDrawWasKongReplacement = false; + state.phase = 'awaitClaims'; + return true; +} + +// What `seat` may do with the tile just discarded. Null when nothing. +// Chows only off the player to the left. Win is only offered when the hand +// would score at least MIN_FAAN. +export function claimOptionsFor(state, seat) { + if (state.phase !== 'awaitClaims' || !state.lastDiscard) return null; + const { kind, from } = state.lastDiscard; + if (seat === from) return null; + const p = state.players[seat]; + const n = p.hand.filter((t) => t === kind).length; + const opts = { win: false, pung: n >= 2, kong: n >= 3, chows: [] }; + if (seat === (from + 1) % 4 && isSuited(kind)) { + const r = rankOf(kind); + const has = (kk) => p.hand.includes(kk); + if (r >= 3 && has(kind - 2) && has(kind - 1)) opts.chows.push([kind - 2, kind - 1]); + if (r >= 2 && r <= 8 && has(kind - 1) && has(kind + 1)) opts.chows.push([kind - 1, kind + 1]); + if (r <= 7 && has(kind + 1) && has(kind + 2)) opts.chows.push([kind + 1, kind + 2]); + } + if (canWinWith(p.hand, p.melds, kind)) { + opts.win = evaluateWin(state, seat, kind, true).faan >= MIN_FAAN; + } + return (opts.win || opts.pung || opts.kong || opts.chows.length) ? opts : null; +} + +// Resolve all claim intents on the current discard. intents is an array of +// { seat, claim } where claim is null (pass) or { type, tiles? }. Priority: +// win beats kong/pung beats chow; among multiple wins the seat nearest +// counter-clockwise from the discarder takes it (one-winner rule). With no +// claims the next player simply draws. Returns { applied } for animation. +export function resolveClaims(state, intents) { + if (state.phase !== 'awaitClaims' || !state.lastDiscard) return { applied: null }; + const { kind, from } = state.lastDiscard; + const bySeat = new Map(); + for (const i of intents) if (i && i.claim) bySeat.set(i.seat, i.claim); + + let chosen = null; + for (let d = 1; d <= 3 && !chosen; d++) { + const s = (from + d) % 4; + if (bySeat.get(s)?.type === 'win') chosen = { seat: s, claim: bySeat.get(s) }; + } + if (!chosen) { + for (const [s, c] of bySeat) { + if (c.type === 'kong' || c.type === 'pung') { chosen = { seat: s, claim: c }; break; } + } + } + if (!chosen) { + for (const [s, c] of bySeat) if (c.type === 'chow') { chosen = { seat: s, claim: c }; break; } + } + + if (!chosen) { + state.lastDiscard = null; + state.turn = (from + 1) % 4; + drawForCurrent(state); + return { applied: null }; + } + + const p = state.players[chosen.seat]; + const c = chosen.claim; + if (c.type === 'win') { + declareWin(state, chosen.seat, { byDiscard: true }); + } else if (c.type === 'pung') { + state.discards[from].pop(); + removeFromHand(p.hand, kind, 2); + p.melds.push({ type: 'pung', kinds: [kind, kind, kind], concealed: false, from }); + state.turn = chosen.seat; + state.drawnTile = null; + state.lastDiscard = null; + state.phase = 'awaitDiscard'; + } else if (c.type === 'kong') { + state.discards[from].pop(); + removeFromHand(p.hand, kind, 3); + p.melds.push({ type: 'kong', kinds: [kind, kind, kind, kind], concealed: false, from }); + state.turn = chosen.seat; + state.lastDiscard = null; + drawReplacement(state); + } else { // chow + state.discards[from].pop(); + const [a, b] = c.tiles; + removeFromHand(p.hand, a, 1); + removeFromHand(p.hand, b, 1); + p.melds.push({ type: 'chow', kinds: [a, b, kind].sort((x, y) => x - y), concealed: false, from }); + state.turn = chosen.seat; + state.drawnTile = null; + state.lastDiscard = null; + state.phase = 'awaitDiscard'; + } + return { applied: chosen }; +} + +// Options for the player on turn while awaiting discard: self-drawn win and +// kong declarations. Self-win is only possible on a drawn tile (a hand +// completed by claiming would have been won off the discard instead). +export function selfActions(state) { + const res = { canWin: false, concealedKongs: [], addedKongs: [] }; + if (state.phase !== 'awaitDiscard') return res; + const p = state.players[state.turn]; + const c = counts34(p.hand); + for (let k = 0; k < 34; k++) if (c[k] === 4) res.concealedKongs.push(k); + for (const m of p.melds) { + if (m.type === 'pung' && p.hand.includes(m.kinds[0])) res.addedKongs.push(m.kinds[0]); + } + if (state.drawnTile !== null) { + const need = 4 - p.melds.length; + if (isThirteenOrphans(c) && p.melds.length === 0) { + res.canWin = evaluateWin(state, state.turn, state.drawnTile, false).faan >= MIN_FAAN; + } else if (decompose(c, need).length > 0) { + res.canWin = evaluateWin(state, state.turn, state.drawnTile, false).faan >= MIN_FAAN; + } + } + return res; +} + +// Declare a concealed or added kong for the player on turn, then draw the +// replacement tile. (Exposed kongs of a discard go through resolveClaims.) +export function declareKong(state, { type, kind }) { + if (state.phase !== 'awaitDiscard') return false; + const p = state.players[state.turn]; + if (type === 'concealed') { + if (p.hand.filter((t) => t === kind).length !== 4) return false; + removeFromHand(p.hand, kind, 4); + p.melds.push({ type: 'kong', kinds: [kind, kind, kind, kind], concealed: true, from: null }); + } else if (type === 'added') { + const m = p.melds.find((x) => x.type === 'pung' && x.kinds[0] === kind); + if (!m || !p.hand.includes(kind)) return false; + removeFromHand(p.hand, kind, 1); + m.type = 'kong'; + m.kinds.push(kind); + } else { + return false; + } + drawReplacement(state); + return true; +} + +// Declare a win for `seat` — self-drawn or off the current discard. Applies +// payments and ends the hand. +export function declareWin(state, seat, { byDiscard }) { + const winTile = byDiscard ? state.lastDiscard.kind : state.drawnTile; + const res = evaluateWin(state, seat, winTile, byDiscard); + const discarder = byDiscard ? state.lastDiscard.from : null; + if (byDiscard) state.discards[discarder].pop(); + for (let s = 0; s < 4; s++) state.players[s].score += res.payments[s]; + state.result = { + type: 'win', winner: seat, byDiscard, discarder, winTile, + faanList: res.faanList, faan: res.faan, base: res.base, payments: res.payments, + }; + state.lastDiscard = null; + state.phase = 'handOver'; + return state; +} + +// Advance to the next hand. The dealer repeats after a dealer win or a drawn +// hand; otherwise the deal passes on, and once the fourth dealer's turn ends +// the round — and the session — is over. +export function startNextHand(state) { + if (state.phase !== 'handOver') return false; + const r = state.result; + const repeat = r.type === 'draw' || (r.type === 'win' && r.winner === state.dealer); + if (!repeat) { + if (state.dealer === 3) { + state.phase = 'gameOver'; + state.winners = computeWinners(state); + return true; + } + state.dealer += 1; + } + state.handNumber += 1; + dealHand(state); + return true; +} + +function computeWinners(state) { + let max = -Infinity; + state.players.forEach((p) => { if (p.score > max) max = p.score; }); + return state.players.filter((p) => p.score === max).map((p) => p.seat); +} + +export function isGameOver(state) { return state.phase === 'gameOver'; } +export function getWinners(state) { return state.winners; } + +// ── hand evaluation ────────────────────────────────────────────────────────── + +// All ways to read `counts` as `need` sets plus a pair. Returns an array of +// { pair, sets: [{ type: 'chow'|'pung', kind }] } — possibly with duplicate +// readings, which is harmless since the evaluator takes the max-faan one. +export function decompose(counts, need = 4) { + const total = counts.reduce((a, b) => a + b, 0); + if (total !== need * 3 + 2) return []; + const out = []; + const c = counts.slice(); + const sets = []; + let pairKind = -1; + const rec = (start) => { + let k = start; + while (k < 34 && c[k] === 0) k++; + if (k >= 34) { out.push({ pair: pairKind, sets: sets.slice() }); return; } + if (c[k] >= 3) { + c[k] -= 3; sets.push({ type: 'pung', kind: k }); + rec(k); + sets.pop(); c[k] += 3; + } + if (isSuited(k) && rankOf(k) <= 7 && c[k + 1] > 0 && c[k + 2] > 0) { + c[k]--; c[k + 1]--; c[k + 2]--; sets.push({ type: 'chow', kind: k }); + rec(k); + sets.pop(); c[k]++; c[k + 1]++; c[k + 2]++; + } + }; + for (let p = 0; p < 34; p++) { + if (c[p] < 2) continue; + c[p] -= 2; pairKind = p; + rec(0); + c[p] += 2; + } + return out; +} + +const ORPHAN_KINDS = [0, 8, 9, 17, 18, 26, 27, 28, 29, 30, 31, 32, 33]; + +export function isThirteenOrphans(counts) { + let total = 0, dup = 0; + for (const k of ORPHAN_KINDS) { + if (counts[k] === 0) return false; + if (counts[k] >= 2) dup++; + total += counts[k]; + } + return total === 14 && dup === 1; +} + +// Would adding `kind` to this concealed hand complete it? +export function canWinWith(hand, melds, kind) { + const c = counts34(hand); + c[kind]++; + if (melds.length === 0 && isThirteenOrphans(c)) return true; + return decompose(c, 4 - melds.length).length > 0; +} + +// Every kind a 13 − 3·melds hand is waiting on. +export function winningTiles(hand, melds) { + const out = []; + for (let k = 0; k < 34; k++) if (canWinWith(hand, melds, k)) out.push(k); + return out; +} + +// Standard-form shanten (sets·2 + partials + pair maximisation), min'd with +// Thirteen Orphans when fully concealed. −1 means a complete hand. Used by +// the AI; counts must hold 13 − 3·meldCount tiles (or 14 − 3·meldCount when +// measuring a hand that still has to discard — the result is then the +// shanten after its best discard). +export function shanten(counts, meldCount = 0) { + const maxSets = 4 - meldCount; + const c = counts.slice(0, 34); + let bestValue = 0; + const rec = (start, sets, partials, pair) => { + // partials beyond block capacity (4 sets total incl. melds) don't count + const value = sets * 2 + Math.min(partials, maxSets - sets) + (pair ? 1 : 0); + if (value > bestValue) bestValue = value; + // even turning everything left into sets cannot beat the best found + if (value + (maxSets - sets) * 2 + (pair ? 0 : 1) <= bestValue) return; + let k = start; + while (k < 34 && c[k] === 0) k++; + if (k >= 34) return; + if (sets < maxSets) { + if (c[k] >= 3) { + c[k] -= 3; + rec(k, sets + 1, partials, pair); + c[k] += 3; + } + if (isSuited(k) && rankOf(k) <= 7 && c[k + 1] > 0 && c[k + 2] > 0) { + c[k]--; c[k + 1]--; c[k + 2]--; + rec(k, sets + 1, partials, pair); + c[k]++; c[k + 1]++; c[k + 2]++; + } + } + if (!pair && c[k] >= 2) { + c[k] -= 2; + rec(k, sets, partials, true); + c[k] += 2; + } + if (sets + partials < maxSets) { + if (c[k] >= 2) { + c[k] -= 2; + rec(k, sets, partials + 1, pair); + c[k] += 2; + } + if (isSuited(k) && rankOf(k) <= 8 && c[k + 1] > 0) { + c[k]--; c[k + 1]--; + rec(k, sets, partials + 1, pair); + c[k]++; c[k + 1]++; + } + if (isSuited(k) && rankOf(k) <= 7 && c[k + 2] > 0) { + c[k]--; c[k + 2]--; + rec(k, sets, partials + 1, pair); + c[k]++; c[k + 2]++; + } + } + c[k]--; // leave this tile as a floater + rec(k, sets, partials, pair); + c[k]++; + }; + rec(0, 0, 0, false); + let best = maxSets * 2 - bestValue; + if (meldCount === 0) { + let kinds = 0, hasDup = false; + for (const k of ORPHAN_KINDS) { + if (counts[k] >= 1) kinds++; + if (counts[k] >= 2) hasDup = true; + } + const sh13 = 13 - kinds - (hasDup ? 1 : 0); + if (sh13 < best) best = sh13; + } + return best; +} + +// ── faan scoring ───────────────────────────────────────────────────────────── + +const row = (id, times = 1) => { + const r = FAAN_BY_ID[id]; + const out = []; + for (let i = 0; i < times; i++) out.push({ id: r.id, label: r.label, faan: r.faan }); + return out; +}; + +// Faan rows for one reading of the hand (concealed decomposition + melds). +function patternRows(dec, meldSets, seatWindKind, roundWindKind) { + const sets = dec.sets.concat(meldSets); + const rows = []; + + // suit purity over every set and the pair (chows are always suited) + const kinds = [dec.pair, ...sets.map((s) => s.kind)]; + const suits = new Set(); + let hasHonor = false; + for (const k of kinds) { + if (isSuited(k)) suits.add(Math.floor(k / 9)); + else hasHonor = true; + } + for (const s of sets) if (s.type === 'chow') suits.add(Math.floor(s.kind / 9)); + if (suits.size === 0) rows.push(...row('all-honors')); + else if (suits.size === 1) rows.push(...row(hasHonor ? 'mixed-one-suit' : 'pure-one-suit')); + + if (sets.every((s) => s.type === 'chow') && isSuited(dec.pair)) rows.push(...row('common-hand')); + if (sets.every((s) => s.type === 'pung')) rows.push(...row('all-pungs')); + + const dragonPungs = sets.filter((s) => s.type === 'pung' && DRAGON_KINDS.includes(s.kind)).length; + const dragonPair = DRAGON_KINDS.includes(dec.pair); + if (dragonPungs === 3) rows.push(...row('great-dragons')); + else if (dragonPungs === 2 && dragonPair) rows.push(...row('small-dragons')); + else if (dragonPungs > 0) rows.push(...row('dragon-pung', dragonPungs)); + + const windPungKinds = sets.filter((s) => s.type === 'pung' && WIND_KINDS.includes(s.kind)).map((s) => s.kind); + const windPair = WIND_KINDS.includes(dec.pair); + if (windPungKinds.length === 4) rows.push(...row('great-winds')); + else if (windPungKinds.length === 3 && windPair) rows.push(...row('small-winds')); + else { + if (windPungKinds.includes(seatWindKind)) rows.push(...row('seat-wind')); + if (windPungKinds.includes(roundWindKind)) rows.push(...row('round-wind')); + } + return applyExcludes(rows); +} + +function applyExcludes(rows) { + const excluded = new Set(); + for (const r of rows) for (const ex of FAAN_BY_ID[r.id].excludes ?? []) excluded.add(ex); + return rows.filter((r) => !excluded.has(r.id)); +} + +// Score a completed hand for `seat`. winTile is added to the concealed hand +// for discard wins (it is already in the hand for self-draws). Returns +// { faanList, faan, base, payments } — payments is a zero-sum array of four +// score deltas: 2× base from the discarder, or 1× base from everyone on a +// self-draw. +export function evaluateWin(state, seat, winTile, byDiscard) { + const p = state.players[seat]; + const counts = counts34(byDiscard ? p.hand.concat([winTile]) : p.hand); + const seatWindKind = WIND_KINDS[seatWindOf(state, seat)]; + const roundWindKind = WIND_KINDS[state.roundWind]; + const meldSets = p.melds.map((m) => ({ type: m.type === 'chow' ? 'chow' : 'pung', kind: m.kinds[0] })); + + let bestRows = []; + let bestFaan = -1; + if (p.melds.length === 0 && isThirteenOrphans(counts)) { + bestRows = row('thirteen-orphans'); + bestFaan = LIMIT_FAAN; + } else { + for (const dec of decompose(counts, 4 - p.melds.length)) { + const rows = patternRows(dec, meldSets, seatWindKind, roundWindKind); + const total = rows.reduce((a, r) => a + r.faan, 0); + if (total > bestFaan) { bestFaan = total; bestRows = rows; } + } + if (bestFaan < 0) { bestFaan = 0; bestRows = []; } // defensive; callers check canWinWith + } + + const faanList = bestRows.slice(); + if (!byDiscard) faanList.push(...row('self-draw')); + if (p.melds.every((m) => m.concealed)) faanList.push(...row('concealed')); + if (!byDiscard && state.wallPos > state.wallEnd) faanList.push(...row('last-tile')); + if (!byDiscard && state.lastDrawWasKongReplacement) faanList.push(...row('kong-draw')); + + const seatWind = seatWindOf(state, seat); + const own = p.bonus.filter((b) => (b - FIRST_BONUS) % 4 === seatWind).length; + if (own > 0) faanList.push(...row('seat-flower', own)); + if ([34, 35, 36, 37].every((k) => p.bonus.includes(k))) faanList.push(...row('flower-set')); + if ([38, 39, 40, 41].every((k) => p.bonus.includes(k))) faanList.push(...row('season-set')); + if (p.bonus.length === 0) faanList.push(...row('no-bonus')); + + const faan = Math.min(faanList.reduce((a, r) => a + r.faan, 0), LIMIT_FAAN); + const base = basePoints(faan); + const payments = [0, 0, 0, 0]; + if (byDiscard) { + payments[seat] = 2 * base; + payments[state.lastDiscard.from] = -2 * base; + } else { + payments[seat] = 3 * base; + for (let s = 0; s < 4; s++) if (s !== seat) payments[s] = -base; + } + return { faanList, faan, base, payments }; +} diff --git a/public/src/games/mahjong/tutorial.md b/public/src/games/mahjong/tutorial.md new file mode 100644 index 0000000..dc6d9af --- /dev/null +++ b/public/src/games/mahjong/tutorial.md @@ -0,0 +1,91 @@ +# Welcome to the Mahjong Table! + +*By Auntie Mei — teahouse owner, undefeated since 1987, owner of the loudest tile-shuffle in Kowloon* + +--- + +Ahhh, you came! Sit, sit. Pour yourself some tea — the pu-erh, not the cheap stuff, you are my guest. You hear that sound? *Clack clack clack.* That is the sound of one hundred and forty-four tiles being shuffled, and it is the most beautiful sound in the world. My grandmother taught me this game, her grandmother taught her, and now Auntie Mei teaches you. Pay attention. There WILL be a quiz, and the quiz is me taking all your points. + +## What's the Goal? + +You and three opponents each build a hand of tiles. The first player to complete a winning hand — **four sets and a pair** — shouts *"Sik wu!"* (or clicks the big Mahjong button, very dignified) and collects points from the others. + +A **set** is one of: +- **Chow** — three tiles in a row of the same suit, like 4-5-6 of Circles +- **Pung** — three identical tiles +- **Kong** — four identical tiles (a special pung with a bonus draw) + +A **pair** is two identical tiles. Four sets plus the pair makes 14 tiles. That's it. That is the whole shape of the game. Everything else is seasoning. + +One session is **one full round**: every player gets to be the dealer (East) at least once. When the fourth dealer finally loses the deal, we count the points, and whoever has the most buys the dim sum. I mean, wins. + +## The Tiles + +Three suits run 1 to 9: **Bamboo**, **Circles**, and **Characters**. Four of each tile. Then the honor tiles: the four **Winds** (East, South, West, North) and the three **Dragons** (Red, Green, White — the white one is the blank tile; my nephew thought it was broken; he is no longer invited). + +There are also eight **bonus tiles** — four Flowers and four Seasons. They are not part of your hand. When you draw one, it is set aside face up and you draw a replacement tile. Free decoration, free faan. We like the flowers very much. + +## How a Turn Works + +On your turn you **draw one tile** from the wall, then **discard one tile** face up in front of you. That's the whole rhythm: draw, discard, draw, discard, *clack clack*. Your hand stays at 13 tiles between turns. + +But here is where mahjong becomes MAHJONG — other players' discards are not garbage. They are opportunity: + +- **Pung!** — anyone holding two of the discarded tile may claim it for a pung. Play jumps to them. +- **Kong!** — anyone holding three of it may claim it for a kong, and takes a replacement tile from the back of the wall. +- **Chow** — only the player to the discarder's **left** (that's the next player) may claim it to complete a run. +- **Mahjong!** — anyone may claim any discard that completes their winning hand. This beats every other claim. + +Claimed sets are placed face up beside your hand. They still count toward your four sets, but everyone can see what you're collecting — and a hand with no claimed sets earns a bonus for staying **concealed**. Choices, choices. + +If the wall runs out before anyone wins, the hand is a draw — nobody pays, and the same dealer redeals. The dealer also keeps the deal whenever the dealer wins. A hot dealer can run the table for a long time. (It is usually me.) + +## You Need Faan to Win + +You cannot win with just *any* complete hand — a hand worth zero is a "chicken hand," and Auntie Mei's table does not pay chickens. Your hand must be worth at least **1 faan**. Press the **Hands** button in the corner any time to slide out the full list of scoring hands. Keep it open while you learn — that is what it is for. + +The big ones: + +| Hand | Faan | +|---|---| +| Common Hand (all chows + suited pair) | 1 | +| All Pungs | 3 | +| Mixed One Suit (one suit + honors) | 3 | +| Small Dragons | 5 | +| Pure One Suit | 6 | +| Small Winds | 6 | +| Great Dragons | 8 | +| Great Winds / All Honors / Thirteen Orphans | 13 (limit) | + +And the seasoning faan: each Dragon pung is 1, a pung of your own seat wind or of East (the round wind) is 1, winning by self-draw is 1, a fully concealed hand is 1, each Flower or Season matching your seat is 1, and finishing with *no* bonus tiles at all is 1. They stack — that is how a humble hand becomes an expensive one. + +## Getting Paid + +Faan converts to base points on a doubling ladder (1 faan = 2, 2 = 4, 3 = 8... capped at the 13-faan limit). + +- **Win by discard** — the player who threw the tile pays you **double** the base, alone. They will apologize. Do not accept the apology, accept the points. +- **Win by self-draw** — *everyone* pays you the base. This is why self-draw is the sweetest win. + +## Auntie Mei's Teahouse Wisdom + +**1. Decide your hand early.** Look at your 13 tiles. Are they leaning toward one suit? Toward pungs? A plan worth 3 faan beats a fast plan worth nothing — remember, zero faan cannot even win. + +**2. Do not claim everything.** Every pung you claim shows the table your plan and kills your concealed bonus. Claim when it truly advances you. My late husband claimed every pung he saw. *Every one.* He never beat me once. + +**3. Watch the discards.** If a player has three exposed sets, they are one tile from glory. Throwing a fresh, never-seen tile at them is how you end up paying double. When in doubt, discard a tile someone already threw — it passed through the table safely once. + +**4. Honor tiles first, usually.** A lone West wind when you are not West and it is not the round wind? It earns nothing and chows with nothing. Out it goes, early, while it is still safe to throw. + +**5. Respect the flowers.** Your own flower or season is a free faan. All four flowers is free faan *plus* the table's envy, which is worth even more. + +**6. The dealer is dangerous.** The dealer keeps the deal by winning. If the dealer is hot, sometimes the wisest hand is a fast, cheap one that simply ends their turn. Throw water on the fire. + +## The AI Opponents + +The computer players come in skill levels 1 to 5. The low ones claim every tile like my husband, rest his soul. The high ones count what they've seen, fold their plans around your discards, and stop throwing you winning tiles right when you need them. Treat a skill-5 opponent like a Kowloon grandmother: politely, and with great fear. + +--- + +That's the game. Draw, discard, claim wisely, count your faan, and never — NEVER — throw the tile that wins it for someone else. More tea? Good. The tiles are shuffled. *Clack clack.* + +Sik wu! 🀄🍵 diff --git a/public/src/main.js b/public/src/main.js index a45f157..ab7e976 100644 --- a/public/src/main.js +++ b/public/src/main.js @@ -67,6 +67,7 @@ import PuddingMonstersGame from './games/puddingmonsters/PuddingMonstersGame.js' import ShiftGame from './games/shift/ShiftGame.js'; import BlockFighterGame from './games/blockfighter/BlockFighterGame.js'; import MahjongMatchGame from './games/mahjongmatch/MahjongMatchGame.js'; +import MahjongGame from './games/mahjong/MahjongGame.js'; const config = { type: Phaser.AUTO, @@ -147,6 +148,7 @@ const config = { ShiftGame, BlockFighterGame, MahjongMatchGame, + MahjongGame, ], }; diff --git a/public/src/scenes/GameRoomScene.js b/public/src/scenes/GameRoomScene.js index 4457a38..26af7ff 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', freecell: 'FreecellGame', rushhour: 'RushHourGame', hexsweeper: 'HexsweeperGame', puddingmonsters: 'PuddingMonstersGame', shift: 'ShiftGame', blockfighter: 'BlockFighterGame', mahjongmatch: 'MahjongMatchGame' }; + 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', rushhour: 'RushHourGame', hexsweeper: 'HexsweeperGame', puddingmonsters: 'PuddingMonstersGame', shift: 'ShiftGame', blockfighter: 'BlockFighterGame', mahjongmatch: 'MahjongMatchGame', mahjong: 'MahjongGame' }; if (slugDispatch[this.game.slug]) { this.scene.start(slugDispatch[this.game.slug], { game: this.game, diff --git a/public/src/scenes/OpponentSelectScene.js b/public/src/scenes/OpponentSelectScene.js index 3f7bd4d..52a3e6e 100644 --- a/public/src/scenes/OpponentSelectScene.js +++ b/public/src/scenes/OpponentSelectScene.js @@ -384,7 +384,7 @@ export default class OpponentSelectScene extends Phaser.Scene { // Skill control: pips always show the level; the +/- buttons appear only // when this opponent is selected. Enabled for games with a 1–5 AI skill. - if (['nerts', 'checkers', 'chess', 'wordle', 'scrabble', 'ghost', 'wordladder', 'othello', 'go', 'mastermind', 'connect4', 'boggle', 'forbiddenisland', 'labyrinth', 'stratego', 'triominoes'].includes(this.gameDef.slug)) { + if (['nerts', 'checkers', 'chess', 'wordle', 'scrabble', 'ghost', 'wordladder', 'othello', 'go', 'mastermind', 'connect4', 'boggle', 'forbiddenisland', 'labyrinth', 'stratego', 'triominoes', 'mahjong'].includes(this.gameDef.slug)) { bio.style.webkitLineClamp = '1'; const skillRow = document.createElement('div'); diff --git a/server/games/registry.js b/server/games/registry.js index 33a8abd..9f6fc8d 100644 --- a/server/games/registry.js +++ b/server/games/registry.js @@ -82,3 +82,4 @@ registerGame({ slug: 'puddingmonsters', name: 'Jell-o Monsters', category: ' registerGame({ slug: 'shift', name: 'Shift', category: 'logic', minPlayers: 1, maxPlayers: 1, minOpponents: 0, maxOpponents: 0, iconFrame: 55 }); registerGame({ slug: 'blockfighter', name: 'Block Fighter', category: 'logic', minPlayers: 1, maxPlayers: 1, minOpponents: 0, maxOpponents: 0, hasTutorial: true, iconFrame: 56 }); registerGame({ slug: 'mahjongmatch', name: 'Mahjong Match', category: 'logic', minPlayers: 1, maxPlayers: 1, minOpponents: 0, maxOpponents: 0, iconFrame: 57 }); +registerGame({ slug: 'mahjong', name: 'Mahjong', category: 'tabletop', minPlayers: 4, maxPlayers: 4, minOpponents: 3, maxOpponents: 3, hasTutorial: true, iconFrame: 58 }); diff --git a/server/scripts/verifyMahjong.js b/server/scripts/verifyMahjong.js new file mode 100644 index 0000000..43f7e66 --- /dev/null +++ b/server/scripts/verifyMahjong.js @@ -0,0 +1,292 @@ +// Headless verification for Mahjong (Hong Kong style). +// node server/scripts/verifyMahjong.js +// Exits non-zero on any failure. +// +// 1. Tile catalog: 144-tile wall, label assets exist on disk. +// 2. Faan evaluator fixtures: known hands score the expected faan rows. +// 3. Payments: zero-sum, points ladder monotonic and capped. +// 4. AI self-play: full sessions with per-step invariants (tile conservation, +// hand sizes, legal claims, minimum faan, zero-sum scores, termination). + +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { + TILES, buildWall, BASE_POINTS, LIMIT_FAAN, MIN_FAAN, +} from '../../public/src/games/mahjong/MahjongData.js'; +import { + createInitialState, dealHand, discardTile, claimOptionsFor, resolveClaims, + selfActions, declareKong, declareWin, startNextHand, isGameOver, getWinners, + counts34, decompose, isThirteenOrphans, canWinWith, winningTiles, shanten, + evaluateWin, +} from '../../public/src/games/mahjong/MahjongLogic.js'; +import { + chooseDiscard, chooseClaim, chooseSelfAction, +} from '../../public/src/games/mahjong/MahjongAI.js'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const IMG_DIR = path.join(__dirname, '../../public/assets/images/mahjong'); + +let failures = 0; +function check(ok, msg) { + if (!ok) { failures++; console.error(` ✗ ${msg}`); } + return ok; +} + +// kind shorthands for fixtures +const b = (n) => n - 1, c = (n) => 9 + n - 1, ch = (n) => 18 + n - 1; +const E = 27, S = 28, W = 29, N = 30, R = 31, G = 32, Wh = 33; + +// ── 1. Tile catalog ────────────────────────────────────────────────────────── +console.log('Tile catalog:'); +check(TILES.length === 42, `kind count ${TILES.length}, expected 42`); +const wall = buildWall(); +check(wall.length === 144, `wall ${wall.length} tiles, expected 144`); +const wc = new Array(42).fill(0); +for (const k of wall) wc[k]++; +for (let k = 0; k < 34; k++) check(wc[k] === 4, `kind ${k} has ${wc[k]} copies, expected 4`); +for (let k = 34; k < 42; k++) check(wc[k] === 1, `bonus kind ${k} has ${wc[k]} copies, expected 1`); +for (const t of TILES) { + if (!t.label) continue; // white dragon is drawn procedurally + const file = path.join(IMG_DIR, `${t.label.replace(/^mahjong-/, '')}.png`); + check(fs.existsSync(file), `missing label asset for ${t.id}: ${file}`); +} +console.log(' ok'); + +// ── 2. Faan fixtures ───────────────────────────────────────────────────────── +// Build a minimal state around one player's completed hand. For discard wins +// `hand` excludes the winning tile; for self-draws it includes it. +function fixtureState({ hand, melds = [], bonus = [], seat = 0, dealer = 0, + byDiscard, winTile, lastKong = false, wallEmpty = false }) { + const players = []; + for (let s = 0; s < 4; s++) { + players.push({ name: `P${s + 1}`, seat: s, score: 0, skill: 3, isAI: s !== 0, + hand: [], melds: [], bonus: [] }); + } + players[seat] = { ...players[seat], hand: hand.slice(), melds, bonus }; + return { + players, dealer, roundWind: 0, + wallPos: wallEmpty ? 1 : 0, wallEnd: wallEmpty ? 0 : 90, + lastDrawWasKongReplacement: lastKong, + lastDiscard: byDiscard ? { kind: winTile, from: (seat + 1) % 4 } : null, + }; +} + +function checkFixture(name, args, expectFaan, mustHave = [], mustNotHave = []) { + const state = fixtureState(args); + const res = evaluateWin(state, args.seat ?? 0, args.winTile, !!args.byDiscard); + const ids = res.faanList.map((r) => r.id); + check(res.faan === expectFaan, `${name}: faan ${res.faan}, expected ${expectFaan} [${ids.join(', ')}]`); + for (const id of mustHave) check(ids.includes(id), `${name}: missing faan row '${id}'`); + for (const id of mustNotHave) check(!ids.includes(id), `${name}: unexpected faan row '${id}'`); + check(res.payments.reduce((a, x) => a + x, 0) === 0, `${name}: payments not zero-sum`); + return res; +} + +console.log('Faan fixtures:'); +const exposedPung = (k, from = 1) => ({ type: 'pung', kinds: [k, k, k], concealed: false, from }); + +checkFixture('thirteen orphans', + { hand: [b(1), b(1), b(9), c(1), c(9), ch(1), ch(9), E, S, W, N, R, G, Wh], byDiscard: false, winTile: b(1) }, + 13, ['thirteen-orphans']); + +checkFixture('pure one suit', + { hand: [b(1), b(1), b(1), b(2), b(3), b(4), b(4), b(5), b(6), b(7), b(8), b(9), b(9)], + byDiscard: true, winTile: b(9) }, + 8, ['pure-one-suit', 'concealed', 'no-bonus'], ['mixed-one-suit']); + +checkFixture('mixed one suit + round wind', + { hand: [c(1), c(2), c(3), c(4), c(5), c(6), c(7), c(8), c(9), E, E, R, R], + seat: 1, byDiscard: true, winTile: E }, + 6, ['mixed-one-suit', 'round-wind', 'concealed', 'no-bonus'], ['seat-wind', 'pure-one-suit']); + +checkFixture('all pungs (exposed)', + { hand: [b(2), b(2), c(5), c(5), c(5), ch(7), ch(7), ch(7), b(9), b(9)], + melds: [exposedPung(N)], seat: 2, byDiscard: true, winTile: b(2) }, + 4, ['all-pungs', 'no-bonus'], ['concealed', 'seat-wind', 'round-wind']); + +checkFixture('common hand', + { hand: [b(1), b(2), b(3), b(4), b(5), b(6), c(1), c(2), c(3), ch(4), ch(5), ch(6), c(9)], + byDiscard: true, winTile: c(9) }, + 3, ['common-hand', 'concealed', 'no-bonus']); + +checkFixture('chicken hand scores zero', + { hand: [c(2), c(3), c(4), c(5), c(6), c(7), ch(3), ch(4), ch(8), ch(8)], + melds: [exposedPung(b(2))], bonus: [35], byDiscard: true, winTile: ch(5) }, + 0, [], ['no-bonus', 'seat-flower', 'common-hand']); + +checkFixture('dealer east pung stacks seat + round wind', + { hand: [b(2), b(3), b(5), b(6), b(7), ch(3), ch(3), ch(3), c(9), c(9)], + melds: [exposedPung(E)], seat: 0, dealer: 0, byDiscard: true, winTile: b(4) }, + 3, ['seat-wind', 'round-wind', 'no-bonus']); + +checkFixture('great dragons', + { hand: [Wh, Wh, Wh, b(2), b(3), c(5), c(5)], + melds: [exposedPung(R), exposedPung(G)], byDiscard: true, winTile: b(4) }, + 9, ['great-dragons', 'no-bonus'], ['dragon-pung', 'small-dragons']); + +checkFixture('small dragons', + { hand: [Wh, Wh, b(2), b(3), b(4), c(6), c(7)], + melds: [exposedPung(R), exposedPung(G)], byDiscard: true, winTile: c(8) }, + 6, ['small-dragons', 'no-bonus'], ['dragon-pung', 'great-dragons']); + +checkFixture('all honors capped at limit', + { hand: [E, E, E, S, S, S, R, R, R, G, G, G, N, N], byDiscard: false, winTile: N }, + 13, ['all-honors'], ['all-pungs', 'mixed-one-suit']); + +checkFixture('self draw context', + { hand: [b(1), b(2), b(3), b(4), b(5), b(6), c(1), c(2), c(3), ch(4), ch(5), ch(6), c(9), c(9)], + byDiscard: false, winTile: c(9) }, + 4, ['common-hand', 'self-draw', 'concealed', 'no-bonus']); + +checkFixture('kong replacement context', + { hand: [b(1), b(2), b(3), b(4), b(5), b(6), c(1), c(2), c(3), ch(4), ch(5), ch(6), c(9), c(9)], + byDiscard: false, winTile: c(9), lastKong: true }, + 5, ['kong-draw', 'self-draw']); + +checkFixture('ambiguous 111222333 read as chows', + { hand: [b(1), b(1), b(1), b(2), b(2), b(2), b(3), b(3), b(3), ch(7), ch(8), ch(9), c(5)], + byDiscard: true, winTile: c(5) }, + 3, ['common-hand', 'concealed', 'no-bonus']); + +checkFixture('flowers: own + full set', + { hand: [b(1), b(2), b(3), b(4), b(5), b(6), c(1), c(2), c(3), ch(4), ch(5), ch(6), c(9)], + bonus: [34, 35, 36, 37], byDiscard: true, winTile: c(9) }, + 5, ['common-hand', 'concealed', 'seat-flower', 'flower-set'], ['no-bonus']); + +// decomposer / shanten sanity +{ + const tenpai = counts34([b(1), b(2), b(3), b(4), b(5), b(6), c(1), c(2), c(3), ch(4), ch(5), ch(6), c(9)]); + check(shanten(tenpai, 0) === 0, `tenpai hand shanten ${shanten(tenpai, 0)}, expected 0`); + const complete = counts34([b(1), b(2), b(3), b(4), b(5), b(6), c(1), c(2), c(3), ch(4), ch(5), ch(6), c(9), c(9)]); + check(shanten(complete, 0) === -1, `complete hand shanten ${shanten(complete, 0)}, expected -1`); + check(decompose(complete, 4).length > 0, 'complete hand fails to decompose'); + const waits = winningTiles([b(1), b(2), b(3), b(4), b(5), b(6), c(1), c(2), c(3), ch(4), ch(5), ch(6), c(9)], []); + check(waits.length === 1 && waits[0] === c(9), `waits [${waits.join(',')}], expected [${c(9)}]`); + check(canWinWith([b(1), b(1), b(9), c(1), c(9), ch(1), ch(9), E, S, W, N, R, G], [], Wh), + 'thirteen orphans not recognised by canWinWith'); + check(isThirteenOrphans(counts34([b(1), b(1), b(9), c(1), c(9), ch(1), ch(9), E, S, W, N, R, G, Wh])), + 'isThirteenOrphans rejects a valid hand'); +} +console.log(` ${failures === 0 ? 'ok' : 'see failures above'}`); + +// ── 3. Points ladder ───────────────────────────────────────────────────────── +console.log('Points ladder:'); +check(BASE_POINTS.length === LIMIT_FAAN + 1, 'BASE_POINTS does not cover 0..LIMIT_FAAN'); +for (let i = 1; i < BASE_POINTS.length; i++) { + check(BASE_POINTS[i] > BASE_POINTS[i - 1], `BASE_POINTS not monotonic at faan ${i}`); +} +console.log(' ok'); + +// ── 4. AI self-play ────────────────────────────────────────────────────────── +console.log('Self-play (40 sessions, mixed skills):'); + +function tileConservation(state) { + let total = state.wallEnd - state.wallPos + 1; + for (const p of state.players) { + total += p.hand.length + p.bonus.length; + for (const m of p.melds) total += m.kinds.length; + } + for (const river of state.discards) total += river.length; + return total; +} + +function checkInvariants(state, tag) { + if (state.phase === 'handOver' || state.phase === 'gameOver') return; + const total = tileConservation(state); + check(total === 144, `${tag}: ${total} tiles accounted for, expected 144`); + for (const p of state.players) { + const drawing = state.phase === 'awaitDiscard' && state.turn === p.seat; + const expected = (drawing ? 14 : 13) - 3 * p.melds.length; + check(p.hand.length === expected, + `${tag}: seat ${p.seat} hand ${p.hand.length}, expected ${expected} (${state.phase})`); + } +} + +function playSession(seed) { + const skills = { 0: 1 + (seed % 5), 1: 1 + ((seed >> 2) % 5), 2: 3, 3: 5 }; + const state = createInitialState({ names: ['A', 'B', 'C', 'D'], skills, seed }); + const dealersSeen = new Set(); + let hands = 0, wins = 0, draws = 0, claims = 0, kongs = 0; + let guard = 100000; + + while (!isGameOver(state) && guard-- > 0) { + checkInvariants(state, `seed ${seed} hand ${state.handNumber}`); + if (state.phase === 'handOver') { + dealersSeen.add(state.dealer); + hands++; + const r = state.result; + if (r.type === 'win') { + wins++; + check(r.faan >= MIN_FAAN, `seed ${seed}: win below minimum faan (${r.faan})`); + check(r.payments.reduce((a, x) => a + x, 0) === 0, `seed ${seed}: payments not zero-sum`); + } else { + draws++; + } + const sum = state.players.reduce((a, p) => a + p.score, 0); + check(sum === 0, `seed ${seed}: cumulative scores sum to ${sum}, expected 0`); + startNextHand(state); + continue; + } + if (state.phase === 'awaitDiscard') { + const seat = state.turn; + const p = state.players[seat]; + const acts = selfActions(state); + const sa = chooseSelfAction(state, seat, acts, p.skill); + if (sa?.type === 'win') { + check(acts.canWin, `seed ${seed}: AI declared an unoffered win`); + declareWin(state, seat, { byDiscard: false }); + continue; + } + if (sa?.type === 'kong') { + kongs++; + check(declareKong(state, sa.spec), `seed ${seed}: illegal kong ${JSON.stringify(sa.spec)}`); + continue; + } + const d = chooseDiscard(state, seat, p.skill); + check(p.hand.includes(d), `seed ${seed}: AI discarded unheld kind ${d}`); + discardTile(state, d); + continue; + } + if (state.phase === 'awaitClaims') { + const from = state.lastDiscard.from; + const intents = []; + for (let seat = 0; seat < 4; seat++) { + const options = claimOptionsFor(state, seat); + if (!options) continue; + check(seat !== from, `seed ${seed}: discarder offered a claim`); + if (options.chows.length) { + check(seat === (from + 1) % 4, `seed ${seed}: chow offered to non-left seat`); + } + const claim = chooseClaim(state, seat, options, state.players[seat].skill); + if (claim) intents.push({ seat, claim }); + } + const { applied } = resolveClaims(state, intents); + if (applied && applied.claim.type !== 'win') claims++; + continue; + } + check(false, `seed ${seed}: unknown phase ${state.phase}`); + break; + } + check(guard > 0, `seed ${seed}: session did not terminate`); + check(dealersSeen.size === 4 || guard <= 0, `seed ${seed}: only ${dealersSeen.size}/4 seats dealt`); + check(getWinners(state).length >= 1, `seed ${seed}: no session winner`); + return { hands, wins, draws, claims, kongs }; +} + +let tHands = 0, tWins = 0, tDraws = 0, tClaims = 0, tKongs = 0; +const t0 = Date.now(); +for (let g = 0; g < 40; g++) { + const r = playSession(1000 + g * 7919); + tHands += r.hands; tWins += r.wins; tDraws += r.draws; tClaims += r.claims; tKongs += r.kongs; +} +const secs = ((Date.now() - t0) / 1000).toFixed(1); +console.log(` ${tHands} hands over 40 sessions in ${secs}s — ${tWins} wins, ${tDraws} drawn, ${tClaims} claims, ${tKongs} kongs`); +check(tWins > 0, 'no hand was ever won — evaluator or AI suspect'); +check(tWins / Math.max(1, tHands) > 0.3, `only ${tWins}/${tHands} hands won — too many goulash draws`); + +if (failures) { + console.error(`\nFAILED: ${failures} check(s).`); + process.exit(1); +} +console.log('\nAll Mahjong checks passed.');