summaryrefslogtreecommitdiff
path: root/pokemontools/vba/battle.py
diff options
context:
space:
mode:
Diffstat (limited to 'pokemontools/vba/battle.py')
-rw-r--r--pokemontools/vba/battle.py188
1 files changed, 188 insertions, 0 deletions
diff --git a/pokemontools/vba/battle.py b/pokemontools/vba/battle.py
new file mode 100644
index 0000000..3bda1a5
--- /dev/null
+++ b/pokemontools/vba/battle.py
@@ -0,0 +1,188 @@
+"""
+Code that attempts to model a battle.
+"""
+
+from pokemontools.vba.vba import crystal as emulator
+import pokemontools.vba.vba as vba
+
+class BattleException(Exception):
+ """
+ Something went terribly wrong in a battle.
+ """
+
+class EmulatorController(object):
+ """
+ Controls the emulator. I don't have a good reason for this.
+ """
+
+class Battle(EmulatorController):
+ """
+ Wrapper around the battle routine inside of the game. This object controls
+ the emulator and provides a sanitized interface for interacting with a
+ battle through python.
+ """
+
+ def __init__(self, emulator=None):
+ """
+ Setup the battle.
+ """
+ self.emulator = emulator
+
+ def is_in_battle(self):
+ """
+ @rtype: bool
+ """
+ return self.emulator.is_in_battle()
+
+ def is_input_required(self):
+ """
+ Detects if the battle is waiting for player input.
+ """
+ return self.is_player_turn() or self.is_mandatory_switch()
+
+ def is_fight_pack_run_menu(self):
+ """
+ Attempts to detect if the current menu is fight-pack-run. This is only
+ for whether or not the player needs to choose what to do next.
+ """
+ signs = ["FIGHT", "PACK", "RUN"]
+ screentext = self.emulator.get_text()
+ return all([sign in screentext for sign in signs])
+
+ def is_player_turn(self):
+ """
+ Detects if the battle is waiting for the player to choose an attack.
+ """
+ return self.is_fight_pack_run_menu()
+
+ def is_mandatory_switch(self):
+ """
+ Detects if the battle is waiting for the player to choose a next
+ pokemon.
+ """
+ # TODO: test when "no" fails to escape for wild battles.
+ # trainer battles: menu asks to select the next mon
+ # wild battles: yes/no box first
+ # The following conditions are probably sufficient:
+ # 1) current pokemon hp is 0
+ # 2) game is polling for input
+
+ # TODO: detect the mandatory switch menu
+ return False
+
+ def skip_start_text(self, max_loops=20):
+ """
+ Skip any initial conversation until the player can select an action.
+ This includes skipping any text that appears on a map from an NPC as
+ well as text that appears prior to the first time the action selection
+ menu appears.
+ """
+ if not self.is_in_battle():
+ while not self.is_in_battle() and max_loops > 0:
+ self.emulator.text_wait()
+ max_loops -= 1
+
+ if max_loops <= 0:
+ raise Exception("Couldn't start the battle.")
+ else:
+ self.emulator.text_wait()
+
+ def skip_end_text(self, loops=20):
+ """
+ Skip through any text that appears after the final attack.
+ """
+ if not self.is_in_battle():
+ # TODO: keep talking until the character can move? A battle can be
+ # triggered inside of a script, and after the battle is ver the
+ # player may not be able to move until the script is done. The
+ # script might only finish after other player input is given, so
+ # using "text_wait() until the player can move" is a bad idea here.
+ self.emulator.text_wait()
+ else:
+ while self.is_in_battle() and loops > 0:
+ self.emulator.text_wait()
+ loops -= 1
+
+ if loops <= 0:
+ raise Exception("Couldn't get out of the battle.")
+
+ def skip_until_input_required(self):
+ """
+ Waits until the battle needs player input.
+ """
+ while not self.is_input_required():
+ self.emulator.text_wait()
+
+ def run(self):
+ """
+ Step through the entire battle.
+ """
+ # Advance to the battle from either of these states:
+ # 1) the player is talking with an npc
+ # 2) the battle has already started but there's initial text
+ # xyz wants to battle, a wild foobar appeared
+ self.skip_start_text()
+
+ while self.is_in_battle():
+ self.skip_until_input_required()
+
+ if self.is_player_turn():
+ # battle hook provides input to handle this situation
+ self.handle_turn()
+ elif self.is_mandatory_switch():
+ # battle hook provides input to handle this situation too
+ self.handle_mandatory_switch()
+ else:
+ raise BattleException("unknown state, aborting")
+
+ # "how did i lose? wah"
+ self.skip_end_text()
+
+ # TODO: return should indicate win/loss (blackout)
+
+ def handle_mandatory_switch(self):
+ """
+ Something fainted, pick the next mon.
+ """
+ raise NotImplementedError
+
+ def handle_turn(self):
+ """
+ Take actions inside of a battle based on the game state.
+ """
+ raise NotImplementedError
+
+class BattleStrategy(Battle):
+ """
+ Throw a pokeball until everyone dies.
+ """
+
+ def handle_mandatory_switch(self):
+ """
+ Something fainted, pick the next mon.
+ """
+ for pokemon in self.emulator.party:
+ if pokemon.hp > 0:
+ break
+ else:
+ # the game failed to do a blackout.. not sure what to do now.
+ raise BattleException("No partymons left. wtf?")
+
+ return pokemon.id
+
+ def handle_turn(self):
+ """
+ Take actions inside of a battle based on the game state.
+ """
+ self.throw_pokeball()
+
+class SimpleBattleStrategy(BattleStrategy):
+ """
+ Attack the enemy with the first move.
+ """
+
+ def handle_turn(self):
+ """
+ Always attack the enemy with the first move.
+ """
+ self.attack(self.battle.party[0].moves[0].name)