fertig-classic-games/public/src/games/mahjong/MahjongData.js

123 lines
7.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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'];