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)
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
class TenpaiAnalysis: ALL_YAOCHUS = Pai.from_list([ "1m","9m","1p","9p","1s","9s","E","S","W","N","P","F","C", ]) 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 @property def tenpai(self): if self.shanten != 0: return False # assert self.shanten.shanten == 0 return (len(self.pais) % 3 != 1) or (len(self.waited_pais) > 0) @property def waited_pais(self): # assert len(self.pais) % 3 == 1, "invalid number of pais" # assert self.shanten.shanten == 0, "not tenpai" return self.waiting
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()
class Dfs(): def __init__(self): self.score_cache = {} self.shanten_analysis = RsShantenAnalysis() def dfs_hora(self, depth, tehais, furos, points, rest=None): """return pai change combinations which achive hora and hora score >= points. Args: depth ([type]): [description] tehais ([type]): [description] furos ([type]): [description] points ([type]): [description] rest ([type], optional): [description]. Defaults to None. Returns: [type]: [description] """ if rest is None: rest = [4] * 34 for i in range(34): rest[i] -= tehais[i] result = {} # initialize for point in points: result[point] = [0] * 34 if sum(tehais) % 3 == 2: for point in points: dfs_results = self._dfs_hora(depth, tehais, furos, point, rest) result[point] = np.array(dfs_results, dtype="int8") elif sum(tehais) % 3 == 1: for point in points: result[point] = np.zeros((34, ), dtype="int8") else: raise Exception("not intended path") return result def _dfs_hora(self, depth, tehais, furos, point, rest, added=None): """returns [bool] * 34 """ stack = deque() furo_num = len(furos) base_shanten = self.shanten_analysis.calc_shanten(tehais, furo_num) if added: initial_history = [tuple((None, added))] else: initial_history = [] stack.append((base_shanten, depth, tehais, furos, initial_history)) i_found = [False] * 34 while len(stack) > 0: target_shanten, target_depth, target_tehais, target_furos, target_history = stack.pop( ) if target_depth <= 0: continue if len(target_history) > 0 and i_found[target_history[0][0]]: continue # apply history for rest for h in target_history: rest[h[1]] -= 1 # sub for i in range(34): temp_i_found = False if target_tehais[i] == 0: continue target_tehais[i] -= 1 shanten = self.shanten_analysis.calc_shanten( target_tehais, furo_num) if shanten - depth > -1: # cannot reach hora target_tehais[i] += 1 continue if target_shanten < shanten: target_tehais[i] += 1 continue rest_depth = target_depth - 1 # add for j in range(34): if temp_i_found: break if target_tehais[j] == 4 or rest[j] == 0: continue target_tehais[j] += 1 target_history.append(tuple((i, j))) changed_shanten = self.shanten_analysis.calc_shanten( target_tehais, furo_num) if changed_shanten - (rest_depth) <= -1: if changed_shanten == -1: # if hora, treat as leaf key = get_key(target_tehais, target_furos, j) if key not in self.score_cache: hora = get_score(target_tehais, target_furos, target_history[-1][1]) self.score_cache[key] = hora.points score = self.score_cache[key] if score >= point: i_found[target_history[0][0]] = True temp_i_found = True else: stack.append((changed_shanten, rest_depth, copy.copy(target_tehais), copy.copy(target_furos), copy.copy(target_history))) target_history.pop() target_tehais[j] -= 1 target_tehais[i] += 1 # restore rest for h in target_history: rest[h[1]] += 1 return i_found
def __init__(self, board, seat=None): self.id = seat self.board = board self.reset() self.shanten_analysis = RsShantenAnalysis()
class Player(): def __init__(self, board, seat=None): self.id = seat self.board = board self.reset() self.shanten_analysis = RsShantenAnalysis() def reset(self): self.score = 25000 self.tehais = [] self.furos = [] self.ho = [] # 鳴かれた牌を含まない self.sutehais = [] # 鳴かれた牌を含む self.extra_anpais = [] # sutehais以外のこのプレーヤーに対する安牌 self.reach_state = "none" self.reach_ho_index = None self.reach_sutehais_index = None self.double_reach = False self.pao_for_id = None self.ippatsu_chance = False self.rinshan = False @property def str_sep_tehais(self): return [t.str for t in self.tehais] @property def red_dora_num(self): num = 0 num += len([t for t in self.tehais if t.is_red]) for furo in self.furos: num += len([p for p in furo.pais if p.is_red]) return num @property def furo_open_red_dora_num(self): num = 0 for furo in self.furos: num += len([p for p in furo.pais if p.is_red]) return num @property def str_tehais(self): # add hidden tehai type_history = set() show_str_reverse = [] if (not self.tehais) or len(self.tehais) == 0: return '' if len(self.tehais) % 3 == 2: # tsumo timing show_str_reverse.append(self.tehais[-1].str) show_str_reverse.append(" ") loop_tehais = self.tehais[:-1] else: loop_tehais = self.tehais for t in reversed(loop_tehais): if t.is_number() and t.type in type_history: if t.is_red: show_str_reverse.append(t.str[0]+t.str[2]) else: show_str_reverse.append(t.str[0]) else: type_history.add(t.type) show_str_reverse.append(t.str) # add furo return ''.join(reversed(show_str_reverse)) @property def anpais(self): return list(sorted(set(self.sutehais + self.extra_anpais))) @property def reach(self): return self.reach_state == "accepted" @property def menzen(self): return len([f for f in self.furos if f.type != MjMove.ankan.value]) == 0 def update_state(self, action): if self.board.previous_action is not None and \ self.board.previous_action['type'] in [MjMove.dahai.value, MjMove.kakan.value] and \ 'actor' in self.board.previous_action and \ self.board.previous_action['actor'] != self.id and \ action['type'] != MjMove.hora.value: self.extra_anpais.append(Pai.from_str(self.board.previous_action['pai'])) action_type = action['type'] if action_type == MjMove.start_game.value: if not self.id and 'id' in action: self.id = action['id'] self.name = f"player{self.id}" if 'names' in action: self.name = action['names'][self.id] self.score = 25000 self.tehais = [] self.furos = [] self.ho = [] self.sutehais = [] self.extra_anpais = [] self.reach_state = None self.reach_ho_index = None self.reach_sutehais_index = None self.double_reach = False self.ippatsu_chance = False self.pao_for_id = None self.rinshan = False elif action_type == MjMove.start_kyoku.value: self.tehais = sorted(Pai.from_list(action['tehais'][self.id])) self.furos = [] self.ho = [] self.sutehais = [] self.extra_anpais = [] self.reach_state = "none" self.reach_ho_index = None self.reach_sutehais_index = None self.double_reach = False self.ippatsu_chance = False self.pao_for_id = None self.rinshan = False elif action_type in [ MjMove.chi.value, MjMove.pon.value, MjMove.daiminkan.value, MjMove.ankan.value ]: self.ippatsu_chance = False elif action_type == MjMove.tsumo.value: if self.board.previous_action['type'] == MjMove.kakan.value: self.ippatsu_chance = False if 'actor' in action and action['actor'] == self.id: if action_type == MjMove.tsumo.value: pai = Pai.from_str(action['pai']) self.tehais = sorted(self.tehais) self.tehais.append(pai) elif action_type == MjMove.dahai.value: pai = Pai.from_str(action['pai']) self.delete_tehai(pai) self.tehais = sorted(self.tehais) self.ho.append(pai) self.sutehais.append(pai) self.ippatsu_chance = False self.rinshan = False if self.reach == False: self.extra_anpais.clear() elif action_type in [ MjMove.chi.value, MjMove.pon.value, MjMove.daiminkan.value, MjMove.ankan.value ]: consumed_pais = Pai.from_list(action['consumed']) for c in consumed_pais: self.delete_tehai(c) furo = { 'type':action['type'], 'consumed':consumed_pais, } if action_type != MjMove.ankan.value: pai = Pai.from_str(action['pai']) furo['taken'] = pai if action_type == MjMove.chi.value: furo['pai_id'] = min( Pai.str_to_id(action['pai']), min( Pai.str_to_id(action['consumed'][0]), Pai.str_to_id(action['consumed'][1])) ) else: furo['pai_id'] = Pai.str_to_id(action['consumed'][0]) if action_type == MjMove.ankan.value: furo['target'] = self.id else: furo['target'] = action['target'] self.furos.append(Furo(furo)) if action_type in [MjMove.daiminkan.value, MjMove.ankan.value]: self.rinshan = True # pao if action_type in [MjMove.daiminkan.value, MjMove.pon.value]: pai = Pai.from_str(action['pai']) if pai.is_sangenpai(): if self.is_daisangen_pao(): self.pao_for_id = action['target'] elif pai.is_wind(): if self.is_daisushi_pao(): self.pao_for_id = action['target'] elif action_type == MjMove.kakan.value: pai = Pai.from_str(action['pai']) self.delete_tehai(pai) pon_index = -1 for i,f in enumerate(self.furos): if f.type == MjMove.pon.value and pai.is_same_symbol(f.taken): pon_index = i if pon_index == -1: raise Exception('not have same symbole pon') self.furos[pon_index] = Furo({ 'type':MjMove.kakan.value, 'taken':self.furos[pon_index].taken, 'consumed':self.furos[pon_index].consumed + [pai], 'target':self.furos[pon_index].target, 'pai_id':self.furos[pon_index].pai_id, }) self.rinshan = True elif action_type == MjMove.reach.value: self.reach_state = 'declared' self.double_reach = self.board.first_turn elif action_type == MjMove.reach_accepted.value: self.reach_state = 'accepted' self.reach_ho_index = len(self.ho)-1 self.reach_sutehais_index = len(self.sutehais)-1 self.ippatsu_chance = True if 'target' in action and action['target'] == self.id: pai = Pai.from_str(action['pai']) if action_type in [ MjMove.pon.value, MjMove.chi.value, MjMove.daiminkan.value ]: taken = self.ho.pop() # assert taken == pai if 'scores' in action: self.score = action['scores'][self.id] @property def jikaze(self): if self.board.oya is not None: wind_index = (4 + self.id - self.board.oya) % 4 return ['E','S','W','N'][wind_index] else: return None @property def tenpai(self): return self.shanten <= 0 @property def furiten(self): if len(self.tehais) % 3 != 1: return False if UNKNOWN_PAI in self.tehais: return False tenpai_info = TenpaiAnalysis(self.tehais) if tenpai_info.tenpai == False: return False anpais = self.anpais return any([a.is_same_symbol(b) for (a,b) in itertools.product(anpais, tenpai_info.waited_pais)]) def is_daisangen_pao(self): return len([f for f in self.furos if f.pais[0].is_sangenpai()]) == 3 def is_daisushi_pao(self): return len([f for f in self.furos if f.pais[0].is_wind()]) == 4 def delete_tehai(self, pai): if pai in self.tehais: pai_index = self.tehais.index(pai) else: pai_index = 0 # assert self.tehais[pai_index].str == UNKNOWN_PAI_STR del self.tehais[pai_index] def get_chi_dahai(self, chi_action): dahai_dic = {} chi_pai = Pai.from_str(chi_action['pai']) chi_consumed = Pai.from_list(chi_action['consumed']) upper_kuikae_exists = False lower_kuikae_exists = False if chi_consumed[0].number + 1 == chi_consumed[1].number: if chi_pai.number + 1 == chi_consumed[0].number and chi_pai.number != 7: upper_kuikae_exists = True elif chi_pai.number - 2 == chi_consumed[0].number and chi_pai.number != 3: lower_kuikae_exists = True for tehai in self.tehais: if tehai.str in dahai_dic: continue if chi_pai.is_same_symbol(tehai): continue if upper_kuikae_exists and \ tehai.type == chi_pai.type and \ tehai.number - 3 == chi_pai.number: continue if lower_kuikae_exists and \ tehai.type == chi_pai.type and \ tehai.number + 3 == chi_pai.number: continue dahai_dic[tehai.str] = '' return [d for d in dahai_dic.keys()] def __str__(self): return f"tehai:{self.tehais}, furo:{self.furos}\n" \ f"sutehai:{self.sutehais}\n" \ f"score:{self.score}, reache:{self.reach}" @property def shanten(self): tehai = [0] * 34 for t in self.tehais: tehai[t.id] += 1 return self.shanten_analysis.calc_shanten(tehai, len(self.furos)) def calc_dahaied_shanten(self, pai:str): pai = Pai.from_str(pai) if pai not in self.tehais: raise Exception(f'tehais not condains pai:{pai}') dahaied_tehai = self.tehais.copy() dahaied_tehai.remove(pai) tehai = [0] * 34 for t in dahaied_tehai: tehai[t.id] += 1 return self.shanten_analysis.calc_shanten(tehai, len(self.furos)) def calc_added_shanten(self, pai:str): pai = Pai.from_str(pai) dahaied_tehai = self.tehais.copy() dahaied_tehai.append(pai) tehai = [0] * 34 for t in dahaied_tehai: tehai[t.id] += 1 return self.shanten_analysis.calc_shanten(tehai, len(self.furos)) def can_hora(self): previous_action = self.board.previous_action if not previous_action: return False if previous_action['type'] == MjMove.tsumo.value and self.id == previous_action['actor']: hora_type = 'tsumo' pais = self.tehais shanten = self.shanten elif previous_action['type'] in [MjMove.dahai.value, MjMove.kakan.value] and self.id != previous_action['actor']: hora_type = 'ron' pais = self.tehais + [Pai.from_str(previous_action['pai'])] shanten = self.calc_added_shanten(previous_action['pai']) else: return False if shanten != -1: return False action = { 'type':MjMove.hora.value, 'pai':previous_action['pai'], 'actor':self.id, 'target':previous_action['actor'], } hora = self.board.get_hora(action, **{'previous_action':previous_action}) return hora.valid and (hora_type == 'tsumo' or self.furiten == False) def can_daiminkan(self, pai:str): if self.reach_state == "accepted": return False, [] target_pai = Pai.from_str(pai) candidates = [t.str for t in self.tehais if target_pai.is_same_symbol(t)] if len(candidates) < 3: return False, [] else: return True, candidates def can_ankan(self, pai:str=None): if pai is None: pai_count = {} for t in self.tehais: if t.id not in pai_count: pai_count[t.id] = 0 pai_count[t.id] += 1 can_ankan_ids = [p for p in pai_count if pai_count[p] == 4] if len(can_ankan_ids) == 0: return False, [] candidates = [] for can_ankan_id in can_ankan_ids: candidate = [p.str for p in self.tehais if p.id == can_ankan_id] candidates.append(candidate) return True, candidates else: in_tehai = [p for p in self.tehais if p.is_same_symbol(Pai.from_str(pai))] return len(in_tehai) == 4, in_tehai def can_kakan(self, pai:str=None): if self.reach_state == "accepted": return False, [] if pai is None: pons = {} for f in self.furos: if f.type == 'pon': pons[f.pais[0].id] = [p.str for p in f.pais] can_kakan_ids = [p.id for p in self.tehais if p.id in pons] if len(can_kakan_ids) == 0: return False, [] candidates = [] for can_kakan_id in can_kakan_ids: candidate = [p.str for p in self.tehais if p.id == can_kakan_id] # assert len(candidate) == 1 candidates.append([candidate[0], pons[can_kakan_id]]) return True, candidates else: target_pai = Pai.from_str(pai) in_tehai = [p for p in self.tehais if p.is_same_symbol(target_pai)] in_pon = [[p.str for p in f.pais] for f in self.furos if f.type == 'pon' and f.pais[0].is_same_symbol(target_pai)] return len(in_tehai)==1 & len(in_pon)==1, [in_tehai[0], in_pon] def can_pon(self, pai:str): if self.reach_state == "accepted": return False, [] target_pai = Pai.from_str(pai) candidates_pai = [t for t in self.tehais if target_pai.is_same_symbol(t)] if len(candidates_pai) < 2: return False, [] red_consumed = ([c for c in candidates_pai if c.is_red]) not_red_consumed = ([c for c in candidates_pai if c.is_red == False]) if len(red_consumed) > 0: if len(candidates_pai) == 2: return True, [ [red_consumed[0].str, not_red_consumed[0].str], ] else: return True, [ [red_consumed[0].str, not_red_consumed[0].str], [not_red_consumed[0].str, not_red_consumed[0].str], ] else: return True, [ [not_red_consumed[0].str, not_red_consumed[1].str] ] def can_chi(self, pai:str): if self.reach_state == "accepted": return False, [] target_pai = Pai.from_str(pai) if not target_pai.is_number(): return False, [] can_chi = False candidates = [] tehais_single = {} for t in self.tehais: if t.str not in tehais_single: tehais_single[t.str] = t single_tehais = tehais_single.values() minus2_candidates_pai = [t.str for t in single_tehais if t.number == target_pai.number-2 and t.type == target_pai.type] minus1_candidates_pai = [t.str for t in single_tehais if t.number == target_pai.number-1 and t.type == target_pai.type] plus1_candidates_pai = [t.str for t in single_tehais if t.number == target_pai.number+1 and t.type == target_pai.type] plus2_candidates_pai = [t.str for t in single_tehais if t.number == target_pai.number+2 and t.type == target_pai.type] cannot_dahai_number = [] # right chi [1,2] [3] if len(minus2_candidates_pai) > 0 and len(minus1_candidates_pai) > 0: cannot_dahai_number.append(target_pai.number-3) cannot_dahai_number.append(target_pai.number) for m2, m1 in itertools.product(minus2_candidates_pai, minus1_candidates_pai): candidates.append([m2, m1]) # center chi [1,3] [2] if len(minus1_candidates_pai) > 0 and len(plus1_candidates_pai) > 0: cannot_dahai_number.append(target_pai.number) for m1, p1 in itertools.product(minus1_candidates_pai, plus1_candidates_pai): candidates.append([m1, p1]) # left chi [2,3] [1] if len(plus1_candidates_pai) > 0 and len(plus2_candidates_pai) > 0: cannot_dahai_number.append(target_pai.number) cannot_dahai_number.append(target_pai.number+3) for p1, p2 in itertools.product(plus1_candidates_pai, plus2_candidates_pai): candidates.append([p1, p2]) # kuikae check ignore_candidates = [] if all([t for t in self.tehais if t.type == target_pai.type]): for candidate in candidates: temp_tehai = copy.copy(self.tehais) temp_tehai.remove(Pai.from_str(candidate[0])) temp_tehai.remove(Pai.from_str(candidate[1])) if len([t for t in temp_tehai if t.number not in cannot_dahai_number]) == 0: ignore_candidates.append(candidate) for ignore_candidate in ignore_candidates: candidates.remove(ignore_candidate) return len(candidates) > 0, candidates
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