@pai-forge/riichi-mahjong 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +58 -0
- package/dist/core/dora.d.ts +14 -0
- package/dist/core/dora.js +71 -0
- package/dist/core/hai.d.ts +30 -0
- package/dist/core/hai.js +78 -0
- package/dist/core/machi.d.ts +11 -0
- package/dist/core/machi.js +58 -0
- package/dist/core/mentsu.d.ts +26 -0
- package/dist/core/mentsu.js +87 -0
- package/dist/core/tehai.d.ts +38 -0
- package/dist/core/tehai.js +87 -0
- package/dist/errors.d.ts +40 -0
- package/dist/errors.js +58 -0
- package/dist/features/machi/index.d.ts +9 -0
- package/dist/features/machi/index.js +37 -0
- package/dist/features/machi/types.d.ts +1 -0
- package/dist/features/machi/types.js +1 -0
- package/dist/features/parser/index.d.ts +19 -0
- package/dist/features/parser/index.js +28 -0
- package/dist/features/parser/mspz.d.ts +85 -0
- package/dist/features/parser/mspz.js +365 -0
- package/dist/features/points/constants.d.ts +27 -0
- package/dist/features/points/constants.js +30 -0
- package/dist/features/points/index.d.ts +21 -0
- package/dist/features/points/index.js +174 -0
- package/dist/features/points/lib/fu/constants.d.ts +37 -0
- package/dist/features/points/lib/fu/constants.js +45 -0
- package/dist/features/points/lib/fu/index.d.ts +11 -0
- package/dist/features/points/lib/fu/index.js +23 -0
- package/dist/features/points/lib/fu/lib/chiitoitsu.d.ts +5 -0
- package/dist/features/points/lib/fu/lib/chiitoitsu.js +17 -0
- package/dist/features/points/lib/fu/lib/kokushi.d.ts +6 -0
- package/dist/features/points/lib/fu/lib/kokushi.js +18 -0
- package/dist/features/points/lib/fu/lib/mentsu.d.ts +11 -0
- package/dist/features/points/lib/fu/lib/mentsu.js +122 -0
- package/dist/features/points/lib/fu/types.d.ts +55 -0
- package/dist/features/points/lib/fu/types.js +1 -0
- package/dist/features/points/types.d.ts +27 -0
- package/dist/features/points/types.js +1 -0
- package/dist/features/shanten/index.d.ts +16 -0
- package/dist/features/shanten/index.js +25 -0
- package/dist/features/shanten/logic/chiitoitsu.d.ts +8 -0
- package/dist/features/shanten/logic/chiitoitsu.js +36 -0
- package/dist/features/shanten/logic/kokushi.d.ts +16 -0
- package/dist/features/shanten/logic/kokushi.js +48 -0
- package/dist/features/shanten/logic/mentsu-te.d.ts +8 -0
- package/dist/features/shanten/logic/mentsu-te.js +129 -0
- package/dist/features/yaku/factory.d.ts +13 -0
- package/dist/features/yaku/factory.js +19 -0
- package/dist/features/yaku/index.d.ts +12 -0
- package/dist/features/yaku/index.js +62 -0
- package/dist/features/yaku/lib/definitions/chiitoitsu.d.ts +2 -0
- package/dist/features/yaku/lib/definitions/chiitoitsu.js +12 -0
- package/dist/features/yaku/lib/definitions/chinitsu.d.ts +2 -0
- package/dist/features/yaku/lib/definitions/chinitsu.js +40 -0
- package/dist/features/yaku/lib/definitions/chinroutou.d.ts +2 -0
- package/dist/features/yaku/lib/definitions/chinroutou.js +21 -0
- package/dist/features/yaku/lib/definitions/chuuren-poutou.d.ts +2 -0
- package/dist/features/yaku/lib/definitions/chuuren-poutou.js +69 -0
- package/dist/features/yaku/lib/definitions/daisangen.d.ts +2 -0
- package/dist/features/yaku/lib/definitions/daisangen.js +26 -0
- package/dist/features/yaku/lib/definitions/daisuushii.d.ts +2 -0
- package/dist/features/yaku/lib/definitions/daisuushii.js +32 -0
- package/dist/features/yaku/lib/definitions/honchan.d.ts +2 -0
- package/dist/features/yaku/lib/definitions/honchan.js +29 -0
- package/dist/features/yaku/lib/definitions/honitsu.d.ts +2 -0
- package/dist/features/yaku/lib/definitions/honitsu.js +40 -0
- package/dist/features/yaku/lib/definitions/honroutou.d.ts +2 -0
- package/dist/features/yaku/lib/definitions/honroutou.js +33 -0
- package/dist/features/yaku/lib/definitions/iipeiko.d.ts +2 -0
- package/dist/features/yaku/lib/definitions/iipeiko.js +46 -0
- package/dist/features/yaku/lib/definitions/ikkitsuukan.d.ts +2 -0
- package/dist/features/yaku/lib/definitions/ikkitsuukan.js +56 -0
- package/dist/features/yaku/lib/definitions/index.d.ts +30 -0
- package/dist/features/yaku/lib/definitions/index.js +90 -0
- package/dist/features/yaku/lib/definitions/junchan.d.ts +2 -0
- package/dist/features/yaku/lib/definitions/junchan.js +25 -0
- package/dist/features/yaku/lib/definitions/kokushi.d.ts +2 -0
- package/dist/features/yaku/lib/definitions/kokushi.js +12 -0
- package/dist/features/yaku/lib/definitions/menzen-tsumo.d.ts +2 -0
- package/dist/features/yaku/lib/definitions/menzen-tsumo.js +8 -0
- package/dist/features/yaku/lib/definitions/pinfu.d.ts +2 -0
- package/dist/features/yaku/lib/definitions/pinfu.js +40 -0
- package/dist/features/yaku/lib/definitions/ryanpeiko.d.ts +2 -0
- package/dist/features/yaku/lib/definitions/ryanpeiko.js +33 -0
- package/dist/features/yaku/lib/definitions/ryuuiisou.d.ts +2 -0
- package/dist/features/yaku/lib/definitions/ryuuiisou.js +43 -0
- package/dist/features/yaku/lib/definitions/sanankou.d.ts +2 -0
- package/dist/features/yaku/lib/definitions/sanankou.js +49 -0
- package/dist/features/yaku/lib/definitions/sankantsu.d.ts +2 -0
- package/dist/features/yaku/lib/definitions/sankantsu.js +18 -0
- package/dist/features/yaku/lib/definitions/sanshoku-doujun.d.ts +2 -0
- package/dist/features/yaku/lib/definitions/sanshoku-doujun.js +58 -0
- package/dist/features/yaku/lib/definitions/sanshoku-doukou.d.ts +2 -0
- package/dist/features/yaku/lib/definitions/sanshoku-doukou.js +53 -0
- package/dist/features/yaku/lib/definitions/shousangen.d.ts +2 -0
- package/dist/features/yaku/lib/definitions/shousangen.js +28 -0
- package/dist/features/yaku/lib/definitions/shousuushii.d.ts +2 -0
- package/dist/features/yaku/lib/definitions/shousuushii.js +34 -0
- package/dist/features/yaku/lib/definitions/suuankou.d.ts +2 -0
- package/dist/features/yaku/lib/definitions/suuankou.js +63 -0
- package/dist/features/yaku/lib/definitions/suukantsu.d.ts +2 -0
- package/dist/features/yaku/lib/definitions/suukantsu.js +18 -0
- package/dist/features/yaku/lib/definitions/tanyao.d.ts +2 -0
- package/dist/features/yaku/lib/definitions/tanyao.js +23 -0
- package/dist/features/yaku/lib/definitions/toitoi.d.ts +2 -0
- package/dist/features/yaku/lib/definitions/toitoi.js +16 -0
- package/dist/features/yaku/lib/definitions/tsuuiisou.d.ts +2 -0
- package/dist/features/yaku/lib/definitions/tsuuiisou.js +35 -0
- package/dist/features/yaku/lib/definitions/yakuhai.d.ts +4 -0
- package/dist/features/yaku/lib/definitions/yakuhai.js +21 -0
- package/dist/features/yaku/lib/index.d.ts +1 -0
- package/dist/features/yaku/lib/index.js +1 -0
- package/dist/features/yaku/lib/structures/chiitoitsu.d.ts +6 -0
- package/dist/features/yaku/lib/structures/chiitoitsu.js +38 -0
- package/dist/features/yaku/lib/structures/index.d.ts +10 -0
- package/dist/features/yaku/lib/structures/index.js +17 -0
- package/dist/features/yaku/lib/structures/kokushi.d.ts +6 -0
- package/dist/features/yaku/lib/structures/kokushi.js +43 -0
- package/dist/features/yaku/lib/structures/mentsu-te.d.ts +24 -0
- package/dist/features/yaku/lib/structures/mentsu-te.js +127 -0
- package/dist/features/yaku/types.d.ts +121 -0
- package/dist/features/yaku/types.js +1 -0
- package/dist/features/yaku/utils.d.ts +19 -0
- package/dist/features/yaku/utils.js +34 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +9 -0
- package/dist/types.d.ts +280 -0
- package/dist/types.js +97 -0
- package/dist/utils/assertions.d.ts +22 -0
- package/dist/utils/assertions.js +33 -0
- package/dist/utils/test-helpers.d.ts +55 -0
- package/dist/utils/test-helpers.js +124 -0
- package/package.json +62 -0
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { isYaochu } from "../../../core/hai";
|
|
2
|
+
import { countHaiKind, validateTehai13 } from "../../../core/tehai";
|
|
3
|
+
import { asHaiKindId } from "../../../utils/assertions";
|
|
4
|
+
/**
|
|
5
|
+
* 国士無双のシャンテン数を計算します。
|
|
6
|
+
*
|
|
7
|
+
* ルール:
|
|
8
|
+
* - 13種類の么九牌(1,9,字牌)を各1枚ずつ揃える。
|
|
9
|
+
* - そのうちのどれか1種類が対子(2枚)になっている必要がある。
|
|
10
|
+
* - 門前限定。
|
|
11
|
+
*
|
|
12
|
+
* 計算式:
|
|
13
|
+
* シャンテン数 = 13 - (么九牌の種類数) - (么九牌の対子があるか ? 1 : 0)
|
|
14
|
+
*
|
|
15
|
+
* @param tehai 手牌
|
|
16
|
+
* @returns シャンテン数 (0: 聴牌, -1: 和了(理論上))。副露している場合は Infinity。
|
|
17
|
+
*/
|
|
18
|
+
export function calculateKokushiShanten(tehai) {
|
|
19
|
+
// 防御的プログラミング (Defensive Programming):
|
|
20
|
+
// 公開API(calculateShanten)側でもバリデーションが行われる想定だが(Facadeパターン)、
|
|
21
|
+
// 内部整合性を保つため、ここでも独立してバリデーションを実施する。
|
|
22
|
+
validateTehai13(tehai);
|
|
23
|
+
// 国士無双は門前のみ
|
|
24
|
+
if (tehai.exposed.length > 0) {
|
|
25
|
+
return Infinity;
|
|
26
|
+
}
|
|
27
|
+
const dist = countHaiKind(tehai.closed);
|
|
28
|
+
// 么九牌の種類数をカウント
|
|
29
|
+
// 同時に、么九牌の対子が存在するかもチェック
|
|
30
|
+
let uniqueYaochuCount = 0;
|
|
31
|
+
let hasYaochuPair = false;
|
|
32
|
+
for (let i = 0; i < dist.length; i++) {
|
|
33
|
+
const kind = asHaiKindId(i);
|
|
34
|
+
if (!isYaochu(kind)) {
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
const count = dist[i];
|
|
38
|
+
if (count !== undefined && count > 0) {
|
|
39
|
+
uniqueYaochuCount++;
|
|
40
|
+
if (count >= 2) {
|
|
41
|
+
hasYaochuPair = true;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
const pairBonus = hasYaochuPair ? 1 : 0;
|
|
46
|
+
// シャンテン数 = 13 - (種類の数) - (対子ボーナス)
|
|
47
|
+
return 13 - uniqueYaochuCount - pairBonus;
|
|
48
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { countHaiKind, validateTehai } from "../../../core/tehai";
|
|
2
|
+
/**
|
|
3
|
+
* 面子手(4面子1雀頭)のシャンテン数を計算する
|
|
4
|
+
*
|
|
5
|
+
* @param tehai 手牌 (13枚 or 14枚)
|
|
6
|
+
* @returns シャンテン数
|
|
7
|
+
*/
|
|
8
|
+
export function calculateMentsuTeShanten(tehai) {
|
|
9
|
+
// 防御的プログラミング
|
|
10
|
+
validateTehai(tehai);
|
|
11
|
+
const counts = countHaiKind(tehai.closed);
|
|
12
|
+
// Mutation is required for the algorithm, so we convert to a mutable number array
|
|
13
|
+
const mutableCounts = Array.from(counts);
|
|
14
|
+
const exposedCount = tehai.exposed.length;
|
|
15
|
+
// 基本シャンテン数 (8 - 2 * 面子数)
|
|
16
|
+
let minShanten = 8 - 2 * exposedCount;
|
|
17
|
+
// 1. 雀頭がある場合
|
|
18
|
+
for (let i = 0; i < 34; i++) {
|
|
19
|
+
if ((mutableCounts[i] ?? 0) >= 2) {
|
|
20
|
+
mutableCounts[i] = (mutableCounts[i] ?? 0) - 2;
|
|
21
|
+
const { m, t } = searchMentsu(mutableCounts);
|
|
22
|
+
const currentMentsu = exposedCount + m;
|
|
23
|
+
const effectiveTaatsu = Math.min(4 - currentMentsu, t);
|
|
24
|
+
const shanten = 8 - 2 * currentMentsu - effectiveTaatsu - 1;
|
|
25
|
+
minShanten = Math.min(minShanten, shanten);
|
|
26
|
+
mutableCounts[i] = (mutableCounts[i] ?? 0) + 2;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
// 2. 雀頭がない場合
|
|
30
|
+
{
|
|
31
|
+
const { m, t } = searchMentsu(mutableCounts);
|
|
32
|
+
const currentMentsu = exposedCount + m;
|
|
33
|
+
const effectiveTaatsu = Math.min(4 - currentMentsu, t);
|
|
34
|
+
const shanten = 8 - 2 * currentMentsu - effectiveTaatsu;
|
|
35
|
+
minShanten = Math.min(minShanten, shanten);
|
|
36
|
+
}
|
|
37
|
+
return minShanten;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* 面子と塔子の最大数を探索する
|
|
41
|
+
*/
|
|
42
|
+
function searchMentsu(counts) {
|
|
43
|
+
let maxScore = -1;
|
|
44
|
+
let bestResult = { m: 0, t: 0 };
|
|
45
|
+
// copies needed because we mutate counts
|
|
46
|
+
const w = [...counts];
|
|
47
|
+
const search = (index, m) => {
|
|
48
|
+
// 34種類すべて見終わったら塔子を数える
|
|
49
|
+
if (index >= 34) {
|
|
50
|
+
const t = countTaatsu(w);
|
|
51
|
+
const score = 2 * m + t;
|
|
52
|
+
if (score > maxScore) {
|
|
53
|
+
maxScore = score;
|
|
54
|
+
bestResult = { m, t };
|
|
55
|
+
}
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
// 牌がない場合は次に進む
|
|
59
|
+
if ((w[index] ?? 0) === 0) {
|
|
60
|
+
search(index + 1, m);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
// A. 刻子 (3枚) の場合
|
|
64
|
+
if ((w[index] ?? 0) >= 3) {
|
|
65
|
+
w[index] = (w[index] ?? 0) - 3;
|
|
66
|
+
search(index, m + 1);
|
|
67
|
+
w[index] = (w[index] ?? 0) + 3;
|
|
68
|
+
}
|
|
69
|
+
// B. 順子 (3枚) の場合
|
|
70
|
+
if (index < 27) {
|
|
71
|
+
const mod = index % 9;
|
|
72
|
+
if (mod < 7) {
|
|
73
|
+
if ((w[index] ?? 0) > 0 &&
|
|
74
|
+
(w[index + 1] ?? 0) > 0 &&
|
|
75
|
+
(w[index + 2] ?? 0) > 0) {
|
|
76
|
+
w[index] = (w[index] ?? 0) - 1;
|
|
77
|
+
w[index + 1] = (w[index + 1] ?? 0) - 1;
|
|
78
|
+
w[index + 2] = (w[index + 2] ?? 0) - 1;
|
|
79
|
+
search(index, m + 1);
|
|
80
|
+
w[index] = (w[index] ?? 0) + 1;
|
|
81
|
+
w[index + 1] = (w[index + 1] ?? 0) + 1;
|
|
82
|
+
w[index + 2] = (w[index + 2] ?? 0) + 1;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
// C. 面子として使わない場合
|
|
87
|
+
search(index + 1, m);
|
|
88
|
+
};
|
|
89
|
+
search(0, 0);
|
|
90
|
+
return bestResult;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* 残った牌から塔子(対子、両面、嵌張、辺張)の数を数える
|
|
94
|
+
*/
|
|
95
|
+
function countTaatsu(counts) {
|
|
96
|
+
let taatsu = 0;
|
|
97
|
+
// countsのコピーを作成
|
|
98
|
+
const w = [...counts];
|
|
99
|
+
for (let i = 0; i < 34; i++) {
|
|
100
|
+
if ((w[i] ?? 0) === 0)
|
|
101
|
+
continue;
|
|
102
|
+
// 順子系の塔子 (1枚 + 1枚)
|
|
103
|
+
if (i < 27) {
|
|
104
|
+
const mod = i % 9;
|
|
105
|
+
// 辺張・両面 (i, i+1)
|
|
106
|
+
if (mod < 8) {
|
|
107
|
+
if ((w[i] ?? 0) > 0 && (w[i + 1] ?? 0) > 0) {
|
|
108
|
+
w[i] = (w[i] ?? 0) - 1;
|
|
109
|
+
w[i + 1] = (w[i + 1] ?? 0) - 1;
|
|
110
|
+
taatsu++;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
// 嵌張 (i, i+2)
|
|
114
|
+
if (mod < 7) {
|
|
115
|
+
if ((w[i] ?? 0) > 0 && (w[i + 2] ?? 0) > 0) {
|
|
116
|
+
w[i] = (w[i] ?? 0) - 1;
|
|
117
|
+
w[i + 2] = (w[i + 2] ?? 0) - 1;
|
|
118
|
+
taatsu++;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
// 対子 (2枚)
|
|
123
|
+
if ((w[i] ?? 0) >= 2) {
|
|
124
|
+
w[i] = (w[i] ?? 0) - 2;
|
|
125
|
+
taatsu++;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return taatsu;
|
|
129
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { YakuHanConfig, YakuName, HouraStructure } from "./types";
|
|
2
|
+
import { YakuDefinition, HouraContext } from "./types";
|
|
3
|
+
/**
|
|
4
|
+
* 役定義を作成するファクトリ関数
|
|
5
|
+
*
|
|
6
|
+
* @param yaku 役の設定情報(名前、飜数など)
|
|
7
|
+
* @param check 役の成立条件を判定する関数 (真偽値を返す)
|
|
8
|
+
* @returns YakuDefinition (isSatisfied, getHansu を持つ)
|
|
9
|
+
*/
|
|
10
|
+
export declare function createYakuDefinition(yaku: Readonly<{
|
|
11
|
+
name: YakuName;
|
|
12
|
+
han: YakuHanConfig;
|
|
13
|
+
}>, check: (hand: HouraStructure, context: HouraContext) => boolean): YakuDefinition;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 役定義を作成するファクトリ関数
|
|
3
|
+
*
|
|
4
|
+
* @param yaku 役の設定情報(名前、飜数など)
|
|
5
|
+
* @param check 役の成立条件を判定する関数 (真偽値を返す)
|
|
6
|
+
* @returns YakuDefinition (isSatisfied, getHansu を持つ)
|
|
7
|
+
*/
|
|
8
|
+
export function createYakuDefinition(yaku, check) {
|
|
9
|
+
return {
|
|
10
|
+
yaku,
|
|
11
|
+
isSatisfied: check,
|
|
12
|
+
getHansu: (hand, context) => {
|
|
13
|
+
if (!check(hand, context)) {
|
|
14
|
+
return 0;
|
|
15
|
+
}
|
|
16
|
+
return context.isMenzen ? yaku.han.closed : yaku.han.open;
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Tehai14, HaiKindId } from "../../types";
|
|
2
|
+
import type { YakuResult } from "./types";
|
|
3
|
+
export type { HouraStructure, YakuResult, YakuName, Hansu, TehaiYaku, YakuHanConfig, Yakuhai, } from "./types";
|
|
4
|
+
export * from "./lib";
|
|
5
|
+
/**
|
|
6
|
+
* 手牌の構造役を検出する
|
|
7
|
+
*
|
|
8
|
+
* @param tehai 判定対象の手牌
|
|
9
|
+
* @param agariHai 和了牌
|
|
10
|
+
* @returns 成立した役と飜数のリスト(最も高得点となる解釈の結果)
|
|
11
|
+
*/
|
|
12
|
+
export declare function detectYaku(tehai: Tehai14, agariHai: HaiKindId, bakaze?: HaiKindId, jikaze?: HaiKindId, doraMarkers?: readonly HaiKindId[], uraDoraMarkers?: readonly HaiKindId[], isTsumo?: boolean): YakuResult;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { getHouraStructures } from "./lib/structures";
|
|
2
|
+
import { isMenzen, isKazehai } from "./utils";
|
|
3
|
+
import { ALL_YAKU_DEFINITIONS } from "./lib/definitions";
|
|
4
|
+
export * from "./lib";
|
|
5
|
+
/**
|
|
6
|
+
* 手牌の構造役を検出する
|
|
7
|
+
*
|
|
8
|
+
* @param tehai 判定対象の手牌
|
|
9
|
+
* @param agariHai 和了牌
|
|
10
|
+
* @returns 成立した役と飜数のリスト(最も高得点となる解釈の結果)
|
|
11
|
+
*/
|
|
12
|
+
export function detectYaku(tehai, agariHai, bakaze, jikaze, doraMarkers, uraDoraMarkers, isTsumo) {
|
|
13
|
+
// 1. 基本情報の抽出
|
|
14
|
+
const isMenzenValue = isMenzen(tehai);
|
|
15
|
+
const context = {
|
|
16
|
+
isMenzen: isMenzenValue,
|
|
17
|
+
agariHai,
|
|
18
|
+
bakaze: bakaze !== undefined && isKazehai(bakaze) ? bakaze : undefined,
|
|
19
|
+
jikaze: jikaze !== undefined && isKazehai(jikaze) ? jikaze : undefined,
|
|
20
|
+
doraMarkers: doraMarkers ?? [],
|
|
21
|
+
uraDoraMarkers: uraDoraMarkers ?? [],
|
|
22
|
+
isTsumo,
|
|
23
|
+
};
|
|
24
|
+
let bestResult = [];
|
|
25
|
+
let maxHan = -1;
|
|
26
|
+
// 2. 手牌の構造分解(面子手、七対子、国士無双)と役判定
|
|
27
|
+
const structuralInterpretations = getHouraStructures(tehai);
|
|
28
|
+
for (const hand of structuralInterpretations) {
|
|
29
|
+
const currentResult = [];
|
|
30
|
+
let currentHan = 0;
|
|
31
|
+
for (const definition of ALL_YAKU_DEFINITIONS) {
|
|
32
|
+
if (definition.isSatisfied(hand, context)) {
|
|
33
|
+
const hansu = definition.getHansu(hand, context);
|
|
34
|
+
// 喰い下がり0の場合は不成立 (getHansuが0を返すはずだが念のため)
|
|
35
|
+
if (hansu === 0)
|
|
36
|
+
continue;
|
|
37
|
+
// TODO: 役牌のダブル役(ダブ東など)の場合、現状の YakuhaiDefinition は Han * count を返すが、
|
|
38
|
+
// YakuResult の形式としては [Name, TotalHan] なのか [Name, Han], [Name, Han] なのか議論が必要。
|
|
39
|
+
// 現状の実装: [checker.yaku.name, hanValue] を count 回 push していた。
|
|
40
|
+
// 新しい getHansu は total han を返すため、1回 push すればよいのか?
|
|
41
|
+
// しかし役の数え方としては「役牌:白」「役牌:發」は別だが、「ダブ東」は「役牌:東」が2つ?
|
|
42
|
+
// 一般的には「自風 東」「場風 東」という2つの役扱い。
|
|
43
|
+
// しかしここでは YakuhaiDefinition が汎用的になっている。
|
|
44
|
+
// ユーザー要望の simple refactor に従い、getHansu が返す値をそのまま1つの役として扱う。
|
|
45
|
+
// ただし Yakuhai の getHansu は (open * count) を返しているので、
|
|
46
|
+
// ダブ東なら 2飜 の役が1つ、という扱いになる。
|
|
47
|
+
currentResult.push([definition.yaku.name, hansu]);
|
|
48
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
49
|
+
currentHan += hansu;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if (currentHan > maxHan) {
|
|
53
|
+
maxHan = currentHan;
|
|
54
|
+
bestResult = currentResult;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
// どの構造としても解釈できない、または役が成立しない場合は空配列
|
|
58
|
+
if (maxHan === -1) {
|
|
59
|
+
return [];
|
|
60
|
+
}
|
|
61
|
+
return bestResult;
|
|
62
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { createYakuDefinition } from "../../factory";
|
|
2
|
+
const CHIITOITSU_YAKU = {
|
|
3
|
+
name: "Chiitoitsu",
|
|
4
|
+
han: {
|
|
5
|
+
open: 0, // 門前限定
|
|
6
|
+
closed: 2,
|
|
7
|
+
},
|
|
8
|
+
};
|
|
9
|
+
const checkChiitoitsu = (hand) => {
|
|
10
|
+
return hand.type === "Chiitoitsu";
|
|
11
|
+
};
|
|
12
|
+
export const chiitoitsuDefinition = createYakuDefinition(CHIITOITSU_YAKU, checkChiitoitsu);
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { isSuupai, kindIdToHaiType } from "../../../../core/hai";
|
|
2
|
+
import { HaiType } from "../../../../types";
|
|
3
|
+
import { createYakuDefinition } from "../../factory";
|
|
4
|
+
const CHINITSU_YAKU = {
|
|
5
|
+
name: "Chinitsu",
|
|
6
|
+
han: {
|
|
7
|
+
open: 5,
|
|
8
|
+
closed: 6,
|
|
9
|
+
},
|
|
10
|
+
};
|
|
11
|
+
const checkChinitsu = (hand) => {
|
|
12
|
+
let blocks;
|
|
13
|
+
if (hand.type === "Mentsu") {
|
|
14
|
+
blocks = [hand.jantou, ...hand.fourMentsu];
|
|
15
|
+
}
|
|
16
|
+
else if (hand.type === "Chiitoitsu") {
|
|
17
|
+
blocks = hand.pairs;
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
// ブロック内の全ての牌をフラットな配列にする
|
|
23
|
+
const allHais = blocks.flatMap((b) => b.hais);
|
|
24
|
+
// 1. 字牌が含まれていないこと
|
|
25
|
+
const hasJihai = allHais.some((k) => kindIdToHaiType(k) === HaiType.Jihai);
|
|
26
|
+
if (hasJihai)
|
|
27
|
+
return false;
|
|
28
|
+
// 2. 数牌が全て同じ種類であること
|
|
29
|
+
const suupais = allHais.filter((k) => isSuupai(k));
|
|
30
|
+
// 数牌が含まれていない(字一色想定だが上記でJihaiチェック済みなので事実上ありえない)場合は不成立
|
|
31
|
+
if (suupais.length === 0)
|
|
32
|
+
return false;
|
|
33
|
+
const firstSuupai = suupais[0];
|
|
34
|
+
if (firstSuupai === undefined)
|
|
35
|
+
return false;
|
|
36
|
+
const firstSuupaiType = kindIdToHaiType(firstSuupai);
|
|
37
|
+
const isAllSameType = suupais.every((k) => kindIdToHaiType(k) === firstSuupaiType);
|
|
38
|
+
return isAllSameType;
|
|
39
|
+
};
|
|
40
|
+
export const chinitsuDefinition = createYakuDefinition(CHINITSU_YAKU, checkChinitsu);
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { isSuupai, isYaochu } from "../../../../core/hai";
|
|
2
|
+
import { createYakuDefinition } from "../../factory";
|
|
3
|
+
const CHINROUTOU_YAKU = {
|
|
4
|
+
name: "Chinroutou",
|
|
5
|
+
han: {
|
|
6
|
+
open: 13,
|
|
7
|
+
closed: 13,
|
|
8
|
+
},
|
|
9
|
+
};
|
|
10
|
+
const checkChinroutou = (hand) => {
|
|
11
|
+
// 老頭牌は6種類(1m,9m,1p,9p,1s,9s)しかないため、七対子(7種)は成立しない
|
|
12
|
+
if (hand.type !== "Mentsu")
|
|
13
|
+
return false;
|
|
14
|
+
const allBlocks = [hand.jantou, ...hand.fourMentsu];
|
|
15
|
+
// 全てが老頭牌(字牌以外の么九牌)で構成されていること
|
|
16
|
+
const allRoutou = allBlocks.every((block) => block.hais.every((k) => isYaochu(k) && isSuupai(k)));
|
|
17
|
+
if (!allRoutou)
|
|
18
|
+
return false;
|
|
19
|
+
return true;
|
|
20
|
+
};
|
|
21
|
+
export const chinroutouDefinition = createYakuDefinition(CHINROUTOU_YAKU, checkChinroutou);
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { createYakuDefinition } from "../../factory";
|
|
2
|
+
const CHUUREN_POUTOU_YAKU = {
|
|
3
|
+
name: "ChuurenPoutou",
|
|
4
|
+
han: {
|
|
5
|
+
open: 0, // 門前限定
|
|
6
|
+
closed: 13,
|
|
7
|
+
},
|
|
8
|
+
};
|
|
9
|
+
const checkChuurenPoutou = (hand, context) => {
|
|
10
|
+
// 1. 門前でなければならない
|
|
11
|
+
if (!context.isMenzen) {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
// 構造は Mentsu 手のみ(基本的には)
|
|
15
|
+
// 構造解析結果がどうあれ、元の手牌構成が九蓮宝燈の形かどうかを確認する
|
|
16
|
+
const allHais = [];
|
|
17
|
+
if (hand.type === "Mentsu") {
|
|
18
|
+
// 面子手の場合
|
|
19
|
+
for (const mentsu of hand.fourMentsu) {
|
|
20
|
+
allHais.push(...mentsu.hais);
|
|
21
|
+
}
|
|
22
|
+
allHais.push(...hand.jantou.hais);
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
// 九蓮宝燈は通常、面子手の特殊形として扱われることが多いが、
|
|
26
|
+
// 構造解析器が Mentsu として解釈できない場合も考慮すべきか?
|
|
27
|
+
// 一旦 Mentsu 型として解釈されていることを前提とする
|
|
28
|
+
// (九蓮宝燈は 111+234+567+8999+α のように分解可能なので Mentsu になるはず)
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
// 2. 混一色チェック(全て同じ色、字牌なし)
|
|
32
|
+
if (allHais.length === 0)
|
|
33
|
+
return false;
|
|
34
|
+
const firstHai = allHais[0];
|
|
35
|
+
if (firstHai === undefined)
|
|
36
|
+
return false;
|
|
37
|
+
// 字牌が含まれていたらNG
|
|
38
|
+
if (firstHai >= 27)
|
|
39
|
+
return false;
|
|
40
|
+
const suit = Math.floor(firstHai / 9); // 0, 1, 2
|
|
41
|
+
for (const hai of allHais) {
|
|
42
|
+
if (hai >= 27)
|
|
43
|
+
return false; // 字牌混入
|
|
44
|
+
if (Math.floor(hai / 9) !== suit)
|
|
45
|
+
return false; // 色混在
|
|
46
|
+
}
|
|
47
|
+
// 3. 数牌のカウントチェック
|
|
48
|
+
// 1が3枚以上, 9が3枚以上, 2-8が1枚以上
|
|
49
|
+
const counts = Array(9).fill(0);
|
|
50
|
+
for (const hai of allHais) {
|
|
51
|
+
const num = hai % 9; // 0-8
|
|
52
|
+
counts[num]++;
|
|
53
|
+
}
|
|
54
|
+
// 1 (index 0) >= 3
|
|
55
|
+
if (counts[0] < 3)
|
|
56
|
+
return false;
|
|
57
|
+
// 9 (index 8) >= 3
|
|
58
|
+
if (counts[8] < 3)
|
|
59
|
+
return false;
|
|
60
|
+
// 2-8 (index 1-7) >= 1
|
|
61
|
+
for (let i = 1; i <= 7; i++) {
|
|
62
|
+
if (counts[i] < 1)
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
// 合計14枚で上記を満たしていれば、必ず九蓮宝燈の形になる
|
|
66
|
+
// (3+3+7 = 13枚が必須パーツで、残り1枚は何でもよいため)
|
|
67
|
+
return true;
|
|
68
|
+
};
|
|
69
|
+
export const chuurenPoutouDefinition = createYakuDefinition(CHUUREN_POUTOU_YAKU, checkChuurenPoutou);
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { createYakuDefinition } from "../../factory";
|
|
2
|
+
import { HaiKind } from "../../../../types";
|
|
3
|
+
const DAISANGEN_YAKU = {
|
|
4
|
+
name: "Daisangen",
|
|
5
|
+
han: {
|
|
6
|
+
open: 13,
|
|
7
|
+
closed: 13,
|
|
8
|
+
},
|
|
9
|
+
};
|
|
10
|
+
const checkDaisangen = (hand) => {
|
|
11
|
+
if (hand.type !== "Mentsu") {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
const sangenpai = [HaiKind.Haku, HaiKind.Hatsu, HaiKind.Chun];
|
|
15
|
+
// 1. 三元牌の刻子・槓子をカウント
|
|
16
|
+
let sangenKoutsuCount = 0;
|
|
17
|
+
const triplets = hand.fourMentsu.filter((m) => m.type === "Koutsu" || m.type === "Kantsu");
|
|
18
|
+
for (const triplet of triplets) {
|
|
19
|
+
if (sangenpai.includes(triplet.hais[0])) {
|
|
20
|
+
sangenKoutsuCount++;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
// 大三元の条件: 三元牌の刻子が3つ全てあること
|
|
24
|
+
return sangenKoutsuCount === 3;
|
|
25
|
+
};
|
|
26
|
+
export const daisangenDefinition = createYakuDefinition(DAISANGEN_YAKU, checkDaisangen);
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { createYakuDefinition } from "../../factory";
|
|
2
|
+
import { HaiKind } from "../../../../types";
|
|
3
|
+
const DAISUUSHII_YAKU = {
|
|
4
|
+
name: "Daisuushii",
|
|
5
|
+
han: {
|
|
6
|
+
// TODO: ダブル役満(26飜)とするかはルールによるため、一旦通常の役満として実装
|
|
7
|
+
open: 13,
|
|
8
|
+
closed: 13,
|
|
9
|
+
},
|
|
10
|
+
};
|
|
11
|
+
const checkDaisuushii = (hand) => {
|
|
12
|
+
if (hand.type !== "Mentsu") {
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
const windTiles = [
|
|
16
|
+
HaiKind.Ton,
|
|
17
|
+
HaiKind.Nan,
|
|
18
|
+
HaiKind.Sha,
|
|
19
|
+
HaiKind.Pei,
|
|
20
|
+
];
|
|
21
|
+
// 1. 風牌の刻子・槓子をカウント
|
|
22
|
+
let windKoutsuCount = 0;
|
|
23
|
+
const triplets = hand.fourMentsu.filter((m) => m.type === "Koutsu" || m.type === "Kantsu");
|
|
24
|
+
for (const triplet of triplets) {
|
|
25
|
+
if (windTiles.includes(triplet.hais[0])) {
|
|
26
|
+
windKoutsuCount++;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
// 大四喜の条件: 風牌の刻子が4つ全てあること
|
|
30
|
+
return windKoutsuCount === 4;
|
|
31
|
+
};
|
|
32
|
+
export const daisuushiiDefinition = createYakuDefinition(DAISUUSHII_YAKU, checkDaisuushii);
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { isYaochu, kindIdToHaiType } from "../../../../core/hai";
|
|
2
|
+
import { HaiType } from "../../../../types";
|
|
3
|
+
import { createYakuDefinition } from "../../factory";
|
|
4
|
+
const HONCHAN_YAKU = {
|
|
5
|
+
name: "Honchan",
|
|
6
|
+
han: {
|
|
7
|
+
open: 1,
|
|
8
|
+
closed: 2,
|
|
9
|
+
},
|
|
10
|
+
};
|
|
11
|
+
const checkHonchan = (hand) => {
|
|
12
|
+
if (hand.type !== "Mentsu")
|
|
13
|
+
return false;
|
|
14
|
+
const allBlocks = [hand.jantou, ...hand.fourMentsu];
|
|
15
|
+
// 1. 全ての面子・雀頭に么九牌(1・9・字牌)が含まれること
|
|
16
|
+
const allHasYaochu = allBlocks.every((block) => block.hais.some((k) => isYaochu(k)));
|
|
17
|
+
if (!allHasYaochu)
|
|
18
|
+
return false;
|
|
19
|
+
// 2. 少なくとも1つの順子が含まれること(混老頭の除外)
|
|
20
|
+
const hasShuntsu = hand.fourMentsu.some((m) => m.type === "Shuntsu");
|
|
21
|
+
if (!hasShuntsu)
|
|
22
|
+
return false;
|
|
23
|
+
// 3. 少なくとも1つの字牌が含まれること(純全帯幺九の除外)
|
|
24
|
+
const hasJihai = allBlocks.some((block) => block.hais.some((k) => kindIdToHaiType(k) === HaiType.Jihai));
|
|
25
|
+
if (!hasJihai)
|
|
26
|
+
return false;
|
|
27
|
+
return true;
|
|
28
|
+
};
|
|
29
|
+
export const honchanDefinition = createYakuDefinition(HONCHAN_YAKU, checkHonchan);
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { isSuupai, kindIdToHaiType } from "../../../../core/hai";
|
|
2
|
+
import { HaiType } from "../../../../types";
|
|
3
|
+
import { createYakuDefinition } from "../../factory";
|
|
4
|
+
const HONITSU_YAKU = {
|
|
5
|
+
name: "Honitsu",
|
|
6
|
+
han: {
|
|
7
|
+
open: 2,
|
|
8
|
+
closed: 3,
|
|
9
|
+
},
|
|
10
|
+
};
|
|
11
|
+
const checkHonitsu = (hand) => {
|
|
12
|
+
let blocks;
|
|
13
|
+
if (hand.type === "Mentsu") {
|
|
14
|
+
blocks = [hand.jantou, ...hand.fourMentsu];
|
|
15
|
+
}
|
|
16
|
+
else if (hand.type === "Chiitoitsu") {
|
|
17
|
+
blocks = hand.pairs;
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
// ブロック内の全ての牌をフラットな配列にする
|
|
23
|
+
const allHais = blocks.flatMap((b) => b.hais);
|
|
24
|
+
// 1. 字牌が少なくとも1つ含まれること(清一色の除外)
|
|
25
|
+
const hasJihai = allHais.some((k) => kindIdToHaiType(k) === HaiType.Jihai);
|
|
26
|
+
if (!hasJihai)
|
|
27
|
+
return false;
|
|
28
|
+
// 2. 数牌が全て同じ種類であること
|
|
29
|
+
const suupais = allHais.filter((k) => isSuupai(k));
|
|
30
|
+
// 数牌が含まれていない場合は字一色(または不成立)なので、ホンイツではない
|
|
31
|
+
if (suupais.length === 0)
|
|
32
|
+
return false;
|
|
33
|
+
const firstSuupai = suupais[0];
|
|
34
|
+
if (firstSuupai === undefined)
|
|
35
|
+
return false;
|
|
36
|
+
const firstSuupaiType = kindIdToHaiType(firstSuupai);
|
|
37
|
+
const isAllSameType = suupais.every((k) => kindIdToHaiType(k) === firstSuupaiType);
|
|
38
|
+
return isAllSameType;
|
|
39
|
+
};
|
|
40
|
+
export const honitsuDefinition = createYakuDefinition(HONITSU_YAKU, checkHonitsu);
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { isYaochu, kindIdToHaiType } from "../../../../core/hai";
|
|
2
|
+
import { HaiType } from "../../../../types";
|
|
3
|
+
import { createYakuDefinition } from "../../factory";
|
|
4
|
+
const HONROUTOU_YAKU = {
|
|
5
|
+
name: "Honroutou",
|
|
6
|
+
han: {
|
|
7
|
+
open: 2,
|
|
8
|
+
closed: 2,
|
|
9
|
+
},
|
|
10
|
+
};
|
|
11
|
+
const checkHonroutou = (hand) => {
|
|
12
|
+
let blocks;
|
|
13
|
+
if (hand.type === "Mentsu") {
|
|
14
|
+
blocks = [hand.jantou, ...hand.fourMentsu];
|
|
15
|
+
}
|
|
16
|
+
else if (hand.type === "Chiitoitsu") {
|
|
17
|
+
blocks = hand.pairs;
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
// 1. 全ての牌が么九牌(1・9・字牌)であること
|
|
23
|
+
// これにより順子(123など)が含まれる可能性も排除される(2,3は么九牌ではないため)
|
|
24
|
+
const allYaochu = blocks.every((block) => block.hais.every((k) => isYaochu(k)));
|
|
25
|
+
if (!allYaochu)
|
|
26
|
+
return false;
|
|
27
|
+
// 2. 少なくとも1つの字牌が含まれること(清老頭の除外)
|
|
28
|
+
const hasJihai = blocks.some((block) => block.hais.some((k) => kindIdToHaiType(k) === HaiType.Jihai));
|
|
29
|
+
if (!hasJihai)
|
|
30
|
+
return false;
|
|
31
|
+
return true;
|
|
32
|
+
};
|
|
33
|
+
export const honroutouDefinition = createYakuDefinition(HONROUTOU_YAKU, checkHonroutou);
|