mahjong 2.0.0.dev1__py3-none-any.whl → 2.0.0.dev2__py3-none-any.whl

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/__init__.py CHANGED
@@ -0,0 +1,87 @@
1
+ """
2
+ _summary_.
3
+
4
+ Tile representation
5
+ ###################
6
+
7
+ mpsz-notation
8
+ -------------
9
+
10
+ The *mpsz-notation* is the same format used by the Tenhou (see https://tenhou.net/2/), where each tile is
11
+ represented by a two-character string: a digit for the rank followed by a letter for the suit.
12
+
13
+ The suit letters correspond to:
14
+
15
+ * `m` - manzu (萬子, characters)
16
+ * `p` - pinzu (筒子, circles)
17
+ * `s` - souzu (索子, bamboo)
18
+ * `z` - jihai (字牌, honors)
19
+
20
+ Within the numbered suits (`m`, `p`, `s`), ranks run from `1` through `9`; the red fives are
21
+ written with rank `0` (e.g., `0m`, `0p`, `0s`).
22
+
23
+ Honors (`z`) use ranks to distinguish individual tiles:
24
+
25
+ * `1` - East (東)
26
+ * `2` - South (南)
27
+ * `3` - West (西)
28
+ * `4` - North (北)
29
+ * `5` - White dragon (白, haku)
30
+ * `6` - Green dragon (發, hatsu)
31
+ * `7` - Red dragon (中, chun)
32
+
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``.
38
+
39
+ 34-format
40
+ ---------
41
+
42
+ In the *34-format*, an entry is assigned to each of the 34 tile types.
43
+ It does **not** distinguish a red five from a normal five - both map to the same entry.
44
+ The format is commonly used in two ways:
45
+
46
+ * A sequence of length 34 where each entry represents the count of the corresponding tile type.
47
+ * A list of tile-type indices, containing one element per tile rather than counts.
48
+ For example, a pon of 1m is represented as ``[0, 0, 0]``.
49
+
50
+ The correspondence between the index and the tile is shown in the table below.
51
+
52
+ +-------+----+----+----+----+----+----+----+----+----+
53
+ | Index | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
54
+ +-------+----+----+----+----+----+----+----+----+----+
55
+ | Tile | 1m | 2m | 3m | 4m | 5m | 6m | 7m | 8m | 9m |
56
+ +-------+----+----+----+----+----+----+----+----+----+
57
+
58
+ +-------+----+----+----+----+----+----+----+----+----+
59
+ | Index | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
60
+ +-------+----+----+----+----+----+----+----+----+----+
61
+ | Tile | 1p | 2p | 3p | 4p | 5p | 6p | 7p | 8p | 9p |
62
+ +-------+----+----+----+----+----+----+----+----+----+
63
+
64
+ +-------+----+----+----+----+----+----+----+----+----+
65
+ | Index | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 |
66
+ +-------+----+----+----+----+----+----+----+----+----+
67
+ | Tile | 1s | 2s | 3s | 4s | 5s | 6s | 7s | 8s | 9s |
68
+ +-------+----+----+----+----+----+----+----+----+----+
69
+
70
+ +-------+-----------+------------+-----------+------------+------------+------------+----------+
71
+ | Index | 27 | 28 | 29 | 30 | 31 | 32 | 33 |
72
+ +-------+-----------+------------+-----------+------------+------------+------------+----------+
73
+ | Tile | East (1z) | South (2z) | West (3z) | North (4z) | White (5z) | Green (6z) | Red (7z) |
74
+ +-------+-----------+------------+-----------+------------+------------+------------+----------+
75
+
76
+ 136-format
77
+ ----------
78
+
79
+ In the *136-format*, each physical tile (four copies of each of the 34 tile types) is assigned a unique integer.
80
+ It is primarily used when treating tiles as individual objects rather than counts, such as melds.
81
+
82
+ The integers range from 0 to 135.
83
+ IDs are assigned in groups of four: the four copies of 1m receive `0`, `1`, `2`, and `3`;
84
+ the four copies of 2m receive `4`, `5`, `6`, and `7`;
85
+ and so on, up to the four copies of red dragon (7z), which receive `132`, `133`, `134`, and `135`.
86
+ Red fives are assigned the indices `16` (`0m`), `52` (`0p`), and `88` (`0s`).
87
+ """
mahjong/agari.py CHANGED
@@ -2,13 +2,37 @@ from collections.abc import Collection, Sequence
2
2
 
3
3
 
4
4
  class Agari:
5
+ """Agari (winning hand) detection."""
6
+
5
7
  @staticmethod
6
8
  def is_agari(tiles_34: Sequence[int], open_sets_34: Collection[Sequence[int]] | None = None) -> bool:
7
9
  """
8
- Determine whether the hand is a winning hand (agari)
9
- :param tiles_34: 34 tiles format array
10
- :param open_sets_34: array of arrays of 34 tiles format
11
- :return: boolean
10
+ Determine whether the given tiles form a complete hand structure.
11
+
12
+ Validate the tile arrangement only (4 melds + 1 pair, seven pairs, or thirteen orphans),
13
+ not yaku or scoring. When open sets are provided, the declared meld tiles are subtracted
14
+ before checking.
15
+
16
+ The concealed portion can be passed alone without melds, which is valid
17
+ for any number of melds (0-4) plus 1 pair:
18
+
19
+ 0 melds + 1 pair (valid hand):
20
+
21
+ >>> tiles_34 = [0] * 34
22
+ >>> tiles_34[0] = 2
23
+ >>> Agari.is_agari(tiles_34)
24
+ True
25
+
26
+ 1 meld (triplet) without a pair is not a valid hand:
27
+
28
+ >>> tiles_34 = [0] * 34
29
+ >>> tiles_34[0] = 3
30
+ >>> Agari.is_agari(tiles_34)
31
+ False
32
+
33
+ :param tiles_34: hand in 34-format count array (length 34)
34
+ :param open_sets_34: declared melds as arrays of tile indices in 34-format
35
+ :return: True if the hand is complete
12
36
  """
13
37
  if open_sets_34:
14
38
  # subtract declared meld tiles so the algorithm checks only the closed portion
mahjong/constants.py CHANGED
@@ -1,24 +1,50 @@
1
- # 1 and 9
2
1
  TERMINAL_INDICES = frozenset([0, 8, 9, 17, 18, 26])
2
+ """Tile indices (34-format) of terminal tiles — 1 and 9 of each suit (man, pin, sou)."""
3
3
 
4
- # dragons and winds
5
4
  EAST = 27
5
+ """Tile index (34-format) for East wind."""
6
+
6
7
  SOUTH = 28
8
+ """Tile index (34-format) for South wind."""
9
+
7
10
  WEST = 29
11
+ """Tile index (34-format) for West wind."""
12
+
8
13
  NORTH = 30
14
+ """Tile index (34-format) for North wind."""
15
+
9
16
  HAKU = 31
17
+ """Tile index (34-format) for Haku (white dragon)."""
18
+
10
19
  HATSU = 32
20
+ """Tile index (34-format) for Hatsu (green dragon)."""
21
+
11
22
  CHUN = 33
23
+ """Tile index (34-format) for Chun (red dragon)."""
12
24
 
13
25
  WINDS = frozenset([EAST, SOUTH, WEST, NORTH])
26
+ """Tile indices (34-format) of all four wind tiles."""
27
+
14
28
  DRAGONS = frozenset([HAKU, HATSU, CHUN])
29
+ """Tile indices (34-format) of all three dragon tiles."""
30
+
15
31
  HONOR_INDICES = WINDS | DRAGONS
32
+ """Tile indices (34-format) of all honor tiles — winds and dragons."""
33
+
16
34
  TERMINAL_AND_HONOR_INDICES = TERMINAL_INDICES | HONOR_INDICES
35
+ """Tile indices (34-format) of all terminal and honor tiles."""
17
36
 
18
37
  FIVE_RED_MAN = 16
38
+ """Tile index (136-format) for the red five of man (characters)."""
39
+
19
40
  FIVE_RED_PIN = 52
41
+ """Tile index (136-format) for the red five of pin (circles)."""
42
+
20
43
  FIVE_RED_SOU = 88
44
+ """Tile index (136-format) for the red five of sou (bamboo)."""
21
45
 
22
46
  AKA_DORAS = frozenset([FIVE_RED_MAN, FIVE_RED_PIN, FIVE_RED_SOU])
47
+ """Tile indices (136-format) of all three red five (aka dora) tiles."""
23
48
 
24
49
  DISPLAY_WINDS = {EAST: "East", SOUTH: "South", WEST: "West", NORTH: "North"}
50
+ """Mapping from wind tile indices (34-format) to their English display names."""
@@ -50,16 +50,81 @@ _Blocks = tuple[_Block, ...]
50
50
 
51
51
 
52
52
  class HandDivider:
53
+ """
54
+ Decompose a winning hand into all possible block combinations.
55
+
56
+ A valid combination consists of 4 melds (sequences, triplets, or quads) plus 1 pair.
57
+ Chiitoitsu (seven pairs) is also recognized when no melds are declared.
58
+ Multiple valid decompositions may exist for the same hand.
59
+
60
+ Each decomposition is a list of blocks, where each block is a list of tile indices
61
+ in 34-format:
62
+
63
+ - pair: ``[tile, tile]`` (length 2, identical tiles)
64
+ - triplet: ``[tile, tile, tile]`` (length 3, identical tiles)
65
+ - sequence: ``[tile, tile+1, tile+2]`` (length 3, consecutive tiles)
66
+ - quad: ``[tile, tile, tile, tile]`` (length 4, identical tiles)
67
+ """
68
+
53
69
  @staticmethod
54
70
  def divide_hand(
55
71
  tiles_34: Sequence[int],
56
72
  melds: Collection[Meld] | None = None,
57
73
  ) -> list[list[list[int]]]:
58
74
  """
59
- Return a list of possible hands.
60
- :param tiles_34:
61
- :param melds: list of Meld objects
62
- :return:
75
+ Return all valid decompositions of the hand into blocks.
76
+
77
+ Decompose the full hand (including any declared melds) into every possible
78
+ combination of 4 melds + 1 pair, or 7 pairs (chiitoitsu, only when no melds
79
+ are declared).
80
+
81
+ Single decomposition (one triplet, three sequences, and a pair):
82
+
83
+ >>> from mahjong.hand_calculating.divider import HandDivider
84
+ >>> from mahjong.tile import TilesConverter
85
+ >>> tiles_34 = TilesConverter.one_line_string_to_34_array("234567m23455s777z")
86
+ >>> result = HandDivider.divide_hand(tiles_34)
87
+ >>> result
88
+ [[[1, 2, 3], [4, 5, 6], [19, 20, 21], [22, 22], [33, 33, 33]]]
89
+
90
+ Multiple decompositions (triplets vs. sequences):
91
+
92
+ >>> tiles_34 = TilesConverter.one_line_string_to_34_array("11122233388899m")
93
+ >>> result = HandDivider.divide_hand(tiles_34)
94
+ >>> result
95
+ [[[0, 0, 0], [1, 1, 1], [2, 2, 2], [7, 7, 7], [8, 8]], [[0, 1, 2], [0, 1, 2], [0, 1, 2], [7, 7, 7], [8, 8]]]
96
+
97
+ Ryanpeikou form (decomposes into both 4 melds + 1 pair and chiitoitsu):
98
+
99
+ >>> tiles_34 = TilesConverter.one_line_string_to_34_array("11223344556677m")
100
+ >>> result = HandDivider.divide_hand(tiles_34)
101
+ >>> len(result)
102
+ 4
103
+ >>> [[0, 0], [1, 1], [2, 2], [3, 3], [4, 4], [5, 5], [6, 6]] in result
104
+ True
105
+ >>> [[0, 1, 2], [0, 1, 2], [3, 4, 5], [3, 4, 5], [6, 6]] in result
106
+ True
107
+
108
+ Hands with declared melds:
109
+
110
+ >>> from mahjong.meld import Meld
111
+ >>> tiles_34 = TilesConverter.string_to_34_array(pin="234777888999", honors="22")
112
+ >>> chi_1 = Meld(meld_type=Meld.CHI, tiles=TilesConverter.string_to_136_array(pin="789"))
113
+ >>> chi_2 = Meld(meld_type=Meld.CHI, tiles=TilesConverter.string_to_136_array(pin="234"))
114
+ >>> result = HandDivider.divide_hand(tiles_34, [chi_1, chi_2])
115
+ >>> result
116
+ [[[10, 11, 12], [15, 16, 17], [15, 16, 17], [15, 16, 17], [28, 28]]]
117
+
118
+ Invalid hands return an empty list:
119
+
120
+ >>> HandDivider.divide_hand([5] * 34)
121
+ []
122
+
123
+ :param tiles_34: hand in 34-format count array (length 34), including tiles from melds
124
+ :param melds: declared melds (:class:`~mahjong.meld.Meld` objects); their tiles are
125
+ subtracted from ``tiles_34`` before decomposing the concealed portion
126
+ :return: list of decompositions, where each decomposition is a list of blocks
127
+ (each block is a list of tile indices in 34-format)
63
128
  """
64
129
  if not HandDivider._validate_tiles(tiles_34):
65
130
  return []
@@ -7,6 +7,16 @@ from mahjong.meld import Meld
7
7
 
8
8
 
9
9
  class FuDetail(TypedDict):
10
+ """
11
+ A single fu component with its point value and reason.
12
+
13
+ Each entry in the fu breakdown returned by :meth:`FuCalculator.calculate_fu`
14
+ describes one source of fu (e.g., base fu, a closed pon, or a wait type).
15
+
16
+ :param fu: fu points awarded for this component
17
+ :param reason: identifier string matching one of the :class:`FuCalculator` reason constants
18
+ """
19
+
10
20
  fu: int
11
21
  reason: str
12
22
 
@@ -20,26 +30,61 @@ _IS_KAN = 2
20
30
 
21
31
 
22
32
  class FuCalculator:
33
+ """
34
+ Calculate fu (minipoints) for a winning hand decomposition.
35
+
36
+ Fu are computed from the hand's meld structure, wait type, pair, and winning method.
37
+ The result is a detailed breakdown of fu components and the total fu rounded up to
38
+ the nearest 10.
39
+ """
40
+
23
41
  BASE = "base"
42
+ """Base fu: 20 for open/tsumo, 30 for closed ron, 25 for chiitoitsu."""
43
+
24
44
  PENCHAN = "penchan"
45
+ """Penchan (edge wait): 2 fu for completing 1-2-3 with the 3, or 7-8-9 with the 7."""
46
+
25
47
  KANCHAN = "kanchan"
48
+ """Kanchan (closed wait): 2 fu for waiting on the middle tile of a sequence."""
49
+
26
50
  VALUED_PAIR = "valued_pair"
51
+ """Valued pair: 2 fu for a pair of dragon, seat wind, or round wind tiles."""
52
+
27
53
  DOUBLE_VALUED_PAIR = "double_valued_pair"
54
+ """Double valued pair: 4 fu when the pair tile is both seat wind and round wind."""
55
+
28
56
  PAIR_WAIT = "pair_wait"
57
+ """Pair wait: 2 fu for winning on the pair tile."""
58
+
29
59
  TSUMO = "tsumo"
60
+ """Tsumo: 2 fu for winning by self-draw."""
61
+
30
62
  HAND_WITHOUT_FU = "hand_without_fu"
63
+ """Open pinfu: 2 fu for an open hand with no other fu sources."""
31
64
 
32
65
  CLOSED_PON = "closed_pon"
66
+ """Closed pon of simple tiles: 4 fu."""
67
+
33
68
  OPEN_PON = "open_pon"
69
+ """Open pon of simple tiles: 2 fu."""
34
70
 
35
71
  CLOSED_TERMINAL_PON = "closed_terminal_pon"
72
+ """Closed pon of terminal or honor tiles: 8 fu."""
73
+
36
74
  OPEN_TERMINAL_PON = "open_terminal_pon"
75
+ """Open pon of terminal or honor tiles: 4 fu."""
37
76
 
38
77
  CLOSED_KAN = "closed_kan"
78
+ """Closed kan of simple tiles: 16 fu."""
79
+
39
80
  OPEN_KAN = "open_kan"
81
+ """Open kan of simple tiles: 8 fu."""
40
82
 
41
83
  CLOSED_TERMINAL_KAN = "closed_terminal_kan"
84
+ """Closed kan of terminal or honor tiles: 32 fu."""
85
+
42
86
  OPEN_TERMINAL_KAN = "open_terminal_kan"
87
+ """Open kan of terminal or honor tiles: 16 fu."""
43
88
 
44
89
  # lookup table indexed by [is_terminal_or_honor][is_kan][is_open] -> (fu, reason)
45
90
  _PON_KAN_FU_TABLE = (
@@ -63,14 +108,70 @@ class FuCalculator:
63
108
  melds: Collection[Meld] | None = None,
64
109
  ) -> tuple[list[FuDetail], int]:
65
110
  """
66
- Calculate hand fu with explanations
67
- :param hand:
68
- :param win_tile: 136 tile format
69
- :param win_group: one set where win tile exists
70
- :param config: HandConfig object
71
- :param valued_tiles: dragons, player wind, round wind
72
- :param melds: opened sets
73
- :return:
111
+ Calculate fu for a winning hand decomposition.
112
+
113
+ Analyze the hand's meld structure, wait type, pair, and winning method to produce
114
+ a detailed breakdown of fu components and the total fu rounded up to the nearest 10.
115
+
116
+ Chiitoitsu (seven pairs) always receives 25 fu:
117
+
118
+ >>> from mahjong.hand_calculating.fu import FuCalculator
119
+ >>> from mahjong.hand_calculating.hand_config import HandConfig
120
+ >>> hand = [[0, 0], [1, 1], [2, 2], [3, 3], [4, 4], [5, 5], [6, 6]]
121
+ >>> win_tile = 24
122
+ >>> win_group = [6, 6]
123
+ >>> fu_details, fu = FuCalculator.calculate_fu(hand, win_tile, win_group, HandConfig())
124
+ >>> fu
125
+ 25
126
+
127
+ A closed ron hand with only chi melds and a non-valued pair (pinfu) receives 30 fu:
128
+
129
+ >>> hand = [[0, 1, 2], [3, 4, 5], [9, 10, 11], [18, 19, 20], [27, 27]]
130
+ >>> win_tile = 20
131
+ >>> win_group = [3, 4, 5]
132
+ >>> fu_details, fu = FuCalculator.calculate_fu(hand, win_tile, win_group, HandConfig())
133
+ >>> fu
134
+ 30
135
+
136
+ A closed pon of simple tiles adds 4 fu:
137
+
138
+ >>> hand = [[2, 2, 2], [3, 4, 5], [9, 10, 11], [18, 19, 20], [27, 27]]
139
+ >>> win_tile = 20
140
+ >>> win_group = [3, 4, 5]
141
+ >>> fu_details, fu = FuCalculator.calculate_fu(hand, win_tile, win_group, HandConfig())
142
+ >>> fu
143
+ 40
144
+ >>> {"fu": 4, "reason": FuCalculator.CLOSED_PON} in fu_details
145
+ True
146
+
147
+ A double valued pair with an open terminal pon:
148
+
149
+ >>> from mahjong.meld import Meld
150
+ >>> hand = [[2, 3, 4], [9, 10, 11], [18, 19, 20], [33, 33, 33], [27, 27]]
151
+ >>> win_tile = 8
152
+ >>> win_group = [2, 3, 4]
153
+ >>> valued_tiles = [27, 27]
154
+ >>> melds = [Meld(meld_type=Meld.PON, tiles=[132, 133, 134], opened=True)]
155
+ >>> fu_details, fu = FuCalculator.calculate_fu(hand, win_tile, win_group, HandConfig(), valued_tiles, melds)
156
+ >>> fu
157
+ 30
158
+ >>> {"fu": 4, "reason": FuCalculator.DOUBLE_VALUED_PAIR} in fu_details
159
+ True
160
+ >>> {"fu": 4, "reason": FuCalculator.OPEN_TERMINAL_PON} in fu_details
161
+ True
162
+
163
+ :param hand: decomposed hand as a collection of tile sets, each a sequence of tile
164
+ indices in 34-format: a pair (length 2), chi (length 3 with consecutive tiles),
165
+ pon (length 3 with identical tiles), or kan (length 4);
166
+ chiitoitsu hands have 7 pairs
167
+ :param win_tile: the winning tile index in 136-format
168
+ :param win_group: the tile set (from ``hand``) that contains the winning tile,
169
+ as tile indices in 34-format
170
+ :param config: hand configuration with win method and optional rule settings
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
173
+ :param melds: declared melds (chi, pon, kan)
174
+ :return: tuple of (fu component list, total fu rounded up to nearest 10)
74
175
  """
75
176
  # chiitoitsu: always 25 fu
76
177
  if len(hand) == 7:
@@ -424,7 +424,7 @@ class HandCalculator:
424
424
  if not is_aotenjou:
425
425
  hand_yaku = yakuman_list
426
426
  else:
427
- scores_calculator.aotenjou_filter_yaku(hand_yaku, config)
427
+ scores_calculator.aotenjou_filter_yaku(hand_yaku, config) # ty: ignore[unresolved-attribute]
428
428
  yakuman_list = []
429
429
 
430
430
  # calculate han
@@ -41,3 +41,49 @@ from mahjong.hand_calculating.yaku_list.tanyao import Tanyao
41
41
  from mahjong.hand_calculating.yaku_list.toitoi import Toitoi
42
42
  from mahjong.hand_calculating.yaku_list.tsumo import Tsumo
43
43
  from mahjong.hand_calculating.yaku_list.ura_dora import UraDora
44
+
45
+ __all__ = [
46
+ "AkaDora",
47
+ "Chankan",
48
+ "Chantai",
49
+ "Chiitoitsu",
50
+ "Chinitsu",
51
+ "Chun",
52
+ "DaburuOpenRiichi",
53
+ "DaburuRiichi",
54
+ "Dora",
55
+ "Haitei",
56
+ "Haku",
57
+ "Hatsu",
58
+ "Honitsu",
59
+ "Honroto",
60
+ "Houtei",
61
+ "Iipeiko",
62
+ "Ippatsu",
63
+ "Ittsu",
64
+ "Junchan",
65
+ "NagashiMangan",
66
+ "OpenRiichi",
67
+ "Pinfu",
68
+ "Renhou",
69
+ "Riichi",
70
+ "Rinshan",
71
+ "RoundWindEast",
72
+ "RoundWindNorth",
73
+ "RoundWindSouth",
74
+ "RoundWindWest",
75
+ "Ryanpeikou",
76
+ "SanKantsu",
77
+ "Sanankou",
78
+ "Sanshoku",
79
+ "SanshokuDoukou",
80
+ "SeatWindEast",
81
+ "SeatWindNorth",
82
+ "SeatWindSouth",
83
+ "SeatWindWest",
84
+ "Shosangen",
85
+ "Tanyao",
86
+ "Toitoi",
87
+ "Tsumo",
88
+ "UraDora",
89
+ ]
@@ -18,3 +18,26 @@ from mahjong.hand_calculating.yaku_list.yakuman.suuankou_tanki import SuuankouTa
18
18
  from mahjong.hand_calculating.yaku_list.yakuman.suukantsu import Suukantsu
19
19
  from mahjong.hand_calculating.yaku_list.yakuman.tenhou import Tenhou
20
20
  from mahjong.hand_calculating.yaku_list.yakuman.tsuisou import Tsuuiisou
21
+
22
+ __all__ = [
23
+ "Chiihou",
24
+ "Chinroutou",
25
+ "ChuurenPoutou",
26
+ "DaburuChuurenPoutou",
27
+ "DaburuKokushiMusou",
28
+ "DaiSuushii",
29
+ "Daichisei",
30
+ "Daisangen",
31
+ "Daisharin",
32
+ "KokushiMusou",
33
+ "Paarenchan",
34
+ "RenhouYakuman",
35
+ "Ryuuiisou",
36
+ "Sashikomi",
37
+ "Shousuushii",
38
+ "Suuankou",
39
+ "SuuankouTanki",
40
+ "Suukantsu",
41
+ "Tenhou",
42
+ "Tsuuiisou",
43
+ ]
mahjong/meld.py CHANGED
@@ -5,15 +5,68 @@ from mahjong.tile import TilesConverter
5
5
 
6
6
 
7
7
  class Meld:
8
+ """
9
+ Representation of a declared meld (called or concealed tile group).
10
+
11
+ A meld is a set of tiles that a player has declared, either by calling another
12
+ player's discard (open meld) or by declaring a concealed kan. Meld types follow
13
+ standard Japanese mahjong terminology:
14
+
15
+ - **Chi** — a sequence of three consecutive suited tiles, called from the player to the left.
16
+ - **Pon** — a triplet of identical tiles, called from any player.
17
+ - **Kan** — a quad of identical tiles (open, closed, or extended).
18
+ - **Shouminkan** — an added kan (extending an existing pon with the fourth tile).
19
+ - **Nuki** — a special declaration used in three-player mahjong (north wind extraction).
20
+
21
+ Create an open chi meld:
22
+
23
+ >>> from mahjong.meld import Meld
24
+ >>> from mahjong.tile import TilesConverter
25
+ >>> tiles = TilesConverter.string_to_136_array(man="123")
26
+ >>> meld = Meld(meld_type=Meld.CHI, tiles=tiles)
27
+ >>> meld.type
28
+ 'chi'
29
+ >>> meld.tiles
30
+ (0, 4, 8)
31
+
32
+ Create a closed kan:
33
+
34
+ >>> tiles = TilesConverter.string_to_136_array(man="1111")
35
+ >>> meld = Meld(meld_type=Meld.KAN, tiles=tiles, opened=False)
36
+ >>> meld.opened
37
+ False
38
+
39
+ :ivar type: one of the meld type constants (CHI, PON, KAN, SHOUMINKAN, NUKI)
40
+ :vartype type: str | None
41
+ :ivar tiles: tile indices in 136-format, stored as an immutable tuple
42
+ :vartype tiles: tuple[int, ...]
43
+ :ivar opened: True for open melds (called from another player), False for closed kan
44
+ :vartype opened: bool
45
+ :ivar called_tile: the specific tile index (136-format) that was called to form this meld
46
+ :vartype called_tile: int | None
47
+ :ivar who: seat index (0-3) of the player who declared the meld
48
+ :vartype who: int | None
49
+ :ivar from_who: seat index (0-3) of the player who discarded the called tile
50
+ :vartype from_who: int | None
51
+ """
52
+
8
53
  CHI = "chi"
54
+ """Chi (sequence) meld type — three consecutive suited tiles."""
55
+
9
56
  PON = "pon"
57
+ """Pon (triplet) meld type — three identical tiles."""
58
+
10
59
  KAN = "kan"
60
+ """Kan (quad) meld type — four identical tiles."""
61
+
11
62
  SHOUMINKAN = "shouminkan"
63
+ """Shouminkan (added kan) meld type — extending a pon with the fourth tile."""
64
+
12
65
  NUKI = "nuki"
66
+ """Nuki (north wind extraction) meld type, at the moment is not used in the library."""
13
67
 
14
68
  type: str | None
15
69
  tiles: tuple[int, ...]
16
- # we need it to distinguish opened and closed kan
17
70
  opened: bool
18
71
  called_tile: int | None
19
72
  who: int | None
@@ -28,6 +81,25 @@ class Meld:
28
81
  who: int | None = None,
29
82
  from_who: int | None = None,
30
83
  ) -> None:
84
+ """
85
+ Initialize a meld.
86
+
87
+ >>> from mahjong.meld import Meld
88
+ >>> from mahjong.tile import TilesConverter
89
+ >>> tiles = TilesConverter.string_to_136_array(man="111")
90
+ >>> meld = Meld(meld_type=Meld.PON, tiles=tiles, who=0, from_who=2)
91
+ >>> meld.type
92
+ 'pon'
93
+ >>> meld.who
94
+ 0
95
+
96
+ :param meld_type: one of the meld type constants (CHI, PON, KAN, SHOUMINKAN, NUKI)
97
+ :param tiles: tile indices in 136-format
98
+ :param opened: True for open melds (called from another player), False for closed kan
99
+ :param called_tile: the specific tile index (136-format) that was called to form this meld
100
+ :param who: seat index (0-3) of the player who declared the meld
101
+ :param from_who: seat index (0-3) of the player who discarded the called tile
102
+ """
31
103
  self.type = meld_type
32
104
  self.tiles = tuple(tiles) if tiles else ()
33
105
  self.opened = opened
@@ -36,17 +108,40 @@ class Meld:
36
108
  self.from_who = from_who
37
109
 
38
110
  def __setattr__(self, name: str, value: object) -> None:
111
+ """Invalidate the tiles_34 cache when tiles are reassigned."""
39
112
  super().__setattr__(name, value)
40
113
  if name == "tiles" and "tiles_34" in self.__dict__:
41
114
  del self.__dict__["tiles_34"]
42
115
 
43
116
  def __str__(self) -> str:
117
+ """
118
+ Return a human-readable string with meld type and tiles.
119
+
120
+ >>> from mahjong.meld import Meld
121
+ >>> from mahjong.tile import TilesConverter
122
+ >>> tiles = TilesConverter.string_to_136_array(man="123")
123
+ >>> meld = Meld(meld_type=Meld.CHI, tiles=tiles)
124
+ >>> str(meld)
125
+ 'Type: chi, Tiles: 123m (0, 4, 8)'
126
+ """
44
127
  return f"Type: {self.type}, Tiles: {TilesConverter.to_one_line_string(self.tiles)} {self.tiles}"
45
128
 
46
- # for calls in array
47
129
  def __repr__(self) -> str:
130
+ """Return the same representation as __str__."""
48
131
  return self.__str__()
49
132
 
50
133
  @cached_property
51
134
  def tiles_34(self) -> list[int]:
135
+ """
136
+ Convert the meld's 136-format tile indices to 34-format tile indices.
137
+
138
+ >>> from mahjong.meld import Meld
139
+ >>> from mahjong.tile import TilesConverter
140
+ >>> tiles = TilesConverter.string_to_136_array(man="111")
141
+ >>> meld = Meld(meld_type=Meld.PON, tiles=tiles)
142
+ >>> meld.tiles_34
143
+ [0, 0, 0]
144
+
145
+ :return: list of tile indices in 34-format
146
+ """
52
147
  return [x // 4 for x in self.tiles]