@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,37 @@
1
+ import { calculateMentsuTeShanten } from "../shanten";
2
+ /**
3
+ * 手牌の受け入れ(有効牌)を計算する。
4
+ * 今回は面子手のみを対象とし、七対子や国士無双は考慮しない。
5
+ *
6
+ * @param tehai 手牌 (13枚)
7
+ * @returns シャンテン数を進める牌のリスト
8
+ */
9
+ export function getUkeire(tehai) {
10
+ const currentShanten = calculateMentsuTeShanten(tehai);
11
+ const ukeireList = [];
12
+ // 全34種の牌について、1枚加えてシャンテン数が下がるか試す
13
+ for (let i = 0; i < 34; i++) {
14
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
15
+ const tile = i;
16
+ // 手牌のコピーを作成(副作用を防ぐため、常に新しいオブジェクトで試行)
17
+ // Tehai13に1枚足すので、厳密にはTehai14として扱う必要があるが、
18
+ // calculateMentsuTeShanten は Tehai<HaiKindId> を受け付けるので、
19
+ // 構造的に { closed, exposed } が適合していればOK。
20
+ // ただし、Tehai13型に準拠したオブジェクトに1枚足すと枚数オーバーになるため、
21
+ // バリデーションを通過させるために Tehai14 として構築するか、
22
+ // calculateMentsuTeShanten 側が Generics で受け入れる点を利用する。
23
+ // 配列のコピーを作成
24
+ const newClosed = [...tehai.closed, tile];
25
+ const newTehai = {
26
+ closed: newClosed,
27
+ exposed: tehai.exposed,
28
+ };
29
+ // シャンテン数を計算
30
+ // ここでバリデーションエラーが出ないように、calculateMentsuTeShanten 側は validateTehai を使用している。
31
+ const newShanten = calculateMentsuTeShanten(newTehai);
32
+ if (newShanten < currentShanten) {
33
+ ukeireList.push(tile);
34
+ }
35
+ }
36
+ return ukeireList;
37
+ }
@@ -0,0 +1 @@
1
+ export type { MachiType } from "../../core/machi";
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,19 @@
1
+ import type { Tehai } from "../../types";
2
+ export type { MspzString, ExtendedMspzString } from "./mspz";
3
+ export { isExtendedMspz } from "./mspz";
4
+ /**
5
+ * 標準的なMSPZ文字列(例: "123m456p...")を解析して手牌オブジェクトを生成します。
6
+ * 副露牌は含まれません。
7
+ *
8
+ * @param input MSPZ形式の文字列
9
+ * @returns 手牌オブジェクト
10
+ */
11
+ export declare function parseMspz(input: string): Tehai;
12
+ /**
13
+ * 拡張MSPZ文字列(例: "123m[123p]...")を解析して手牌オブジェクトを生成します。
14
+ * 副露牌(`[...]`)や暗槓(`(...)`)を含めることができます。
15
+ *
16
+ * @param input 拡張MSPZ形式の文字列
17
+ * @returns 手牌オブジェクト
18
+ */
19
+ export declare function parseExtendedMspz(input: string): Tehai;
@@ -0,0 +1,28 @@
1
+ import { parseExtendedMspz as internalParseExtendedMspz, parseMspzToHaiKindIds, asMspz, asExtendedMspz, } from "./mspz";
2
+ export { isExtendedMspz } from "./mspz";
3
+ /**
4
+ * 標準的なMSPZ文字列(例: "123m456p...")を解析して手牌オブジェクトを生成します。
5
+ * 副露牌は含まれません。
6
+ *
7
+ * @param input MSPZ形式の文字列
8
+ * @returns 手牌オブジェクト
9
+ */
10
+ export function parseMspz(input) {
11
+ const ids = parseMspzToHaiKindIds(asMspz(input));
12
+ return {
13
+ closed: ids,
14
+ exposed: [],
15
+ };
16
+ }
17
+ /**
18
+ * 拡張MSPZ文字列(例: "123m[123p]...")を解析して手牌オブジェクトを生成します。
19
+ * 副露牌(`[...]`)や暗槓(`(...)`)を含めることができます。
20
+ *
21
+ * @param input 拡張MSPZ形式の文字列
22
+ * @returns 手牌オブジェクト
23
+ */
24
+ export function parseExtendedMspz(input) {
25
+ // parseExtendedMspz returns { closed: HaiKindId[], exposed: CompletedMentsu[] }
26
+ // which is compatible with Tehai interface.
27
+ return internalParseExtendedMspz(asExtendedMspz(input));
28
+ }
@@ -0,0 +1,85 @@
1
+ import { CompletedMentsu, HaiId, HaiKindDistribution, HaiKindId } from "../../types";
2
+ /**
3
+ * 標準的なMSPZ形式の文字列(拡張記法を含まない)
4
+ */
5
+ export type MspzString = string & {
6
+ readonly __brand: "MspzString";
7
+ };
8
+ /**
9
+ * 拡張MSPZ形式の文字列
10
+ * 通常のMSPZに加え、`[...]` (副露) や `(...)` (暗槓) を含むことができます。
11
+ */
12
+ export type ExtendedMspzString = string & {
13
+ readonly __brand: "ExtendedMspzString";
14
+ };
15
+ /**
16
+ * 文字列が拡張MSPZ形式(`[` または `(` を含み、かつ正しい書式)かどうかを判定します。
17
+ * @param input 判定対象の文字列
18
+ * @returns 拡張MSPZ形式であれば true
19
+ */
20
+ export declare function isExtendedMspz(input: string): input is ExtendedMspzString;
21
+ /**
22
+ * 文字列を ExtendedMspzString 型として扱います。
23
+ * 拡張MSPZ形式であることを検証します。
24
+ *
25
+ * @param input 変換対象の文字列
26
+ * @throws {Error} 拡張MSPZ形式でない場合
27
+ * @returns ExtendedMspzString
28
+ */
29
+ export declare function asExtendedMspz(input: string): ExtendedMspzString;
30
+ /**
31
+ * 文字列が標準的なMSPZ形式(拡張記法を含まず、かつ正しい書式)かどうかを判定します。
32
+ * @param input 判定対象の文字列
33
+ * @returns 標準MSPZ形式であれば true
34
+ */
35
+ export declare function isMspz(input: string): input is MspzString;
36
+ /**
37
+ * 拡張MSPZ解析結果
38
+ */
39
+ export interface ExtendedMspzParseResult {
40
+ readonly closed: readonly HaiKindId[];
41
+ readonly exposed: readonly CompletedMentsu[];
42
+ }
43
+ /**
44
+ * 拡張MSPZ形式の文字列を解析して、純手牌と副露のリストに変換します。
45
+ *
46
+ * @param input 拡張MSPZ形式の文字列
47
+ * @returns 解析結果オブジェクト
48
+ */
49
+ export declare function parseExtendedMspz(input: string): ExtendedMspzParseResult;
50
+ /**
51
+ * 13枚の牌種ID配列を 34種の牌種分布(所持数分布)に変換します。
52
+ * @throws {ShoushaiError} 牌の数が13枚より少ない場合
53
+ * @throws {TahaiError} 牌の数が13枚より多い場合
54
+ */
55
+ export declare function createDistribution(hais: readonly HaiKindId[]): HaiKindDistribution;
56
+ /**
57
+ * 13枚の牌ID配列を 34種の牌種分布(所持数分布)に変換します。
58
+ * @throws {ShoushaiError} 牌の数が13枚より少ない場合
59
+ * @throws {TahaiError} 牌の数が13枚より多い場合
60
+ */
61
+ export declare function haiIdsToDistribution(hais: readonly HaiId[]): HaiKindDistribution;
62
+ /**
63
+ * 13枚の牌種ID配列を MSPZ形式の文字列(例: "123m456p...")に変換します。
64
+ * すべての牌をソートして表記します。
65
+ * @throws {ShoushaiError} 牌の数が13枚より少ない場合
66
+ * @throws {TahaiError} 牌の数が13枚より多い場合
67
+ */
68
+ export declare function haiKindIdsToMspzString(hais: readonly HaiKindId[]): string;
69
+ /**
70
+ * 文字列を MspzString 型として扱います。
71
+ * 標準的なMSPZ形式(拡張記法を含まない)であることを検証します。
72
+ *
73
+ * @param input 変換対象の文字列
74
+ * @throws {Error} 拡張記法が含まれている場合
75
+ * @returns MspzString
76
+ */
77
+ export declare function asMspz(input: string): MspzString;
78
+ /**
79
+ * MSPZ形式の文字列(例: "123m456p")を解析して HaiKindId の配列に変換します。
80
+ * 主にテストデータの作成用途で使用します。
81
+ *
82
+ * @param mspz MSPZ形式の文字列
83
+ * @returns HaiKindId の配列
84
+ */
85
+ export declare function parseMspzToHaiKindIds(mspz: MspzString): HaiKindId[];
@@ -0,0 +1,365 @@
1
+ import { asHaiKindId, isTuple3, isTuple4 } from "../../utils/assertions";
2
+ import { ShoushaiError, TahaiError } from "../../errors";
3
+ import { FuroType, HaiKind, MentsuType, Tacha, } from "../../types";
4
+ import { haiIdToKindId, haiKindToNumber } from "../../core/hai";
5
+ // 1つ以上の数字 + 1つのサフィックス (m, p, s, z)
6
+ const BLOCK_PATTERN = "\\d+[mpsz]";
7
+ // 標準的なMSPZ: ブロックの繰り返し (空文字列も許容)
8
+ const STANDARD_MSPZ_REGEX = new RegExp(`^(${BLOCK_PATTERN})*$`);
9
+ // 拡張パート: [...] または (...) で囲まれたブロック (囲みの中も同様のブロック構造)
10
+ // 注: 現在のパーサー実装では、[] の中は単純な BLOCK_PATTERN の連続を許容しているか?
11
+ // parseMentsuString: mspzStringToHaiKindIds を呼んでいる。
12
+ // mspzStringToHaiKindIds: \d+[mpsz] をパースする。
13
+ // したがって、[] の中身も BLOCK_PATTERN の繰り返しであるべき。
14
+ const EXTENDED_BLOCK_PATTERN = `(${BLOCK_PATTERN}|\\[(${BLOCK_PATTERN})+\\]|\\((${BLOCK_PATTERN})+\\))`;
15
+ const EXTENDED_MSPZ_REGEX = new RegExp(`^${EXTENDED_BLOCK_PATTERN}*$`);
16
+ /**
17
+ * 文字列が拡張MSPZ形式(`[` または `(` を含み、かつ正しい書式)かどうかを判定します。
18
+ * @param input 判定対象の文字列
19
+ * @returns 拡張MSPZ形式であれば true
20
+ */
21
+ export function isExtendedMspz(input) {
22
+ // ブラケットを含む、かつ拡張書式にマッチする場合のみ true
23
+ // (ブラケットを含まない適正なMSPZは、ここでの定義上 ExtendedMspzString とはみなさない = MspzString と区別する)
24
+ return ((input.includes("[") || input.includes("(")) &&
25
+ EXTENDED_MSPZ_REGEX.test(input));
26
+ }
27
+ /**
28
+ * 文字列を ExtendedMspzString 型として扱います。
29
+ * 拡張MSPZ形式であることを検証します。
30
+ *
31
+ * @param input 変換対象の文字列
32
+ * @throws {Error} 拡張MSPZ形式でない場合
33
+ * @returns ExtendedMspzString
34
+ */
35
+ export function asExtendedMspz(input) {
36
+ if (!isExtendedMspz(input)) {
37
+ throw new Error(`Invalid Extended MSPZ string: ${input}`);
38
+ }
39
+ return input;
40
+ }
41
+ /**
42
+ * 文字列が標準的なMSPZ形式(拡張記法を含まず、かつ正しい書式)かどうかを判定します。
43
+ * @param input 判定対象の文字列
44
+ * @returns 標準MSPZ形式であれば true
45
+ */
46
+ export function isMspz(input) {
47
+ return STANDARD_MSPZ_REGEX.test(input);
48
+ }
49
+ /**
50
+ * 拡張MSPZ形式の文字列を解析して、純手牌と副露のリストに変換します。
51
+ *
52
+ * @param input 拡張MSPZ形式の文字列
53
+ * @returns 解析結果オブジェクト
54
+ */
55
+ export function parseExtendedMspz(input) {
56
+ const closedParts = [];
57
+ const exposed = [];
58
+ let current = "";
59
+ let mode = "closed";
60
+ // 文字単位でパース
61
+ for (const char of input) {
62
+ if (char === "[") {
63
+ if (mode !== "closed")
64
+ throw new Error("Nested brackets are not supported");
65
+ if (current.length > 0)
66
+ closedParts.push(current);
67
+ // current = ""; // OLD
68
+ current = "["; // NEW: Start capturing with bracket
69
+ mode = "open";
70
+ }
71
+ else if (char === "]") {
72
+ if (mode !== "open")
73
+ throw new Error("Unexpected closing bracket ']'");
74
+ current += "]"; // NEW: End capturing with bracket
75
+ // exposed.push(parseMentsuString(current, "open")); // OLD
76
+ exposed.push(parseMentsuFromExtendedMspz(asExtendedMspz(current))); // NEW
77
+ current = "";
78
+ mode = "closed";
79
+ }
80
+ else if (char === "(") {
81
+ if (mode !== "closed")
82
+ throw new Error("Nested parentheses are not supported");
83
+ if (current.length > 0)
84
+ closedParts.push(current);
85
+ // current = ""; // OLD
86
+ current = "("; // NEW
87
+ mode = "ankan";
88
+ }
89
+ else if (char === ")") {
90
+ if (mode !== "ankan")
91
+ throw new Error("Unexpected closing parenthesis ')'");
92
+ current += ")"; // NEW
93
+ // exposed.push(parseMentsuString(current, "ankan")); // OLD
94
+ exposed.push(parseMentsuFromExtendedMspz(asExtendedMspz(current))); // NEW
95
+ current = "";
96
+ mode = "closed";
97
+ }
98
+ else {
99
+ current += char;
100
+ }
101
+ }
102
+ // 残りのclosed部分
103
+ if (current.length > 0) {
104
+ if (mode !== "closed")
105
+ throw new Error("Unclosed bracket or parenthesis");
106
+ closedParts.push(current);
107
+ }
108
+ // closed部分を結合してパース
109
+ const fullClosedMspz = closedParts.join("");
110
+ const closedIds = parseMspzToHaiKindIds(asMspz(fullClosedMspz));
111
+ return {
112
+ closed: closedIds,
113
+ exposed: exposed,
114
+ };
115
+ }
116
+ /**
117
+ * 副露・暗槓のブロック文字列(例: "[123m]", "(11z)")を解析してCompletedMentsuを生成する内部関数
118
+ * 括弧の種類から副露か暗槓かを自動判定します。
119
+ */
120
+ function parseMentsuFromExtendedMspz(block) {
121
+ let mode;
122
+ let content;
123
+ if (block.startsWith("[") && block.endsWith("]")) {
124
+ mode = "open";
125
+ content = block.slice(1, -1);
126
+ }
127
+ else if (block.startsWith("(") && block.endsWith(")")) {
128
+ mode = "ankan";
129
+ content = block.slice(1, -1);
130
+ }
131
+ else {
132
+ throw new Error(`Invalid Extended MSPZ block: ${block} (must be [...] or (...))`);
133
+ }
134
+ // 中身は標準MSPZ形式である必要がある
135
+ const ids = parseMspzToHaiKindIds(asMspz(content));
136
+ if (ids.length === 0) {
137
+ throw new Error("Empty mentsu specification");
138
+ }
139
+ // 枚数チェック & 種類判定
140
+ const count = ids.length;
141
+ const isAllSame = ids.every((id) => id === ids[0]);
142
+ // 暗槓 (Ankan)
143
+ if (mode === "ankan") {
144
+ if (count !== 4 || !isAllSame) {
145
+ throw new Error(`Invalid Ankan: ${block} (must be 4 identical tiles)`);
146
+ }
147
+ if (!isTuple4(ids)) {
148
+ throw new Error("Internal Error: ids length check mismatch");
149
+ }
150
+ const kantsu = {
151
+ type: MentsuType.Kantsu,
152
+ hais: ids,
153
+ // Ankan has no furo info (or minimal)
154
+ };
155
+ return kantsu;
156
+ }
157
+ // 副露 (Open)
158
+ if (count === 4 && isAllSame) {
159
+ // Daiminkan
160
+ if (!isTuple4(ids)) {
161
+ throw new Error("Internal Error: ids length check mismatch");
162
+ }
163
+ const kantsu = {
164
+ type: MentsuType.Kantsu,
165
+ hais: ids,
166
+ furo: { type: FuroType.Daiminkan, from: Tacha.Toimen }, // Default
167
+ };
168
+ return kantsu;
169
+ }
170
+ else if (count === 3 && isAllSame) {
171
+ // Pon
172
+ if (!isTuple3(ids)) {
173
+ throw new Error("Internal Error: ids length check mismatch");
174
+ }
175
+ const koutsu = {
176
+ type: MentsuType.Koutsu,
177
+ hais: ids,
178
+ furo: { type: FuroType.Pon, from: Tacha.Toimen }, // Default
179
+ };
180
+ return koutsu;
181
+ }
182
+ else if (count === 3) {
183
+ // Chi (Should check continuity, strictly speaking but relying on user for now or implicit check)
184
+ // Minimal check: sorted? mspzStringToHaiKindIds sorts by default?
185
+ // mspzStringToHaiKindIds does NOT sort across different suits, but "123m" results in sorted array.
186
+ // Let's assume valid sequence for now.
187
+ if (!isTuple3(ids)) {
188
+ throw new Error("Internal Error: ids length check mismatch");
189
+ }
190
+ const shuntsu = {
191
+ type: MentsuType.Shuntsu,
192
+ hais: ids,
193
+ furo: { type: FuroType.Chi, from: Tacha.Kamicha }, // Default
194
+ };
195
+ return shuntsu;
196
+ }
197
+ throw new Error(`Invalid Mentsu specification: ${block} (must be 3 or 4 tiles)`);
198
+ }
199
+ /**
200
+ * 13枚の牌種ID配列を 34種の牌種分布(所持数分布)に変換します。
201
+ * @throws {ShoushaiError} 牌の数が13枚より少ない場合
202
+ * @throws {TahaiError} 牌の数が13枚より多い場合
203
+ */
204
+ export function createDistribution(hais) {
205
+ if (hais.length < 13) {
206
+ throw new ShoushaiError(`Invalid number of tiles: expected 13, got ${hais.length}`);
207
+ }
208
+ if (hais.length > 13) {
209
+ throw new TahaiError(`Invalid number of tiles: expected 13, got ${hais.length}`);
210
+ }
211
+ const counts = Array.from({ length: 34 }, () => 0);
212
+ for (const kind of hais) {
213
+ counts[kind] = (counts[kind] ?? 0) + 1;
214
+ }
215
+ // Tupleへの変換はアサーションが必要だが、生成ロジックが保証しているため安全
216
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
217
+ return counts;
218
+ }
219
+ /**
220
+ * 13枚の牌ID配列を 34種の牌種分布(所持数分布)に変換します。
221
+ * @throws {ShoushaiError} 牌の数が13枚より少ない場合
222
+ * @throws {TahaiError} 牌の数が13枚より多い場合
223
+ */
224
+ export function haiIdsToDistribution(
225
+ // Branded type makes linter think it's mutable object, but it's primitive number.
226
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
227
+ hais) {
228
+ const kinds = hais.map(haiIdToKindId);
229
+ return createDistribution(kinds);
230
+ }
231
+ /**
232
+ * 13枚の牌種ID配列を MSPZ形式の文字列(例: "123m456p...")に変換します。
233
+ * すべての牌をソートして表記します。
234
+ * @throws {ShoushaiError} 牌の数が13枚より少ない場合
235
+ * @throws {TahaiError} 牌の数が13枚より多い場合
236
+ */
237
+ export function haiKindIdsToMspzString(hais) {
238
+ const counts = createDistribution(hais);
239
+ let result = "";
240
+ // 萬子
241
+ const manzu = [];
242
+ for (let i = 0; i < 9; i++) {
243
+ const kind = asHaiKindId(HaiKind.ManZu1 + i);
244
+ const count = counts[kind];
245
+ for (let j = 0; j < count; j++) {
246
+ const num = haiKindToNumber(kind);
247
+ if (num !== undefined)
248
+ manzu.push(num);
249
+ }
250
+ }
251
+ if (manzu.length > 0) {
252
+ result += manzu.join("") + "m";
253
+ }
254
+ // 筒子
255
+ const pinzu = [];
256
+ for (let i = 0; i < 9; i++) {
257
+ const kind = asHaiKindId(HaiKind.PinZu1 + i);
258
+ const count = counts[kind];
259
+ for (let j = 0; j < count; j++) {
260
+ const num = haiKindToNumber(kind);
261
+ if (num !== undefined)
262
+ pinzu.push(num);
263
+ }
264
+ }
265
+ if (pinzu.length > 0) {
266
+ result += pinzu.join("") + "p";
267
+ }
268
+ // 索子
269
+ const souzu = [];
270
+ for (let i = 0; i < 9; i++) {
271
+ const kind = asHaiKindId(HaiKind.SouZu1 + i);
272
+ const count = counts[kind];
273
+ for (let j = 0; j < count; j++) {
274
+ const num = haiKindToNumber(kind);
275
+ if (num !== undefined)
276
+ souzu.push(num);
277
+ }
278
+ }
279
+ if (souzu.length > 0) {
280
+ result += souzu.join("") + "s";
281
+ }
282
+ // 字牌
283
+ const jihai = [];
284
+ for (let i = 0; i < 7; i++) {
285
+ const kind = asHaiKindId(HaiKind.Ton + i);
286
+ const count = counts[kind];
287
+ for (let j = 0; j < count; j++) {
288
+ // 字牌は 1-7 で表すことが多い
289
+ const num = i + 1;
290
+ jihai.push(num);
291
+ }
292
+ }
293
+ if (jihai.length > 0) {
294
+ result += jihai.join("") + "z";
295
+ }
296
+ return result;
297
+ }
298
+ /**
299
+ * 文字列を MspzString 型として扱います。
300
+ * 標準的なMSPZ形式(拡張記法を含まない)であることを検証します。
301
+ *
302
+ * @param input 変換対象の文字列
303
+ * @throws {Error} 拡張記法が含まれている場合
304
+ * @returns MspzString
305
+ */
306
+ export function asMspz(input) {
307
+ if (!isMspz(input)) {
308
+ throw new Error(`Invalid MSPZ string: ${input}`);
309
+ }
310
+ return input;
311
+ }
312
+ /**
313
+ * MSPZ形式の文字列(例: "123m456p")を解析して HaiKindId の配列に変換します。
314
+ * 主にテストデータの作成用途で使用します。
315
+ *
316
+ * @param mspz MSPZ形式の文字列
317
+ * @returns HaiKindId の配列
318
+ */
319
+ export function parseMspzToHaiKindIds(mspz) {
320
+ const result = [];
321
+ let currentNumbers = [];
322
+ for (const char of mspz) {
323
+ if (char >= "0" && char <= "9") {
324
+ currentNumbers.push(parseInt(char, 10));
325
+ }
326
+ else {
327
+ // Suffix handling
328
+ let base;
329
+ switch (char) {
330
+ case "m":
331
+ base = HaiKind.ManZu1;
332
+ break;
333
+ case "p":
334
+ base = HaiKind.PinZu1;
335
+ break;
336
+ case "s":
337
+ base = HaiKind.SouZu1;
338
+ break;
339
+ case "z":
340
+ base = HaiKind.Ton;
341
+ break;
342
+ default:
343
+ // 無視する
344
+ currentNumbers = [];
345
+ continue;
346
+ }
347
+ for (const num of currentNumbers) {
348
+ if (char === "z") {
349
+ // 字牌: 1=東(27), ... 7=中(33)
350
+ if (num >= 1 && num <= 7) {
351
+ result.push(asHaiKindId(base + num - 1));
352
+ }
353
+ }
354
+ else {
355
+ // 数牌: 1-9
356
+ if (num >= 1) {
357
+ result.push(asHaiKindId(base + num - 1));
358
+ }
359
+ }
360
+ }
361
+ currentNumbers = [];
362
+ }
363
+ }
364
+ return result;
365
+ }
@@ -0,0 +1,27 @@
1
+ export declare const SCORE_OYA_MULTIPLIER = 1.5;
2
+ export declare const SCORE_BASE_MANGAN = 2000;
3
+ export declare const SCORE_BASE_HANEMAN = 3000;
4
+ export declare const SCORE_BASE_BAIMAN = 4000;
5
+ export declare const SCORE_BASE_SANBAIMAN = 6000;
6
+ export declare const SCORE_BASE_YAKUMAN = 8000;
7
+ /**
8
+ * 満貫以上の判定基準となる飜数
9
+ *
10
+ * "5飜" であれば符数に関わらず満貫以上が確定するため 5 を設定しています。
11
+ *
12
+ * Q: 4飜は満貫ではないのか?
13
+ * A: 4飜でも符数が高ければ(40符以上など)満貫になりますが、以下のようなケースでは満貫(8000点)に届きません。
14
+ * - 七対子 (25符): 6400点
15
+ * - 門前ツモ・愚形など (30符): 7900点 (※切り上げ満貫なしの場合)
16
+ * - 鳴き手・ロン (30符): 7700点 (※切り上げ満貫なしの場合)
17
+ *
18
+ * そのため、4飜以下の場合は計算による基本点が基準値(2000)を超えたかどうかで判定します。
19
+ */
20
+ export declare const HAN_MANGAN = 5;
21
+ export declare const HAN_HANEMAN = 6;
22
+ export declare const HAN_BAIMAN = 8;
23
+ export declare const HAN_SANBAIMAN = 11;
24
+ export declare const HAN_YAKUMAN = 13;
25
+ export declare const HAS_YAKUMAN = 13;
26
+ export declare const HAS_DOUBLE_YAKUMAN = 26;
27
+ export declare const BASE_POINT_LIMIT = 2000;
@@ -0,0 +1,30 @@
1
+ export const SCORE_OYA_MULTIPLIER = 1.5; // 親は1.5倍
2
+ export const SCORE_BASE_MANGAN = 2000;
3
+ export const SCORE_BASE_HANEMAN = 3000;
4
+ export const SCORE_BASE_BAIMAN = 4000;
5
+ export const SCORE_BASE_SANBAIMAN = 6000;
6
+ export const SCORE_BASE_YAKUMAN = 8000;
7
+ /**
8
+ * 満貫以上の判定基準となる飜数
9
+ *
10
+ * "5飜" であれば符数に関わらず満貫以上が確定するため 5 を設定しています。
11
+ *
12
+ * Q: 4飜は満貫ではないのか?
13
+ * A: 4飜でも符数が高ければ(40符以上など)満貫になりますが、以下のようなケースでは満貫(8000点)に届きません。
14
+ * - 七対子 (25符): 6400点
15
+ * - 門前ツモ・愚形など (30符): 7900点 (※切り上げ満貫なしの場合)
16
+ * - 鳴き手・ロン (30符): 7700点 (※切り上げ満貫なしの場合)
17
+ *
18
+ * そのため、4飜以下の場合は計算による基本点が基準値(2000)を超えたかどうかで判定します。
19
+ */
20
+ export const HAN_MANGAN = 5;
21
+ export const HAN_HANEMAN = 6;
22
+ export const HAN_BAIMAN = 8;
23
+ export const HAN_SANBAIMAN = 11;
24
+ export const HAN_YAKUMAN = 13;
25
+ export const HAS_YAKUMAN = 13; // 便宜上の飜数
26
+ export const HAS_DOUBLE_YAKUMAN = 26;
27
+ // 切り上げ満貫の閾値 (30符4飜 = 1920 -> 2000? 60符3飜=1920)
28
+ // 一般的には 2000点(子) / 3000点(親) が満貫の最低点(ベース)
29
+ // 符計算による基本点が 2000 を超えたら満貫
30
+ export const BASE_POINT_LIMIT = 2000;
@@ -0,0 +1,21 @@
1
+ import { type Tehai14 } from "../../types";
2
+ import type { FuResult } from "./lib/fu/types";
3
+ import type { HouraContext } from "../yaku/types";
4
+ import type { ScoreCalculationConfig, ScoreLevel, ScoreResult } from "./types";
5
+ export type { ScoreCalculationConfig, ScoreLevel, ScoreResult };
6
+ /**
7
+ * 手牌とコンテキストから点数を計算する(公開API)
8
+ *
9
+ * 手牌の構造解析を行い、最も高点となる解釈を採用して点数を返します。
10
+ *
11
+ * @param tehai 手牌 (14枚)
12
+ * @param config 点数計算の設定 (場風、自風、ドラなど)
13
+ * @returns 点数計算結果
14
+ */
15
+ export declare function calculateScore(tehai: Tehai14, config: Readonly<ScoreCalculationConfig>): ScoreResult;
16
+ /**
17
+ * 基本的な点数計算ロジック (内部用・テスト用)
18
+ */
19
+ export declare function calculateBasicScore(yakuHansu: number, fuResult: Readonly<FuResult>, dora: number, context: Readonly<HouraContext & {
20
+ isOya: boolean;
21
+ }>): ScoreResult;