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.
- {mahjong-2.0.0.dev2/mahjong.egg-info → mahjong-2.1.0}/PKG-INFO +10 -5
- {mahjong-2.0.0.dev2 → mahjong-2.1.0}/README.md +3 -3
- {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong/__init__.py +25 -6
- mahjong-2.1.0/mahjong/hand_calculating/__init__.py +23 -0
- {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong/hand_calculating/fu.py +1 -1
- {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong/hand_calculating/hand.py +150 -30
- mahjong-2.1.0/mahjong/hand_calculating/hand_config.py +345 -0
- mahjong-2.1.0/mahjong/hand_calculating/hand_response.py +125 -0
- {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong/hand_calculating/scores.py +133 -41
- mahjong-2.1.0/mahjong/hand_calculating/yaku.py +87 -0
- {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong/hand_calculating/yaku_config.py +110 -55
- {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong/hand_calculating/yaku_list/__init__.py +12 -0
- {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong/hand_calculating/yaku_list/aka_dora.py +3 -3
- {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong/hand_calculating/yaku_list/chankan.py +2 -4
- {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong/hand_calculating/yaku_list/chantai.py +4 -2
- {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong/hand_calculating/yaku_list/chiitoitsu.py +2 -3
- {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong/hand_calculating/yaku_list/chinitsu.py +2 -3
- {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong/hand_calculating/yaku_list/chun.py +2 -3
- {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong/hand_calculating/yaku_list/daburu_open_riichi.py +2 -4
- {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong/hand_calculating/yaku_list/daburu_riichi.py +2 -4
- {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong/hand_calculating/yaku_list/dora.py +4 -0
- {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong/hand_calculating/yaku_list/haitei.py +2 -4
- {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong/hand_calculating/yaku_list/haku.py +2 -3
- {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong/hand_calculating/yaku_list/hatsu.py +2 -3
- {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong/hand_calculating/yaku_list/honitsu.py +2 -3
- {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong/hand_calculating/yaku_list/honroto.py +2 -3
- {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong/hand_calculating/yaku_list/houtei.py +2 -4
- {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong/hand_calculating/yaku_list/iipeiko.py +2 -3
- {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong/hand_calculating/yaku_list/ippatsu.py +2 -4
- {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong/hand_calculating/yaku_list/ittsu.py +2 -3
- {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong/hand_calculating/yaku_list/junchan.py +4 -3
- {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong/hand_calculating/yaku_list/nagashi_mangan.py +2 -4
- {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong/hand_calculating/yaku_list/open_riichi.py +3 -0
- {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong/hand_calculating/yaku_list/pinfu.py +2 -3
- {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong/hand_calculating/yaku_list/renhou.py +2 -4
- {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong/hand_calculating/yaku_list/riichi.py +3 -0
- {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong/hand_calculating/yaku_list/rinshan.py +2 -4
- {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong/hand_calculating/yaku_list/round_wind_east.py +8 -3
- {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong/hand_calculating/yaku_list/round_wind_north.py +8 -3
- {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong/hand_calculating/yaku_list/round_wind_south.py +8 -3
- {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong/hand_calculating/yaku_list/round_wind_west.py +8 -3
- {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong/hand_calculating/yaku_list/ryanpeiko.py +2 -3
- {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong/hand_calculating/yaku_list/sanankou.py +10 -3
- {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong/hand_calculating/yaku_list/sankantsu.py +8 -3
- {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong/hand_calculating/yaku_list/sanshoku.py +2 -3
- {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong/hand_calculating/yaku_list/sanshoku_douko.py +2 -3
- {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong/hand_calculating/yaku_list/seat_wind_east.py +8 -3
- {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong/hand_calculating/yaku_list/seat_wind_north.py +8 -3
- {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong/hand_calculating/yaku_list/seat_wind_south.py +8 -3
- {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong/hand_calculating/yaku_list/seat_wind_west.py +8 -3
- {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong/hand_calculating/yaku_list/shosangen.py +2 -3
- {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong/hand_calculating/yaku_list/tanyao.py +2 -3
- {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong/hand_calculating/yaku_list/toitoi.py +2 -3
- {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong/hand_calculating/yaku_list/tsumo.py +2 -4
- {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong/hand_calculating/yaku_list/ura_dora.py +4 -0
- {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong/hand_calculating/yaku_list/yakuman/__init__.py +9 -0
- {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong/hand_calculating/yaku_list/yakuman/chiihou.py +3 -0
- {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong/hand_calculating/yaku_list/yakuman/chinroto.py +3 -5
- {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong/hand_calculating/yaku_list/yakuman/chuuren_poutou.py +2 -3
- {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong/hand_calculating/yaku_list/yakuman/daburu_chuuren_poutou.py +3 -1
- {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong/hand_calculating/yaku_list/yakuman/daburu_kokushi.py +3 -1
- {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong/hand_calculating/yaku_list/yakuman/daichisei.py +2 -3
- {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong/hand_calculating/yaku_list/yakuman/daisangen.py +2 -3
- {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong/hand_calculating/yaku_list/yakuman/daisharin.py +18 -4
- {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong/hand_calculating/yaku_list/yakuman/daisuushi.py +2 -3
- {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong/hand_calculating/yaku_list/yakuman/kokushi.py +10 -2
- {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong/hand_calculating/yaku_list/yakuman/paarenchan.py +4 -4
- {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong/hand_calculating/yaku_list/yakuman/renhou_yakuman.py +2 -4
- {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong/hand_calculating/yaku_list/yakuman/ryuisou.py +2 -3
- {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong/hand_calculating/yaku_list/yakuman/sashikomi.py +2 -4
- {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong/hand_calculating/yaku_list/yakuman/shosuushi.py +2 -3
- {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong/hand_calculating/yaku_list/yakuman/suuankou.py +9 -3
- {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong/hand_calculating/yaku_list/yakuman/suuankou_tanki.py +3 -0
- {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong/hand_calculating/yaku_list/yakuman/suukantsu.py +8 -3
- {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong/hand_calculating/yaku_list/yakuman/tenhou.py +2 -4
- {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong/hand_calculating/yaku_list/yakuman/tsuisou.py +2 -8
- {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong/shanten.py +57 -21
- {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong/tile.py +2 -0
- {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong/utils.py +28 -4
- {mahjong-2.0.0.dev2 → mahjong-2.1.0/mahjong.egg-info}/PKG-INFO +10 -5
- {mahjong-2.0.0.dev2 → mahjong-2.1.0}/pyproject.toml +22 -16
- mahjong-2.0.0.dev2/mahjong/hand_calculating/__init__.py +0 -0
- mahjong-2.0.0.dev2/mahjong/hand_calculating/hand_config.py +0 -146
- mahjong-2.0.0.dev2/mahjong/hand_calculating/hand_response.py +0 -54
- mahjong-2.0.0.dev2/mahjong/hand_calculating/yaku.py +0 -27
- {mahjong-2.0.0.dev2 → mahjong-2.1.0}/LICENSE.txt +0 -0
- {mahjong-2.0.0.dev2 → mahjong-2.1.0}/MANIFEST.in +0 -0
- {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong/agari.py +0 -0
- {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong/constants.py +0 -0
- {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong/hand_calculating/divider.py +0 -0
- {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong/meld.py +0 -0
- {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong/py.typed +0 -0
- {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong.egg-info/SOURCES.txt +0 -0
- {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong.egg-info/dependency_links.txt +0 -0
- {mahjong-2.0.0.dev2 → mahjong-2.1.0}/mahjong.egg-info/top_level.txt +0 -0
- {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.
|
|
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
|
[](https://github.com/MahjongRepository/mahjong/actions/workflows/lint_and_test.yml)
|
|
32
37
|
[](https://mahjongrepository.github.io/mahjong)
|
|
33
38
|
|
|
34
|
-
|
|
39
|
+
Japanese riichi mahjong hand calculator library.
|
|
35
40
|
|
|
36
|
-
|
|
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
|
|
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
|
[](https://github.com/MahjongRepository/mahjong/actions/workflows/lint_and_test.yml)
|
|
8
8
|
[](https://mahjongrepository.github.io/mahjong)
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
Japanese riichi mahjong hand calculator library.
|
|
11
11
|
|
|
12
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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,
|
|
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
|
-
|
|
37
|
-
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
|
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)
|
|
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(
|
|
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
|
|
668
|
+
calculated_hands.sort(key=_hand_priority, reverse=True)
|
|
546
669
|
# correctly sort expensive hands by fu details
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
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"]
|