@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,34 @@
|
|
|
1
|
+
import { createYakuDefinition } from "../../factory";
|
|
2
|
+
import { HaiKind } from "../../../../types";
|
|
3
|
+
const SHOUSUUSHII_YAKU = {
|
|
4
|
+
name: "Shousuushii",
|
|
5
|
+
han: {
|
|
6
|
+
open: 13,
|
|
7
|
+
closed: 13,
|
|
8
|
+
},
|
|
9
|
+
};
|
|
10
|
+
const checkShousuushii = (hand) => {
|
|
11
|
+
if (hand.type !== "Mentsu") {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
const windTiles = [
|
|
15
|
+
HaiKind.Ton,
|
|
16
|
+
HaiKind.Nan,
|
|
17
|
+
HaiKind.Sha,
|
|
18
|
+
HaiKind.Pei,
|
|
19
|
+
];
|
|
20
|
+
// 1. 風牌の刻子・槓子をカウント
|
|
21
|
+
let windKoutsuCount = 0;
|
|
22
|
+
const triplets = hand.fourMentsu.filter((m) => m.type === "Koutsu" || m.type === "Kantsu");
|
|
23
|
+
for (const triplet of triplets) {
|
|
24
|
+
if (windTiles.includes(triplet.hais[0])) {
|
|
25
|
+
windKoutsuCount++;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
// 2. 風牌の雀頭があるかチェック
|
|
29
|
+
const isWindJantou = windTiles.includes(hand.jantou.hais[0]);
|
|
30
|
+
// 小四喜の条件: 風牌の刻子が3つ かつ 風牌の雀頭が1つ
|
|
31
|
+
// (合計で4種類の風牌が揃うことになる。例: 東東東 南南南 西西西 北北)
|
|
32
|
+
return windKoutsuCount === 3 && isWindJantou;
|
|
33
|
+
};
|
|
34
|
+
export const shousuushiiDefinition = createYakuDefinition(SHOUSUUSHII_YAKU, checkShousuushii);
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
const SUUANKOU_YAKU = {
|
|
2
|
+
name: "Suuankou",
|
|
3
|
+
han: {
|
|
4
|
+
open: 0, // 門前限定(構造上必然的にそうなるが、定義としても門前)
|
|
5
|
+
closed: 13, // 通常役満(13) または ダブル役満(26)
|
|
6
|
+
},
|
|
7
|
+
};
|
|
8
|
+
const checkSuuankou = (hand, context) => {
|
|
9
|
+
if (hand.type !== "Mentsu") {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
// 1. 刻子・槓子を抽出
|
|
13
|
+
const triplets = hand.fourMentsu.filter((m) => m.type === "Koutsu" || m.type === "Kantsu");
|
|
14
|
+
let ankouCount = 0;
|
|
15
|
+
for (const triplet of triplets) {
|
|
16
|
+
// 副露している刻子は暗刻ではない
|
|
17
|
+
if (triplet.furo)
|
|
18
|
+
continue;
|
|
19
|
+
const isAgariHaiInTriplet = triplet.hais.includes(context.agariHai);
|
|
20
|
+
const isTanki = hand.jantou.hais[0] === context.agariHai;
|
|
21
|
+
if (context.isTsumo) {
|
|
22
|
+
// ツモなら、副露していなければ全て暗刻
|
|
23
|
+
ankouCount++;
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
// ロン和了の場合
|
|
27
|
+
if (isAgariHaiInTriplet) {
|
|
28
|
+
// 和了牌を含む刻子(シャボ待ちロン)は明刻
|
|
29
|
+
// 単騎待ちロンなら暗刻
|
|
30
|
+
if (isTanki) {
|
|
31
|
+
ankouCount++;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
// 和了牌を含まない刻子は暗刻
|
|
36
|
+
ankouCount++;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return ankouCount === 4;
|
|
41
|
+
};
|
|
42
|
+
// 四暗刻はダブル役満(単騎待ち)判定が必要なため、
|
|
43
|
+
// createYakuDefinitionのデフォルトのgetHansuロジックをオーバーライドするか、
|
|
44
|
+
// このcheck関数内で判定してフラグを渡すなどの工夫が必要。
|
|
45
|
+
// しかし createYakuDefinition は boolean を返す check 関数しか受け取らない。
|
|
46
|
+
// ここでは factory 側を拡張するのではなく、この定義でカスタム実装を行うか、
|
|
47
|
+
// あるいは factory を通さずに直接 YakuDefinition を作る。
|
|
48
|
+
// factoryの改修はスコープ外のため、ここではオブジェクトリテラルで定義を作成する。
|
|
49
|
+
export const suuankouDefinition = {
|
|
50
|
+
yaku: SUUANKOU_YAKU,
|
|
51
|
+
isSatisfied: (hand, context) => checkSuuankou(hand, context),
|
|
52
|
+
getHansu: (hand, context) => {
|
|
53
|
+
if (!checkSuuankou(hand, context))
|
|
54
|
+
return 0;
|
|
55
|
+
// 四暗刻単騎の判定
|
|
56
|
+
const isTanki = hand.type === "Mentsu" && hand.jantou.hais[0] === context.agariHai;
|
|
57
|
+
// 単騎待ちならダブル役満(26)
|
|
58
|
+
if (isTanki) {
|
|
59
|
+
return 26;
|
|
60
|
+
}
|
|
61
|
+
return 13;
|
|
62
|
+
},
|
|
63
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { createYakuDefinition } from "../../factory";
|
|
2
|
+
const SUUKANTSU_YAKU = {
|
|
3
|
+
name: "Suukantsu",
|
|
4
|
+
han: {
|
|
5
|
+
open: 13,
|
|
6
|
+
closed: 13,
|
|
7
|
+
},
|
|
8
|
+
};
|
|
9
|
+
const checkSuukantsu = (hand) => {
|
|
10
|
+
if (hand.type !== "Mentsu") {
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
// 1. 槓子を抽出
|
|
14
|
+
const kantsuList = hand.fourMentsu.filter((m) => m.type === "Kantsu");
|
|
15
|
+
// 2. 槓子が4つあれば成立
|
|
16
|
+
return kantsuList.length === 4;
|
|
17
|
+
};
|
|
18
|
+
export const suukantsuDefinition = createYakuDefinition(SUUKANTSU_YAKU, checkSuukantsu);
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { isYaochu } from "../../../../core/hai";
|
|
2
|
+
import { createYakuDefinition } from "../../factory";
|
|
3
|
+
const TANYAO_YAKU = {
|
|
4
|
+
name: "Tanyao",
|
|
5
|
+
han: {
|
|
6
|
+
open: 1,
|
|
7
|
+
closed: 1,
|
|
8
|
+
},
|
|
9
|
+
};
|
|
10
|
+
const checkTanyao = (hand) => {
|
|
11
|
+
if (hand.type !== "Mentsu")
|
|
12
|
+
return false;
|
|
13
|
+
// 雀頭のチェック
|
|
14
|
+
if (isYaochu(hand.jantou.hais[0]))
|
|
15
|
+
return false;
|
|
16
|
+
// 面子のチェック
|
|
17
|
+
for (const mentsu of hand.fourMentsu) {
|
|
18
|
+
if (mentsu.hais.some(isYaochu))
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
return true;
|
|
22
|
+
};
|
|
23
|
+
export const tanyaoDefinition = createYakuDefinition(TANYAO_YAKU, checkTanyao);
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { createYakuDefinition } from "../../factory";
|
|
2
|
+
const TOITOI_YAKU = {
|
|
3
|
+
name: "Toitoi",
|
|
4
|
+
han: {
|
|
5
|
+
open: 2,
|
|
6
|
+
closed: 2,
|
|
7
|
+
},
|
|
8
|
+
};
|
|
9
|
+
const checkToitoi = (hand) => {
|
|
10
|
+
if (hand.type !== "Mentsu") {
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
// 全ての面子が刻子(Koutsu)または槓子(Kantsu)であることを確認
|
|
14
|
+
return hand.fourMentsu.every((mentsu) => mentsu.type === "Koutsu" || mentsu.type === "Kantsu");
|
|
15
|
+
};
|
|
16
|
+
export const toitoiDefinition = createYakuDefinition(TOITOI_YAKU, checkToitoi);
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { createYakuDefinition } from "../../factory";
|
|
2
|
+
import { HaiKind } from "../../../../types";
|
|
3
|
+
const TSUUIISOU_YAKU = {
|
|
4
|
+
name: "Tsuuiisou",
|
|
5
|
+
han: {
|
|
6
|
+
open: 13,
|
|
7
|
+
closed: 13,
|
|
8
|
+
},
|
|
9
|
+
};
|
|
10
|
+
const isJihai = (id) => {
|
|
11
|
+
return id >= HaiKind.Ton && id <= HaiKind.Chun;
|
|
12
|
+
};
|
|
13
|
+
const checkTsuuiisou = (hand) => {
|
|
14
|
+
const allHais = [];
|
|
15
|
+
if (hand.type === "Mentsu") {
|
|
16
|
+
// 面子手の場合
|
|
17
|
+
for (const mentsu of hand.fourMentsu) {
|
|
18
|
+
allHais.push(...mentsu.hais);
|
|
19
|
+
}
|
|
20
|
+
allHais.push(...hand.jantou.hais);
|
|
21
|
+
}
|
|
22
|
+
else if (hand.type === "Chiitoitsu") {
|
|
23
|
+
// 七対子の場合
|
|
24
|
+
for (const pair of hand.pairs) {
|
|
25
|
+
allHais.push(...pair.hais);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
// 国士無双など(国士は字一色にはなり得ないが、構造上は考慮)
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
// 全ての牌が字牌であれば成立
|
|
33
|
+
return allHais.every(isJihai);
|
|
34
|
+
};
|
|
35
|
+
export const tsuuiisouDefinition = createYakuDefinition(TSUUIISOU_YAKU, checkTsuuiisou);
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { HaiKind } from "../../../../types";
|
|
2
|
+
import { createYakuDefinition } from "../../factory";
|
|
3
|
+
function createYakuhaiDefinition(name, tile) {
|
|
4
|
+
const HAN_CONFIG = { closed: 1, open: 1 };
|
|
5
|
+
const check = (hand) => {
|
|
6
|
+
if (hand.type !== "Mentsu")
|
|
7
|
+
return false;
|
|
8
|
+
for (const mentsu of hand.fourMentsu) {
|
|
9
|
+
if (mentsu.type === "Koutsu" || mentsu.type === "Kantsu") {
|
|
10
|
+
if (mentsu.hais[0] === tile) {
|
|
11
|
+
return true;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
return false;
|
|
16
|
+
};
|
|
17
|
+
return createYakuDefinition({ name, han: HAN_CONFIG }, check);
|
|
18
|
+
}
|
|
19
|
+
export const hakuDefinition = createYakuhaiDefinition("Haku", HaiKind.Haku);
|
|
20
|
+
export const hatsuDefinition = createYakuhaiDefinition("Hatsu", HaiKind.Hatsu);
|
|
21
|
+
export const chunDefinition = createYakuhaiDefinition("Chun", HaiKind.Chun);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./definitions";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./definitions";
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { countHaiKind } from "../../../../core/tehai";
|
|
2
|
+
/**
|
|
3
|
+
* 手牌を七対子(7つの対子)として構造化する。
|
|
4
|
+
*/
|
|
5
|
+
export function getHouraStructuresForChiitoitsu(tehai) {
|
|
6
|
+
// 七対子は門前のみ(定義によっては鳴きも許容する場合があるが、一般的には門前)
|
|
7
|
+
if (tehai.exposed.length > 0)
|
|
8
|
+
return [];
|
|
9
|
+
const counts = countHaiKind(tehai.closed);
|
|
10
|
+
const pairs = [];
|
|
11
|
+
for (let i = 0; i < 34; i++) {
|
|
12
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
13
|
+
const kind = i;
|
|
14
|
+
const count = counts[kind];
|
|
15
|
+
if (count === 2) {
|
|
16
|
+
pairs.push({ type: "Toitsu", hais: [kind, kind] });
|
|
17
|
+
}
|
|
18
|
+
else if (count === 4) {
|
|
19
|
+
// 4枚使いの七対子を認めるか(ローカルルール次第だが、通常は認めない)
|
|
20
|
+
// ここでは標準的なルールに従い、4枚あっても2対子とはみなさない実装とする
|
|
21
|
+
// ※4枚使い七対子を実装する場合は pairs.push(...) を2回行う
|
|
22
|
+
return [];
|
|
23
|
+
}
|
|
24
|
+
else if (count > 0) {
|
|
25
|
+
// 2枚でない牌がある場合は七対子不成立
|
|
26
|
+
return [];
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
if (pairs.length !== 7)
|
|
30
|
+
return [];
|
|
31
|
+
return [
|
|
32
|
+
{
|
|
33
|
+
type: "Chiitoitsu",
|
|
34
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
35
|
+
pairs: pairs,
|
|
36
|
+
},
|
|
37
|
+
];
|
|
38
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Tehai14 } from "../../../../types";
|
|
2
|
+
import type { HouraStructure } from "../../types";
|
|
3
|
+
export * from "./mentsu-te";
|
|
4
|
+
export * from "./chiitoitsu";
|
|
5
|
+
export * from "./kokushi";
|
|
6
|
+
/**
|
|
7
|
+
* 手牌をすべての可能な和了形に構造化する。
|
|
8
|
+
* 面子手、七対子、国士無双の全ての可能性を探索する。
|
|
9
|
+
*/
|
|
10
|
+
export declare function getHouraStructures(tehai: Tehai14): HouraStructure[];
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { getHouraStructuresForMentsuTe } from "./mentsu-te";
|
|
2
|
+
import { getHouraStructuresForChiitoitsu } from "./chiitoitsu";
|
|
3
|
+
import { getHouraStructuresForKokushi } from "./kokushi";
|
|
4
|
+
export * from "./mentsu-te";
|
|
5
|
+
export * from "./chiitoitsu";
|
|
6
|
+
export * from "./kokushi";
|
|
7
|
+
/**
|
|
8
|
+
* 手牌をすべての可能な和了形に構造化する。
|
|
9
|
+
* 面子手、七対子、国士無双の全ての可能性を探索する。
|
|
10
|
+
*/
|
|
11
|
+
export function getHouraStructures(tehai) {
|
|
12
|
+
return [
|
|
13
|
+
...getHouraStructuresForMentsuTe(tehai),
|
|
14
|
+
...getHouraStructuresForChiitoitsu(tehai),
|
|
15
|
+
...getHouraStructuresForKokushi(tehai),
|
|
16
|
+
];
|
|
17
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { countHaiKind } from "../../../../core/tehai";
|
|
2
|
+
import { isYaochu } from "../../../../core/hai";
|
|
3
|
+
/**
|
|
4
|
+
* 手牌を国士無双(13種の么九牌+雀頭)として構造化する。
|
|
5
|
+
*/
|
|
6
|
+
export function getHouraStructuresForKokushi(tehai) {
|
|
7
|
+
// 国士無双は門前のみ
|
|
8
|
+
if (tehai.exposed.length > 0)
|
|
9
|
+
return [];
|
|
10
|
+
const counts = countHaiKind(tehai.closed);
|
|
11
|
+
const yaochuList = [];
|
|
12
|
+
let jantou;
|
|
13
|
+
for (let i = 0; i < 34; i++) {
|
|
14
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
15
|
+
const kind = i;
|
|
16
|
+
const count = counts[kind];
|
|
17
|
+
if (count > 0) {
|
|
18
|
+
if (!isYaochu(kind))
|
|
19
|
+
return []; // 么九牌以外が含まれていれば不成立
|
|
20
|
+
if (count === 1) {
|
|
21
|
+
yaochuList.push(kind);
|
|
22
|
+
}
|
|
23
|
+
else if (count === 2) {
|
|
24
|
+
if (jantou !== undefined)
|
|
25
|
+
return []; // 雀頭が既に存在すれば不成立(複数棋の雀頭候補)
|
|
26
|
+
jantou = kind;
|
|
27
|
+
yaochuList.push(kind);
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
return []; // 3枚以上あれば不成立
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
if (yaochuList.length !== 13 || jantou === undefined)
|
|
35
|
+
return [];
|
|
36
|
+
return [
|
|
37
|
+
{
|
|
38
|
+
type: "Kokushi",
|
|
39
|
+
yaochu: yaochuList,
|
|
40
|
+
jantou,
|
|
41
|
+
},
|
|
42
|
+
];
|
|
43
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { MentsuHouraStructure } from "../../types";
|
|
2
|
+
import type { Tehai14 } from "../../../../types";
|
|
3
|
+
/**
|
|
4
|
+
* 手牌を標準形(4面子1雀頭)に構造化する。
|
|
5
|
+
* 七対子や国士無双は対象外。
|
|
6
|
+
*
|
|
7
|
+
* 【役判定について】
|
|
8
|
+
* この関数は純粋に「4面子1雀頭」の形になっているかのみを検証します。
|
|
9
|
+
* 役が成立しているかどうか(和了できるかどうか)は判定しません。
|
|
10
|
+
* そのため、役なし(Yakunashi)の手牌であっても構造的に整合していれば結果を返します。
|
|
11
|
+
*
|
|
12
|
+
* 【戻り値が配列である理由について】
|
|
13
|
+
* 麻雀の手牌は、同じ牌構成であっても複数の解釈(多義性)が成立する場合があります。
|
|
14
|
+
* 例: `111222333m`
|
|
15
|
+
* - 三暗刻 (111 + 222 + 333)
|
|
16
|
+
* - 三連刻/一盃口 (123 + 123 + 123)
|
|
17
|
+
*
|
|
18
|
+
* このように成立する役が変わる可能性があるため、可能な全ての構造化パターンをリストとして返します。
|
|
19
|
+
* 利用側は、これらのパターンのうち最も高得点となるものを選択する必要があります。
|
|
20
|
+
*
|
|
21
|
+
* @param tehai 和了形の手牌
|
|
22
|
+
* @returns 可能な構造化パターンのリスト。構造化できない場合は空配列。
|
|
23
|
+
*/
|
|
24
|
+
export declare function getHouraStructuresForMentsuTe(tehai: Tehai14): MentsuHouraStructure[];
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { validateTehai14, countHaiKind } from "../../../../core/tehai";
|
|
2
|
+
import { isTuple4 } from "../../../../utils/assertions";
|
|
3
|
+
/**
|
|
4
|
+
* 手牌を標準形(4面子1雀頭)に構造化する。
|
|
5
|
+
* 七対子や国士無双は対象外。
|
|
6
|
+
*
|
|
7
|
+
* 【役判定について】
|
|
8
|
+
* この関数は純粋に「4面子1雀頭」の形になっているかのみを検証します。
|
|
9
|
+
* 役が成立しているかどうか(和了できるかどうか)は判定しません。
|
|
10
|
+
* そのため、役なし(Yakunashi)の手牌であっても構造的に整合していれば結果を返します。
|
|
11
|
+
*
|
|
12
|
+
* 【戻り値が配列である理由について】
|
|
13
|
+
* 麻雀の手牌は、同じ牌構成であっても複数の解釈(多義性)が成立する場合があります。
|
|
14
|
+
* 例: `111222333m`
|
|
15
|
+
* - 三暗刻 (111 + 222 + 333)
|
|
16
|
+
* - 三連刻/一盃口 (123 + 123 + 123)
|
|
17
|
+
*
|
|
18
|
+
* このように成立する役が変わる可能性があるため、可能な全ての構造化パターンをリストとして返します。
|
|
19
|
+
* 利用側は、これらのパターンのうち最も高得点となるものを選択する必要があります。
|
|
20
|
+
*
|
|
21
|
+
* @param tehai 和了形の手牌
|
|
22
|
+
* @returns 可能な構造化パターンのリスト。構造化できない場合は空配列。
|
|
23
|
+
*/
|
|
24
|
+
export function getHouraStructuresForMentsuTe(tehai) {
|
|
25
|
+
validateTehai14(tehai);
|
|
26
|
+
// HaiKindDistributionはreadonlyなので、可変配列に複製する
|
|
27
|
+
const counts = [...countHaiKind(tehai.closed)];
|
|
28
|
+
const results = [];
|
|
29
|
+
// 1. 雀頭候補を探す
|
|
30
|
+
for (let i = 0; i < 34; i++) {
|
|
31
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
32
|
+
const kind = i;
|
|
33
|
+
if ((counts[kind] ?? 0) >= 2) {
|
|
34
|
+
// 雀頭抜き出し
|
|
35
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
36
|
+
counts[kind] -= 2;
|
|
37
|
+
// 残りの牌で面子分解
|
|
38
|
+
const requiredMentsuCount = 4 - tehai.exposed.length;
|
|
39
|
+
const subResults = decomposeClosedMentsu(counts, requiredMentsuCount);
|
|
40
|
+
// subResultsには閉じた部分で見つかった面子のリストが含まれる
|
|
41
|
+
for (const closedMentsu of subResults) {
|
|
42
|
+
// 副露面子と結合して完全な構成を作成する
|
|
43
|
+
const fullMentsuList = [...closedMentsu, ...tehai.exposed];
|
|
44
|
+
// 4面子であることを確認(ロジック上は保証されているはずだが、念のため)
|
|
45
|
+
if (isTuple4(fullMentsuList)) {
|
|
46
|
+
results.push({
|
|
47
|
+
type: "Mentsu",
|
|
48
|
+
fourMentsu: fullMentsuList,
|
|
49
|
+
jantou: { type: "Toitsu", hais: [kind, kind] },
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
// バックトラック
|
|
54
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
55
|
+
counts[kind] += 2;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return results;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* 閉じた手牌の残りを面子に分解する再帰関数
|
|
62
|
+
*/
|
|
63
|
+
function decomposeClosedMentsu(
|
|
64
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
65
|
+
counts, requiredCount) {
|
|
66
|
+
if (requiredCount === 0) {
|
|
67
|
+
// 全ての牌が使用されたか確認
|
|
68
|
+
const remaining = counts.reduce((acc, c) => acc + c, 0);
|
|
69
|
+
return remaining === 0 ? [[]] : [];
|
|
70
|
+
}
|
|
71
|
+
// 面子の重複順列を防ぎ決定論的な順序を強制するため、カウントが0より大きい最初の牌を見つける
|
|
72
|
+
let firstIndex = -1;
|
|
73
|
+
for (let i = 0; i < 34; i++) {
|
|
74
|
+
if ((counts[i] ?? 0) > 0) {
|
|
75
|
+
firstIndex = i;
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (firstIndex === -1) {
|
|
80
|
+
// Should not happen if requiredCount > 0, unless invalid hand
|
|
81
|
+
return [];
|
|
82
|
+
}
|
|
83
|
+
const results = [];
|
|
84
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
85
|
+
const kind = firstIndex;
|
|
86
|
+
// 刻子を試す
|
|
87
|
+
if ((counts[kind] ?? 0) >= 3) {
|
|
88
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
89
|
+
counts[kind] -= 3;
|
|
90
|
+
const tails = decomposeClosedMentsu(counts, requiredCount - 1);
|
|
91
|
+
const koutsu = { type: "Koutsu", hais: [kind, kind, kind] };
|
|
92
|
+
for (const tail of tails) {
|
|
93
|
+
results.push([koutsu, ...tail]);
|
|
94
|
+
}
|
|
95
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
96
|
+
counts[kind] += 3; // バックトラック
|
|
97
|
+
}
|
|
98
|
+
// 順子を試す
|
|
99
|
+
// 数牌(0-26)かつ7を超えない(n, n+1, n+2を作れる)場合のみ有効
|
|
100
|
+
if (kind < 27 && kind % 9 <= 6) {
|
|
101
|
+
const k1 = kind;
|
|
102
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
103
|
+
const k2 = (kind + 1);
|
|
104
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
105
|
+
const k3 = (kind + 2);
|
|
106
|
+
if ((counts[k2] ?? 0) > 0 && (counts[k3] ?? 0) > 0) {
|
|
107
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
108
|
+
counts[k1] -= 1;
|
|
109
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
110
|
+
counts[k2] -= 1;
|
|
111
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
112
|
+
counts[k3] -= 1;
|
|
113
|
+
const tails = decomposeClosedMentsu(counts, requiredCount - 1);
|
|
114
|
+
const shuntsu = { type: "Shuntsu", hais: [k1, k2, k3] };
|
|
115
|
+
for (const tail of tails) {
|
|
116
|
+
results.push([shuntsu, ...tail]);
|
|
117
|
+
}
|
|
118
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
119
|
+
counts[k1] += 1;
|
|
120
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
121
|
+
counts[k2] += 1;
|
|
122
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
123
|
+
counts[k3] += 1; // バックトラック
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return results;
|
|
127
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import type { HaiKindId, Kazehai, Shuntsu, Koutsu, Kantsu, Toitsu, Mentsu, CompletedMentsu } from "../../types";
|
|
2
|
+
export type { Kazehai, Shuntsu, Koutsu, Kantsu, Toitsu, Mentsu };
|
|
3
|
+
/**
|
|
4
|
+
* 役牌 (Yakuhai)
|
|
5
|
+
*
|
|
6
|
+
* 構造的に成立する三元牌。
|
|
7
|
+
* ※場風・自風は状況役(Bakaze, Jikaze)として別途定義するためここには含めない。
|
|
8
|
+
*/
|
|
9
|
+
export type Yakuhai = "Haku" | "Hatsu" | "Chun";
|
|
10
|
+
/**
|
|
11
|
+
* 手牌役 (TehaiYaku)
|
|
12
|
+
*
|
|
13
|
+
* 手牌役(手牌の構成のみで成立する役)の識別子。
|
|
14
|
+
* 偶然役(嶺上開花など)や状況役(場風、自風、立直など)は含まない。
|
|
15
|
+
*/
|
|
16
|
+
export type TehaiYaku = "Tanyao" | "Pinfu" | "Iipeikou" | Yakuhai | "SanshokuDoujun" | "Ikkitsuukan" | "Honchan" | "Chiitoitsu" | "Toitoi" | "Sanankou" | "Sankantsu" | "SanshokuDoukou" | "Honroutou" | "Shousangen" | "Honitsu" | "Junchan" | "Ryanpeikou" | "Chinitsu" | "KokushiMusou" | "Suuankou" | "Daisangen" | "Shousuushii" | "Daisuushii" | "Tsuuiisou" | "Chinroutou" | "Ryuuiisou" | "ChuurenPoutou" | "Suukantsu" | "MenzenTsumo";
|
|
17
|
+
/**
|
|
18
|
+
* 役の飜数 (Hansu)
|
|
19
|
+
*
|
|
20
|
+
* 1, 2, 3, 5(流し満貫/清一色喰い下がり), 6(清一色), 13(役満), 26(ダブル役満)
|
|
21
|
+
*/
|
|
22
|
+
export type Hansu = 1 | 2 | 3 | 5 | 6 | 13 | 26;
|
|
23
|
+
export interface MentsuHouraStructure {
|
|
24
|
+
readonly type: "Mentsu";
|
|
25
|
+
readonly fourMentsu: readonly [
|
|
26
|
+
CompletedMentsu,
|
|
27
|
+
CompletedMentsu,
|
|
28
|
+
CompletedMentsu,
|
|
29
|
+
CompletedMentsu
|
|
30
|
+
];
|
|
31
|
+
readonly jantou: Toitsu;
|
|
32
|
+
}
|
|
33
|
+
export interface ChiitoitsuHouraStructure {
|
|
34
|
+
readonly type: "Chiitoitsu";
|
|
35
|
+
readonly pairs: readonly [
|
|
36
|
+
Toitsu,
|
|
37
|
+
Toitsu,
|
|
38
|
+
Toitsu,
|
|
39
|
+
Toitsu,
|
|
40
|
+
Toitsu,
|
|
41
|
+
Toitsu,
|
|
42
|
+
Toitsu
|
|
43
|
+
];
|
|
44
|
+
}
|
|
45
|
+
export interface KokushiHouraStructure {
|
|
46
|
+
readonly type: "Kokushi";
|
|
47
|
+
/** 13種類の么九牌(重複なし) */
|
|
48
|
+
readonly yaochu: readonly HaiKindId[];
|
|
49
|
+
/** 雀頭となる牌の種類 */
|
|
50
|
+
readonly jantou: HaiKindId;
|
|
51
|
+
}
|
|
52
|
+
export type HouraStructure = MentsuHouraStructure | ChiitoitsuHouraStructure | KokushiHouraStructure;
|
|
53
|
+
/**
|
|
54
|
+
* 役の飜数定義
|
|
55
|
+
*/
|
|
56
|
+
export interface YakuHanConfig {
|
|
57
|
+
/** 門前時の飜数 */
|
|
58
|
+
readonly closed: Hansu;
|
|
59
|
+
/**
|
|
60
|
+
* 鳴きあり時の飜数 (0なら不成立)
|
|
61
|
+
*
|
|
62
|
+
* @remarks
|
|
63
|
+
* この値が 0 の場合、その役は**門前限定(Menzen-only)**であることを意味します。
|
|
64
|
+
* 役判定ロジックにおいては、この値が 0 でかつ手牌が副露されている場合、
|
|
65
|
+
* 役の条件を満たしていても不成立とみなされます。
|
|
66
|
+
*/
|
|
67
|
+
readonly open: Hansu | 0;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* 役ID (YakuName)
|
|
71
|
+
*
|
|
72
|
+
* 全ての役の識別子ユニオン。
|
|
73
|
+
*/
|
|
74
|
+
export type YakuName = TehaiYaku;
|
|
75
|
+
/**
|
|
76
|
+
* 役判定結果 (YakuResult)
|
|
77
|
+
*
|
|
78
|
+
* 成立した役と、その飜数のペアのリスト。
|
|
79
|
+
* 役が一つも成立しない場合は空配列となる。
|
|
80
|
+
*/
|
|
81
|
+
export type YakuResult = readonly [YakuName, Hansu][];
|
|
82
|
+
export interface HouraContext {
|
|
83
|
+
/** 手牌が門前かどうか(暗槓が含まれていても門前扱い) */
|
|
84
|
+
readonly isMenzen: boolean;
|
|
85
|
+
/** 和了牌(平和判定などに必要)。省略時は判定不能な役がある */
|
|
86
|
+
readonly agariHai: HaiKindId;
|
|
87
|
+
/** 場風牌 */
|
|
88
|
+
readonly bakaze?: Kazehai | undefined;
|
|
89
|
+
/** 自風牌 */
|
|
90
|
+
readonly jikaze?: Kazehai | undefined;
|
|
91
|
+
/** ツモ和了かどうか(暗刻系役の判定などに使用) */
|
|
92
|
+
readonly isTsumo?: boolean | undefined;
|
|
93
|
+
/**
|
|
94
|
+
* ドラ表示牌 (表ドラ) のリスト
|
|
95
|
+
*/
|
|
96
|
+
readonly doraMarkers: readonly HaiKindId[];
|
|
97
|
+
/**
|
|
98
|
+
* 裏ドラ表示牌のリスト (リーチ時のみ有効)
|
|
99
|
+
*/
|
|
100
|
+
readonly uraDoraMarkers?: readonly HaiKindId[];
|
|
101
|
+
}
|
|
102
|
+
export interface Yaku {
|
|
103
|
+
readonly name: YakuName;
|
|
104
|
+
/** 飜数 (喰い下がり考慮) */
|
|
105
|
+
readonly han: {
|
|
106
|
+
readonly open: Hansu | 0;
|
|
107
|
+
readonly closed: Hansu;
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* 役の成立判定関数
|
|
112
|
+
* @param hand 分解された手牌構造
|
|
113
|
+
* @param context 判定コンテキスト
|
|
114
|
+
* @returns 成立回数 (0なら不成立、役牌などで複数成立しうる)
|
|
115
|
+
*/
|
|
116
|
+
export type YakuCheck = (hand: HouraStructure, context: HouraContext) => number;
|
|
117
|
+
export interface YakuDefinition {
|
|
118
|
+
readonly yaku: Yaku;
|
|
119
|
+
readonly isSatisfied: (hand: HouraStructure, context: HouraContext) => boolean;
|
|
120
|
+
readonly getHansu: (hand: HouraStructure, context: HouraContext) => Hansu | 0;
|
|
121
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|