Created
July 2, 2014 10:16
-
-
Save zheplusplus/6208b9658ce737a751e9 to your computer and use it in GitHub Desktop.
Mahjong Waits Algorithm
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class Group(object): | |
pass | |
class Sequence(Group): | |
def __init__(self, suit, starting_rank): | |
self.suit = suit | |
self.starting_rank = starting_rank | |
class Triplet(Group): | |
def __init__(self, suit, rank): | |
self.suit = suit | |
self.rank = rank | |
class NoGroup(object): | |
def value(self): | |
return None | |
def __add__(self, rhs): | |
return self | |
def append_group(self, group): | |
return self | |
class Groups(list, NoGroup): | |
def __init__(self, value=None): | |
list.__init__(self, value or []) | |
def value(self): | |
return self | |
def __add__(self, rhs): | |
if type(rhs) is NoGroup: | |
return NoGroup() | |
return Groups(list.__add__(self, rhs)) | |
def append_group(self, group): | |
self.append(group) | |
return self |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class Tile(object): | |
def __init__(self, suit, rank): | |
self.suit = suit | |
self.rank = rank | |
def __eq__(self, rhs): | |
return self.suit == rhs.suit and self.rank == rhs.rank | |
def __hash__(self): | |
return hash(self.suit) * hash(self.rank) | |
def __repr__(self): | |
return (self.suit, self.rank + 1).__repr__() | |
class TilesSuite(object): | |
def __init__(self, count, suit): | |
self.rank_count = [0] * count | |
self.suit = suit | |
def count(self, rank): | |
return self.rank_count[rank] | |
def add_rank(self, rank): | |
self.rank_count[rank] += 1 | |
def copy_rank(self): | |
return self.rank_count[:] | |
def list_tiles(self): | |
return [(Tile(self.suit, i), e) | |
for i, e in enumerate(self.rank_count) if e != 0] | |
def total(self): | |
return sum(self.rank_count) | |
WIND_SUIT = 3 | |
DRAGON_SUIT = 4 | |
class HandTiles(object): | |
def __init__(self): | |
self.numerics = [TilesSuite(9, i) for i in xrange(3)] | |
self.wind = TilesSuite(4, WIND_SUIT) | |
self.dragon = TilesSuite(3, DRAGON_SUIT) | |
def total(self): | |
return (sum([self.numerics[i].total() for i in xrange(3)]) + | |
self.wind.total() + self.dragon.total()) | |
def list_tiles(self): | |
return (sum([self.numerics[i].list_tiles() for i in xrange(3)], []) + | |
self.wind.list_tiles() + self.dragon.list_tiles()) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import tile | |
import group | |
def waits(tiles): | |
return set(_waits_13(tiles) + _waits_7pairs(tiles) + _waits_4groups(tiles)) | |
_13_ORPHANS_WAITS = ( | |
[tile.Tile(0, 0), tile.Tile(0, 8), tile.Tile(1, 0), tile.Tile(1, 8), | |
tile.Tile(2, 0), tile.Tile(2, 8)] + | |
[tile.Tile(tile.WIND_SUIT, i) for i in xrange(4)] + | |
[tile.Tile(tile.DRAGON_SUIT, i) for i in xrange(3)]) | |
def _waits_13(tiles): | |
if tiles.total() != 13: | |
return [] | |
orphans = sorted( | |
[(tile.Tile(s, 0), s.count(0)) for s in tiles.numerics] + | |
[(tile.Tile(s, 8), s.count(8)) for s in tiles.numerics] + | |
[(tile.Tile(tile.WIND_SUIT, i), tiles.wind.count(i)) | |
for i in xrange(4)] + | |
[(tile.Tile(tile.DRAGON_SUIT, i), tiles.dragon.count(i)) | |
for i in xrange(3)], key=lambda k: k[1]) | |
if orphans[1][1] == 0: | |
return [] | |
if orphans[0][1] == 1: | |
return _13_ORPHANS_WAITS | |
return [orphans[0][0]] | |
def _waits_7pairs(tiles): | |
if tiles.total() != 13: | |
return [] | |
tiles_list = tiles.list_tiles() | |
# NOTE: Japanese Mahjong doesn't allow two same pairs in 7-pairs | |
# in that case the predicate should be | |
# 7 != len(tiles_list) | |
if 7 < len(tiles_list): | |
return [] | |
odds = [t[0] for t in tiles_list if t[1] % 2 == 1] | |
return odds if len(odds) == 1 else [] | |
def _waits_4groups(tiles): | |
triplets, pairs, singles = _wind_dragon_groups(tiles) | |
if pairs and singles: | |
return [] | |
if 1 == len(singles): | |
groups = _detect_completed_numeric_groups(tiles) | |
return [] if groups is None else [singles[0][0]] | |
if 2 == len(pairs): | |
groups = _detect_completed_numeric_groups(tiles) | |
return [] if groups is None else [pairs[0][0], pairs[1][0]] | |
if 1 == len(pairs): | |
waits, pair_wait = _detect_numeric_suit_with_two_more(tiles) | |
if len(waits) == 0: | |
return [] | |
if pair_wait: | |
waits.append(pairs[0][0]) | |
return waits | |
return (_detect_numeric_suit_with_one_more(tiles) + | |
_detect_2_numeric_suits_with_2_more(tiles)) | |
def _wind_dragon_groups(tiles): | |
tiles_list = tiles.wind.list_tiles() + tiles.dragon.list_tiles() | |
if len(tiles_list) == 0: | |
return [], [], [] | |
singles = [t for t in tiles_list if t[1] == 1] | |
pairs = [t for t in tiles_list if t[1] == 2] | |
triplets = [t for t in tiles_list if t[1] == 3] | |
quads = [t for t in tiles_list if t[1] == 4] | |
return triplets + quads, pairs, singles + quads | |
def _detect_completed_numeric_groups(tiles): | |
for i in xrange(3): | |
if tiles.numerics[i].total() % 3 != 0: | |
return None | |
return sum([_completed_numeric_groups(i, tiles.numerics[i].copy_rank()) | |
for i in xrange(3)], group.Groups()).value() | |
def _detect_numeric_suit_with_one_more(tiles): | |
mod_numerics = sorted([(i, tiles.numerics[i].total() % 3) | |
for i in xrange(3)], key=lambda k: -k[1]) | |
if mod_numerics[0][1] != 1 or mod_numerics[1][1] != 0: | |
return [] | |
suit_one_more = mod_numerics[0][0] | |
group_suit_a, group_suit_b = mod_numerics[1][0], mod_numerics[2][0] | |
completed_groups = ( | |
_completed_numeric_groups( | |
group_suit_a, tiles.numerics[group_suit_a].copy_rank()) + | |
_completed_numeric_groups( | |
group_suit_b, tiles.numerics[group_suit_b].copy_rank()) | |
).value() | |
if completed_groups is None: | |
return [] | |
tiles = tiles.numerics[suit_one_more].copy_rank() | |
result = [] | |
for i, c in enumerate(tiles): | |
if c != 0: | |
copy_tiles = tiles[:] | |
copy_tiles[i] -= 1 | |
groups = _completed_numeric_groups(suit_one_more, copy_tiles) | |
if groups.value() is not None: | |
result.append(tile.Tile(suit_one_more, i)) | |
if c >= 2: | |
copy_tiles = tiles[:] | |
copy_tiles[i] -= 2 | |
result.extend(_numeric_suit_with_two_more( | |
suit_one_more, copy_tiles)[0]) | |
return result | |
def _detect_numeric_suit_with_two_more(tiles): | |
mod_numerics = sorted([(i, tiles.numerics[i].total() % 3) | |
for i in xrange(3)], key=lambda k: -k[1]) | |
if mod_numerics[0][1] != 2 or mod_numerics[1][1] != 0: | |
return [], False | |
group_suit_a, group_suit_b = mod_numerics[1][0], mod_numerics[2][0] | |
completed_groups = ( | |
_completed_numeric_groups( | |
group_suit_a, tiles.numerics[group_suit_a].copy_rank()) + | |
_completed_numeric_groups( | |
group_suit_b, tiles.numerics[group_suit_b].copy_rank()) | |
).value() | |
if completed_groups is None: | |
return [], False | |
suit_two_more = mod_numerics[0][0] | |
return _numeric_suit_with_two_more( | |
suit_two_more, tiles.numerics[suit_two_more].copy_rank()) | |
def _detect_2_numeric_suits_with_2_more(tiles): | |
mod_numerics = sorted([(i, tiles.numerics[i].total() % 3) | |
for i in xrange(3)], key=lambda k: -k[1]) | |
if mod_numerics[0][1] != 2 or mod_numerics[1][1] != 2: | |
return [] | |
group_suit = mod_numerics[2][0] | |
completed_groups = _completed_numeric_groups( | |
group_suit, tiles.numerics[group_suit].copy_rank()).value() | |
if completed_groups is None: | |
return [] | |
suit_2more_a, suit_2more_b = mod_numerics[0][0], mod_numerics[1][0] | |
waits_a, flag_pair_a = _numeric_suit_with_two_more( | |
suit_2more_a, tiles.numerics[suit_2more_a].copy_rank()) | |
waits_b, flag_pair_b = _numeric_suit_with_two_more( | |
suit_2more_b, tiles.numerics[suit_2more_b].copy_rank()) | |
result = [] | |
if flag_pair_a: | |
result.extend(waits_b) | |
if flag_pair_b: | |
result.extend(waits_a) | |
return result | |
def _numeric_suit_with_two_more(suit, tiles): | |
result = [tile.Tile(suit, pair_rank) for pair_rank, groups in | |
_numeric_groups_with_pair(suit, tiles[:])] | |
flag_pair = len(result) != 0 | |
for starting_rank, groups in _numeric_groups_with_side_waits( | |
suit, tiles[:]): | |
if starting_rank != 0: | |
result.append(tile.Tile(suit, starting_rank - 1)) | |
if starting_rank != 7: | |
result.append(tile.Tile(suit, starting_rank + 2)) | |
result.extend([ | |
tile.Tile(suit, staring_rank + 1) | |
for staring_rank, groups in _numeric_groups_with_middle_waits( | |
suit, tiles[:])]) | |
return result, flag_pair | |
def _numeric_groups_with_pair(suit, suit_tiles): | |
result = [] | |
for i, c in enumerate(suit_tiles): | |
if c >= 2: | |
copy_tiles = suit_tiles[:] | |
copy_tiles[i] -= 2 | |
groups = _completed_numeric_groups(suit, copy_tiles) | |
if groups.value() is not None: | |
result.append((i, groups)) | |
return result | |
def _numeric_groups_with_side_waits(suit, suit_tiles): | |
result = [] | |
for i in xrange(8): | |
if suit_tiles[i] and suit_tiles[i + 1]: | |
copy_tiles = suit_tiles[:] | |
copy_tiles[i] -= 1 | |
copy_tiles[i + 1] -= 1 | |
groups = _completed_numeric_groups(suit, copy_tiles) | |
if groups.value() is not None: | |
result.append((i, groups)) | |
return result | |
def _numeric_groups_with_middle_waits(suit, suit_tiles): | |
result = [] | |
for i in xrange(7): | |
if suit_tiles[i] and suit_tiles[i + 2]: | |
copy_tiles = suit_tiles[:] | |
copy_tiles[i] -= 1 | |
copy_tiles[i + 2] -= 1 | |
groups = _completed_numeric_groups(suit, copy_tiles) | |
if groups.value() is not None: | |
result.append((i, groups)) | |
return result | |
def _completed_numeric_groups(suit, suit_tiles): | |
for i, c in enumerate(suit_tiles): | |
if c != 0: | |
first_rank = i | |
break | |
else: | |
return group.Groups() | |
if suit_tiles[first_rank] >= 3: | |
tri = group.Triplet(suit, first_rank) | |
suit_tiles[first_rank] -= 3 | |
return _completed_numeric_groups(suit, suit_tiles).append_group(tri) | |
if (first_rank > 6 or suit_tiles[first_rank] > suit_tiles[first_rank + 1] | |
or suit_tiles[first_rank] > suit_tiles[first_rank + 2]): | |
return group.NoGroup() | |
seqs = [group.Sequence(suit, first_rank) | |
for _ in xrange(suit_tiles[first_rank])] | |
suit_tiles[first_rank + 1] -= suit_tiles[first_rank] | |
suit_tiles[first_rank + 2] -= suit_tiles[first_rank] | |
suit_tiles[first_rank] = 0 | |
return _completed_numeric_groups(suit, suit_tiles) + seqs |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment