def __init__(self, player): super(MainAI, self).__init__(player) self.agari = Agari() self.shanten = Shanten() self.defence = DefenceHandler(player) self.hand_divider = HandDivider() self.finished_hand = FinishedHand() self.previous_shanten = 7 self.current_strategy = None self.waiting = [] self.in_defence = False
class GreedyAII(BaseAI): version = '0.0.1' def __init__(self, table, player): super(GreedyAII, self).__init__(table, player) self.shanten = Shanten() def discard_tile(self): gd_player = GreedyPlayer("Me") h = Hand(TilesConverter.to_one_line_string(self.player.tiles)) t = gd_player.select_best_tile(h) tiles = TilesConverter.to_34_array(self.player.tiles) shanten = self.shanten.calculate_shanten(tiles) if shanten == 0: self.player.in_tempai = True types = ['m', 'p', 's', 'z'] if h.test_win(): return Shanten.AGARI_STATE else: tile_in_hand = TilesConverter.find_34_tile_in_136_array( t.get_number() + (t.get_type() >> 4) * 9 - 1, self.player.tiles) return tile_in_hand
def test_shanten_number_and_open_sets(self): shanten = Shanten() tiles = self._string_to_34_array(sou='44467778', pin='222567') open_sets = [] self.assertEqual( shanten.calculate_shanten(tiles, open_sets_34=open_sets), Shanten.AGARI_STATE) open_sets = [self._string_to_open_34_set(sou='777')] self.assertEqual( shanten.calculate_shanten(tiles, open_sets_34=open_sets), 0) tiles = self._string_to_34_array(sou='23455567', pin='222', man='345') open_sets = [ self._string_to_open_34_set(man='345'), self._string_to_open_34_set(sou='555') ] self.assertEqual( shanten.calculate_shanten(tiles, open_sets_34=open_sets), 0)
def test_shanten_number_and_kokushi_musou(self): shanten = Shanten() tiles = self._string_to_136_array(sou='19', pin='19', man='19', honors='12345677') self.assertEqual(shanten.calculate_shanten(self._to_34_array(tiles)), Shanten.AGARI_STATE) tiles = self._string_to_136_array(sou='129', pin='19', man='19', honors='1234567') self.assertEqual(shanten.calculate_shanten(self._to_34_array(tiles)), 0) tiles = self._string_to_136_array(sou='129', pin='129', man='19', honors='123456') self.assertEqual(shanten.calculate_shanten(self._to_34_array(tiles)), 1) tiles = self._string_to_136_array(sou='129', pin='129', man='129', honors='12345') self.assertEqual(shanten.calculate_shanten(self._to_34_array(tiles)), 2)
def __init__(self, player): super(MainAI, self).__init__(player) # we don't `defense` since our algorithm will defense by evaluating "trade-off" value. self.defence = DefenceHandler(player) # strategy_type is set to None. We use this BaseStrategy to call meld. self.current_strategy = BaseStrategy(None, player) # shantan self.shanten = Shanten() # We load the classifiers and regressors here self.clf_is_waiting = pickle.load( open(abs_data_path + "/train_model/trained_models/is_waiting.sav", "rb")) self.clf_waiting_tile = [] for n in range(34): clf = pickle.load( open( abs_data_path + "/train_model/trained_models/waiting_tile_{}.sav".format( n), "rb")) self.clf_waiting_tile.append(clf) self.rgrs_scores = pickle.load( open(abs_data_path + "/train_model/trained_models/scores.sav", "rb")) self.rgrs_wfw_scores = pickle.load( open(abs_data_path + "/train_model/trained_models/wfw_scores.sav", "rb")) # We also load the scalers for regressors self.scaler_scores = pickle.load( open( abs_data_path + "/train_model/trained_models/scaler_scores.sav", "rb")) self.scaler_wfw_scores = pickle.load( open( abs_data_path + "/train_model/trained_models/scaler_wfw_scores.sav", "rb"))
def test_shanten_number_and_chitoitsu(self): shanten = Shanten() tiles = self._string_to_34_array(sou='114477', pin='114477', man='77') self.assertEqual(shanten.calculate_shanten(tiles), Shanten.AGARI_STATE) tiles = self._string_to_34_array(sou='114477', pin='114477', man='76') self.assertEqual(shanten.calculate_shanten(tiles), 0) tiles = self._string_to_34_array(sou='114477', pin='114479', man='76') self.assertEqual(shanten.calculate_shanten(tiles), 1) tiles = self._string_to_34_array(sou='114477', pin='14479', man='76', honors='1') self.assertEqual(shanten.calculate_shanten(tiles), 2)
class MainAI(BaseAI): version = '0.0.6' agari = None shanten = None defence = None def __init__(self, table, player): super(MainAI, self).__init__(table, player) self.agari = Agari() self.shanten = Shanten() self.defence = Defence(table) def discard_tile(self): results, shanten = self.calculate_outs() if shanten == 0: self.player.in_tempai = True # we are win! if shanten == Shanten.AGARI_STATE: return Shanten.AGARI_STATE # Disable defence for now # if self.defence.go_to_defence_mode(): # self.player.in_tempai = False # tile_in_hand = self.defence.calculate_safe_tile_against_riichi() # if we wasn't able to find a safe tile, let's discard a random one # if not tile_in_hand: # tile_in_hand = self.player.tiles[random.randrange(len(self.player.tiles) - 1)] # else: # tile34 = results[0]['discard'] # tile_in_hand = TilesConverter.find_34_tile_in_136_array(tile34, self.player.tiles) tile34 = results[0]['discard'] tile_in_hand = TilesConverter.find_34_tile_in_136_array( tile34, self.player.tiles) return tile_in_hand def calculate_outs(self): tiles = TilesConverter.to_34_array(self.player.tiles) shanten = self.shanten.calculate_shanten(tiles) # win if shanten == Shanten.AGARI_STATE: return [], shanten raw_data = {} for i in range(0, 34): if not tiles[i]: continue tiles[i] -= 1 raw_data[i] = [] for j in range(0, 34): if i == j or tiles[j] >= 4: continue tiles[j] += 1 if self.shanten.calculate_shanten(tiles) == shanten - 1: raw_data[i].append(j) tiles[j] -= 1 tiles[i] += 1 if raw_data[i]: raw_data[i] = { 'tile': i, 'tiles_count': self.count_tiles(raw_data[i], tiles), 'waiting': raw_data[i] } results = [] tiles = TilesConverter.to_34_array(self.player.tiles) for tile in range(0, len(tiles)): if tile in raw_data and raw_data[tile] and raw_data[tile][ 'tiles_count']: item = raw_data[tile] waiting = [] for item2 in item['waiting']: waiting.append(item2) results.append({ 'discard': item['tile'], 'waiting': waiting, 'tiles_count': item['tiles_count'] }) # if we have character and honor candidates to discard with same tiles count, # we need to discard honor tile first results = sorted(results, key=lambda x: (x['tiles_count'], x['discard']), reverse=True) return results, shanten def count_tiles(self, raw_data, tiles): n = 0 for i in range(0, len(raw_data)): n += 4 - tiles[raw_data[i]] return n
def __init__(self, table, player): super(MainAI, self).__init__(table, player) self.agari = Agari() self.shanten = Shanten() self.defence = Defence(table)
def __init__(self, table, player): super(GreedyAII, self).__init__(table, player) self.shanten = Shanten()
class MainAI(BaseAI): """ AI that is based on Monte Carlo simulation and opponent model. """ version = 'random' def __init__(self, player): super(MainAI, self).__init__(player) # we don't `defense` since our algorithm will defense by evaluating "trade-off" value. self.defence = DefenceHandler(player) # strategy_type is set to None. We use this BaseStrategy to call meld. self.current_strategy = BaseStrategy(None, player) # shantan self.shanten = Shanten() # We load the classifiers and regressors here self.clf_is_waiting = pickle.load( open(abs_data_path + "/train_model/trained_models/is_waiting.sav", "rb")) self.clf_waiting_tile = [] for n in range(34): clf = pickle.load( open( abs_data_path + "/train_model/trained_models/waiting_tile_{}.sav".format( n), "rb")) self.clf_waiting_tile.append(clf) self.rgrs_scores = pickle.load( open(abs_data_path + "/train_model/trained_models/scores.sav", "rb")) self.rgrs_wfw_scores = pickle.load( open(abs_data_path + "/train_model/trained_models/wfw_scores.sav", "rb")) # We also load the scalers for regressors self.scaler_scores = pickle.load( open( abs_data_path + "/train_model/trained_models/scaler_scores.sav", "rb")) self.scaler_wfw_scores = pickle.load( open( abs_data_path + "/train_model/trained_models/scaler_wfw_scores.sav", "rb")) def erase_state(self): self.current_strategy = None self.in_defence = False def reset_melds(self): """This setting is used to record the discarded tiles immediately after calling melds. Whenever a `NEXTREADY` command is sent, this function should be called. """ self.meld_sets = [[], [], [], []] # four players self.meld_discarded_tiles = [[], [], [], []] # four players def determine_strategy(self): return False def can_call_kan(self, tile, open_kan): """ Method will decide should we call a kan, or upgrade pon to kan :param tile: 136 tile format :param open_kan: boolean :return: kan type """ # we don't need to add dora for other players if self.player.ai.in_defence: return None if open_kan: # we don't want to start open our hand from called kan if not self.player.is_open_hand: return None # there is no sense to call open kan when we are not in tempai if not self.player.in_tempai: return None # we have a bad wait, rinshan chance is low if len(self.waiting) < 2: return None tile_34 = tile // 4 tiles_34 = TilesConverter.to_34_array(self.player.tiles) closed_hand_34 = TilesConverter.to_34_array(self.player.closed_hand) pon_melds = [x for x in self.player.open_hand_34_tiles if is_pon(x)] # let's check can we upgrade opened pon to the kan if pon_melds: for meld in pon_melds: # tile is equal to our already opened pon, # so let's call chankan! if tile_34 in meld: return Meld.CHANKAN count_of_needed_tiles = 4 # for open kan 3 tiles is enough to call a kan if open_kan: count_of_needed_tiles = 3 # we have 3 tiles in our hand, # so we can try to call closed meld if closed_hand_34[tile_34] == count_of_needed_tiles: if not open_kan: # to correctly count shanten in the hand # we had do subtract drown tile tiles_34[tile_34] -= 1 melds = self.player.open_hand_34_tiles previous_shanten = self.shanten.calculate_shanten(tiles_34, melds) melds += [[tile_34, tile_34, tile_34]] new_shanten = self.shanten.calculate_shanten(tiles_34, melds) # called kan will not ruin our hand if new_shanten <= previous_shanten: return Meld.KAN return None def try_to_call_meld(self, tile, is_kamicha_discard): if not self.current_strategy: return None, None return self.current_strategy.try_to_call_meld(tile, is_kamicha_discard) # @deprecated # def discard_tile_randomly(self): # tile_to_discard = random.randrange(len(self.player.tiles) - 1) # tile_to_discard = self.player.tiles[tile_to_discard] # print("opponnet model discards: {}\n".format(tile_to_discard)) # return tile_to_discard def discard_tile(self): tile_to_discard = random.randrange(len(self.player.tiles) - 1) tile_to_discard = self.player.tiles[tile_to_discard] print("\n") scores = [] for tile in self.player.tiles: score = 0 # TODO: the value of Sim(tile) should be determined by Monte-Carlo # simulation. sim = 0 # Not Losing Probability NLP = 1 for p in range(1, 4): # Losing Probability: LP(p,tile) LP = self.prob_is_waiting(p) * self.prob_winning_tile( p, tile // 4) # tile in 34 format # Accumulated Not Losing Probability NLP *= (1 - LP) # Hand Score (by discarding a winning tile): HS(p,tile) HS = self.hand_score(p, tile // 4) EL = LP * HS score -= EL score += sim * NLP scores.append(score) # Find out the highest score choice n = scores.index(max(scores)) tile_to_discard = self.player.tiles[n] print("hands: {}".format(self.player.tiles)) print("scores: {}".format(scores)) print("opponnet model discards: {}\n".format(tile_to_discard)) return tile_to_discard def get_table_info(self): """ Get table information return: table information """ table = self.player.table dora_tiles = [d for d in range(136) if table.is_dora(d)] table_turns = min( [len(table.players[m].discards) + 1 for m in range(4)]) table_info = "{},{},{},{},{},{},{},{},{},{}".format( table.count_of_honba_sticks, table.count_of_remaining_tiles, table.count_of_riichi_sticks, table.round_number, table.round_wind, table_turns, table.dealer_seat, table.dora_indicators, dora_tiles, table.revealed_tiles) return table_info def parse_table_info(self, table_info): """ Parse the table info param table_info: str, information of table contained in a string return: tuple, numerical data of table information """ (table_count_of_honba_sticks, table_count_of_remaining_tiles, table_count_of_riichi_sticks, table_round_number, table_round_wind, table_turns, table_dealer_seat, table_dora_indicators, table_dora_tiles, table_revealed_tiles) = ast.literal_eval(table_info) return (table_count_of_honba_sticks, table_count_of_remaining_tiles, table_count_of_riichi_sticks, table_round_number, table_round_wind, table_turns, table_dealer_seat, table_dora_indicators, table_dora_tiles, table_revealed_tiles) def get_player_info(self, p): """ Get player information param p: int (1-3), player index of the opponent return: player information """ table = self.player.table player_info = "" player = table.players[p] # Discarded tiles can be seen by everybody discarded_tiles = [(d.value, 1) if d.is_tsumogiri else (d.value, 0) for d in player.discards] # winning tiles of opponents are invisible, we just keep an empty 34 element array here winning_tiles = [0 for _ in range(34)] # Meld sets (both open and concealed) are also visible to everybody meld_sets = [mt.tiles for mt in player.melds] meld_open = [mt.opened for mt in player.melds] if meld_sets != self.meld_sets[p]: # It might happen that one player will discard more than one tile # in a round, simply b/c he pon or kan more than once. n = len(meld_sets) - len(self.meld_sets[p]) self.meld_discarded_tiles[p].extend(discarded_tiles[-n:]) # We need to update the self.meld_sets in order to compare with new # information later. self.meld_sets[p] = meld_sets #print("!{}, {}, {}, {}".format(meld_sets, meld_open, self.meld_discarded_tiles[p], self.meld_sets[p])) melds = [(meld_sets[k], 1 if meld_open[k] else 0, self.meld_discarded_tiles[p][k]) for k in range(len(meld_sets))] string_to_save = "{},{},{},{},{},{},{},{},{},{},{},{},{}".format( winning_tiles, discarded_tiles, player.dealer_seat, 1 if player.in_riichi else 0, 1 if player.is_dealer else 0, 1 if player.is_open_hand else 0, melds, -1, # we don't need player's name; player.name if player.name else -1, player.position if player.position else -1, -1, # we don't need player's rank; player.rank if player.rank else -1, player.scores if player.scores else -1, player.seat if player.seat else -1, player.uma if player.uma else -1 #player.closed_hand, # this info is invisible #player.in_defence_mode, # this info is invisible #player.in_tempai, # this info is invisible #player.last_draw, # this info is invisible #player.tiles, # this info is invisible ) player_info += string_to_save return player_info def parse_player_info(self, p, player_info): """Parse the player info param p: int (1-3), player index of the opponent param player_info: str, information of player contained in a string return: tuple, numerical data of player information """ ( player_winning_tiles, player_discarded_tiles, player_dealer_seat, player_in_riichi, player_is_dealer, player_is_open_hand, player_melds, player_name, # player_name has been replaced with -1 player_position, player_rank, # player_rank has been replaced with -1 player_scores, player_seat, player_uma) = ast.literal_eval(player_info) # We need p (player index) here b/c we don't want to lose any information # of the player. Although for the moment we might not use it, but perhaps # a later model will need information such as player rank et cetera. table = self.player.table player = table.players[p] # replace back the player name, although we may not need it player_name = player.name # replace back the player rank, although we may not need it player_rank = player.rank return ( player_winning_tiles, player_discarded_tiles, player_dealer_seat, player_in_riichi, player_is_dealer, player_is_open_hand, player_melds, player_name, # player_name has been replaced with -1 player_position, player_rank, # player_rank has been replaced with -1 player_scores, player_seat, player_uma) def prob_is_waiting(self, p): """The probability that an opponent p is waiting. param p: int (1-3), index of opponent player return: probability of opponent p being waiting """ # Get table information table_info = self.get_table_info() # Get player information player_info = self.get_player_info(p) # Parse the table info (table_count_of_honba_sticks, table_count_of_remaining_tiles, table_count_of_riichi_sticks, table_round_number, table_round_wind, table_turns, table_dealer_seat, table_dora_indicators, table_dora_tiles, table_revealed_tiles) = self.parse_table_info(table_info) # Parse the player info ( player_winning_tiles, player_discarded_tiles, player_dealer_seat, player_in_riichi, player_is_dealer, player_is_open_hand, player_melds, player_name, # player_name has been replaced with -1 player_position, player_rank, # player_rank has been replaced with -1 player_scores, player_seat, player_uma) = self.parse_player_info(p, player_info) features = gen_is_waiting_features( table_count_of_honba_sticks, table_count_of_remaining_tiles, table_count_of_riichi_sticks, table_round_number, table_round_wind, table_turns, table_dealer_seat, table_dora_indicators, table_dora_tiles, table_revealed_tiles, player_winning_tiles, player_discarded_tiles, player_dealer_seat, player_in_riichi, player_is_dealer, player_is_open_hand, player_melds, player_name, player_position, player_rank, player_scores, player_seat, player_uma) f1, f2, f3, f4, f5, f6, f7, f8, f9 = features opponent_info = [f1] + [f2] + [f3] + [f4] + [f5] + f6 + f7 + f8 + f9 opponent_info = np.array([opponent_info]) # Probability of `p` is waiting clf = self.clf_is_waiting prob_of_is_waiting = clf.predict_proba(opponent_info)[0][1] return prob_of_is_waiting # def prob_is_waiting(self, p): # """The probability that an opponent p is waiting. # param p: int (1-3), index of opponent player # return: probability of opponent p being waiting # """ # # Get table information # table = self.player.table # dora_tiles = [d for d in range(136) if table.is_dora(d) ] # table_turns = min([len(table.players[m].discards)+1 for m in range(4)]) # table_info = "{},{},{},{},{},{},{},{},{},{}".format( # table.count_of_honba_sticks, # table.count_of_remaining_tiles, # table.count_of_riichi_sticks, # table.round_number, # table.round_wind, # table_turns, # table.dealer_seat, # table.dora_indicators, # dora_tiles, # table.revealed_tiles # ) # # # Get player information # player_info = "" # player = table.players[p] # # Discarded tiles can be seen by everybody # discarded_tiles = [(d.value,1) if d.is_tsumogiri else (d.value,0) for d in player.discards] # # winning tiles of opponents are invisible, we just keep an empty 34 element array here # winning_tiles = [0 for _ in range(34)] # # Meld sets (both open and concealed) are also visible to everybody # meld_sets = [mt.tiles for mt in player.melds] # meld_open = [mt.opened for mt in player.melds] # if meld_sets != self.meld_sets[p]: # # It might happen that one player will discard more than one tile # # in a round, simply b/c he pon or kan more than once. # n = len(meld_sets) - len(self.meld_sets[p]) # self.meld_discarded_tiles[p].extend(discarded_tiles[-n:]) # # We need to update the self.meld_sets in order to compare with new # # information later. # self.meld_sets[p] = meld_sets # #print("!{}, {}, {}, {}".format(meld_sets, meld_open, self.meld_discarded_tiles[p], self.meld_sets[p])) # melds = [(meld_sets[k], 1 if meld_open[k] else 0, self.meld_discarded_tiles[p][k]) for k in range(len(meld_sets))] # string_to_save = "{},{},{},{},{},{},{},{},{},{},{},{},{}".format( # winning_tiles, # discarded_tiles, # player.dealer_seat, # 1 if player.in_riichi else 0, # 1 if player.is_dealer else 0, # 1 if player.is_open_hand else 0, # melds, # -1, # we don't need player's name; player.name if player.name else -1, # player.position if player.position else -1, # -1, # we don't need player's rank; player.rank if player.rank else -1, # player.scores if player.scores else -1, # player.seat if player.seat else -1, # player.uma if player.uma else -1 # #player.closed_hand, # this info is invisible # #player.in_defence_mode, # this info is invisible # #player.in_tempai, # this info is invisible # #player.last_draw, # this info is invisible # #player.tiles, # this info is invisible # ) # player_info += string_to_save # # # Parse the table info # (table_count_of_honba_sticks, # table_count_of_remaining_tiles, # table_count_of_riichi_sticks, # table_round_number, # table_round_wind, # table_turns, # table_dealer_seat, # table_dora_indicators, # table_dora_tiles, # table_revealed_tiles) = ast.literal_eval(table_info) # # # Parse the player info # (player_winning_tiles, # player_discarded_tiles, # player_dealer_seat, # player_in_riichi, # player_is_dealer, # player_is_open_hand, # player_melds, # player_name, # player_name has been replaced with -1 # player_position, # player_rank, # player_rank has been replaced with -1 # player_scores, # player_seat, # player_uma) = ast.literal_eval(player_info) # # # replace back the player name, although we may not need it # player_name = player.name # # replace back the player rank, although we may not need it # player_rank = player.rank # # features = gen_is_waiting_features(table_count_of_honba_sticks, # table_count_of_remaining_tiles, # table_count_of_riichi_sticks, # table_round_number, # table_round_wind, # table_turns, # table_dealer_seat, # table_dora_indicators, # table_dora_tiles, # table_revealed_tiles, # player_winning_tiles, # player_discarded_tiles, # player_dealer_seat, # player_in_riichi, # player_is_dealer, # player_is_open_hand, # player_melds, # player_name, # player_position, # player_rank, # player_scores, # player_seat, # player_uma) # f1, f2, f3, f4, f5, f6, f7, f8, f9 = features # opponent_info = [f1]+[f2]+[f3]+[f4]+[f5]+f6+f7+f8+f9 # opponent_info = np.array([opponent_info]) # # # Probability of p is waiting # clf = self.clf_is_waiting # prob_of_is_waiting = clf.predict_proba(opponent_info)[0][1] # return prob_of_is_waiting def prob_winning_tile(self, p, tile): """The probability that an opponent `p` is waiting for `tile`. param p: int (1-3), index of opponent player param tile: int (0-33), index of the tile kind return: probability of tile being waiting tile for opponent p """ # Get table information table_info = self.get_table_info() # Get player information player_info = self.get_player_info(p) # Parse the table info (table_count_of_honba_sticks, table_count_of_remaining_tiles, table_count_of_riichi_sticks, table_round_number, table_round_wind, table_turns, table_dealer_seat, table_dora_indicators, table_dora_tiles, table_revealed_tiles) = self.parse_table_info(table_info) # Parse the player info ( player_winning_tiles, player_discarded_tiles, player_dealer_seat, player_in_riichi, player_is_dealer, player_is_open_hand, player_melds, player_name, # player_name has been replaced with -1 player_position, player_rank, # player_rank has been replaced with -1 player_scores, player_seat, player_uma) = self.parse_player_info(p, player_info) features = gen_waiting_tiles_features( table_count_of_honba_sticks, table_count_of_remaining_tiles, table_count_of_riichi_sticks, table_round_number, table_round_wind, table_turns, table_dealer_seat, table_dora_indicators, table_dora_tiles, table_revealed_tiles, player_winning_tiles, player_discarded_tiles, player_dealer_seat, player_in_riichi, player_is_dealer, player_is_open_hand, player_melds, player_name, player_position, player_rank, player_scores, player_seat, player_uma) f1, f2, f3, f4, f5, f6, f7, f8, f9, f10, f11 = features opponent_info = [f1] + [f2] + [f3] + [f4] + [ f5 ] + f6 + f7 + f8 + f9 + f10 + f11 opponent_info = np.array([opponent_info]) # Probability of `tile` is waiting tile for `p` clf = self.clf_waiting_tile[tile] # load (tile-th) classifier prob_of_winning_tile = clf.predict_proba(opponent_info)[0][1] return prob_of_winning_tile # TODO: this function has not been finished, as I need to go back to modify # the HS (for HS_WFW there is no need) training model to add back # `discarded tile` as one of the features. def hand_score(self, p, tile): """Use our trained model to predict loss if discarding a winning tile `tile` to the opponent `p`. param p: int (1-3), player index of the opponent param tile: int (0-33), tile index in 34 format return: hand score lost to the opponent """ # Get table information table_info = self.get_table_info() # Get player information player_info = self.get_player_info(p) # Parse the table info (table_count_of_honba_sticks, table_count_of_remaining_tiles, table_count_of_riichi_sticks, table_round_number, table_round_wind, table_turns, table_dealer_seat, table_dora_indicators, table_dora_tiles, table_revealed_tiles) = self.parse_table_info(table_info) # Parse the player info ( player_winning_tiles, player_discarded_tiles, player_dealer_seat, player_in_riichi, player_is_dealer, player_is_open_hand, player_melds, player_name, # player_name has been replaced with -1 player_position, player_rank, # player_rank has been replaced with -1 player_scores, player_seat, player_uma) = self.parse_player_info(p, player_info) features = gen_scores_features( table_count_of_honba_sticks, table_count_of_remaining_tiles, table_count_of_riichi_sticks, table_round_number, table_round_wind, table_turns, table_dealer_seat, table_dora_indicators, table_dora_tiles, table_revealed_tiles, player_winning_tiles, player_discarded_tiles, player_dealer_seat, player_in_riichi, player_is_dealer, player_is_open_hand, player_melds, player_name, player_position, player_rank, player_scores, player_seat, player_uma) f1, f2, f3, f4, f5, f6, f7, f8, f9, f10, f11, f12, f13 = features f14 = tile opponent_info = [f1] + [f2] + [f3] + [f4] + [f5] + f6 + f7 + [f8] + [ f9 ] + [f10] + [f11] + [f12] + [f13] + [f14] opponent_info = np.array([opponent_info]) # Predicted hand score rgrs = self.rgrs_scores scaler = self.scaler_scores scaled_opponent_info = scaler.transform(opponent_info) log_HS = rgrs.predict(scaled_opponent_info)[0] # HS = np.exp(log_HS) return log_HS
def test_shanten_number(self): shanten = Shanten() tiles = self._string_to_136_array(sou='111234567', pin='11', man='567') self.assertEqual(shanten.calculate_shanten(self._to_34_array(tiles)), Shanten.AGARI_STATE) tiles = self._string_to_136_array(sou='111345677', pin='11', man='567') self.assertEqual(shanten.calculate_shanten(self._to_34_array(tiles)), 0) tiles = self._string_to_136_array(sou='111345677', pin='15', man='567') self.assertEqual(shanten.calculate_shanten(self._to_34_array(tiles)), 1) tiles = self._string_to_136_array(sou='11134567', pin='15', man='1578') self.assertEqual(shanten.calculate_shanten(self._to_34_array(tiles)), 2) tiles = self._string_to_136_array(sou='113456', pin='1358', man='1358') self.assertEqual(shanten.calculate_shanten(self._to_34_array(tiles)), 3) tiles = self._string_to_136_array(sou='1589', pin='13588', man='1358', honors='1') self.assertEqual(shanten.calculate_shanten(self._to_34_array(tiles)), 4) tiles = self._string_to_136_array(sou='159', pin='13588', man='1358', honors='12') self.assertEqual(shanten.calculate_shanten(self._to_34_array(tiles)), 5) tiles = self._string_to_136_array(sou='1589', pin='258', man='1358', honors='123') self.assertEqual(shanten.calculate_shanten(self._to_34_array(tiles)), 6) tiles = self._string_to_136_array(sou='11123456788999') self.assertEqual(shanten.calculate_shanten(self._to_34_array(tiles)), Shanten.AGARI_STATE) tiles = self._string_to_136_array(sou='11122245679999') self.assertEqual(shanten.calculate_shanten(self._to_34_array(tiles)), 0)
class MainAI(BaseAI): version = '0.2.7' agari = None shanten = None defence = None hand_divider = None finished_hand = None previous_shanten = 7 in_defence = False waiting = None current_strategy = None def __init__(self, player): super(MainAI, self).__init__(player) self.agari = Agari() self.shanten = Shanten() self.defence = DefenceHandler(player) self.hand_divider = HandDivider() self.finished_hand = FinishedHand() self.previous_shanten = 7 self.current_strategy = None self.waiting = [] self.in_defence = False def erase_state(self): self.current_strategy = None self.in_defence = False def discard_tile(self): results, shanten = self.calculate_outs(self.player.tiles, self.player.closed_hand, self.player.open_hand_34_tiles) selected_tile = self.process_discard_options_and_select_tile_to_discard( results, shanten) # bot think that there is a threat on the table # and better to fold # if we can't find safe tiles, let's continue to build our hand if self.defence.should_go_to_defence_mode(selected_tile): if not self.in_defence: logger.info('We decided to fold against other players') self.in_defence = True defence_tile = self.defence.try_to_find_safe_tile_to_discard( results) if defence_tile: return self.process_discard_option(defence_tile, self.player.closed_hand) else: self.in_defence = False return self.process_discard_option(selected_tile, self.player.closed_hand) def process_discard_options_and_select_tile_to_discard( self, results, shanten): tiles_34 = TilesConverter.to_34_array(self.player.tiles) # we had to update tiles value there # because it is related with shanten number for result in results: result.tiles_count = self.count_tiles(result.waiting, tiles_34) result.calculate_value(shanten) # current strategy can affect on our discard options # so, don't use strategy specific choices for calling riichi if self.current_strategy: results = self.current_strategy.determine_what_to_discard( self.player.closed_hand, results, shanten, False, None) return self.chose_tile_to_discard(results) def calculate_outs(self, tiles, closed_hand, open_sets_34=None): """ :param tiles: array of tiles in 136 format :param closed_hand: array of tiles in 136 format :param open_sets_34: array of array with tiles in 34 format :return: """ tiles_34 = TilesConverter.to_34_array(tiles) closed_tiles_34 = TilesConverter.to_34_array(closed_hand) is_agari = self.agari.is_agari(tiles_34, self.player.open_hand_34_tiles) results = [] for hand_tile in range(0, 34): if not closed_tiles_34[hand_tile]: continue tiles_34[hand_tile] -= 1 shanten = self.shanten.calculate_shanten(tiles_34, open_sets_34) waiting = [] for j in range(0, 34): if hand_tile == j or tiles_34[j] == 4: continue tiles_34[j] += 1 if self.shanten.calculate_shanten(tiles_34, open_sets_34) == shanten - 1: waiting.append(j) tiles_34[j] -= 1 tiles_34[hand_tile] += 1 if waiting: results.append( DiscardOption(player=self.player, shanten=shanten, tile_to_discard=hand_tile, waiting=waiting, tiles_count=self.count_tiles( waiting, tiles_34))) if is_agari: shanten = Shanten.AGARI_STATE else: shanten = self.shanten.calculate_shanten(tiles_34, open_sets_34) return results, shanten def count_tiles(self, waiting, tiles_34): n = 0 for item in waiting: n += 4 - self.player.total_tiles(item, tiles_34) return n def try_to_call_meld(self, tile, is_kamicha_discard): if not self.current_strategy: return None, None return self.current_strategy.try_to_call_meld(tile, is_kamicha_discard) def determine_strategy(self): # for already opened hand we don't need to give up on selected strategy if self.player.is_open_hand and self.current_strategy: return False old_strategy = self.current_strategy self.current_strategy = None # order is important strategies = [ YakuhaiStrategy(BaseStrategy.YAKUHAI, self.player), HonitsuStrategy(BaseStrategy.HONITSU, self.player), ] if settings.OPEN_TANYAO: strategies.append(TanyaoStrategy(BaseStrategy.TANYAO, self.player)) for strategy in strategies: if strategy.should_activate_strategy(): self.current_strategy = strategy if self.current_strategy: if not old_strategy or self.current_strategy.type != old_strategy.type: message = '{} switched to {} strategy'.format( self.player.name, self.current_strategy) if old_strategy: message += ' from {}'.format(old_strategy) logger.debug(message) logger.debug('With hand: {}'.format( TilesConverter.to_one_line_string(self.player.tiles))) if not self.current_strategy and old_strategy: logger.debug('{} gave up on {}'.format(self.player.name, old_strategy)) return self.current_strategy and True or False def chose_tile_to_discard(self, results: [DiscardOption]) -> DiscardOption: """ Try to find best tile to discard, based on different valuations """ def sorting(x): # - is important for x.tiles_count # in that case we will discard tile that will give for us more tiles # to complete a hand return x.shanten, -x.tiles_count, x.valuation had_to_be_discarded_tiles = [ x for x in results if x.had_to_be_discarded ] if had_to_be_discarded_tiles: had_to_be_discarded_tiles = sorted(had_to_be_discarded_tiles, key=sorting) selected_tile = had_to_be_discarded_tiles[0] else: results = sorted(results, key=sorting) # remove needed tiles from discard options results = [x for x in results if not x.had_to_be_saved] # let's chose most valuable tile first temp_tile = results[0] # and let's find all tiles with same shanten results_with_same_shanten = [ x for x in results if x.shanten == temp_tile.shanten ] possible_options = [temp_tile] for discard_option in results_with_same_shanten: # there is no sense to check already chosen tile if discard_option.tile_to_discard == temp_tile.tile_to_discard: continue # we don't need to select tiles almost dead waits if discard_option.tiles_count <= 2: continue # let's check all other tiles with same shanten # maybe we can find tiles that have almost same tiles count number if temp_tile.tiles_count - 2 < discard_option.tiles_count < temp_tile.tiles_count + 2: possible_options.append(discard_option) # let's sort got tiles by value and let's chose less valuable tile to discard possible_options = sorted(possible_options, key=lambda x: x.valuation) selected_tile = possible_options[0] return selected_tile def process_discard_option(self, selected_tile, closed_hand): self.waiting = selected_tile.waiting self.player.ai.previous_shanten = selected_tile.shanten self.player.in_tempai = self.player.ai.previous_shanten == 0 return selected_tile.find_tile_in_hand(closed_hand) def estimate_hand_value(self, win_tile, tiles=None, call_riichi=False): """ :param win_tile: 34 tile format :param tiles: :param call_riichi: :return: """ win_tile *= 4 # we don't need to think, that our waiting is aka dora if win_tile in AKA_DORA_LIST: win_tile += 1 if not tiles: tiles = self.player.tiles tiles += [win_tile] result = self.finished_hand.estimate_hand_value( tiles=tiles, win_tile=win_tile, is_tsumo=False, is_riichi=call_riichi, is_dealer=self.player.is_dealer, open_sets=self.player.open_hand_34_tiles, player_wind=self.player.player_wind, round_wind=self.player.table.round_wind, dora_indicators=self.player.table.dora_indicators) return result def should_call_riichi(self): # empty waiting can be found in some cases if not self.waiting: return False # we have a good wait, let's riichi if len(self.waiting) > 1: return True waiting = self.waiting[0] tiles = self.player.closed_hand + [waiting * 4] closed_melds = [x for x in self.player.melds if not x.opened] for meld in closed_melds: tiles.extend(meld.tiles[:3]) tiles_34 = TilesConverter.to_34_array(tiles) results = self.hand_divider.divide_hand(tiles_34, [], []) result = results[0] count_of_pairs = len([x for x in result if is_pair(x)]) # with chitoitsu we can call a riichi with pair wait if count_of_pairs == 7: return True for hand_set in result: # better to not call a riichi for a pair wait # it can be easily improved if is_pair(hand_set) and waiting in hand_set: return False return True def can_call_kan(self, tile, open_kan): """ Method will decide should we call a kan, or upgrade pon to kan :param tile: 136 tile format :param open_kan: boolean :return: kan type """ # we don't need to add dora for other players if self.player.ai.in_defence: return None if open_kan: # we don't want to start open our hand from called kan if not self.player.is_open_hand: return None # there is no sense to call open kan when we are not in tempai if not self.player.in_tempai: return None # we have a bad wait, rinshan chance is low if len(self.waiting) < 2: return None tile_34 = tile // 4 tiles_34 = TilesConverter.to_34_array(self.player.tiles) closed_hand_34 = TilesConverter.to_34_array(self.player.closed_hand) pon_melds = [x for x in self.player.open_hand_34_tiles if is_pon(x)] # let's check can we upgrade opened pon to the kan if pon_melds: for meld in pon_melds: # tile is equal to our already opened pon, # so let's call chankan! if tile_34 in meld: return Meld.CHANKAN count_of_needed_tiles = 4 # for open kan 3 tiles is enough to call a kan if open_kan: count_of_needed_tiles = 3 # we have 3 tiles in our hand, # so we can try to call closed meld if closed_hand_34[tile_34] == count_of_needed_tiles: if not open_kan: # to correctly count shanten in the hand # we had do subtract drown tile tiles_34[tile_34] -= 1 melds = self.player.open_hand_34_tiles previous_shanten = self.shanten.calculate_shanten(tiles_34, melds) melds += [[tile_34, tile_34, tile_34]] new_shanten = self.shanten.calculate_shanten(tiles_34, melds) # called kan will not ruin our hand if new_shanten <= previous_shanten: return Meld.KAN return None @property def valued_honors(self): return [ CHUN, HAKU, HATSU, self.player.table.round_wind, self.player.player_wind ]
def __init__(self, table, player): super(SLCNNPlayer, self).__init__(table, player) self.model = load_model("../supervised_learning/cnn_model.h5") self.model.load_weights("../supervised_learning/cnn_weights.h5") self.shanten = Shanten()
class SLCNNPlayer(BaseAI): version = '0.0.2' def __init__(self, table, player): super(SLCNNPlayer, self).__init__(table, player) self.model = load_model("../supervised_learning/cnn_model.h5") self.model.load_weights("../supervised_learning/cnn_weights.h5") self.shanten = Shanten() def mahjong_tile_to_discard_tile(self, t): return TilesConverter.find_34_tile_in_136_array( t.get_number() + (t.get_type() >> 4) * 9 - 1, self.player.tiles) def discard_tile(self): h = Hand(TilesConverter.to_one_line_string(self.player.tiles)) tiles = TilesConverter.to_34_array(self.player.tiles) shanten = self.shanten.calculate_shanten(tiles) if shanten == 0: self.player.in_tempai = True if h.test_win(): return Shanten.AGARI_STATE elif self.player.in_tempai: results, st = self.calculate_outs() tile34 = results[0]['discard'] tile_in_hand = TilesConverter.find_34_tile_in_136_array( tile34, self.player.tiles) return tile_in_hand else: hand_data = h.get_data() it = int( self.model.predict_classes(transformCSVHandToCNNMatrix( expandHandToCSV(hand_data)), verbose=0)[0]) t = hand_data[it] tile_in_hand = self.mahjong_tile_to_discard_tile(t) return tile_in_hand ''' Adding this bit for calculating which tile to discard when calling Richii. ''' def calculate_outs(self): tiles = TilesConverter.to_34_array(self.player.tiles) shanten = self.shanten.calculate_shanten(tiles) # win if shanten == Shanten.AGARI_STATE: return [], shanten raw_data = {} for i in range(0, 34): if not tiles[i]: continue tiles[i] -= 1 raw_data[i] = [] for j in range(0, 34): if i == j or tiles[j] >= 4: continue tiles[j] += 1 if self.shanten.calculate_shanten(tiles) == shanten - 1: raw_data[i].append(j) tiles[j] -= 1 tiles[i] += 1 if raw_data[i]: raw_data[i] = { 'tile': i, 'tiles_count': self.count_tiles(raw_data[i], tiles), 'waiting': raw_data[i] } results = [] tiles = TilesConverter.to_34_array(self.player.tiles) for tile in range(0, len(tiles)): if tile in raw_data and raw_data[tile] and raw_data[tile][ 'tiles_count']: item = raw_data[tile] waiting = [] for item2 in item['waiting']: waiting.append(item2) results.append({ 'discard': item['tile'], 'waiting': waiting, 'tiles_count': item['tiles_count'] }) # if we have character and honor candidates to discard with same tiles count, # we need to discard honor tile first results = sorted(results, key=lambda x: (x['tiles_count'], x['discard']), reverse=True) return results, shanten def count_tiles(self, raw_data, tiles): n = 0 for i in range(0, len(raw_data)): n += 4 - tiles[raw_data[i]] return n