def __init__(self): """ changed the input from filename to tiles_state_and_action data By Jun Lin """ self.shanten_calculator = Shanten() self.hc = HandCalculator() self.sc = ScoresCalculator() self.hand_cache_shanten = {} self.hand_cache_points = {}
def test_kiriage_mangan(self): hand = ScoresCalculator() config = HandConfig(options=OptionalRules(kiriage=True)) result = hand.calculate_scores(han=4, fu=30, config=config) self.assertEqual(result['main'], 8000) result = hand.calculate_scores(han=3, fu=60, config=config) self.assertEqual(result['main'], 8000) config = HandConfig(player_wind=EAST, options=OptionalRules(kiriage=True)) result = hand.calculate_scores(han=4, fu=30, config=config) self.assertEqual(result['main'], 12000) result = hand.calculate_scores(han=3, fu=60, config=config) self.assertEqual(result['main'], 12000)
def test_calculate_scores_with_bonus(self): hand = ScoresCalculator() config = HandConfig(player_wind=EAST, is_tsumo=True, tsumi_number=2, kyoutaku_number=3) result = hand.calculate_scores(han=3, fu=30, config=config) self.assertEqual(result["main"], 2000) self.assertEqual(result["additional"], 2000) self.assertEqual(result["main_bonus"], 200) self.assertEqual(result["additional_bonus"], 200) self.assertEqual(result["kyoutaku_bonus"], 3000) self.assertEqual(result["total"], 9600) config = HandConfig(player_wind=WEST, is_tsumo=True, tsumi_number=4, kyoutaku_number=1) result = hand.calculate_scores(han=4, fu=30, config=config) self.assertEqual(result["main"], 3900) self.assertEqual(result["additional"], 2000) self.assertEqual(result["main_bonus"], 400) self.assertEqual(result["additional_bonus"], 400) self.assertEqual(result["kyoutaku_bonus"], 1000) self.assertEqual(result["total"], 10100) config = HandConfig(player_wind=WEST, tsumi_number=5) result = hand.calculate_scores(han=6, fu=30, config=config) self.assertEqual(result["main"], 12000) self.assertEqual(result["additional"], 0) self.assertEqual(result["main_bonus"], 1500) self.assertEqual(result["additional_bonus"], 0) self.assertEqual(result["kyoutaku_bonus"], 0) self.assertEqual(result["total"], 13500) config = HandConfig(player_wind=EAST, tsumi_number=5) result = hand.calculate_scores(han=5, fu=30, config=config) self.assertEqual(result["main"], 12000) self.assertEqual(result["additional"], 0) self.assertEqual(result["main_bonus"], 1500) self.assertEqual(result["additional_bonus"], 0) self.assertEqual(result["kyoutaku_bonus"], 0) self.assertEqual(result["total"], 13500)
def test_calculate_scores_and_ron_by_dealer(self): hand = ScoresCalculator() config = HandConfig(player_wind=EAST, options=OptionalRules(kazoe_limit=HandConfig.KAZOE_NO_LIMIT)) result = hand.calculate_scores(han=1, fu=30, config=config) self.assertEqual(result['main'], 1500) result = hand.calculate_scores(han=2, fu=30, config=config) self.assertEqual(result['main'], 2900) result = hand.calculate_scores(han=3, fu=30, config=config) self.assertEqual(result['main'], 5800) result = hand.calculate_scores(han=4, fu=30, config=config) self.assertEqual(result['main'], 11600) result = hand.calculate_scores(han=5, fu=0, config=config) self.assertEqual(result['main'], 12000) result = hand.calculate_scores(han=6, fu=0, config=config) self.assertEqual(result['main'], 18000) result = hand.calculate_scores(han=8, fu=0, config=config) self.assertEqual(result['main'], 24000) result = hand.calculate_scores(han=11, fu=0, config=config) self.assertEqual(result['main'], 36000) result = hand.calculate_scores(han=13, fu=0, config=config) self.assertEqual(result['main'], 48000) result = hand.calculate_scores(han=26, fu=0, config=config) self.assertEqual(result['main'], 96000) result = hand.calculate_scores(han=39, fu=0, config=config) self.assertEqual(result['main'], 144000) result = hand.calculate_scores(han=52, fu=0, config=config) self.assertEqual(result['main'], 192000) result = hand.calculate_scores(han=65, fu=0, config=config) self.assertEqual(result['main'], 240000) result = hand.calculate_scores(han=78, fu=0, config=config) self.assertEqual(result['main'], 288000)
def test_calculate_scores_and_tsumo_by_dealer(self): hand = ScoresCalculator() config = HandConfig(player_wind=EAST, is_tsumo=True, options=OptionalRules(kazoe_limit=HandConfig.KAZOE_NO_LIMIT)) result = hand.calculate_scores(han=1, fu=30, config=config) self.assertEqual(result['main'], 500) self.assertEqual(result['additional'], 500) result = hand.calculate_scores(han=3, fu=30, config=config) self.assertEqual(result['main'], 2000) self.assertEqual(result['additional'], 2000) result = hand.calculate_scores(han=4, fu=30, config=config) self.assertEqual(result['main'], 3900) self.assertEqual(result['additional'], 3900) result = hand.calculate_scores(han=5, fu=0, config=config) self.assertEqual(result['main'], 4000) self.assertEqual(result['additional'], 4000) result = hand.calculate_scores(han=6, fu=0, config=config) self.assertEqual(result['main'], 6000) self.assertEqual(result['additional'], 6000) result = hand.calculate_scores(han=8, fu=0, config=config) self.assertEqual(result['main'], 8000) self.assertEqual(result['additional'], 8000) result = hand.calculate_scores(han=11, fu=0, config=config) self.assertEqual(result['main'], 12000) self.assertEqual(result['additional'], 12000) result = hand.calculate_scores(han=13, fu=0, config=config) self.assertEqual(result['main'], 16000) self.assertEqual(result['additional'], 16000) result = hand.calculate_scores(han=26, fu=0, config=config) self.assertEqual(result['main'], 32000) self.assertEqual(result['additional'], 32000) result = hand.calculate_scores(han=39, fu=0, config=config) self.assertEqual(result['main'], 48000) self.assertEqual(result['additional'], 48000) result = hand.calculate_scores(han=52, fu=0, config=config) self.assertEqual(result['main'], 64000) self.assertEqual(result['additional'], 64000) result = hand.calculate_scores(han=65, fu=0, config=config) self.assertEqual(result['main'], 80000) self.assertEqual(result['additional'], 80000) result = hand.calculate_scores(han=78, fu=0, config=config) self.assertEqual(result['main'], 96000) self.assertEqual(result['additional'], 96000)
def test_calculate_scores_and_ron(self): hand = ScoresCalculator() config = HandConfig(options=OptionalRules(kazoe_limit=HandConfig.KAZOE_NO_LIMIT)) result = hand.calculate_scores(han=1, fu=30, config=config) self.assertEqual(result['main'], 1000) result = hand.calculate_scores(han=1, fu=110, config=config) self.assertEqual(result['main'], 3600) result = hand.calculate_scores(han=2, fu=30, config=config) self.assertEqual(result['main'], 2000) result = hand.calculate_scores(han=3, fu=30, config=config) self.assertEqual(result['main'], 3900) result = hand.calculate_scores(han=4, fu=30, config=config) self.assertEqual(result['main'], 7700) result = hand.calculate_scores(han=4, fu=40, config=config) self.assertEqual(result['main'], 8000) result = hand.calculate_scores(han=5, fu=0, config=config) self.assertEqual(result['main'], 8000) result = hand.calculate_scores(han=6, fu=0, config=config) self.assertEqual(result['main'], 12000) result = hand.calculate_scores(han=8, fu=0, config=config) self.assertEqual(result['main'], 16000) result = hand.calculate_scores(han=11, fu=0, config=config) self.assertEqual(result['main'], 24000) result = hand.calculate_scores(han=13, fu=0, config=config) self.assertEqual(result['main'], 32000) result = hand.calculate_scores(han=26, fu=0, config=config) self.assertEqual(result['main'], 64000) result = hand.calculate_scores(han=39, fu=0, config=config) self.assertEqual(result['main'], 96000) result = hand.calculate_scores(han=52, fu=0, config=config) self.assertEqual(result['main'], 128000) result = hand.calculate_scores(han=65, fu=0, config=config) self.assertEqual(result['main'], 160000) result = hand.calculate_scores(han=78, fu=0, config=config) self.assertEqual(result['main'], 192000)
def estimate_hand_value(self, tiles, win_tile, melds=None, dora_indicators=None, config=None): """ :param tiles: array with 14 tiles in 136-tile format :param win_tile: 136 format tile that caused win (ron or tsumo) :param melds: array with Meld objects :param dora_indicators: array of tiles in 136-tile format :param config: HandConfig object :return: HandResponse object """ if not melds: melds = [] if not dora_indicators: dora_indicators = [] self.config = config or HandConfig() agari = Agari() hand_yaku = [] scores_calculator = ScoresCalculator() tiles_34 = TilesConverter.to_34_array(tiles) divider = HandDivider() fu_calculator = FuCalculator() opened_melds = [x.tiles_34 for x in melds if x.opened] all_melds = [x.tiles_34 for x in melds] is_open_hand = len(opened_melds) > 0 # special situation if self.config.is_nagashi_mangan: hand_yaku.append(self.config.yaku.nagashi_mangan) fu = 30 han = self.config.yaku.nagashi_mangan.han_closed cost = scores_calculator.calculate_scores(han, fu, self.config, False) return HandResponse(cost, han, fu, hand_yaku) if win_tile not in tiles: return HandResponse(error="Win tile not in the hand") if self.config.is_riichi and is_open_hand: return HandResponse( error="Riichi can't be declared with open hand") if self.config.is_ippatsu and is_open_hand: return HandResponse( error="Ippatsu can't be declared with open hand") if self.config.is_ippatsu and not self.config.is_riichi and not self.config.is_daburu_riichi: return HandResponse( error="Ippatsu can't be declared without riichi") if not agari.is_agari(tiles_34, all_melds): return HandResponse(error='Hand is not winning') if not self.config.options.has_double_yakuman: self.config.yaku.daburu_kokushi.han_closed = 13 self.config.yaku.suuankou_tanki.han_closed = 13 self.config.yaku.daburu_chuuren_poutou.han_closed = 13 self.config.yaku.daisuushi.han_closed = 13 self.config.yaku.daisuushi.han_open = 13 hand_options = divider.divide_hand(tiles_34, melds) calculated_hands = [] for hand in hand_options: is_chiitoitsu = self.config.yaku.chiitoitsu.is_condition_met(hand) valued_tiles = [ HAKU, HATSU, CHUN, self.config.player_wind, self.config.round_wind ] win_groups = self._find_win_groups(win_tile, hand, opened_melds) for win_group in win_groups: cost = None error = None hand_yaku = [] han = 0 fu_details, fu = fu_calculator.calculate_fu( hand, win_tile, win_group, self.config, valued_tiles, melds) is_pinfu = len( fu_details) == 1 and not is_chiitoitsu and not is_open_hand pon_sets = [x for x in hand if is_pon(x)] chi_sets = [x for x in hand if is_chi(x)] if self.config.is_tsumo: if not is_open_hand: hand_yaku.append(self.config.yaku.tsumo) if is_pinfu: hand_yaku.append(self.config.yaku.pinfu) # let's skip hand that looks like chitoitsu, but it contains open sets if is_chiitoitsu and is_open_hand: continue if is_chiitoitsu: hand_yaku.append(self.config.yaku.chiitoitsu) is_daisharin = self.config.yaku.daisharin.is_condition_met( hand, self.config.options.has_daisharin_other_suits) if self.config.options.has_daisharin and is_daisharin: self.config.yaku.daisharin.rename(hand) hand_yaku.append(self.config.yaku.daisharin) is_tanyao = self.config.yaku.tanyao.is_condition_met(hand) if is_open_hand and not self.config.options.has_open_tanyao: is_tanyao = False if is_tanyao: hand_yaku.append(self.config.yaku.tanyao) if self.config.is_riichi and not self.config.is_daburu_riichi: hand_yaku.append(self.config.yaku.riichi) if self.config.is_daburu_riichi: hand_yaku.append(self.config.yaku.daburu_riichi) if self.config.is_ippatsu: hand_yaku.append(self.config.yaku.ippatsu) if self.config.is_rinshan: hand_yaku.append(self.config.yaku.rinshan) if self.config.is_chankan: hand_yaku.append(self.config.yaku.chankan) if self.config.is_haitei: hand_yaku.append(self.config.yaku.haitei) if self.config.is_houtei: hand_yaku.append(self.config.yaku.houtei) if self.config.is_renhou: if self.config.options.renhou_as_yakuman: hand_yaku.append(self.config.yaku.renhou_yakuman) else: hand_yaku.append(self.config.yaku.renhou) if self.config.is_tenhou: hand_yaku.append(self.config.yaku.tenhou) if self.config.is_chiihou: hand_yaku.append(self.config.yaku.chiihou) if self.config.yaku.honitsu.is_condition_met(hand): hand_yaku.append(self.config.yaku.honitsu) if self.config.yaku.chinitsu.is_condition_met(hand): hand_yaku.append(self.config.yaku.chinitsu) if self.config.yaku.tsuisou.is_condition_met(hand): hand_yaku.append(self.config.yaku.tsuisou) if self.config.yaku.honroto.is_condition_met(hand): hand_yaku.append(self.config.yaku.honroto) if self.config.yaku.chinroto.is_condition_met(hand): hand_yaku.append(self.config.yaku.chinroto) # small optimization, try to detect yaku with chi required sets only if we have chi sets in hand if len(chi_sets): if self.config.yaku.chanta.is_condition_met(hand): hand_yaku.append(self.config.yaku.chanta) if self.config.yaku.junchan.is_condition_met(hand): hand_yaku.append(self.config.yaku.junchan) if self.config.yaku.ittsu.is_condition_met(hand): hand_yaku.append(self.config.yaku.ittsu) if not is_open_hand: if self.config.yaku.ryanpeiko.is_condition_met(hand): hand_yaku.append(self.config.yaku.ryanpeiko) elif self.config.yaku.iipeiko.is_condition_met(hand): hand_yaku.append(self.config.yaku.iipeiko) if self.config.yaku.sanshoku.is_condition_met(hand): hand_yaku.append(self.config.yaku.sanshoku) # small optimization, try to detect yaku with pon required sets only if we have pon sets in hand if len(pon_sets): if self.config.yaku.toitoi.is_condition_met(hand): hand_yaku.append(self.config.yaku.toitoi) if self.config.yaku.sanankou.is_condition_met( hand, win_tile, melds, self.config.is_tsumo): hand_yaku.append(self.config.yaku.sanankou) if self.config.yaku.sanshoku_douko.is_condition_met(hand): hand_yaku.append(self.config.yaku.sanshoku_douko) if self.config.yaku.shosangen.is_condition_met(hand): hand_yaku.append(self.config.yaku.shosangen) if self.config.yaku.haku.is_condition_met(hand): hand_yaku.append(self.config.yaku.haku) if self.config.yaku.hatsu.is_condition_met(hand): hand_yaku.append(self.config.yaku.hatsu) if self.config.yaku.chun.is_condition_met(hand): hand_yaku.append(self.config.yaku.chun) if self.config.yaku.east.is_condition_met( hand, self.config.player_wind, self.config.round_wind): if self.config.player_wind == EAST: hand_yaku.append(self.config.yaku.yakuhai_place) if self.config.round_wind == EAST: hand_yaku.append(self.config.yaku.yakuhai_round) if self.config.yaku.south.is_condition_met( hand, self.config.player_wind, self.config.round_wind): if self.config.player_wind == SOUTH: hand_yaku.append(self.config.yaku.yakuhai_place) if self.config.round_wind == SOUTH: hand_yaku.append(self.config.yaku.yakuhai_round) if self.config.yaku.west.is_condition_met( hand, self.config.player_wind, self.config.round_wind): if self.config.player_wind == WEST: hand_yaku.append(self.config.yaku.yakuhai_place) if self.config.round_wind == WEST: hand_yaku.append(self.config.yaku.yakuhai_round) if self.config.yaku.north.is_condition_met( hand, self.config.player_wind, self.config.round_wind): if self.config.player_wind == NORTH: hand_yaku.append(self.config.yaku.yakuhai_place) if self.config.round_wind == NORTH: hand_yaku.append(self.config.yaku.yakuhai_round) if self.config.yaku.daisangen.is_condition_met(hand): hand_yaku.append(self.config.yaku.daisangen) if self.config.yaku.shosuushi.is_condition_met(hand): hand_yaku.append(self.config.yaku.shosuushi) if self.config.yaku.daisuushi.is_condition_met(hand): hand_yaku.append(self.config.yaku.daisuushi) if self.config.yaku.ryuisou.is_condition_met(hand): hand_yaku.append(self.config.yaku.ryuisou) # closed kan can't be used in chuuren_poutou if not len( melds ) and self.config.yaku.chuuren_poutou.is_condition_met( hand): if tiles_34[win_tile // 4] == 2 or tiles_34[win_tile // 4] == 4: hand_yaku.append( self.config.yaku.daburu_chuuren_poutou) else: hand_yaku.append(self.config.yaku.chuuren_poutou) if not is_open_hand and self.config.yaku.suuankou.is_condition_met( hand, win_tile, self.config.is_tsumo): if tiles_34[win_tile // 4] == 2: hand_yaku.append(self.config.yaku.suuankou_tanki) else: hand_yaku.append(self.config.yaku.suuankou) if self.config.yaku.sankantsu.is_condition_met( hand, melds): hand_yaku.append(self.config.yaku.sankantsu) if self.config.yaku.suukantsu.is_condition_met( hand, melds): hand_yaku.append(self.config.yaku.suukantsu) # yakuman is not connected with other yaku yakuman_list = [x for x in hand_yaku if x.is_yakuman] if yakuman_list: hand_yaku = yakuman_list # calculate han for item in hand_yaku: if is_open_hand and item.han_open: han += item.han_open else: han += item.han_closed if han == 0: error = 'There are no yaku in the hand' cost = None # we don't need to add dora to yakuman if not yakuman_list: tiles_for_dora = tiles[:] # we had to search for dora in kan fourth tiles as well for meld in melds: if meld.type == Meld.KAN or meld.type == Meld.CHANKAN: tiles_for_dora.append(meld.tiles[3]) count_of_dora = 0 count_of_aka_dora = 0 for tile in tiles_for_dora: count_of_dora += plus_dora(tile, dora_indicators) for tile in tiles_for_dora: if is_aka_dora(tile, self.config.options.has_aka_dora): count_of_aka_dora += 1 if count_of_dora: self.config.yaku.dora.han_open = count_of_dora self.config.yaku.dora.han_closed = count_of_dora hand_yaku.append(self.config.yaku.dora) han += count_of_dora if count_of_aka_dora: self.config.yaku.aka_dora.han_open = count_of_aka_dora self.config.yaku.aka_dora.han_closed = count_of_aka_dora hand_yaku.append(self.config.yaku.aka_dora) han += count_of_aka_dora if not error: cost = scores_calculator.calculate_scores( han, fu, self.config, len(yakuman_list) > 0) calculated_hand = { 'cost': cost, 'error': error, 'hand_yaku': hand_yaku, 'han': han, 'fu': fu, 'fu_details': fu_details } calculated_hands.append(calculated_hand) # exception hand if not is_open_hand and self.config.yaku.kokushi.is_condition_met( None, tiles_34): if tiles_34[win_tile // 4] == 2: hand_yaku.append(self.config.yaku.daburu_kokushi) else: hand_yaku.append(self.config.yaku.kokushi) if self.config.is_renhou and self.config.options.renhou_as_yakuman: hand_yaku.append(self.config.yaku.renhou_yakuman) if self.config.is_tenhou: hand_yaku.append(self.config.yaku.tenhou) if self.config.is_chiihou: hand_yaku.append(self.config.yaku.chiihou) # calculate han han = 0 for item in hand_yaku: if is_open_hand and item.han_open: han += item.han_open else: han += item.han_closed fu = 0 cost = scores_calculator.calculate_scores(han, fu, self.config, len(hand_yaku) > 0) calculated_hands.append({ 'cost': cost, 'error': None, 'hand_yaku': hand_yaku, 'han': han, 'fu': fu, 'fu_details': [] }) # let's use cost for most expensive hand calculated_hands = sorted(calculated_hands, key=lambda x: (x['han'], x['fu']), reverse=True) calculated_hand = calculated_hands[0] cost = calculated_hand['cost'] error = calculated_hand['error'] hand_yaku = calculated_hand['hand_yaku'] han = calculated_hand['han'] fu = calculated_hand['fu'] fu_details = calculated_hand['fu_details'] return HandResponse(cost, han, fu, hand_yaku, error, fu_details)
class FeatureGenerator: def __init__(self): """ changed the input from filename to tiles_state_and_action data By Jun Lin """ self.shanten_calculator = Shanten() self.hc = HandCalculator() self.sc = ScoresCalculator() self.hand_cache_shanten = {} self.hand_cache_points = {} def build_cache_key(self, tiles_34): return hashlib.md5(marshal.dumps(tiles_34)).hexdigest() def calculate_shanten_or_get_from_cache(self, closed_hand_34): key = self.build_cache_key(closed_hand_34) if key in self.hand_cache_shanten: return self.hand_cache_shanten[key] result = self.shanten_calculator.calculate_shanten(closed_hand_34) self.hand_cache_shanten[key] = result return result def calculate_ponits_or_get_from_cache(self, closed_left_tiles_34, win_tile, melds, dora_indicators): tiles_34 = closed_left_tiles_34[:] for meld in melds: for x in meld.tiles_34: tiles_34[x] += 1 key = self.build_cache_key(tiles_34 + [win_tile] + dora_indicators) if key in self.hand_cache_points: return self.hand_cache_points[key] # print(closed_left_tiles_34) # print(melds) hc_result = self.hc.estimate_hand_value( TilesConverter.to_136_array(tiles_34), win_tile, melds, dora_indicators) sc_result = self.sc.calculate_scores(hc_result.han, hc_result.fu, HandConfig(HandConstants())) result = sc_result["main"] self.hand_cache_points[key] = result return result def canwinbyreplace(self, closed_left_tiles_34, melds, dora_indicators, tiles_could_draw, replacelimit): def _draw(closed_left_tiles_34, melds, dora_indicators, tiles_could_draw, replacelimit): if self.calculate_shanten_or_get_from_cache( closed_left_tiles_34) > replacelimit: return 0 result = 0 if replacelimit == 0: for idx in range(34): if tiles_could_draw[idx] > 0: closed_left_tiles_34[idx] += 1 if self.calculate_shanten_or_get_from_cache( closed_left_tiles_34) == -1: ponits = self.calculate_ponits_or_get_from_cache( closed_left_tiles_34, idx * 4, melds, dora_indicators) result = max(result, ponits) closed_left_tiles_34[idx] -= 1 else: for idx, count in enumerate(tiles_could_draw): if count > 0: tiles_could_draw[idx] -= 1 closed_left_tiles_34[idx] += 1 ponits = _discard(closed_left_tiles_34, melds, dora_indicators, tiles_could_draw, replacelimit) result = max(result, ponits) closed_left_tiles_34[idx] -= 1 tiles_could_draw[idx] += 1 return result def _discard(closed_left_tiles_34, melds, dora_indicators, tiles_could_draw, replacelimit): result = 0 for idx, count in enumerate(closed_left_tiles_34): if count > 0: closed_left_tiles_34[idx] -= 1 replacelimit -= 1 ponits = _draw(closed_left_tiles_34, melds, dora_indicators, tiles_could_draw, replacelimit) result = max(result, ponits) replacelimit += 1 closed_left_tiles_34[idx] += 1 return result return _draw(closed_left_tiles_34, melds, dora_indicators, tiles_could_draw, replacelimit) def open_hands_detail_to_melds(self, open_hands_detail): melds = [] for ohd in open_hands_detail: tiles = ohd["tiles"] if ohd["meld_type"] == "Pon": meld_type = "pon" opened = True elif ohd["meld_type"] == "Chi": meld_type = "chi" opened = True elif ohd["meld_type"] == "AnKan": meld_type = "kan" opened = False else: meld_type = "kan" opened = True meld = Meld(meld_type, tiles, opened) melds.append(meld) return melds def getPlayerTiles(self, player_tiles): closed_hand_136 = player_tiles.get('closed_hand:', []) open_hand_136 = player_tiles.get('open_hand', []) discarded_tiles_136 = player_tiles.get('discarded_tiles', []) closed_hand_feature = np.zeros((4, 34)) open_hand_feature = np.zeros((4, 34)) discarded_tiles_feature = np.zeros((4, 34)) for val in closed_hand_136: idx = 0 while (closed_hand_feature[idx][val // 4] == 1): idx += 1 closed_hand_feature[idx][val // 4] = 1 for val in open_hand_136: idx = 0 while (open_hand_feature[idx][val // 4] == 1): idx += 1 open_hand_feature[idx][val // 4] = 1 for val in discarded_tiles_136: idx = 0 while (discarded_tiles_feature[idx][val // 4] == 1): idx += 1 discarded_tiles_feature[idx][val // 4] = 1 return np.concatenate( (closed_hand_feature, open_hand_feature, discarded_tiles_feature)) def getSelfTiles(self, tiles_state_and_action): player_tiles = tiles_state_and_action["player_tiles"] return self.getPlayerTiles(player_tiles) def getEnemiesTiles(self, tiles_state_and_action): player_seat = tiles_state_and_action["player_id"] enemies_tiles_list = tiles_state_and_action["enemies_tiles"] if (len(enemies_tiles_list) == 3): enemies_tiles_list.insert(player_seat, tiles_state_and_action["player_tiles"]) enemies_tiles_feature = np.empty((0, 34)) for i in range(3): player_seat = (player_seat + 1) % 4 player_tiles = self.getPlayerTiles(enemies_tiles_list[player_seat]) enemies_tiles_feature = np.concatenate( (enemies_tiles_feature, player_tiles)) return enemies_tiles_feature def getDoraIndicatorList(self, tiles_state_and_action): dora_indicator_list = tiles_state_and_action["dora"] dora_indicator_feature = np.zeros((5, 34)) for idx, val in enumerate(dora_indicator_list): dora_indicator_feature[idx][val // 4] = 1 return dora_indicator_feature def getDoraList(self, tiles_state_and_action): def indicator2dora(dora_indicator): dora = dora_indicator // 4 if dora < 27: # EAST if dora == 8: dora = -1 elif dora == 17: dora = 8 elif dora == 26: dora = 17 else: dora -= 9 * 3 if dora == 3: dora = -1 elif dora == 6: dora = 3 dora += 9 * 3 dora += 1 return dora dora_indicator_list = tiles_state_and_action["dora"] dora_feature = np.zeros((5, 34)) for idx, dora_indicator in enumerate(dora_indicator_list): dora_feature[idx][indicator2dora(dora_indicator)] = 1 return dora_feature def getScoreList1(self, tiles_state_and_action): def trans_score(score): feature = np.zeros((34)) if score > 50000: score = 50000 a = score // (50000.0 / 33) # alpha = a + 1 - (score*33.0/50000) # alpha = round(alpha, 2) # feature[int(a)] = alpha # if a<33: # feature[int(a+1)] = 1-alpha feature[int(a)] = 1 return feature player_seat = tiles_state_and_action["player_id"] scores_list = tiles_state_and_action["scores"] scores_feature = np.zeros((4, 34)) for i in range(4): score = scores_list[player_seat] scores_feature[i] = trans_score(score) player_seat += 1 player_seat %= 4 return scores_feature def getBoard1(self, tiles_state_and_action): def wind(c): if c == 'E': return 27 if c == 'S': return 28 if c == 'W': return 29 if c == 'N': return 30 player_seat = tiles_state_and_action["player_id"] dealer_seat = tiles_state_and_action["dealer"] repeat_dealer = tiles_state_and_action["repeat_dealer"] riichi_bets = tiles_state_and_action["riichi_bets"] player_wind = tiles_state_and_action["player_wind"] prevailing_wind = tiles_state_and_action["prevailing_wind"] dealer_feature = np.zeros((1, 34)) dealer_feature[0][(dealer_seat + 4 - player_seat) % 4] = 1 repeat_dealer_feature = np.zeros((1, 34)) repeat_dealer_feature[0][repeat_dealer] = 1 riichi_bets_feature = np.zeros((1, 34)) riichi_bets_feature[0][riichi_bets] = 1 player_wind_feature = np.zeros((1, 34)) player_wind_feature[0][wind(player_wind)] = 1 prevailing_wind_feature = np.zeros((1, 34)) prevailing_wind_feature[0][wind(prevailing_wind)] = 1 return np.concatenate( (dealer_feature, repeat_dealer_feature, riichi_bets_feature, player_wind_feature, prevailing_wind_feature)) def getLookAheadFeature(self, tiles_state_and_action): # 0 for whether can be discarded # 1 2 3 for shanten # 4 5 6 7 for whether can get 2k 4k 6k 8k points with replacing 3 tiles ---- need review: takes too long! # 8 9 10 for in Shimocha Toimen Kamicha discarded # lookAheadFeature = np.zeros((11, 34)) player_tiles = tiles_state_and_action["player_tiles"] closed_hand_136 = player_tiles.get('closed_hand:', []) open_hands_detail = tiles_state_and_action["open_hands_detail"] melds = self.open_hands_detail_to_melds(open_hands_detail) discarded_tiles_136 = player_tiles.get('discarded_tiles', []) player_seat = tiles_state_and_action["player_id"] enemies_tiles_list = tiles_state_and_action["enemies_tiles"] dora_indicators = tiles_state_and_action["dora"] if (len(enemies_tiles_list) == 3): enemies_tiles_list.insert(player_seat, player_tiles) tiles_could_draw = np.ones(34) * 4 for player in enemies_tiles_list: for tile_set in [ player.get('closed_hand:', []), player.get('open_hand', []), player.get('discarded_tiles', []) ]: for tile in tile_set: tiles_could_draw[tile // 4] -= 1 for dora_tile in dora_indicators: tiles_could_draw[tile // 4] -= 1 def feature_process(i, closed_hand_136, melds, dora_indicators, tiles_could_draw, player_seat, enemies_tiles_list): feature = np.zeros((11, 1)) discard_tiles = [ x for x in [i * 4, i * 4 + 1, i * 4 + 2, i * 4 + 3] if x in closed_hand_136 ] if len(discard_tiles) != 0: discard_tile = discard_tiles[0] feature[0] = 1 closed_left_tiles_34 = TilesConverter.to_34_array( [t for t in closed_hand_136 if t != discard_tile]) shanten = self.calculate_shanten_or_get_from_cache( closed_left_tiles_34) for i in range(3): if shanten <= i: feature[i + 1] = 1 maxscore = 0 #self.canwinbyreplace(closed_left_tiles_34, melds, dora_indicators, #tiles_could_draw, replacelimit = 2) scores = [2000, 4000, 6000, 8000] for i in range(4): if maxscore >= scores[i]: feature[i + 4] = 1 seat = player_seat for i in range(3): seat = (seat + 1) % 4 if discard_tile // 4 in [ t // 4 for t in enemies_tiles_list[seat].get( 'discarded_tiles', []) ]: feature[i + 8] = 1 return feature results = Parallel(n_jobs=8)(delayed( feature_process)(i, closed_hand_136, melds, dora_indicators, tiles_could_draw, player_seat, enemies_tiles_list) for i in range(34)) return np.concatenate(results, axis=1) def getGeneralFeature(self, tiles_state_and_action): return np.concatenate(( #self.getLookAheadFeature(tiles_state_and_action), #(11,34) self.getSelfTiles(tiles_state_and_action), # (12,34) self.getDoraList(tiles_state_and_action), # (5,34) self.getBoard1(tiles_state_and_action), # (5,34) self.getEnemiesTiles(tiles_state_and_action), # (36,34) self.getScoreList1(tiles_state_and_action) # (4,34) )) def ChiFeatureGenerator(self, tiles_state_and_action): """ changed the input from filename to tiles_state_and_action data By Jun Lin """ def _chilist(last_player_discarded_tile, closed_hand_136): res = [] last_player_discarded_tile_34 = last_player_discarded_tile // 4 tile_set = last_player_discarded_tile_34 // 9 if tile_set == 3: return res closed_hand_34_detail = [[] for i in range(34)] for tile in closed_hand_136: closed_hand_34_detail[tile // 4].append(tile) if last_player_discarded_tile_34 > tile_set * 9 + 1: if len(closed_hand_34_detail[last_player_discarded_tile_34 - 1] ) != 0 and len(closed_hand_34_detail[ last_player_discarded_tile_34 - 2]) != 0: res.append([ closed_hand_34_detail[last_player_discarded_tile_34 - 1][0], closed_hand_34_detail[last_player_discarded_tile_34 - 2][0], last_player_discarded_tile ]) if last_player_discarded_tile_34 > tile_set * 9 and last_player_discarded_tile_34 < tile_set * 9 + 8: if len(closed_hand_34_detail[last_player_discarded_tile_34 - 1] ) != 0 and len(closed_hand_34_detail[ last_player_discarded_tile_34 + 1]) != 0: res.append([ closed_hand_34_detail[last_player_discarded_tile_34 - 1][0], closed_hand_34_detail[last_player_discarded_tile_34 + 1][0], last_player_discarded_tile ]) if last_player_discarded_tile_34 < tile_set * 9 + 7: if len(closed_hand_34_detail[last_player_discarded_tile_34 + 1] ) != 0 and len(closed_hand_34_detail[ last_player_discarded_tile_34 + 2]) != 0: res.append([ closed_hand_34_detail[last_player_discarded_tile_34 + 1][0], closed_hand_34_detail[last_player_discarded_tile_34 + 2][0], last_player_discarded_tile ]) return res # res = [] # pairs = [(a, b) for idx, a in enumerate(closed_hand_136) for b in closed_hand_136[idx + 1:]] # for p in pairs: # meld = [last_player_discarded_tile,p[0],p[1]] # if all(tile < 36 for tile in meld) or all(36 <= tile < 72 for tile in meld) or all(36 <= tile < 72 for tile in meld): # meld34 = [tile//4 for tile in meld] # if any(tile+1 in meld34 and tile-1 in meld34 for tile in meld34): # res.append(meld) # return res could_chi = tiles_state_and_action["could_chi"] last_player_discarded_tile = tiles_state_and_action[ "last_player_discarded_tile"] closed_hand_136 = tiles_state_and_action["player_tiles"][ 'closed_hand:'] action = tiles_state_and_action["action"] if could_chi == 1: generalFeature = self.getGeneralFeature(tiles_state_and_action) if action[0] == 'Chi': print(_chilist(last_player_discarded_tile, closed_hand_136)) print(action) for chimeld in _chilist(last_player_discarded_tile, closed_hand_136): last_player_discarded_tile_feature = np.zeros((1, 34)) for chitile in chimeld: last_player_discarded_tile_feature[0][chitile // 4] = 1 x = np.concatenate( (last_player_discarded_tile_feature, generalFeature)) if action[0] == 'Chi' and all( chitile // 4 in [a // 4 for a in action[1]] for chitile in chimeld): y = 1 else: y = 0 # yield {'features': x.reshape((x.shape[0], x.shape[1], 1)), # "labels": to_categorical(y, num_classes=2)} yield x.reshape( (x.shape[0], x.shape[1], 1)), to_categorical(y, num_classes=2) def PonFeatureGenerator(self, tiles_state_and_action): """ changed the input from filename to tiles_state_and_action data By Jun Lin """ last_discarded_tile = tiles_state_and_action[ "last_player_discarded_tile"] closed_hand_136 = tiles_state_and_action["player_tiles"][ 'closed_hand:'] action = tiles_state_and_action["action"] if tiles_state_and_action["could_pon"] == 1: last_discarded_tile_feature = np.zeros((1, 34)) last_discarded_tile_feature[0][last_discarded_tile // 4] = 1 x = np.concatenate( (last_discarded_tile_feature, self.getGeneralFeature(tiles_state_and_action))) if action[0] == 'Pon': y = 1 else: y = 0 # yield {'features': x.reshape((x.shape[0], x.shape[1], 1)), # "labels": to_categorical(y, num_classes=2)} yield x.reshape( (x.shape[0], x.shape[1], 1)), to_categorical(y, num_classes=2) def KanFeatureGenerator(self, tiles_state_and_action): """ changed the input from filename to tiles_state_and_action data By Jun Lin """ def could_ankan(closed_hand_136): count = np.zeros(34) for tile in closed_hand_136: count[tile // 4] += 1 if count[tile // 4] == 4: return True return False def could_kakan(closed_hand_136, open_hand_136): count = np.zeros(34) for tile in open_hand_136: count[tile // 4] += 1 for tile in closed_hand_136: if count[tile // 4] == 3: return True return False last_discarded_tile = tiles_state_and_action[ "last_player_discarded_tile"] closed_hand_136 = tiles_state_and_action["player_tiles"][ 'closed_hand:'] open_hand_136 = tiles_state_and_action["player_tiles"]['open_hand'] action = tiles_state_and_action["action"] if tiles_state_and_action["could_minkan"] == 1: # Minkan kan_type_feature = np.zeros((3, 34)) kan_type_feature[0] = 1 last_discarded_tile_feature = np.zeros((1, 34)) last_discarded_tile_feature[0][last_discarded_tile // 4] = 1 x = np.concatenate( (kan_type_feature, last_discarded_tile_feature, self.getGeneralFeature(tiles_state_and_action))) if action[0] == 'MinKan' and (last_discarded_tile in action[1]): y = 1 else: y = 0 # yield {'features': x.reshape((x.shape[0], x.shape[1], 1)), # "labels": to_categorical(y, num_classes=2)} yield x.reshape( (x.shape[0], x.shape[1], 1)), to_categorical(y, num_classes=2) else: if could_ankan(closed_hand_136): # AnKan kan_type_feature = np.zeros((3, 34)) kan_type_feature[1] = 1 last_discarded_tile_feature = np.zeros((1, 34)) x = np.concatenate( (kan_type_feature, last_discarded_tile_feature, self.getGeneralFeature(tiles_state_and_action))) if action[0] == 'AnKan': y = 1 else: y = 0 # yield {'features': x.reshape((x.shape[0], x.shape[1], 1)), # "labels": to_categorical(y, num_classes=2)} yield x.reshape( (x.shape[0], x.shape[1], 1)), to_categorical(y, num_classes=2) else: if could_kakan(closed_hand_136, open_hand_136): # KaKan kan_type_feature = np.zeros((3, 34)) kan_type_feature[2] = 1 last_discarded_tile_feature = np.zeros((1, 34)) x = np.concatenate( (kan_type_feature, last_discarded_tile_feature, self.getGeneralFeature(tiles_state_and_action))) if action[0] == 'KaKan': y = 1 else: y = 0 # yield {'features': x.reshape((x.shape[0], x.shape[1], 1)), # "labels": to_categorical(y, num_classes=2)} yield x.reshape((x.shape[0], x.shape[1], 1)), to_categorical(y, num_classes=2) def RiichiFeatureGenerator(self, tiles_state_and_action): action = tiles_state_and_action["action"] if tiles_state_and_action["is_FCH"] == 1: tiles_34 = TilesConverter.to_34_array( tiles_state_and_action["player_tiles"]["closed_hand:"]) # min_shanten = self.shanten_calculator.calculate_shanten(tiles_34) min_shanten = self.calculate_shanten_or_get_from_cache(tiles_34) if min_shanten == 0: x = self.getGeneralFeature(tiles_state_and_action) if action[0] == 'REACH': y = 1 else: y = 0 # yield {'features': x.reshape((x.shape[0], x.shape[1], 1)), # "labels": to_categorical(y, num_classes=2)} yield x.reshape( (x.shape[0], x.shape[1], 1)), to_categorical(y, num_classes=2) def DiscardFeatureGenerator(self, tiles_state_and_action): x = self.getGeneralFeature(tiles_state_and_action) y = tiles_state_and_action["discarded_tile"] // 4 yield x.reshape( (x.shape[0], x.shape[1], 1)), to_categorical(y, num_classes=34)
def calculate_predictions(self, model, epoch): real_indices = [] predicted_indices = [] data_files = os.listdir(self.data_path) correct_predictions = 0 border_30_correct_predictions = 0 border_20_correct_predictions = 0 border_10_correct_predictions = 0 for f in data_files: if not f.startswith("test_"): continue test_file_path = os.path.join(self.data_path, f) test_data = hickle.load(test_file_path) test_input = np.asarray(test_data["input_data"]).astype("float32") test_verification = test_data["verification_data"] predictions = model.predict(test_input, verbose=1) logger.info("predictions shape = {}".format(predictions.shape)) for i, prediction in enumerate(predictions): original_cost, han, fu, is_dealer = test_verification[i] key = AgariRiichiCostProtocol.build_category_key(han, fu) real_index = AgariRiichiCostProtocol.HAND_COST_CATEGORIES[key] real_indices.append(real_index) predicted_index = np.argmax(prediction) predicted_key = sorted([ x[0] for x in AgariRiichiCostProtocol.HAND_COST_CATEGORIES.items() if x[1] == predicted_index ])[-1] if "-" in predicted_key: han = int(predicted_key.split("-")[0]) fu = int(predicted_key.split("-")[1]) else: han = int(predicted_key) fu = 0 predicted_indices.append(predicted_index) hand = ScoresCalculator() player_wind = is_dealer and EAST or SOUTH config = HandConfig(player_wind=player_wind) predicted_cost = hand.calculate_scores(han=han, fu=fu, config=config)["main"] if is_dealer and self.is_dealer_hand_correctly_predicted( original_cost, predicted_cost): correct_predictions += 1 if not is_dealer and self.is_regular_hand_correctly_predicted( original_cost, predicted_cost): correct_predictions += 1 if self.error_border_predicted(original_cost, predicted_cost, 30): border_30_correct_predictions += 1 if self.error_border_predicted(original_cost, predicted_cost, 20): border_20_correct_predictions += 1 if self.error_border_predicted(original_cost, predicted_cost, 10): border_10_correct_predictions += 1 assert len(real_indices) == len(predicted_indices) accuracy = accuracy_score(real_indices, predicted_indices) precision, recall, fscore, _ = precision_recall_fscore_support( real_indices, predicted_indices, average="macro") mean_squared_error_result = mean_squared_error(real_indices, predicted_indices) empirical_prediction = (correct_predictions / len(real_indices)) * 100 border_30_correct_predictions = (border_30_correct_predictions / len(real_indices)) * 100 border_20_correct_predictions = (border_20_correct_predictions / len(real_indices)) * 100 border_10_correct_predictions = (border_10_correct_predictions / len(real_indices)) * 100 logger.info("accuracy: {}".format(accuracy)) logger.info("precision: {}".format(precision)) logger.info("recall: {}".format(recall)) logger.info("fscore (more is better): {}".format(fscore)) logger.info("mean squared error: {}".format(mean_squared_error_result)) logger.info(f"30%: {border_30_correct_predictions}") logger.info(f"20%: {border_20_correct_predictions}") logger.info(f"10%: {border_10_correct_predictions}") logger.info(f"empirical: {empirical_prediction}") if epoch: self.after_epoch_attrs.append({ "epoch": epoch, "accuracy": accuracy, "precision": precision, "recall": recall, "fscore": fscore, "mean_squared_error": mean_squared_error_result, "empirical": empirical_prediction, "30": border_30_correct_predictions, "20": border_20_correct_predictions, "10": border_10_correct_predictions, })
def test_calculate_scores_and_tsumo(self): hand = ScoresCalculator() config = HandConfig( is_tsumo=True, options=OptionalRules(kazoe_limit=HandConfig.KAZOE_NO_LIMIT)) result = hand.calculate_scores(han=1, fu=30, config=config) self.assertEqual(result["main"], 500) self.assertEqual(result["additional"], 300) result = hand.calculate_scores(han=3, fu=30, config=config) self.assertEqual(result["main"], 2000) self.assertEqual(result["additional"], 1000) result = hand.calculate_scores(han=3, fu=60, config=config) self.assertEqual(result["main"], 3900) self.assertEqual(result["additional"], 2000) result = hand.calculate_scores(han=4, fu=30, config=config) self.assertEqual(result["main"], 3900) self.assertEqual(result["additional"], 2000) result = hand.calculate_scores(han=5, fu=0, config=config) self.assertEqual(result["main"], 4000) self.assertEqual(result["additional"], 2000) result = hand.calculate_scores(han=6, fu=0, config=config) self.assertEqual(result["main"], 6000) self.assertEqual(result["additional"], 3000) result = hand.calculate_scores(han=8, fu=0, config=config) self.assertEqual(result["main"], 8000) self.assertEqual(result["additional"], 4000) result = hand.calculate_scores(han=11, fu=0, config=config) self.assertEqual(result["main"], 12000) self.assertEqual(result["additional"], 6000) result = hand.calculate_scores(han=13, fu=0, config=config) self.assertEqual(result["main"], 16000) self.assertEqual(result["additional"], 8000) result = hand.calculate_scores(han=26, fu=0, config=config) self.assertEqual(result["main"], 32000) self.assertEqual(result["additional"], 16000) result = hand.calculate_scores(han=39, fu=0, config=config) self.assertEqual(result["main"], 48000) self.assertEqual(result["additional"], 24000) result = hand.calculate_scores(han=52, fu=0, config=config) self.assertEqual(result["main"], 64000) self.assertEqual(result["additional"], 32000) result = hand.calculate_scores(han=65, fu=0, config=config) self.assertEqual(result["main"], 80000) self.assertEqual(result["additional"], 40000) result = hand.calculate_scores(han=78, fu=0, config=config) self.assertEqual(result["main"], 96000) self.assertEqual(result["additional"], 48000)