mahjong 2.0.0.dev2__tar.gz → 2.1.0__tar.gz

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 (96) hide show
  1. {mahjong-2.0.0.dev2/mahjong.egg-info → mahjong-2.1.0}/PKG-INFO +10 -5
  2. {mahjong-2.0.0.dev2 → mahjong-2.1.0}/README.md +3 -3
  3. {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong/__init__.py +25 -6
  4. mahjong-2.1.0/mahjong/hand_calculating/__init__.py +23 -0
  5. {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong/hand_calculating/fu.py +1 -1
  6. {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong/hand_calculating/hand.py +150 -30
  7. mahjong-2.1.0/mahjong/hand_calculating/hand_config.py +345 -0
  8. mahjong-2.1.0/mahjong/hand_calculating/hand_response.py +125 -0
  9. {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong/hand_calculating/scores.py +133 -41
  10. mahjong-2.1.0/mahjong/hand_calculating/yaku.py +87 -0
  11. {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong/hand_calculating/yaku_config.py +110 -55
  12. {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong/hand_calculating/yaku_list/__init__.py +12 -0
  13. {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong/hand_calculating/yaku_list/aka_dora.py +3 -3
  14. {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong/hand_calculating/yaku_list/chankan.py +2 -4
  15. {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong/hand_calculating/yaku_list/chantai.py +4 -2
  16. {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong/hand_calculating/yaku_list/chiitoitsu.py +2 -3
  17. {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong/hand_calculating/yaku_list/chinitsu.py +2 -3
  18. {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong/hand_calculating/yaku_list/chun.py +2 -3
  19. {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong/hand_calculating/yaku_list/daburu_open_riichi.py +2 -4
  20. {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong/hand_calculating/yaku_list/daburu_riichi.py +2 -4
  21. {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong/hand_calculating/yaku_list/dora.py +4 -0
  22. {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong/hand_calculating/yaku_list/haitei.py +2 -4
  23. {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong/hand_calculating/yaku_list/haku.py +2 -3
  24. {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong/hand_calculating/yaku_list/hatsu.py +2 -3
  25. {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong/hand_calculating/yaku_list/honitsu.py +2 -3
  26. {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong/hand_calculating/yaku_list/honroto.py +2 -3
  27. {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong/hand_calculating/yaku_list/houtei.py +2 -4
  28. {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong/hand_calculating/yaku_list/iipeiko.py +2 -3
  29. {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong/hand_calculating/yaku_list/ippatsu.py +2 -4
  30. {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong/hand_calculating/yaku_list/ittsu.py +2 -3
  31. {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong/hand_calculating/yaku_list/junchan.py +4 -3
  32. {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong/hand_calculating/yaku_list/nagashi_mangan.py +2 -4
  33. {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong/hand_calculating/yaku_list/open_riichi.py +3 -0
  34. {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong/hand_calculating/yaku_list/pinfu.py +2 -3
  35. {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong/hand_calculating/yaku_list/renhou.py +2 -4
  36. {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong/hand_calculating/yaku_list/riichi.py +3 -0
  37. {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong/hand_calculating/yaku_list/rinshan.py +2 -4
  38. {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong/hand_calculating/yaku_list/round_wind_east.py +8 -3
  39. {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong/hand_calculating/yaku_list/round_wind_north.py +8 -3
  40. {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong/hand_calculating/yaku_list/round_wind_south.py +8 -3
  41. {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong/hand_calculating/yaku_list/round_wind_west.py +8 -3
  42. {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong/hand_calculating/yaku_list/ryanpeiko.py +2 -3
  43. {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong/hand_calculating/yaku_list/sanankou.py +10 -3
  44. {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong/hand_calculating/yaku_list/sankantsu.py +8 -3
  45. {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong/hand_calculating/yaku_list/sanshoku.py +2 -3
  46. {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong/hand_calculating/yaku_list/sanshoku_douko.py +2 -3
  47. {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong/hand_calculating/yaku_list/seat_wind_east.py +8 -3
  48. {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong/hand_calculating/yaku_list/seat_wind_north.py +8 -3
  49. {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong/hand_calculating/yaku_list/seat_wind_south.py +8 -3
  50. {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong/hand_calculating/yaku_list/seat_wind_west.py +8 -3
  51. {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong/hand_calculating/yaku_list/shosangen.py +2 -3
  52. {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong/hand_calculating/yaku_list/tanyao.py +2 -3
  53. {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong/hand_calculating/yaku_list/toitoi.py +2 -3
  54. {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong/hand_calculating/yaku_list/tsumo.py +2 -4
  55. {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong/hand_calculating/yaku_list/ura_dora.py +4 -0
  56. {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong/hand_calculating/yaku_list/yakuman/__init__.py +9 -0
  57. {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong/hand_calculating/yaku_list/yakuman/chiihou.py +3 -0
  58. {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong/hand_calculating/yaku_list/yakuman/chinroto.py +3 -5
  59. {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong/hand_calculating/yaku_list/yakuman/chuuren_poutou.py +2 -3
  60. {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong/hand_calculating/yaku_list/yakuman/daburu_chuuren_poutou.py +3 -1
  61. {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong/hand_calculating/yaku_list/yakuman/daburu_kokushi.py +3 -1
  62. {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong/hand_calculating/yaku_list/yakuman/daichisei.py +2 -3
  63. {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong/hand_calculating/yaku_list/yakuman/daisangen.py +2 -3
  64. {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong/hand_calculating/yaku_list/yakuman/daisharin.py +18 -4
  65. {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong/hand_calculating/yaku_list/yakuman/daisuushi.py +2 -3
  66. {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong/hand_calculating/yaku_list/yakuman/kokushi.py +10 -2
  67. {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong/hand_calculating/yaku_list/yakuman/paarenchan.py +4 -4
  68. {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong/hand_calculating/yaku_list/yakuman/renhou_yakuman.py +2 -4
  69. {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong/hand_calculating/yaku_list/yakuman/ryuisou.py +2 -3
  70. {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong/hand_calculating/yaku_list/yakuman/sashikomi.py +2 -4
  71. {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong/hand_calculating/yaku_list/yakuman/shosuushi.py +2 -3
  72. {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong/hand_calculating/yaku_list/yakuman/suuankou.py +9 -3
  73. {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong/hand_calculating/yaku_list/yakuman/suuankou_tanki.py +3 -0
  74. {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong/hand_calculating/yaku_list/yakuman/suukantsu.py +8 -3
  75. {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong/hand_calculating/yaku_list/yakuman/tenhou.py +2 -4
  76. {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong/hand_calculating/yaku_list/yakuman/tsuisou.py +2 -8
  77. {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong/shanten.py +57 -21
  78. {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong/tile.py +2 -0
  79. {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong/utils.py +28 -4
  80. {mahjong-2.0.0.dev2 → mahjong-2.1.0/mahjong.egg-info}/PKG-INFO +10 -5
  81. {mahjong-2.0.0.dev2 → mahjong-2.1.0}/pyproject.toml +22 -16
  82. mahjong-2.0.0.dev2/mahjong/hand_calculating/__init__.py +0 -0
  83. mahjong-2.0.0.dev2/mahjong/hand_calculating/hand_config.py +0 -146
  84. mahjong-2.0.0.dev2/mahjong/hand_calculating/hand_response.py +0 -54
  85. mahjong-2.0.0.dev2/mahjong/hand_calculating/yaku.py +0 -27
  86. {mahjong-2.0.0.dev2 → mahjong-2.1.0}/LICENSE.txt +0 -0
  87. {mahjong-2.0.0.dev2 → mahjong-2.1.0}/MANIFEST.in +0 -0
  88. {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong/agari.py +0 -0
  89. {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong/constants.py +0 -0
  90. {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong/hand_calculating/divider.py +0 -0
  91. {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong/meld.py +0 -0
  92. {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong/py.typed +0 -0
  93. {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong.egg-info/SOURCES.txt +0 -0
  94. {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong.egg-info/dependency_links.txt +0 -0
  95. {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong.egg-info/top_level.txt +0 -0
  96. {mahjong-2.0.0.dev2 → mahjong-2.1.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mahjong
3
- Version: 2.0.0.dev2
3
+ Version: 2.1.0
4
4
  Summary: Mahjong hands calculation
5
5
  Author-email: Alexey Lisikhin <alexey@nihisil.com>
6
6
  Maintainer: Apricot S.
@@ -9,14 +9,19 @@ Project-URL: Homepage, https://github.com/MahjongRepository/mahjong
9
9
  Project-URL: Documentation, https://mahjongrepository.github.io/mahjong
10
10
  Project-URL: Wiki, https://github.com/MahjongRepository/mahjong/wiki
11
11
  Classifier: Development Status :: 5 - Production/Stable
12
- Classifier: Environment :: Console
13
12
  Classifier: Intended Audience :: Developers
14
13
  Classifier: Programming Language :: Python
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3 :: Only
15
16
  Classifier: Programming Language :: Python :: 3.10
16
17
  Classifier: Programming Language :: Python :: 3.11
17
18
  Classifier: Programming Language :: Python :: 3.12
18
19
  Classifier: Programming Language :: Python :: 3.13
19
20
  Classifier: Programming Language :: Python :: 3.14
21
+ Classifier: Programming Language :: Python :: 3.15
22
+ Classifier: Topic :: Games/Entertainment :: Board Games
23
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
24
+ Classifier: Typing :: Typed
20
25
  Requires-Python: >=3.10
21
26
  Description-Content-Type: text/markdown
22
27
  License-File: LICENSE.txt
@@ -31,11 +36,11 @@ Dynamic: license-file
31
36
  [![Linters and tests](https://github.com/MahjongRepository/mahjong/actions/workflows/lint_and_test.yml/badge.svg)](https://github.com/MahjongRepository/mahjong/actions/workflows/lint_and_test.yml)
32
37
  [![API Docs](https://img.shields.io/badge/api-master-yellow.svg)](https://mahjongrepository.github.io/mahjong)
33
38
 
34
- This library can calculate hand cost (han, fu with details, yaku, and scores) for riichi mahjong (Japanese version).
39
+ Japanese riichi mahjong hand calculator library.
35
40
 
36
- Also calculating of shanten is supported.
41
+ Compute hand cost (han, fu, yaku, and scores), validate winning hands, and calculate shanten (minimum tiles to tenpai).
37
42
 
38
- The code was validated on tenhou.net phoenix replays in total on **26,148,038 hands**.
43
+ The code was validated on tenhou.net phoenix replays: **26,148,050** four-player and **8,406,421** three-player winning hands.
39
44
 
40
45
  So, we can say that our hand calculator works the same way that tenhou.net hand calculation.
41
46
 
@@ -7,11 +7,11 @@
7
7
  [![Linters and tests](https://github.com/MahjongRepository/mahjong/actions/workflows/lint_and_test.yml/badge.svg)](https://github.com/MahjongRepository/mahjong/actions/workflows/lint_and_test.yml)
8
8
  [![API Docs](https://img.shields.io/badge/api-master-yellow.svg)](https://mahjongrepository.github.io/mahjong)
9
9
 
10
- This library can calculate hand cost (han, fu with details, yaku, and scores) for riichi mahjong (Japanese version).
10
+ Japanese riichi mahjong hand calculator library.
11
11
 
12
- Also calculating of shanten is supported.
12
+ Compute hand cost (han, fu, yaku, and scores), validate winning hands, and calculate shanten (minimum tiles to tenpai).
13
13
 
14
- The code was validated on tenhou.net phoenix replays in total on **26,148,038 hands**.
14
+ The code was validated on tenhou.net phoenix replays: **26,148,050** four-player and **8,406,421** three-player winning hands.
15
15
 
16
16
  So, we can say that our hand calculator works the same way that tenhou.net hand calculation.
17
17
 
@@ -1,5 +1,27 @@
1
1
  """
2
- _summary_.
2
+ Japanese riichi mahjong hand calculator library.
3
+
4
+ Compute hand cost (han, fu, yaku, and scores), validate winning hands, and calculate shanten (minimum tiles to tenpai).
5
+
6
+ .. rubric:: Main entry points
7
+
8
+ * :class:`~mahjong.hand_calculating.hand.HandCalculator` — estimate hand value (han, fu, cost,
9
+ and yaku list) given tiles, win tile, melds, and dora indicators.
10
+ * :class:`~mahjong.agari.Agari` — check whether tiles form a complete (winning) hand structure.
11
+ * :class:`~mahjong.shanten.Shanten` — calculate how many tiles away a hand is from winning.
12
+
13
+ .. rubric:: Tile and meld utilities
14
+
15
+ * :class:`~mahjong.tile.TilesConverter` — convert between mpsz-notation strings, 34-format count
16
+ arrays, and 136-format tile index lists.
17
+ * :class:`~mahjong.meld.Meld` — represent declared melds (chi, pon, kan).
18
+
19
+ .. rubric:: Configuration and results
20
+
21
+ * :class:`~mahjong.hand_calculating.hand_config.HandConfig` — configure win conditions (tsumo/ron),
22
+ wind context, and optional rule variants (open tanyao, aka dora, double yakuman, etc.).
23
+ * :class:`~mahjong.hand_calculating.hand_response.HandResponse` — structured result containing
24
+ han, fu, fu details, cost breakdown, and yaku list.
3
25
 
4
26
  Tile representation
5
27
  ###################
@@ -30,11 +52,8 @@ Honors (`z`) use ranks to distinguish individual tiles:
30
52
  * `6` - Green dragon (發, hatsu)
31
53
  * `7` - Red dragon (中, chun)
32
54
 
33
- Multiple tiles may be concatenated without separators to form a string that represents a collection. For example:
34
-
35
- ``123m406p789s11555z``
36
-
37
- is equivalent to ``1m2m3m4p0p6p7s8s9s1z1z5z5z5z``.
55
+ Multiple tiles may be concatenated without separators to form a string that represents a collection
56
+ (e.g., ``123m406p789s11555z`` is equivalent to ``1m2m3m4p0p6p7s8s9s1z1z5z5z5z``).
38
57
 
39
58
  34-format
40
59
  ---------
@@ -0,0 +1,23 @@
1
+ """
2
+ Hand value calculation pipeline.
3
+
4
+ This subpackage evaluates a winning hand and produces its han, fu, yaku list,
5
+ and payment amounts. The calculation flows through several stages:
6
+
7
+ 1. :class:`~mahjong.hand_calculating.divider.HandDivider` decomposes the hand into all
8
+ valid block combinations (4 melds + 1 pair, or 7 pairs for chiitoitsu).
9
+ 2. :class:`~mahjong.hand_calculating.yaku.Yaku` subclasses test each decomposition for
10
+ matching yaku patterns (e.g., tanyao, pinfu, honitsu).
11
+ 3. :class:`~mahjong.hand_calculating.fu.FuCalculator` computes minipoints from the meld
12
+ structure, wait type, pair, and winning method.
13
+ 4. :class:`~mahjong.hand_calculating.scores.ScoresCalculator` converts han and fu into
14
+ payment amounts, accounting for honba and kyoutaku bonuses.
15
+
16
+ :class:`~mahjong.hand_calculating.hand.HandCalculator` orchestrates the full pipeline and
17
+ returns a :class:`~mahjong.hand_calculating.hand_response.HandResponse` with the
18
+ highest-scoring result.
19
+
20
+ Behavior is controlled by :class:`~mahjong.hand_calculating.hand_config.HandConfig`, which
21
+ specifies win conditions (tsumo/ron), wind context, and optional rule variants such as
22
+ open tanyao, aka dora, and double yakuman.
23
+ """
@@ -169,7 +169,7 @@ class FuCalculator:
169
169
  as tile indices in 34-format
170
170
  :param config: hand configuration with win method and optional rule settings
171
171
  :param valued_tiles: tile indices in 34-format for tiles that grant pair fu
172
- (dragons, player wind, round wind); pass the same index twice for double-valued
172
+ (dragons, seat wind, round wind); pass the same index twice for double-valued
173
173
  :param melds: declared melds (chi, pon, kan)
174
174
  :return: tuple of (fu component list, total fu rounded up to nearest 10)
175
175
  """
@@ -1,7 +1,7 @@
1
1
  from collections.abc import Collection
2
2
  from typing import TypedDict
3
3
 
4
- from mahjong.constants import AKA_DORAS, CHUN, HAKU, HATSU
4
+ from mahjong.constants import AKA_DORAS, CHUN, HAKU, HATSU, NORTH
5
5
  from mahjong.hand_calculating.divider import HandDivider
6
6
  from mahjong.hand_calculating.fu import FuCalculator, FuDetail
7
7
  from mahjong.hand_calculating.hand_config import HandConfig
@@ -22,36 +22,85 @@ class _CalculatedHand(TypedDict):
22
22
  fu_details: list[FuDetail]
23
23
 
24
24
 
25
+ def _hand_priority(hand: _CalculatedHand) -> tuple[int, bool, int, int]:
26
+ """
27
+ Rank a hand decomposition, the most expensive one is used.
28
+
29
+ The highest score wins, and on an equal score a real yakuman wins over counted (kazoe)
30
+ yakuman built from regular yaku, as on Tenhou.
31
+ """
32
+ total = hand["cost"]["total"] if hand["cost"] else -1
33
+ has_yakuman = any(yaku.is_yakuman for yaku in hand["hand_yaku"])
34
+ return total, has_yakuman, hand["han"], hand["fu"]
35
+
36
+
25
37
  # suit bitmask: sou=1, pin=2, man=4
26
38
  _ALL_SUITS_MASK = 7
27
39
 
28
- _DEFAULT_CONFIG = HandConfig()
29
-
30
40
 
31
41
  class HandCalculator:
42
+ """
43
+ Hand value estimator.
44
+
45
+ Evaluate a winning hand's han, fu, yaku, and payment amounts. Accepts tiles in
46
+ 136-format, an optional :class:`~mahjong.hand_calculating.hand_config.HandConfig` for
47
+ win conditions and rule variants, and optional melds and dora/ura-dora indicators. When multiple
48
+ valid decompositions exist, the highest-scoring result is returned.
49
+ """
50
+
51
+ # basic hand validation
32
52
  ERR_NO_WINNING_TILE = "winning_tile_not_in_hand"
53
+ """``win_tile`` index is not present in the ``tiles`` array."""
54
+ ERR_HAND_NOT_WINNING = "hand_not_winning"
55
+ """No valid decomposition exists; the hand is not a winning hand."""
56
+ ERR_NO_YAKU = "no_yaku"
57
+ """Hand can be decomposed but has zero han (no yaku applies)."""
58
+
59
+ # riichi constraints
33
60
  ERR_OPEN_HAND_RIICHI = "open_hand_riichi_not_allowed"
61
+ """Riichi declared on an open hand (open melds present)."""
34
62
  ERR_OPEN_HAND_DABURI = "open_hand_daburi_not_allowed"
63
+ """Double riichi declared on an open hand."""
35
64
  ERR_IPPATSU_WITHOUT_RIICHI = "ippatsu_without_riichi_not_allowed"
36
- ERR_HAND_NOT_WINNING = "hand_not_winning"
37
- ERR_NO_YAKU = "no_yaku"
65
+ """Ippatsu claimed without riichi or double riichi."""
66
+
67
+ # win-condition conflicts
38
68
  ERR_CHANKAN_WITH_TSUMO = "chankan_with_tsumo_not_allowed"
69
+ """Chankan (robbing a kan) requires a ron win."""
39
70
  ERR_RINSHAN_WITHOUT_TSUMO = "rinshan_without_tsumo_not_allowed"
71
+ """Rinshan kaihou (win after kan) requires a tsumo win."""
40
72
  ERR_HAITEI_WITHOUT_TSUMO = "haitei_without_tsumo_not_allowed"
73
+ """Haitei raoyue (last-tile draw) requires a tsumo win."""
41
74
  ERR_HOUTEI_WITH_TSUMO = "houtei_with_tsumo_not_allowed"
75
+ """Houtei raoyui (last-tile discard) requires a ron win."""
42
76
  ERR_HAITEI_WITH_RINSHAN = "haitei_with_rinshan_not_allowed"
77
+ """Haitei and rinshan are mutually exclusive (different last-tile sources)."""
43
78
  ERR_HOUTEI_WITH_CHANKAN = "houtei_with_chankan_not_allowed"
79
+ """Houtei and chankan are mutually exclusive."""
80
+
81
+ # tenhou (blessing of heaven) constraints
44
82
  ERR_TENHOU_NOT_AS_DEALER = "tenhou_not_as_dealer_not_allowed"
83
+ """Tenhou is exclusive to the dealer (East player)."""
45
84
  ERR_TENHOU_WITHOUT_TSUMO = "tenhou_without_tsumo_not_allowed"
85
+ """Tenhou requires a tsumo win on the dealer's first draw."""
46
86
  ERR_TENHOU_WITH_MELD = "tenhou_with_meld_not_allowed"
87
+ """Tenhou requires a closed hand with no declared melds."""
88
+
89
+ # chiihou (blessing of earth) constraints
47
90
  ERR_CHIIHOU_AS_DEALER = "chiihou_as_dealer_not_allowed"
91
+ """Chiihou is exclusive to non-dealer players."""
48
92
  ERR_CHIIHOU_WITHOUT_TSUMO = "chiihou_without_tsumo_not_allowed"
93
+ """Chiihou requires a tsumo win on the player's first draw."""
49
94
  ERR_CHIIHOU_WITH_MELD = "chiihou_with_meld_not_allowed"
95
+ """Chiihou requires a closed hand with no declared melds."""
96
+
97
+ # renhou (blessing of man) constraints
50
98
  ERR_RENHOU_AS_DEALER = "renhou_as_dealer_not_allowed"
99
+ """Renhou is exclusive to non-dealer players."""
51
100
  ERR_RENHOU_WITH_TSUMO = "renhou_with_tsumo_not_allowed"
101
+ """Renhou requires a ron win before the player's first draw."""
52
102
  ERR_RENHOU_WITH_MELD = "renhou_with_meld_not_allowed"
53
-
54
- # more possible errors, like tenhou and haitei can't be together (so complicated :<)
103
+ """Renhou requires a closed hand with no declared melds."""
55
104
 
56
105
  @staticmethod
57
106
  def estimate_hand_value(
@@ -64,16 +113,84 @@ class HandCalculator:
64
113
  ura_dora_indicators: Collection[int] | None = None,
65
114
  ) -> HandResponse:
66
115
  """
67
- :param tiles: array with 14 tiles in 136-tile format
68
- :param win_tile: 136-tile format tile that caused win (ron or tsumo)
69
- :param melds: array with Meld objects
70
- :param dora_indicators: array of tiles in 136-tile format
71
- :param config: HandConfig object
72
- :param scores_calculator_factory: ScoresCalculator class or subclass
73
- :param ura_dora_indicators: array of tiles in 136-tile format (only counted with riichi)
74
- :return: HandResponse object
116
+ Estimate the point value of a winning hand.
117
+
118
+ Validate the hand and win conditions, decompose the hand into all possible
119
+ block combinations, evaluate yaku and fu for each decomposition, and return
120
+ the highest-scoring result as a :class:`~mahjong.hand_calculating.hand_response.HandResponse`.
121
+
122
+ Basic closed ron with tanyao:
123
+
124
+ >>> from mahjong.hand_calculating.hand import HandCalculator
125
+ >>> from mahjong.tile import TilesConverter
126
+ >>> tiles = TilesConverter.string_to_136_array(man="22444", pin="333567", sou="444")
127
+ >>> win_tile = TilesConverter.string_to_136_array(sou="4")[0]
128
+ >>> result = HandCalculator.estimate_hand_value(tiles, win_tile)
129
+ >>> result.han
130
+ 1
131
+ >>> result.fu
132
+ 40
133
+ >>> result.cost["main"]
134
+ 1300
135
+
136
+ Tsumo win with riichi and pinfu:
137
+
138
+ >>> from mahjong.hand_calculating.hand_config import HandConfig
139
+ >>> tiles = TilesConverter.string_to_136_array(man="234789", pin="12345666")
140
+ >>> win_tile = TilesConverter.string_to_136_array(pin="6")[0]
141
+ >>> config = HandConfig(is_tsumo=True, is_riichi=True)
142
+ >>> result = HandCalculator.estimate_hand_value(tiles, win_tile, config=config)
143
+ >>> result.han
144
+ 3
145
+ >>> result.fu
146
+ 20
147
+ >>> result.cost["main"]
148
+ 1300
149
+ >>> result.cost["additional"]
150
+ 700
151
+
152
+ Dealer tsumo with riichi and ippatsu:
153
+
154
+ >>> from mahjong.constants import EAST
155
+ >>> config = HandConfig(is_tsumo=True, is_riichi=True, is_ippatsu=True, player_wind=EAST)
156
+ >>> result = HandCalculator.estimate_hand_value(tiles, win_tile, config=config)
157
+ >>> result.error is None
158
+ True
159
+
160
+ Open hand with melds and dora:
161
+
162
+ >>> from mahjong.meld import Meld
163
+ >>> tiles = TilesConverter.string_to_136_array(man="234567", pin="22", sou="234", honors="555")
164
+ >>> win_tile = TilesConverter.string_to_136_array(man="7")[0]
165
+ >>> melds = [Meld(meld_type=Meld.PON, tiles=TilesConverter.string_to_136_array(honors="555"))]
166
+ >>> dora_indicators = [TilesConverter.string_to_136_array(man="6")[0]]
167
+ >>> result = HandCalculator.estimate_hand_value(tiles, win_tile, melds=melds, dora_indicators=dora_indicators)
168
+ >>> result.han
169
+ 2
170
+
171
+ Invalid hand returns an error:
172
+
173
+ >>> tiles = TilesConverter.string_to_136_array(man="12345")
174
+ >>> win_tile = TilesConverter.string_to_136_array(man="1")[0]
175
+ >>> result = HandCalculator.estimate_hand_value(tiles, win_tile)
176
+ >>> result.error == HandCalculator.ERR_HAND_NOT_WINNING
177
+ True
178
+
179
+ :param tiles: hand tiles in 136-format (14 tiles including the winning tile;
180
+ 15 with one kan, 16 with two, etc.)
181
+ :param win_tile: the winning tile index in 136-format (must be present in ``tiles``)
182
+ :param melds: declared melds (:class:`~mahjong.meld.Meld` objects for chi, pon, kan)
183
+ :param dora_indicators: dora indicator tile indices in 136-format
184
+ :param config: hand configuration with win conditions, wind context, and optional rules;
185
+ defaults to a closed ron with no special conditions
186
+ :param scores_calculator_factory: scoring calculator class; pass
187
+ :class:`~mahjong.hand_calculating.scores.Aotenjou` for aotenjou (limitless) scoring
188
+ :param ura_dora_indicators: ura dora indicator tile indices in 136-format
189
+ (counted only when riichi or double riichi is declared)
190
+ :return: :class:`~mahjong.hand_calculating.hand_response.HandResponse` with scoring
191
+ details on success, or with :attr:`~mahjong.hand_calculating.hand_response.HandResponse.error`
192
+ set on failure
75
193
  """
76
-
77
194
  if not melds:
78
195
  melds = []
79
196
 
@@ -83,9 +200,9 @@ class HandCalculator:
83
200
  if not ura_dora_indicators:
84
201
  ura_dora_indicators = []
85
202
 
86
- config = config or _DEFAULT_CONFIG
203
+ config = config or HandConfig()
87
204
 
88
- hand_yaku = []
205
+ hand_yaku: list[Yaku] = []
89
206
  scores_calculator = scores_calculator_factory()
90
207
  tiles_34 = TilesConverter.to_34_array(tiles)
91
208
 
@@ -172,8 +289,10 @@ class HandCalculator:
172
289
  hand_options = HandDivider.divide_hand(tiles_34, melds)
173
290
 
174
291
  # precompute dora counts, invariant across all hand decompositions
175
- dora_count_map = build_dora_count_map(dora_indicators)
292
+ dora_count_map = build_dora_count_map(dora_indicators, config.options.is_three_player)
176
293
  precomputed_dora = count_dora_for_hand(tiles_34, dora_count_map)
294
+ if config.num_nuki_dora > 0:
295
+ precomputed_dora += dora_count_map.get(NORTH, 0) * config.num_nuki_dora + config.num_nuki_dora
177
296
 
178
297
  precomputed_aka_dora = 0
179
298
  if config.options.has_aka_dora:
@@ -181,8 +300,10 @@ class HandCalculator:
181
300
 
182
301
  precomputed_ura_dora = 0
183
302
  if config.is_riichi or config.is_daburu_riichi:
184
- ura_count_map = build_dora_count_map(ura_dora_indicators)
303
+ ura_count_map = build_dora_count_map(ura_dora_indicators, config.options.is_three_player)
185
304
  precomputed_ura_dora = count_dora_for_hand(tiles_34, ura_count_map)
305
+ if config.num_nuki_dora > 0:
306
+ precomputed_ura_dora += ura_count_map.get(NORTH, 0) * config.num_nuki_dora
186
307
 
187
308
  yakuhai_seat_wind_yaku = (
188
309
  config.yaku.seat_wind_east,
@@ -424,7 +545,7 @@ class HandCalculator:
424
545
  if not is_aotenjou:
425
546
  hand_yaku = yakuman_list
426
547
  else:
427
- scores_calculator.aotenjou_filter_yaku(hand_yaku, config) # ty: ignore[unresolved-attribute]
548
+ scores_calculator.aotenjou_filter_yaku(hand_yaku, config)
428
549
  yakuman_list = []
429
550
 
430
551
  # calculate han
@@ -515,7 +636,7 @@ class HandCalculator:
515
636
 
516
637
  count_of_dora = 0
517
638
  for tile in tiles_for_dora:
518
- count_of_dora += plus_dora(tile, dora_indicators)
639
+ count_of_dora += plus_dora(tile, dora_indicators, is_three_player=config.options.is_three_player)
519
640
 
520
641
  if count_of_dora:
521
642
  config.yaku.dora.han_open = count_of_dora
@@ -526,7 +647,9 @@ class HandCalculator:
526
647
  if config.is_riichi or config.is_daburu_riichi:
527
648
  count_of_ura_dora = 0
528
649
  for tile in tiles_for_dora:
529
- count_of_ura_dora += plus_dora(tile, ura_dora_indicators)
650
+ count_of_ura_dora += plus_dora(
651
+ tile, ura_dora_indicators, is_three_player=config.options.is_three_player
652
+ )
530
653
 
531
654
  if count_of_ura_dora:
532
655
  config.yaku.ura_dora.han_closed = count_of_ura_dora
@@ -542,14 +665,11 @@ class HandCalculator:
542
665
  return HandResponse(error=HandCalculator.ERR_HAND_NOT_WINNING)
543
666
 
544
667
  # find most expensive hand
545
- calculated_hands = sorted(calculated_hands, key=lambda x: (x["han"], x["fu"]), reverse=True)
668
+ calculated_hands.sort(key=_hand_priority, reverse=True)
546
669
  # correctly sort expensive hands by fu details
547
- calculated_hands = [
548
- x
549
- for x in calculated_hands
550
- if x["han"] == calculated_hands[0]["han"] and x["fu"] == calculated_hands[0]["fu"]
551
- ]
552
- calculated_hands = sorted(calculated_hands, key=lambda x: sum([y["fu"] for y in x["fu_details"]]), reverse=True)
670
+ top_priority = _hand_priority(calculated_hands[0])
671
+ calculated_hands = [x for x in calculated_hands if _hand_priority(x) == top_priority]
672
+ calculated_hands.sort(key=lambda x: sum([y["fu"] for y in x["fu_details"]]), reverse=True)
553
673
  calculated_hand = calculated_hands[0]
554
674
 
555
675
  error = calculated_hand["error"]