Merge branch 'DoorPerf' of https://github.com/compiling/ALttPEntranceRandomizer into compiling-DoorPerf

This commit is contained in:
aerinon
2020-05-26 14:35:12 -06:00
12 changed files with 222 additions and 207 deletions

View File

@@ -1,12 +1,18 @@
import copy import copy
from enum import Enum, unique, Flag
import logging
import json import json
from collections import OrderedDict, deque, defaultdict import logging
from collections import OrderedDict, Counter, deque, defaultdict
from enum import Enum, unique
try:
from fast_enum import FastEnum
except ImportError:
from enum import Flag
FastEnum = Flag
from source.classes.BabelFish import BabelFish from source.classes.BabelFish import BabelFish
from EntranceShuffle import door_addresses from EntranceShuffle import door_addresses, indirect_connections
from _vendor.collections_extended import bag
from Utils import int16_as_bytes from Utils import int16_as_bytes
from Tables import normal_offset_table, spiral_offset_table, multiply_lookup, divisor_lookup from Tables import normal_offset_table, spiral_offset_table, multiply_lookup, divisor_lookup
from RoomData import Room from RoomData import Room
@@ -126,6 +132,12 @@ class World(object):
for region in regions if regions else self.regions: for region in regions if regions else self.regions:
region.world = self region.world = self
self._region_cache[region.player][region.name] = region self._region_cache[region.player][region.name] = region
for exit in region.exits:
self._entrance_cache[(exit.name, exit.player)] = exit
def initialize_doors(self, doors):
for door in doors:
self._door_cache[(door.name, door.player)] = door
def get_regions(self, player=None): def get_regions(self, player=None):
return self.regions if player is None else self._region_cache[player].values() return self.regions if player is None else self._region_cache[player].values()
@@ -235,41 +247,41 @@ class World(object):
if ret.has('Golden Sword', item.player): if ret.has('Golden Sword', item.player):
pass pass
elif ret.has('Tempered Sword', item.player) and self.difficulty_requirements[item.player].progressive_sword_limit >= 4: elif ret.has('Tempered Sword', item.player) and self.difficulty_requirements[item.player].progressive_sword_limit >= 4:
ret.prog_items.add(('Golden Sword', item.player)) ret.prog_items['Golden Sword', item.player] += 1
elif ret.has('Master Sword', item.player) and self.difficulty_requirements[item.player].progressive_sword_limit >= 3: elif ret.has('Master Sword', item.player) and self.difficulty_requirements[item.player].progressive_sword_limit >= 3:
ret.prog_items.add(('Tempered Sword', item.player)) ret.prog_items['Tempered Sword', item.player] += 1
elif ret.has('Fighter Sword', item.player) and self.difficulty_requirements[item.player].progressive_sword_limit >= 2: elif ret.has('Fighter Sword', item.player) and self.difficulty_requirements[item.player].progressive_sword_limit >= 2:
ret.prog_items.add(('Master Sword', item.player)) ret.prog_items['Master Sword', item.player] += 1
elif self.difficulty_requirements[item.player].progressive_sword_limit >= 1: elif self.difficulty_requirements[item.player].progressive_sword_limit >= 1:
ret.prog_items.add(('Fighter Sword', item.player)) ret.prog_items['Fighter Sword', item.player] += 1
elif 'Glove' in item.name: elif 'Glove' in item.name:
if ret.has('Titans Mitts', item.player): if ret.has('Titans Mitts', item.player):
pass pass
elif ret.has('Power Glove', item.player): elif ret.has('Power Glove', item.player):
ret.prog_items.add(('Titans Mitts', item.player)) ret.prog_items['Titans Mitts', item.player] += 1
else: else:
ret.prog_items.add(('Power Glove', item.player)) ret.prog_items['Power Glove', item.player] += 1
elif 'Shield' in item.name: elif 'Shield' in item.name:
if ret.has('Mirror Shield', item.player): if ret.has('Mirror Shield', item.player):
pass pass
elif ret.has('Red Shield', item.player) and self.difficulty_requirements[item.player].progressive_shield_limit >= 3: elif ret.has('Red Shield', item.player) and self.difficulty_requirements[item.player].progressive_shield_limit >= 3:
ret.prog_items.add(('Mirror Shield', item.player)) ret.prog_items['Mirror Shield', item.player] += 1
elif ret.has('Blue Shield', item.player) and self.difficulty_requirements[item.player].progressive_shield_limit >= 2: elif ret.has('Blue Shield', item.player) and self.difficulty_requirements[item.player].progressive_shield_limit >= 2:
ret.prog_items.add(('Red Shield', item.player)) ret.prog_items['Red Shield', item.player] += 1
elif self.difficulty_requirements[item.player].progressive_shield_limit >= 1: elif self.difficulty_requirements[item.player].progressive_shield_limit >= 1:
ret.prog_items.add(('Blue Shield', item.player)) ret.prog_items['Blue Shield', item.player] += 1
elif 'Bow' in item.name: elif 'Bow' in item.name:
if ret.has('Silver Arrows', item.player): if ret.has('Silver Arrows', item.player):
pass pass
elif ret.has('Bow', item.player) and self.difficulty_requirements[item.player].progressive_bow_limit >= 2: elif ret.has('Bow', item.player) and self.difficulty_requirements[item.player].progressive_bow_limit >= 2:
ret.prog_items.add(('Silver Arrows', item.player)) ret.prog_items['Silver Arrows', item.player] += 1
elif self.difficulty_requirements[item.player].progressive_bow_limit >= 1: elif self.difficulty_requirements[item.player].progressive_bow_limit >= 1:
ret.prog_items.add(('Bow', item.player)) ret.prog_items['Bow', item.player] += 1
elif item.name.startswith('Bottle'): elif item.name.startswith('Bottle'):
if ret.bottle_count(item.player) < self.difficulty_requirements[item.player].progressive_bottle_limit: if ret.bottle_count(item.player) < self.difficulty_requirements[item.player].progressive_bottle_limit:
ret.prog_items.add((item.name, item.player)) ret.prog_items[item.name, item.player] += 1
elif item.advancement or item.smallkey or item.bigkey: elif item.advancement or item.smallkey or item.bigkey:
ret.prog_items.add((item.name, item.player)) ret.prog_items[item.name, item.player] += 1
for item in self.itempool: for item in self.itempool:
soft_collect(item) soft_collect(item)
@@ -384,7 +396,6 @@ class World(object):
prog_locations = [location for location in self.get_locations() if location.item is not None and (location.item.advancement or location.event) and location not in state.locations_checked] prog_locations = [location for location in self.get_locations() if location.item is not None and (location.item.advancement or location.event) and location not in state.locations_checked]
while prog_locations: while prog_locations:
state.sweep_for_crystal_access()
sphere = [] sphere = []
# build up spheres of collection radius. Everything in each sphere is independent from each other in dependencies and only depends on lower spheres # build up spheres of collection radius. Everything in each sphere is independent from each other in dependencies and only depends on lower spheres
for location in prog_locations: for location in prog_locations:
@@ -408,11 +419,10 @@ class World(object):
class CollectionState(object): class CollectionState(object):
def __init__(self, parent): def __init__(self, parent):
self.prog_items = bag() self.prog_items = Counter()
self.world = parent self.world = parent
self.reachable_regions = {player: set() for player in range(1, parent.players + 1)} self.reachable_regions = {player: dict() for player in range(1, parent.players + 1)}
self.colored_regions = {player: {} for player in range(1, parent.players + 1)} self.blocked_connections = {player: dict() for player in range(1, parent.players + 1)}
self.blocked_color_regions = {player: set() for player in range(1, parent.players + 1)}
self.events = [] self.events = []
self.path = {} self.path = {}
self.locations_checked = set() self.locations_checked = set()
@@ -421,88 +431,71 @@ class CollectionState(object):
self.collect(item, True) self.collect(item, True)
def update_reachable_regions(self, player): def update_reachable_regions(self, player):
player_regions = self.world.get_regions(player)
self.stale[player] = False self.stale[player] = False
rrp = self.reachable_regions[player] rrp = self.reachable_regions[player]
ccr = self.colored_regions[player] bc = self.blocked_connections[player]
blocked = self.blocked_color_regions[player]
new_regions = True
reachable_regions_count = len(rrp)
while new_regions:
player_regions = [region for region in player_regions if region not in rrp]
for candidate in player_regions:
if candidate.can_reach_private(self):
rrp.add(candidate)
if candidate.type == RegionType.Dungeon:
c_switch_present = False
for ext in candidate.exits:
door = self.world.check_for_door(ext.name, player)
if door is not None and door.crystal == CrystalBarrier.Either:
c_switch_present = True
break
if c_switch_present:
ccr[candidate] = CrystalBarrier.Either
self.spread_crystal_access(candidate, CrystalBarrier.Either, rrp, ccr, player)
for ext in candidate.exits:
connect = ext.connected_region
if connect in rrp and not ext.can_reach(self):
blocked.add(candidate)
else:
color_type = CrystalBarrier.Null
for entrance in candidate.entrances:
if entrance.parent_region in rrp:
if entrance.can_reach(self):
door = self.world.check_for_door(entrance.name, player)
if door is None or entrance.parent_region.type != RegionType.Dungeon:
color_type |= CrystalBarrier.Orange
elif entrance.parent_region in ccr.keys():
color_type |= (ccr[entrance.parent_region] & (door.crystal or CrystalBarrier.Either))
else:
blocked.add(entrance.parent_region)
if color_type:
ccr[candidate] = color_type
for ext in candidate.exits:
connect = ext.connected_region
if connect in rrp and connect in ccr:
door = self.world.check_for_door(ext.name, player)
if door is not None and not door.blocked:
if ext.can_reach(self):
new_color = ccr[connect] | (ccr[candidate] & (door.crystal or CrystalBarrier.Either))
if new_color != ccr[connect]:
self.spread_crystal_access(candidate, new_color, rrp, ccr, player)
else:
blocked.add(candidate)
new_regions = len(rrp) > reachable_regions_count
reachable_regions_count = len(rrp)
def spread_crystal_access(self, region, crystal, rrp, ccr, player): # init on first call - this can't be done on construction since the regions don't exist yet
queue = deque([(region, crystal)]) start = self.world.get_region('Menu', player)
visited = set() if not start in rrp:
updated = False rrp[start] = CrystalBarrier.Orange
while len(queue) > 0: for exit in start.exits:
region, crystal = queue.popleft() bc[exit] = CrystalBarrier.Orange
visited.add(region)
for ext in region.exits: queue = deque(self.blocked_connections[player].items())
connect = ext.connected_region
if connect is not None and connect.type == RegionType.Dungeon: # run BFS on all connections, and keep track of those blocked by missing items
if connect not in visited and connect in rrp and connect in ccr: while True:
if ext.can_reach(self): try:
door = self.world.check_for_door(ext.name, player) connection, crystal_state = queue.popleft()
new_region = connection.connected_region
if new_region is None or new_region in rrp and (new_region.type != RegionType.Dungeon or (rrp[new_region] & crystal_state) == crystal_state):
bc.pop(connection, None)
elif connection.can_reach(self):
if new_region.type == RegionType.Dungeon:
new_crystal_state = crystal_state
for exit in new_region.exits:
door = exit.door
if door is not None and door.crystal == CrystalBarrier.Either:
new_crystal_state = CrystalBarrier.Either
break
if new_region in rrp:
new_crystal_state |= rrp[new_region]
rrp[new_region] = new_crystal_state
for exit in new_region.exits:
door = exit.door
if door is not None and not door.blocked: if door is not None and not door.blocked:
current_crystal = ccr[connect] door_crystal_state = new_crystal_state & (door.crystal or CrystalBarrier.Either)
new_crystal = current_crystal | (crystal & (door.crystal or CrystalBarrier.Either)) bc[exit] = door_crystal_state
if current_crystal != new_crystal: queue.append((exit, door_crystal_state))
updated = True elif door is None:
ccr[connect] = new_crystal queue.append((exit, new_crystal_state))
queue.append((connect, new_crystal)) else:
return updated new_crystal_state = CrystalBarrier.Orange
rrp[new_region] = new_crystal_state
bc.pop(connection, None)
for exit in new_region.exits:
bc[exit] = new_crystal_state
queue.append((exit, new_crystal_state))
self.path[new_region] = (new_region.name, self.path.get(connection, None))
# Retry connections if the new region can unblock them
if new_region.name in indirect_connections:
new_entrance = self.world.get_entrance(indirect_connections[new_region.name], player)
if new_entrance in bc and new_entrance not in queue and new_entrance.parent_region in rrp:
queue.append((new_entrance, rrp[new_entrance.parent_region]))
except IndexError:
break
def copy(self): def copy(self):
ret = CollectionState(self.world) ret = CollectionState(self.world)
ret.prog_items = self.prog_items.copy() ret.prog_items = self.prog_items.copy()
ret.reachable_regions = {player: copy.copy(self.reachable_regions[player]) for player in range(1, self.world.players + 1)} ret.reachable_regions = {player: copy.copy(self.reachable_regions[player]) for player in range(1, self.world.players + 1)}
ret.colored_regions = {player: copy.copy(self.colored_regions[player]) for player in range(1, self.world.players + 1)} ret.blocked_connections = {player: copy.copy(self.blocked_connections[player]) for player in range(1, self.world.players + 1)}
ret.blocked_color_regions = {player: copy.copy(self.blocked_color_regions[player]) for player in range(1, self.world.players + 1)}
ret.events = copy.copy(self.events) ret.events = copy.copy(self.events)
ret.path = copy.copy(self.path) ret.path = copy.copy(self.path)
ret.locations_checked = copy.copy(self.locations_checked) ret.locations_checked = copy.copy(self.locations_checked)
@@ -523,19 +516,6 @@ class CollectionState(object):
return spot.can_reach(self) return spot.can_reach(self)
def sweep_for_crystal_access(self):
for player, rrp in self.reachable_regions.items():
updated = True
while updated:
if self.stale[player]:
self.update_reachable_regions(player)
updated = False
dungeon_regions = self.blocked_color_regions[player]
ccr = self.colored_regions[player]
for region in dungeon_regions.copy():
if region in ccr.keys():
updated |= self.spread_crystal_access(region, ccr[region], rrp, ccr, player)
self.stale[player] = updated
def sweep_for_events(self, key_only=False, locations=None): def sweep_for_events(self, key_only=False, locations=None):
# this may need improvement # this may need improvement
@@ -554,18 +534,13 @@ class CollectionState(object):
self.collect(event.item, True, event) self.collect(event.item, True, event)
new_locations = len(reachable_events) > checked_locations new_locations = len(reachable_events) > checked_locations
checked_locations = len(reachable_events) checked_locations = len(reachable_events)
if new_locations:
self.sweep_for_crystal_access()
def can_reach_blue(self, region, player): def can_reach_blue(self, region, player):
if region not in self.colored_regions[player].keys(): return region in self.reachable_regions[player] and self.reachable_regions[player][region] in [CrystalBarrier.Blue, CrystalBarrier.Either]
return False
return self.colored_regions[player][region] in [CrystalBarrier.Blue, CrystalBarrier.Either]
def can_reach_orange(self, region, player): def can_reach_orange(self, region, player):
if region not in self.colored_regions[player].keys(): return region in self.reachable_regions[player] and self.reachable_regions[player][region] in [CrystalBarrier.Orange, CrystalBarrier.Either]
return False
return self.colored_regions[player][region] in [CrystalBarrier.Orange, CrystalBarrier.Either]
def _do_not_flood_the_keys(self, reachable_events): def _do_not_flood_the_keys(self, reachable_events):
adjusted_checks = list(reachable_events) adjusted_checks = list(reachable_events)
@@ -584,14 +559,14 @@ class CollectionState(object):
def has(self, item, player, count=1): def has(self, item, player, count=1):
if count == 1: if count == 1:
return (item, player) in self.prog_items return (item, player) in self.prog_items
return self.prog_items.count((item, player)) >= count return self.prog_items[item, player] >= count
def has_key(self, item, player, count=1): def has_key(self, item, player, count=1):
if self.world.retro[player]: if self.world.retro[player]:
return self.can_buy_unlimited('Small Key (Universal)', player) return self.can_buy_unlimited('Small Key (Universal)', player)
if count == 1: if count == 1:
return (item, player) in self.prog_items return (item, player) in self.prog_items
return self.prog_items.count((item, player)) >= count return self.prog_items[item, player] >= count
def can_buy_unlimited(self, item, player): def can_buy_unlimited(self, item, player):
for shop in self.world.shops: for shop in self.world.shops:
@@ -600,7 +575,7 @@ class CollectionState(object):
return False return False
def item_count(self, item, player): def item_count(self, item, player):
return self.prog_items.count((item, player)) return self.prog_items[item, player]
def has_crystals(self, count, player): def has_crystals(self, count, player):
crystals = ['Crystal 1', 'Crystal 2', 'Crystal 3', 'Crystal 4', 'Crystal 5', 'Crystal 6', 'Crystal 7'] crystals = ['Crystal 1', 'Crystal 2', 'Crystal 3', 'Crystal 4', 'Crystal 5', 'Crystal 6', 'Crystal 7']
@@ -634,9 +609,9 @@ class CollectionState(object):
def can_extend_magic(self, player, smallmagic=16, fullrefill=False): #This reflects the total magic Link has, not the total extra he has. def can_extend_magic(self, player, smallmagic=16, fullrefill=False): #This reflects the total magic Link has, not the total extra he has.
basemagic = 8 basemagic = 8
if self.has('Quarter Magic', player): if self.has('Magic Upgrade (1/4)', player):
basemagic = 32 basemagic = 32
elif self.has('Half Magic', player): elif self.has('Magic Upgrade (1/2)', player):
basemagic = 16 basemagic = 16
if self.can_buy_unlimited('Green Potion', player) or self.can_buy_unlimited('Blue Potion', player): if self.can_buy_unlimited('Green Potion', player) or self.can_buy_unlimited('Blue Potion', player):
if self.world.difficulty_adjustments[player] == 'hard' and not fullrefill: if self.world.difficulty_adjustments[player] == 'hard' and not fullrefill:
@@ -657,9 +632,8 @@ class CollectionState(object):
def can_shoot_arrows(self, player): def can_shoot_arrows(self, player):
if self.world.retro[player]: if self.world.retro[player]:
#TODO: need to decide how we want to handle wooden arrows longer-term (a can-buy-a check, or via dynamic shop location) #todo: Non-progressive silvers grant wooden arrows, but progressive bows do not. Always require shop arrows to be safe
#FIXME: Should do something about hard+ ganon only silvers. For the moment, i believe they effective grant wooden, so we are safe return self.has('Bow', player) and self.can_buy_unlimited('Single Arrow', player)
return self.has('Bow', player) and (self.has('Silver Arrows', player) or self.can_buy_unlimited('Single Arrow', player))
return self.has('Bow', player) return self.has('Bow', player)
def can_get_good_bee(self, player): def can_get_good_bee(self, player):
@@ -734,63 +708,63 @@ class CollectionState(object):
if self.has('Golden Sword', item.player): if self.has('Golden Sword', item.player):
pass pass
elif self.has('Tempered Sword', item.player) and self.world.difficulty_requirements[item.player].progressive_sword_limit >= 4: elif self.has('Tempered Sword', item.player) and self.world.difficulty_requirements[item.player].progressive_sword_limit >= 4:
self.prog_items.add(('Golden Sword', item.player)) self.prog_items['Golden Sword', item.player] += 1
changed = True changed = True
elif self.has('Master Sword', item.player) and self.world.difficulty_requirements[item.player].progressive_sword_limit >= 3: elif self.has('Master Sword', item.player) and self.world.difficulty_requirements[item.player].progressive_sword_limit >= 3:
self.prog_items.add(('Tempered Sword', item.player)) self.prog_items['Tempered Sword', item.player] += 1
changed = True changed = True
elif self.has('Fighter Sword', item.player) and self.world.difficulty_requirements[item.player].progressive_sword_limit >= 2: elif self.has('Fighter Sword', item.player) and self.world.difficulty_requirements[item.player].progressive_sword_limit >= 2:
self.prog_items.add(('Master Sword', item.player)) self.prog_items['Master Sword', item.player] += 1
changed = True changed = True
elif self.world.difficulty_requirements[item.player].progressive_sword_limit >= 1: elif self.world.difficulty_requirements[item.player].progressive_sword_limit >= 1:
self.prog_items.add(('Fighter Sword', item.player)) self.prog_items['Fighter Sword', item.player] += 1
changed = True changed = True
elif 'Glove' in item.name: elif 'Glove' in item.name:
if self.has('Titans Mitts', item.player): if self.has('Titans Mitts', item.player):
pass pass
elif self.has('Power Glove', item.player): elif self.has('Power Glove', item.player):
self.prog_items.add(('Titans Mitts', item.player)) self.prog_items['Titans Mitts', item.player] += 1
changed = True changed = True
else: else:
self.prog_items.add(('Power Glove', item.player)) self.prog_items['Power Glove', item.player] += 1
changed = True changed = True
elif 'Shield' in item.name: elif 'Shield' in item.name:
if self.has('Mirror Shield', item.player): if self.has('Mirror Shield', item.player):
pass pass
elif self.has('Red Shield', item.player) and self.world.difficulty_requirements[item.player].progressive_shield_limit >= 3: elif self.has('Red Shield', item.player) and self.world.difficulty_requirements[item.player].progressive_shield_limit >= 3:
self.prog_items.add(('Mirror Shield', item.player)) self.prog_items['Mirror Shield', item.player] += 1
changed = True changed = True
elif self.has('Blue Shield', item.player) and self.world.difficulty_requirements[item.player].progressive_shield_limit >= 2: elif self.has('Blue Shield', item.player) and self.world.difficulty_requirements[item.player].progressive_shield_limit >= 2:
self.prog_items.add(('Red Shield', item.player)) self.prog_items['Red Shield', item.player] += 1
changed = True changed = True
elif self.world.difficulty_requirements[item.player].progressive_shield_limit >= 1: elif self.world.difficulty_requirements[item.player].progressive_shield_limit >= 1:
self.prog_items.add(('Blue Shield', item.player)) self.prog_items['Blue Shield', item.player] += 1
changed = True changed = True
elif 'Bow' in item.name: elif 'Bow' in item.name:
if self.has('Silver Arrows', item.player): if self.has('Silver Arrows', item.player):
pass pass
elif self.has('Bow', item.player): elif self.has('Bow', item.player):
self.prog_items.add(('Silver Arrows', item.player)) self.prog_items['Silver Arrows', item.player] += 1
changed = True changed = True
else: else:
self.prog_items.add(('Bow', item.player)) self.prog_items['Bow', item.player] += 1
changed = True changed = True
elif 'Armor' in item.name: elif 'Armor' in item.name:
if self.has('Red Mail', item.player): if self.has('Red Mail', item.player):
pass pass
elif self.has('Blue Mail', item.player): elif self.has('Blue Mail', item.player):
self.prog_items.add(('Red Mail', item.player)) self.prog_items['Red Mail', item.player] += 1
changed = True changed = True
else: else:
self.prog_items.add(('Blue Mail', item.player)) self.prog_items['Blue Mail', item.player] += 1
changed = True changed = True
elif item.name.startswith('Bottle'): elif item.name.startswith('Bottle'):
if self.bottle_count(item.player) < self.world.difficulty_requirements[item.player].progressive_bottle_limit: if self.bottle_count(item.player) < self.world.difficulty_requirements[item.player].progressive_bottle_limit:
self.prog_items.add((item.name, item.player)) self.prog_items[item.name, item.player] += 1
changed = True changed = True
elif event or item.advancement: elif event or item.advancement:
self.prog_items.add((item.name, item.player)) self.prog_items[item.name, item.player] += 1
changed = True changed = True
self.stale[item.player] = True self.stale[item.player] = True
@@ -839,13 +813,13 @@ class CollectionState(object):
to_remove = None to_remove = None
if to_remove is not None: if to_remove is not None:
try:
self.prog_items.remove((to_remove, item.player))
except ValueError:
return
self.prog_items[to_remove, item.player] -= 1
if self.prog_items[to_remove, item.player] < 1:
del (self.prog_items[to_remove, item.player])
# invalidate caches, nothing can be trusted anymore now # invalidate caches, nothing can be trusted anymore now
self.reachable_regions[item.player] = set() self.reachable_regions[item.player] = dict()
self.blocked_connections[item.player] = set()
self.stale[item.player] = True self.stale[item.player] = True
def __getattr__(self, item): def __getattr__(self, item):
@@ -935,10 +909,11 @@ class Entrance(object):
self.access_rule = lambda state: True self.access_rule = lambda state: True
self.player = player self.player = player
self.door = None self.door = None
self.hide_path = False
def can_reach(self, state): def can_reach(self, state):
if self.parent_region.can_reach(state) and self.access_rule(state): if self.parent_region.can_reach(state) and self.access_rule(state):
if not self in state.path: if not self.hide_path and not self in state.path:
state.path[self] = (self.name, state.path.get(self.parent_region, (self.parent_region.name, None))) state.path[self] = (self.name, state.path.get(self.parent_region, (self.parent_region.name, None)))
return True return True
@@ -1135,8 +1110,7 @@ class PolSlot(Enum):
EastWest = 1 EastWest = 1
Stairs = 2 Stairs = 2
@unique class CrystalBarrier(FastEnum):
class CrystalBarrier(Flag):
Null = 0 # no special requirement Null = 0 # no special requirement
Blue = 1 # blue must be down and explore state set to Blue Blue = 1 # blue must be down and explore state set to Blue
Orange = 2 # orange must be down and explore state set to Orange Orange = 2 # orange must be down and explore state set to Orange

View File

@@ -18,6 +18,7 @@ def ArmosKnightsDefeatRule(state, player):
# Magic amounts are probably a bit overkill # Magic amounts are probably a bit overkill
return ( return (
state.has_blunt_weapon(player) or state.has_blunt_weapon(player) or
state.can_shoot_arrows(player) or
(state.has('Cane of Somaria', player) and state.can_extend_magic(player, 10)) or (state.has('Cane of Somaria', player) and state.can_extend_magic(player, 10)) or
(state.has('Cane of Byrna', player) and state.can_extend_magic(player, 16)) or (state.has('Cane of Byrna', player) and state.can_extend_magic(player, 16)) or
(state.has('Ice Rod', player) and state.can_extend_magic(player, 32)) or (state.has('Ice Rod', player) and state.can_extend_magic(player, 32)) or
@@ -26,18 +27,19 @@ def ArmosKnightsDefeatRule(state, player):
state.has('Red Boomerang', player)) state.has('Red Boomerang', player))
def LanmolasDefeatRule(state, player): def LanmolasDefeatRule(state, player):
# TODO: Allow the canes here?
return ( return (
state.has_blunt_weapon(player) or state.has_blunt_weapon(player) or
state.has('Fire Rod', player) or state.has('Fire Rod', player) or
state.has('Ice Rod', player) or state.has('Ice Rod', player) or
state.has('Cane of Somaria', player) or
state.has('Cane of Byrna', player) or
state.can_shoot_arrows(player)) state.can_shoot_arrows(player))
def MoldormDefeatRule(state, player): def MoldormDefeatRule(state, player):
return state.has_blunt_weapon(player) return state.has_blunt_weapon(player)
def HelmasaurKingDefeatRule(state, player): def HelmasaurKingDefeatRule(state, player):
return state.has_blunt_weapon(player) or state.can_shoot_arrows(player) return state.has_sword(player) or state.can_shoot_arrows(player)
def ArrghusDefeatRule(state, player): def ArrghusDefeatRule(state, player):
if not state.has('Hookshot', player): if not state.has('Hookshot', player):
@@ -95,7 +97,11 @@ def VitreousDefeatRule(state, player):
def TrinexxDefeatRule(state, player): def TrinexxDefeatRule(state, player):
if not (state.has('Fire Rod', player) and state.has('Ice Rod', player)): if not (state.has('Fire Rod', player) and state.has('Ice Rod', player)):
return False return False
return state.has('Hammer', player) or state.has_beam_sword(player) or (state.has_sword(player) and state.can_extend_magic(player, 32)) return (state.has('Hammer', player) or
state.has('Golden Sword', player) or
state.has('Tempered Sword', player) or
(state.has('Master Sword', player) and state.can_extend_magic(player, 16)) or
(state.has_sword(player) and state.can_extend_magic(player, 32)))
def AgahnimDefeatRule(state, player): def AgahnimDefeatRule(state, player):
return state.has_sword(player) or state.has('Hammer', player) or state.has('Bug Catching Net', player) return state.has_sword(player) or state.has('Hammer', player) or state.has('Bug Catching Net', player)

View File

@@ -365,7 +365,7 @@ def main_dungeon_generation(dungeon_builders, recombinant_builders, connections_
name = ' '.join(builder.name.split(' ')[:-1]) name = ' '.join(builder.name.split(' ')[:-1])
origin_list = list(builder.entrance_list) origin_list = list(builder.entrance_list)
find_enabled_origins(builder.sectors, enabled_entrances, origin_list, entrances_map, name) find_enabled_origins(builder.sectors, enabled_entrances, origin_list, entrances_map, name)
if len(origin_list) <= 0 or not pre_validate(builder, origin_list, world, player): if len(origin_list) <= 0 or not pre_validate(builder, origin_list, split_dungeon, world, player):
if last_key == builder.name or loops > 1000: if last_key == builder.name or loops > 1000:
origin_name = world.get_region(origin_list[0], player).entrances[0].parent_region.name if len(origin_list) > 0 else 'no origin' origin_name = world.get_region(origin_list[0], player).entrances[0].parent_region.name if len(origin_list) > 0 else 'no origin'
raise Exception('Infinite loop detected for "%s" located at %s' % (builder.name, origin_name)) raise Exception('Infinite loop detected for "%s" located at %s' % (builder.name, origin_name))
@@ -398,7 +398,7 @@ def determine_entrance_list(world, player):
region = world.get_region(region_name, player) region = world.get_region(region_name, player)
for ent in region.entrances: for ent in region.entrances:
parent = ent.parent_region parent = ent.parent_region
if parent.type != RegionType.Dungeon or parent.name == 'Sewer Drop': if (parent.type != RegionType.Dungeon and parent.name != 'Menu') or parent.name == 'Sewer Drop':
if parent.name not in world.inaccessible_regions[player]: if parent.name not in world.inaccessible_regions[player]:
entrance_map[key].append(region_name) entrance_map[key].append(region_name)
else: else:

View File

@@ -41,7 +41,7 @@ Intr = DoorType.Interior
def create_doors(world, player): def create_doors(world, player):
world.doors += [ doors = [
# hyrule castle # hyrule castle
create_door(player, 'Hyrule Castle Lobby W', Nrml).dir(We, 0x61, Mid, High).toggler().pos(0), create_door(player, 'Hyrule Castle Lobby W', Nrml).dir(We, 0x61, Mid, High).toggler().pos(0),
create_door(player, 'Hyrule Castle Lobby E', Nrml).dir(Ea, 0x61, Mid, High).toggler().pos(2), create_door(player, 'Hyrule Castle Lobby E', Nrml).dir(Ea, 0x61, Mid, High).toggler().pos(2),
@@ -1071,6 +1071,10 @@ def create_doors(world, player):
create_door(player, 'GT Brightly Lit Hall NW', Nrml).dir(No, 0x1d, Left, High).big_key().pos(0), create_door(player, 'GT Brightly Lit Hall NW', Nrml).dir(No, 0x1d, Left, High).big_key().pos(0),
create_door(player, 'GT Agahnim 2 SW', Nrml).dir(So, 0x0d, Left, High).no_exit().trap(0x4).pos(0) create_door(player, 'GT Agahnim 2 SW', Nrml).dir(So, 0x0d, Left, High).no_exit().trap(0x4).pos(0)
] ]
world.doors += doors
world.initialize_doors(doors)
create_paired_doors(world, player) create_paired_doors(world, player)
# swamp events # swamp events

View File

@@ -25,7 +25,7 @@ class GraphPiece:
# Dungeons shouldn't be generated until all entrances are appropriately accessible # Dungeons shouldn't be generated until all entrances are appropriately accessible
def pre_validate(builder, entrance_region_names, world, player): def pre_validate(builder, entrance_region_names, split_dungeon, world, player):
entrance_regions = convert_regions(entrance_region_names, world, player) entrance_regions = convert_regions(entrance_region_names, world, player)
proposed_map = {} proposed_map = {}
doors_to_connect = {} doors_to_connect = {}
@@ -36,7 +36,7 @@ def pre_validate(builder, entrance_region_names, world, player):
for door in sector.outstanding_doors: for door in sector.outstanding_doors:
doors_to_connect[door.name] = door doors_to_connect[door.name] = door
all_regions.update(sector.regions) all_regions.update(sector.regions)
bk_needed = bk_needed or determine_if_bk_needed(sector, False, world, player) bk_needed = bk_needed or determine_if_bk_needed(sector, split_dungeon, world, player)
bk_special = bk_special or check_for_special(sector) bk_special = bk_special or check_for_special(sector)
dungeon, hangers, hooks = gen_dungeon_info(builder.name, builder.sectors, entrance_regions, proposed_map, dungeon, hangers, hooks = gen_dungeon_info(builder.name, builder.sectors, entrance_regions, proposed_map,
doors_to_connect, bk_needed, bk_special, world, player) doors_to_connect, bk_needed, bk_special, world, player)

View File

@@ -1204,7 +1204,9 @@ def link_inverted_entrances(world, player):
sanc_doors = [door for door in Inverted_Dark_Sanctuary_Doors if door in bomb_shop_doors] sanc_doors = [door for door in Inverted_Dark_Sanctuary_Doors if door in bomb_shop_doors]
sanc_door = random.choice(sanc_doors) sanc_door = random.choice(sanc_doors)
bomb_shop_doors.remove(sanc_door) bomb_shop_doors.remove(sanc_door)
connect_doors(world, [sanc_door], ['Inverted Dark Sanctuary'], player)
connect_entrance(world, sanc_door, 'Inverted Dark Sanctuary', player)
world.get_entrance('Inverted Dark Sanctuary Exit', player).connect(world.get_entrance(sanc_door, player).parent_region)
lw_dm_entrances = ['Paradox Cave (Bottom)', 'Paradox Cave (Middle)', 'Paradox Cave (Top)', 'Old Man House (Bottom)', lw_dm_entrances = ['Paradox Cave (Bottom)', 'Paradox Cave (Middle)', 'Paradox Cave (Top)', 'Old Man House (Bottom)',
'Fairy Ascension Cave (Bottom)', 'Fairy Ascension Cave (Top)', 'Spiral Cave (Bottom)', 'Old Man Cave (East)', 'Fairy Ascension Cave (Bottom)', 'Fairy Ascension Cave (Top)', 'Spiral Cave (Bottom)', 'Old Man Cave (East)',
@@ -1279,7 +1281,8 @@ def link_inverted_entrances(world, player):
sanc_doors = [door for door in Inverted_Dark_Sanctuary_Doors if door in dw_entrances] sanc_doors = [door for door in Inverted_Dark_Sanctuary_Doors if door in dw_entrances]
sanc_door = random.choice(sanc_doors) sanc_door = random.choice(sanc_doors)
dw_entrances.remove(sanc_door) dw_entrances.remove(sanc_door)
connect_doors(world, [sanc_door], ['Inverted Dark Sanctuary'], player) connect_entrance(world, sanc_door, 'Inverted Dark Sanctuary', player)
world.get_entrance('Inverted Dark Sanctuary Exit', player).connect(world.get_entrance(sanc_door, player).parent_region)
# tavern back door cannot be shuffled yet # tavern back door cannot be shuffled yet
connect_doors(world, ['Tavern North'], ['Tavern'], player) connect_doors(world, ['Tavern North'], ['Tavern'], player)
@@ -1410,7 +1413,8 @@ def link_inverted_entrances(world, player):
sanc_doors = [door for door in Inverted_Dark_Sanctuary_Doors if door in dw_entrances] sanc_doors = [door for door in Inverted_Dark_Sanctuary_Doors if door in dw_entrances]
sanc_door = random.choice(sanc_doors) sanc_door = random.choice(sanc_doors)
dw_entrances.remove(sanc_door) dw_entrances.remove(sanc_door)
connect_doors(world, [sanc_door], ['Inverted Dark Sanctuary'], player) connect_entrance(world, sanc_door, 'Inverted Dark Sanctuary', player)
world.get_entrance('Inverted Dark Sanctuary Exit', player).connect(world.get_entrance(sanc_door, player).parent_region)
# place old man house # place old man house
# no dw must exits in inverted, but we randomize whether cave is in light or dark world # no dw must exits in inverted, but we randomize whether cave is in light or dark world
@@ -1547,7 +1551,8 @@ def link_inverted_entrances(world, player):
sanc_doors = [door for door in Inverted_Dark_Sanctuary_Doors if door in entrances] sanc_doors = [door for door in Inverted_Dark_Sanctuary_Doors if door in entrances]
sanc_door = random.choice(sanc_doors) sanc_door = random.choice(sanc_doors)
entrances.remove(sanc_door) entrances.remove(sanc_door)
connect_doors(world, [sanc_door], ['Inverted Dark Sanctuary'], player) connect_entrance(world, sanc_door, 'Inverted Dark Sanctuary', player)
world.get_entrance('Inverted Dark Sanctuary Exit', player).connect(world.get_entrance(sanc_door, player).parent_region)
# tavern back door cannot be shuffled yet # tavern back door cannot be shuffled yet
connect_doors(world, ['Tavern North'], ['Tavern'], player) connect_doors(world, ['Tavern North'], ['Tavern'], player)
@@ -1680,7 +1685,8 @@ def link_inverted_entrances(world, player):
sanc_door = random.choice(sanc_doors) sanc_door = random.choice(sanc_doors)
entrances.remove(sanc_door) entrances.remove(sanc_door)
doors.remove(sanc_door) doors.remove(sanc_door)
connect_doors(world, [sanc_door], ['Inverted Dark Sanctuary'], player) connect_entrance(world, sanc_door, 'Inverted Dark Sanctuary', player)
world.get_entrance('Inverted Dark Sanctuary Exit', player).connect(world.get_entrance(sanc_door, player).parent_region)
# now let's deal with mandatory reachable stuff # now let's deal with mandatory reachable stuff
def extract_reachable_exit(cavelist): def extract_reachable_exit(cavelist):
@@ -2298,6 +2304,8 @@ Bomb_Shop_Multi_Cave_Doors = ['Hyrule Castle Entrance (South)',
'Death Mountain Return Cave (East)', 'Death Mountain Return Cave (East)',
'Death Mountain Return Cave (West)', 'Death Mountain Return Cave (West)',
'Spectacle Rock Cave Peak', 'Spectacle Rock Cave Peak',
'Spectacle Rock Cave',
'Spectacle Rock Cave (Bottom)',
'Paradox Cave (Bottom)', 'Paradox Cave (Bottom)',
'Paradox Cave (Middle)', 'Paradox Cave (Middle)',
'Paradox Cave (Top)', 'Paradox Cave (Top)',
@@ -2638,8 +2646,6 @@ Inverted_Bomb_Shop_Multi_Cave_Doors = ['Hyrule Castle Entrance (South)',
'Death Mountain Return Cave (East)', 'Death Mountain Return Cave (East)',
'Death Mountain Return Cave (West)', 'Death Mountain Return Cave (West)',
'Spectacle Rock Cave Peak', 'Spectacle Rock Cave Peak',
'Spectacle Rock Cave',
'Spectacle Rock Cave (Bottom)',
'Paradox Cave (Bottom)', 'Paradox Cave (Bottom)',
'Paradox Cave (Middle)', 'Paradox Cave (Middle)',
'Paradox Cave (Top)', 'Paradox Cave (Top)',
@@ -2814,7 +2820,10 @@ Isolated_LH_Doors = ['Kings Grave',
'Turtle Rock Isolated Ledge Entrance'] 'Turtle Rock Isolated Ledge Entrance']
# these are connections that cannot be shuffled and always exist. They link together separate parts of the world we need to divide into regions # these are connections that cannot be shuffled and always exist. They link together separate parts of the world we need to divide into regions
mandatory_connections = [('Lake Hylia Central Island Pier', 'Lake Hylia Central Island'), mandatory_connections = [('Links House S&Q', 'Links House'),
('Sanctuary S&Q', 'Sanctuary'),
('Old Man S&Q', 'Old Man House'),
('Lake Hylia Central Island Pier', 'Lake Hylia Central Island'),
('Lake Hylia Central Island Teleporter', 'Dark Lake Hylia Central Island'), ('Lake Hylia Central Island Teleporter', 'Dark Lake Hylia Central Island'),
('Zoras River', 'Zoras River'), ('Zoras River', 'Zoras River'),
('Kings Grave Outer Rocks', 'Kings Grave Area'), ('Kings Grave Outer Rocks', 'Kings Grave Area'),
@@ -2923,7 +2932,11 @@ mandatory_connections = [('Lake Hylia Central Island Pier', 'Lake Hylia Central
('Pyramid Drop', 'East Dark World') ('Pyramid Drop', 'East Dark World')
] ]
inverted_mandatory_connections = [('Lake Hylia Central Island Pier', 'Lake Hylia Central Island'), inverted_mandatory_connections = [('Links House S&Q', 'Inverted Links House'),
('Dark Sanctuary S&Q', 'Inverted Dark Sanctuary'),
('Old Man S&Q', 'Old Man House'),
('Castle Ledge S&Q', 'Hyrule Castle Ledge'),
('Lake Hylia Central Island Pier', 'Lake Hylia Central Island'),
('Lake Hylia Island', 'Lake Hylia Island'), ('Lake Hylia Island', 'Lake Hylia Island'),
('Zoras River', 'Zoras River'), ('Zoras River', 'Zoras River'),
('Kings Grave Outer Rocks', 'Kings Grave Area'), ('Kings Grave Outer Rocks', 'Kings Grave Area'),
@@ -3352,6 +3365,7 @@ inverted_default_connections = [('Waterfall of Wishing', 'Waterfall of Wishing'
('Inverted Links House Exit', 'South Dark World'), ('Inverted Links House Exit', 'South Dark World'),
('Inverted Big Bomb Shop', 'Inverted Big Bomb Shop'), ('Inverted Big Bomb Shop', 'Inverted Big Bomb Shop'),
('Inverted Dark Sanctuary', 'Inverted Dark Sanctuary'), ('Inverted Dark Sanctuary', 'Inverted Dark Sanctuary'),
('Inverted Dark Sanctuary Exit', 'West Dark World'),
('Old Man Cave (West)', 'Bumper Cave'), ('Old Man Cave (West)', 'Bumper Cave'),
('Old Man Cave (East)', 'Death Mountain Return Cave'), ('Old Man Cave (East)', 'Death Mountain Return Cave'),
('Old Man Cave Exit (West)', 'West Dark World'), ('Old Man Cave Exit (West)', 'West Dark World'),
@@ -3485,11 +3499,23 @@ inverted_default_dungeon_connections = [('Desert Palace Entrance (South)', 'Dese
] ]
# format: # format:
indirect_connections = {
'Turtle Rock (Top)': 'Turtle Rock',
'East Dark World': 'Pyramid Fairy',
'Big Bomb Shop': 'Pyramid Fairy',
'Dark Desert': 'Pyramid Fairy',
'West Dark World': 'Pyramid Fairy',
'South Dark World': 'Pyramid Fairy',
'Light World': 'Pyramid Fairy',
'Old Man Cave': 'Old Man S&Q'
}
# Key=Name # Key=Name
# addr = (door_index, exitdata) # multiexit # addr = (door_index, exitdata) # multiexit
# | ([addr], None) # holes # | ([addr], None) # holes
# exitdata = (room_id, ow_area, vram_loc, scroll_y, scroll_x, link_y, link_x, camera_y, camera_x, unknown_1, unknown_2, door_1, door_2) # exitdata = (room_id, ow_area, vram_loc, scroll_y, scroll_x, link_y, link_x, camera_y, camera_x, unknown_1, unknown_2, door_1, door_2)
# ToDo somehow merge this with creation of the locations
# ToDo somehow merge this with creation of the locations # ToDo somehow merge this with creation of the locations
door_addresses = {'Links House': (0x00, (0x0104, 0x2c, 0x0506, 0x0a9a, 0x0832, 0x0ae8, 0x08b8, 0x0b07, 0x08bf, 0x06, 0xfe, 0x0816, 0x0000)), door_addresses = {'Links House': (0x00, (0x0104, 0x2c, 0x0506, 0x0a9a, 0x0832, 0x0ae8, 0x08b8, 0x0b07, 0x08bf, 0x06, 0xfe, 0x0816, 0x0000)),
'Inverted Big Bomb Shop': (0x00, (0x0104, 0x2c, 0x0506, 0x0a9a, 0x0832, 0x0ae8, 0x08b8, 0x0b07, 0x08bf, 0x06, 0xfe, 0x0816, 0x0000)), 'Inverted Big Bomb Shop': (0x00, (0x0104, 0x2c, 0x0506, 0x0a9a, 0x0832, 0x0ae8, 0x08b8, 0x0b07, 0x08bf, 0x06, 0xfe, 0x0816, 0x0000)),

View File

@@ -6,6 +6,7 @@ from Regions import create_lw_region, create_dw_region, create_cave_region, crea
def create_inverted_regions(world, player): def create_inverted_regions(world, player):
world.regions += [ world.regions += [
create_dw_region(player, 'Menu', None, ['Links House S&Q', 'Dark Sanctuary S&Q', 'Old Man S&Q', 'Castle Ledge S&Q']),
create_lw_region(player, 'Light World', ['Mushroom', 'Bottle Merchant', 'Flute Spot', 'Sunken Treasure', 'Purple Chest', 'Bombos Tablet'], create_lw_region(player, 'Light World', ['Mushroom', 'Bottle Merchant', 'Flute Spot', 'Sunken Treasure', 'Purple Chest', 'Bombos Tablet'],
["Blinds Hideout", "Hyrule Castle Secret Entrance Drop", 'Kings Grave Outer Rocks', 'Dam', ["Blinds Hideout", "Hyrule Castle Secret Entrance Drop", 'Kings Grave Outer Rocks', 'Dam',
'Inverted Big Bomb Shop', 'Tavern North', 'Chicken House', 'Aginahs Cave', 'Sahasrahlas Hut', 'Kakariko Well Drop', 'Kakariko Well Cave', 'Inverted Big Bomb Shop', 'Tavern North', 'Chicken House', 'Aginahs Cave', 'Sahasrahlas Hut', 'Kakariko Well Drop', 'Kakariko Well Cave',
@@ -176,7 +177,7 @@ def create_inverted_regions(world, player):
create_cave_region(player, 'C-Shaped House', 'a house with a chest', ['C-Shaped House']), create_cave_region(player, 'C-Shaped House', 'a house with a chest', ['C-Shaped House']),
create_cave_region(player, 'Chest Game', 'a game of 16 chests', ['Chest Game']), create_cave_region(player, 'Chest Game', 'a game of 16 chests', ['Chest Game']),
create_cave_region(player, 'Red Shield Shop', 'the rare shop'), create_cave_region(player, 'Red Shield Shop', 'the rare shop'),
create_cave_region(player, 'Inverted Dark Sanctuary', 'a storyteller'), create_cave_region(player, 'Inverted Dark Sanctuary', 'a storyteller', None, ['Inverted Dark Sanctuary Exit']),
create_cave_region(player, 'Bumper Cave', 'a connector', None, ['Bumper Cave Exit (Bottom)', 'Bumper Cave Exit (Top)']), create_cave_region(player, 'Bumper Cave', 'a connector', None, ['Bumper Cave Exit (Bottom)', 'Bumper Cave Exit (Top)']),
create_dw_region(player, 'Skull Woods Forest', None, ['Skull Woods First Section Hole (East)', 'Skull Woods First Section Hole (West)', 'Skull Woods First Section Hole (North)', create_dw_region(player, 'Skull Woods Forest', None, ['Skull Woods First Section Hole (East)', 'Skull Woods First Section Hole (West)', 'Skull Woods First Section Hole (North)',
'Skull Woods First Section Door', 'Skull Woods Second Section Door (East)']), 'Skull Woods First Section Door', 'Skull Woods Second Section Door (East)']),

View File

@@ -371,7 +371,6 @@ def copy_world(world):
create_inverted_regions(ret, player) create_inverted_regions(ret, player)
create_dungeon_regions(ret, player) create_dungeon_regions(ret, player)
create_shops(ret, player) create_shops(ret, player)
create_doors(ret, player)
create_rooms(ret, player) create_rooms(ret, player)
create_dungeons(ret, player) create_dungeons(ret, player)
@@ -418,6 +417,10 @@ def copy_world(world):
ret.state.stale = {player: True for player in range(1, world.players + 1)} ret.state.stale = {player: True for player in range(1, world.players + 1)}
ret.doors = world.doors ret.doors = world.doors
for door in ret.doors:
entrance = ret.check_for_entrance(door.name, door.player)
if entrance is not None:
entrance.door = door
ret.paired_doors = world.paired_doors ret.paired_doors = world.paired_doors
ret.rooms = world.rooms ret.rooms = world.rooms
ret.inaccessible_regions = world.inaccessible_regions ret.inaccessible_regions = world.inaccessible_regions
@@ -469,7 +472,6 @@ def create_playthrough(world):
logging.getLogger('').debug(world.fish.translate("cli","cli","building.collection.spheres")) logging.getLogger('').debug(world.fish.translate("cli","cli","building.collection.spheres"))
while sphere_candidates: while sphere_candidates:
state.sweep_for_events(key_only=True) state.sweep_for_events(key_only=True)
state.sweep_for_crystal_access()
sphere = [] sphere = []
# build up spheres of collection radius. Everything in each sphere is independent from each other in dependencies and only depends on lower spheres # build up spheres of collection radius. Everything in each sphere is independent from each other in dependencies and only depends on lower spheres
@@ -487,7 +489,7 @@ def create_playthrough(world):
logging.getLogger('').debug(world.fish.translate("cli","cli","building.calculating.spheres"), len(collection_spheres), len(sphere), len(prog_locations)) logging.getLogger('').debug(world.fish.translate("cli","cli","building.calculating.spheres"), len(collection_spheres), len(sphere), len(prog_locations))
if not sphere: if not sphere:
logging.getLogger('').debug(world.fish.translate("cli","cli","cannot.reach.items"), [world.fish.translate("cli","cli","cannot.reach.item") % (location.item.name, location.item.player, location.name, location.player) for location in sphere_candidates]) logging.getLogger('').error(world.fish.translate("cli","cli","cannot.reach.items"), [world.fish.translate("cli","cli","cannot.reach.item") % (location.item.name, location.item.player, location.name, location.player) for location in sphere_candidates])
if any([world.accessibility[location.item.player] != 'none' for location in sphere_candidates]): if any([world.accessibility[location.item.player] != 'none' for location in sphere_candidates]):
raise RuntimeError(world.fish.translate("cli","cli","cannot.reach.progression")) raise RuntimeError(world.fish.translate("cli","cli","cannot.reach.progression"))
else: else:
@@ -531,7 +533,6 @@ def create_playthrough(world):
collection_spheres = [] collection_spheres = []
while required_locations: while required_locations:
state.sweep_for_events(key_only=True) state.sweep_for_events(key_only=True)
state.sweep_for_crystal_access()
sphere = list(filter(lambda loc: state.can_reach(loc) and state.not_flooding_a_key(world, loc), required_locations)) sphere = list(filter(lambda loc: state.can_reach(loc) and state.not_flooding_a_key(world, loc), required_locations))

View File

@@ -4,6 +4,7 @@ from BaseClasses import Region, Location, Entrance, RegionType, Shop, ShopType
def create_regions(world, player): def create_regions(world, player):
world.regions += [ world.regions += [
create_lw_region(player, 'Menu', None, ['Links House S&Q', 'Sanctuary S&Q', 'Old Man S&Q']),
create_lw_region(player, 'Light World', ['Mushroom', 'Bottle Merchant', 'Flute Spot', 'Sunken Treasure', 'Purple Chest'], create_lw_region(player, 'Light World', ['Mushroom', 'Bottle Merchant', 'Flute Spot', 'Sunken Treasure', 'Purple Chest'],
["Blinds Hideout", "Hyrule Castle Secret Entrance Drop", 'Zoras River', 'Kings Grave Outer Rocks', 'Dam', ["Blinds Hideout", "Hyrule Castle Secret Entrance Drop", 'Zoras River', 'Kings Grave Outer Rocks', 'Dam',
'Links House', 'Tavern North', 'Chicken House', 'Aginahs Cave', 'Sahasrahlas Hut', 'Kakariko Well Drop', 'Kakariko Well Cave', 'Links House', 'Tavern North', 'Chicken House', 'Aginahs Cave', 'Sahasrahlas Hut', 'Kakariko Well Drop', 'Kakariko Well Cave',

3
Rom.py
View File

@@ -2122,7 +2122,8 @@ def set_inverted_mode(world, player, rom):
rom.write_bytes(snes_to_pc(0x06B2AB), [0xF0, 0xE1, 0x05]) rom.write_bytes(snes_to_pc(0x06B2AB), [0xF0, 0xE1, 0x05])
def patch_shuffled_dark_sanc(world, rom, player): def patch_shuffled_dark_sanc(world, rom, player):
dark_sanc_entrance = str(world.get_region('Inverted Dark Sanctuary', player).entrances[0].name) dark_sanc = world.get_region('Inverted Dark Sanctuary', player)
dark_sanc_entrance = str([i for i in dark_sanc.entrances if i.parent_region.name != 'Menu'][0].name)
room_id, ow_area, vram_loc, scroll_y, scroll_x, link_y, link_x, camera_y, camera_x, unknown_1, unknown_2, door_1, door_2 = door_addresses[dark_sanc_entrance][1] room_id, ow_area, vram_loc, scroll_y, scroll_x, link_y, link_x, camera_y, camera_x, unknown_1, unknown_2, door_1, door_2 = door_addresses[dark_sanc_entrance][1]
door_index = door_addresses[str(dark_sanc_entrance)][0] door_index = door_addresses[str(dark_sanc_entrance)][0]

View File

@@ -1,27 +1,18 @@
import logging import logging
from BaseClasses import CollectionState, RegionType, DoorType from collections import deque
from BaseClasses import CollectionState, RegionType, DoorType, Entrance
from Regions import key_only_locations from Regions import key_only_locations
from RoomData import DoorKind from RoomData import DoorKind
from collections import deque
def set_rules(world, player): def set_rules(world, player):
if world.logic[player] == 'nologic': if world.logic[player] == 'nologic':
logging.getLogger('').info('WARNING! Seeds generated under this logic often require major glitches and may be impossible!') logging.getLogger('').info('WARNING! Seeds generated under this logic often require major glitches and may be impossible!')
if world.mode[player] != 'inverted': world.get_region('Menu', player).can_reach_private = lambda state: True
world.get_region('Links House', player).can_reach_private = lambda state: True for exit in world.get_region('Menu', player).exits:
world.get_region('Sanctuary', player).can_reach_private = lambda state: True exit.hide_path = True
old_rule = world.get_region('Old Man House', player).can_reach
world.get_region('Old Man House', player).can_reach_private = lambda state: state.can_reach('Old Man', 'Location', player) or old_rule(state)
return
else:
world.get_region('Inverted Links House', player).can_reach_private = lambda state: True
world.get_region('Inverted Dark Sanctuary', player).entrances[0].parent_region.can_reach_private = lambda state: True
if world.shuffle[player] != 'vanilla':
old_rule = world.get_region('Old Man House', player).can_reach
world.get_region('Old Man House', player).can_reach_private = lambda state: state.can_reach('Old Man', 'Location', player) or old_rule(state)
world.get_region('Hyrule Castle Ledge', player).can_reach_private = lambda state: True
return return
global_rules(world, player) global_rules(world, player)
@@ -113,8 +104,11 @@ def global_rules(world, player):
add_item_rule(world.get_location('Ganon', player), lambda item: item.name == 'Triforce' and item.player == player) add_item_rule(world.get_location('Ganon', player), lambda item: item.name == 'Triforce' and item.player == player)
# we can s&q to the old man house after we rescue him. This may be somewhere completely different if caves are shuffled! # we can s&q to the old man house after we rescue him. This may be somewhere completely different if caves are shuffled!
old_rule = world.get_region('Old Man House', player).can_reach_private world.get_region('Menu', player).can_reach_private = lambda state: True
world.get_region('Old Man House', player).can_reach_private = lambda state: state.can_reach('Old Man', 'Location', player) or old_rule(state) for exit in world.get_region('Menu', player).exits:
exit.hide_path = True
set_rule(world.get_entrance('Old Man S&Q', player), lambda state: state.can_reach('Old Man', 'Location', player))
set_rule(world.get_location('Sunken Treasure', player), lambda state: state.has('Open Floodgate', player)) set_rule(world.get_location('Sunken Treasure', player), lambda state: state.has('Open Floodgate', player))
set_rule(world.get_location('Dark Blacksmith Ruins', player), lambda state: state.has('Return Smith', player)) set_rule(world.get_location('Dark Blacksmith Ruins', player), lambda state: state.has('Return Smith', player))
@@ -168,6 +162,13 @@ def global_rules(world, player):
set_defeat_dungeon_boss_rule(world.get_location('Tower of Hera - Boss', player)) set_defeat_dungeon_boss_rule(world.get_location('Tower of Hera - Boss', player))
set_defeat_dungeon_boss_rule(world.get_location('Tower of Hera - Prize', player)) set_defeat_dungeon_boss_rule(world.get_location('Tower of Hera - Prize', player))
# Castle Tower
set_rule(world.get_entrance('Tower Gold Knights SW', player), lambda state: state.can_kill_most_things(player))
set_rule(world.get_entrance('Tower Gold Knights EN', player), lambda state: state.can_kill_most_things(player))
set_rule(world.get_entrance('Tower Dark Archers WN', player), lambda state: state.can_kill_most_things(player))
set_rule(world.get_entrance('Tower Red Spears WN', player), lambda state: state.can_kill_most_things(player))
set_rule(world.get_entrance('Tower Red Guards EN', player), lambda state: state.can_kill_most_things(player))
set_rule(world.get_entrance('Tower Red Guards SW', player), lambda state: state.can_kill_most_things(player))
set_rule(world.get_entrance('Tower Altar NW', player), lambda state: state.has_sword(player)) set_rule(world.get_entrance('Tower Altar NW', player), lambda state: state.has_sword(player))
set_defeat_dungeon_boss_rule(world.get_location('Agahnim 1', player)) set_defeat_dungeon_boss_rule(world.get_location('Agahnim 1', player))
@@ -207,6 +208,8 @@ def global_rules(world, player):
set_rule(world.get_entrance('Swamp Drain Left Up Stairs', player), lambda state: state.has('Drained Swamp', player) or state.has('Flippers', player)) set_rule(world.get_entrance('Swamp Drain Left Up Stairs', player), lambda state: state.has('Drained Swamp', player) or state.has('Flippers', player))
set_rule(world.get_location('Swamp Palace - Flooded Room - Left', player), lambda state: state.has('Drained Swamp', player)) set_rule(world.get_location('Swamp Palace - Flooded Room - Left', player), lambda state: state.has('Drained Swamp', player))
set_rule(world.get_location('Swamp Palace - Flooded Room - Right', player), lambda state: state.has('Drained Swamp', player)) set_rule(world.get_location('Swamp Palace - Flooded Room - Right', player), lambda state: state.has('Drained Swamp', player))
set_rule(world.get_entrance('Swamp Flooded Spot Ladder', player), lambda state: state.has('Flippers', player) or state.has('Drained Swamp', player))
set_rule(world.get_entrance('Swamp Drain Left Up Stairs', player), lambda state: state.has('Flippers', player) or state.has('Drained Swamp', player))
set_rule(world.get_entrance('Swamp Waterway NW', player), lambda state: state.has('Flippers', player)) set_rule(world.get_entrance('Swamp Waterway NW', player), lambda state: state.has('Flippers', player))
set_rule(world.get_entrance('Swamp Waterway N', player), lambda state: state.has('Flippers', player)) set_rule(world.get_entrance('Swamp Waterway N', player), lambda state: state.has('Flippers', player))
set_rule(world.get_entrance('Swamp Waterway NE', player), lambda state: state.has('Flippers', player)) set_rule(world.get_entrance('Swamp Waterway NE', player), lambda state: state.has('Flippers', player))
@@ -386,16 +389,6 @@ def global_rules(world, player):
def default_rules(world, player): def default_rules(world, player):
if world.mode[player] == 'standard':
# Links house requires reaching Sanc so skipping that chest isn't a softlock.
world.get_region('Hyrule Castle Secret Entrance', player).can_reach_private = lambda state: True
old_rule = world.get_region('Links House', player).can_reach_private
world.get_region('Links House', player).can_reach_private = lambda state: state.has('Zelda Delivered', player) or old_rule(state)
else:
# these are default save&quit points and always accessible
world.get_region('Links House', player).can_reach_private = lambda state: True
world.get_region('Sanctuary', player).can_reach_private = lambda state: True
# overworld requirements # overworld requirements
set_rule(world.get_entrance('Kings Grave', player), lambda state: state.has_Boots(player)) set_rule(world.get_entrance('Kings Grave', player), lambda state: state.has_Boots(player))
set_rule(world.get_entrance('Kings Grave Outer Rocks', player), lambda state: state.can_lift_heavy_rocks(player)) set_rule(world.get_entrance('Kings Grave Outer Rocks', player), lambda state: state.can_lift_heavy_rocks(player))
@@ -508,12 +501,7 @@ def default_rules(world, player):
def inverted_rules(world, player): def inverted_rules(world, player):
# s&q regions. link's house entrance is set to true so the filler knows the chest inside can always be reached # s&q regions. link's house entrance is set to true so the filler knows the chest inside can always be reached
world.get_region('Inverted Links House', player).can_reach_private = lambda state: True set_rule(world.get_entrance('Castle Ledge S&Q', player), lambda state: state.has_Mirror(player) and state.has('Beat Agahnim 1', player))
world.get_region('Inverted Links House', player).entrances[0].can_reach = lambda state: True
world.get_region('Inverted Dark Sanctuary', player).entrances[0].parent_region.can_reach_private = lambda state: True
old_rule = world.get_region('Hyrule Castle Ledge', player).can_reach_private
world.get_region('Hyrule Castle Ledge', player).can_reach_private = lambda state: (state.has_Mirror(player) and state.has('Beat Agahnim 1', player) and state.can_reach_light_world(player)) or old_rule(state)
# overworld requirements # overworld requirements
set_rule(world.get_location('Maze Race', player), lambda state: state.has_Pearl(player)) set_rule(world.get_location('Maze Race', player), lambda state: state.has_Pearl(player))
@@ -826,7 +814,19 @@ std_kill_rooms = {
} # all trap rooms? } # all trap rooms?
def add_connection(parent_name, target_name, entrance_name, world, player):
parent = world.get_region(parent_name, player)
target = world.get_region(target_name, player)
connection = Entrance(player, entrance_name, parent)
parent.exits.append(connection)
connection.connect(target)
def standard_rules(world, player): def standard_rules(world, player):
add_connection('Menu', 'Hyrule Castle Secret Entrance', 'Uncle S&Q', world, player)
world.get_entrance('Uncle S&Q', player).hide_path = True
set_rule(world.get_entrance('Links House S&Q', player), lambda state: state.can_reach('Sanctuary', 'Region', player))
set_rule(world.get_entrance('Sanctuary S&Q', player), lambda state: state.can_reach('Sanctuary', 'Region', player))
# these are because of rails # these are because of rails
if world.shuffle[player] != 'vanilla': if world.shuffle[player] != 'vanilla':
set_rule(world.get_entrance('Hyrule Castle Exit (East)', player), lambda state: state.has('Zelda Delivered', player)) set_rule(world.get_entrance('Hyrule Castle Exit (East)', player), lambda state: state.has('Zelda Delivered', player))
@@ -1049,7 +1049,7 @@ def set_big_bomb_rules(world, player):
# the basic routes assume you can reach eastern light world with the bomb. # the basic routes assume you can reach eastern light world with the bomb.
# you can then use the southern teleporter, or (if you have beaten Aga1) the hyrule castle gate warp # you can then use the southern teleporter, or (if you have beaten Aga1) the hyrule castle gate warp
def basic_routes(state): def basic_routes(state):
return southern_teleporter(state) or state.can_reach('Top of Pyramid', 'Entrance', player) return southern_teleporter(state) or state.has('Beat Agahnim 1', player)
# Key for below abbreviations: # Key for below abbreviations:
# P = pearl # P = pearl
@@ -1082,7 +1082,7 @@ def set_big_bomb_rules(world, player):
#1. Mirror and enter via gate: Need mirror and Aga1 #1. Mirror and enter via gate: Need mirror and Aga1
#2. cross peg bridge: Need hammer and moon pearl #2. cross peg bridge: Need hammer and moon pearl
# -> CPB or (M and A) # -> CPB or (M and A)
add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: cross_peg_bridge(state) or (state.has_Mirror(player) and state.can_reach('Top of Pyramid', 'Entrance', player))) add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: cross_peg_bridge(state) or (state.has_Mirror(player) and state.has('Beat Agahnim 1', player)))
elif bombshop_entrance.name in Isolated_DW_entrances: elif bombshop_entrance.name in Isolated_DW_entrances:
# 1. mirror then flute then basic routes # 1. mirror then flute then basic routes
# -> M and Flute and BR # -> M and Flute and BR

View File

@@ -1 +1,2 @@
aenum aenum
fast-enum