From 0283856044c98792fd62db6672f01f81876d5e26 Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Sat, 2 Nov 2013 00:05:57 -0500 Subject: a basic battle handling framework --- pokemontools/vba/battle.py | 105 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 pokemontools/vba/battle.py (limited to 'pokemontools/vba/battle.py') diff --git a/pokemontools/vba/battle.py b/pokemontools/vba/battle.py new file mode 100644 index 0000000..3d4bb55 --- /dev/null +++ b/pokemontools/vba/battle.py @@ -0,0 +1,105 @@ +""" +Code that attempts to model a battle. +""" + +from pokemontools.vba.vba import crystal as emulator + +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. + """ + + # Maybe this should be an instance variable instead, but since there can + # only be one emulator per instance (??) it doesn't really matter right + # now. + emulator = emulator + + def __init__(self, hook): + """ + Setup the battle. + + @param hook: object that implements handle_turn and handle_mandatory_switch + @type hook: BattleHook + """ + self.hook = hook + + @classmethod + def is_in_battle(cls): + """ + @rtype: bool + """ + return cls.emulator.is_in_battle() + + def run(self): + """ + Step through the entire battle. + """ + # xyz wants to battle + self.skip_start_text() + + while self.is_in_battle(): + self.skip_crap() + + if self.is_player_turn(): + self.hook.handle_turn() + elif self.is_mandatory_switch(): + self.hook.handle_mandatory_switch() + else: + raise BattleException("unknown state, aborting") + + # "how did i lose? wah" + self.skip_end_text() + +class BattleHook(object): + """ + Hooks that are called during a battle. + """ + + def __init__(self, battle): + """ + Makes references to some common objects. + """ + self.battle = battle + self.emulator = battle.emulator + + 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.battle.throw_pokeball() + +class SimpleBattleHook(BattleHook): + """ + Attack the enemy with the first move. + """ + + def handle_turn(self): + """ + Always attack the enemy with the first move. + """ + self.battle.attack(self.battle.party[0].moves[0].name) -- cgit v1.2.3 From 9eb78d9c5a4e3c63960c7fb990df2c5bf859f591 Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Sat, 2 Nov 2013 00:14:37 -0500 Subject: simplify the number of battle-related classes --- pokemontools/vba/battle.py | 32 +++++++++++++++++--------------- 1 file changed, 17 insertions(+), 15 deletions(-) (limited to 'pokemontools/vba/battle.py') diff --git a/pokemontools/vba/battle.py b/pokemontools/vba/battle.py index 3d4bb55..9546378 100644 --- a/pokemontools/vba/battle.py +++ b/pokemontools/vba/battle.py @@ -26,14 +26,11 @@ class Battle(EmulatorController): # now. emulator = emulator - def __init__(self, hook): + def __init__(self, emulator=emulator): """ Setup the battle. - - @param hook: object that implements handle_turn and handle_mandatory_switch - @type hook: BattleHook """ - self.hook = hook + self.emulator = emulator @classmethod def is_in_battle(cls): @@ -62,17 +59,22 @@ class Battle(EmulatorController): # "how did i lose? wah" self.skip_end_text() -class BattleHook(object): - """ - Hooks that are called during a battle. - """ + def handle_mandatory_switch(self): + """ + Something fainted, pick the next mon. + """ + raise NotImplementedError - def __init__(self, battle): + def handle_turn(self): """ - Makes references to some common objects. + Take actions inside of a battle based on the game state. """ - self.battle = battle - self.emulator = battle.emulator + raise NotImplementedError + +class BattleStrategy(Battle): + """ + Throw a pokeball until everyone dies. + """ def handle_mandatory_switch(self): """ @@ -93,7 +95,7 @@ class BattleHook(object): """ self.battle.throw_pokeball() -class SimpleBattleHook(BattleHook): +class SimpleBattleStrategy(BattleStrategy): """ Attack the enemy with the first move. """ @@ -102,4 +104,4 @@ class SimpleBattleHook(BattleHook): """ Always attack the enemy with the first move. """ - self.battle.attack(self.battle.party[0].moves[0].name) + self.attack(self.battle.party[0].moves[0].name) -- cgit v1.2.3 From 016d0b886444f864cf494c8d91f9de883ec9a57d Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Sat, 2 Nov 2013 13:54:35 -0500 Subject: fix some hook calls in Battle.run --- pokemontools/vba/battle.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'pokemontools/vba/battle.py') diff --git a/pokemontools/vba/battle.py b/pokemontools/vba/battle.py index 9546378..8897ccb 100644 --- a/pokemontools/vba/battle.py +++ b/pokemontools/vba/battle.py @@ -50,9 +50,9 @@ class Battle(EmulatorController): self.skip_crap() if self.is_player_turn(): - self.hook.handle_turn() + self.handle_turn() elif self.is_mandatory_switch(): - self.hook.handle_mandatory_switch() + self.handle_mandatory_switch() else: raise BattleException("unknown state, aborting") -- cgit v1.2.3 From 2b7e903f847e1c6de7e63549f915a89b0a5795e3 Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Sun, 3 Nov 2013 01:53:04 -0500 Subject: implement more parts of the battler --- pokemontools/vba/battle.py | 58 +++++++++++++++++++++++++++++++++++++++------- 1 file changed, 49 insertions(+), 9 deletions(-) (limited to 'pokemontools/vba/battle.py') diff --git a/pokemontools/vba/battle.py b/pokemontools/vba/battle.py index 8897ccb..08d0b93 100644 --- a/pokemontools/vba/battle.py +++ b/pokemontools/vba/battle.py @@ -3,6 +3,7 @@ Code that attempts to model a battle. """ from pokemontools.vba.vba import crystal as emulator +import pokemontools.vba.vba as vba class BattleException(Exception): """ @@ -21,23 +22,62 @@ class Battle(EmulatorController): battle through python. """ - # Maybe this should be an instance variable instead, but since there can - # only be one emulator per instance (??) it doesn't really matter right - # now. - emulator = emulator - - def __init__(self, emulator=emulator): + def __init__(self, emulator=None): """ Setup the battle. """ self.emulator = emulator - @classmethod - def is_in_battle(cls): + def is_in_battle(self): """ @rtype: bool """ - return cls.emulator.is_in_battle() + return self.emulator.is_in_battle() + + 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): + return is_fight_pack_run_menu() + + def is_mandatory_switch(self): + return False # TODO + + def skip_start_text(self, max_loops=20): + """ + Skip any initial conversation until the battle has begun. + """ + 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): + if not self.is_in_battle(): + # TODO: keep talking until the character can move? + 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_crap(self): + while not self.is_flight_pack_run_menu(): + self.emulator.text_wait() def run(self): """ -- cgit v1.2.3 From 1ac0e437d19963d46bd5360e0790b2294ade72b8 Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Sat, 9 Nov 2013 14:44:36 -0600 Subject: add a bunch of docstrings in the battler --- pokemontools/vba/battle.py | 49 ++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 41 insertions(+), 8 deletions(-) (limited to 'pokemontools/vba/battle.py') diff --git a/pokemontools/vba/battle.py b/pokemontools/vba/battle.py index 08d0b93..1949fbb 100644 --- a/pokemontools/vba/battle.py +++ b/pokemontools/vba/battle.py @@ -34,6 +34,12 @@ class Battle(EmulatorController): """ 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 @@ -44,14 +50,28 @@ class Battle(EmulatorController): return all([sign in screentext for sign in signs]) def is_player_turn(self): - return is_fight_pack_run_menu() + """ + 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): - return False # TODO + """ + Detects if the battle is waiting for the player to choose a next + pokemon. + """ + # TODO: detect the mandatry switch menu + # The following conditions are probably sufficient: + # 1) current pokemon hp is 0 + # 2) game is polling for input + return False def skip_start_text(self, max_loops=20): """ - Skip any initial conversation until the battle has begun. + 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: @@ -64,6 +84,9 @@ class Battle(EmulatorController): 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? self.emulator.text_wait() @@ -75,23 +98,31 @@ class Battle(EmulatorController): if loops <= 0: raise Exception("Couldn't get out of the battle.") - def skip_crap(self): - while not self.is_flight_pack_run_menu(): + 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. """ - # xyz wants to 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_crap() + 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") @@ -99,6 +130,8 @@ class Battle(EmulatorController): # "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. @@ -133,7 +166,7 @@ class BattleStrategy(Battle): """ Take actions inside of a battle based on the game state. """ - self.battle.throw_pokeball() + self.throw_pokeball() class SimpleBattleStrategy(BattleStrategy): """ -- cgit v1.2.3 From 6a1699d32e6fb88401bd3079a5c1ddcf4e8711f5 Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Sat, 9 Nov 2013 14:50:55 -0600 Subject: even more comments in the battle routines --- pokemontools/vba/battle.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) (limited to 'pokemontools/vba/battle.py') diff --git a/pokemontools/vba/battle.py b/pokemontools/vba/battle.py index 1949fbb..3bda1a5 100644 --- a/pokemontools/vba/battle.py +++ b/pokemontools/vba/battle.py @@ -60,10 +60,14 @@ class Battle(EmulatorController): Detects if the battle is waiting for the player to choose a next pokemon. """ - # TODO: detect the mandatry switch menu + # 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): @@ -88,7 +92,11 @@ class Battle(EmulatorController): Skip through any text that appears after the final attack. """ if not self.is_in_battle(): - # TODO: keep talking until the character can move? + # 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: -- cgit v1.2.3 From b363e6cca00c05687c34d735abb93d5117f7b209 Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Sat, 9 Nov 2013 15:18:55 -0600 Subject: fix a syntax error in battle.py --- pokemontools/vba/battle.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'pokemontools/vba/battle.py') diff --git a/pokemontools/vba/battle.py b/pokemontools/vba/battle.py index 3bda1a5..5aeb923 100644 --- a/pokemontools/vba/battle.py +++ b/pokemontools/vba/battle.py @@ -168,7 +168,7 @@ class BattleStrategy(Battle): # the game failed to do a blackout.. not sure what to do now. raise BattleException("No partymons left. wtf?") - return pokemon.id + return pokemon.id def handle_turn(self): """ -- cgit v1.2.3 From 982cff40af88af1b0c04d1e135ef8ec84007e41a Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Sat, 9 Nov 2013 17:55:30 -0600 Subject: draw the menu in skip_until_input_required Step forward 10 frames so that the menu actually draws. Otherwise the user will probably be confused about the actual state of the battle. --- pokemontools/vba/battle.py | 3 +++ 1 file changed, 3 insertions(+) (limited to 'pokemontools/vba/battle.py') diff --git a/pokemontools/vba/battle.py b/pokemontools/vba/battle.py index 5aeb923..824f27a 100644 --- a/pokemontools/vba/battle.py +++ b/pokemontools/vba/battle.py @@ -113,6 +113,9 @@ class Battle(EmulatorController): while not self.is_input_required(): self.emulator.text_wait() + # let the text draw so that the state is more obvious + self.emulator.vba.step(count=10) + def run(self): """ Step through the entire battle. -- cgit v1.2.3 From 966985411f01b799fa71f4823da7a8cd6d9cc47b Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Mon, 11 Nov 2013 00:55:54 -0600 Subject: detect the "mandatory switch" menu This requires a slightly slower text_wait function. There is probably a way to refactor that function in a way that doesn't cause cancer. --- pokemontools/vba/battle.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) (limited to 'pokemontools/vba/battle.py') diff --git a/pokemontools/vba/battle.py b/pokemontools/vba/battle.py index 824f27a..87cd7b1 100644 --- a/pokemontools/vba/battle.py +++ b/pokemontools/vba/battle.py @@ -67,8 +67,10 @@ class Battle(EmulatorController): # 1) current pokemon hp is 0 # 2) game is polling for input - # TODO: detect the mandatory switch menu - return False + if "CANCEL Which ?" in self.emulator.get_text(): + return True + else: + return False def skip_start_text(self, max_loops=20): """ -- cgit v1.2.3