Skip to content

Commit 29d062f

Browse files
authored
docs: add hand config documentation (#244)
* docs: add hand config documentation * address PR comments
1 parent 3ab651e commit 29d062f

6 files changed

Lines changed: 204 additions & 16 deletions

File tree

.github/wiki/v2-English.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ It supports optional features like:
1616
| Disable or enable double yakuman (like suuanko tanki) | `has_double_yakuman` | `True` |
1717
| Upper limit for cumulative han of non-yakuman yaku (counted yakuman / counted sanbaiman / no limit) | `kazoe_limit` | `HandConstants.KAZOE_LIMITED` |
1818
| Support kiriage mangan | `kiriage` | `False` |
19-
| Allow to disable additional +2 fu in open hand (you can make 1-20 hand with that setting) | `fu_for_open_pinfu` | `True` |
19+
| Whether to add 2 fu for open pinfu-form ron (totaling 30 fu); when disabled, 1 han 20 fu hands become possible | `fu_for_open_pinfu` | `True` |
2020
| Disable or enable pinfu tsumo | `fu_for_pinfu_tsumo` | `False` |
2121
| Counting renhou as 5 han or yakuman | `renhou_as_yakuman` | `False` |
2222
| Disable or enable Daisharin yakuman | `has_daisharin` | `False` |

mahjong/hand_calculating/fu.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,7 @@ def calculate_fu(
169169
as tile indices in 34-format
170170
:param config: hand configuration with win method and optional rule settings
171171
: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
173173
:param melds: declared melds (chi, pon, kan)
174174
:return: tuple of (fu component list, total fu rounded up to nearest 10)
175175
"""

mahjong/hand_calculating/hand_config.py

Lines changed: 198 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,80 @@
1+
"""
2+
Configuration classes for the hand calculator.
3+
4+
.. rubric:: Classes
5+
6+
* :class:`HandConstants` - constants for kazoe (counted) yakuman limit modes
7+
* :class:`OptionalRules` - toggle optional rule variants (open tanyao, aka dora, etc.)
8+
* :class:`HandConfig` - full hand configuration combining win conditions, wind context,
9+
bonuses, and optional rules
10+
"""
11+
112
from mahjong.constants import EAST
213
from mahjong.hand_calculating.yaku_config import YakuConfig
314

415

516
class HandConstants:
6-
# Hands over 26+ han don't count as double yakuman
17+
"""Constants for kazoe (counted) yakuman limit modes."""
18+
719
KAZOE_LIMITED = 0
8-
# Hands over 13+ is a sanbaiman
20+
"""Kazoe hands (13+ han) are capped at single yakuman."""
21+
922
KAZOE_SANBAIMAN = 1
10-
# 26+ han as double yakuman, 39+ han as triple yakuman, etc.
23+
"""Kazoe hands (13+ han) are capped at sanbaiman."""
24+
1125
KAZOE_NO_LIMIT = 2
26+
"""No cap on kazoe hands; 13+ han as yakuman, 26+ as double yakuman, and so on."""
1227

1328

1429
class OptionalRules:
1530
"""
16-
All the supported optional rules
31+
Toggle optional rule variants for hand evaluation.
32+
33+
Japanese mahjong has many rule variations across different parlors and online platforms.
34+
This class controls which optional rules are active during hand calculation.
35+
36+
:ivar has_open_tanyao: allow tanyao on open hands (kuitan)
37+
:vartype has_open_tanyao: bool
38+
:ivar has_aka_dora: enable red five tiles as bonus dora
39+
:vartype has_aka_dora: bool
40+
:ivar has_double_yakuman: allow double yakuman scoring for applicable hands
41+
:vartype has_double_yakuman: bool
42+
:ivar kazoe_limit: kazoe yakuman limit mode; one of
43+
:attr:`HandConstants.KAZOE_LIMITED`, :attr:`HandConstants.KAZOE_SANBAIMAN`,
44+
or :attr:`HandConstants.KAZOE_NO_LIMIT`
45+
:vartype kazoe_limit: int
46+
:ivar kiriage: round up to mangan when han/fu are at the boundary (4 han 30 fu or 3 han 60 fu)
47+
:vartype kiriage: bool
48+
:ivar fu_for_open_pinfu: add 2 fu for open pinfu-form ron (totaling 30 fu);
49+
when False, 1 han 20 fu hands become possible
50+
:vartype fu_for_open_pinfu: bool
51+
:ivar fu_for_pinfu_tsumo: award 2 fu for tsumo on pinfu hands (disabling the 0-fu
52+
tsumo pinfu exception)
53+
:vartype fu_for_pinfu_tsumo: bool
54+
:ivar renhou_as_yakuman: treat renhou as yakuman instead of mangan
55+
:vartype renhou_as_yakuman: bool
56+
:ivar has_daisharin: enable daisharin 22334455667788p as yakuman;
57+
automatically set to True when ``has_daisharin_other_suits`` is True
58+
:vartype has_daisharin: bool
59+
:ivar has_daisharin_other_suits: allow daisharin in man and sou suits (not only pin)
60+
:vartype has_daisharin_other_suits: bool
61+
:ivar has_daichisei: enable daichisei (tsuuiisou / all honors as seven pairs) as yakuman
62+
:vartype has_daichisei: bool
63+
:ivar has_sashikomi_yakuman: enable sashikomi (intentional deal-in for open riichi)
64+
as yakuman
65+
:vartype has_sashikomi_yakuman: bool
66+
:ivar limit_to_sextuple_yakuman: cap yakuman multiplier at 6x
67+
:vartype limit_to_sextuple_yakuman: bool
68+
:ivar paarenchan_needs_yaku: require at least one yaku for paarenchan to count
69+
:vartype paarenchan_needs_yaku: bool
1770
"""
1871

1972
has_open_tanyao: bool
2073
has_aka_dora: bool
2174
has_double_yakuman: bool
22-
# not implemented! tenhou does not support double yakuman for a single yaku
2375
kazoe_limit: int
2476
kiriage: bool
25-
# if false, 1-20 hand will be possible
2677
fu_for_open_pinfu: bool
27-
# if true, pinfu tsumo will be disabled
2878
fu_for_pinfu_tsumo: bool
2979
renhou_as_yakuman: bool
3080
has_daisharin: bool
@@ -51,6 +101,31 @@ def __init__(
51101
paarenchan_needs_yaku: bool = True,
52102
has_daichisei: bool = False,
53103
) -> None:
104+
"""
105+
Initialize optional rules.
106+
107+
>>> from mahjong.hand_calculating.hand_config import OptionalRules
108+
>>> options = OptionalRules(has_open_tanyao=True, kiriage=True)
109+
>>> options.has_open_tanyao
110+
True
111+
>>> options.kiriage
112+
True
113+
114+
:param has_open_tanyao: allow tanyao on open hands (kuitan)
115+
:param has_aka_dora: enable red five tiles as bonus dora
116+
:param has_double_yakuman: allow double yakuman scoring
117+
:param kazoe_limit: kazoe yakuman limit mode
118+
:param kiriage: round up to mangan at boundary han/fu combinations
119+
:param fu_for_open_pinfu: award 2 fu for open hands with no other fu sources
120+
:param fu_for_pinfu_tsumo: award tsumo fu even for pinfu hands
121+
:param renhou_as_yakuman: treat renhou as yakuman
122+
:param has_daisharin: enable daisharin yakuman
123+
:param has_daisharin_other_suits: allow daisharin in all suits
124+
:param has_sashikomi_yakuman: enable sashikomi yakuman
125+
:param limit_to_sextuple_yakuman: cap yakuman multiplier at 6x
126+
:param paarenchan_needs_yaku: require yaku for paarenchan
127+
:param has_daichisei: enable daichisei yakuman
128+
"""
54129
self.has_open_tanyao = has_open_tanyao
55130
self.has_aka_dora = has_aka_dora
56131
self.has_double_yakuman = has_double_yakuman
@@ -69,7 +144,82 @@ def __init__(
69144

70145
class HandConfig(HandConstants):
71146
"""
72-
Special class to pass various settings to the hand calculator object
147+
Configuration for the hand calculator combining win conditions, wind context, and optional rules.
148+
149+
Pass a ``HandConfig`` instance to
150+
:meth:`~mahjong.hand_calculating.hand.HandCalculator.estimate_hand_value` to describe
151+
how the hand was won and which rules apply.
152+
153+
A default config represents a closed ron with no special conditions:
154+
155+
>>> from mahjong.hand_calculating.hand_config import HandConfig
156+
>>> config = HandConfig()
157+
>>> config.is_tsumo
158+
False
159+
>>> config.is_dealer
160+
False
161+
162+
Configure a dealer tsumo win with riichi and ippatsu:
163+
164+
>>> from mahjong.constants import EAST
165+
>>> config = HandConfig(is_tsumo=True, is_riichi=True, is_ippatsu=True, player_wind=EAST)
166+
>>> config.is_tsumo
167+
True
168+
>>> config.is_dealer
169+
True
170+
171+
Include score bonuses and optional rules:
172+
173+
>>> from mahjong.hand_calculating.hand_config import OptionalRules
174+
>>> options = OptionalRules(has_open_tanyao=True, has_aka_dora=True)
175+
>>> config = HandConfig(tsumi_number=2, kyoutaku_number=3, options=options)
176+
>>> config.tsumi_number
177+
2
178+
>>> config.options.has_open_tanyao
179+
True
180+
181+
:ivar yaku: yaku definition objects used during hand evaluation
182+
:vartype yaku: ~mahjong.hand_calculating.yaku_config.YakuConfig
183+
:ivar options: optional rule settings
184+
:vartype options: OptionalRules
185+
:ivar is_tsumo: hand won by self-draw
186+
:vartype is_tsumo: bool
187+
:ivar is_riichi: player declared riichi
188+
:vartype is_riichi: bool
189+
:ivar is_ippatsu: win within one turn of declaring riichi
190+
:vartype is_ippatsu: bool
191+
:ivar is_rinshan: win on a replacement tile after calling kan
192+
:vartype is_rinshan: bool
193+
:ivar is_chankan: win by robbing another player's kan declaration
194+
:vartype is_chankan: bool
195+
:ivar is_haitei: win on the last drawable tile (tsumo) of the round
196+
:vartype is_haitei: bool
197+
:ivar is_houtei: win on the discard following the last drawable tile (ron)
198+
:vartype is_houtei: bool
199+
:ivar is_daburu_riichi: player declared double riichi (riichi on first turn)
200+
:vartype is_daburu_riichi: bool
201+
:ivar is_nagashi_mangan: all discards are terminals and honors with no calls against them
202+
:vartype is_nagashi_mangan: bool
203+
:ivar is_tenhou: dealer wins on the initial draw
204+
:vartype is_tenhou: bool
205+
:ivar is_renhou: non-dealer wins on the first go-around before any calls
206+
:vartype is_renhou: bool
207+
:ivar is_chiihou: non-dealer wins on the initial draw
208+
:vartype is_chiihou: bool
209+
:ivar is_open_riichi: player declared open riichi (revealed hand)
210+
:vartype is_open_riichi: bool
211+
:ivar is_dealer: True when ``player_wind`` is East
212+
:vartype is_dealer: bool
213+
:ivar player_wind: tile index in 34-format of the player's seat wind, or None
214+
:vartype player_wind: int | None
215+
:ivar round_wind: tile index in 34-format of the round wind, or None
216+
:vartype round_wind: int | None
217+
:ivar paarenchan: consecutive dealer wins count; above 0 enables paarenchan yakuman check
218+
:vartype paarenchan: int
219+
:ivar kyoutaku_number: number of riichi deposits on the table (1000 points each)
220+
:vartype kyoutaku_number: int
221+
:ivar tsumi_number: number of honba counters (100 points each per counter)
222+
:vartype tsumi_number: int
73223
"""
74224

75225
yaku: YakuConfig
@@ -92,11 +242,10 @@ class HandConfig(HandConstants):
92242
is_dealer: bool
93243
player_wind: int | None
94244
round_wind: int | None
95-
# for optional yakuman paarenchan above 0 means that dealer has paarenchan possibility
96245
paarenchan: int
97246

98-
kyoutaku_number: int # 1000-point
99-
tsumi_number: int # 100-point
247+
kyoutaku_number: int
248+
tsumi_number: int
100249

101250
def __init__(
102251
self,
@@ -120,6 +269,44 @@ def __init__(
120269
paarenchan: int = 0,
121270
options: OptionalRules | None = None,
122271
) -> None:
272+
"""
273+
Initialize hand configuration.
274+
275+
>>> from mahjong.hand_calculating.hand_config import HandConfig, OptionalRules
276+
>>> from mahjong.constants import EAST, SOUTH
277+
>>> config = HandConfig(
278+
... is_tsumo=True,
279+
... is_riichi=True,
280+
... player_wind=EAST,
281+
... round_wind=SOUTH,
282+
... options=OptionalRules(has_open_tanyao=True),
283+
... )
284+
>>> config.is_dealer
285+
True
286+
>>> config.round_wind == SOUTH
287+
True
288+
289+
:param is_tsumo: hand won by self-draw
290+
:param is_riichi: player declared riichi
291+
:param is_ippatsu: win within one turn of declaring riichi
292+
:param is_rinshan: win on a replacement tile after calling kan
293+
:param is_chankan: win by robbing another player's kan
294+
:param is_haitei: win on the last drawable tile
295+
:param is_houtei: win on the discard following the last drawable tile
296+
:param is_daburu_riichi: player declared double riichi
297+
:param is_nagashi_mangan: all discards are terminals and honors
298+
:param is_tenhou: dealer wins on the initial draw
299+
:param is_renhou: non-dealer wins on the first go-around
300+
:param is_chiihou: non-dealer wins on the initial draw
301+
:param is_open_riichi: player declared open riichi
302+
:param player_wind: tile index in 34-format of the player's seat wind
303+
:param round_wind: tile index in 34-format of the round wind
304+
:param kyoutaku_number: riichi deposits on the table (1000 points each)
305+
:param tsumi_number: honba counters (100 points each per counter)
306+
:param paarenchan: consecutive dealer wins count for paarenchan check
307+
:param options: optional rule settings; defaults to :class:`OptionalRules` with
308+
all defaults when None
309+
"""
123310
self.yaku = YakuConfig()
124311
self.options = options or OptionalRules()
125312

mahjong/hand_calculating/scores.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,11 +59,11 @@ def calculate_scores(han: int, fu: int, config: HandConfig, is_yakuman: bool = F
5959

6060
# kazoe hand
6161
if han >= 13 and not is_yakuman:
62-
# Hands over 26+ han don't count as double yakuman
62+
# kazoe hands capped at single yakuman
6363
if config.options.kazoe_limit == HandConfig.KAZOE_LIMITED:
6464
han = 13
6565
yaku_level = "kazoe yakuman"
66-
# Hands over 13+ is a sanbaiman
66+
# kazoe hands capped at sanbaiman
6767
elif config.options.kazoe_limit == HandConfig.KAZOE_SANBAIMAN:
6868
han = 12
6969
yaku_level = "kazoe sanbaiman"

mahjong/tile.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@ class Tile:
1313
for consumers that need to track discards with tsumogiri information.
1414
1515
:ivar value: tile index (typically in 136-format)
16+
:vartype value: Any
1617
:ivar is_tsumogiri: True if the tile was discarded immediately after drawing it
18+
:vartype is_tsumogiri: Any
1719
"""
1820

1921
value: Any

pyproject.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,6 @@ convention = "pep257"
115115

116116
# temporary
117117
"./mahjong/hand_calculating/__init__.py" = ["D"]
118-
"./mahjong/hand_calculating/hand_config.py" = ["D"]
119118
"./mahjong/hand_calculating/hand_response.py" = ["D"]
120119
"./mahjong/hand_calculating/hand.py" = ["D"]
121120
"./mahjong/hand_calculating/scores.py" = ["D"]

0 commit comments

Comments
 (0)