@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.
Files changed (134) hide show
  1. package/README.md +58 -0
  2. package/dist/core/dora.d.ts +14 -0
  3. package/dist/core/dora.js +71 -0
  4. package/dist/core/hai.d.ts +30 -0
  5. package/dist/core/hai.js +78 -0
  6. package/dist/core/machi.d.ts +11 -0
  7. package/dist/core/machi.js +58 -0
  8. package/dist/core/mentsu.d.ts +26 -0
  9. package/dist/core/mentsu.js +87 -0
  10. package/dist/core/tehai.d.ts +38 -0
  11. package/dist/core/tehai.js +87 -0
  12. package/dist/errors.d.ts +40 -0
  13. package/dist/errors.js +58 -0
  14. package/dist/features/machi/index.d.ts +9 -0
  15. package/dist/features/machi/index.js +37 -0
  16. package/dist/features/machi/types.d.ts +1 -0
  17. package/dist/features/machi/types.js +1 -0
  18. package/dist/features/parser/index.d.ts +19 -0
  19. package/dist/features/parser/index.js +28 -0
  20. package/dist/features/parser/mspz.d.ts +85 -0
  21. package/dist/features/parser/mspz.js +365 -0
  22. package/dist/features/points/constants.d.ts +27 -0
  23. package/dist/features/points/constants.js +30 -0
  24. package/dist/features/points/index.d.ts +21 -0
  25. package/dist/features/points/index.js +174 -0
  26. package/dist/features/points/lib/fu/constants.d.ts +37 -0
  27. package/dist/features/points/lib/fu/constants.js +45 -0
  28. package/dist/features/points/lib/fu/index.d.ts +11 -0
  29. package/dist/features/points/lib/fu/index.js +23 -0
  30. package/dist/features/points/lib/fu/lib/chiitoitsu.d.ts +5 -0
  31. package/dist/features/points/lib/fu/lib/chiitoitsu.js +17 -0
  32. package/dist/features/points/lib/fu/lib/kokushi.d.ts +6 -0
  33. package/dist/features/points/lib/fu/lib/kokushi.js +18 -0
  34. package/dist/features/points/lib/fu/lib/mentsu.d.ts +11 -0
  35. package/dist/features/points/lib/fu/lib/mentsu.js +122 -0
  36. package/dist/features/points/lib/fu/types.d.ts +55 -0
  37. package/dist/features/points/lib/fu/types.js +1 -0
  38. package/dist/features/points/types.d.ts +27 -0
  39. package/dist/features/points/types.js +1 -0
  40. package/dist/features/shanten/index.d.ts +16 -0
  41. package/dist/features/shanten/index.js +25 -0
  42. package/dist/features/shanten/logic/chiitoitsu.d.ts +8 -0
  43. package/dist/features/shanten/logic/chiitoitsu.js +36 -0
  44. package/dist/features/shanten/logic/kokushi.d.ts +16 -0
  45. package/dist/features/shanten/logic/kokushi.js +48 -0
  46. package/dist/features/shanten/logic/mentsu-te.d.ts +8 -0
  47. package/dist/features/shanten/logic/mentsu-te.js +129 -0
  48. package/dist/features/yaku/factory.d.ts +13 -0
  49. package/dist/features/yaku/factory.js +19 -0
  50. package/dist/features/yaku/index.d.ts +12 -0
  51. package/dist/features/yaku/index.js +62 -0
  52. package/dist/features/yaku/lib/definitions/chiitoitsu.d.ts +2 -0
  53. package/dist/features/yaku/lib/definitions/chiitoitsu.js +12 -0
  54. package/dist/features/yaku/lib/definitions/chinitsu.d.ts +2 -0
  55. package/dist/features/yaku/lib/definitions/chinitsu.js +40 -0
  56. package/dist/features/yaku/lib/definitions/chinroutou.d.ts +2 -0
  57. package/dist/features/yaku/lib/definitions/chinroutou.js +21 -0
  58. package/dist/features/yaku/lib/definitions/chuuren-poutou.d.ts +2 -0
  59. package/dist/features/yaku/lib/definitions/chuuren-poutou.js +69 -0
  60. package/dist/features/yaku/lib/definitions/daisangen.d.ts +2 -0
  61. package/dist/features/yaku/lib/definitions/daisangen.js +26 -0
  62. package/dist/features/yaku/lib/definitions/daisuushii.d.ts +2 -0
  63. package/dist/features/yaku/lib/definitions/daisuushii.js +32 -0
  64. package/dist/features/yaku/lib/definitions/honchan.d.ts +2 -0
  65. package/dist/features/yaku/lib/definitions/honchan.js +29 -0
  66. package/dist/features/yaku/lib/definitions/honitsu.d.ts +2 -0
  67. package/dist/features/yaku/lib/definitions/honitsu.js +40 -0
  68. package/dist/features/yaku/lib/definitions/honroutou.d.ts +2 -0
  69. package/dist/features/yaku/lib/definitions/honroutou.js +33 -0
  70. package/dist/features/yaku/lib/definitions/iipeiko.d.ts +2 -0
  71. package/dist/features/yaku/lib/definitions/iipeiko.js +46 -0
  72. package/dist/features/yaku/lib/definitions/ikkitsuukan.d.ts +2 -0
  73. package/dist/features/yaku/lib/definitions/ikkitsuukan.js +56 -0
  74. package/dist/features/yaku/lib/definitions/index.d.ts +30 -0
  75. package/dist/features/yaku/lib/definitions/index.js +90 -0
  76. package/dist/features/yaku/lib/definitions/junchan.d.ts +2 -0
  77. package/dist/features/yaku/lib/definitions/junchan.js +25 -0
  78. package/dist/features/yaku/lib/definitions/kokushi.d.ts +2 -0
  79. package/dist/features/yaku/lib/definitions/kokushi.js +12 -0
  80. package/dist/features/yaku/lib/definitions/menzen-tsumo.d.ts +2 -0
  81. package/dist/features/yaku/lib/definitions/menzen-tsumo.js +8 -0
  82. package/dist/features/yaku/lib/definitions/pinfu.d.ts +2 -0
  83. package/dist/features/yaku/lib/definitions/pinfu.js +40 -0
  84. package/dist/features/yaku/lib/definitions/ryanpeiko.d.ts +2 -0
  85. package/dist/features/yaku/lib/definitions/ryanpeiko.js +33 -0
  86. package/dist/features/yaku/lib/definitions/ryuuiisou.d.ts +2 -0
  87. package/dist/features/yaku/lib/definitions/ryuuiisou.js +43 -0
  88. package/dist/features/yaku/lib/definitions/sanankou.d.ts +2 -0
  89. package/dist/features/yaku/lib/definitions/sanankou.js +49 -0
  90. package/dist/features/yaku/lib/definitions/sankantsu.d.ts +2 -0
  91. package/dist/features/yaku/lib/definitions/sankantsu.js +18 -0
  92. package/dist/features/yaku/lib/definitions/sanshoku-doujun.d.ts +2 -0
  93. package/dist/features/yaku/lib/definitions/sanshoku-doujun.js +58 -0
  94. package/dist/features/yaku/lib/definitions/sanshoku-doukou.d.ts +2 -0
  95. package/dist/features/yaku/lib/definitions/sanshoku-doukou.js +53 -0
  96. package/dist/features/yaku/lib/definitions/shousangen.d.ts +2 -0
  97. package/dist/features/yaku/lib/definitions/shousangen.js +28 -0
  98. package/dist/features/yaku/lib/definitions/shousuushii.d.ts +2 -0
  99. package/dist/features/yaku/lib/definitions/shousuushii.js +34 -0
  100. package/dist/features/yaku/lib/definitions/suuankou.d.ts +2 -0
  101. package/dist/features/yaku/lib/definitions/suuankou.js +63 -0
  102. package/dist/features/yaku/lib/definitions/suukantsu.d.ts +2 -0
  103. package/dist/features/yaku/lib/definitions/suukantsu.js +18 -0
  104. package/dist/features/yaku/lib/definitions/tanyao.d.ts +2 -0
  105. package/dist/features/yaku/lib/definitions/tanyao.js +23 -0
  106. package/dist/features/yaku/lib/definitions/toitoi.d.ts +2 -0
  107. package/dist/features/yaku/lib/definitions/toitoi.js +16 -0
  108. package/dist/features/yaku/lib/definitions/tsuuiisou.d.ts +2 -0
  109. package/dist/features/yaku/lib/definitions/tsuuiisou.js +35 -0
  110. package/dist/features/yaku/lib/definitions/yakuhai.d.ts +4 -0
  111. package/dist/features/yaku/lib/definitions/yakuhai.js +21 -0
  112. package/dist/features/yaku/lib/index.d.ts +1 -0
  113. package/dist/features/yaku/lib/index.js +1 -0
  114. package/dist/features/yaku/lib/structures/chiitoitsu.d.ts +6 -0
  115. package/dist/features/yaku/lib/structures/chiitoitsu.js +38 -0
  116. package/dist/features/yaku/lib/structures/index.d.ts +10 -0
  117. package/dist/features/yaku/lib/structures/index.js +17 -0
  118. package/dist/features/yaku/lib/structures/kokushi.d.ts +6 -0
  119. package/dist/features/yaku/lib/structures/kokushi.js +43 -0
  120. package/dist/features/yaku/lib/structures/mentsu-te.d.ts +24 -0
  121. package/dist/features/yaku/lib/structures/mentsu-te.js +127 -0
  122. package/dist/features/yaku/types.d.ts +121 -0
  123. package/dist/features/yaku/types.js +1 -0
  124. package/dist/features/yaku/utils.d.ts +19 -0
  125. package/dist/features/yaku/utils.js +34 -0
  126. package/dist/index.d.ts +12 -0
  127. package/dist/index.js +9 -0
  128. package/dist/types.d.ts +280 -0
  129. package/dist/types.js +97 -0
  130. package/dist/utils/assertions.d.ts +22 -0
  131. package/dist/utils/assertions.js +33 -0
  132. package/dist/utils/test-helpers.d.ts +55 -0
  133. package/dist/utils/test-helpers.js +124 -0
  134. package/package.json +62 -0
@@ -0,0 +1,174 @@
1
+ import { HaiKind } from "../../types";
2
+ import { countDora } from "../../core/dora";
3
+ import { getHouraStructures } from "../yaku/lib/structures";
4
+ import { ALL_YAKU_DEFINITIONS } from "../yaku/lib/definitions";
5
+ import { calculateFu } from "./lib/fu";
6
+ import { isMenzen } from "../yaku/utils";
7
+ import { BASE_POINT_LIMIT, HAN_BAIMAN, HAN_HANEMAN, HAN_MANGAN, HAN_SANBAIMAN, HAN_YAKUMAN, SCORE_BASE_BAIMAN, SCORE_BASE_HANEMAN, SCORE_BASE_MANGAN, SCORE_BASE_SANBAIMAN, SCORE_BASE_YAKUMAN, } from "./constants";
8
+ /**
9
+ * 100点単位で切り上げる
10
+ */
11
+ function ceil100(points) {
12
+ return Math.ceil(points / 100) * 100;
13
+ }
14
+ /**
15
+ * 手牌とコンテキストから点数を計算する(公開API)
16
+ *
17
+ * 手牌の構造解析を行い、最も高点となる解釈を採用して点数を返します。
18
+ *
19
+ * @param tehai 手牌 (14枚)
20
+ * @param config 点数計算の設定 (場風、自風、ドラなど)
21
+ * @returns 点数計算結果
22
+ */
23
+ export function calculateScore(tehai, config) {
24
+ const menzen = isMenzen(tehai);
25
+ const isOya = config.jikaze === HaiKind.Ton;
26
+ const context = {
27
+ isMenzen: menzen,
28
+ agariHai: config.agariHai,
29
+ bakaze: config.bakaze,
30
+ jikaze: config.jikaze,
31
+ isTsumo: config.isTsumo,
32
+ isOya: isOya,
33
+ doraMarkers: config.doraMarkers,
34
+ ...(config.uraDoraMarkers ? { uraDoraMarkers: config.uraDoraMarkers } : {}),
35
+ };
36
+ const structuralInterpretations = getHouraStructures(tehai);
37
+ let bestResult = null;
38
+ let maxTotalPoints = -1;
39
+ for (const hand of structuralInterpretations) {
40
+ // 1. 役の判定
41
+ let yakuHansu = 0;
42
+ let isPinfu = false;
43
+ for (const definition of ALL_YAKU_DEFINITIONS) {
44
+ if (definition.isSatisfied(hand, context)) {
45
+ const h = definition.getHansu(hand, context);
46
+ if (h > 0) {
47
+ yakuHansu += h;
48
+ if (definition.yaku.name === "Pinfu") {
49
+ isPinfu = true;
50
+ }
51
+ }
52
+ }
53
+ }
54
+ // 役がない場合はこの構造は不成立
55
+ if (yakuHansu === 0)
56
+ continue;
57
+ // 2. 符の計算
58
+ const fuResult = calculateFu(hand, context, isPinfu);
59
+ // 3. ドラの計算
60
+ // context.doraMarkers にドラ表示牌が入っている前提
61
+ const dora = countDora(tehai, context.doraMarkers);
62
+ // 4. 点数計算 (基本計算)
63
+ const result = calculateBasicScore(yakuHansu, fuResult, dora, context);
64
+ if (result.points.total > maxTotalPoints) {
65
+ maxTotalPoints = result.points.total;
66
+ bestResult = result;
67
+ }
68
+ }
69
+ if (!bestResult) {
70
+ throw new Error("役が成立していません (No Yaku found)");
71
+ }
72
+ return bestResult;
73
+ }
74
+ /**
75
+ * 基本的な点数計算ロジック (内部用・テスト用)
76
+ */
77
+ export function calculateBasicScore(yakuHansu, fuResult, dora, context) {
78
+ const totalHan = yakuHansu + dora;
79
+ const fu = fuResult.total;
80
+ let basePoints = fu * Math.pow(2, 2 + totalHan);
81
+ let level = "Normal";
82
+ // 満貫以上の判定
83
+ // 5飜以上 は満貫確定
84
+ // 4飜以下でも 基本点が2000を超えたら満貫 (切り上げ満貫採用なら1920->2000)
85
+ // ここでは厳密な計算 (2000以上) とする。※30符4飜は1920なのでNormal、60符3飜は1920、70符3飜(2240)は満貫
86
+ if (totalHan >= HAN_YAKUMAN) {
87
+ // 役満(数え役満)
88
+ // ダブル役満等は呼び出し元でHandを判定して Han=26 とかに固定して渡してもらう想定
89
+ // またはHan=13以上はすべてYakumanとして扱う(シングル)
90
+ // ここでは13以上をYakuman、26以上をDoubleとする簡易判定を入れる
91
+ if (totalHan >= 26) {
92
+ level = "DoubleYakuman"; // 例
93
+ basePoints = SCORE_BASE_YAKUMAN * 2;
94
+ }
95
+ else {
96
+ level = "Yakuman";
97
+ basePoints = SCORE_BASE_YAKUMAN;
98
+ }
99
+ }
100
+ else if (totalHan >= HAN_SANBAIMAN) {
101
+ level = "Sanbaiman";
102
+ basePoints = SCORE_BASE_SANBAIMAN;
103
+ }
104
+ else if (totalHan >= HAN_BAIMAN) {
105
+ level = "Baiman";
106
+ basePoints = SCORE_BASE_BAIMAN;
107
+ }
108
+ else if (totalHan >= HAN_HANEMAN) {
109
+ level = "Haneman";
110
+ basePoints = SCORE_BASE_HANEMAN;
111
+ }
112
+ else if (totalHan >= HAN_MANGAN || basePoints >= BASE_POINT_LIMIT) {
113
+ level = "Mangan";
114
+ basePoints = SCORE_BASE_MANGAN;
115
+ }
116
+ // 支払い計算
117
+ let mainPayment = 0;
118
+ let subPayment = 0;
119
+ let totalScore = 0;
120
+ if (context.isTsumo) {
121
+ // ツモ和了
122
+ if (context.isOya) {
123
+ // 親ツモ: オール (基本点 * 2)
124
+ const pay = ceil100(basePoints * 2);
125
+ mainPayment = pay;
126
+ // ScorePayment型の定義について:
127
+ // main: ロンなら支払い総額、ツモなら親の支払い
128
+ // sub: ツモの時の子の支払い
129
+ // 親ツモの場合、「親の支払い」は発生せず、全員が「子」として支払います。
130
+ // ここでは `main` を「子一人あたりの支払い」として使用します。
131
+ // 親ツモ: 子の支払い(main) * 3
132
+ // 子ツモ: 親の支払い(main) + 子の支払い(sub) * 2
133
+ const childPay = ceil100(basePoints * 2);
134
+ mainPayment = childPay; // 各子の支払い
135
+ subPayment = 0;
136
+ totalScore = childPay * 3;
137
+ }
138
+ else {
139
+ // 子ツモ
140
+ // 親の支払い: 基本点 * 2
141
+ // 子の支払い: 基本点 * 1
142
+ const parentPay = ceil100(basePoints * 2);
143
+ const childPay = ceil100(basePoints * 1);
144
+ mainPayment = parentPay;
145
+ subPayment = childPay;
146
+ totalScore = parentPay + childPay * 2;
147
+ }
148
+ }
149
+ else {
150
+ // ロン和了
151
+ if (context.isOya) {
152
+ // 親ロン: 基本点 * 6
153
+ const pay = ceil100(basePoints * 6);
154
+ mainPayment = pay;
155
+ totalScore = pay;
156
+ }
157
+ else {
158
+ // 子ロン: 基本点 * 4
159
+ const pay = ceil100(basePoints * 4);
160
+ mainPayment = pay;
161
+ totalScore = pay;
162
+ }
163
+ }
164
+ return {
165
+ han: totalHan,
166
+ fu: fu,
167
+ level,
168
+ points: {
169
+ main: mainPayment,
170
+ sub: subPayment,
171
+ total: totalScore,
172
+ },
173
+ };
174
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * 符計算に関する定数定義
3
+ */
4
+ export declare const FU_BASE: {
5
+ readonly NORMAL: 20;
6
+ readonly CHIITOITSU: 25;
7
+ readonly KOKUSHI: 20;
8
+ };
9
+ export declare const FU_AGARI: {
10
+ readonly TSUMO: 2;
11
+ readonly MENZEN_RON: 10;
12
+ };
13
+ export declare const FU_JANTOU: {
14
+ readonly YAKUHAI: 2;
15
+ readonly DOUBLE_WIND_CAP: 2;
16
+ };
17
+ export declare const FU_MACHI: {
18
+ readonly KANCHAN: 2;
19
+ readonly PENCHAN: 2;
20
+ readonly TANKI: 2;
21
+ readonly RYANMEN: 0;
22
+ readonly SHANPON: 0;
23
+ };
24
+ export declare const FU_KOUTSU: {
25
+ readonly SUUPAI_OPEN: 2;
26
+ readonly SUUPAI_CLOSED: 4;
27
+ readonly YAOCHU_OPEN: 4;
28
+ readonly YAOCHU_CLOSED: 8;
29
+ };
30
+ export declare const FU_KANTSU: {
31
+ readonly SUUPAI_OPEN: 8;
32
+ readonly SUUPAI_CLOSED: 16;
33
+ readonly YAOCHU_OPEN: 16;
34
+ readonly YAOCHU_CLOSED: 32;
35
+ };
36
+ export declare const FU_PINFU_TSUMO = 20;
37
+ export declare const FU_OPEN_PINFU_GLAZE = 30;
@@ -0,0 +1,45 @@
1
+ /**
2
+ * 符計算に関する定数定義
3
+ */
4
+ // 符底 (FuTei)
5
+ export const FU_BASE = {
6
+ NORMAL: 20,
7
+ CHIITOITSU: 25,
8
+ KOKUSHI: 20, // 便宜上
9
+ };
10
+ // 和了符 (AgariFu)
11
+ export const FU_AGARI = {
12
+ TSUMO: 2,
13
+ MENZEN_RON: 10,
14
+ };
15
+ // 雀頭符 (JantouFu)
16
+ export const FU_JANTOU = {
17
+ YAKUHAI: 2,
18
+ // 連風牌の扱い(設定により4符または2符)
19
+ DOUBLE_WIND_CAP: 2,
20
+ };
21
+ // 待ち符 (MachiFu)
22
+ export const FU_MACHI = {
23
+ KANCHAN: 2,
24
+ PENCHAN: 2,
25
+ TANKI: 2,
26
+ RYANMEN: 0,
27
+ SHANPON: 0,
28
+ };
29
+ // 面子符 (MentsuFu)
30
+ // [Yaochu/Suupai]_[Open/Closed]
31
+ export const FU_KOUTSU = {
32
+ SUUPAI_OPEN: 2,
33
+ SUUPAI_CLOSED: 4,
34
+ YAOCHU_OPEN: 4,
35
+ YAOCHU_CLOSED: 8,
36
+ };
37
+ export const FU_KANTSU = {
38
+ SUUPAI_OPEN: 8,
39
+ SUUPAI_CLOSED: 16,
40
+ YAOCHU_OPEN: 16,
41
+ YAOCHU_CLOSED: 32,
42
+ };
43
+ // 例外処理用定数
44
+ export const FU_PINFU_TSUMO = 20;
45
+ export const FU_OPEN_PINFU_GLAZE = 30; // 喰いタン平和形などの20符切り上げ
@@ -0,0 +1,11 @@
1
+ import type { HouraStructure, HouraContext } from "../../../yaku/types";
2
+ import type { FuResult } from "./types";
3
+ /**
4
+ * 手牌の符を計算する (calculateFu)
5
+ *
6
+ * @param hand 和了形の手牌構造
7
+ * @param context 和了コンテキスト (場風、自風、ツモ/ロン等)
8
+ * @param isPinfu 平和が成立しているかどうか (平和ツモ20符例外の適用に必要)
9
+ * @returns 符計算結果
10
+ */
11
+ export declare function calculateFu(hand: HouraStructure, context: HouraContext, isPinfu?: boolean): FuResult;
@@ -0,0 +1,23 @@
1
+ import { calculateChiitoitsuFu } from "./lib/chiitoitsu";
2
+ import { calculateKokushiFu } from "./lib/kokushi";
3
+ import { calculateMentsuFu } from "./lib/mentsu";
4
+ /**
5
+ * 手牌の符を計算する (calculateFu)
6
+ *
7
+ * @param hand 和了形の手牌構造
8
+ * @param context 和了コンテキスト (場風、自風、ツモ/ロン等)
9
+ * @param isPinfu 平和が成立しているかどうか (平和ツモ20符例外の適用に必要)
10
+ * @returns 符計算結果
11
+ */
12
+ export function calculateFu(hand, context, isPinfu = false) {
13
+ // 1. 七対子 (ChiiToitsu)
14
+ if (hand.type === "Chiitoitsu") {
15
+ return calculateChiitoitsuFu();
16
+ }
17
+ // 2. 国士無双 (Kokushi)
18
+ if (hand.type === "Kokushi") {
19
+ return calculateKokushiFu();
20
+ }
21
+ // 3. 面子手 (Mentsu)
22
+ return calculateMentsuFu(hand, context, isPinfu);
23
+ }
@@ -0,0 +1,5 @@
1
+ import type { FuResult } from "../types";
2
+ /**
3
+ * 七対子の符を計算する
4
+ */
5
+ export declare function calculateChiitoitsuFu(): FuResult;
@@ -0,0 +1,17 @@
1
+ import { FU_BASE } from "../constants";
2
+ /**
3
+ * 七対子の符を計算する
4
+ */
5
+ export function calculateChiitoitsuFu() {
6
+ const details = {
7
+ base: FU_BASE.CHIITOITSU,
8
+ mentsu: 0,
9
+ jantou: 0,
10
+ machi: 0,
11
+ agari: 0,
12
+ };
13
+ return {
14
+ total: FU_BASE.CHIITOITSU,
15
+ details,
16
+ };
17
+ }
@@ -0,0 +1,6 @@
1
+ import type { FuResult } from "../types";
2
+ /**
3
+ * 国士無双の符を計算する
4
+ * (点数計算上は役満固定だが、便宜上符底のみを返す)
5
+ */
6
+ export declare function calculateKokushiFu(): FuResult;
@@ -0,0 +1,18 @@
1
+ import { FU_BASE } from "../constants";
2
+ /**
3
+ * 国士無双の符を計算する
4
+ * (点数計算上は役満固定だが、便宜上符底のみを返す)
5
+ */
6
+ export function calculateKokushiFu() {
7
+ const details = {
8
+ base: FU_BASE.KOKUSHI,
9
+ mentsu: 0,
10
+ jantou: 0,
11
+ machi: 0,
12
+ agari: 0,
13
+ };
14
+ return {
15
+ total: FU_BASE.KOKUSHI,
16
+ details,
17
+ };
18
+ }
@@ -0,0 +1,11 @@
1
+ import type { FuResult } from "../types";
2
+ import type { HouraContext } from "../../../../yaku/types";
3
+ import type { MentsuHouraStructure } from "../../../../yaku/types";
4
+ /**
5
+ * 面子手の符を計算する
6
+ *
7
+ * @param hand 面子手の構造
8
+ * @param context 和了コンテキスト
9
+ * @param isPinfu 平和成立フラグ
10
+ */
11
+ export declare function calculateMentsuFu(hand: MentsuHouraStructure, context: HouraContext, isPinfu: boolean): FuResult;
@@ -0,0 +1,122 @@
1
+ import { isYaochu } from "../../../../../core/hai";
2
+ import { HaiKind } from "../../../../../types";
3
+ import { classifyMachi } from "../../../../../core/machi";
4
+ import { FU_BASE, FU_KOUTSU, FU_KANTSU, FU_JANTOU, FU_MACHI, FU_AGARI, FU_PINFU_TSUMO, FU_OPEN_PINFU_GLAZE, } from "../constants";
5
+ /**
6
+ * 面子手の符を計算する
7
+ *
8
+ * @param hand 面子手の構造
9
+ * @param context 和了コンテキスト
10
+ * @param isPinfu 平和成立フラグ
11
+ */
12
+ export function calculateMentsuFu(hand, context, isPinfu) {
13
+ const details = {
14
+ base: FU_BASE.NORMAL,
15
+ mentsu: 0,
16
+ jantou: 0,
17
+ machi: 0,
18
+ agari: 0,
19
+ };
20
+ // 1. 面子符 (MentsuFu)
21
+ for (const mentsu of hand.fourMentsu) {
22
+ let fu = 0;
23
+ const isYaochuMentsu = isYaochu(mentsu.hais[0]);
24
+ if (mentsu.type === "Koutsu") {
25
+ // 刻子
26
+ const openFu = isYaochuMentsu
27
+ ? FU_KOUTSU.YAOCHU_OPEN
28
+ : FU_KOUTSU.SUUPAI_OPEN;
29
+ const closedFu = isYaochuMentsu
30
+ ? FU_KOUTSU.YAOCHU_CLOSED
31
+ : FU_KOUTSU.SUUPAI_CLOSED;
32
+ fu = closedFu;
33
+ // 明刻判定
34
+ let isOpen = !!mentsu.furo;
35
+ if (!isOpen && !context.isTsumo) {
36
+ // ロン和了で、かつその牌を含む刻子であれば明刻扱い
37
+ if (mentsu.hais.includes(context.agariHai)) {
38
+ isOpen = true;
39
+ }
40
+ }
41
+ if (isOpen) {
42
+ fu = openFu;
43
+ }
44
+ details.mentsu += fu;
45
+ }
46
+ else if (mentsu.type === "Kantsu") {
47
+ // 槓子
48
+ const openFu = isYaochuMentsu
49
+ ? FU_KANTSU.YAOCHU_OPEN
50
+ : FU_KANTSU.SUUPAI_OPEN;
51
+ const closedFu = isYaochuMentsu
52
+ ? FU_KANTSU.YAOCHU_CLOSED
53
+ : FU_KANTSU.SUUPAI_CLOSED;
54
+ fu = closedFu;
55
+ if (mentsu.furo) {
56
+ fu = openFu;
57
+ }
58
+ details.mentsu += fu;
59
+ }
60
+ }
61
+ // 2. 雀頭符 (JantouFu)
62
+ const headHai = hand.jantou.hais[0];
63
+ let jantouFu = 0;
64
+ if (headHai === HaiKind.Haku ||
65
+ headHai === HaiKind.Hatsu ||
66
+ headHai === HaiKind.Chun) {
67
+ jantouFu += FU_JANTOU.YAKUHAI;
68
+ }
69
+ if (headHai === context.bakaze) {
70
+ jantouFu += FU_JANTOU.YAKUHAI;
71
+ }
72
+ if (headHai === context.jikaze) {
73
+ jantouFu += FU_JANTOU.YAKUHAI;
74
+ }
75
+ // 連風牌の加算上限
76
+ if (jantouFu > FU_JANTOU.DOUBLE_WIND_CAP) {
77
+ jantouFu = FU_JANTOU.DOUBLE_WIND_CAP;
78
+ }
79
+ details.jantou = jantouFu;
80
+ // 3. 待ち符 (MachiFu)
81
+ const machiType = classifyMachi(hand, context.agariHai);
82
+ if (machiType === "Kanchan")
83
+ details.machi = FU_MACHI.KANCHAN;
84
+ else if (machiType === "Penchan")
85
+ details.machi = FU_MACHI.PENCHAN;
86
+ else if (machiType === "Tanki")
87
+ details.machi = FU_MACHI.TANKI;
88
+ else
89
+ details.machi = 0; // Ryanmen, Shanpon
90
+ // 4. 和了符 (AgariFu)
91
+ if (context.isTsumo) {
92
+ if (!isPinfu) {
93
+ details.agari = FU_AGARI.TSUMO;
94
+ }
95
+ }
96
+ else {
97
+ if (context.isMenzen) {
98
+ details.agari = FU_AGARI.MENZEN_RON;
99
+ }
100
+ }
101
+ // 合計
102
+ let sum = details.base +
103
+ details.mentsu +
104
+ details.jantou +
105
+ details.machi +
106
+ details.agari;
107
+ // 平和ツモ例外
108
+ if (isPinfu && context.isTsumo) {
109
+ return { total: FU_PINFU_TSUMO, details };
110
+ }
111
+ // 切り上げ (喰いタン平和形など)
112
+ if (sum === 20 && !context.isTsumo && !context.isMenzen) {
113
+ sum = FU_OPEN_PINFU_GLAZE;
114
+ }
115
+ else {
116
+ sum = Math.ceil(sum / 10) * 10;
117
+ }
118
+ return {
119
+ total: sum,
120
+ details,
121
+ };
122
+ }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * 符の構成要素 (FuDetails)
3
+ *
4
+ * 符計算の内訳を保持するインターフェース。
5
+ */
6
+ export interface FuDetails {
7
+ /**
8
+ * 符底 (FuTei)
9
+ *
10
+ * 一般的な和了形: 20符
11
+ * 七対子: 25符
12
+ */
13
+ readonly base: 20 | 25;
14
+ /**
15
+ * 面子符の合計 (MentsuFu)
16
+ *
17
+ * 刻子・槓子による加算符。順子は0符。
18
+ */
19
+ readonly mentsu: number;
20
+ /**
21
+ * 雀頭符 (JantouFu)
22
+ *
23
+ * 役牌の対子による加算符。
24
+ */
25
+ readonly jantou: number;
26
+ /**
27
+ * 待ち符 (MachiFu)
28
+ *
29
+ * 単騎・嵌張・辺張待ちによる加算符(2符)。
30
+ */
31
+ readonly machi: number;
32
+ /**
33
+ * 和了符 (AgariFu)
34
+ *
35
+ * ツモ和了: 2符
36
+ * 門前ロン: 10符
37
+ */
38
+ readonly agari: number;
39
+ }
40
+ /**
41
+ * 符計算結果 (FuResult)
42
+ */
43
+ export interface FuResult {
44
+ /**
45
+ * 最終的な符数 (Total)
46
+ *
47
+ * 内訳の合計を10符単位で切り上げたもの。
48
+ * (例: 22符 -> 30符)
49
+ */
50
+ readonly total: number;
51
+ /**
52
+ * 計算の内訳
53
+ */
54
+ readonly details: FuDetails;
55
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,27 @@
1
+ import type { HaiKindId, Kazehai } from "../../types";
2
+ export interface ScoreCalculationConfig {
3
+ /** 和了牌 */
4
+ agariHai: HaiKindId;
5
+ /** ツモ和了かどうか (必須) */
6
+ isTsumo: boolean;
7
+ /** 自風 (必須) */
8
+ jikaze: Kazehai;
9
+ /** 場風 (必須) */
10
+ bakaze: Kazehai;
11
+ /** ドラ表示牌 (必須、なければ空配列) */
12
+ doraMarkers: readonly HaiKindId[];
13
+ /** 裏ドラ表示牌 (任意) */
14
+ uraDoraMarkers?: readonly HaiKindId[];
15
+ }
16
+ export type ScoreLevel = "Normal" | "Mangan" | "Haneman" | "Baiman" | "Sanbaiman" | "Yakuman" | "DoubleYakuman" | "TripleYakuman";
17
+ export interface ScorePayment {
18
+ main: number;
19
+ sub?: number;
20
+ total: number;
21
+ }
22
+ export interface ScoreResult {
23
+ han: number;
24
+ fu: number;
25
+ level: ScoreLevel;
26
+ points: ScorePayment;
27
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,16 @@
1
+ import type { Tehai13 } from "../../types";
2
+ import { calculateChiitoitsuShanten } from "./logic/chiitoitsu";
3
+ import { calculateKokushiShanten } from "./logic/kokushi";
4
+ import { calculateMentsuTeShanten } from "./logic/mentsu-te";
5
+ export { calculateChiitoitsuShanten, calculateKokushiShanten, calculateMentsuTeShanten, };
6
+ /**
7
+ * シャンテン数を計算します。
8
+ * 面子手、七対子、国士無双のシャンテン数のうち最小値を返します。
9
+ *
10
+ * NOTE: 入力は必ず牌種ID (`HaiKindId`) である必要があります。
11
+ * 物理牌ID (`HaiId`) を持っている場合は、事前に `haiIdToKindId` で変換してください。
12
+ *
13
+ * @param tehai 手牌
14
+ * @returns シャンテン数
15
+ */
16
+ export declare function calculateShanten(tehai: Tehai13, useChiitoitsu?: boolean, useKokushi?: boolean): number;
@@ -0,0 +1,25 @@
1
+ import { validateTehai13 } from "../../core/tehai";
2
+ import { calculateChiitoitsuShanten } from "./logic/chiitoitsu";
3
+ import { calculateKokushiShanten } from "./logic/kokushi";
4
+ import { calculateMentsuTeShanten } from "./logic/mentsu-te";
5
+ export { calculateChiitoitsuShanten, calculateKokushiShanten, calculateMentsuTeShanten, };
6
+ /**
7
+ * シャンテン数を計算します。
8
+ * 面子手、七対子、国士無双のシャンテン数のうち最小値を返します。
9
+ *
10
+ * NOTE: 入力は必ず牌種ID (`HaiKindId`) である必要があります。
11
+ * 物理牌ID (`HaiId`) を持っている場合は、事前に `haiIdToKindId` で変換してください。
12
+ *
13
+ * @param tehai 手牌
14
+ * @returns シャンテン数
15
+ */
16
+ export function calculateShanten(tehai, useChiitoitsu = true, useKokushi = true) {
17
+ // Facadeパターン: 公開APIのエントリーポイントで入力を保証する
18
+ validateTehai13(tehai);
19
+ const chiitoitsuShanten = useChiitoitsu
20
+ ? calculateChiitoitsuShanten(tehai)
21
+ : Infinity;
22
+ const kokushiShanten = useKokushi ? calculateKokushiShanten(tehai) : Infinity;
23
+ const mentsuShanten = calculateMentsuTeShanten(tehai);
24
+ return Math.min(chiitoitsuShanten, kokushiShanten, mentsuShanten);
25
+ }
@@ -0,0 +1,8 @@
1
+ import type { Tehai13 } from "../../../types";
2
+ /**
3
+ * 七対子のシャンテン数を計算する
4
+ *
5
+ * @param tehai 手牌 (13枚)
6
+ * @returns シャンテン数 (0: 聴牌, -1: 和了 - 理論上)
7
+ */
8
+ export declare function calculateChiitoitsuShanten(tehai: Tehai13): number;
@@ -0,0 +1,36 @@
1
+ import { countHaiKind, validateTehai13 } from "../../../core/tehai";
2
+ /**
3
+ * 七対子のシャンテン数を計算する
4
+ *
5
+ * @param tehai 手牌 (13枚)
6
+ * @returns シャンテン数 (0: 聴牌, -1: 和了 - 理論上)
7
+ */
8
+ export function calculateChiitoitsuShanten(tehai) {
9
+ // 防御的プログラミング (Defensive Programming):
10
+ // 公開API(calculateShanten)側でもバリデーションが行われる想定だが(Facadeパターン)、
11
+ // 内部整合性を保つため、ここでも独立してバリデーションを実施する。
12
+ validateTehai13(tehai);
13
+ // シャンテン数を計算する前にバリデーションを実行する
14
+ // 七対子は門前のみ
15
+ if (tehai.exposed.length > 0) {
16
+ return Infinity;
17
+ }
18
+ // HaiId/HaiKindId の正規化は廃止。呼び出し元で HaiKindId を保証する。
19
+ const haiCounts = countHaiKind(tehai.closed);
20
+ let pairs = 0;
21
+ let kinds = 0;
22
+ for (const count of haiCounts) {
23
+ if (count > 0) {
24
+ kinds++;
25
+ }
26
+ if (count >= 2) {
27
+ pairs++;
28
+ }
29
+ }
30
+ let shanten = 6 - pairs;
31
+ // 種類不足ペナルティ
32
+ if (kinds < 7) {
33
+ shanten += 7 - kinds;
34
+ }
35
+ return shanten;
36
+ }
@@ -0,0 +1,16 @@
1
+ import type { Tehai13 } from "../../../types";
2
+ /**
3
+ * 国士無双のシャンテン数を計算します。
4
+ *
5
+ * ルール:
6
+ * - 13種類の么九牌(1,9,字牌)を各1枚ずつ揃える。
7
+ * - そのうちのどれか1種類が対子(2枚)になっている必要がある。
8
+ * - 門前限定。
9
+ *
10
+ * 計算式:
11
+ * シャンテン数 = 13 - (么九牌の種類数) - (么九牌の対子があるか ? 1 : 0)
12
+ *
13
+ * @param tehai 手牌
14
+ * @returns シャンテン数 (0: 聴牌, -1: 和了(理論上))。副露している場合は Infinity。
15
+ */
16
+ export declare function calculateKokushiShanten(tehai: Tehai13): number;