Skip to content

Commit d8b82b5

Browse files
committed
Add HandEnding.faan and .base_points
game.py: + HandEnding.faan simply sums Wu.faan and Player.bonus_faan + HandEnding.points computes the actual points resulting from the faan, as changes to current points of each player + game.STOCK_TABLES are stock faan->base-point mappings that come with the library; you are free to provide your own in an argument to HandEnding.base_points melds.py: + WuFlag.(bonus flags) * Moved constants because it makes sense players.py: * Player.bonus_faan now returns the flags regarding bonuses too __main__.py: + Since running Hands standalone is supported now, do so by default. Run a full game with --game cmdarg + Show point deltas as well as faan and flags
1 parent f6b95a0 commit d8b82b5

4 files changed

Lines changed: 157 additions & 41 deletions

File tree

mahjong/__main__.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,13 @@
2626
print(', '.join(map(str, p.bonus)))
2727
#'''
2828
#''' # uncomment this opening triple quote when commenting out one elsewhere
29-
from mahjong.game import Game, UserIO, Question, HandEnding, HandResult
29+
import sys
30+
from mahjong.game import Game, Hand, UserIO, Question, HandEnding, HandResult
3031

31-
game = Game(extra_hands=False)
32+
if '--game' in sys.argv:
33+
game = Game(extra_hands=False)
34+
else:
35+
game = Hand(None)
3236
print('Note: all indexes are **1-based**.')
3337
print('This is a rudimentary text-based mahjong implementation.')
3438
print("It's purely as a proof-of-concept/manual testing method.")
@@ -109,9 +113,9 @@
109113
print('Goulash! Nobody wins. Starting next game...')
110114
else:
111115
assert question.choice is not None
112-
print('Player #%s won with %s (%s faan; %s)! Starting next game...' % (
116+
print('Player #%s won with %s (%s faan; %s points; %s)! Starting next game...' % (
113117
question.winner.seat.value, ','.join(map(str, question.choice)),
114-
*question.wu.faan(question.choice, (question.winner.seat, game.round.wind))
118+
question.faan()[0], *question.points(1)
115119
))
116120
question = question.answer()
117121
print('Game Over!')

mahjong/game.py

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,34 @@
2626
'Turn',
2727
]
2828

29+
STOCK_TABLES = {
30+
'enwp': {
31+
1: 1, 2: 1, 3: 1,
32+
4: 2, 5: 2, 6: 2,
33+
7: 4, 8: 4, 9: 4,
34+
10: 8
35+
},
36+
'zhwp': {
37+
0: 1, 1: 2, 2: 4, 3: 8,
38+
4: 16, 5: 24, 6: 32, 7: 48,
39+
8: 64, 9: 96, 10: 128, 11: 192,
40+
12: 256, 13: 384
41+
},
42+
# in the app I play,
43+
# 3 faan = 1200c = 1x1200
44+
# 4 faan = 2400c = 2x1200
45+
# 5 faan = 3600c = 3x1200
46+
# 6 faan = 4800c = 4x1200
47+
# 7 faan = 7200c = 6x1200
48+
# 8 faan = 9600c = 8x1200
49+
'random_app': {
50+
1: 1, 2: 1, 3: 1,
51+
4: 2, 5: 3, 6: 4,
52+
7: 6, 8: 8, 9: 10,
53+
10: 13
54+
}
55+
}
56+
2957
# data classes
3058

3159
def _answer(gen: Generator, ans=None) -> YieldType:
@@ -54,6 +82,71 @@ class HandEnding(NamedTuple):
5482
def answer(self, ans=None):
5583
return _answer(self.hand.gen, ans)
5684

85+
def faan(self) -> Tuple[int, Optional[WuFlag]]:
86+
"""Calculate the faan for this hand."""
87+
if self.winner is None or self.wu is None or self.choice is None:
88+
return (0, None)
89+
points = self.wu.faan(self.choice, (self.winner.seat, self.hand.wind))
90+
bonus = self.winner.bonus_faan()
91+
return (points[0] + bonus[0], points[1] | bonus[1])
92+
93+
def points(self, min_faan: int = 3, table: Mapping[int, int] = None) \
94+
-> Tuple[List[int], Optional[WuFlag]]:
95+
"""Calculate the change in points for each player.
96+
97+
Parameters
98+
-----------
99+
``min_faan``: int
100+
Minimum faan before the hand is recognized as a valid winning hand
101+
``table``: Mapping[int, int]
102+
Mapping of faan to base points. The largest faan value is assumed
103+
to be the limit, and values larger than it will be mapped to it.
104+
105+
Returns
106+
--------
107+
Tuple[List[int], Optional[WuFlag]]
108+
A list of four point deltas, representing each player, and
109+
the flags that apply to this hand. The former is four zeroes,
110+
and the latter is None, on goulash or not enough faan.
111+
"""
112+
points = [0, 0, 0, 0]
113+
if self.winner is None:
114+
return (points, None)
115+
faan, flags = self.faan()
116+
if faan < min_faan:
117+
return (points, None)
118+
winner = self.winner
119+
wu = self.wu
120+
if table is None:
121+
table = STOCK_TABLES['random_app']
122+
limit = max(table.keys())
123+
base = table[min(faan, limit)]
124+
# special penalties that make the perpetrator pay
125+
# for everyone else on top of themselves
126+
if ((WuFlag.TWELVE_PIECE in wu.flags and WuFlag.SELF_DRAW in wu.flags)
127+
or (WuFlag.GAVE_KONG in wu.flags and WuFlag.SELF_DRAW in wu.flags)
128+
or WuFlag.GAVE_DRAGON in wu.flags) and wu.discarder is not None:
129+
points[winner.seat] += base * 3
130+
points[wu.discarder] -= base * 3
131+
# loser pays double in normal losing conditions
132+
elif wu.discarder is not None:
133+
points[winner.seat] += base * 2
134+
points[wu.discarder] -= base * 2
135+
# self draw means everyone pays
136+
elif WuFlag.SELF_DRAW in wu.flags:
137+
for i in range(4):
138+
if i == winner.seat:
139+
points[i] += base * 3
140+
else:
141+
points[i] -= base
142+
else:
143+
raise ValueError('Somehow, nobody gets points. '
144+
'Possibly report to maintainer with '
145+
'the following information: '
146+
+ str(self.wu.__dict__)
147+
+ ' ;; ' + str(self.winner.__dict__))
148+
return (points, flags)
149+
57150
class Question(Enum):
58151
MELD_FROM_DISCARD_Q = 16
59152
SHOW_EKFCP_Q = 23

mahjong/melds.py

Lines changed: 36 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,42 @@ class WuFlag(Flag):
5757
GAVE_DRAGON = 1 << 29
5858
GAVE_KONG = 1 << 30
5959

60+
# bonuses
61+
NO_BONUSES = 1 << 31
62+
ALIGNED_FLOWERS = 1 << 32
63+
ALIGNED_SEASONS = 1 << 33
64+
TABLE_OF_FLOWERS = 1 << 34
65+
TABLE_OF_SEASONS = 1 << 35
66+
HAND_OF_BONUSES = 1 << 36
67+
68+
# special case: 1 & 9 of each suit + every value of honors suits
69+
THIRTEEN_ORPHANS = set(map(Tile.from_str, (
70+
'tong/1|tong/9|zhu/1|zhu/9|wan/1|wan/9|' # simples
71+
'feng/1|feng/2|feng/3|feng/4|long/1|long/2|long/3' # honors
72+
).split('|')))
73+
74+
FLAG_FAAN = {
75+
WuFlag.CHICKEN_HAND: 0,
76+
WuFlag.COMMON_HAND: 1, WuFlag.ALL_IN_TRIPLETS: 3,
77+
WuFlag.ALL_HONOR_TILES: 10, WuFlag.ALL_ONE_SUIT: 7,
78+
WuFlag.MIXED_ONE_SUIT: 3, WuFlag.GREAT_DRAGONS: 8,
79+
WuFlag.SMALL_DRAGONS: 5, WuFlag.GREAT_WINDS: 13,
80+
WuFlag.SMALL_WINDS: 10, WuFlag.THIRTEEN_ORPHANS: 13,
81+
WuFlag.ALL_KONGS: 13, WuFlag.SELF_TRIPLETS: 8,
82+
WuFlag.ORPHANS: 10, WuFlag.NINE_GATES: 10,
83+
WuFlag.RED_DRAGON: 1, WuFlag.GREAT_WINDS: 1,
84+
WuFlag.WHITE_DRAGON: 1, WuFlag.SEAT_WIND: 1,
85+
WuFlag.PREVAILING_WIND: 1, WuFlag.MIXED_ORPHANS: 1,
86+
WuFlag.SELF_DRAW: 1, WuFlag.ALL_FROM_WALL: 1,
87+
WuFlag.ROBBING_KONG: 1, WuFlag.LAST_CATCH: 1,
88+
WuFlag.BY_KONG: 1, WuFlag.DOUBLE_KONG: 8,
89+
WuFlag.HEAVENLY: 13, WuFlag.EARTHLY: 13,
90+
WuFlag.TWELVE_PIECE: 0, WuFlag.GAVE_DRAGON: 0, WuFlag.GAVE_KONG: 0,
91+
WuFlag.NO_BONUSES: 1, WuFlag.ALIGNED_FLOWERS: 1,
92+
WuFlag.ALIGNED_SEASONS: 1, WuFlag.TABLE_OF_FLOWERS: 2,
93+
WuFlag.TABLE_OF_SEASONS: 2, WuFlag.HAND_OF_BONUSES: 8,
94+
}
95+
6096
def _tname(obj) -> str:
6197
return type(obj).__name__
6298

@@ -512,33 +548,6 @@ def __init__(
512548
tile for meld in (melds or []) for tile in meld.tiles)
513549
self.fixed_melds = []
514550

515-
# special case: 1 & 9 of each suit + every value of honors suits
516-
THIRTEEN_ORPHANS = set(_UncheckedWu.from_str(
517-
'tong/1|tong/9|zhu/1|zhu/9|wan/1|wan/9|' # simples
518-
'feng/1|feng/2|feng/3|feng/4|long/1|long/2|long/3' # honors
519-
).tiles)
520-
521-
FLAG_FAAN = {
522-
WuFlag.CHICKEN_HAND: 0,
523-
WuFlag.COMMON_HAND: 1, WuFlag.ALL_IN_TRIPLETS: 3,
524-
WuFlag.ALL_HONOR_TILES: 10, WuFlag.ALL_ONE_SUIT: 7,
525-
WuFlag.MIXED_ONE_SUIT: 3, WuFlag.GREAT_DRAGONS: 8,
526-
WuFlag.SMALL_DRAGONS: 5, WuFlag.GREAT_WINDS: 13,
527-
WuFlag.SMALL_WINDS: 10, WuFlag.THIRTEEN_ORPHANS: 13,
528-
WuFlag.ALL_KONGS: 13, WuFlag.SELF_TRIPLETS: 8,
529-
WuFlag.ORPHANS: 10, WuFlag.NINE_GATES: 10,
530-
WuFlag.RED_DRAGON: 1, WuFlag.GREAT_WINDS: 1,
531-
WuFlag.WHITE_DRAGON: 1, WuFlag.SEAT_WIND: 1,
532-
WuFlag.PREVAILING_WIND: 1, WuFlag.MIXED_ORPHANS: 1,
533-
WuFlag.SELF_DRAW: 1, WuFlag.ALL_FROM_WALL: 1,
534-
WuFlag.ROBBING_KONG: 1, WuFlag.LAST_CATCH: 1,
535-
WuFlag.BY_KONG: 1, WuFlag.DOUBLE_KONG: 8,
536-
WuFlag.HEAVENLY: 13, WuFlag.EARTHLY: 13,
537-
WuFlag.NO_BONUSES: 1, WuFlag.ALIGNED_FLOWERS: 1,
538-
WuFlag.ALIGNED_SEASONS: 1, WuFlag.TABLE_OF_FLOWERS: 2,
539-
WuFlag.TABLE_OF_SEASONS: 2, WuFlag.HAND_OF_BONUSES: 8,
540-
}
541-
542551
if 0:
543552
# TODO: figure out why one particular one takes multiple seconds:
544553
# 1.25s (3.94s when debug): tong/1|tong/1|tong/1|tong/2|tong/2|tong/2|tong/3|tong/3|tong/3|tong/4|tong/4|tong/4|tong/5|tong/5

mahjong/players.py

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
from typing import List
1+
from typing import List, Tuple
22
from .tiles import Bonuses, Tile, BonusTile, Wind
3-
from .melds import Meld
3+
from .melds import Meld, WuFlag, faan
44

55
class Player:
66
"""Represents one Mahjong player."""
@@ -45,22 +45,32 @@ def show_meld(self, discard: Tile, meld: Meld):
4545
self.hand.remove(tile)
4646
self.shown.append(meld) # Step 19
4747

48-
def bonus_faan(self) -> int:
48+
def bonus_faan(self) -> Tuple[int, WuFlag]:
4949
"""Get faan won from bonus tiles or lack thereof."""
50-
points = 0
50+
flags = WuFlag.CHICKEN_HAND
5151
if not self.bonus:
52-
points += 1 # :(
52+
flags |= WuFlag.NO_BONUSES
5353
found = {
5454
Bonuses.GUI: [False]*4,
5555
Bonuses.HUA: [False]*4
5656
}
5757
for tile in self.bonus:
5858
if tile.number == self.seat:
59-
points += 1
59+
if tile.suit == Bonuses.HUA:
60+
flags |= WuFlag.ALIGNED_FLOWERS
61+
elif tile.suit == Bonuses.GUI:
62+
flags |= WuFlag.ALIGNED_SEASONS
6063
found[tile.suit][tile.number] = True
61-
for v in found.values():
64+
for k, v in found.items():
6265
if all(v):
63-
points += 1
66+
if k == Bonuses.HUA:
67+
flags |= WuFlag.TABLE_OF_FLOWERS
68+
elif k == Bonuses.GUI:
69+
flags |= WuFlag.TABLE_OF_SEASONS
6470
if all(map(all, found.values())):
65-
points += 8
66-
return points
71+
flags = WuFlag.HAND_OF_BONUSES
72+
if WuFlag.TABLE_OF_FLOWERS in flags:
73+
flags &= ~(WuFlag.ALIGNED_FLOWERS)
74+
if WuFlag.TABLE_OF_SEASONS in flags:
75+
flags &= ~(WuFlag.ALIGNED_SEASONS)
76+
return (faan(flags), flags)

0 commit comments

Comments
 (0)