anvilsign in

collin/mahjong

master / src / game / tiles.ts
1// Tile codes 0..41
2// 0..8 萬 (characters) 1-9
3// 9..17 條 (bamboo) 1-9
4// 18..26 筒 (dots) 1-9
5// 27..30 winds 東 南 西 北
6// 31..33 dragons 中 發 白
7// 34..37 seasons 春 夏 秋 冬 (seat 0..3)
8// 38..41 flowers 梅 蘭 菊 竹 (seat 0..3)
9
10export type Tile = number;
11
12export const NUM_SUITED = 27;
13export const NUM_BASIC = 34; // everything except flowers
14export const FIRST_FLOWER = 34;
15export const NUM_TILES = 42;
16/** Tiles in a full wall: 34 basic × 4 + 8 flowers. */
17export const WALL_SIZE = 144;
18/** Stacks of two, 18 to a side of the square. */
19export const WALL_STACKS = WALL_SIZE / 2;
20export const STACKS_PER_SIDE = WALL_STACKS / 4;
21
22export const EAST = 27;
23export const SOUTH = 28;
24export const WEST = 29;
25export const NORTH = 30;
26export const RED = 31;
27export const GREEN = 32;
28export const WHITE = 33;
29
30export type SuitKind = 'm' | 's' | 'p' | 'wind' | 'dragon' | 'flower';
31
32export function suitOf(t: Tile): SuitKind {
33 if (t < 9) return 'm';
34 if (t < 18) return 's';
35 if (t < 27) return 'p';
36 if (t < 31) return 'wind';
37 if (t < 34) return 'dragon';
38 return 'flower';
39}
40
41export const isFlower = (t: Tile) => t >= FIRST_FLOWER;
42export const isHonor = (t: Tile) => t >= NUM_SUITED && t < NUM_BASIC;
43export const isSuited = (t: Tile) => t < NUM_SUITED;
44/** 1..9 for suited tiles, 0 otherwise */
45export const rankOf = (t: Tile) => (isSuited(t) ? (t % 9) + 1 : 0);
46export const isTerminal = (t: Tile) => isSuited(t) && (rankOf(t) === 1 || rankOf(t) === 9);
47
48/** The seat (0..3) a flower belongs to. */
49export const flowerSeat = (t: Tile) => (t - FIRST_FLOWER) % 4;
50/** 0 = 春夏秋冬 set, 1 = 梅蘭菊竹 set */
51export const flowerSet = (t: Tile) => (t - FIRST_FLOWER < 4 ? 0 : 1);
52
53const DIGITS = ['一', '二', '三', '四', '五', '六', '七', '八', '九'];
54const HONORS = ['東', '南', '西', '北', '中', '發', '白'];
55const FLOWERS = ['春', '夏', '秋', '冬', '梅', '蘭', '菊', '竹'];
56const SUIT_MARK = { m: '萬', s: '條', p: '筒' } as const;
57
58/** Big glyph shown on the tile face. */
59export function faceOf(t: Tile): { main: string; sub?: string } {
60 const s = suitOf(t);
61 if (s === 'm' || s === 's' || s === 'p') {
62 return { main: DIGITS[rankOf(t) - 1], sub: SUIT_MARK[s] };
63 }
64 if (s === 'flower') return { main: FLOWERS[t - FIRST_FLOWER] };
65 return { main: HONORS[t - NUM_SUITED] };
66}
67
68/** Short text label, e.g. "五萬", "東", "春". */
69export function labelOf(t: Tile): string {
70 const f = faceOf(t);
71 return f.main + (f.sub ?? '');
72}
73
74export const WIND_NAMES = ['東', '南', '西', '北'];
75export const WIND_EN = ['East', 'South', 'West', 'North'];
76
77/**
78 * Column of each tile in the sprite sheet (gnome-mahjongg's "postmodern"
79 * theme, 43 columns × 2 rows; the second row is the highlighted variant).
80 * Its order is dots, winds, dragons, characters, bamboo, seasons, flowers —
81 * not ours, so this is the lookup between the two.
82 */
83export const SPRITE_COL: number[] = [
84 15, 16, 17, 18, 19, 20, 21, 22, 23, // 萬 1-9
85 24, 25, 26, 27, 28, 29, 30, 31, 32, // 條 1-9
86 0, 1, 2, 3, 4, 5, 6, 7, 8, // 筒 1-9
87 12, 11, 10, 9, // 東 南 西 北
88 13, 14, 37, // 中 發 白
89 33, 34, 35, 36, // 春 夏 秋 冬
90 38, 39, 40, 41, // 梅 蘭 菊 竹
91];
92export const SPRITE_COLS = 43;
93
94/** A full Taiwanese wall: 4× each basic tile + 1× each flower = 144. */
95export function buildWall(): Tile[] {
96 const w: Tile[] = [];
97 for (let t = 0; t < NUM_BASIC; t++) for (let i = 0; i < 4; i++) w.push(t);
98 for (let t = FIRST_FLOWER; t < NUM_TILES; t++) w.push(t);
99 return w;
100}
101
102/** mulberry32 — small deterministic PRNG so hands can be replayed from a seed. */
103export function makeRng(seed: number) {
104 let a = seed >>> 0;
105 return () => {
106 a = (a + 0x6d2b79f5) | 0;
107 let t = Math.imul(a ^ (a >>> 15), 1 | a);
108 t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
109 return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
110 };
111}
112
113export function shuffle<T>(arr: T[], rng: () => number): T[] {
114 const a = arr.slice();
115 for (let i = a.length - 1; i > 0; i--) {
116 const j = Math.floor(rng() * (i + 1));
117 [a[i], a[j]] = [a[j], a[i]];
118 }
119 return a;
120}
121
122export function sortTiles(tiles: Tile[]): Tile[] {
123 return tiles.slice().sort((a, b) => a - b);
124}
125
126export function toCounts(tiles: Tile[]): number[] {
127 const c = new Array(NUM_BASIC).fill(0);
128 for (const t of tiles) if (t < NUM_BASIC) c[t]++;
129 return c;
130}