class ShantenFuroAppendFeature(FuroAppendFeature): shanten_analysis = RsShantenAnalysis() @classmethod def get_length(cls)->int: return 8 # 1:furo can decrease shanten, 0~5: current_shanten @classmethod def calc(cls, result:np.array, board_state:BoardState, player_id:int, candidate_furo:Dict, oracle_enable_flag:bool=False): player_tehai = board_state.tehais[player_id] tehais = [0] * 34 for pai in player_tehai: tehais[pai.id] += 1 furo_num = len(board_state.furos[player_id]) current_shanten = cls.shanten_analysis.calc_shanten(tehais, furo_num) # ignore kan if candidate_furo["type"] not in [MjMove.ankan.value, MjMove.daiminkan.value, MjMove.kakan.value]: add_pai = Pai.from_str(candidate_furo["pai"]) tehais[add_pai.id] += 1 added_shanten = cls.shanten_analysis.calc_shanten(tehais, furo_num) if current_shanten > added_shanten: result[0,:,0] = 1 offset = 1 target_channel = max(0,min(6,current_shanten)) + offset result[target_channel, :, 0] = 1
def main(): dfs = Dfs() shanten_analysis = RsShantenAnalysis() tehai = [ 0, 2, 2, 2, 2, 0, 0, 0, 0, 0, 0, 2, 0, 0, 2, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ] tehai = [ 0, 2, 0, 1, 1, 1, 2, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0 ] shanten_noraml, _, shanten_chitoitsu = shanten_analysis.calc_all_shanten( tehai, 0) # result = dfs.dfs_with_score_chitoitsu(tehai, [], 3, shanten_chitoitsu) # print(len(result)) # print(result) for i in range(1): result = dfs.dfs_with_score_normal(tehai, [], 2, shanten_noraml) print(len(result)) for r in result: print(r)
def __init__(self, pais): self.pais = pais self.shanten_analysis = RsShantenAnalysis() self.tehai = [0]*34 for p in pais: self.tehai[p.id] += 1 self.furo_num = (14 - len(pais)) // 3 self.shanten = self.shanten_analysis.calc_shanten(self.tehai, self.furo_num) self.waiting = [] if self.shanten != 0: return for waiting_id in range(34): if self.tehai[waiting_id] == 4: # already use all pais continue self.tehai[waiting_id] += 1 if self.shanten_analysis.calc_shanten(self.tehai, self.furo_num) == -1: self.waiting.append(Pai.from_id(waiting_id)) self.tehai[waiting_id] -= 1
def __init__(self): self.score_cache = {} self.shanten_analysis = RsShantenAnalysis()
def __init__(self, board, seat=None): self.id = seat self.board = board self.reset() self.shanten_analysis = RsShantenAnalysis()
class HorapointDfsFeature(Feature): """this class has unusual interface, because handling dfs object cache. """ shanten_analysis = RsShantenAnalysis() target_points = [1000, 2000, 3900, 7700, 12000, 16000, 18000, 24000, 32000] DEPTH = 2 YAKU_CH = len(YAKU_CHANNEL_MAP) * DEPTH # depth 1, depth 2, depth 3. POINT_CH = len(target_points) * DEPTH ONE_PLAYER_LENGTH = YAKU_CH + POINT_CH @classmethod def get_length(cls)->int: return cls.ONE_PLAYER_LENGTH * 4 @classmethod def calc(cls, result:np.array, board_state:BoardState, player_id:int, oracle_feature_flag:bool, dfs=Dfs()): for i_from_player, seat_alined_player_id in cls.get_seat_order_ids(player_id): if not oracle_feature_flag: if i_from_player != 0: # print("skip, oracle feature disabled.") continue player_tehai = board_state.tehais[seat_alined_player_id] # # 比較のためターチャの特徴量は無視 # if i_from_player != 0: # continue nums = [0] * 34 for t in player_tehai: nums[t.id] += 1 tehai_akadora_num = len([p for p in player_tehai if p.is_red]) player_furos = board_state.furos[seat_alined_player_id] # num 14 check if len(player_tehai) + len(player_furos) * 3 != 14: # return pass furo_akadora_num = 0 for furo in player_furos: furo_akadora_num += len([p for p in furo.pais if p.is_red]) # # ignore -1, more than 2 dfs_result = None oya = board_state.oya == seat_alined_player_id bakaze = board_state.bakaze jikaze = board_state.jikaze[seat_alined_player_id] doras = [p.succ for p in Pai.from_list(board_state.dora_markers)] uradoras = [] num_akadoras = tehai_akadora_num + furo_akadora_num shanten_normal, shanten_kokushi, shanten_chitoitsu = cls.shanten_analysis.calc_all_shanten(nums, len(player_furos)) results = [] if 0 <= shanten_normal <= cls.DEPTH-1: normal_results = dfs.dfs_with_score_normal( nums, player_furos, cls.DEPTH, oya=oya, bakaze=bakaze, jikaze=jikaze, doras=doras, uradoras=uradoras, num_akadoras=num_akadoras, shanten_normal=shanten_normal, ) results.extend(normal_results) if 0 <= shanten_chitoitsu <= cls.DEPTH-1: chitoitsu_results = dfs.dfs_with_score_chitoitsu( nums, player_furos, cls.DEPTH, oya=oya, bakaze=bakaze, jikaze=jikaze, doras=doras, uradoras=uradoras, num_akadoras=num_akadoras, shanten_chitoitsu=shanten_chitoitsu, ) results.extend(chitoitsu_results) if 0 <= shanten_kokushi <= cls.DEPTH-1: kokushi_results = dfs.dfs_with_score_kokushi( nums, player_furos, cls.DEPTH, oya=oya, shanten_kokushi=shanten_kokushi, ) results.extend(kokushi_results) results = [r for r in results if r.valid()] if len(results) == 0: continue if i_from_player == 0: # ある牌を打牌(マイナス)した際に和了可能な役か。 # プレーヤー(14枚形)の際に適用。 for i in range(34): i_dahaiable_horas = [r for r in results if r.is_dahaiable(i)] if len(i_dahaiable_horas) == 0: continue yaku_dist_set = set() point_dist_set = set() for hora in i_dahaiable_horas: dist = hora.distance() point = hora.get_point() for yaku, yaku_fan in hora.get_yakus(): if yaku == "dora": if yaku_fan > 12: yaku_fan = 12 for dora_num in range(1,yaku_fan+1): yaku_dist_set.add((yaku+str(dora_num), dist)) else: yaku_dist_set.add((yaku, dist)) point_dist_set.add((point, dist)) for (yaku, dist) in yaku_dist_set: # add yaku feature if yaku in YAKU_CHANNEL_MAP: target_channel = YAKU_CHANNEL_MAP[yaku] + ((dist-1) * len(YAKU_CHANNEL_MAP)) player_offset = i_from_player*cls.ONE_PLAYER_LENGTH result[player_offset + target_channel,i,0] = 1 for (point, dist) in point_dist_set: # add hora point feature for point_index, target_point in enumerate(cls.target_points): if point >= target_point: target_channel = cls.YAKU_CH + point_index + (dist-1) * len(cls.target_points) player_offset = i_from_player*cls.ONE_PLAYER_LENGTH result[player_offset + target_channel,i,0] = 1 else: # ある牌を追加した際に和了可能な役か。 # 自分以外のプレーヤー(13枚系)の際に適用。 for i in range(34): i_need_horas = [r for r in results if r.is_tsumoneed(i)] if len(i_need_horas) == 0: continue yaku_dist_set = set() point_dist_set = set() for hora in i_need_horas: dist = hora.distance() point = hora.get_point() for yaku, yaku_fan in hora.get_yakus(): if yaku == "dora": if yaku_fan > 12: yaku_fan = 12 for dora_num in range(1,yaku_fan+1): yaku_dist_set.add((yaku+str(dora_num), dist)) else: yaku_dist_set.add((yaku, dist)) point_dist_set.add((point, dist)) for (yaku, dist) in yaku_dist_set: # add yaku feature if yaku in YAKU_CHANNEL_MAP: target_channel = YAKU_CHANNEL_MAP[yaku] + ((dist-1) * len(YAKU_CHANNEL_MAP)) player_offset = i_from_player*cls.ONE_PLAYER_LENGTH result[player_offset + target_channel,i,0] = 1 for (point, dist) in point_dist_set: # add hora point feature for point_index, target_point in enumerate(cls.target_points): if point >= target_point: target_channel = cls.YAKU_CH + point_index + (dist-1) * len(cls.target_points) player_offset = i_from_player*cls.ONE_PLAYER_LENGTH result[player_offset + target_channel,i,0] = 1
class Node(): shanten_analysis = RsShantenAnalysis() def __init__(self, tehais, taken=None): # copy tehai state self.tehai = [0] * 34 for t in tehais: self.tehai[t.id] += 1 self.furo_num = (14 - sum(self.tehai)) // 3 if taken: self.tehai[taken.id] += 1 def valid_change(self, sub_id, add_id): return not (self.tehai[add_id] >= 4 or self.tehai[sub_id] <= 0) def valid_sub(self, sub_id): return self.tehai[sub_id] > 0 def valid_add(self, add_id): return self.tehai[add_id] < 4 def change(self, sub_id, add_id): # assert self.valid_change(sub_id, add_id) self.tehai[sub_id] -= 1 self.tehai[add_id] += 1 def sub(self, sub_id): # assert self.valid_sub(sub_id) self.tehai[sub_id] -= 1 def add(self, add_id): # assert self.valid_add(add_id) self.tehai[add_id] += 1 def calc_ukeire_num(self, rest_num=None): if rest_num is None: rest_num = [4] * 34 for i in range(34): rest_num[i] -= self.tehai[i] target_shanten = self.shanten target_cache = {} node = copy.deepcopy(self) for i in range(34): # da loop if self.tehai[i] == 0: continue for j in range(34): # tsumo loop if rest_num[j] == 0: continue if i == j: continue if node.valid_change(sub_id=i, add_id=j): node.change(i, j) changed_shanten = node.shanten node.change(j, i) if changed_shanten < target_shanten: target_cache = {} target_shanten = changed_shanten if changed_shanten == target_shanten: target_cache[(i, j)] = changed_shanten valid_nums = [0] * 34 for n in target_cache: da_index = n[0] tsumo_index = n[1] valid_nums[da_index] += rest_num[tsumo_index] return valid_nums @property def shanten(self): normal, chitoitsu, kokushi = self.shanten_analysis.calc_all_shanten( self.tehai, self.furo_num) if chitoitsu < 1 and chitoitsu < normal and chitoitsu < kokushi: return chitoitsu if kokushi < 3 and kokushi < normal and kokushi < chitoitsu: return kokushi else: return normal
class MaxUkeireClient(Client): ShantenAnalyser = RsShantenAnalysis() def __init__(self, id=None, name=None): if name is None: name = f"MaxUkeire{id}" super(MaxUkeireClient, self).__init__(id, name) def clone(self): return self def think(self, board_state: BoardState): message = board_state.previous_action if message['type'] == MjMove.start_game.value and 'id' in message: self.id = message['id'] if len(board_state.possible_actions[self.id]) == 1: return board_state.possible_actions[self.id][-1] candidates = board_state.possible_actions[self.id] if 'actor' in board_state.previous_action and \ board_state.previous_action['actor'] == self.id: return self.think_on_tsumo(board_state, candidates) else: return self.think_on_other_dahai(board_state, candidates) def think_on_tsumo(self, board_state: BoardState, candidates: List[Dict]): my_tehais = board_state.tehais[self.id] rest_in_view = board_state.restpai_in_view[self.id] # if can hora, always do hora hora_candidates = [ c for c in candidates if c['type'] == MjMove.hora.value ] if len(hora_candidates) == 1: return hora_candidates[0] # if can reach, always do reach reach_candiadtes = [ c for c in candidates if c['type'] == MjMove.reach.value ] if len(reach_candiadtes) == 1: return reach_candiadtes[0] # dahai think dahai_candiadtes = [ c for c in candidates if c['type'] == MjMove.dahai.value ] # calclate the number of shanten reducing tsumo with each dahai node = Node(my_tehais) valid_nums = node.calc_ukeire_num(rest_num=rest_in_view) max_valid_dahai_action = None max_valid_num = -1 for candidate in dahai_candiadtes: valid_dahai_id = Pai.str_to_id(candidate['pai']) valid_num = valid_nums[valid_dahai_id] if max_valid_num < valid_num: max_valid_dahai_action = candidate max_valid_num = valid_num return max_valid_dahai_action def think_on_other_dahai(self, board_state: BoardState, candidates: List[Dict]): my_tehais = board_state.tehais[self.id] # if there is only one candidate, return that. if len(candidates) == 1: return candidates[-1] # if can hora, always do hora hora_candidates = [ c for c in candidates if c['type'] == MjMove.hora.value ] if len(hora_candidates) == 1: return hora_candidates[0] # if there is other player reach, don't furo. if any(board_state.reach): none_candidates = [ c for c in candidates if c['type'] == MjMove.none.value ] if len(none_candidates) == 0: raise Exception("not intended path, none candidate not found.") else: return none_candidates[0] # this path assumes there is no other player reach # shanten reduceable yakuhai pon is executed. furo_types = [ MjMove.daiminkan.value, MjMove.pon.value, MjMove.chi.value ] furo_candidates = [c for c in candidates if c['type'] in furo_types] for candidate in furo_candidates: if candidate["type"] == MjMove.pon.value: candidate_pai = Pai.from_str(candidate["pai"]) is_dragon = candidate_pai.is_sangenpai() is_jikaze = candidate_pai.str == board_state.jikaze is_bakaze = candidate_pai.str == board_state.bakaze # if is_yakuhai, 20% do pon if is_dragon or is_jikaze or is_bakaze: before_node = Node(board_state.tehais[self.id]) executed_node = Node(board_state.tehais[self.id], candidate_pai) if before_node.shanten > executed_node.shanten and\ random.random() < 0.2: return candidate # if already furo, and shanten reduceable, 60% furo. if len(board_state.furos[self.id]) > 0: candidate_pai = Pai.from_str(candidate["pai"]) before_node = Node(board_state.tehais[self.id]) executed_node = Node(board_state.tehais[self.id], candidate_pai) if before_node.shanten > executed_node.shanten and\ random.random() < 0.6: return candidate none_candidates = [ c for c in candidates if c['type'] == MjMove.none.value ] if len(none_candidates) == 0: raise Exception("not intended path, none candidate not found.") else: return none_candidates[0] @classmethod def calclate_valid_nums(cls, tehais): tehai = np.zeros(34, dtype=int) for t in tehais: tehai[t.id] += 1 # assert all([t >= 0 and t <= 4 for t in tehai]) start = datetime.datetime.now() # calclate 2 times change moves = {} for i in range(34): # remove for j in range(34): # add if i == j: continue for k in range(34): # add if i == k: continue tmp_tehai = tehai.copy() tmp_tehai[i] -= 1 tmp_tehai[j] += 1 tmp_tehai[k] += 1 if tmp_tehai[i] >= 0 and tmp_tehai[j] <= 4 and \ tmp_tehai[k] <= 4: moves[(i, j, k)] = cls.ShantenAnalyser.calc_all_shanten( tmp_tehai, 0) end = datetime.datetime.now() return tehai