def is_ready(cls, arr: list, left: list = None): # block error parameter if not arr: raise ValueError("arr is empty") length = len(arr) if length not in cls.concealed_count: raise ValueError(f"concealed length error: {length}") if left: # left tiles set keys = left else: # total mj set MjSet.generate_dictionary() keys = [key for key in MjSet.dictionary] # need 1 tile candidates = [] for key in keys: test = arr + [key] test.sort() if cls.is_good_concealed(test): candidates.append(key) if candidates: return tuple((True, candidates)) return tuple((False, []))
def test_mj_set(self): mj_set = MjSet(flower=True) count = len(mj_set.tiles) self.assertEqual(count, 144) mj_set = MjSet(flower=False) count = len(mj_set.tiles) self.assertEqual(count, 136)
def test_decide_discard_by_value(): mj_set = MjSet() ai = PlayerAiAdv("bot02") for _ in range(3): ai.draw(mj_set) mj_set.shuffle() for _ in range(11): ai.draw(mj_set) ai.decide_discard_by_value(wall=mj_set)
def test_decide_discard_by_remove_melds(): mj_set = MjSet() ai = PlayerAiPro("bot01") for _ in range(3): ai.draw(mj_set) mj_set.shuffle() for _ in range(11): ai.draw(mj_set) ai.decide_discard_by_remove_melds()
def __init__(self, players: list = None, prevailing_wind: str = '东', flower=False): if not players: raise ValueError("mahjong needs player!") self._mj_set = MjSet(flower) self.out_of_tiles = False self.robbing_a_kong = False self.mahjong_on_kong = False self._players = players self._positions = dict() for index, player in enumerate(self._players): wind = Suit.get_wind_by_index(index) player.position = wind self._positions[wind] = player self._dealer: Player = self._players[0] self._prevailing_wind = prevailing_wind self._winner = None self.firer = None # state machine self._state_machine = HandStateMachine() self._states = ["begin", "prepared", "playing", "scoring", "end"] self._transitions = [ { 'trigger': 'prepare', 'source': 'begin', 'dest': 'prepared' }, # 准备 { 'trigger': 'deal', 'source': 'prepared', 'dest': 'playing' }, # 拿四张 { 'trigger': 'mahjong', 'source': 'playing', 'dest': 'scoring' }, # 胡牌 { 'trigger': 'withdraw', 'source': 'playing', 'dest': 'scoring' }, # 流局 { 'trigger': 'score', 'source': 'scoring', 'dest': 'end' }, # 算分 ] Machine(model=self._state_machine, states=self._states, transitions=self._transitions, initial='begin')
def test_convert(): mj_set = MjSet() mj_set.shuffle() concealed = [] for _ in range(13): concealed.append(mj_set.draw()) print(Rule.convert_tiles_to_str(concealed)) arr = Rule.convert_tiles_to_arr(concealed) print(arr) tiles = Rule.convert_arr_to_tiles(arr) print(Rule.convert_tiles_to_str(tiles))
def test_decide_discard_by_left_meld_and_eye(): mj_set = MjSet() ai = PlayerAiAdv("bot02") for _ in range(3): ai.draw(mj_set) mj_set.shuffle() for _ in range(11): ai.draw(mj_set) ai.sort_concealed() print("ai.concealed_str =", ai.concealed_str) result = ai.decide_discard_by_left_meld_and_eye() for x in result: print(x, Rule.convert_tiles_to_str(Rule.convert_arr_to_tiles(result[x])))
def draw_from_back(self, mj_set: MjSet): tile = mj_set.draw_from_back() if not tile: print("mj_set.draw_from_back() None") return None while Rule.is_flower(tile) and mj_set.tiles: # (self, 'get a flower from back:', tile) self.flowers.append(tile) self._draw_count += 1 tile = mj_set.draw_from_back() if not tile: print("mj_set.draw_from_back() is_flower None") return None self.add(tile) return tile
def test_player_ai_base(self): mj_set = MjSet() ai = PlayerAi("貂蝉") for _ in range(13): ai.draw(mj_set) self.assertEqual(len(ai.concealed), 13) mj_set.shuffle() for _ in range(10): ai.draw(mj_set) tile = ai.decide_discard() self.assertIsNotNone(tile) ai.discard(tile) self.assertEqual(len(ai.concealed), 13) self.assertEqual(len(ai.discarded), 10)
def test_hand(): mj_set = MjSet() mj_set.shuffle() # ai = PlayerAiPro("bot02") ai = PlayerAiAdv("bot02") for _ in range(13): ai.draw(mj_set) ai.sort_concealed() print("concealed: " + Rule.convert_tiles_to_str(ai.concealed)) mj_set.shuffle() for _ in range(100): tile = ai.draw(mj_set) print(f"draw: {tile}") if Rule.is_mahjong(ai.concealed): print("Mahjong!!!") break tile = ai.decide_discard() print(f"discard: {tile}") ai.discard(tile) ai.sort_concealed() print("concealed: " + Rule.convert_tiles_to_str(ai.concealed)) print("discarded: " + Rule.convert_tiles_to_str(ai.discarded)) print(f"strategies: {ai.strategies}") print(f"strategies_time: {ai.strategies_time}") print("draw count: " + str(ai.draw_count))
def test_is_ready(): mj_set = MjSet() concealed = [] for _ in range(4): concealed.append(mj_set.draw()) concealed.append(mj_set.draw()) concealed.append(mj_set.draw()) mj_set.draw() concealed.append(mj_set.draw()) print(Rule.convert_tiles_to_str(concealed)) print(Rule.is_ready(concealed))
def draw(self, mj_set: MjSet): tile = mj_set.draw() if not tile: print("mj_set.draw() None") return None if not Rule.is_flower(tile): self.add(tile) self._draw_count += 1 return tile print(self, 'get a flower:', tile) self.flowers.append(tile) tile = self.draw_from_back(mj_set) print("draw_from_back:", tile) return tile
def test_ai_mahjong(self): mj_set = MjSet() ai = PlayerAi("西施") mj_set.shuffle() for _ in range(13): ai.draw(mj_set) ai.sort_concealed() mj_set.shuffle() mahjong = False for _ in range(100): tile = ai.draw(mj_set) if Rule.is_mahjong(ai.concealed): mahjong = True break tile = ai.decide_discard() ai.discard(tile) ai.sort_concealed() # self.assertTrue(mahjong) self.assertGreater(len(ai.concealed), 0)
def value_arr(cls, arr: list, left=None, players_count: int = 1, deep: int = 2) -> MjValue: if not arr: value = MjValue(meld=0, orphan=0, count_down=0, is_ready=False, waiting=[]) return value if not left: # total tile types, same chance MjSet.generate_dictionary() left = [key for key in MjSet.dictionary] count_all = len(left) # basic info melds_count = cls.get_most_melds_length_from_arr(arr) orphans_arr = cls.get_orphans(arr) orphans_count = len(orphans_arr) result = MjValue(meld=melds_count, orphan=orphans_count) # is ready test ready_info = cls.is_ready(arr) is_ready = ready_info[0] if is_ready: combins = ready_info[1] if not combins: combins = [] waiting = [] fail_chance = 1 for x in combins: chance = cls.chance_of_combin_and_wall( wall=left, combin=[x], players_count=players_count) fail_chance *= 1 - chance value = 1 w = ([x], chance, value) waiting.append(w) total_chance = 1 - fail_chance result.is_ready = is_ready result.listening = waiting result.mahjong_chance = total_chance # count down test begin = time() count_down_result = cls.countdown_of_ready(arr, left=left, deep=deep) # ("count_down duration = %0.2f" % (time() - begin)) # ("count_down_result =", count_down_result) count_down = count_down_result[0] result.count_down = count_down if count_down > max(cls.concealed_count): return result combins = count_down_result[1] waiting = [] value = 1 total_chance = 0 if count_down <= 3 and len(combins) > 0: for combin in combins: chance = cls.chance_of_combin_and_wall( wall=left, combin=combin, players_count=players_count) total_chance += chance w = (combin, chance, value) waiting.append(w) result.waiting = waiting result.waiting_chance = total_chance return result
def countdown_of_ready(cls, concealed: list, left: list = None, deep: int = 2): arr = concealed[:] arr.sort() # block error parameter if left is None: left = [] if not arr: raise ValueError("arr is empty") length = len(arr) if length not in cls.concealed_count: raise ValueError(f"concealed length error: {length}") if deep < 1 or 4 < deep: raise ValueError(f"deep {deep}, should be in [1,2,3,4,5]") if left: # left tiles set keys = left else: # total mj set MjSet.generate_dictionary() keys = [key for key in MjSet.dictionary] # remove the keys that not relative to 'arr' elements for key in keys[::-1]: if key not in arr \ and key + 1 not in arr \ and key - 1 not in arr: keys.remove(key) # need how many tiles? candidates = [] countdown = [] # best combinations for arr combins = MjMath.get_best_meld_combins_from_arr(arr) for combin in combins: completed = [] for meld in combin: completed = completed + meld shorter = MjMath.list_sub(arr, completed) shorter_len = len(shorter) if shorter_len >= 11: return (999, None) for drop in range(1, deep + 1): if drop >= shorter_len: continue for remove in combinations(shorter, drop): remains = MjMath.list_sub(shorter, list(remove)) for add in combinations(keys, drop + 1): test = remains + list(add) more = list(add) more.sort() if more in candidates: continue if MjMath.has_orphan(test): continue if len(set(test)) == len(test): continue test.sort() if cls.is_good_concealed(test): candidates.append(more) countdown.append(drop + 1) if candidates: return tuple((min(countdown), candidates)) # default return tuple((999, None))
def prepare(self): self._mj_set = MjSet() self.choice_dealer() self._state_machine.prepare()
class Hand(object): __slots__ = ("_state_machine", "_machine", "_states", "_transitions", "_mj_set", "_players", "_prevailing_wind", "_dealer", '_positions', '_winner', 'firer', 'out_of_tiles', 'robbing_a_kong', 'mahjong_on_kong') def __init__(self, players: list = None, prevailing_wind: str = '东', flower=False): if not players: raise ValueError("mahjong needs player!") self._mj_set = MjSet(flower) self.out_of_tiles = False self.robbing_a_kong = False self.mahjong_on_kong = False self._players = players self._positions = dict() for index, player in enumerate(self._players): wind = Suit.get_wind_by_index(index) player.position = wind self._positions[wind] = player self._dealer: Player = self._players[0] self._prevailing_wind = prevailing_wind self._winner = None self.firer = None # state machine self._state_machine = HandStateMachine() self._states = ["begin", "prepared", "playing", "scoring", "end"] self._transitions = [ { 'trigger': 'prepare', 'source': 'begin', 'dest': 'prepared' }, # 准备 { 'trigger': 'deal', 'source': 'prepared', 'dest': 'playing' }, # 拿四张 { 'trigger': 'mahjong', 'source': 'playing', 'dest': 'scoring' }, # 胡牌 { 'trigger': 'withdraw', 'source': 'playing', 'dest': 'scoring' }, # 流局 { 'trigger': 'score', 'source': 'scoring', 'dest': 'end' }, # 算分 ] Machine(model=self._state_machine, states=self._states, transitions=self._transitions, initial='begin') # print(self.state) def __str__(self): return '\r\n'.join([f'{x.position}:{x}' for x in self._players]) @property def state(self): return self._state_machine.state @property def players(self): return self._players @property def mj_set(self): return self._mj_set @property def dealer(self): return self._dealer @property def winner(self): return self._winner @property def positions(self): return self._positions def _shuffle(self): self._mj_set.shuffle() def _reset_players(self): for player in self._players: player.reset() def _get_next_player(self, current): bingo = False for player in cycle(self.players): if bingo: return player if current == player: bingo = True raise LookupError(f"can not find next player for {current}") def prepare(self): self._reset_players() self._state_machine.prepare() self._shuffle() def deal(self): for _ in range(max(MjMath.concealed_count) // 4): for player in self._players: player.draw_stack(self._mj_set) for player in self.players: player.draw(self._mj_set) player.sort_concealed() self._state_machine.deal() def play(self): current = self._dealer # the current player player = current before = None # the previous player current_discard = None # discard tile by previous player for player in self._players: print(f'{player.position}: {player}') print("=" * 40) have_winner = False while not have_winner: if current_discard: wind = current.position player = current for index in range(3): # test for hu by other test = player.concealed[:] test.append(current_discard) if Rule.is_mahjong(test): player.concealed.append(current_discard) print(f"winner is {player}, by {before}") self._winner = player self._state_machine.mahjong() have_winner = True break wind = Suit.get_next_wind(wind) player = self.positions[wind] if have_winner: break # interrupted by exposed kong / pong / chow interrupted = False if current_discard: # Melding another for current, +1, +2 players player = None # try kong ( must have tiles ): if self.mj_set.tiles: wind = current.position player = current for index in range(3): if player.try_exposed_kong(tile=current_discard, owner=before, mj_set=self._mj_set): interrupted = True break wind = Suit.get_next_wind(wind) player = self.positions[wind] # try pong: if not interrupted: wind = current.position player = current for index in range(3): if player.try_exposed_pong(tile=current_discard, owner=before): interrupted = True break wind = Suit.get_next_wind(wind) player = self.positions[wind] # try chow: if not interrupted: wind = current.position player = current if player.try_exposed_chow(current_discard, before): interrupted = True if not interrupted: before.put_on_desk(current_discard) # end if current_discard: if interrupted: current = player else: # test for out of tiles if not self.mj_set.tiles: print("out of tiles!") self._winner = None self._state_machine.withdraw() break # draw current.draw(self._mj_set) # test for hu by self if Rule.is_mahjong(current.concealed): print(f"winner is {current}, by self!") self._winner = current self._state_machine.mahjong() break # self kong current.try_conceal_kong(self.mj_set) if isinstance(current, PlayerAiAdv): wall = self._mj_set.tiles tile = current.decide_discard(players_count=len(self.players), wall=wall) else: tile = current.decide_discard() print(current, 'discard:', tile) current.discard(tile) current.sort_concealed() current_discard = tile # (f'{current} discard {tile}') # (Rule.convert_tiles_to_str(current.concealed)) # next player next = self._get_next_player(current) before = current current = next # end while print("tiles left :", len(self.mj_set.tiles)) for player in self.players: if player == self.winner: print(f"winner {player}: ", Rule.convert_tiles_to_str(player.concealed)) else: print(f"{player}: ", Rule.convert_tiles_to_str(player.concealed)) left = Rule.convert_tiles_to_arr(self.mj_set.tiles) left.sort() print(self.players[0].strategies) print(self.players[0].strategies_time) # end def play() def score(self): # score the play result self._state_machine.score() pass