@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.
- 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 +42 -0
- package/dist/core/tehai.js +156 -0
- package/dist/errors.d.ts +83 -0
- package/dist/errors.js +117 -0
- package/dist/features/machi/index.d.ts +9 -0
- package/dist/features/machi/index.js +48 -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/score/constants.d.ts +27 -0
- package/dist/features/score/constants.js +30 -0
- package/dist/features/score/index.d.ts +45 -0
- package/dist/features/score/index.js +207 -0
- package/dist/features/score/lib/fu/constants.d.ts +37 -0
- package/dist/features/score/lib/fu/constants.js +45 -0
- package/dist/features/score/lib/fu/index.d.ts +11 -0
- package/dist/features/score/lib/fu/index.js +23 -0
- package/dist/features/score/lib/fu/lib/chiitoitsu.d.ts +5 -0
- package/dist/features/score/lib/fu/lib/chiitoitsu.js +17 -0
- package/dist/features/score/lib/fu/lib/kokushi.d.ts +6 -0
- package/dist/features/score/lib/fu/lib/kokushi.js +18 -0
- package/dist/features/score/lib/fu/lib/mentsu.d.ts +11 -0
- package/dist/features/score/lib/fu/lib/mentsu.js +136 -0
- package/dist/features/score/lib/fu/types.d.ts +56 -0
- package/dist/features/score/lib/fu/types.js +1 -0
- package/dist/features/score/types.d.ts +78 -0
- package/dist/features/score/types.js +22 -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 +21 -0
- package/dist/features/yaku/index.js +59 -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/iipeikou.d.ts +2 -0
- package/dist/features/yaku/lib/definitions/iipeikou.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/ryanpeikou.d.ts +2 -0
- package/dist/features/yaku/lib/definitions/ryanpeikou.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 +21 -0
- package/dist/index.js +53 -0
- package/dist/types.d.ts +290 -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 +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,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
|
+
}
|