def decide_discard_by_value(self, wall=None, players_count: int = 4, deep: int = 2): left = Rule.convert_tiles_to_arr(wall) arr = Rule.convert_tiles_to_arr(self.concealed) arr.sort() if deep == 1: # (arr) pass keys = list(set(arr)) candidates = dict() self.sort_concealed() # ("self.concealed_str =", self.concealed_str) # ("self.concealed =", Rule.convert_tiles_to_arr(self.concealed)) for key in keys: test = arr[:] test.remove(key) mj_value = MjMath.value_arr(test, left=left, players_count=players_count, deep=deep) value = MjMath.convert_mj_value_to_int(mj_value) # (value) if key not in candidates: candidates[key] = value else: raise LookupError(f"duplicate key: {key}") max_chance = 0 # find max_chance for key in candidates: if candidates[key] == 0: # don't care about candidate who have no chance continue if max_chance < candidates[key]: max_chance = candidates[key] if not max_chance: return None # find candidate who have the min chance temp = [] for key in candidates: if candidates[key] == max_chance: temp.append(key) if not temp: return None tiles = Rule.convert_arr_to_tiles(temp) return tiles
def test_get_melds_from_arr(self): arr = [1, 1, 1, 2, 3, 4, 5, 6, 7, 8, 9, 9, 9] # 九宝莲灯 combins = MjMath.get_best_meld_combins_from_arr(arr) should_be = '''[[], [1, 1, 1], [3, 4, 5], [6, 7, 8], [9, 9, 9]] [[], [1, 1, 1], [2, 3, 4], [5, 6, 7], [9, 9, 9]] [[], [1, 1, 1], [2, 3, 4], [6, 7, 8], [9, 9, 9]]''' test = "\n".join([f'{x}' for x in combins]) self.assertEqual(test, should_be) arr = [50, 50, 50, 60, 60, 60, 70, 70, 70, 80, 80] # 小四喜 combins = MjMath.get_best_meld_combins_from_arr(arr) should_be = """[[], [50, 50, 50], [60, 60, 60], [70, 70, 70]]""" test = "\n".join([f'{x}' for x in combins]) self.assertEqual(test, should_be) # 随机牌组 mj_set = MjSet() mj_set.shuffle() tiles = [] for _ in range(13): tiles.append(mj_set.draw()) arr = Rule.convert_tiles_to_arr(tiles) melds = MjMath.get_best_meld_combins_from_arr(arr) test = "\n".join([f'{x}' for x in melds]) self.assertTrue(test.index('[[]') == 0)
def try_exposed_chow(self, tile: Tile, owner) -> bool: arr = Rule.convert_tiles_to_arr(self.concealed) outer = tile.key combins = MjMath.get_chow_combins_from_arr(arr=arr, outer=outer) if not combins: return False # convert combins to index arrays choices = [] for combin in combins: choice = [] for x in combin: index = arr.index(x) choice.append(index) choices.append(choice) allowed_cmd = ['chow', 'cancel'] cmd = self.waiting_4_cmd(allowed_cmd=allowed_cmd, choices=choices) if cmd == 'cancel': return False elif cmd == 'chow': arr = choices[self.current_index] inners = [] for index in arr: inners.append(self._concealed[index]) super().exposed_chow(inners=inners, tile=tile, owner=owner) return True else: raise ValueError("exposed chow error cmd:", cmd)
def decide_discard_by_is_ready(self): arr = Rule.convert_tiles_to_arr(self.concealed) arr_sorted = arr[:] arr_sorted.sort() candidates = dict() for x in arr_sorted: test = arr_sorted[:] test.remove(x) result = MjMath.is_ready(test) if result[0]: # is_ready! if x not in candidates: candidates[x] = set() candidates[x] = set(result[1]) else: continue if not candidates: return None arr_len = [len(candidates[x]) for x in candidates] max_len = max(arr_len) best_choices = [] for x in candidates: if len(candidates[x]) == max_len: best_choices.append(x) one = choice(best_choices) tile = Rule.convert_key_to_tile(one) return tile
def decide_discard_by_random_orphan(self): arr = Rule.convert_tiles_to_arr(self.concealed) orphans_arr = MjMath.get_orphans(arr) if not orphans_arr: return None one = choice(orphans_arr) tile = Rule.convert_key_to_tile(one) return tile
def try_exposed_chow(self, tile: Tile, owner) -> bool: arr = Rule.convert_tiles_to_arr(self.concealed) outer = tile.key # 胡吃一气 combins = MjMath.get_chow_combins_from_arr(arr=arr, outer=outer) if not combins: return False combin = choice(combins) inners = Rule.convert_arr_to_tiles(combin) self.exposed_chow(inners=inners, tile=tile, owner=owner) return True
def decide_discard_by_loneliest_orphan(self, keep_one_orphan: bool = False): arr = Rule.convert_tiles_to_arr(self.concealed) orphans_arr = MjMath.get_orphans(arr) if keep_one_orphan: if len(orphans_arr) <= 1: return None else: if len(orphans_arr) <= 0: return None loneliest_arr = MjMath.get_loneliest_from_arr(orphans_arr) if not loneliest_arr: return None one = choice(loneliest_arr) tile = Rule.convert_key_to_tile(one) return tile
def decide_discard_by_not_in_meld(self): arr = Rule.convert_tiles_to_arr(self.concealed) not_123 = MjMath.get_not_in_123(arr) not_pair = MjMath.get_not_in_pair(arr) if (not not_123) and (not not_pair): return None loneliest = not_123 & not_pair key = 0 if loneliest: key = choice(list(loneliest)) if not_pair: key = choice(list(not_pair)) if not_123: key = choice(list(not_123)) if key: return Rule.convert_key_to_tile(key) return None
def decide_discard_by_remove_melds(self): arr = Rule.convert_tiles_to_arr(self.concealed) combins = MjMath.get_best_meld_combins_from_arr(arr) if not combins: return None remain_combins = [] for combin in combins: arr_completed = [] for meld in combin: arr_completed += meld arr_remain = MjMath.list_sub(arr, arr_completed) arr_remain.sort() remain_combins.append(arr_remain) loneliest_arrs = [] for combin in remain_combins: loneliest_arr = MjMath.get_loneliest_from_arr(combin) loneliest_arrs.append(loneliest_arr) # select orphans from loneliest_arrs orphans = [] for arr in loneliest_arrs: orphans += arr test = list(set(orphans)) test.sort() orphans = MjMath.get_orphans(test) if orphans: one = choice(orphans) tile = Rule.convert_key_to_tile(one) return tile # select one from loneliest_arrs by distance loneliest_candidates = MjMath.get_loneliest_from_arr(test) if not loneliest_candidates: raise LookupError( f"loneliest_candidates of {test} is empty! I can't believe that!" ) one = choice(loneliest_candidates) tile = Rule.convert_key_to_tile(one) return tile
def decide_discard_by_left_meld_and_eye(self): arr = Rule.convert_tiles_to_arr(self.concealed) arr.sort() keys = list(set(arr)) candidates = dict() for key in keys: test = arr[:] test.remove(key) result = MjMath.count_of_melds_and_eys(test) count = result[0] if result[1]: # has eye count += 1 if count not in candidates: candidates[count] = [] candidates[count].append(key) if not candidates: return None if len(candidates) == 1: return None len_arr = [key for key in candidates] max_len = max(len_arr) return Rule.convert_arr_to_tiles(candidates[max_len])
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 and not self.out_of_tiles: if current_discard: wind = current.position player = current for index in range(3): # test for hu by other if player.try_mahjong(current_discard): player.concealed.append(current_discard) print(f"winner is {player}, by {before}") self._winner = player self.firer = before self._state_machine.mahjong() have_winner = True self.refresh_screen(state='mahjong') 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): try: if player.try_exposed_kong(tile=current_discard, owner=before, mj_set=self._mj_set): self.refresh_screen('drawing') # self._sound_kong.play() interrupted = True break except OutOfTilesError as e: self.withdraw() except HaveWinnerError as e: self._winner = player self.mahjong_on_kong = True self.firer = player self._state_machine.mahjong() have_winner = True self.refresh_screen(state='mahjong') break wind = Suit.get_next_wind(wind) player = self.positions[wind] if self._winner: break # 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): self.refresh_screen() self._sound_pong.play() 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): self.refresh_screen() self._sound_chow.play() interrupted = True if not interrupted: before.put_on_desk(current_discard) before.discarding = None # end if current_discard: if interrupted: current = player else: # test for out of tiles if not self.mj_set.tiles: self.withdraw() break # draw new_tile = current.draw(self._mj_set) if not new_tile: self.withdraw() break self.refresh_screen(state='drawing') # test for hu by self if current.try_mahjong(tile=None): print(f"winner is {current}, by self!") self._winner = current self.firer = current self._state_machine.mahjong() self.refresh_screen(state='mahjong') break # self kong try: if current.try_conceal_kong(self.mj_set): pass except OutOfTilesError as e: self.withdraw() except HaveWinnerError as e: self._winner = current self.mahjong_on_kong = True self.firer = current self._state_machine.mahjong() have_winner = True self.refresh_screen(state='mahjong') break if self.winner: break # test for exposed kong from exposed pong result_of_try = False try: result_of_try = current.try_exposed_kong_from_exposed_pong( mj_set=self.mj_set) except OutOfTilesError as e: self.withdraw() except HaveWinnerError as e: self._winner = current self.mahjong_on_kong = True self.firer = current self._state_machine.mahjong() have_winner = True self.refresh_screen(state='mahjong') break if self._winner: break if result_of_try: # try rob kong by others self.refresh_screen() # others try mahjong player = None wind = current.position for index in range(3): wind = Suit.get_next_wind(wind) player = self.positions[wind] if player.try_mahjong(tile=new_tile): print(f"winner is {player}, by rob {current}!") player.concealed.append(new_tile) self._winner = player self.firer = current self._state_machine.mahjong() self.refresh_screen(state='mahjong') self.robbing_a_kong = True break if self._winner: break 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() current.discard(tile) print(current, 'discard tile:', tile) current_discard = tile current.sort_concealed() # (f'{current} discard {tile}') # (Rule.convert_tiles_to_str(current.concealed)) # next player next = self._get_next_player(current) before = current current = next self.refresh_screen() self.sound_discard() # end while self.refresh_screen(state='scoring') 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()
def sort_concealed(self): arr = Rule.convert_tiles_to_arr(self._concealed) arr.sort() self._concealed = Rule.convert_arr_to_tiles(arr)
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)
def decide_discard(self, players_count: int = 4, wall: list = []) -> Tile: start = time() if len(self._concealed) <= 5: tiles = self.decide_discard_by_value(wall=None, players_count=players_count, deep=2) duration = time() - start self.record_strategies_time('decide_discard_by_value_2_last', duration) self.record_strategies('decide_discard_by_value_2_last') if tiles: return choice(tiles) # discard the orphan tile # check for ready chance # discard the orphan tile start = time() test = self._concealed[:-1] is_ready = Rule.is_ready(test) if is_ready: tile = self.decide_discard_by_loneliest_orphan( keep_one_orphan=True) else: tile = self.decide_discard_by_loneliest_orphan( keep_one_orphan=False) duration = time() - start self.record_strategies('decide_discard_by_loneliest_orphan') self.record_strategies_time('decide_discard_by_loneliest_orphan', duration) if tile: return tile start = time() tiles = self.decide_discard_by_value(wall=wall, players_count=players_count, deep=1) duration = time() - start self.record_strategies_time('decide_discard_by_value_1', duration) self.record_strategies('decide_discard_by_value_1') if tiles: return choice(tiles) start = time() tiles = self.decide_discard_by_left_meld_and_eye() duration = time() - start self.record_strategies_time('decide_discard_by_left_meld_and_eye', duration) self.record_strategies('decide_discard_by_left_meld_and_eye') if tiles: return choice(tiles) start = time() arr = Rule.convert_tiles_to_arr(self.concealed) combins = MjMath.get_best_meld_combins_from_arr(arr) if combins and len(combins) > 0 and len(combins[0]) > 3: tiles = self.decide_discard_by_value(wall=None, players_count=players_count, deep=2) duration = time() - start self.record_strategies_time('decide_discard_by_value_2', duration) self.record_strategies('decide_discard_by_value_2') if tiles: return choice(tiles) # finally, decide by random start = time() tile = self.decide_discard_random() duration = time() - start self.record_strategies_time('decide_discard_random', duration) self.record_strategies('decide_discard_random') return tile