@pai-forge/riichi-mahjong 0.3.4 → 0.3.5

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/index.js CHANGED
@@ -70,7 +70,7 @@ class MahjongError extends Error {
70
70
  constructor(message) {
71
71
  super(message);
72
72
  this.name = "MahjongError";
73
- Object.setPrototypeOf(this, MahjongError.prototype);
73
+ Object.setPrototypeOf(this, new.target.prototype);
74
74
  }
75
75
  }
76
76
  class ShoushaiError extends MahjongError {
@@ -80,7 +80,6 @@ class ShoushaiError extends MahjongError {
80
80
  constructor(message = "手牌が規定枚数(13枚)より少ないです。") {
81
81
  super(message);
82
82
  this.name = "ShoushaiError";
83
- Object.setPrototypeOf(this, ShoushaiError.prototype);
84
83
  }
85
84
  }
86
85
  class TahaiError extends MahjongError {
@@ -90,7 +89,6 @@ class TahaiError extends MahjongError {
90
89
  constructor(message = "手牌が規定枚数(13枚)より多いです。") {
91
90
  super(message);
92
91
  this.name = "TahaiError";
93
- Object.setPrototypeOf(this, TahaiError.prototype);
94
92
  }
95
93
  }
96
94
  class MahjongArgumentError extends MahjongError {
@@ -100,7 +98,6 @@ class MahjongArgumentError extends MahjongError {
100
98
  constructor(message) {
101
99
  super(message);
102
100
  this.name = "MahjongArgumentError";
103
- Object.setPrototypeOf(this, MahjongArgumentError.prototype);
104
101
  }
105
102
  }
106
103
  class DuplicatedHaiIdError extends MahjongError {
@@ -110,7 +107,6 @@ class DuplicatedHaiIdError extends MahjongError {
110
107
  constructor(message = "牌IDが重複しています。") {
111
108
  super(message);
112
109
  this.name = "DuplicatedHaiIdError";
113
- Object.setPrototypeOf(this, DuplicatedHaiIdError.prototype);
114
110
  }
115
111
  }
116
112
  class InvalidHaiQuantityError extends MahjongError {
@@ -120,7 +116,6 @@ class InvalidHaiQuantityError extends MahjongError {
120
116
  constructor(message = "同種の牌が5枚以上存在します。") {
121
117
  super(message);
122
118
  this.name = "InvalidHaiQuantityError";
123
- Object.setPrototypeOf(this, InvalidHaiQuantityError.prototype);
124
119
  }
125
120
  }
126
121
  class ChomboError extends MahjongError {
@@ -130,7 +125,6 @@ class ChomboError extends MahjongError {
130
125
  constructor(message = "不正な和了です。") {
131
126
  super(message);
132
127
  this.name = "ChomboError";
133
- Object.setPrototypeOf(this, ChomboError.prototype);
134
128
  }
135
129
  }
136
130
  class NoYakuError extends ChomboError {
@@ -140,7 +134,15 @@ class NoYakuError extends ChomboError {
140
134
  constructor(message = "役が成立していません。") {
141
135
  super(message);
142
136
  this.name = "NoYakuError";
143
- Object.setPrototypeOf(this, NoYakuError.prototype);
137
+ }
138
+ }
139
+ class MspzParseError extends MahjongError {
140
+ /**
141
+ *
142
+ */
143
+ constructor(message = "MSPZ文字列の解析に失敗しました。") {
144
+ super(message);
145
+ this.name = "MspzParseError";
144
146
  }
145
147
  }
146
148
  function isTuple2(arr) {
@@ -211,7 +213,7 @@ function countHaiKind(hais) {
211
213
  }
212
214
  return counts;
213
215
  }
214
- function validateTehai13(tehai) {
216
+ function assertTehai13(tehai) {
215
217
  const count = calculateTehaiCount(tehai);
216
218
  if (count < 13) {
217
219
  throw new ShoushaiError();
@@ -221,7 +223,10 @@ function validateTehai13(tehai) {
221
223
  }
222
224
  validateHaiConsistency(tehai);
223
225
  }
224
- function validateTehai14(tehai) {
226
+ function validateTehai13(tehai) {
227
+ assertTehai13(tehai);
228
+ }
229
+ function assertTehai14(tehai) {
225
230
  const count = calculateTehaiCount(tehai);
226
231
  if (count < 14) {
227
232
  throw new ShoushaiError();
@@ -231,6 +236,9 @@ function validateTehai14(tehai) {
231
236
  }
232
237
  validateHaiConsistency(tehai);
233
238
  }
239
+ function validateTehai14(tehai) {
240
+ assertTehai14(tehai);
241
+ }
234
242
  function validateTehai(tehai) {
235
243
  const count = calculateTehaiCount(tehai);
236
244
  if (count < 13) {
@@ -255,13 +263,7 @@ function validateHaiConsistency(tehai) {
255
263
  }
256
264
  const counts = /* @__PURE__ */ new Map();
257
265
  for (const hai of allHais) {
258
- let kind = hai;
259
- if (hai > 33) {
260
- if (hai < 36) kind = Math.floor(hai / 4);
261
- else if (hai < 72) kind = Math.floor((hai - 36) / 4) + 9;
262
- else if (hai < 108) kind = Math.floor((hai - 72) / 4) + 18;
263
- else kind = Math.floor((hai - 108) / 4) + 27;
264
- }
266
+ const kind = isHaiIdMode ? haiIdToKindId(hai) : hai;
265
267
  const current = counts.get(kind) ?? 0;
266
268
  if (current + 1 > 4) {
267
269
  throw new InvalidHaiQuantityError();
@@ -355,21 +357,12 @@ function getDoraNext(indicator) {
355
357
  return indicator;
356
358
  }
357
359
  function countDora(tehai, indicators) {
358
- let count = 0;
359
360
  const doraHais = indicators.map(getDoraNext);
360
- for (const hai of tehai.closed) {
361
- for (const dora of doraHais) {
362
- if (hai === dora) count++;
363
- }
364
- }
365
- for (const mentsu of tehai.exposed) {
366
- for (const hai of mentsu.hais) {
367
- for (const dora of doraHais) {
368
- if (hai === dora) count++;
369
- }
370
- }
371
- }
372
- return count;
361
+ const allHais = [...tehai.closed, ...tehai.exposed.flatMap((m) => m.hais)];
362
+ return allHais.reduce(
363
+ (count, hai) => count + doraHais.filter((d) => d === hai).length,
364
+ 0
365
+ );
373
366
  }
374
367
  function classifyMachi(hand, agariHai) {
375
368
  if (hand.type !== "Mentsu") return void 0;
@@ -566,7 +559,7 @@ function getUkeire(tehai) {
566
559
  ];
567
560
  const haiCounts = countHaiKind(allHais);
568
561
  for (let i = 0; i < 34; i++) {
569
- const tile = i;
562
+ const tile = asHaiKindId(i);
570
563
  if (haiCounts[tile] >= 4) {
571
564
  continue;
572
565
  }
@@ -725,6 +718,53 @@ function isMenzen(tehai) {
725
718
  function isKazehai(id) {
726
719
  return id === HaiKind.Ton || id === HaiKind.Nan || id === HaiKind.Sha || id === HaiKind.Pei;
727
720
  }
721
+ function countShuntsuPairs(hand) {
722
+ if (hand.type !== "Mentsu") {
723
+ return 0;
724
+ }
725
+ const shuntsuList = hand.fourMentsu.filter(
726
+ (mentsu) => mentsu.type === "Shuntsu"
727
+ );
728
+ const shuntsuCounts = /* @__PURE__ */ new Map();
729
+ for (const shuntsu of shuntsuList) {
730
+ const key = shuntsu.hais[0];
731
+ const currentCount = shuntsuCounts.get(key) ?? 0;
732
+ shuntsuCounts.set(key, currentCount + 1);
733
+ }
734
+ let pairCount = 0;
735
+ for (const count of shuntsuCounts.values()) {
736
+ pairCount += Math.floor(count / 2);
737
+ }
738
+ return pairCount;
739
+ }
740
+ function analyzeIshokuPattern(hand) {
741
+ let blocks;
742
+ if (hand.type === "Mentsu") {
743
+ blocks = [hand.jantou, ...hand.fourMentsu];
744
+ } else if (hand.type === "Chiitoitsu") {
745
+ blocks = hand.pairs;
746
+ } else {
747
+ return void 0;
748
+ }
749
+ const allHais = blocks.flatMap((b) => b.hais);
750
+ const hasJihai = allHais.some((k) => kindIdToHaiType(k) === HaiType.Jihai);
751
+ const suupais = allHais.filter((k) => isSuupai(k));
752
+ if (suupais.length === 0) {
753
+ return { hasJihai, suupaiSuit: void 0 };
754
+ }
755
+ const firstSuupai = suupais[0];
756
+ if (firstSuupai === void 0) {
757
+ return { hasJihai, suupaiSuit: void 0 };
758
+ }
759
+ const firstSuupaiType = kindIdToHaiType(firstSuupai);
760
+ const isAllSameType = suupais.every(
761
+ (k) => kindIdToHaiType(k) === firstSuupaiType
762
+ );
763
+ return {
764
+ hasJihai,
765
+ suupaiSuit: isAllSameType ? firstSuupaiType : void 0
766
+ };
767
+ }
728
768
  function createYakuDefinition(yaku, check) {
729
769
  return {
730
770
  yaku,
@@ -798,25 +838,7 @@ const IIPEIKO_YAKU = {
798
838
  }
799
839
  };
800
840
  const checkIipeikou = (hand) => {
801
- if (hand.type !== "Mentsu") {
802
- return false;
803
- }
804
- const shuntsuList = hand.fourMentsu.filter(
805
- (mentsu) => mentsu.type === "Shuntsu"
806
- );
807
- if (shuntsuList.length < 2) {
808
- return false;
809
- }
810
- const shuntsuCounts = /* @__PURE__ */ new Map();
811
- for (const shuntsu of shuntsuList) {
812
- const key = shuntsu.hais[0];
813
- const currentCount = shuntsuCounts.get(key) ?? 0;
814
- shuntsuCounts.set(key, currentCount + 1);
815
- }
816
- let pairCount = 0;
817
- for (const count of shuntsuCounts.values()) {
818
- pairCount += Math.floor(count / 2);
819
- }
841
+ const pairCount = countShuntsuPairs(hand);
820
842
  return pairCount === 1;
821
843
  };
822
844
  const iipeikouDefinition = createYakuDefinition(
@@ -835,22 +857,13 @@ const checkRyanpeikou = (hand) => {
835
857
  if (hand.type !== "Mentsu") {
836
858
  return false;
837
859
  }
838
- const shuntsuList = hand.fourMentsu.filter(
860
+ const shuntsuCount = hand.fourMentsu.filter(
839
861
  (mentsu) => mentsu.type === "Shuntsu"
840
- );
841
- if (shuntsuList.length < 4) {
862
+ ).length;
863
+ if (shuntsuCount < 4) {
842
864
  return false;
843
865
  }
844
- const shuntsuCounts = /* @__PURE__ */ new Map();
845
- for (const shuntsu of shuntsuList) {
846
- const key = shuntsu.hais[0];
847
- const currentCount = shuntsuCounts.get(key) ?? 0;
848
- shuntsuCounts.set(key, currentCount + 1);
849
- }
850
- let pairCount = 0;
851
- for (const count of shuntsuCounts.values()) {
852
- pairCount += Math.floor(count / 2);
853
- }
866
+ const pairCount = countShuntsuPairs(hand);
854
867
  return pairCount >= 2;
855
868
  };
856
869
  const ryanpeikouDefinition = createYakuDefinition(
@@ -1510,26 +1523,9 @@ const HONITSU_YAKU = {
1510
1523
  }
1511
1524
  };
1512
1525
  const checkHonitsu = (hand) => {
1513
- let blocks;
1514
- if (hand.type === "Mentsu") {
1515
- blocks = [hand.jantou, ...hand.fourMentsu];
1516
- } else if (hand.type === "Chiitoitsu") {
1517
- blocks = hand.pairs;
1518
- } else {
1519
- return false;
1520
- }
1521
- const allHais = blocks.flatMap((b) => b.hais);
1522
- const hasJihai = allHais.some((k) => kindIdToHaiType(k) === HaiType.Jihai);
1523
- if (!hasJihai) return false;
1524
- const suupais = allHais.filter((k) => isSuupai(k));
1525
- if (suupais.length === 0) return false;
1526
- const firstSuupai = suupais[0];
1527
- if (firstSuupai === void 0) return false;
1528
- const firstSuupaiType = kindIdToHaiType(firstSuupai);
1529
- const isAllSameType = suupais.every(
1530
- (k) => kindIdToHaiType(k) === firstSuupaiType
1531
- );
1532
- return isAllSameType;
1526
+ const result = analyzeIshokuPattern(hand);
1527
+ if (result === void 0) return false;
1528
+ return result.hasJihai && result.suupaiSuit !== void 0;
1533
1529
  };
1534
1530
  const honitsuDefinition = createYakuDefinition(
1535
1531
  HONITSU_YAKU,
@@ -1543,26 +1539,9 @@ const CHINITSU_YAKU = {
1543
1539
  }
1544
1540
  };
1545
1541
  const checkChinitsu = (hand) => {
1546
- let blocks;
1547
- if (hand.type === "Mentsu") {
1548
- blocks = [hand.jantou, ...hand.fourMentsu];
1549
- } else if (hand.type === "Chiitoitsu") {
1550
- blocks = hand.pairs;
1551
- } else {
1552
- return false;
1553
- }
1554
- const allHais = blocks.flatMap((b) => b.hais);
1555
- const hasJihai = allHais.some((k) => kindIdToHaiType(k) === HaiType.Jihai);
1556
- if (hasJihai) return false;
1557
- const suupais = allHais.filter((k) => isSuupai(k));
1558
- if (suupais.length === 0) return false;
1559
- const firstSuupai = suupais[0];
1560
- if (firstSuupai === void 0) return false;
1561
- const firstSuupaiType = kindIdToHaiType(firstSuupai);
1562
- const isAllSameType = suupais.every(
1563
- (k) => kindIdToHaiType(k) === firstSuupaiType
1564
- );
1565
- return isAllSameType;
1542
+ const result = analyzeIshokuPattern(hand);
1543
+ if (result === void 0) return false;
1544
+ return !result.hasJihai && result.suupaiSuit !== void 0;
1566
1545
  };
1567
1546
  const chinitsuDefinition = createYakuDefinition(
1568
1547
  CHINITSU_YAKU,
@@ -1648,15 +1627,15 @@ function detectYakuForStructure(hand, context) {
1648
1627
  function getTotalHan(yakuResult) {
1649
1628
  return yakuResult.reduce((sum, [, han]) => sum + han, 0);
1650
1629
  }
1651
- function detectYaku(tehai, agariHai, bakaze, jikaze, doraMarkers, uraDoraMarkers, isTsumo) {
1630
+ function detectYaku(tehai, config) {
1652
1631
  const context = {
1653
1632
  isMenzen: isMenzen(tehai),
1654
- agariHai,
1655
- bakaze: bakaze !== void 0 && isKazehai(bakaze) ? bakaze : void 0,
1656
- jikaze: jikaze !== void 0 && isKazehai(jikaze) ? jikaze : void 0,
1657
- doraMarkers: doraMarkers ?? [],
1658
- uraDoraMarkers: uraDoraMarkers ?? [],
1659
- isTsumo
1633
+ agariHai: config.agariHai,
1634
+ bakaze: config.bakaze,
1635
+ jikaze: config.jikaze,
1636
+ doraMarkers: config.doraMarkers ?? [],
1637
+ uraDoraMarkers: config.uraDoraMarkers ?? [],
1638
+ isTsumo: config.isTsumo
1660
1639
  };
1661
1640
  const structuralInterpretations = getHouraStructures(tehai);
1662
1641
  let bestResult = [];
@@ -1680,7 +1659,7 @@ function isExtendedMspz(input) {
1680
1659
  }
1681
1660
  function asExtendedMspz(input) {
1682
1661
  if (!isExtendedMspz(input)) {
1683
- throw new Error(`Invalid Extended MSPZ string: ${input}`);
1662
+ throw new MspzParseError(`Invalid Extended MSPZ string: ${input}`);
1684
1663
  }
1685
1664
  return input;
1686
1665
  }
@@ -1695,25 +1674,26 @@ function parseExtendedMspz$1(input) {
1695
1674
  for (const char of input) {
1696
1675
  if (char === "[") {
1697
1676
  if (mode !== "closed")
1698
- throw new Error("Nested brackets are not supported");
1677
+ throw new MspzParseError("Nested brackets are not supported");
1699
1678
  if (current.length > 0) closedParts.push(current);
1700
1679
  current = "[";
1701
1680
  mode = "open";
1702
1681
  } else if (char === "]") {
1703
- if (mode !== "open") throw new Error("Unexpected closing bracket ']'");
1682
+ if (mode !== "open")
1683
+ throw new MspzParseError("Unexpected closing bracket ']'");
1704
1684
  current += "]";
1705
1685
  exposed.push(parseMentsuFromExtendedMspz(asExtendedMspz(current)));
1706
1686
  current = "";
1707
1687
  mode = "closed";
1708
1688
  } else if (char === "(") {
1709
1689
  if (mode !== "closed")
1710
- throw new Error("Nested parentheses are not supported");
1690
+ throw new MspzParseError("Nested parentheses are not supported");
1711
1691
  if (current.length > 0) closedParts.push(current);
1712
1692
  current = "(";
1713
1693
  mode = "ankan";
1714
1694
  } else if (char === ")") {
1715
1695
  if (mode !== "ankan")
1716
- throw new Error("Unexpected closing parenthesis ')'");
1696
+ throw new MspzParseError("Unexpected closing parenthesis ')'");
1717
1697
  current += ")";
1718
1698
  exposed.push(parseMentsuFromExtendedMspz(asExtendedMspz(current)));
1719
1699
  current = "";
@@ -1723,7 +1703,8 @@ function parseExtendedMspz$1(input) {
1723
1703
  }
1724
1704
  }
1725
1705
  if (current.length > 0) {
1726
- if (mode !== "closed") throw new Error("Unclosed bracket or parenthesis");
1706
+ if (mode !== "closed")
1707
+ throw new MspzParseError("Unclosed bracket or parenthesis");
1727
1708
  closedParts.push(current);
1728
1709
  }
1729
1710
  const fullClosedMspz = closedParts.join("");
@@ -1743,22 +1724,24 @@ function parseMentsuFromExtendedMspz(block) {
1743
1724
  mode = "ankan";
1744
1725
  content = block.slice(1, -1);
1745
1726
  } else {
1746
- throw new Error(
1727
+ throw new MspzParseError(
1747
1728
  `Invalid Extended MSPZ block: ${block} (must be [...] or (...))`
1748
1729
  );
1749
1730
  }
1750
1731
  const ids = parseMspzToHaiKindIds(asMspz(content));
1751
1732
  if (ids.length === 0) {
1752
- throw new Error("Empty mentsu specification");
1733
+ throw new MspzParseError("Empty mentsu specification");
1753
1734
  }
1754
1735
  const count = ids.length;
1755
1736
  const isAllSame = ids.every((id) => id === ids[0]);
1756
1737
  if (mode === "ankan") {
1757
1738
  if (count !== 4 || !isAllSame) {
1758
- throw new Error(`Invalid Ankan: ${block} (must be 4 identical tiles)`);
1739
+ throw new MspzParseError(
1740
+ `Invalid Ankan: ${block} (must be 4 identical tiles)`
1741
+ );
1759
1742
  }
1760
1743
  if (!isTuple4(ids)) {
1761
- throw new Error("Internal Error: ids length check mismatch");
1744
+ throw new MspzParseError("Internal Error: ids length check mismatch");
1762
1745
  }
1763
1746
  const kantsu = {
1764
1747
  type: MentsuType.Kantsu,
@@ -1769,7 +1752,7 @@ function parseMentsuFromExtendedMspz(block) {
1769
1752
  }
1770
1753
  if (count === 4 && isAllSame) {
1771
1754
  if (!isTuple4(ids)) {
1772
- throw new Error("Internal Error: ids length check mismatch");
1755
+ throw new MspzParseError("Internal Error: ids length check mismatch");
1773
1756
  }
1774
1757
  const kantsu = {
1775
1758
  type: MentsuType.Kantsu,
@@ -1780,7 +1763,7 @@ function parseMentsuFromExtendedMspz(block) {
1780
1763
  return kantsu;
1781
1764
  } else if (count === 3 && isAllSame) {
1782
1765
  if (!isTuple3(ids)) {
1783
- throw new Error("Internal Error: ids length check mismatch");
1766
+ throw new MspzParseError("Internal Error: ids length check mismatch");
1784
1767
  }
1785
1768
  const koutsu = {
1786
1769
  type: MentsuType.Koutsu,
@@ -1791,7 +1774,7 @@ function parseMentsuFromExtendedMspz(block) {
1791
1774
  return koutsu;
1792
1775
  } else if (count === 3) {
1793
1776
  if (!isTuple3(ids)) {
1794
- throw new Error("Internal Error: ids length check mismatch");
1777
+ throw new MspzParseError("Internal Error: ids length check mismatch");
1795
1778
  }
1796
1779
  const shuntsu = {
1797
1780
  type: MentsuType.Shuntsu,
@@ -1801,13 +1784,13 @@ function parseMentsuFromExtendedMspz(block) {
1801
1784
  };
1802
1785
  return shuntsu;
1803
1786
  }
1804
- throw new Error(
1787
+ throw new MspzParseError(
1805
1788
  `Invalid Mentsu specification: ${block} (must be 3 or 4 tiles)`
1806
1789
  );
1807
1790
  }
1808
1791
  function asMspz(input) {
1809
1792
  if (!isMspz(input)) {
1810
- throw new Error(`Invalid MSPZ string: ${input}`);
1793
+ throw new MspzParseError(`Invalid MSPZ string: ${input}`);
1811
1794
  }
1812
1795
  return input;
1813
1796
  }
@@ -1938,7 +1921,7 @@ const VALID_FU_VALUES = [
1938
1921
  function toFu(value) {
1939
1922
  const fu = VALID_FU_VALUES.find((f) => f === value);
1940
1923
  if (fu === void 0) {
1941
- throw new Error(`Invalid fu value: ${value}`);
1924
+ throw new MahjongError(`Invalid fu value: ${value}`);
1942
1925
  }
1943
1926
  return fu;
1944
1927
  }
@@ -2108,7 +2091,7 @@ function getLimitBasePoints(level) {
2108
2091
  case ScoreLevel.Mangan:
2109
2092
  return SCORE_BASE_MANGAN;
2110
2093
  case ScoreLevel.Normal:
2111
- return null;
2094
+ return void 0;
2112
2095
  }
2113
2096
  }
2114
2097
  function createScoreContext(tehai, config) {
@@ -2126,7 +2109,7 @@ function createScoreContext(tehai, config) {
2126
2109
  function calculateScoreForTehai(tehai, config) {
2127
2110
  const context = createScoreContext(tehai, config);
2128
2111
  const structuralInterpretations = getHouraStructures(tehai);
2129
- let bestResult = null;
2112
+ let bestResult = void 0;
2130
2113
  let maxTotalPoints = -1;
2131
2114
  for (const hand of structuralInterpretations) {
2132
2115
  const yakuResult = detectYakuForStructure(hand, context);
@@ -2203,11 +2186,14 @@ export {
2203
2186
  MahjongArgumentError,
2204
2187
  MahjongError,
2205
2188
  MentsuType,
2189
+ MspzParseError,
2206
2190
  NoYakuError,
2207
2191
  ShoushaiError,
2208
2192
  Tacha,
2209
2193
  TahaiError,
2210
2194
  YAOCHU_KIND_IDS,
2195
+ assertTehai13,
2196
+ assertTehai14,
2211
2197
  calculateScoreForTehai,
2212
2198
  calculateShanten,
2213
2199
  classifyMachi,