From 7e660dbd23863efc909abe32932972c495460e8b Mon Sep 17 00:00:00 2001 From: Alchav <59858495+Alchav@users.noreply.github.com> Date: Sat, 13 Apr 2024 11:58:50 -0400 Subject: [PATCH] =?UTF-8?q?Pok=C3=A9mon=20Red=20and=20Blue:=200.4.5=20Fixe?= =?UTF-8?q?s=20(#3106)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- worlds/pokemon_rb/__init__.py | 102 +++++---------------------------- worlds/pokemon_rb/client.py | 4 +- worlds/pokemon_rb/locations.py | 2 +- worlds/pokemon_rb/pokemon.py | 101 +++++++++++++++++++++++++++++--- worlds/pokemon_rb/regions.py | 2 +- 5 files changed, 111 insertions(+), 100 deletions(-) diff --git a/worlds/pokemon_rb/__init__.py b/worlds/pokemon_rb/__init__.py index beb2010b..b44e2f3b 100644 --- a/worlds/pokemon_rb/__init__.py +++ b/worlds/pokemon_rb/__init__.py @@ -18,7 +18,7 @@ from .options import pokemon_rb_options from .rom_addresses import rom_addresses from .text import encode_text from .rom import generate_output, get_base_rom_bytes, get_base_rom_path, RedDeltaPatch, BlueDeltaPatch -from .pokemon import process_pokemon_data, process_move_data +from .pokemon import process_pokemon_data, process_move_data, verify_hm_moves from .encounters import process_pokemon_locations, process_trainer_data from .rules import set_rules from .level_scaling import level_scaling @@ -279,12 +279,12 @@ class PokemonRedBlueWorld(World): def fill_hook(self, progitempool, usefulitempool, filleritempool, fill_locations): if not self.multiworld.badgesanity[self.player]: # Door Shuffle options besides Simple place badges during door shuffling - if not self.multiworld.door_shuffle[self.player] not in ("off", "simple"): + if self.multiworld.door_shuffle[self.player] in ("off", "simple"): badges = [item for item in progitempool if "Badge" in item.name and item.player == self.player] for badge in badges: self.multiworld.itempool.remove(badge) progitempool.remove(badge) - for _ in range(5): + for attempt in range(6): badgelocs = [ self.multiworld.get_location(loc, self.player) for loc in [ "Pewter Gym - Brock Prize", "Cerulean Gym - Misty Prize", @@ -293,6 +293,12 @@ class PokemonRedBlueWorld(World): "Cinnabar Gym - Blaine Prize", "Viridian Gym - Giovanni Prize" ] if self.multiworld.get_location(loc, self.player).item is None] state = self.multiworld.get_all_state(False) + # Give it two tries to place badges with wild Pokemon and learnsets as-is. + # If it can't, then try with all Pokemon collected, and we'll try to fix HM move availability after. + if attempt > 1: + for mon in poke_data.pokemon_data.keys(): + state.collect(self.create_item(mon), True) + state.sweep_for_events() self.multiworld.random.shuffle(badges) self.multiworld.random.shuffle(badgelocs) badgelocs_copy = badgelocs.copy() @@ -312,6 +318,7 @@ class PokemonRedBlueWorld(World): break else: raise FillError(f"Failed to place badges for player {self.player}") + verify_hm_moves(self.multiworld, self, self.player) if self.multiworld.key_items_only[self.player]: return @@ -355,97 +362,14 @@ class PokemonRedBlueWorld(World): for location in self.multiworld.get_locations(self.player): if location.name in locs: location.show_in_spoiler = False - - def intervene(move, test_state): - move_bit = pow(2, poke_data.hm_moves.index(move) + 2) - viable_mons = [mon for mon in self.local_poke_data if self.local_poke_data[mon]["tms"][6] & move_bit] - if self.multiworld.randomize_wild_pokemon[self.player] and viable_mons: - accessible_slots = [loc for loc in self.multiworld.get_reachable_locations(test_state, self.player) if - loc.type == "Wild Encounter"] - - def number_of_zones(mon): - zones = set() - for loc in [slot for slot in accessible_slots if slot.item.name == mon]: - zones.add(loc.name.split(" - ")[0]) - return len(zones) - - placed_mons = [slot.item.name for slot in accessible_slots] - - if self.multiworld.area_1_to_1_mapping[self.player]: - placed_mons.sort(key=lambda i: number_of_zones(i)) - else: - # this sort method doesn't work if you reference the same list being sorted in the lambda - placed_mons_copy = placed_mons.copy() - placed_mons.sort(key=lambda i: placed_mons_copy.count(i)) - - placed_mon = placed_mons.pop() - replace_mon = self.multiworld.random.choice(viable_mons) - replace_slot = self.multiworld.random.choice([slot for slot in accessible_slots if slot.item.name - == placed_mon]) - if self.multiworld.area_1_to_1_mapping[self.player]: - zone = " - ".join(replace_slot.name.split(" - ")[:-1]) - replace_slots = [slot for slot in accessible_slots if slot.name.startswith(zone) and slot.item.name - == placed_mon] - for replace_slot in replace_slots: - replace_slot.item = self.create_item(replace_mon) - else: - replace_slot.item = self.create_item(replace_mon) - else: - tms_hms = self.local_tms + poke_data.hm_moves - flag = tms_hms.index(move) - mon_list = [mon for mon in poke_data.pokemon_data.keys() if test_state.has(mon, self.player)] - self.multiworld.random.shuffle(mon_list) - mon_list.sort(key=lambda mon: self.local_move_data[move]["type"] not in - [self.local_poke_data[mon]["type1"], self.local_poke_data[mon]["type2"]]) - for mon in mon_list: - if test_state.has(mon, self.player): - self.local_poke_data[mon]["tms"][int(flag / 8)] |= 1 << (flag % 8) - break - - last_intervene = None - while True: - intervene_move = None - test_state = self.multiworld.get_all_state(False) - if not logic.can_learn_hm(test_state, "Surf", self.player): - intervene_move = "Surf" - elif not logic.can_learn_hm(test_state, "Strength", self.player): - intervene_move = "Strength" - # cut may not be needed if accessibility is minimal, unless you need all 8 badges and badgesanity is off, - # as you will require cut to access celadon gyn - elif ((not logic.can_learn_hm(test_state, "Cut", self.player)) and - (self.multiworld.accessibility[self.player] != "minimal" or ((not - self.multiworld.badgesanity[self.player]) and max( - self.multiworld.elite_four_badges_condition[self.player], - self.multiworld.route_22_gate_condition[self.player], - self.multiworld.victory_road_condition[self.player]) - > 7) or (self.multiworld.door_shuffle[self.player] not in ("off", "simple")))): - intervene_move = "Cut" - elif ((not logic.can_learn_hm(test_state, "Flash", self.player)) - and self.multiworld.dark_rock_tunnel_logic[self.player] - and (self.multiworld.accessibility[self.player] != "minimal" - or self.multiworld.door_shuffle[self.player])): - intervene_move = "Flash" - # If no Pokémon can learn Fly, then during door shuffle it would simply not treat the free fly maps - # as reachable, and if on no door shuffle or simple, fly is simply never necessary. - # We only intervene if a Pokémon is able to learn fly but none are reachable, as that would have been - # considered in door shuffle. - elif ((not logic.can_learn_hm(test_state, "Fly", self.player)) - and self.multiworld.door_shuffle[self.player] not in - ("off", "simple") and [self.fly_map, self.town_map_fly_map] != ["Pallet Town", "Pallet Town"]): - intervene_move = "Fly" - if intervene_move: - if intervene_move == last_intervene: - raise Exception(f"Caught in infinite loop attempting to ensure {intervene_move} is available to player {self.player}") - intervene(intervene_move, test_state) - last_intervene = intervene_move - else: - break + verify_hm_moves(self.multiworld, self, self.player) # Delete evolution events for Pokémon that are not in logic in an all_state so that accessibility check does not # fail. Re-use test_state from previous final loop. + all_state = self.multiworld.get_all_state(False) evolutions_region = self.multiworld.get_region("Evolution", self.player) for location in evolutions_region.locations.copy(): - if not test_state.can_reach(location, player=self.player): + if not all_state.can_reach(location, player=self.player): evolutions_region.locations.remove(location) if self.multiworld.old_man[self.player] == "early_parcel": diff --git a/worlds/pokemon_rb/client.py b/worlds/pokemon_rb/client.py index 8ed21443..97ca1264 100644 --- a/worlds/pokemon_rb/client.py +++ b/worlds/pokemon_rb/client.py @@ -31,7 +31,7 @@ DATA_LOCATIONS = { "CrashCheck2": (0x1617, 1), # Progressive keys, should never be above 10. Just before Dexsanity flags. "CrashCheck3": (0x1A70, 1), - # Route 18 script value. Should never be above 2. Just before Hidden items flags. + # Route 18 Gate script value. Should never be above 3. Just before Hidden items flags. "CrashCheck4": (0x16DD, 1), } @@ -116,7 +116,7 @@ class PokemonRBClient(BizHawkClient): or data["CrashCheck1"][0] & 0xF0 or data["CrashCheck1"][1] & 0xFF or data["CrashCheck2"][0] or data["CrashCheck3"][0] > 10 - or data["CrashCheck4"][0] > 2): + or data["CrashCheck4"][0] > 3): # Should mean game crashed logger.warning("Pokémon Red/Blue game may have crashed. Disconnecting from server.") self.game_state = False diff --git a/worlds/pokemon_rb/locations.py b/worlds/pokemon_rb/locations.py index abaa58fc..b7b7e533 100644 --- a/worlds/pokemon_rb/locations.py +++ b/worlds/pokemon_rb/locations.py @@ -175,7 +175,7 @@ location_data = [ LocationData("Route 2-SE", "South Item", "Moon Stone", rom_addresses["Missable_Route_2_Item_1"], Missable(25)), LocationData("Route 2-SE", "North Item", "HP Up", rom_addresses["Missable_Route_2_Item_2"], Missable(26)), - LocationData("Route 4-E", "Item", "TM04 Whirlwind", rom_addresses["Missable_Route_4_Item"], Missable(27)), + LocationData("Route 4-C", "Item", "TM04 Whirlwind", rom_addresses["Missable_Route_4_Item"], Missable(27)), LocationData("Route 9", "Item", "TM30 Teleport", rom_addresses["Missable_Route_9_Item"], Missable(28)), LocationData("Route 12-N", "Island Item", "TM16 Pay Day", rom_addresses["Missable_Route_12_Item_1"], Missable(30)), LocationData("Route 12-Grass", "Item Behind Cuttable Tree", "Iron", rom_addresses["Missable_Route_12_Item_2"], Missable(31)), diff --git a/worlds/pokemon_rb/pokemon.py b/worlds/pokemon_rb/pokemon.py index 267f424c..28098a2c 100644 --- a/worlds/pokemon_rb/pokemon.py +++ b/worlds/pokemon_rb/pokemon.py @@ -1,5 +1,5 @@ from copy import deepcopy -from . import poke_data +from . import poke_data, logic from .rom_addresses import rom_addresses @@ -135,7 +135,6 @@ def process_pokemon_data(self): learnsets = deepcopy(poke_data.learnsets) tms_hms = self.local_tms + poke_data.hm_moves - compat_hms = set() for mon, mon_data in local_poke_data.items(): @@ -323,19 +322,20 @@ def process_pokemon_data(self): mon_data["tms"][int(flag / 8)] &= ~(1 << (flag % 8)) hm_verify = ["Surf", "Strength"] - if self.multiworld.accessibility[self.player] == "locations" or ((not + if self.multiworld.accessibility[self.player] != "minimal" or ((not self.multiworld.badgesanity[self.player]) and max(self.multiworld.elite_four_badges_condition[self.player], self.multiworld.route_22_gate_condition[self.player], self.multiworld.victory_road_condition[self.player]) > 7) or (self.multiworld.door_shuffle[self.player] not in ("off", "simple")): hm_verify += ["Cut"] - if self.multiworld.accessibility[self.player] == "locations" or (not + if self.multiworld.accessibility[self.player] != "minimal" or (not self.multiworld.dark_rock_tunnel_logic[self.player]) and ((self.multiworld.trainersanity[self.player] or self.multiworld.extra_key_items[self.player]) or self.multiworld.door_shuffle[self.player]): hm_verify += ["Flash"] - # Fly does not need to be verified. Full/Insanity door shuffle connects reachable regions to unreachable regions, - # so if Fly is available and can be learned, the towns you can fly to would be reachable, but if no Pokémon can - # learn it this simply would not occur + # Fly does not need to be verified. Full/Insanity/Decoupled door shuffle connects reachable regions to unreachable + # regions, so if Fly is available and can be learned, the towns you can fly to would be considered reachable for + # door shuffle purposes, but if no Pokémon can learn it, that connection would just be out of logic and it would + # ensure connections to those towns. for hm_move in hm_verify: if hm_move not in compat_hms: @@ -346,3 +346,90 @@ def process_pokemon_data(self): self.local_poke_data = local_poke_data self.learnsets = learnsets + + +def verify_hm_moves(multiworld, world, player): + def intervene(move, test_state): + move_bit = pow(2, poke_data.hm_moves.index(move) + 2) + viable_mons = [mon for mon in world.local_poke_data if world.local_poke_data[mon]["tms"][6] & move_bit] + if multiworld.randomize_wild_pokemon[player] and viable_mons: + accessible_slots = [loc for loc in multiworld.get_reachable_locations(test_state, player) if + loc.type == "Wild Encounter"] + + def number_of_zones(mon): + zones = set() + for loc in [slot for slot in accessible_slots if slot.item.name == mon]: + zones.add(loc.name.split(" - ")[0]) + return len(zones) + + placed_mons = [slot.item.name for slot in accessible_slots] + + if multiworld.area_1_to_1_mapping[player]: + placed_mons.sort(key=lambda i: number_of_zones(i)) + else: + # this sort method doesn't work if you reference the same list being sorted in the lambda + placed_mons_copy = placed_mons.copy() + placed_mons.sort(key=lambda i: placed_mons_copy.count(i)) + + placed_mon = placed_mons.pop() + replace_mon = multiworld.random.choice(viable_mons) + replace_slot = multiworld.random.choice([slot for slot in accessible_slots if slot.item.name + == placed_mon]) + if multiworld.area_1_to_1_mapping[player]: + zone = " - ".join(replace_slot.name.split(" - ")[:-1]) + replace_slots = [slot for slot in accessible_slots if slot.name.startswith(zone) and slot.item.name + == placed_mon] + for replace_slot in replace_slots: + replace_slot.item = world.create_item(replace_mon) + else: + replace_slot.item = world.create_item(replace_mon) + else: + tms_hms = world.local_tms + poke_data.hm_moves + flag = tms_hms.index(move) + mon_list = [mon for mon in poke_data.pokemon_data.keys() if test_state.has(mon, player)] + multiworld.random.shuffle(mon_list) + mon_list.sort(key=lambda mon: world.local_move_data[move]["type"] not in + [world.local_poke_data[mon]["type1"], world.local_poke_data[mon]["type2"]]) + for mon in mon_list: + if test_state.has(mon, player): + world.local_poke_data[mon]["tms"][int(flag / 8)] |= 1 << (flag % 8) + break + + last_intervene = None + while True: + intervene_move = None + test_state = multiworld.get_all_state(False) + if not logic.can_learn_hm(test_state, "Surf", player): + intervene_move = "Surf" + elif not logic.can_learn_hm(test_state, "Strength", player): + intervene_move = "Strength" + # cut may not be needed if accessibility is minimal, unless you need all 8 badges and badgesanity is off, + # as you will require cut to access celadon gyn + elif ((not logic.can_learn_hm(test_state, "Cut", player)) and + (multiworld.accessibility[player] != "minimal" or ((not + multiworld.badgesanity[player]) and max( + multiworld.elite_four_badges_condition[player], + multiworld.route_22_gate_condition[player], + multiworld.victory_road_condition[player]) + > 7) or (multiworld.door_shuffle[player] not in ("off", "simple")))): + intervene_move = "Cut" + elif ((not logic.can_learn_hm(test_state, "Flash", player)) + and multiworld.dark_rock_tunnel_logic[player] + and (multiworld.accessibility[player] != "minimal" + or multiworld.door_shuffle[player])): + intervene_move = "Flash" + # If no Pokémon can learn Fly, then during door shuffle it would simply not treat the free fly maps + # as reachable, and if on no door shuffle or simple, fly is simply never necessary. + # We only intervene if a Pokémon is able to learn fly but none are reachable, as that would have been + # considered in door shuffle. + elif ((not logic.can_learn_hm(test_state, "Fly", player)) + and multiworld.door_shuffle[player] not in + ("off", "simple") and [world.fly_map, world.town_map_fly_map] != ["Pallet Town", "Pallet Town"]): + intervene_move = "Fly" + if intervene_move: + if intervene_move == last_intervene: + raise Exception(f"Caught in infinite loop attempting to ensure {intervene_move} is available to player {player}") + intervene(intervene_move, test_state) + last_intervene = intervene_move + else: + break \ No newline at end of file diff --git a/worlds/pokemon_rb/regions.py b/worlds/pokemon_rb/regions.py index afeb301c..b8f3d829 100644 --- a/worlds/pokemon_rb/regions.py +++ b/worlds/pokemon_rb/regions.py @@ -1948,7 +1948,7 @@ def create_regions(self): for entrance in reversed(region.exits): if isinstance(entrance, PokemonRBWarp): region.exits.remove(entrance) - multiworld.regions.entrance_cache[self.player] = cache + multiworld.regions.entrance_cache[self.player] = cache.copy() if badge_locs: for loc in badge_locs: loc.item = None