Skip to content

Commit

Permalink
Add HandEnding.faan and .base_points
Browse files Browse the repository at this point in the history
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
  • Loading branch information
Kenny2github committed Feb 7, 2021
1 parent f6b95a0 commit d8b82b5
Show file tree
Hide file tree
Showing 4 changed files with 157 additions and 41 deletions.
12 changes: 8 additions & 4 deletions mahjong/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,13 @@
print(', '.join(map(str, p.bonus)))
#'''
#''' # uncomment this opening triple quote when commenting out one elsewhere
from mahjong.game import Game, UserIO, Question, HandEnding, HandResult
import sys
from mahjong.game import Game, Hand, UserIO, Question, HandEnding, HandResult

game = Game(extra_hands=False)
if '--game' in sys.argv:
game = Game(extra_hands=False)
else:
game = Hand(None)
print('Note: all indexes are **1-based**.')
print('This is a rudimentary text-based mahjong implementation.')
print("It's purely as a proof-of-concept/manual testing method.")
Expand Down Expand Up @@ -109,9 +113,9 @@
print('Goulash! Nobody wins. Starting next game...')
else:
assert question.choice is not None
print('Player #%s won with %s (%s faan; %s)! Starting next game...' % (
print('Player #%s won with %s (%s faan; %s points; %s)! Starting next game...' % (
question.winner.seat.value, ','.join(map(str, question.choice)),
*question.wu.faan(question.choice, (question.winner.seat, game.round.wind))
question.faan()[0], *question.points(1)
))
question = question.answer()
print('Game Over!')
Expand Down
93 changes: 93 additions & 0 deletions mahjong/game.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,34 @@
'Turn',
]

STOCK_TABLES = {
'enwp': {
1: 1, 2: 1, 3: 1,
4: 2, 5: 2, 6: 2,
7: 4, 8: 4, 9: 4,
10: 8
},
'zhwp': {
0: 1, 1: 2, 2: 4, 3: 8,
4: 16, 5: 24, 6: 32, 7: 48,
8: 64, 9: 96, 10: 128, 11: 192,
12: 256, 13: 384
},
# in the app I play,
# 3 faan = 1200c = 1x1200
# 4 faan = 2400c = 2x1200
# 5 faan = 3600c = 3x1200
# 6 faan = 4800c = 4x1200
# 7 faan = 7200c = 6x1200
# 8 faan = 9600c = 8x1200
'random_app': {
1: 1, 2: 1, 3: 1,
4: 2, 5: 3, 6: 4,
7: 6, 8: 8, 9: 10,
10: 13
}
}

# data classes

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

def faan(self) -> Tuple[int, Optional[WuFlag]]:
"""Calculate the faan for this hand."""
if self.winner is None or self.wu is None or self.choice is None:
return (0, None)
points = self.wu.faan(self.choice, (self.winner.seat, self.hand.wind))
bonus = self.winner.bonus_faan()
return (points[0] + bonus[0], points[1] | bonus[1])

def points(self, min_faan: int = 3, table: Mapping[int, int] = None) \
-> Tuple[List[int], Optional[WuFlag]]:
"""Calculate the change in points for each player.
Parameters
-----------
``min_faan``: int
Minimum faan before the hand is recognized as a valid winning hand
``table``: Mapping[int, int]
Mapping of faan to base points. The largest faan value is assumed
to be the limit, and values larger than it will be mapped to it.
Returns
--------
Tuple[List[int], Optional[WuFlag]]
A list of four point deltas, representing each player, and
the flags that apply to this hand. The former is four zeroes,
and the latter is None, on goulash or not enough faan.
"""
points = [0, 0, 0, 0]
if self.winner is None:
return (points, None)
faan, flags = self.faan()
if faan < min_faan:
return (points, None)
winner = self.winner
wu = self.wu
if table is None:
table = STOCK_TABLES['random_app']
limit = max(table.keys())
base = table[min(faan, limit)]
# special penalties that make the perpetrator pay
# for everyone else on top of themselves
if ((WuFlag.TWELVE_PIECE in wu.flags and WuFlag.SELF_DRAW in wu.flags)
or (WuFlag.GAVE_KONG in wu.flags and WuFlag.SELF_DRAW in wu.flags)
or WuFlag.GAVE_DRAGON in wu.flags) and wu.discarder is not None:
points[winner.seat] += base * 3
points[wu.discarder] -= base * 3
# loser pays double in normal losing conditions
elif wu.discarder is not None:
points[winner.seat] += base * 2
points[wu.discarder] -= base * 2
# self draw means everyone pays
elif WuFlag.SELF_DRAW in wu.flags:
for i in range(4):
if i == winner.seat:
points[i] += base * 3
else:
points[i] -= base
else:
raise ValueError('Somehow, nobody gets points. '
'Possibly report to maintainer with '
'the following information: '
+ str(self.wu.__dict__)
+ ' ;; ' + str(self.winner.__dict__))
return (points, flags)

class Question(Enum):
MELD_FROM_DISCARD_Q = 16
SHOW_EKFCP_Q = 23
Expand Down
63 changes: 36 additions & 27 deletions mahjong/melds.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,42 @@ class WuFlag(Flag):
GAVE_DRAGON = 1 << 29
GAVE_KONG = 1 << 30

# bonuses
NO_BONUSES = 1 << 31
ALIGNED_FLOWERS = 1 << 32
ALIGNED_SEASONS = 1 << 33
TABLE_OF_FLOWERS = 1 << 34
TABLE_OF_SEASONS = 1 << 35
HAND_OF_BONUSES = 1 << 36

# special case: 1 & 9 of each suit + every value of honors suits
THIRTEEN_ORPHANS = set(map(Tile.from_str, (
'tong/1|tong/9|zhu/1|zhu/9|wan/1|wan/9|' # simples
'feng/1|feng/2|feng/3|feng/4|long/1|long/2|long/3' # honors
).split('|')))

FLAG_FAAN = {
WuFlag.CHICKEN_HAND: 0,
WuFlag.COMMON_HAND: 1, WuFlag.ALL_IN_TRIPLETS: 3,
WuFlag.ALL_HONOR_TILES: 10, WuFlag.ALL_ONE_SUIT: 7,
WuFlag.MIXED_ONE_SUIT: 3, WuFlag.GREAT_DRAGONS: 8,
WuFlag.SMALL_DRAGONS: 5, WuFlag.GREAT_WINDS: 13,
WuFlag.SMALL_WINDS: 10, WuFlag.THIRTEEN_ORPHANS: 13,
WuFlag.ALL_KONGS: 13, WuFlag.SELF_TRIPLETS: 8,
WuFlag.ORPHANS: 10, WuFlag.NINE_GATES: 10,
WuFlag.RED_DRAGON: 1, WuFlag.GREAT_WINDS: 1,
WuFlag.WHITE_DRAGON: 1, WuFlag.SEAT_WIND: 1,
WuFlag.PREVAILING_WIND: 1, WuFlag.MIXED_ORPHANS: 1,
WuFlag.SELF_DRAW: 1, WuFlag.ALL_FROM_WALL: 1,
WuFlag.ROBBING_KONG: 1, WuFlag.LAST_CATCH: 1,
WuFlag.BY_KONG: 1, WuFlag.DOUBLE_KONG: 8,
WuFlag.HEAVENLY: 13, WuFlag.EARTHLY: 13,
WuFlag.TWELVE_PIECE: 0, WuFlag.GAVE_DRAGON: 0, WuFlag.GAVE_KONG: 0,
WuFlag.NO_BONUSES: 1, WuFlag.ALIGNED_FLOWERS: 1,
WuFlag.ALIGNED_SEASONS: 1, WuFlag.TABLE_OF_FLOWERS: 2,
WuFlag.TABLE_OF_SEASONS: 2, WuFlag.HAND_OF_BONUSES: 8,
}

def _tname(obj) -> str:
return type(obj).__name__

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

# special case: 1 & 9 of each suit + every value of honors suits
THIRTEEN_ORPHANS = set(_UncheckedWu.from_str(
'tong/1|tong/9|zhu/1|zhu/9|wan/1|wan/9|' # simples
'feng/1|feng/2|feng/3|feng/4|long/1|long/2|long/3' # honors
).tiles)

FLAG_FAAN = {
WuFlag.CHICKEN_HAND: 0,
WuFlag.COMMON_HAND: 1, WuFlag.ALL_IN_TRIPLETS: 3,
WuFlag.ALL_HONOR_TILES: 10, WuFlag.ALL_ONE_SUIT: 7,
WuFlag.MIXED_ONE_SUIT: 3, WuFlag.GREAT_DRAGONS: 8,
WuFlag.SMALL_DRAGONS: 5, WuFlag.GREAT_WINDS: 13,
WuFlag.SMALL_WINDS: 10, WuFlag.THIRTEEN_ORPHANS: 13,
WuFlag.ALL_KONGS: 13, WuFlag.SELF_TRIPLETS: 8,
WuFlag.ORPHANS: 10, WuFlag.NINE_GATES: 10,
WuFlag.RED_DRAGON: 1, WuFlag.GREAT_WINDS: 1,
WuFlag.WHITE_DRAGON: 1, WuFlag.SEAT_WIND: 1,
WuFlag.PREVAILING_WIND: 1, WuFlag.MIXED_ORPHANS: 1,
WuFlag.SELF_DRAW: 1, WuFlag.ALL_FROM_WALL: 1,
WuFlag.ROBBING_KONG: 1, WuFlag.LAST_CATCH: 1,
WuFlag.BY_KONG: 1, WuFlag.DOUBLE_KONG: 8,
WuFlag.HEAVENLY: 13, WuFlag.EARTHLY: 13,
WuFlag.NO_BONUSES: 1, WuFlag.ALIGNED_FLOWERS: 1,
WuFlag.ALIGNED_SEASONS: 1, WuFlag.TABLE_OF_FLOWERS: 2,
WuFlag.TABLE_OF_SEASONS: 2, WuFlag.HAND_OF_BONUSES: 8,
}

if 0:
# TODO: figure out why one particular one takes multiple seconds:
# 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
Expand Down
30 changes: 20 additions & 10 deletions mahjong/players.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from typing import List
from typing import List, Tuple
from .tiles import Bonuses, Tile, BonusTile, Wind
from .melds import Meld
from .melds import Meld, WuFlag, faan

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

def bonus_faan(self) -> int:
def bonus_faan(self) -> Tuple[int, WuFlag]:
"""Get faan won from bonus tiles or lack thereof."""
points = 0
flags = WuFlag.CHICKEN_HAND
if not self.bonus:
points += 1 # :(
flags |= WuFlag.NO_BONUSES
found = {
Bonuses.GUI: [False]*4,
Bonuses.HUA: [False]*4
}
for tile in self.bonus:
if tile.number == self.seat:
points += 1
if tile.suit == Bonuses.HUA:
flags |= WuFlag.ALIGNED_FLOWERS
elif tile.suit == Bonuses.GUI:
flags |= WuFlag.ALIGNED_SEASONS
found[tile.suit][tile.number] = True
for v in found.values():
for k, v in found.items():
if all(v):
points += 1
if k == Bonuses.HUA:
flags |= WuFlag.TABLE_OF_FLOWERS
elif k == Bonuses.GUI:
flags |= WuFlag.TABLE_OF_SEASONS
if all(map(all, found.values())):
points += 8
return points
flags = WuFlag.HAND_OF_BONUSES
if WuFlag.TABLE_OF_FLOWERS in flags:
flags &= ~(WuFlag.ALIGNED_FLOWERS)
if WuFlag.TABLE_OF_SEASONS in flags:
flags &= ~(WuFlag.ALIGNED_SEASONS)
return (faan(flags), flags)

0 comments on commit d8b82b5

Please sign in to comment.