Merge branch 'OriginOverworldShuffle' into OverworldShuffle
This commit is contained in:
195
BaseClasses.py
195
BaseClasses.py
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user