예제 #1
0
 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 = {}
예제 #2
0
    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)
예제 #3
0
    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)
예제 #4
0
    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)
예제 #5
0
    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)
예제 #6
0
    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)
예제 #7
0
    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)
예제 #8
0
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)
예제 #9
0
    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,
            })
예제 #10
0
    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)