@pai-forge/riichi-mahjong 0.2.0 → 0.3.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 (151) hide show
  1. package/dist/core/dora.d.ts +14 -0
  2. package/dist/core/dora.js +71 -0
  3. package/dist/core/hai.d.ts +30 -0
  4. package/dist/core/hai.js +78 -0
  5. package/dist/core/machi.d.ts +11 -0
  6. package/dist/core/machi.js +58 -0
  7. package/dist/core/mentsu.d.ts +26 -0
  8. package/dist/core/mentsu.js +87 -0
  9. package/dist/core/tehai.d.ts +42 -0
  10. package/dist/core/tehai.js +156 -0
  11. package/dist/errors.d.ts +83 -0
  12. package/dist/errors.js +117 -0
  13. package/dist/features/machi/index.d.ts +9 -0
  14. package/dist/features/machi/index.js +48 -0
  15. package/dist/features/machi/types.d.ts +1 -0
  16. package/dist/features/machi/types.js +1 -0
  17. package/dist/features/parser/index.d.ts +19 -0
  18. package/dist/features/parser/index.js +28 -0
  19. package/dist/features/parser/mspz.d.ts +85 -0
  20. package/dist/features/parser/mspz.js +365 -0
  21. package/dist/features/points/constants.d.ts +27 -0
  22. package/dist/features/points/constants.js +30 -0
  23. package/dist/features/points/index.d.ts +21 -0
  24. package/dist/features/points/index.js +174 -0
  25. package/dist/features/points/lib/fu/constants.d.ts +37 -0
  26. package/dist/features/points/lib/fu/constants.js +45 -0
  27. package/dist/features/points/lib/fu/index.d.ts +11 -0
  28. package/dist/features/points/lib/fu/index.js +23 -0
  29. package/dist/features/points/lib/fu/lib/chiitoitsu.d.ts +5 -0
  30. package/dist/features/points/lib/fu/lib/chiitoitsu.js +17 -0
  31. package/dist/features/points/lib/fu/lib/kokushi.d.ts +6 -0
  32. package/dist/features/points/lib/fu/lib/kokushi.js +18 -0
  33. package/dist/features/points/lib/fu/lib/mentsu.d.ts +11 -0
  34. package/dist/features/points/lib/fu/lib/mentsu.js +122 -0
  35. package/dist/features/points/lib/fu/types.d.ts +55 -0
  36. package/dist/features/points/lib/fu/types.js +1 -0
  37. package/dist/features/points/types.d.ts +27 -0
  38. package/dist/features/points/types.js +1 -0
  39. package/dist/features/score/constants.d.ts +27 -0
  40. package/dist/features/score/constants.js +30 -0
  41. package/dist/features/score/index.d.ts +45 -0
  42. package/dist/features/score/index.js +207 -0
  43. package/dist/features/score/lib/fu/constants.d.ts +37 -0
  44. package/dist/features/score/lib/fu/constants.js +45 -0
  45. package/dist/features/score/lib/fu/index.d.ts +11 -0
  46. package/dist/features/score/lib/fu/index.js +23 -0
  47. package/dist/features/score/lib/fu/lib/chiitoitsu.d.ts +5 -0
  48. package/dist/features/score/lib/fu/lib/chiitoitsu.js +17 -0
  49. package/dist/features/score/lib/fu/lib/kokushi.d.ts +6 -0
  50. package/dist/features/score/lib/fu/lib/kokushi.js +18 -0
  51. package/dist/features/score/lib/fu/lib/mentsu.d.ts +11 -0
  52. package/dist/features/score/lib/fu/lib/mentsu.js +136 -0
  53. package/dist/features/score/lib/fu/types.d.ts +56 -0
  54. package/dist/features/score/lib/fu/types.js +1 -0
  55. package/dist/features/score/types.d.ts +78 -0
  56. package/dist/features/score/types.js +22 -0
  57. package/dist/features/shanten/index.d.ts +16 -0
  58. package/dist/features/shanten/index.js +25 -0
  59. package/dist/features/shanten/logic/chiitoitsu.d.ts +8 -0
  60. package/dist/features/shanten/logic/chiitoitsu.js +36 -0
  61. package/dist/features/shanten/logic/kokushi.d.ts +16 -0
  62. package/dist/features/shanten/logic/kokushi.js +48 -0
  63. package/dist/features/shanten/logic/mentsu-te.d.ts +8 -0
  64. package/dist/features/shanten/logic/mentsu-te.js +129 -0
  65. package/dist/features/yaku/factory.d.ts +13 -0
  66. package/dist/features/yaku/factory.js +19 -0
  67. package/dist/features/yaku/index.d.ts +21 -0
  68. package/dist/features/yaku/index.js +59 -0
  69. package/dist/features/yaku/lib/definitions/chiitoitsu.d.ts +2 -0
  70. package/dist/features/yaku/lib/definitions/chiitoitsu.js +12 -0
  71. package/dist/features/yaku/lib/definitions/chinitsu.d.ts +2 -0
  72. package/dist/features/yaku/lib/definitions/chinitsu.js +40 -0
  73. package/dist/features/yaku/lib/definitions/chinroutou.d.ts +2 -0
  74. package/dist/features/yaku/lib/definitions/chinroutou.js +21 -0
  75. package/dist/features/yaku/lib/definitions/chuuren-poutou.d.ts +2 -0
  76. package/dist/features/yaku/lib/definitions/chuuren-poutou.js +69 -0
  77. package/dist/features/yaku/lib/definitions/daisangen.d.ts +2 -0
  78. package/dist/features/yaku/lib/definitions/daisangen.js +26 -0
  79. package/dist/features/yaku/lib/definitions/daisuushii.d.ts +2 -0
  80. package/dist/features/yaku/lib/definitions/daisuushii.js +32 -0
  81. package/dist/features/yaku/lib/definitions/honchan.d.ts +2 -0
  82. package/dist/features/yaku/lib/definitions/honchan.js +29 -0
  83. package/dist/features/yaku/lib/definitions/honitsu.d.ts +2 -0
  84. package/dist/features/yaku/lib/definitions/honitsu.js +40 -0
  85. package/dist/features/yaku/lib/definitions/honroutou.d.ts +2 -0
  86. package/dist/features/yaku/lib/definitions/honroutou.js +33 -0
  87. package/dist/features/yaku/lib/definitions/iipeikou.d.ts +2 -0
  88. package/dist/features/yaku/lib/definitions/iipeikou.js +46 -0
  89. package/dist/features/yaku/lib/definitions/ikkitsuukan.d.ts +2 -0
  90. package/dist/features/yaku/lib/definitions/ikkitsuukan.js +56 -0
  91. package/dist/features/yaku/lib/definitions/index.d.ts +30 -0
  92. package/dist/features/yaku/lib/definitions/index.js +90 -0
  93. package/dist/features/yaku/lib/definitions/junchan.d.ts +2 -0
  94. package/dist/features/yaku/lib/definitions/junchan.js +25 -0
  95. package/dist/features/yaku/lib/definitions/kokushi.d.ts +2 -0
  96. package/dist/features/yaku/lib/definitions/kokushi.js +12 -0
  97. package/dist/features/yaku/lib/definitions/menzen-tsumo.d.ts +2 -0
  98. package/dist/features/yaku/lib/definitions/menzen-tsumo.js +8 -0
  99. package/dist/features/yaku/lib/definitions/pinfu.d.ts +2 -0
  100. package/dist/features/yaku/lib/definitions/pinfu.js +40 -0
  101. package/dist/features/yaku/lib/definitions/ryanpeikou.d.ts +2 -0
  102. package/dist/features/yaku/lib/definitions/ryanpeikou.js +33 -0
  103. package/dist/features/yaku/lib/definitions/ryuuiisou.d.ts +2 -0
  104. package/dist/features/yaku/lib/definitions/ryuuiisou.js +43 -0
  105. package/dist/features/yaku/lib/definitions/sanankou.d.ts +2 -0
  106. package/dist/features/yaku/lib/definitions/sanankou.js +49 -0
  107. package/dist/features/yaku/lib/definitions/sankantsu.d.ts +2 -0
  108. package/dist/features/yaku/lib/definitions/sankantsu.js +18 -0
  109. package/dist/features/yaku/lib/definitions/sanshoku-doujun.d.ts +2 -0
  110. package/dist/features/yaku/lib/definitions/sanshoku-doujun.js +58 -0
  111. package/dist/features/yaku/lib/definitions/sanshoku-doukou.d.ts +2 -0
  112. package/dist/features/yaku/lib/definitions/sanshoku-doukou.js +53 -0
  113. package/dist/features/yaku/lib/definitions/shousangen.d.ts +2 -0
  114. package/dist/features/yaku/lib/definitions/shousangen.js +28 -0
  115. package/dist/features/yaku/lib/definitions/shousuushii.d.ts +2 -0
  116. package/dist/features/yaku/lib/definitions/shousuushii.js +34 -0
  117. package/dist/features/yaku/lib/definitions/suuankou.d.ts +2 -0
  118. package/dist/features/yaku/lib/definitions/suuankou.js +63 -0
  119. package/dist/features/yaku/lib/definitions/suukantsu.d.ts +2 -0
  120. package/dist/features/yaku/lib/definitions/suukantsu.js +18 -0
  121. package/dist/features/yaku/lib/definitions/tanyao.d.ts +2 -0
  122. package/dist/features/yaku/lib/definitions/tanyao.js +23 -0
  123. package/dist/features/yaku/lib/definitions/toitoi.d.ts +2 -0
  124. package/dist/features/yaku/lib/definitions/toitoi.js +16 -0
  125. package/dist/features/yaku/lib/definitions/tsuuiisou.d.ts +2 -0
  126. package/dist/features/yaku/lib/definitions/tsuuiisou.js +35 -0
  127. package/dist/features/yaku/lib/definitions/yakuhai.d.ts +4 -0
  128. package/dist/features/yaku/lib/definitions/yakuhai.js +21 -0
  129. package/dist/features/yaku/lib/index.d.ts +1 -0
  130. package/dist/features/yaku/lib/index.js +1 -0
  131. package/dist/features/yaku/lib/structures/chiitoitsu.d.ts +6 -0
  132. package/dist/features/yaku/lib/structures/chiitoitsu.js +38 -0
  133. package/dist/features/yaku/lib/structures/index.d.ts +10 -0
  134. package/dist/features/yaku/lib/structures/index.js +17 -0
  135. package/dist/features/yaku/lib/structures/kokushi.d.ts +6 -0
  136. package/dist/features/yaku/lib/structures/kokushi.js +43 -0
  137. package/dist/features/yaku/lib/structures/mentsu-te.d.ts +24 -0
  138. package/dist/features/yaku/lib/structures/mentsu-te.js +127 -0
  139. package/dist/features/yaku/types.d.ts +121 -0
  140. package/dist/features/yaku/types.js +1 -0
  141. package/dist/features/yaku/utils.d.ts +19 -0
  142. package/dist/features/yaku/utils.js +34 -0
  143. package/dist/index.d.ts +21 -0
  144. package/dist/index.js +53 -0
  145. package/dist/types.d.ts +290 -0
  146. package/dist/types.js +97 -0
  147. package/dist/utils/assertions.d.ts +22 -0
  148. package/dist/utils/assertions.js +33 -0
  149. package/dist/utils/test-helpers.d.ts +55 -0
  150. package/dist/utils/test-helpers.js +124 -0
  151. package/package.json +3 -2
package/dist/errors.js ADDED
@@ -0,0 +1,117 @@
1
+ /**
2
+ * riichi-mahjong ライブラリの基本エラークラス
3
+ * 全てのカスタムエラーはこのクラスを継承します。
4
+ */
5
+ export class MahjongError extends Error {
6
+ /**
7
+ *
8
+ */
9
+ constructor(message) {
10
+ super(message);
11
+ this.name = "MahjongError";
12
+ // TypeScriptでカスタムエラーを正しく動作させるためのハック
13
+ // TypeScriptでカスタムエラーを正しく動作させるためのハック
14
+ Object.setPrototypeOf(this, MahjongError.prototype);
15
+ }
16
+ }
17
+ /**
18
+ * ツモれなかった場合のエラー (少牌)
19
+ * 手牌が規定枚数(13枚)より少ない場合にスローされます。
20
+ */
21
+ export class ShoushaiError extends MahjongError {
22
+ /**
23
+ *
24
+ */
25
+ constructor(message = "手牌が規定枚数(13枚)より少ないです。") {
26
+ super(message);
27
+ this.name = "ShoushaiError";
28
+ Object.setPrototypeOf(this, ShoushaiError.prototype);
29
+ }
30
+ }
31
+ /**
32
+ * 切り忘れの場合のエラー (多牌)
33
+ * 手牌が規定枚数(13枚)より多い場合にスローされます。
34
+ */
35
+ export class TahaiError extends MahjongError {
36
+ /**
37
+ *
38
+ */
39
+ constructor(message = "手牌が規定枚数(13枚)より多いです。") {
40
+ super(message);
41
+ this.name = "TahaiError";
42
+ Object.setPrototypeOf(this, TahaiError.prototype);
43
+ }
44
+ }
45
+ /**
46
+ * 引数が不正な場合のエラー
47
+ * 必要なパラメータが不足している、または不正な値の場合にスローされます。
48
+ */
49
+ export class MahjongArgumentError extends MahjongError {
50
+ /**
51
+ *
52
+ */
53
+ constructor(message) {
54
+ super(message);
55
+ this.name = "MahjongArgumentError";
56
+ Object.setPrototypeOf(this, MahjongArgumentError.prototype);
57
+ }
58
+ }
59
+ /**
60
+ * 牌IDが重複している場合のエラー
61
+ * (物理的な牌IDは一意である必要があります)
62
+ */
63
+ export class DuplicatedHaiIdError extends MahjongError {
64
+ /**
65
+ *
66
+ */
67
+ constructor(message = "牌IDが重複しています。") {
68
+ super(message);
69
+ this.name = "DuplicatedHaiIdError";
70
+ Object.setPrototypeOf(this, DuplicatedHaiIdError.prototype);
71
+ }
72
+ }
73
+ /**
74
+ * 牌の枚数が不正な場合のエラー
75
+ * (同種の牌は最大4枚までです)
76
+ */
77
+ export class InvalidHaiQuantityError extends MahjongError {
78
+ /**
79
+ *
80
+ */
81
+ constructor(message = "同種の牌が5枚以上存在します。") {
82
+ super(message);
83
+ this.name = "InvalidHaiQuantityError";
84
+ Object.setPrototypeOf(this, InvalidHaiQuantityError.prototype);
85
+ }
86
+ }
87
+ /**
88
+ * チョンボ(錯和)の基底エラークラス
89
+ *
90
+ * 不正な和了宣言に関するエラーの基底クラス。
91
+ * 具体的なチョンボ種別(役なし、フリテン等)はサブクラスで定義する。
92
+ */
93
+ export class ChomboError extends MahjongError {
94
+ /**
95
+ *
96
+ */
97
+ constructor(message = "不正な和了です。") {
98
+ super(message);
99
+ this.name = "ChomboError";
100
+ Object.setPrototypeOf(this, ChomboError.prototype);
101
+ }
102
+ }
103
+ /**
104
+ * 役なし和了のエラー
105
+ *
106
+ * 和了形は成立しているが、役が一つも成立していない場合にスローされます。
107
+ */
108
+ export class NoYakuError extends ChomboError {
109
+ /**
110
+ *
111
+ */
112
+ constructor(message = "役が成立していません。") {
113
+ super(message);
114
+ this.name = "NoYakuError";
115
+ Object.setPrototypeOf(this, NoYakuError.prototype);
116
+ }
117
+ }
@@ -0,0 +1,9 @@
1
+ import type { HaiKindId, Tehai13 } from "../../types";
2
+ /**
3
+ * 手牌の受け入れ(有効牌)を計算する。
4
+ * 今回は面子手のみを対象とし、七対子や国士無双は考慮しない。
5
+ *
6
+ * @param tehai 手牌 (13枚)
7
+ * @returns シャンテン数を進める牌のリスト
8
+ */
9
+ export declare function getUkeire(tehai: Tehai13): HaiKindId[];
@@ -0,0 +1,48 @@
1
+ import { countHaiKind } from "../../core/tehai";
2
+ import { calculateMentsuTeShanten } from "../shanten";
3
+ /**
4
+ * 手牌の受け入れ(有効牌)を計算する。
5
+ * 今回は面子手のみを対象とし、七対子や国士無双は考慮しない。
6
+ *
7
+ * @param tehai 手牌 (13枚)
8
+ * @returns シャンテン数を進める牌のリスト
9
+ */
10
+ export function getUkeire(tehai) {
11
+ const currentShanten = calculateMentsuTeShanten(tehai);
12
+ const ukeireList = [];
13
+ // 手牌の全ての牌(純手牌 + 副露)をカウント
14
+ const allHais = [
15
+ ...tehai.closed,
16
+ ...tehai.exposed.flatMap((m) => m.hais),
17
+ ];
18
+ const haiCounts = countHaiKind(allHais);
19
+ // 全34種の牌について、1枚加えてシャンテン数が下がるか試す
20
+ for (let i = 0; i < 34; i++) {
21
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
22
+ const tile = i;
23
+ // 4枚使い切っている牌種はスキップ(山に残っていない)
24
+ if (haiCounts[tile] >= 4) {
25
+ continue;
26
+ }
27
+ // 手牌のコピーを作成(副作用を防ぐため、常に新しいオブジェクトで試行)
28
+ // Tehai13に1枚足すので、厳密にはTehai14として扱う必要があるが、
29
+ // calculateMentsuTeShanten は Tehai<HaiKindId> を受け付けるので、
30
+ // 構造的に { closed, exposed } が適合していればOK。
31
+ // ただし、Tehai13型に準拠したオブジェクトに1枚足すと枚数オーバーになるため、
32
+ // バリデーションを通過させるために Tehai14 として構築するか、
33
+ // calculateMentsuTeShanten 側が Generics で受け入れる点を利用する。
34
+ // 配列のコピーを作成
35
+ const newClosed = [...tehai.closed, tile];
36
+ const newTehai = {
37
+ closed: newClosed,
38
+ exposed: tehai.exposed,
39
+ };
40
+ // シャンテン数を計算
41
+ // ここでバリデーションエラーが出ないように、calculateMentsuTeShanten 側は validateTehai を使用している。
42
+ const newShanten = calculateMentsuTeShanten(newTehai);
43
+ if (newShanten < currentShanten) {
44
+ ukeireList.push(tile);
45
+ }
46
+ }
47
+ return ukeireList;
48
+ }
@@ -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
+ }