Skip to content

Commit f8043e5

Browse files
Alchavqwint
authored andcommitted
Pokémon Red and Blue: 0.4.5 Fixes (ArchipelagoMW#3106)
1 parent 07693a3 commit f8043e5

File tree

5 files changed

+111
-100
lines changed

5 files changed

+111
-100
lines changed

worlds/pokemon_rb/__init__.py

+13-89
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
from .rom_addresses import rom_addresses
1919
from .text import encode_text
2020
from .rom import generate_output, get_base_rom_bytes, get_base_rom_path, RedDeltaPatch, BlueDeltaPatch
21-
from .pokemon import process_pokemon_data, process_move_data
21+
from .pokemon import process_pokemon_data, process_move_data, verify_hm_moves
2222
from .encounters import process_pokemon_locations, process_trainer_data
2323
from .rules import set_rules
2424
from .level_scaling import level_scaling
@@ -279,12 +279,12 @@ def stage_fill_hook(cls, multiworld, progitempool, usefulitempool, filleritempoo
279279
def fill_hook(self, progitempool, usefulitempool, filleritempool, fill_locations):
280280
if not self.multiworld.badgesanity[self.player]:
281281
# Door Shuffle options besides Simple place badges during door shuffling
282-
if not self.multiworld.door_shuffle[self.player] not in ("off", "simple"):
282+
if self.multiworld.door_shuffle[self.player] in ("off", "simple"):
283283
badges = [item for item in progitempool if "Badge" in item.name and item.player == self.player]
284284
for badge in badges:
285285
self.multiworld.itempool.remove(badge)
286286
progitempool.remove(badge)
287-
for _ in range(5):
287+
for attempt in range(6):
288288
badgelocs = [
289289
self.multiworld.get_location(loc, self.player) for loc in [
290290
"Pewter Gym - Brock Prize", "Cerulean Gym - Misty Prize",
@@ -293,6 +293,12 @@ def fill_hook(self, progitempool, usefulitempool, filleritempool, fill_locations
293293
"Cinnabar Gym - Blaine Prize", "Viridian Gym - Giovanni Prize"
294294
] if self.multiworld.get_location(loc, self.player).item is None]
295295
state = self.multiworld.get_all_state(False)
296+
# Give it two tries to place badges with wild Pokemon and learnsets as-is.
297+
# If it can't, then try with all Pokemon collected, and we'll try to fix HM move availability after.
298+
if attempt > 1:
299+
for mon in poke_data.pokemon_data.keys():
300+
state.collect(self.create_item(mon), True)
301+
state.sweep_for_events()
296302
self.multiworld.random.shuffle(badges)
297303
self.multiworld.random.shuffle(badgelocs)
298304
badgelocs_copy = badgelocs.copy()
@@ -312,6 +318,7 @@ def fill_hook(self, progitempool, usefulitempool, filleritempool, fill_locations
312318
break
313319
else:
314320
raise FillError(f"Failed to place badges for player {self.player}")
321+
verify_hm_moves(self.multiworld, self, self.player)
315322

316323
if self.multiworld.key_items_only[self.player]:
317324
return
@@ -355,97 +362,14 @@ def pre_fill(self) -> None:
355362
for location in self.multiworld.get_locations(self.player):
356363
if location.name in locs:
357364
location.show_in_spoiler = False
358-
359-
def intervene(move, test_state):
360-
move_bit = pow(2, poke_data.hm_moves.index(move) + 2)
361-
viable_mons = [mon for mon in self.local_poke_data if self.local_poke_data[mon]["tms"][6] & move_bit]
362-
if self.multiworld.randomize_wild_pokemon[self.player] and viable_mons:
363-
accessible_slots = [loc for loc in self.multiworld.get_reachable_locations(test_state, self.player) if
364-
loc.type == "Wild Encounter"]
365-
366-
def number_of_zones(mon):
367-
zones = set()
368-
for loc in [slot for slot in accessible_slots if slot.item.name == mon]:
369-
zones.add(loc.name.split(" - ")[0])
370-
return len(zones)
371-
372-
placed_mons = [slot.item.name for slot in accessible_slots]
373-
374-
if self.multiworld.area_1_to_1_mapping[self.player]:
375-
placed_mons.sort(key=lambda i: number_of_zones(i))
376-
else:
377-
# this sort method doesn't work if you reference the same list being sorted in the lambda
378-
placed_mons_copy = placed_mons.copy()
379-
placed_mons.sort(key=lambda i: placed_mons_copy.count(i))
380-
381-
placed_mon = placed_mons.pop()
382-
replace_mon = self.multiworld.random.choice(viable_mons)
383-
replace_slot = self.multiworld.random.choice([slot for slot in accessible_slots if slot.item.name
384-
== placed_mon])
385-
if self.multiworld.area_1_to_1_mapping[self.player]:
386-
zone = " - ".join(replace_slot.name.split(" - ")[:-1])
387-
replace_slots = [slot for slot in accessible_slots if slot.name.startswith(zone) and slot.item.name
388-
== placed_mon]
389-
for replace_slot in replace_slots:
390-
replace_slot.item = self.create_item(replace_mon)
391-
else:
392-
replace_slot.item = self.create_item(replace_mon)
393-
else:
394-
tms_hms = self.local_tms + poke_data.hm_moves
395-
flag = tms_hms.index(move)
396-
mon_list = [mon for mon in poke_data.pokemon_data.keys() if test_state.has(mon, self.player)]
397-
self.multiworld.random.shuffle(mon_list)
398-
mon_list.sort(key=lambda mon: self.local_move_data[move]["type"] not in
399-
[self.local_poke_data[mon]["type1"], self.local_poke_data[mon]["type2"]])
400-
for mon in mon_list:
401-
if test_state.has(mon, self.player):
402-
self.local_poke_data[mon]["tms"][int(flag / 8)] |= 1 << (flag % 8)
403-
break
404-
405-
last_intervene = None
406-
while True:
407-
intervene_move = None
408-
test_state = self.multiworld.get_all_state(False)
409-
if not logic.can_learn_hm(test_state, "Surf", self.player):
410-
intervene_move = "Surf"
411-
elif not logic.can_learn_hm(test_state, "Strength", self.player):
412-
intervene_move = "Strength"
413-
# cut may not be needed if accessibility is minimal, unless you need all 8 badges and badgesanity is off,
414-
# as you will require cut to access celadon gyn
415-
elif ((not logic.can_learn_hm(test_state, "Cut", self.player)) and
416-
(self.multiworld.accessibility[self.player] != "minimal" or ((not
417-
self.multiworld.badgesanity[self.player]) and max(
418-
self.multiworld.elite_four_badges_condition[self.player],
419-
self.multiworld.route_22_gate_condition[self.player],
420-
self.multiworld.victory_road_condition[self.player])
421-
> 7) or (self.multiworld.door_shuffle[self.player] not in ("off", "simple")))):
422-
intervene_move = "Cut"
423-
elif ((not logic.can_learn_hm(test_state, "Flash", self.player))
424-
and self.multiworld.dark_rock_tunnel_logic[self.player]
425-
and (self.multiworld.accessibility[self.player] != "minimal"
426-
or self.multiworld.door_shuffle[self.player])):
427-
intervene_move = "Flash"
428-
# If no Pokémon can learn Fly, then during door shuffle it would simply not treat the free fly maps
429-
# as reachable, and if on no door shuffle or simple, fly is simply never necessary.
430-
# We only intervene if a Pokémon is able to learn fly but none are reachable, as that would have been
431-
# considered in door shuffle.
432-
elif ((not logic.can_learn_hm(test_state, "Fly", self.player))
433-
and self.multiworld.door_shuffle[self.player] not in
434-
("off", "simple") and [self.fly_map, self.town_map_fly_map] != ["Pallet Town", "Pallet Town"]):
435-
intervene_move = "Fly"
436-
if intervene_move:
437-
if intervene_move == last_intervene:
438-
raise Exception(f"Caught in infinite loop attempting to ensure {intervene_move} is available to player {self.player}")
439-
intervene(intervene_move, test_state)
440-
last_intervene = intervene_move
441-
else:
442-
break
365+
verify_hm_moves(self.multiworld, self, self.player)
443366

444367
# Delete evolution events for Pokémon that are not in logic in an all_state so that accessibility check does not
445368
# fail. Re-use test_state from previous final loop.
369+
all_state = self.multiworld.get_all_state(False)
446370
evolutions_region = self.multiworld.get_region("Evolution", self.player)
447371
for location in evolutions_region.locations.copy():
448-
if not test_state.can_reach(location, player=self.player):
372+
if not all_state.can_reach(location, player=self.player):
449373
evolutions_region.locations.remove(location)
450374

451375
if self.multiworld.old_man[self.player] == "early_parcel":

worlds/pokemon_rb/client.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
"CrashCheck2": (0x1617, 1),
3232
# Progressive keys, should never be above 10. Just before Dexsanity flags.
3333
"CrashCheck3": (0x1A70, 1),
34-
# Route 18 script value. Should never be above 2. Just before Hidden items flags.
34+
# Route 18 Gate script value. Should never be above 3. Just before Hidden items flags.
3535
"CrashCheck4": (0x16DD, 1),
3636
}
3737

@@ -116,7 +116,7 @@ async def game_watcher(self, ctx):
116116
or data["CrashCheck1"][0] & 0xF0 or data["CrashCheck1"][1] & 0xFF
117117
or data["CrashCheck2"][0]
118118
or data["CrashCheck3"][0] > 10
119-
or data["CrashCheck4"][0] > 2):
119+
or data["CrashCheck4"][0] > 3):
120120
# Should mean game crashed
121121
logger.warning("Pokémon Red/Blue game may have crashed. Disconnecting from server.")
122122
self.game_state = False

worlds/pokemon_rb/locations.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,7 @@ def __init__(self, flag):
175175
LocationData("Route 2-SE", "South Item", "Moon Stone", rom_addresses["Missable_Route_2_Item_1"],
176176
Missable(25)),
177177
LocationData("Route 2-SE", "North Item", "HP Up", rom_addresses["Missable_Route_2_Item_2"], Missable(26)),
178-
LocationData("Route 4-E", "Item", "TM04 Whirlwind", rom_addresses["Missable_Route_4_Item"], Missable(27)),
178+
LocationData("Route 4-C", "Item", "TM04 Whirlwind", rom_addresses["Missable_Route_4_Item"], Missable(27)),
179179
LocationData("Route 9", "Item", "TM30 Teleport", rom_addresses["Missable_Route_9_Item"], Missable(28)),
180180
LocationData("Route 12-N", "Island Item", "TM16 Pay Day", rom_addresses["Missable_Route_12_Item_1"], Missable(30)),
181181
LocationData("Route 12-Grass", "Item Behind Cuttable Tree", "Iron", rom_addresses["Missable_Route_12_Item_2"], Missable(31)),

worlds/pokemon_rb/pokemon.py

+94-7
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from copy import deepcopy
2-
from . import poke_data
2+
from . import poke_data, logic
33
from .rom_addresses import rom_addresses
44

55

@@ -135,7 +135,6 @@ def process_pokemon_data(self):
135135
learnsets = deepcopy(poke_data.learnsets)
136136
tms_hms = self.local_tms + poke_data.hm_moves
137137

138-
139138
compat_hms = set()
140139

141140
for mon, mon_data in local_poke_data.items():
@@ -323,19 +322,20 @@ def roll_tm_compat(roll_move):
323322
mon_data["tms"][int(flag / 8)] &= ~(1 << (flag % 8))
324323

325324
hm_verify = ["Surf", "Strength"]
326-
if self.multiworld.accessibility[self.player] == "locations" or ((not
325+
if self.multiworld.accessibility[self.player] != "minimal" or ((not
327326
self.multiworld.badgesanity[self.player]) and max(self.multiworld.elite_four_badges_condition[self.player],
328327
self.multiworld.route_22_gate_condition[self.player], self.multiworld.victory_road_condition[self.player])
329328
> 7) or (self.multiworld.door_shuffle[self.player] not in ("off", "simple")):
330329
hm_verify += ["Cut"]
331-
if self.multiworld.accessibility[self.player] == "locations" or (not
330+
if self.multiworld.accessibility[self.player] != "minimal" or (not
332331
self.multiworld.dark_rock_tunnel_logic[self.player]) and ((self.multiworld.trainersanity[self.player] or
333332
self.multiworld.extra_key_items[self.player])
334333
or self.multiworld.door_shuffle[self.player]):
335334
hm_verify += ["Flash"]
336-
# Fly does not need to be verified. Full/Insanity door shuffle connects reachable regions to unreachable regions,
337-
# so if Fly is available and can be learned, the towns you can fly to would be reachable, but if no Pokémon can
338-
# learn it this simply would not occur
335+
# Fly does not need to be verified. Full/Insanity/Decoupled door shuffle connects reachable regions to unreachable
336+
# regions, so if Fly is available and can be learned, the towns you can fly to would be considered reachable for
337+
# door shuffle purposes, but if no Pokémon can learn it, that connection would just be out of logic and it would
338+
# ensure connections to those towns.
339339

340340
for hm_move in hm_verify:
341341
if hm_move not in compat_hms:
@@ -346,3 +346,90 @@ def roll_tm_compat(roll_move):
346346

347347
self.local_poke_data = local_poke_data
348348
self.learnsets = learnsets
349+
350+
351+
def verify_hm_moves(multiworld, world, player):
352+
def intervene(move, test_state):
353+
move_bit = pow(2, poke_data.hm_moves.index(move) + 2)
354+
viable_mons = [mon for mon in world.local_poke_data if world.local_poke_data[mon]["tms"][6] & move_bit]
355+
if multiworld.randomize_wild_pokemon[player] and viable_mons:
356+
accessible_slots = [loc for loc in multiworld.get_reachable_locations(test_state, player) if
357+
loc.type == "Wild Encounter"]
358+
359+
def number_of_zones(mon):
360+
zones = set()
361+
for loc in [slot for slot in accessible_slots if slot.item.name == mon]:
362+
zones.add(loc.name.split(" - ")[0])
363+
return len(zones)
364+
365+
placed_mons = [slot.item.name for slot in accessible_slots]
366+
367+
if multiworld.area_1_to_1_mapping[player]:
368+
placed_mons.sort(key=lambda i: number_of_zones(i))
369+
else:
370+
# this sort method doesn't work if you reference the same list being sorted in the lambda
371+
placed_mons_copy = placed_mons.copy()
372+
placed_mons.sort(key=lambda i: placed_mons_copy.count(i))
373+
374+
placed_mon = placed_mons.pop()
375+
replace_mon = multiworld.random.choice(viable_mons)
376+
replace_slot = multiworld.random.choice([slot for slot in accessible_slots if slot.item.name
377+
== placed_mon])
378+
if multiworld.area_1_to_1_mapping[player]:
379+
zone = " - ".join(replace_slot.name.split(" - ")[:-1])
380+
replace_slots = [slot for slot in accessible_slots if slot.name.startswith(zone) and slot.item.name
381+
== placed_mon]
382+
for replace_slot in replace_slots:
383+
replace_slot.item = world.create_item(replace_mon)
384+
else:
385+
replace_slot.item = world.create_item(replace_mon)
386+
else:
387+
tms_hms = world.local_tms + poke_data.hm_moves
388+
flag = tms_hms.index(move)
389+
mon_list = [mon for mon in poke_data.pokemon_data.keys() if test_state.has(mon, player)]
390+
multiworld.random.shuffle(mon_list)
391+
mon_list.sort(key=lambda mon: world.local_move_data[move]["type"] not in
392+
[world.local_poke_data[mon]["type1"], world.local_poke_data[mon]["type2"]])
393+
for mon in mon_list:
394+
if test_state.has(mon, player):
395+
world.local_poke_data[mon]["tms"][int(flag / 8)] |= 1 << (flag % 8)
396+
break
397+
398+
last_intervene = None
399+
while True:
400+
intervene_move = None
401+
test_state = multiworld.get_all_state(False)
402+
if not logic.can_learn_hm(test_state, "Surf", player):
403+
intervene_move = "Surf"
404+
elif not logic.can_learn_hm(test_state, "Strength", player):
405+
intervene_move = "Strength"
406+
# cut may not be needed if accessibility is minimal, unless you need all 8 badges and badgesanity is off,
407+
# as you will require cut to access celadon gyn
408+
elif ((not logic.can_learn_hm(test_state, "Cut", player)) and
409+
(multiworld.accessibility[player] != "minimal" or ((not
410+
multiworld.badgesanity[player]) and max(
411+
multiworld.elite_four_badges_condition[player],
412+
multiworld.route_22_gate_condition[player],
413+
multiworld.victory_road_condition[player])
414+
> 7) or (multiworld.door_shuffle[player] not in ("off", "simple")))):
415+
intervene_move = "Cut"
416+
elif ((not logic.can_learn_hm(test_state, "Flash", player))
417+
and multiworld.dark_rock_tunnel_logic[player]
418+
and (multiworld.accessibility[player] != "minimal"
419+
or multiworld.door_shuffle[player])):
420+
intervene_move = "Flash"
421+
# If no Pokémon can learn Fly, then during door shuffle it would simply not treat the free fly maps
422+
# as reachable, and if on no door shuffle or simple, fly is simply never necessary.
423+
# We only intervene if a Pokémon is able to learn fly but none are reachable, as that would have been
424+
# considered in door shuffle.
425+
elif ((not logic.can_learn_hm(test_state, "Fly", player))
426+
and multiworld.door_shuffle[player] not in
427+
("off", "simple") and [world.fly_map, world.town_map_fly_map] != ["Pallet Town", "Pallet Town"]):
428+
intervene_move = "Fly"
429+
if intervene_move:
430+
if intervene_move == last_intervene:
431+
raise Exception(f"Caught in infinite loop attempting to ensure {intervene_move} is available to player {player}")
432+
intervene(intervene_move, test_state)
433+
last_intervene = intervene_move
434+
else:
435+
break

worlds/pokemon_rb/regions.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1948,7 +1948,7 @@ def create_regions(self):
19481948
for entrance in reversed(region.exits):
19491949
if isinstance(entrance, PokemonRBWarp):
19501950
region.exits.remove(entrance)
1951-
multiworld.regions.entrance_cache[self.player] = cache
1951+
multiworld.regions.entrance_cache[self.player] = cache.copy()
19521952
if badge_locs:
19531953
for loc in badge_locs:
19541954
loc.item = None

0 commit comments

Comments
 (0)