Merge branch 'OriginOverworldShuffle' into OverworldShuffle

This commit is contained in:
2024-02-28 16:53:09 -06:00
98 changed files with 9378 additions and 1273 deletions

View File

@@ -64,7 +64,6 @@ class World(object):
self.dark_world_light_cone = False
self.clock_mode = 'none'
self.rupoor_cost = 10
self.aga_randomness = True
self.lock_aga_door_in_escape = False
self.save_and_quit_from_boss = True
self.override_bomb_check = False
@@ -87,6 +86,7 @@ class World(object):
self.owedges = []
self._owedge_cache = {}
self.owswaps = {}
self.owcrossededges = {}
self.owwhirlpools = {}
self.owflutespots = {}
self.owsectors = {}
@@ -114,6 +114,7 @@ class World(object):
set_player_attr('_region_cache', {})
set_player_attr('player_names', [])
set_player_attr('owswaps', [[],[],[]])
set_player_attr('owcrossededges', [])
set_player_attr('owwhirlpools', [])
set_player_attr('owsectors', None)
set_player_attr('remote_items', False)
@@ -132,7 +133,7 @@ class World(object):
set_player_attr('can_access_trock_front', None)
set_player_attr('can_access_trock_big_chest', None)
set_player_attr('can_access_trock_middle', None)
set_player_attr('fix_fake_world', logic[player] not in ['owglitches', 'nologic']
set_player_attr('fix_fake_world', logic[player] not in ['owglitches', 'hybridglitches', 'nologic']
or shuffle[player] in ['lean', 'swapped', 'crossed', 'insanity'])
set_player_attr('mapshuffle', False)
set_player_attr('compassshuffle', False)
@@ -170,7 +171,8 @@ class World(object):
set_player_attr('door_self_loops', False)
set_player_attr('door_type_mode', 'original')
set_player_attr('trap_door_mode', 'optional')
set_player_attr('key_logic_algorithm', 'default')
set_player_attr('key_logic_algorithm', 'partial')
set_player_attr('aga_randomness', True)
set_player_attr('shopsanity', False)
set_player_attr('mixed_travel', 'prevent')
@@ -240,29 +242,16 @@ class World(object):
raise RuntimeError('No such region %s for player %d' % (regionname, player))
def get_owedge(self, edgename, player):
if isinstance(edgename, OWEdge):
return edgename
try:
return self._owedge_cache[(edgename, player)]
except KeyError:
for edge in self.owedges:
if edge.name == edgename and edge.player == player:
self._owedge_cache[(edgename, player)] = edge
return edge
edge = self.check_for_owedge(edgename, player)
if edge is None:
raise RuntimeError('No such edge %s for player %d' % (edgename, player))
return edge
def get_entrance(self, entrance, player):
if isinstance(entrance, Entrance):
return entrance
try:
return self._entrance_cache[(entrance, player)]
except KeyError:
for region in self.regions:
for exit in region.exits:
if exit.name == entrance and exit.player == player:
self._entrance_cache[(entrance, player)] = exit
return exit
ent = self.check_for_entrance(entrance, player)
if ent is None:
raise RuntimeError('No such entrance %s for player %d' % (entrance, player))
return ent
def remove_entrance(self, entrance, player):
if (entrance, player) in self._entrance_cache.keys():
@@ -296,16 +285,10 @@ class World(object):
raise RuntimeError('No such dungeon %s for player %d' % (dungeonname, player))
def get_door(self, doorname, player):
if isinstance(doorname, Door):
return doorname
try:
return self._door_cache[(doorname, player)]
except KeyError:
for door in self.doors:
if door.name == doorname and door.player == player:
self._door_cache[(doorname, player)] = door
return door
door = self.check_for_door(doorname, player)
if door is None:
raise RuntimeError('No such door %s for player %d' % (doorname, player))
return door
def get_portal(self, portal_name, player):
if isinstance(portal_name, Portal):
@@ -319,18 +302,6 @@ class World(object):
return portal
raise RuntimeError('No such portal %s for player %d' % (portal_name, player))
def check_for_owedge(self, edgename, player):
if isinstance(edgename, OWEdge):
return edgename
try:
return self._owedge_cache[(edgename, player)]
except KeyError:
for edge in self.owedges:
if edge.name == edgename and edge.player == player:
self._owedge_cache[(edgename, player)] = edge
return edge
return None
def is_tile_swapped(self, owid, player):
return (self.mode[player] == 'inverted') != (owid in self.owswaps[player][0] and self.owMixed[player])
@@ -341,7 +312,7 @@ class World(object):
return self.is_tile_swapped(0x03, player) and self.is_tile_swapped(0x1b, player)
def is_bombshop_start(self, player):
return self.is_tile_swapped(0x2c, player) and (self.shuffle[player] in ['vanilla', 'dungeonssimple', 'dungeonsfull'] or not self.shufflelinks[player])
return self.is_tile_swapped(0x2c, player)
def is_pyramid_open(self, player):
if self.open_pyramid[player] == 'yes':
@@ -349,13 +320,35 @@ class World(object):
elif self.open_pyramid[player] == 'no':
return False
else:
if self.shuffle[player] not in ['vanilla', 'dungeonssimple', 'dungeonsfull']:
if self.shuffle[player] not in ['vanilla', 'dungeonssimple', 'dungeonsfull', 'district']:
return False
elif self.goal[player] in ['crystals', 'trinity', 'z1', 'ganonhunt']:
return True
else:
return False
def check_for_owedge(self, edgename, player):
if isinstance(edgename, OWEdge):
return edgename
try:
if edgename[-1] == '*':
edgename = edgename[:-1]
edge = self.check_for_owedge(edgename, player)
if self.is_tile_swapped(edge.owIndex, player):
from OverworldShuffle import parallel_links
if edgename in parallel_links.keys() or edgename in parallel_links.inverse.keys():
edgename = parallel_links[edgename] if edgename in parallel_links.keys() else parallel_links.inverse[edgename][0]
return self.check_for_owedge(edgename, player)
else:
raise Exception("Edge notated with * doesn't have a parallel edge: %s" & edgename)
return self._owedge_cache[(edgename, player)]
except KeyError:
for edge in self.owedges:
if edge.name == edgename and edge.player == player:
self._owedge_cache[(edgename, player)] = edge
return edge
return None
def check_for_door(self, doorname, player):
if isinstance(doorname, Door):
return doorname
@@ -618,7 +611,7 @@ class World(object):
if not sphere:
# ran out of places and did not finish yet, quit
if log_error:
missing_locations = ", ".join([x.name for x in prog_locations])
missing_locations = ", ".join([f'{x.name} (#{x.player})' for x in prog_locations])
logging.getLogger('').error(f'Cannot reach the following locations: {missing_locations}')
return False
@@ -653,9 +646,45 @@ class CollectionState(object):
self.opened_doors = {player: set() for player in range(1, parent.players + 1)}
self.dungeons_to_check = {player: defaultdict(dict) for player in range(1, parent.players + 1)}
self.dungeon_limits = None
self.placing_item = None
self.placing_items = None
# self.trace = None
def can_reach_from(self, spot, start, player=None):
old_state = self.copy()
# old_state.path = {old_state.world.get_region(start, player)}
old_state.stale[player] = False
old_state.reachable_regions[player] = dict()
old_state.blocked_connections[player] = dict()
rrp = old_state.reachable_regions[player]
bc = old_state.blocked_connections[player]
# init on first call - this can't be done on construction since the regions don't exist yet
start = self.world.get_region(start, player)
if start in self.reachable_regions[player]:
rrp[start] = self.reachable_regions[player][start]
for conn in start.exits:
bc[conn] = self.blocked_connections[player][conn]
else:
rrp[start] = CrystalBarrier.Orange
for conn in start.exits:
bc[conn] = CrystalBarrier.Orange
queue = deque(old_state.blocked_connections[player].items())
old_state.traverse_world(queue, rrp, bc, player)
if old_state.world.key_logic_algorithm[player] == 'default':
unresolved_events = [x for y in old_state.reachable_regions[player] for x in y.locations
if x.event and x.item and (x.item.smallkey or x.item.bigkey or x.item.advancement)
and x not in old_state.locations_checked and x.can_reach(old_state)]
unresolved_events = old_state._do_not_flood_the_keys(unresolved_events)
if len(unresolved_events) == 0:
old_state.check_key_doors_in_dungeons(rrp, player)
if self.world.get_region(spot, player) in rrp:
return True
else:
return False
def update_reachable_regions(self, player):
self.stale[player] = False
rrp = self.reachable_regions[player]
@@ -931,7 +960,7 @@ class CollectionState(object):
return door_candidates
door_candidates, skip = [], set()
if (state.world.accessibility[player] != 'locations' and remaining_keys == 0 and dungeon_name != 'Universal'
and state.placing_item and state.placing_item.name == small_key_name):
and state.placing_items and any(i.name == small_key_name and i.player == player for i in state.placing_items)):
key_logic = state.world.key_logic[player][dungeon_name]
for door, paired in key_logic.sm_doors.items():
if door.name in key_logic.door_rules:
@@ -976,7 +1005,7 @@ class CollectionState(object):
player: defaultdict(dict, {name: copy.copy(checklist)
for name, checklist in self.dungeons_to_check[player].items()})
for player in range(1, self.world.players + 1)}
ret.placing_item = self.placing_item
ret.placing_items = self.placing_items
return ret
def apply_dungeon_exploration(self, rrp, player, dungeon_name, checklist):
@@ -1074,7 +1103,7 @@ class CollectionState(object):
# try to resolve a name
if resolution_hint == 'Location':
spot = self.world.get_location(spot, player)
elif resolution_hint in ['Entrance', 'OWEdge', 'OWTerrain', 'Ledge', 'Portal', 'Whirlpool', 'Mirror', 'Flute']:
elif resolution_hint in ['Entrance', 'OWEdge', 'OWTerrain', 'OpenTerrain', 'Ledge', 'OWG', 'Portal', 'Whirlpool', 'Mirror', 'Flute']:
spot = self.world.get_entrance(spot, player)
else:
# default to Region
@@ -1222,6 +1251,12 @@ class CollectionState(object):
def can_lift_rocks(self, player):
return self.has('Power Glove', player) or self.has('Titans Mitts', player)
def can_bomb_clip(self, region, player: int) -> bool:
return self.is_not_bunny(region, player) and self.has('Pegasus Boots', player) and self.can_use_bombs(player)
def can_dash_clip(self, region, player: int) -> bool:
return self.is_not_bunny(region, player) and self.has('Pegasus Boots', player)
def has_bottle(self, player):
return self.bottle_count(player) > 0
@@ -1463,7 +1498,9 @@ class CollectionState(object):
return self.has('Fire Rod', player) or (self.has('Bombos', player) and self.can_use_medallions(player))
def can_avoid_lasers(self, player):
return self.has('Mirror Shield', player) or self.has('Cane of Byrna', player) or self.has('Cape', player)
return (self.has('Mirror Shield', player) or
self.has('Cape', player) or
(self.has('Cane of Byrna', player) and self.world.difficulty_adjustments[player] not in ['hard', 'expert']))
def is_not_bunny(self, region, player):
return self.has_Pearl(player) or not region.can_cause_bunny(player)
@@ -1508,6 +1545,9 @@ class CollectionState(object):
def can_superbunny_mirror_with_sword(self, player):
return self.has_Mirror(player) and self.has_sword(player)
def can_bunny_pocket(self, player):
return self.has_Boots(player) and (self.has_Mirror(player) or self.has_bottle(player))
def collect(self, item, event=False, location=None):
if location:
@@ -1826,6 +1866,9 @@ class Region(object):
return self.is_dark_world if self.world.mode[player] != 'inverted' else self.is_light_world
def is_outdoors(self):
return self.type in {RegionType.LightWorld, RegionType.DarkWorld}
def __str__(self):
return str(self.__unicode__())
@@ -1897,8 +1940,8 @@ class Entrance(object):
if region not in explored_regions:
explored_regions[region] = path
for exit in region.exits:
if exit.connected_region and (not ignore_ledges or exit.spot_type != 'Ledge') \
and exit.connected_region.name not in ['Dig Game Area'] \
if exit.connected_region and (not ignore_ledges or exit.spot_type not in ['Ledge', 'OWG']) \
and exit.name not in ['Dig Game To Ledge Drop'] \
and exit.access_rule(state):
if exit.connected_region == destination:
found = True
@@ -2512,7 +2555,7 @@ class OWEdge(object):
self.unknownX = 0x0
self.unknownY = 0x0
if self.owIndex < 0x40 or self.owIndex >= 0x80:
if self.owIndex & 0x40 == 0:
self.worldType = WorldType.Light
else:
self.worldType = WorldType.Dark
@@ -2553,6 +2596,12 @@ class OWEdge(object):
self.specialID = special_id
return self
def is_tile_swapped(self, world):
return world.is_tile_swapped(self.owIndex, self.player)
def is_lw(self, world):
return (self.worldType == WorldType.Light) != self.is_tile_swapped(world)
def __eq__(self, other):
return isinstance(other, self.__class__) and self.name == other.name
@@ -3048,6 +3097,7 @@ class Spoiler(object):
self.world = world
self.hashes = {}
self.overworlds = {}
self.whirlpools = {}
self.maps = {}
self.entrances = {}
self.doors = {}
@@ -3072,6 +3122,12 @@ class Spoiler(object):
else:
self.overworlds[(entrance, direction, player)] = OrderedDict([('player', player), ('entrance', entrance), ('exit', exit), ('direction', direction)])
def set_whirlpool(self, entrance, exit, direction, player):
if self.world.players == 1:
self.whirlpools[(entrance, direction, player)] = OrderedDict([('entrance', entrance), ('exit', exit), ('direction', direction)])
else:
self.whirlpools[(entrance, direction, player)] = OrderedDict([('player', player), ('entrance', entrance), ('exit', exit), ('direction', direction)])
def set_map(self, type, text, data, player):
if self.world.players == 1:
self.maps[(type, player)] = OrderedDict([('type', type), ('text', text), ('data', data)])
@@ -3275,6 +3331,7 @@ class Spoiler(object):
self.parse_data()
out = OrderedDict()
out['Overworld'] = list(self.overworlds.values())
out['Whirlpools'] = list(self.whirlpools.values())
out['Maps'] = list(self.maps.values())
out['Entrances'] = list(self.entrances.values())
out['Doors'] = list(self.doors.values())
@@ -3447,46 +3504,50 @@ class Spoiler(object):
for fairy, bottle in self.bottles.items():
outfile.write(f'{fairy}: {bottle}\n')
if self.overworlds or self.maps:
outfile.write('\n\nOverworld:\n\n')
if self.maps:
# flute shuffle
for player in range(1, self.world.players + 1):
if ('flute', player) in self.maps:
outfile.write('Flute Spots:\n')
outfile.write('\n\nFlute Spots:\n\n')
break
for player in range(1, self.world.players + 1):
if ('flute', player) in self.maps:
if self.world.players > 1:
outfile.write(str('(Player ' + str(player) + ')\n')) # player name
outfile.write(self.maps[('flute', player)]['text'] + '\n\n')
outfile.write(self.maps[('flute', player)]['text'])
# overworld tile flips
for player in range(1, self.world.players + 1):
if ('swaps', player) in self.maps:
outfile.write('OW Tile Flips:\n')
outfile.write('\n\nOW Tile Flips:\n\n')
break
for player in range(1, self.world.players + 1):
if ('swaps', player) in self.maps:
if self.world.players > 1:
outfile.write(str('(Player ' + str(player) + ')\n')) # player name
outfile.write(self.maps[('swaps', player)]['text'] + '\n\n')
outfile.write(self.maps[('swaps', player)]['text'])
# crossed groups
for player in range(1, self.world.players + 1):
if ('groups', player) in self.maps:
outfile.write('OW Crossed Groups:\n')
outfile.write('\n\nOW Crossed Groups:\n\n')
break
for player in range(1, self.world.players + 1):
if ('groups', player) in self.maps:
if self.world.players > 1:
outfile.write(str('(Player ' + str(player) + ')\n')) # player name
outfile.write(self.maps[('groups', player)]['text'] + '\n\n')
outfile.write(self.maps[('groups', player)]['text'])
if self.overworlds:
outfile.write('\n\nOverworld Edges:\n\n')
# overworld transitions
outfile.write('\n'.join(['%s%s %s %s' % (f'{self.world.get_player_names(entry["player"])}: ' if self.world.players > 1 else '', self.world.fish.translate("meta","overworlds",entry['entrance']), '<=>' if entry['direction'] == 'both' else '<=' if entry['direction'] == 'exit' else '=>', self.world.fish.translate("meta","overworlds",entry['exit'])) for entry in self.overworlds.values()]))
if self.whirlpools:
outfile.write('\n\nWhirlpools:\n\n')
# whirlpools
outfile.write('\n'.join(['%s%s %s %s' % (f'{self.world.get_player_names(entry["player"])}: ' if self.world.players > 1 else '', self.world.fish.translate("meta","whirlpools",entry['entrance']), '<=>' if entry['direction'] == 'both' else '<=' if entry['direction'] == 'exit' else '=>', self.world.fish.translate("meta","whirlpools",entry['exit'])) for entry in self.whirlpools.values()]))
if self.entrances:
# entrances: To/From overworld; Checking w/ & w/out "Exit" and translating accordingly
outfile.write('\n\nEntrances:\n\n')
@@ -3669,10 +3730,10 @@ class Pot(object):
# byte 0: DDDE EEEE (DR, ER)
dr_mode = {"basic": 1, "crossed": 2, "vanilla": 0, "partitioned": 3, 'paired': 4}
er_mode = {"vanilla": 0, "simple": 1, "restricted": 2, "full": 3, "crossed": 4, "insanity": 5, 'lite': 8,
'lean': 9, "dungeonsfull": 7, "dungeonssimple": 6, "swapped": 10}
'lean': 9, "dungeonsfull": 7, "dungeonssimple": 6, "swapped": 10, "district": 11}
# byte 1: LLLW WSSS (logic, mode, sword)
logic_mode = {"noglitches": 0, "minorglitches": 1, "nologic": 2, "owglitches": 3, "majorglitches": 4}
# byte 1: LLLW WSS? (logic, mode, sword)
logic_mode = {"noglitches": 0, "minorglitches": 1, "nologic": 2, "owglitches": 3, "majorglitches": 4, "hybridglitches": 5}
world_mode = {"open": 0, "standard": 1, "inverted": 2}
sword_mode = {"random": 0, "assured": 1, "swordless": 2, "swordless_b": 2, "vanilla": 3, "bombs": 4, "pseudo": 5, "assured_pseudo": 5, "byrna": 6, "somaria": 6, "cane": 6, "bees": 7, "bugnet": 7}
@@ -3717,7 +3778,7 @@ boss_mode = {"none": 0, "simple": 1, "full": 2, "chaos": 3, 'random': 3, 'unique
# byte 11: OOOT WCCC (OWR layout, free terrain, whirlpools, OWR crossed)
or_mode = {"vanilla": 0, "parallel": 1, "full": 2}
orcrossed_mode = {"none": 0, "polar": 1, "grouped": 2, "limited": 3, "chaos": 4}
orcrossed_mode = {"none": 0, "polar": 1, "grouped": 2, "unrestricted": 4}
# byte 12: KMB? FF?? (keep similar, mixed/tile flip, bonk drops, flute spots)
flutespot_mode = {"vanilla": 0, "balanced": 1, "random": 2}