def __init__(self, chosen_class=PlayerClass.THE_SILENT):
     self.game = Game()
     self.errors = 0
     self.choose_good_card = False
     self.skipped_cards = False
     self.visited_shop = False
     self.map_route = []
     self.chosen_class = chosen_class
     self.priorities = Priority()
     self.change_class(chosen_class)
Beispiel #2
0
    def __init__(self, logfile, chosen_class=PlayerClass.IRONCLAD):
        self.chosen_class = chosen_class
        self.change_class(chosen_class)
        self.action_delay = 2.0  # seconds delay per action, useful for actually seeing what's going on.
        # high delay will steal mouse focus??
        self.ascension = 0
        self.debug_queue = [
            "AI Initialized.", "Delay timer set to " + str(self.action_delay)
        ]
        self.cmd_queue = []
        self.logfile = logfile
        self.skipping_card = False
        self.paused = False
        self.step = False
        self.debug_level = 6
        self.root = SelectorBehaviour("Root Context Selector")
        self.init_behaviour_tree(self.root)  # Warning: uses British spelling
        self.behaviour_tree = py_trees.trees.BehaviourTree(self.root)
        self.blackboard = py_trees.blackboard.Blackboard()
        self.blackboard.game = Game()
        # call behaviour_tree.tick() for one tick
        # can use behaviour.tick_once() to tick a specific behaviour

        # SIMPLE TRAITS
        self.errors = 0
        self.choose_good_card = False
        self.map_route = []
        self.upcoming_rooms = []
        self.priorities = Priority()
Beispiel #3
0
 def unpause_agent(self):
     print("Communicator: game update " + str(time.time()),
           file=self.logfile,
           flush=True)
     print("Communicator's game state:", file=self.logfile, flush=True)
     print(str(self.last_game_state), file=self.logfile, flush=True)
     self.last_game_state = Game.from_json(
         communication_state.get("game_state"),
         communication_state.get("available_commands"))
     self.receive_game_state_update()
Beispiel #4
0
    def receive_game_state_update(self,
                                  block=False,
                                  perform_callbacks=True,
                                  repeat=False):
        """Using the next message from Communication Mod, update the stored game state

		:param block: set to True to wait for the next message
		:type block: bool
		:param perform_callbacks: set to True to perform callbacks based on the new game state
		:type perform_callbacks: bool
		:return: whether a message was received
		"""

        message = ""
        if repeat:
            message = self.last_msg
        else:
            message = self.get_next_raw_message(block)

        if message is not None:
            communication_state = json.loads(message)
            self.last_error = communication_state.get("error", None)
            self.game_is_ready = communication_state.get("ready_for_command")
            if self.last_error is None:
                self.in_game = communication_state.get("in_game")
                if self.in_game:
                    self.last_game_state = Game.from_json(
                        communication_state.get("game_state"),
                        communication_state.get("available_commands"))
            else:
                print("Communicator detected error",
                      file=self.logfile,
                      flush=True)
            if perform_callbacks:
                if self.last_error is not None:
                    self.action_queue.clear()
                    new_action = self.error_callback(self.last_error)
                    self.add_action_to_queue(new_action)
                elif self.in_game:
                    if len(self.action_queue) == 0 and perform_callbacks:
                        #print(str(self.last_game_state), file=self.logfile, flush=True)
                        new_action = self.state_change_callback(
                            self.last_game_state)
                        self.add_action_to_queue(new_action)
                elif self.stop_after_run:
                    self.clear_actions()
                else:
                    new_action = self.out_of_game_callback()
                    self.add_action_to_queue(new_action)
            return True
        return False
class SimpleAgent:
    def __init__(self, chosen_class=PlayerClass.THE_SILENT):
        self.game = Game()
        self.errors = 0
        self.choose_good_card = False
        self.skipped_cards = False
        self.visited_shop = False
        self.map_route = []
        self.chosen_class = chosen_class
        self.priorities = Priority()
        self.change_class(chosen_class)

    def max_leaf_decision(self, r):
        for children in LevelOrderGroupIter(r, maxlevel=2):
            for node in children:
                if node in r.children:
                    if node.name.grade == r.name.grade:
                        if not node.children:
                            return node.name.decision[0]
                        else:
                            return self.max_leaf_decision(node)

    def change_class(self, new_class):
        self.chosen_class = new_class
        if self.chosen_class == PlayerClass.THE_SILENT:
            self.priorities = SilentPriority()
        elif self.chosen_class == PlayerClass.IRONCLAD:
            self.priorities = IroncladPriority()
        elif self.chosen_class == PlayerClass.DEFECT:
            self.priorities = DefectPowerPriority()
        else:
            self.priorities = random.choice(list(PlayerClass))

    def handle_error(self, error):
        raise Exception(error)

    def get_next_action_in_game(self, game_state):
        self.game = game_state
        #time.sleep(0.07)
        if self.game.choice_available:
            return self.handle_screen()
        if self.game.proceed_available:
            return ProceedAction()
        if self.game.play_available:
            if self.game.room_type == "MonsterRoomBoss" and len(
                    self.game.get_real_potions()) > 0:
                potion_action = self.use_next_potion()
                if potion_action is not None:
                    return potion_action
            return self.get_play_card_action()

        if self.game.end_available:
            return EndTurnAction()
        if self.game.cancel_available:
            return CancelAction()

    def get_next_action_out_of_game(self):
        return StartGameAction(self.chosen_class)

    def is_monster_attacking(self):
        for monster in self.game.monsters:
            if monster.intent.is_attack() or monster.intent == Intent.NONE:
                return True
        return False

    def get_incoming_damage(self):
        incoming_damage = 0
        for monster in self.game.monsters:
            if not monster.is_gone and not monster.half_dead:
                if monster.move_adjusted_damage is not None:
                    incoming_damage += monster.move_adjusted_damage * monster.move_hits
                elif monster.intent == Intent.NONE:
                    incoming_damage += 5 * self.game.act
        return incoming_damage

    def get_low_hp_target(self):
        available_monsters = [
            monster for monster in self.game.monsters if monster.current_hp > 0
            and not monster.half_dead and not monster.is_gone
        ]
        best_monster = min(available_monsters, key=lambda x: x.current_hp)
        return best_monster

    def get_high_hp_target(self):
        available_monsters = [
            monster for monster in self.game.monsters if monster.current_hp > 0
            and not monster.half_dead and not monster.is_gone
        ]
        best_monster = max(available_monsters, key=lambda x: x.current_hp)
        return best_monster

    def many_monsters_alive(self):
        available_monsters = [
            monster for monster in self.game.monsters if monster.current_hp > 0
            and not monster.half_dead and not monster.is_gone
        ]
        return len(available_monsters) > 1

    def get_play_card_action(self):
        #test a card like strike, card 1 may not be strike so watch out
        #return [self.game.hand[1], self.game.monsters[0]]

        #test end turn
        #return "End_Turn"
        d = 0

        #make SimGame object containing current real gamestate
        n = getstate(copy.deepcopy(self.game))
        #root node containing current real gamestate
        root = Node(n, parent=None)
        build_tree(root)
        eval_tree(root)
        tree_search(root)
        original_stdout = sys.stdout
        with open('tree.txt', 'w', encoding='utf8') as f:
            sys.stdout = f
            for pre, fill, node in RenderTree(root):
                print("%s%s%s" % (pre, 'decision' + str(node.name.decision),
                                  node.name.grade))
                # for c in node.name.hand:
                #     print(pre, c.name + ' ' + c.uuid)
        sys.stdout = original_stdout

        d = self.max_leaf_decision(root)
        root = None
        #delete tree

        #if we can't make the tree for some reason
        if d == None:
            playable_cards = [
                card for card in self.game.hand if card.is_playable
            ]
            zero_cost_cards = [
                card for card in playable_cards if card.cost == 0
            ]
            zero_cost_attacks = [
                card for card in zero_cost_cards
                if card.type == spirecomm.spire.card.CardType.ATTACK
            ]
            zero_cost_non_attacks = [
                card for card in zero_cost_cards
                if card.type != spirecomm.spire.card.CardType.ATTACK
            ]
            nonzero_cost_cards = [
                card for card in playable_cards if card.cost != 0
            ]
            aoe_cards = [
                card for card in playable_cards
                if self.priorities.is_card_aoe(card)
            ]
            if self.game.player.block > self.get_incoming_damage() - (
                    self.game.act + 4):
                offensive_cards = [
                    card for card in nonzero_cost_cards
                    if not self.priorities.is_card_defensive(card)
                ]
                if len(offensive_cards) > 0:
                    nonzero_cost_cards = offensive_cards
                else:
                    nonzero_cost_cards = [
                        card for card in nonzero_cost_cards
                        if not card.exhausts
                    ]
            if len(playable_cards) == 0:
                return EndTurnAction()
            if len(zero_cost_non_attacks) > 0:
                card_to_play = self.priorities.get_best_card_to_play(
                    zero_cost_non_attacks)
            elif len(nonzero_cost_cards) > 0:
                card_to_play = self.priorities.get_best_card_to_play(
                    nonzero_cost_cards)
                if len(aoe_cards) > 0 and self.many_monsters_alive(
                ) and card_to_play.type == spirecomm.spire.card.CardType.ATTACK:
                    card_to_play = self.priorities.get_best_card_to_play(
                        aoe_cards)
            elif len(zero_cost_attacks) > 0:
                card_to_play = self.priorities.get_best_card_to_play(
                    zero_cost_attacks)
            else:
                # This shouldn't happen!
                return EndTurnAction()
            if card_to_play.has_target:
                available_monsters = [
                    monster for monster in self.game.monsters
                    if monster.current_hp > 0 and not monster.half_dead
                    and not monster.is_gone
                ]
                if len(available_monsters) == 0:
                    return EndTurnAction()
                if card_to_play.type == spirecomm.spire.card.CardType.ATTACK:
                    target = self.get_low_hp_target()
                else:
                    target = self.get_high_hp_target()
                return PlayCardAction(card=card_to_play, target_monster=target)
            else:
                return PlayCardAction(card=card_to_play)

        if isinstance(d, str):
            return EndTurnAction()

        with open('PCA.txt', 'w') as f:
            sys.stdout = f
            print(d)
            print(len(d))
            for m in range(len(self.game.monsters)):
                print('index' + str(m))
                print(self.game.monsters[m].name)
            print(" ")
        sys.stdout = original_stdout

        #simple play the card in d[0]
        if len(d) == 1:
            for c in self.game.hand:
                if c.uuid == d[0].uuid:
                    d[0] = c
            return PlayCardAction(d[0])

        #if card needs a target(s)
        #format d[0] card, d[1] target index
        if (len(d) == 2) and (isinstance(d[1], int)):
            for c in self.game.hand:
                if c.uuid == d[0].uuid:
                    d[0] = c
            return PlayCardAction(card=d[0],
                                  target_monster=self.game.monsters[d[1]])

        #else format is d[0] is the card to play
        #d[1] is the second Action to do
        #d[1][0] is the monster target index, can have 'No Monster Target'
        #d[1][1] is the card to be selected, can have 'No Card Target'
        else:
            for c in self.game.hand:
                if c.uuid == d[0].uuid:
                    d[0] = c
            #converting monster index to object
            if isinstance(d[1][0], int):
                d[1][0] = self.game.monsters[d[1][0]]
            #convert sim card object to real card
            if not (d[1][1] == 'No Card Target'):
                for c in self.game.hand + self.game.draw_pile + self.game.discard_pile + self.game.exhaust_pile:
                    if c.uuid == d[1][1].uuid:
                        d[1][1] = c
                return DoubleAction(d)

    def use_next_potion(self):
        for potion in self.game.get_real_potions():
            if potion.can_use:
                if potion.requires_target:
                    return PotionAction(
                        True,
                        potion=potion,
                        target_monster=self.get_low_hp_target())
                else:
                    return PotionAction(True, potion=potion)

    def handle_screen(self):
        if self.game.screen_type == ScreenType.EVENT:
            if self.game.screen.event_id in [
                    "Vampires", "Masked Bandits", "Knowing Skull", "Ghosts",
                    "Liars Game", "Golden Idol", "Drug Dealer", "The Library"
            ]:
                return ChooseAction(len(self.game.screen.options) - 1)
            else:
                return ChooseAction(0)
        elif self.game.screen_type == ScreenType.CHEST:
            return OpenChestAction()
        elif self.game.screen_type == ScreenType.SHOP_ROOM:
            if not self.visited_shop:
                self.visited_shop = True
                return ChooseShopkeeperAction()
            else:
                self.visited_shop = False
                return ProceedAction()
        elif self.game.screen_type == ScreenType.REST:
            return self.choose_rest_option()
        elif self.game.screen_type == ScreenType.CARD_REWARD:
            return self.choose_card_reward()
        elif self.game.screen_type == ScreenType.COMBAT_REWARD:
            for reward_item in self.game.screen.rewards:
                if reward_item.reward_type == RewardType.POTION and self.game.are_potions_full(
                ):
                    continue
                elif reward_item.reward_type == RewardType.CARD and self.skipped_cards:
                    continue
                else:
                    return CombatRewardAction(reward_item)
            self.skipped_cards = False
            return ProceedAction()
        elif self.game.screen_type == ScreenType.MAP:
            return self.make_map_choice()
        elif self.game.screen_type == ScreenType.BOSS_REWARD:
            relics = self.game.screen.relics
            best_boss_relic = self.priorities.get_best_boss_relic(relics)
            return BossRewardAction(best_boss_relic)
        elif self.game.screen_type == ScreenType.SHOP_SCREEN:
            if self.game.screen.purge_available and self.game.gold >= self.game.screen.purge_cost:
                return ChooseAction(name="purge")
            for card in self.game.screen.cards:
                if self.game.gold >= card.price and not self.priorities.should_skip(
                        card):
                    return BuyCardAction(card)
            for relic in self.game.screen.relics:
                if self.game.gold >= relic.price:
                    return BuyRelicAction(relic)
            return CancelAction()
        elif self.game.screen_type == ScreenType.GRID:
            if not self.game.choice_available:
                return ProceedAction()
            if self.game.screen.for_upgrade or self.choose_good_card:
                available_cards = self.priorities.get_sorted_cards(
                    self.game.screen.cards)
            else:
                available_cards = self.priorities.get_sorted_cards(
                    self.game.screen.cards, reverse=True)
            num_cards = self.game.screen.num_cards
            return CardSelectAction(available_cards[:num_cards])
        elif self.game.screen_type == ScreenType.HAND_SELECT:
            if not self.game.choice_available:
                return ProceedAction()
            # Usually, we don't want to choose the whole hand for a hand select. 3 seems like a good compromise.
            num_cards = min(self.game.screen.num_cards, 3)
            return CardSelectAction(
                self.priorities.get_cards_for_action(self.game.current_action,
                                                     self.game.screen.cards,
                                                     num_cards))
        else:
            return ProceedAction()

    def choose_rest_option(self):
        rest_options = self.game.screen.rest_options
        if len(rest_options) > 0 and not self.game.screen.has_rested:
            if RestOption.REST in rest_options and self.game.current_hp < self.game.max_hp / 2:
                return RestAction(RestOption.REST)
            elif RestOption.REST in rest_options and self.game.act != 1 and self.game.floor % 17 == 15 and self.game.current_hp < self.game.max_hp * 0.9:
                return RestAction(RestOption.REST)
            elif RestOption.SMITH in rest_options:
                return RestAction(RestOption.SMITH)
            elif RestOption.LIFT in rest_options:
                return RestAction(RestOption.LIFT)
            elif RestOption.DIG in rest_options:
                return RestAction(RestOption.DIG)
            elif RestOption.REST in rest_options and self.game.current_hp < self.game.max_hp:
                return RestAction(RestOption.REST)
            else:
                return ChooseAction(0)
        else:
            return ProceedAction()

    def count_copies_in_deck(self, card):
        count = 0
        for deck_card in self.game.deck:
            if deck_card.card_id == card.card_id:
                count += 1
        return count

    def choose_card_reward(self):
        reward_cards = self.game.screen.cards
        if self.game.screen.can_skip and not self.game.in_combat:
            pickable_cards = [
                card for card in reward_cards
                if self.priorities.needs_more_copies(
                    card, self.count_copies_in_deck(card))
            ]
        else:
            pickable_cards = reward_cards
        if len(pickable_cards) > 0:
            potential_pick = self.priorities.get_best_card(pickable_cards)
            return CardRewardAction(potential_pick)
        elif self.game.screen.can_bowl:
            return CardRewardAction(bowl=True)
        else:
            self.skipped_cards = True
            return CancelAction()

    def generate_map_route(self):
        node_rewards = self.priorities.MAP_NODE_PRIORITIES.get(self.game.act)
        best_rewards = {
            0: {
                node.x: node_rewards[node.symbol]
                for node in self.game.map.nodes[0].values()
            }
        }
        best_parents = {
            0: {node.x: 0
                for node in self.game.map.nodes[0].values()}
        }
        min_reward = min(node_rewards.values())
        map_height = max(self.game.map.nodes.keys())
        for y in range(0, map_height):
            best_rewards[y + 1] = {
                node.x: min_reward * 20
                for node in self.game.map.nodes[y + 1].values()
            }
            best_parents[y + 1] = {
                node.x: -1
                for node in self.game.map.nodes[y + 1].values()
            }
            for x in best_rewards[y]:
                node = self.game.map.get_node(x, y)
                best_node_reward = best_rewards[y][x]
                for child in node.children:
                    test_child_reward = best_node_reward + node_rewards[
                        child.symbol]
                    if test_child_reward > best_rewards[y + 1][child.x]:
                        best_rewards[y + 1][child.x] = test_child_reward
                        best_parents[y + 1][child.x] = node.x
        best_path = [0] * (map_height + 1)
        best_path[map_height] = max(best_rewards[map_height].keys(),
                                    key=lambda x: best_rewards[map_height][x])
        for y in range(map_height, 0, -1):
            best_path[y - 1] = best_parents[y][best_path[y]]
        self.map_route = best_path

    def make_map_choice(self):
        if len(self.game.screen.next_nodes
               ) > 0 and self.game.screen.next_nodes[0].y == 0:
            self.generate_map_route()
            self.game.screen.current_node.y = -1
        if self.game.screen.boss_available:
            return ChooseMapBossAction()
        chosen_x = self.map_route[self.game.screen.current_node.y + 1]
        for choice in self.game.screen.next_nodes:
            if choice.x == chosen_x:
                return ChooseMapNodeAction(choice)
        # This should never happen
        return ChooseAction(0)
Beispiel #6
0
class RandomAgent:

    def __init__(self, chosen_class=PlayerClass.THE_SILENT):
        self.game = Game()
        self.errors = 0
        self.choose_good_card = False
        self.skipped_cards = False
        self.visited_shop = False
        self.map_route = []
        self.chosen_class = chosen_class
        self.priorities = Priority()
        self.change_class(chosen_class)

    def change_class(self, new_class):
        self.chosen_class = new_class
        if self.chosen_class == PlayerClass.DEFECT:
            self.priorities = DefectPowerPriority()
        else:
            self.priorities = random.choice(list(PlayerClass))

    def handle_error(self, error):
        raise Exception(error)

    def get_next_action_in_game(self, game_state):
        self.game = game_state
        #time.sleep(0.07)
        if self.game.choice_available:
            return self.handle_screen()
        if self.game.proceed_available:
            return ProceedAction()
        if self.game.play_available:
            if self.game.room_type == "MonsterRoomBoss" and len(self.game.get_real_potions()) > 0:
                potion_action = self.use_next_potion()
                if potion_action is not None:
                    return potion_action
            return self.get_play_card_action()
        if self.game.end_available:
            return EndTurnAction()
        if self.game.cancel_available:
            return CancelAction()

    def get_next_action_out_of_game(self):
        return StartGameAction(self.chosen_class)

    def is_monster_attacking(self):
        for monster in self.game.monsters:
            if monster.intent.is_attsack() or monster.intent == Intent.NONE:
                return True
        return False

    def get_incoming_damage(self):
        incoming_damage = 0
        for monster in self.game.monsters:
            if not monster.is_gone and not monster.half_dead:
                if monster.move_adjusted_damage is not None:
                    incoming_damage += monster.move_adjusted_damage * monster.move_hits
                elif monster.intent == Intent.NONE:
                    incoming_damage += 5 * self.game.act
        return incoming_damage

    def get_low_hp_target(self):
        available_monsters = [monster for monster in self.game.monsters if monster.current_hp > 0 and not monster.half_dead and not monster.is_gone]
        best_monster = min(available_monsters, key=lambda x: x.current_hp)
        return best_monster

    def get_high_hp_target(self):
        available_monsters = [monster for monster in self.game.monsters if monster.current_hp > 0 and not monster.half_dead and not monster.is_gone]
        best_monster = max(available_monsters, key=lambda x: x.current_hp)
        return best_monster

    def many_monsters_alive(self):
        available_monsters = [monster for monster in self.game.monsters if monster.current_hp > 0 and not monster.half_dead and not monster.is_gone]
        return len(available_monsters) > 1

    def get_play_card_action(self):
        playable_cards = [card for card in self.game.hand if card.is_playable]
        card_to_play = random.choice(playable_cards)
        if card_to_play.has_target:
            available_monsters = [monster for monster in self.game.monsters if monster.current_hp > 0 and not monster.is_gone]
            if len(available_monsters) == 0:
                return EndTurnAction()
            else:
                target = random.choice(available_monsters)
            return PlayCardAction(card=card_to_play, target_monster=target)
        else:
            return PlayCardAction(card=card_to_play)

    def use_next_potion(self):
        for potion in self.game.get_real_potions():
            if potion.can_use:
                if potion.requires_target:
                    return PotionAction(True, potion=potion, target_monster=self.get_low_hp_target())
                else:
                    return PotionAction(True, potion=potion)

    def handle_screen(self):
        if self.game.screen_type == ScreenType.EVENT:
            return ChooseAction(random.randint(0, len(self.game.screen.options)-1))

        elif self.game.screen_type == ScreenType.CHEST:
            return OpenChestAction()

        elif self.game.screen_type == ScreenType.SHOP_ROOM:
            if not self.visited_shop:
                self.visited_shop = True
                return ChooseShopkeeperAction()
            else:
                self.visited_shop = False
                return ProceedAction()

        elif self.game.screen_type == ScreenType.REST:
            return self.choose_rest_option()

        elif self.game.screen_type == ScreenType.CARD_REWARD:
            return self.choose_card_reward()

        elif self.game.screen_type == ScreenType.COMBAT_REWARD:
            for reward_item in self.game.screen.rewards:
                if reward_item.reward_type == RewardType.POTION and self.game.are_potions_full():
                    continue
                elif reward_item.reward_type == RewardType.CARD and self.skipped_cards:
                    continue
                else:
                    return CombatRewardAction(reward_item)
            self.skipped_cards = False
            return ProceedAction()

        elif self.game.screen_type == ScreenType.MAP:
            return self.make_map_choice()

        elif self.game.screen_type == ScreenType.BOSS_REWARD:
            relics = self.game.screen.relics
            best_boss_relic = self.priorities.get_best_boss_relic(relics)
            return BossRewardAction(best_boss_relic)

        elif self.game.screen_type == ScreenType.SHOP_SCREEN:
            if self.game.screen.purge_available and self.game.gold >= self.game.screen.purge_cost:
                return ChooseAction(name="purge")
            for card in self.game.screen.cards:
                if self.game.gold >= card.price and not self.priorities.should_skip(card):
                    return BuyCardAction(card)
            for relic in self.game.screen.relics:
                if self.game.gold >= relic.price:
                    return BuyRelicAction(relic)
            return CancelAction()

        elif self.game.screen_type == ScreenType.GRID:
            if not self.game.choice_available:
                return ProceedAction()
            if self.game.screen.for_upgrade or self.choose_good_card:
                available_cards = self.priorities.get_sorted_cards(self.game.screen.cards)
            else:
                available_cards = self.priorities.get_sorted_cards(self.game.screen.cards, reverse=True)
            num_cards = self.game.screen.num_cards
            return CardSelectAction(available_cards[:num_cards])

        elif self.game.screen_type == ScreenType.HAND_SELECT:
            if not self.game.choice_available:
                return ProceedAction()
            # Usually, we don't want to choose the whole hand for a hand select. 3 seems like a good compromise.
            num_cards = min(self.game.screen.num_cards, 3)
            return CardSelectAction(self.priorities.get_cards_for_action(self.game.current_action, self.game.screen.cards, num_cards))
        else:
            return ProceedAction()

    def choose_rest_option(self):
        rest_options = self.game.screen.rest_options
        if len(rest_options) != 0:
            return RestAction(random.choice(rest_options))
        return ProceedAction()

    def count_copies_in_deck(self, card):
        count = 0
        for deck_card in self.game.deck:
            if deck_card.card_id == card.card_id:
                count += 1
        return count

    def choose_card_reward(self):
        reward_cards = self.game.screen.cards
        option = random.randint(0, reward_cards)

        if option < len(reward_cards):
            return CardRewardAction(reward_cards[option])
        else:
            self.skipped_cards = True
            return CancelAction()

    def generate_map_route(self):
        node_rewards = self.priorities.MAP_NODE_PRIORITIES.get(self.game.act)
        best_rewards = {0: {node.x: node_rewards[node.symbol] for node in self.game.map.nodes[0].values()}}
        best_parents = {0: {node.x: 0 for node in self.game.map.nodes[0].values()}}
        min_reward = min(node_rewards.values())
        map_height = max(self.game.map.nodes.keys())
        for y in range(0, map_height):
            best_rewards[y+1] = {node.x: min_reward * 20 for node in self.game.map.nodes[y+1].values()}
            best_parents[y+1] = {node.x: -1 for node in self.game.map.nodes[y+1].values()}
            for x in best_rewards[y]:
                node = self.game.map.get_node(x, y)
                best_node_reward = best_rewards[y][x]
                for child in node.children:
                    test_child_reward = best_node_reward + node_rewards[child.symbol]
                    if test_child_reward > best_rewards[y+1][child.x]:
                        best_rewards[y+1][child.x] = test_child_reward
                        best_parents[y+1][child.x] = node.x
        best_path = [0] * (map_height + 1)
        best_path[map_height] = max(best_rewards[map_height].keys(), key=lambda x: best_rewards[map_height][x])
        for y in range(map_height, 0, -1):
            best_path[y - 1] = best_parents[y][best_path[y]]
        self.map_route = best_path

    def make_map_choice(self):
        if len(self.game.screen.next_nodes) > 0 and self.game.screen.next_nodes[0].y == 0:
            self.generate_map_route()
            self.game.screen.current_node.y = -1
        if self.game.screen.boss_available:
            return ChooseMapBossAction()
        chosen_x = self.map_route[self.game.screen.current_node.y + 1]
        for choice in self.game.screen.next_nodes:
            if choice.x == chosen_x:
                return ChooseMapNodeAction(choice)
        # This should never happen
        return ChooseAction(0)