Merge branch 'DoorDevUnstable' into Sandbox

# Conflicts:
#	ItemList.py
#	Items.py
#	Main.py
#	Rom.py
#	data/base2current.bps
This commit is contained in:
aerinon
2023-03-10 13:43:38 -07:00
63 changed files with 4599 additions and 988 deletions
+22 -22
View File
@@ -32,10 +32,10 @@ jobs:
steps:
# checkout commit
- name: Checkout commit
uses: actions/checkout@v2
uses: actions/checkout@v3
# install python
- name: Install python
uses: actions/setup-python@v2
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
architecture: "x64"
@@ -50,21 +50,21 @@ jobs:
pip install pyinstaller
# get parent directory
- name: Get Repo Name
uses: mad9000/actions-find-and-replace-string@1
uses: mad9000/actions-find-and-replace-string@3
id: repoName
with:
source: ${{ github.repository }}
find: '${{ github.repository_owner }}/'
replace: ''
- name: Get Parent Directory Path (!Windows)
uses: mad9000/actions-find-and-replace-string@1
uses: mad9000/actions-find-and-replace-string@3
id: parentDirNotWin
with:
source: ${{ github.workspace }}
find: '${{ steps.repoName.outputs.value }}/${{ steps.repoName.outputs.value }}'
replace: ${{ steps.repoName.outputs.value }}
- name: Get Parent Directory Path (Windows)
uses: mad9000/actions-find-and-replace-string@1
uses: mad9000/actions-find-and-replace-string@3
id: parentDir
with:
source: ${{ steps.parentDirNotWin.outputs.value }}
@@ -92,7 +92,7 @@ jobs:
python ./resources/ci/common/prepare_binary.py
# upload binary artifacts for later step
- name: Upload Binary Artifacts
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v3
with:
name: binaries-${{ matrix.os-name }}
path: ${{ steps.parentDir.outputs.value }}/artifact
@@ -117,10 +117,10 @@ jobs:
steps:
# checkout commit
- name: Checkout commit
uses: actions/checkout@v2
uses: actions/checkout@v3
# install python
- name: Install Python
uses: actions/setup-python@v2
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
architecture: "x64"
@@ -134,21 +134,21 @@ jobs:
python ./resources/ci/common/install.py
# get parent directory
- name: Get Repo Name
uses: mad9000/actions-find-and-replace-string@1
uses: mad9000/actions-find-and-replace-string@3
id: repoName
with:
source: ${{ github.repository }}
find: '${{ github.repository_owner }}/'
replace: ''
- name: Get Parent Directory Path (!Windows)
uses: mad9000/actions-find-and-replace-string@1
uses: mad9000/actions-find-and-replace-string@3
id: parentDirNotWin
with:
source: ${{ github.workspace }}
find: '${{ steps.repoName.outputs.value }}/${{ steps.repoName.outputs.value }}'
replace: ${{ steps.repoName.outputs.value }}
- name: Get Parent Directory Path (Windows)
uses: mad9000/actions-find-and-replace-string@1
uses: mad9000/actions-find-and-replace-string@3
id: parentDir
with:
source: ${{ steps.parentDirNotWin.outputs.value }}
@@ -156,7 +156,7 @@ jobs:
replace: ${{ steps.repoName.outputs.value }}
# download binary artifact
- name: Download Binary Artifact
uses: actions/download-artifact@v2
uses: actions/download-artifact@v3
with:
name: binaries-${{ matrix.os-name }}
path: ./
@@ -170,13 +170,13 @@ jobs:
python ./resources/ci/common/prepare_release.py
# upload appversion artifact for later step
- name: Upload AppVersion Artifact
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v3
with:
name: appversion-${{ matrix.os-name }}
path: ./resources/app/meta/manifests/app_version.txt
# upload archive artifact for later step
- name: Upload Archive Artifact
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v3
with:
name: archive-${{ matrix.os-name }}
path: ${{ steps.parentDir.outputs.value }}/deploy
@@ -202,24 +202,24 @@ jobs:
steps:
# checkout commit
- name: Checkout commit
uses: actions/checkout@v2
uses: actions/checkout@v3
# get parent directory
- name: Get Repo Name
uses: mad9000/actions-find-and-replace-string@1
uses: mad9000/actions-find-and-replace-string@3
id: repoName
with:
source: ${{ github.repository }}
find: '${{ github.repository_owner }}/'
replace: ''
- name: Get Parent Directory Path (!Windows)
uses: mad9000/actions-find-and-replace-string@1
uses: mad9000/actions-find-and-replace-string@3
id: parentDirNotWin
with:
source: ${{ github.workspace }}
find: '${{ steps.repoName.outputs.value }}/${{ steps.repoName.outputs.value }}'
replace: ${{ steps.repoName.outputs.value }}
- name: Get Parent Directory Path (Windows)
uses: mad9000/actions-find-and-replace-string@1
uses: mad9000/actions-find-and-replace-string@3
id: parentDir
with:
source: ${{ steps.parentDirNotWin.outputs.value }}
@@ -230,25 +230,25 @@ jobs:
python -m pip install pytz requests
# download appversion artifact
- name: Download AppVersion Artifact
uses: actions/download-artifact@v2
uses: actions/download-artifact@v3
with:
name: appversion-${{ matrix.os-name }}
path: ${{ steps.parentDir.outputs.value }}/build
# download ubuntu archive artifact
- name: Download Ubuntu Archive Artifact
uses: actions/download-artifact@v2
uses: actions/download-artifact@v3
with:
name: archive-ubuntu-latest
path: ${{ steps.parentDir.outputs.value }}/deploy/linux
# download macos archive artifact
- name: Download MacOS Archive Artifact
uses: actions/download-artifact@v2
uses: actions/download-artifact@v3
with:
name: archive-macOS-latest
path: ${{ steps.parentDir.outputs.value }}/deploy/macos
# download windows archive artifact
- name: Download Windows Archive Artifact
uses: actions/download-artifact@v2
uses: actions/download-artifact@v3
with:
name: archive-windows-latest
path: ${{ steps.parentDir.outputs.value }}/deploy/windows
+1
View File
@@ -0,0 +1 @@
<randall.rupper@gmail.com> <randall.rupper@sirsidynix.com>
+109 -26
View File
@@ -29,6 +29,8 @@ class World(object):
self.doorShuffle = doorShuffle.copy()
self.intensity = {}
self.door_type_mode = {}
self.trap_door_mode = {}
self.key_logic_algorithm = {}
self.logic = logic.copy()
self.mode = mode.copy()
self.swords = swords.copy()
@@ -110,7 +112,8 @@ 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'] or shuffle[player] in ['crossed', 'insanity'])
set_player_attr('fix_fake_world', logic[player] not in ['owglitches', 'nologic']
or shuffle[player] in ['lean', 'crossed', 'insanity'])
set_player_attr('mapshuffle', False)
set_player_attr('compassshuffle', False)
set_player_attr('keyshuffle', 'none')
@@ -139,10 +142,12 @@ class World(object):
set_player_attr('pot_contents', None)
set_player_attr('pseudoboots', False)
set_player_attr('collection_rate', False)
set_player_attr('colorizepots', False)
set_player_attr('colorizepots', True)
set_player_attr('pot_pool', {})
set_player_attr('decoupledoors', 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('shopsanity', False)
set_player_attr('mixed_travel', 'prevent')
@@ -156,7 +161,9 @@ class World(object):
def finish_init(self):
for player in range(1, self.players + 1):
if self.mode[player] == 'retro':
self.mode[player] == 'open'
self.mode[player] = 'open'
if self.goal[player] == 'completionist':
self.accessibility[player] = 'locations'
def get_name_string_for_object(self, obj):
return obj.name if self.players == 1 else f'{obj.name} ({self.get_player_names(obj.player)})'
@@ -472,7 +479,10 @@ class World(object):
if self.has_beaten_game(state):
return True
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
or self.goal[location.player] == 'completionist')
and location not in state.locations_checked]
while prog_locations:
sphere = []
@@ -504,6 +514,7 @@ class CollectionState(object):
self.world = parent
if not skip_init:
self.prog_items = Counter()
self.forced_keys = Counter()
self.reachable_regions = {player: dict() for player in range(1, parent.players + 1)}
self.blocked_connections = {player: dict() for player in range(1, parent.players + 1)}
self.events = []
@@ -518,6 +529,7 @@ 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.trace = None
def update_reachable_regions(self, player):
@@ -535,12 +547,13 @@ class CollectionState(object):
queue = deque(self.blocked_connections[player].items())
self.traverse_world(queue, rrp, bc, player)
unresolved_events = [x for y in self.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 self.locations_checked and x.can_reach(self)]
unresolved_events = self._do_not_flood_the_keys(unresolved_events)
if len(unresolved_events) == 0:
self.check_key_doors_in_dungeons(rrp, player)
if self.world.key_logic_algorithm[player] == 'default':
unresolved_events = [x for y in self.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 self.locations_checked and x.can_reach(self)]
unresolved_events = self._do_not_flood_the_keys(unresolved_events)
if len(unresolved_events) == 0:
self.check_key_doors_in_dungeons(rrp, player)
def traverse_world(self, queue, rrp, bc, player):
# run BFS on all connections, and keep track of those blocked by missing items
@@ -632,6 +645,7 @@ class CollectionState(object):
def check_key_doors_in_dungeons(self, rrp, player):
for dungeon_name, checklist in self.dungeons_to_check[player].items():
# todo: optimization idea - abort exploration if there are unresolved events now
if self.apply_dungeon_exploration(rrp, player, dungeon_name, checklist):
continue
init_door_candidates = self.should_explore_child_state(self, dungeon_name, player)
@@ -740,6 +754,15 @@ class CollectionState(object):
rrp[k] = missing_regions[k]
possible_path = terminal_states[0].path[k]
self.path[k] = paths[k] = possible_path
for conn in k.exits:
if self.is_small_door(conn):
door = conn.door if conn.door.smallKey else conn.door.controller
key_logic = self.world.key_logic[player][dungeon_name]
if door.name not in self.reached_doors[player]:
self.door_counter[player][0][dungeon_name] += 1
self.reached_doors[player].add(door.name)
if key_logic.sm_doors[door]:
self.reached_doors[player].add(key_logic.sm_doors[door].name)
missing_bc = {}
for blocked, crystal in common_bc.items():
if (blocked not in bc and blocked.parent_region in rrp
@@ -790,7 +813,23 @@ class CollectionState(object):
else:
door_candidates.append(door.name)
return door_candidates
return None
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):
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:
rule = key_logic.door_rules[door.name]
key = KeyRuleType.AllowSmall
if (key in rule.new_rules and key_total >= rule.new_rules[key] and door.name not in skip
and door.name in state.reached_doors[player] and door.name not in state.opened_doors[player]
and rule.small_location.item is None):
if paired:
door_candidates.append((door.name, paired.name))
skip.add(paired.name)
else:
door_candidates.append(door.name)
return door_candidates if door_candidates else None
@staticmethod
def print_rrp(rrp):
@@ -806,6 +845,7 @@ class CollectionState(object):
def copy(self):
ret = CollectionState(self.world, skip_init=True)
ret.prog_items = self.prog_items.copy()
ret.forced_keys = self.forced_keys.copy()
ret.reachable_regions = {player: copy.copy(self.reachable_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.events = copy.copy(self.events)
@@ -820,6 +860,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
return ret
def apply_dungeon_exploration(self, rrp, player, dungeon_name, checklist):
@@ -929,29 +970,30 @@ class CollectionState(object):
checked_locations = set([l for l in locations if l in self.locations_checked])
reachable_events = [location for location in locations if location.event and location.can_reach(self)]
reachable_events = self._do_not_flood_the_keys(reachable_events)
found_new = False
for event in reachable_events:
if event not in checked_locations:
self.events.append((event.name, event.player))
self.collect(event.item, True, event)
return len(reachable_events) > len(checked_locations)
found_new = True
return found_new
def sweep_for_events(self, key_only=False, locations=None):
# this may need improvement
if locations is None:
locations = self.world.get_filled_locations()
new_locations = True
checked_locations = 0
while new_locations:
reachable_events = [location for location in locations if location.event and
(not key_only or (self.world.keyshuffle[location.item.player] == 'none' and location.item.smallkey) or (not self.world.bigkeyshuffle[location.item.player] and location.item.bigkey))
and location.can_reach(self)]
reachable_events = self._do_not_flood_the_keys(reachable_events)
new_locations = False
for event in reachable_events:
if (event.name, event.player) not in self.events:
self.events.append((event.name, event.player))
self.collect(event.item, True, event)
new_locations = len(reachable_events) > checked_locations
checked_locations = len(reachable_events)
new_locations = True
def can_reach_blue(self, region, player):
@@ -1011,6 +1053,14 @@ class CollectionState(object):
return (item, player) in self.prog_items
return self.prog_items[item, player] >= count
def has_sm_key_strict(self, item, player, count=1):
if self.world.keyshuffle[player] == 'universal':
if self.world.mode[player] == 'standard' and self.world.doorShuffle[player] == 'vanilla' and item == 'Small Key (Escape)':
return True # Cannot access the shop until escape is finished. This is safe because the key is manually placed in make_custom_item_pool
return self.can_buy_unlimited('Small Key (Universal)', player)
obtained = self.prog_items[item, player] - self.forced_keys[item, player]
return obtained >= count
def can_buy_unlimited(self, item, player):
for shop in self.world.shops[player]:
if shop.region.player == player and shop.has_unlimited(item) and shop.region.can_reach(self):
@@ -1020,6 +1070,12 @@ class CollectionState(object):
def item_count(self, item, player):
return self.prog_items[item, player]
def everything(self, player):
all_locations = self.world.get_filled_locations(player)
all_locations.remove(self.world.get_location('Ganon', player))
return (len([x for x in self.locations_checked if x.player == player])
>= len(all_locations))
def has_crystals(self, count, player):
crystals = ['Crystal 1', 'Crystal 2', 'Crystal 3', 'Crystal 4', 'Crystal 5', 'Crystal 6', 'Crystal 7']
return len([crystal for crystal in crystals if self.has(crystal, player)]) >= count
@@ -1137,6 +1193,8 @@ class CollectionState(object):
return self.has('Fire Rod', player) or self.has('Lamp', player)
def can_flute(self, player):
if self.world.mode[player] == 'standard' and not self.has('Zelda Delivered', player):
return False # can't flute in rain state
if any(map(lambda i: i.name in ['Ocarina', 'Ocarina (Activated)'], self.world.precollected_items)):
return True
lw = self.world.get_region('Light World', player)
@@ -1199,6 +1257,8 @@ class CollectionState(object):
def collect(self, item, event=False, location=None):
if location:
self.locations_checked.add(location)
if item and item.smallkey and location.forced_item is not None:
self.forced_keys[item.name, item.player] += 1
if not item:
return
changed = False
@@ -1385,10 +1445,10 @@ class Region(object):
or (item.bigkey and not self.world.bigkeyshuffle[item.player])
or (item.map and not self.world.mapshuffle[item.player])
or (item.compass and not self.world.compassshuffle[item.player]))
sewer_hack = self.world.mode[item.player] == 'standard' and item.name == 'Small Key (Escape)'
if sewer_hack or inside_dungeon_item:
# not all small keys to escape must be in escape
# sewer_hack = self.world.mode[item.player] == 'standard' and item.name == 'Small Key (Escape)'
if inside_dungeon_item:
return self.dungeon and self.dungeon.is_dungeon_item(item) and item.player == self.player
return True
def __str__(self):
@@ -1849,6 +1909,9 @@ class Door(object):
return world.get_room(self.roomIndex, self.player).kind(self)
return None
def dungeon_name(self):
return self.entrance.parent_region.dungeon.name if self.entrance.parent_region.dungeon else 'Cave'
def __eq__(self, other):
return isinstance(other, self.__class__) and self.name == other.name
@@ -2260,6 +2323,9 @@ class Item(object):
def __unicode__(self):
return self.world.get_name_string_for_object(self) if self.world else f'{self.name} (Player {self.player})'
def __eq__(self, other):
return self.name == other.name and self.player == other.player
# have 6 address that need to be filled
class Crystal(Item):
@@ -2395,6 +2461,8 @@ class Spoiler(object):
'door_shuffle': self.world.doorShuffle,
'intensity': self.world.intensity,
'door_type_mode': self.world.door_type_mode,
'trap_door_mode': self.world.trap_door_mode,
'key_logic': self.world.key_logic_algorithm,
'decoupledoors': self.world.decoupledoors,
'dungeon_counters': self.world.dungeon_counters,
'item_pool': self.world.difficulty,
@@ -2574,7 +2642,7 @@ class Spoiler(object):
outfile.write('Mode: %s\n' % self.metadata['mode'][player])
outfile.write('Swords: %s\n' % self.metadata['weapons'][player])
outfile.write('Goal: %s\n' % self.metadata['goal'][player])
if self.metadata['goal'][player] in ['triforcehunt', 'trinity']:
if self.metadata['goal'][player] in ['triforcehunt', 'trinity', 'ganonhunt']:
outfile.write('Triforce Pieces Required: %s\n' % self.metadata['triforcegoal'][player])
outfile.write('Triforce Pieces Total: %s\n' % self.metadata['triforcepool'][player])
outfile.write('Crystals required for GT: %s\n' % (str(self.world.crystals_gt_orig[player])))
@@ -2595,13 +2663,14 @@ class Spoiler(object):
outfile.write(f"GT/Ganon Shuffled: {yn(self.metadata['shuffleganon'])}\n")
outfile.write(f"Overworld Map: {self.metadata['overworld_map'][player]}\n")
outfile.write(f"Take Any Caves: {self.metadata['take_any'][player]}\n")
outfile.write(f"Overworld Map: {self.metadata['overworld_map'][player]}\n")
if self.metadata['goal'][player] != 'trinity':
outfile.write('Pyramid hole pre-opened: %s\n' % ('Yes' if self.metadata['open_pyramid'][player] else 'No'))
outfile.write('Door Shuffle: %s\n' % self.metadata['door_shuffle'][player])
if self.metadata['door_shuffle'][player] != 'vanilla':
outfile.write(f"Intensity: {self.metadata['intensity'][player]}\n")
outfile.write(f"Door Type Mode: {self.metadata['door_type_mode'][player]}\n")
outfile.write(f"Trap Door Mode: {self.metadata['trap_door_mode'][player]}\n")
outfile.write(f"Key Logic Algorithm: {self.metadata['key_logic'][player]}\n")
outfile.write(f"Decouple Doors: {yn(self.metadata['decoupledoors'][player])}\n")
outfile.write(f"Experimental: {yn(self.metadata['experimental'][player])}\n")
outfile.write(f"Dungeon Counters: {self.metadata['dungeon_counters'][player]}\n")
@@ -2617,6 +2686,7 @@ class Spoiler(object):
outfile.write('Enemy health: %s\n' % self.metadata['enemy_health'][player])
outfile.write('Enemy damage: %s\n' % self.metadata['enemy_damage'][player])
outfile.write(f"Hints: {yn(self.metadata['hints'][player])}\n")
outfile.write('Race: %s\n' % ('Yes' if self.world.settings.world_rep['meta']['race'] else 'No'))
if self.startinventory:
outfile.write('Starting Inventory:'.ljust(line_width))
@@ -2854,7 +2924,8 @@ world_mode = {"open": 0, "standard": 1, "inverted": 2}
sword_mode = {"random": 0, "assured": 1, "swordless": 2, "vanilla": 3}
# byte 2: GGGD DFFH (goal, diff, item_func, hints)
goal_mode = {'ganon': 0, 'pedestal': 1, 'dungeons': 2, 'triforcehunt': 3, 'crystals': 4, 'trinity': 5}
goal_mode = {'ganon': 0, 'pedestal': 1, 'dungeons': 2, 'triforcehunt': 3, 'crystals': 4, 'trinity': 5,
'ganonhunt': 6, 'completionist': 7}
diff_mode = {"normal": 0, "hard": 1, "expert": 2}
func_mode = {"normal": 0, "hard": 1, "expert": 2}
@@ -2892,15 +2963,19 @@ boss_mode = {"none": 0, "simple": 1, "full": 2, "chaos": 3, 'random': 3, 'unique
# byte 10: settings_version
# byte 11: FBBB, TTSS (flute_mode, bow_mode, take_any, small_key_mode)
# byte 11: FBBB TTSS (flute_mode, bow_mode, take_any, small_key_mode)
flute_mode = {'normal': 0, 'active': 1}
keyshuffle_mode = {'none': 0, 'wild': 1, 'universal': 2} # reserved 8 modes?
take_any_mode = {'none': 0, 'random': 1, 'fixed': 2}
bow_mode = {'progressive': 0, 'silvers': 1, 'retro': 2, 'retro_silver': 3}
bow_mode = {'progressive': 0, 'silvers': 1, 'retro': 2, 'retro_silvers': 3}
# additions
# psuedoboots does not effect code
# sfx_shuffle and other adjust items does not effect settings code
# byte 12: POOT TKKK (pseudoboots, overworld_map, trap_door_mode, key_logic_algo)
overworld_map_mode = {'default': 0, 'compass': 1, 'map': 2}
trap_door_mode = {'vanilla': 0, 'optional': 1, 'boss': 2, 'oneway': 3}
key_logic_algo = {'default': 0, 'partial': 1, 'strict': 2}
# sfx_shuffle and other adjust items does not affect settings code
# Bump this when making changes that are not backwards compatible (nearly all of them)
settings_version = 1
@@ -2944,7 +3019,10 @@ class Settings(object):
settings_version,
(flute_mode[w.flute_mode[p]] << 7 | bow_mode[w.bow_mode[p]] << 4
| take_any_mode[w.take_any[p]] << 2 | keyshuffle_mode[w.keyshuffle[p]])
| take_any_mode[w.take_any[p]] << 2 | keyshuffle_mode[w.keyshuffle[p]]),
((0x80 if w.pseudoboots[p] else 0) | overworld_map_mode[w.overworld_map[p]] << 6
| trap_door_mode[w.trap_door_mode[p]] << 4 | key_logic_algo[w.key_logic_algorithm[p]]),
])
return base64.b64encode(code, "+-".encode()).decode()
@@ -3012,6 +3090,11 @@ class Settings(object):
args.bow_mode[p] = r(bow_mode)[(settings[11] & 0x70) >> 4]
args.take_any[p] = r(take_any_mode)[(settings[11] & 0xC) >> 2]
args.keyshuffle[p] = r(keyshuffle_mode)[settings[11] & 0x3]
if len(settings) > 12:
args.pseudoboots[p] = True if settings[12] & 0x80 else False
args.overworld_map[p] = r(overworld_map_mode)[(settings[12] & 0x60) >> 6]
args.trap_door_mode[p] = r(trap_door_mode)[(settings[12] & 0x14) >> 4]
args.key_logic_algorithm[p] = r(key_logic_algo)[settings[12] & 0x07]
class KeyRuleType(FastEnum):
+12 -3
View File
@@ -122,9 +122,14 @@ def parse_cli(argv, no_defaults=False):
defaults = copy.deepcopy(ret)
for player in range(1, player_num + 1):
playerargs = parse_cli(shlex.split(getattr(ret, f"p{player}")), True)
if playerargs.filename:
playersettings = apply_settings_file({}, playerargs.filename)
for k, v in playersettings.items():
setattr(playerargs, k, v)
for name in ['logic', 'mode', 'swords', 'goal', 'difficulty', 'item_functionality',
'flute_mode', 'bow_mode', 'take_any',
'flute_mode', 'bow_mode', 'take_any', 'boots_hint',
'shuffle', 'door_shuffle', 'intensity', 'crystals_ganon', 'crystals_gt', 'openpyramid',
'mapshuffle', 'compassshuffle', 'keyshuffle', 'bigkeyshuffle', 'startinventory',
'usestartinventory', 'bombbag', 'overworld_map', 'restrict_boss_items',
@@ -135,7 +140,8 @@ def parse_cli(argv, no_defaults=False):
'ow_palettes', 'uw_palettes', 'sprite', 'disablemusic', 'quickswap', 'fastmenu', 'heartcolor',
'heartbeep', 'remote_items', 'shopsanity', 'dropshuffle', 'pottery', 'keydropshuffle',
'mixed_travel', 'standardize_palettes', 'code', 'reduce_flashing', 'shuffle_sfx',
'msu_resume', 'collection_rate', 'colorizepots', 'decoupledoors', 'door_type_mode']:
'msu_resume', 'collection_rate', 'colorizepots', 'decoupledoors', 'door_type_mode',
'trap_door_mode', 'key_logic_algorithm']:
value = getattr(defaults, name) if getattr(playerargs, name) is None else getattr(playerargs, name)
if player == 1:
setattr(ret, name, {1: value})
@@ -161,6 +167,7 @@ def parse_settings():
"retro": False,
"bombbag": False,
"mode": "open",
"boots_hint": False,
"logic": "noglitches",
"goal": "ganon",
"crystals_gt": "7",
@@ -198,7 +205,7 @@ def parse_settings():
'keydropshuffle': False,
'dropshuffle': 'none',
'pottery': 'none',
'colorizepots': False,
'colorizepots': True,
'shufflepots': False,
'mapshuffle': False,
'compassshuffle': False,
@@ -208,6 +215,8 @@ def parse_settings():
'door_shuffle': 'vanilla',
'intensity': 2,
'door_type_mode': 'original',
'trap_door_mode': 'optional',
'key_logic_algorithm': 'default',
'decoupledoors': False,
'experimental': False,
'dungeon_counters': 'default',
+299 -191
View File
@@ -6,7 +6,7 @@ from enum import unique, Flag
from typing import DefaultDict, Dict, List
from itertools import chain
from BaseClasses import RegionType, Region, Door, DoorType, Direction, Sector, CrystalBarrier, DungeonInfo, dungeon_keys
from BaseClasses import RegionType, Region, Door, DoorType, Sector, CrystalBarrier, DungeonInfo, dungeon_keys
from BaseClasses import PotFlags, LocationType, Direction
from Doors import reset_portals
from Dungeons import dungeon_regions, region_starts, standard_starts, split_region_starts
@@ -15,13 +15,12 @@ from Items import ItemFactory
from RoomData import DoorKind, PairedDoor, reset_rooms
from source.dungeon.DungeonStitcher import GenerationException, generate_dungeon
from source.dungeon.DungeonStitcher import ExplorationState as ExplorationState2
# from DungeonGenerator import generate_dungeon
from DungeonGenerator import ExplorationState, convert_regions, pre_validate, determine_required_paths, drop_entrances
from DungeonGenerator import ExplorationState, convert_regions, determine_required_paths, drop_entrances
from DungeonGenerator import create_dungeon_builders, split_dungeon_builder, simple_dungeon_builder, default_dungeon_entrances
from DungeonGenerator import dungeon_portals, dungeon_drops, connect_doors, count_reserved_locations
from DungeonGenerator import valid_region_to_explore
from KeyDoorShuffle import analyze_dungeon, build_key_layout, validate_key_layout, determine_prize_lock
from KeyDoorShuffle import validate_bk_layout, check_bk_special
from KeyDoorShuffle import validate_bk_layout
from Utils import ncr, kth_combination
@@ -129,7 +128,7 @@ def link_doors_prep(world, player):
vanilla_key_logic(world, player)
def link_doors_main(world, player):
def create_dungeon_pool(world, player):
pool = None
if world.doorShuffle[player] == 'basic':
pool = [([name], regions) for name, regions in dungeon_regions.items()]
@@ -143,6 +142,11 @@ def link_doors_main(world, player):
elif world.doorShuffle[player] != 'vanilla':
logging.getLogger('').error('Invalid door shuffle setting: %s' % world.doorShuffle[player])
raise Exception('Invalid door shuffle setting: %s' % world.doorShuffle[player])
return pool
def link_doors_main(world, player):
pool = create_dungeon_pool(world, player)
if pool:
main_dungeon_pool(pool, world, player)
if world.doorShuffle[player] != 'vanilla':
@@ -226,21 +230,37 @@ def vanilla_key_logic(world, player):
add_inaccessible_doors(world, player)
entrances_map, potentials, connections = determine_entrance_list(world, player)
for builder in builders:
enabled_entrances = world.enabled_entrances[player] = {}
builder_queue = deque(builders)
last_key, loops = None, 0
while len(builder_queue) > 0:
builder = builder_queue.popleft()
origin_list = entrances_map[builder.name]
start_regions = convert_regions(origin_list, world, player)
doors = convert_key_doors(default_small_key_doors[builder.name], world, player)
key_layout = build_key_layout(builder, start_regions, doors, world, player)
valid = validate_key_layout(key_layout, world, player)
if not valid:
logging.getLogger('').info('Vanilla key layout not valid %s', builder.name)
builder.key_door_proposal = doors
if player not in world.key_logic.keys():
world.key_logic[player] = {}
analyze_dungeon(key_layout, world, player)
world.key_logic[player][builder.name] = key_layout.key_logic
world.key_layout[player][builder.name] = key_layout
log_key_logic(builder.name, key_layout.key_logic)
find_enabled_origins(builder.sectors, enabled_entrances, origin_list, entrances_map, builder.name)
if len(origin_list) <= 0:
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')
raise GenerationException(f'Infinite loop detected for "{builder.name}" located at {origin_name}')
builder_queue.append(builder)
last_key = builder.name
loops += 1
else:
find_new_entrances(builder.master_sector, entrances_map, connections, potentials,
enabled_entrances, world, player)
start_regions = convert_regions(origin_list, world, player)
doors = convert_key_doors(default_small_key_doors[builder.name], world, player)
key_layout = build_key_layout(builder, start_regions, doors, {}, world, player)
valid = validate_key_layout(key_layout, world, player)
if not valid:
logging.getLogger('').info('Vanilla key layout not valid %s', builder.name)
builder.key_door_proposal = doors
if player not in world.key_logic.keys():
world.key_logic[player] = {}
analyze_dungeon(key_layout, world, player)
world.key_logic[player][builder.name] = key_layout.key_logic
world.key_layout[player][builder.name] = key_layout
log_key_logic(builder.name, key_layout.key_logic)
# if world.shuffle[player] == 'vanilla' and world.accessibility[player] == 'items' and not world.retro[player] and not world.keydropshuffle[player]:
# validate_vanilla_key_logic(world, player)
@@ -477,14 +497,14 @@ def choose_portals(world, player):
info.required_passage = {x: y for x, y in info.required_passage.items() if len(y) > 0}
for target_region, possible_portals in info.required_passage.items():
candidates = find_portal_candidates(master_door_list, dungeon, custom, allowed, need_passage=True,
bk_shuffle=bk_shuffle, rupee_bow=rupee_bow_flag)
bk_shuffle=bk_shuffle, standard=std_flag, rupee_bow=rupee_bow_flag)
choice, portal = assign_portal(candidates, possible_portals, custom, world, player)
portal.destination = True
clean_up_portal_assignment(portal_assignment, dungeon, portal, master_door_list, outstanding_portals)
dead_end_choices = info.total - 1 - len(portal_assignment[dungeon])
for i in range(0, dead_end_choices):
candidates = find_portal_candidates(master_door_list, dungeon, custom, allowed, dead_end_allowed=True,
bk_shuffle=bk_shuffle, rupee_bow=rupee_bow_flag)
bk_shuffle=bk_shuffle, standard=std_flag, rupee_bow=rupee_bow_flag)
possible_portals = outstanding_portals if not info.sole_entrance else [x for x in outstanding_portals if x != info.sole_entrance]
choice, portal = assign_portal(candidates, possible_portals, custom, world, player)
if choice.deadEnd:
@@ -542,7 +562,9 @@ def customizer_portals(master_door_list, world, player):
custom_doors = world.customizer.get_doors()[player]
if custom_doors and 'lobbies' in custom_doors:
for portal, assigned_door in custom_doors['lobbies'].items():
door = next(x for x in master_door_list if x.name == assigned_door)
door = next((x for x in master_door_list if x.name == assigned_door), None)
if door is None:
raise Exception(f'{assigned_door} not found. Check for typos')
custom_portals[portal] = door
assigned_doors.add(door)
if custom_doors and 'doors' in custom_doors:
@@ -552,9 +574,27 @@ def customizer_portals(master_door_list, world, player):
if isinstance(dest, str):
door = world.get_door(dest, player)
assigned_doors.add(door)
else:
elif 'dest' in dest:
door = world.get_door(dest['dest'], player)
assigned_doors.add(door)
# restricts connected doors to the customized portals
if assigned_doors:
pool = create_dungeon_pool(world, player)
if pool:
pool_map = {}
for pool, region_list in pool:
sector_pool = convert_to_sectors(region_list, world, player)
merge_sectors(sector_pool, world, player)
for p in pool:
pool_map[p] = sector_pool
for portal, assigned_door in custom_portals.items():
portal_region = world.get_door(assigned_door, player).entrance.parent_region
portal_dungeon = world.get_region(f'{portal} Portal', player).dungeon.name
sector_pool = pool_map[portal_dungeon]
sector = next((s for s in sector_pool if portal_region in s.regions), None)
for door in sector.outstanding_doors:
if door.portalAble:
door.dungeonLink = portal_dungeon
return custom_portals, assigned_doors
@@ -798,15 +838,18 @@ def main_dungeon_pool(dungeon_pool, world, player):
hc = world.get_dungeon('Hyrule Castle', player)
hc_compass = ItemFactory('Compass (Escape)', player)
hc_compass.advancement = world.restrict_boss_items[player] != 'none'
hc.dungeon_items.append(hc_compass)
if hc.dungeon_items.count(hc_compass) < 1:
hc.dungeon_items.append(hc_compass)
if 'Agahnims Tower' in pool:
at = world.get_dungeon('Agahnims Tower', player)
at_compass = ItemFactory('Compass (Agahnims Tower)', player)
at_compass.advancement = world.restrict_boss_items[player] != 'none'
at.dungeon_items.append(at_compass)
if at.dungeon_items.count(at_compass) < 1:
at.dungeon_items.append(at_compass)
at_map = ItemFactory('Map (Agahnims Tower)', player)
at_map.advancement = world.restrict_boss_items[player] != 'none'
at.dungeon_items.append(at_map)
if at.dungeon_items.count(at_map) < 1:
at.dungeon_items.append(at_map)
sector_pool = convert_to_sectors(region_list, world, player)
merge_sectors(sector_pool, world, player)
# todo: which dungeon to create
@@ -831,7 +874,8 @@ def main_dungeon_pool(dungeon_pool, world, player):
for name in pool:
builder = world.dungeon_layouts[player][name]
region_set = builder.master_sector.region_set()
builder.bk_required = builder.bk_door_proposal or any(x in region_set for x in special_bk_regions)
builder.bk_required = (builder.bk_door_proposal or any(x in region_set for x in special_bk_regions)
or len(world.key_logic[player][name].bk_chests) > 0)
dungeon = world.get_dungeon(name, player)
if not builder.bk_required or builder.bk_provided:
dungeon.big_key = None
@@ -1018,7 +1062,8 @@ def main_dungeon_generation(dungeon_builders, recombinant_builders, connections_
find_standard_origins(builder, recombinant_builders, origin_list)
find_enabled_origins(builder.sectors, enabled_entrances, origin_list, entrances_map, name)
split_dungeon = treat_split_as_whole_dungeon(split_dungeon, name, origin_list, world, player)
if len(origin_list) <= 0 or not pre_validate(builder, origin_list, split_dungeon, world, player):
# todo: figure out pre-validate, ensure all needed origins are enabled?
if len(origin_list) <= 0: # or not pre_validate(builder, origin_list, split_dungeon, world, player):
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'
raise GenerationException(f'Infinite loop detected for "{builder.name}" located at {origin_name}')
@@ -1068,14 +1113,14 @@ def determine_entrance_list(world, player):
connections = {}
for key, portal_list in dungeon_portals.items():
entrance_map[key] = []
r_names = {}
r_names = []
if key in dungeon_drops.keys():
for drop in dungeon_drops[key]:
r_names[drop] = None
r_names.append((drop, None))
for portal_name in portal_list:
portal = world.get_portal(portal_name, player)
r_names[portal.door.entrance.parent_region.name] = portal
for region_name, portal in r_names.items():
r_names.append((portal.door.entrance.parent_region.name, portal))
for region_name, portal in r_names:
if portal:
region = world.get_region(portal.name + ' Portal', player)
else:
@@ -1236,10 +1281,13 @@ def cross_dungeon(world, player):
if world.restrict_boss_items[player] != 'none':
hc_compass.advancement = at_compass.advancement = at_map.advancement = True
hc = world.get_dungeon('Hyrule Castle', player)
hc.dungeon_items.append(hc_compass)
if hc.dungeon_items.count(hc_compass) < 1:
hc.dungeon_items.append(hc_compass)
at = world.get_dungeon('Agahnims Tower', player)
at.dungeon_items.append(at_compass)
at.dungeon_items.append(at_map)
if at.dungeon_items.count(at_compass) < 1:
at.dungeon_items.append(at_compass)
if at.dungeon_items.count(at_map) < 1:
at.dungeon_items.append(at_map)
setup_custom_door_types(world, player)
assign_cross_keys(dungeon_builders, world, player)
@@ -1695,7 +1743,6 @@ def setup_custom_door_types(world, player):
custom_doors = custom_doors[player]
if 'doors' not in custom_doors:
return
# todo: dash/bomb door pool specific
customizeable_types = ['Key Door', 'Dash Door', 'Bomb Door', 'Trap Door', 'Big Key Door']
world.custom_door_types[player] = type_map = {x: defaultdict(list) for x in customizeable_types}
for door, dest in custom_doors['doors'].items():
@@ -1707,8 +1754,8 @@ def setup_custom_door_types(world, player):
if d.type == DoorType.SpiralStairs:
type_map[door_kind][dungeon.name].append(d)
else:
# check if the
if d.dest.type in [DoorType.Interior, DoorType.Normal]:
# check if the dest is paired
if d.dest and d.dest.type in [DoorType.Interior, DoorType.Normal] and door_kind != 'Trap Door':
type_map[door_kind][dungeon.name].append((d, d.dest))
else:
type_map[door_kind][dungeon.name].append(d)
@@ -1737,12 +1784,12 @@ class DoorTypePool:
self.tricky += counts[6]
def chaos_shuffle(self, counts):
weights = [1, 2, 4, 3, 2, 1]
weights = [1, 2, 4, 3, 2]
return [random.choices(self.get_choices(counts[i]), weights=weights)[0] for i, c in enumerate(counts)]
@staticmethod
def get_choices(number):
return [max(number-i, 0) for i in range(-1, 5)]
return [max(number+i, 0) for i in range(-1, 4)]
class BuilderDoorCandidates:
@@ -1756,88 +1803,103 @@ class BuilderDoorCandidates:
def shuffle_door_types(door_type_pools, paths, world, player):
start_regions_map = {}
for name, builder in world.dungeon_layouts[player].items():
start_regions = convert_regions(builder.path_entrances, world, player)
start_regions = convert_regions(find_possible_entrances(world, player, builder), world, player)
start_regions_map[name] = start_regions
builder.candidates = BuilderDoorCandidates()
all_custom = defaultdict(list)
if player in world.custom_door_types:
for custom_dict in world.custom_door_types[player].values():
for dungeon, doors in custom_dict.items():
all_custom[dungeon].extend(doors)
world.paired_doors[player].clear()
used_doors = shuffle_trap_doors(door_type_pools, paths, start_regions_map, world, player)
used_doors = shuffle_trap_doors(door_type_pools, paths, start_regions_map, all_custom, world, player)
# big keys
used_doors = shuffle_big_key_doors(door_type_pools, used_doors, start_regions_map, world, player)
used_doors = shuffle_big_key_doors(door_type_pools, used_doors, start_regions_map, all_custom, world, player)
# small keys
used_doors = shuffle_small_key_doors(door_type_pools, used_doors, start_regions_map, world, player)
used_doors = shuffle_small_key_doors(door_type_pools, used_doors, start_regions_map, all_custom, world, player)
# bombable / dashable
used_doors = shuffle_bomb_dash_doors(door_type_pools, used_doors, start_regions_map, world, player)
used_doors = shuffle_bomb_dash_doors(door_type_pools, used_doors, start_regions_map, all_custom, world, player)
# handle paired list
def shuffle_trap_doors(door_type_pools, paths, start_regions_map, world, player):
def shuffle_trap_doors(door_type_pools, paths, start_regions_map, all_custom, world, player):
used_doors = set()
for pool, door_type_pool in door_type_pools:
ttl = 0
suggestion_map, trap_map, flex_map = {}, {}, {}
remaining = door_type_pool.traps
if player in world.custom_door_types:
custom_trap_doors = world.custom_door_types[player]['Trap Door']
else:
custom_trap_doors = defaultdict(list)
for dungeon in pool:
builder = world.dungeon_layouts[player][dungeon]
find_trappable_candidates(builder, world, player)
if custom_trap_doors[dungeon]:
builder.candidates.trap = filter_key_door_pool(builder.candidates.trap, custom_trap_doors[dungeon])
remaining -= len(custom_trap_doors[dungeon])
ttl += len(builder.candidates.trap)
if ttl == 0:
continue
for dungeon in pool:
builder = world.dungeon_layouts[player][dungeon]
proportion = len(builder.candidates.trap)
calc = int(round(proportion * door_type_pool.traps/ttl))
suggested = min(proportion, calc)
remaining -= suggested
suggestion_map[dungeon] = suggested
flex_map[dungeon] = (proportion - suggested) if suggested < proportion else 0
for dungeon in pool:
builder = world.dungeon_layouts[player][dungeon]
valid_traps, trap_number = find_valid_trap_combination(builder, suggestion_map[dungeon],
start_regions_map[dungeon], paths, world, player,
drop=True)
trap_map[dungeon] = valid_traps
if trap_number < suggestion_map[dungeon]:
flex_map[dungeon] = 0
remaining += suggestion_map[dungeon] - trap_number
suggestion_map[dungeon] = trap_number
builder_order = [x for x in pool if flex_map[x] > 0]
random.shuffle(builder_order)
queue = deque(builder_order)
while len(queue) > 0 and remaining > 0:
dungeon = queue.popleft()
builder = world.dungeon_layouts[player][dungeon]
increased = suggestion_map[dungeon] + 1
valid_traps, trap_number = find_valid_trap_combination(builder, increased, start_regions_map[dungeon],
paths, world, player)
if valid_traps:
if world.trap_door_mode[player] != 'oneway':
ttl = 0
suggestion_map, trap_map, flex_map = {}, {}, {}
remaining = door_type_pool.traps
if player in world.custom_door_types and 'Trap Door' in world.custom_door_types[player]:
custom_trap_doors = world.custom_door_types[player]['Trap Door']
else:
custom_trap_doors = defaultdict(list)
for dungeon in pool:
builder = world.dungeon_layouts[player][dungeon]
if 'Mire Warping Pool' in builder.master_sector.region_set():
custom_trap_doors[dungeon].append(world.get_door('Mire Warping Pool ES', player))
world.custom_door_types[player]['Trap Door'] = custom_trap_doors
find_trappable_candidates(builder, world, player)
if all_custom[dungeon]:
builder.candidates.trap = filter_key_door_pool(builder.candidates.trap, all_custom[dungeon])
remaining -= len(custom_trap_doors[dungeon])
ttl += len(builder.candidates.trap)
if ttl == 0:
continue
for dungeon in pool:
builder = world.dungeon_layouts[player][dungeon]
proportion = len(builder.candidates.trap)
calc = int(round(proportion * door_type_pool.traps/ttl))
suggested = min(proportion, calc)
remaining -= suggested
suggestion_map[dungeon] = suggested
flex_map[dungeon] = (proportion - suggested) if suggested < proportion else 0
for dungeon in pool:
builder = world.dungeon_layouts[player][dungeon]
valid_traps, trap_number = find_valid_trap_combination(builder, suggestion_map[dungeon],
start_regions_map[dungeon], paths, world, player,
drop=True)
trap_map[dungeon] = valid_traps
remaining -= 1
suggestion_map[dungeon] = increased
flex_map[dungeon] -= 1
if flex_map[dungeon] > 0:
queue.append(dungeon)
# time to re-assign
if trap_number < suggestion_map[dungeon]:
flex_map[dungeon] = 0
remaining += suggestion_map[dungeon] - trap_number
suggestion_map[dungeon] = trap_number
builder_order = [x for x in pool if flex_map[x] > 0]
random.shuffle(builder_order)
queue = deque(builder_order)
while len(queue) > 0 and remaining > 0:
dungeon = queue.popleft()
builder = world.dungeon_layouts[player][dungeon]
increased = suggestion_map[dungeon] + 1
valid_traps, trap_number = find_valid_trap_combination(builder, increased, start_regions_map[dungeon],
paths, world, player)
if valid_traps:
trap_map[dungeon] = valid_traps
remaining -= 1
suggestion_map[dungeon] = increased
flex_map[dungeon] -= 1
if flex_map[dungeon] > 0:
queue.append(dungeon)
# time to re-assign
else:
trap_map = {dungeon: [] for dungeon in pool}
for dungeon in pool:
builder = world.dungeon_layouts[player][dungeon]
if 'Mire Warping Pool' in builder.master_sector.region_set():
trap_map[dungeon].append(world.get_door('Mire Warping Pool ES', player))
reassign_trap_doors(trap_map, world, player)
for name, traps in trap_map.items():
used_doors.update(traps)
return used_doors
def shuffle_big_key_doors(door_type_pools, used_doors, start_regions_map, world, player):
def shuffle_big_key_doors(door_type_pools, used_doors, start_regions_map, all_custom, world, player):
for pool, door_type_pool in door_type_pools:
ttl = 0
suggestion_map, bk_map, flex_map = {}, {}, {}
remaining = door_type_pool.bigs
if player in world.custom_door_types:
if player in world.custom_door_types and 'Big Key Door' in world.custom_door_types[player]:
custom_bk_doors = world.custom_door_types[player]['Big Key Door']
else:
custom_bk_doors = defaultdict(list)
@@ -1845,16 +1907,17 @@ def shuffle_big_key_doors(door_type_pools, used_doors, start_regions_map, world,
for dungeon in pool:
builder = world.dungeon_layouts[player][dungeon]
find_big_key_candidates(builder, start_regions_map[dungeon], used_doors, world, player)
if custom_bk_doors[dungeon]:
builder.candidates.big = filter_key_door_pool(builder.candidates.big, custom_bk_doors[dungeon])
if all_custom[dungeon]:
builder.candidates.big = filter_key_door_pool(builder.candidates.big, all_custom[dungeon])
remaining -= len(custom_bk_doors[dungeon])
ttl += len(builder.candidates.big)
if ttl == 0:
continue
remaining = max(0, remaining)
for dungeon in pool:
builder = world.dungeon_layouts[player][dungeon]
proportion = len(builder.candidates.big)
calc = int(round(proportion * door_type_pool.bigs/ttl))
calc = int(round(proportion * remaining/ttl))
suggested = min(proportion, calc)
remaining -= suggested
suggestion_map[dungeon] = suggested
@@ -1891,48 +1954,56 @@ def shuffle_big_key_doors(door_type_pools, used_doors, start_regions_map, world,
return used_doors
def shuffle_small_key_doors(door_type_pools, used_doors, start_regions_map, world, player):
def shuffle_small_key_doors(door_type_pools, used_doors, start_regions_map, all_custom, world, player):
max_computation = 11 # this is around 6 billion worse case factorial don't want to exceed this much
for pool, door_type_pool in door_type_pools:
ttl = 0
suggestion_map, small_map, flex_map = {}, {}, {}
remaining = door_type_pool.smalls
total_keys = remaining
if player in world.custom_door_types:
if player in world.custom_door_types and 'Key Door' in world.custom_door_types[player]:
custom_key_doors = world.custom_door_types[player]['Key Door']
else:
custom_key_doors = defaultdict(list)
total_adjustable = len(pool) > 1
for dungeon in pool:
builder = world.dungeon_layouts[player][dungeon]
if not total_adjustable:
builder.total_keys = total_keys
find_small_key_door_candidates(builder, start_regions_map[dungeon], used_doors, world, player)
if custom_key_doors[dungeon]:
builder.candidates.small = filter_key_door_pool(builder.candidates.small, custom_key_doors[dungeon])
remaining -= len(custom_key_doors[dungeon])
builder.key_doors_num = max(0, len(builder.candidates.small) - builder.key_drop_cnt)
custom_doors = 0
if all_custom[dungeon]:
builder.candidates.small = filter_key_door_pool(builder.candidates.small, all_custom[dungeon])
custom_doors = len(custom_key_doors[dungeon])
remaining -= custom_doors
builder.key_doors_num = max(0, len(builder.candidates.small) - builder.key_drop_cnt) + custom_doors
total_keys -= builder.key_drop_cnt
ttl += builder.key_doors_num
remaining = max(0, remaining)
for dungeon in pool:
builder = world.dungeon_layouts[player][dungeon]
calculated = int(round(builder.key_doors_num*total_keys/ttl))
max_keys = max(0, builder.location_cnt - calc_used_dungeon_items(builder, world, player))
cand_len = max(0, len(builder.candidates.small) - builder.key_drop_cnt)
limit = min(max_keys, cand_len)
limit = min(max_keys, cand_len, max_computation)
suggested = min(calculated, limit)
combo_size = ncr(len(builder.candidates.small), suggested + builder.key_drop_cnt)
while combo_size > 500000 and suggested > 0:
suggested -= 1
combo_size = ncr(len(builder.candidates.small), suggested + builder.key_drop_cnt)
suggestion_map[dungeon] = builder.key_doors_num = suggested + builder.key_drop_cnt
remaining -= suggested + builder.key_drop_cnt
key_door_num = min(suggested + builder.key_drop_cnt, max_computation)
combo_size = ncr(len(builder.candidates.small), key_door_num)
suggestion_map[dungeon] = builder.key_doors_num = key_door_num
remaining -= key_door_num + builder.key_drop_cnt
builder.combo_size = combo_size
flex_map[dungeon] = (limit - suggested) if suggested < limit else 0
flex_map[dungeon] = (limit - key_door_num) if key_door_num < limit else 0
for dungeon in pool:
builder = world.dungeon_layouts[player][dungeon]
if total_adjustable:
builder.total_keys = max(suggestion_map[dungeon], builder.key_drop_cnt)
valid_doors, small_number = find_valid_combination(builder, suggestion_map[dungeon],
start_regions_map[dungeon], world, player)
small_map[dungeon] = valid_doors
actual_chest_keys = small_number - builder.key_drop_cnt
if actual_chest_keys < suggestion_map[dungeon]:
if total_adjustable:
builder.total_keys = actual_chest_keys + builder.key_drop_cnt
flex_map[dungeon] = 0
remaining += suggestion_map[dungeon] - actual_chest_keys
suggestion_map[dungeon] = small_number
@@ -1943,6 +2014,8 @@ def shuffle_small_key_doors(door_type_pools, used_doors, start_regions_map, worl
builder = queue.popleft()
dungeon = builder.name
increased = suggestion_map[dungeon] + 1
if increased > max_computation:
continue
builder.key_doors_num = increased
valid_doors, small_number = find_valid_combination(builder, increased, start_regions_map[dungeon],
world, player)
@@ -1951,6 +2024,8 @@ def shuffle_small_key_doors(door_type_pools, used_doors, start_regions_map, worl
remaining -= 1
suggestion_map[dungeon] = increased
flex_map[dungeon] -= 1
if total_adjustable:
builder.total_keys = max(increased, builder.key_drop_cnt)
if flex_map[dungeon] > 0:
builder.combo_size = ncr(len(builder.candidates.small), builder.key_doors_num)
queue.append(builder)
@@ -1976,14 +2051,14 @@ def shuffle_small_key_doors(door_type_pools, used_doors, start_regions_map, worl
return used_doors
def shuffle_bomb_dash_doors(door_type_pools, used_doors, start_regions_map, world, player):
def shuffle_bomb_dash_doors(door_type_pools, used_doors, start_regions_map, all_custom, world, player):
for pool, door_type_pool in door_type_pools:
ttl = 0
suggestion_map, bd_map = {}, {}
remaining_bomb = door_type_pool.bombable
remaining_dash = door_type_pool.dashable
if player in world.custom_door_types:
if player in world.custom_door_types and 'Bomb Door' in world.custom_door_types[player]:
custom_bomb_doors = world.custom_door_types[player]['Bomb Door']
custom_dash_doors = world.custom_door_types[player]['Dash Door']
else:
@@ -1993,11 +2068,9 @@ def shuffle_bomb_dash_doors(door_type_pools, used_doors, start_regions_map, worl
for dungeon in pool:
builder = world.dungeon_layouts[player][dungeon]
find_bd_candidates(builder, start_regions_map[dungeon], used_doors, world, player)
if custom_bomb_doors[dungeon]:
builder.candidates.bomb_dash = filter_key_door_pool(builder.candidates.bomb_dash, custom_bomb_doors[dungeon])
if all_custom[dungeon]:
builder.candidates.bomb_dash = filter_key_door_pool(builder.candidates.bomb_dash, all_custom[dungeon])
remaining_bomb -= len(custom_bomb_doors[dungeon])
if custom_dash_doors[dungeon]:
builder.candidates.bomb_dash = filter_key_door_pool(builder.candidates.bomb_dash, custom_dash_doors[dungeon])
remaining_dash -= len(custom_dash_doors[dungeon])
ttl += len(builder.candidates.bomb_dash)
if ttl == 0:
@@ -2094,7 +2167,7 @@ def find_trappable_candidates(builder, world, player):
if ext.door and ext.door.type in [DoorType.Interior, DoorType.Normal]]
for d in filtered_doors:
# I only support the first 3 due to the trapFlag right now
if 0 <= d.doorListPos < 3 and not d.entranceFlag:
if 0 <= d.doorListPos < 3 and not d.entranceFlag and d.name != 'Skull Small Hall WS':
room = world.get_room(d.roomIndex, player)
kind = room.kind(d)
if d.type == DoorType.Interior:
@@ -2115,20 +2188,21 @@ def find_trappable_candidates(builder, world, player):
for ext in world.get_region(r, player).exits:
if ext.door:
d = ext.door
if d.blocked and d.trapFlag != 0 and 'Boss' not in d.name and 'Agahnim' not in d.name:
if d.blocked and d.trapFlag != 0 and exclude_boss_traps(d):
builder.candidates.trap.append(d)
def find_valid_trap_combination(builder, suggested, start_regions, paths, world, player, drop=True):
trap_door_pool = builder.candidates.trap
trap_doors_needed = suggested
if player in world.custom_door_types:
if player in world.custom_door_types and 'Trap Door' in world.custom_door_types[player]:
custom_trap_doors = world.custom_door_types[player]['Trap Door'][builder.name]
else:
custom_trap_doors = []
if custom_trap_doors:
trap_door_pool = filter_key_door_pool(trap_door_pool, custom_trap_doors)
trap_doors_needed -= len(custom_trap_doors)
trap_doors_needed = max(0, trap_doors_needed)
if len(trap_door_pool) < trap_doors_needed:
if not drop:
return None, 0
@@ -2139,7 +2213,7 @@ def find_valid_trap_combination(builder, suggested, start_regions, paths, world,
proposal = kth_combination(sample_list[itr], trap_door_pool, trap_doors_needed)
proposal.extend(custom_trap_doors)
start_regions = filter_start_regions(builder, start_regions, world, player)
start_regions, event_starts = filter_start_regions(builder, start_regions, world, player)
while not validate_trap_layout(proposal, builder, start_regions, paths, world, player):
itr += 1
if itr >= len(sample_list):
@@ -2160,14 +2234,32 @@ def find_valid_trap_combination(builder, suggested, start_regions, paths, world,
# eliminate start region if portal marked as destination
def filter_start_regions(builder, start_regions, world, player):
std_flag = world.mode[player] == 'standard' and builder.name == 'Hyrule Castle'
excluded = {}
excluded = {} # todo: drop lobbies, might be better to white list instead (two entrances per region)
event_doors = {}
for region in start_regions:
portal = next((x for x in world.dungeon_portals[player] if x.door.entrance.parent_region == region), None)
if portal and portal.destination:
excluded[region] = None
# make sure that a drop is not accessible for this "destination"
drop_region = next((x.parent_region for x in region.entrances
if x.parent_region.type in [RegionType.LightWorld, RegionType.DarkWorld]
or x.parent_region.name == 'Sewer Drop'), None)
if not drop_region:
excluded[region] = None
if portal and not portal.destination:
portal_entrance_region = portal.door.entrance.parent_region.name
if portal_entrance_region not in builder.path_entrances:
excluded[region] = None
if std_flag and (not portal or portal.find_portal_entrance().parent_region.name != 'Hyrule Castle Courtyard'):
excluded[region] = None
return [x for x in start_regions if x not in excluded.keys()]
if portal is None:
entrance = next((x for x in region.entrances
if x.parent_region.type in [RegionType.LightWorld, RegionType.DarkWorld]
or x.parent_region.name == 'Sewer Drop'), None)
event_doors[entrance] = None
else:
event_doors[portal.find_portal_entrance()] = None
return [x for x in start_regions if x not in excluded.keys()], event_doors
def validate_trap_layout(proposal, builder, start_regions, paths, world, player):
@@ -2193,6 +2285,7 @@ def find_bk_special_location(builder, world, player):
return loc
return None
def check_required_paths_with_traps(paths, proposal, dungeon_name, start_regions, world, player):
cached_initial_state = None
if len(paths[dungeon_name]) > 0:
@@ -2238,7 +2331,7 @@ def reassign_trap_doors(trap_map, world, player):
logger = logging.getLogger('')
for name, traps in trap_map.items():
builder = world.dungeon_layouts[player][name]
queue = deque(find_current_trap_doors(builder))
queue = deque(find_current_trap_doors(builder, world, player))
while len(queue) > 0:
d = queue.pop()
if d.type is DoorType.Interior and d not in traps:
@@ -2257,16 +2350,21 @@ def reassign_trap_doors(trap_map, world, player):
d.blocked = False
for d in traps:
change_door_to_trap(d, world, player)
world.spoiler.set_door_type(d.name, 'Trap Door', player)
logger.debug('Trap Door: %s', d.name)
world.spoiler.set_door_type(f'{d.name} ({d.dungeon_name()})', 'Trap Door', player)
logger.debug(f'Trap Door: {d.name} ({d.dungeon_name()})')
def find_current_trap_doors(builder):
def exclude_boss_traps(d):
return ' Boss ' not in d.name and ' Agahnim ' not in d.name and d.name not in ['Skull Spike Corner SW']
def find_current_trap_doors(builder, world, player):
checker = exclude_boss_traps if world.trap_door_mode[player] in ['vanilla', 'optional'] else (lambda x: True)
current_doors = []
for region in builder.master_sector.regions:
for ext in region.exits:
d = ext.door
if d and d.blocked and d.trapFlag != 0:
if d and d.blocked and d.trapFlag != 0 and checker(d):
current_doors.append(d)
return current_doors
@@ -2276,7 +2374,9 @@ def change_door_to_trap(d, world, player):
if d.type is DoorType.Interior:
kind = room.kind(d)
new_kind = None
if kind == DoorKind.TrapTriggerable and d.direction in [Direction.South, Direction.East]:
if kind == DoorKind.Trap:
new_kind = DoorKind.Trap
elif kind == DoorKind.TrapTriggerable and d.direction in [Direction.South, Direction.East]:
new_kind = DoorKind.Trap
elif kind == DoorKind.Trap2 and d.direction in [Direction.North, Direction.West]:
new_kind = DoorKind.Trap
@@ -2356,21 +2456,16 @@ def find_big_key_door_candidates(region, checked, used, world, player):
if valid and d.dest not in candidates: # interior doors are not separable yet
candidates.append(d.dest)
elif d.type == DoorType.Normal:
if decoupled:
valid = kind in okay_normals
else:
valid = kind in okay_normals
if valid and not decoupled:
d2 = d.dest
if d2 not in candidates and d2 not in used:
if d2.type == DoorType.Normal:
room_b = world.get_room(d2.roomIndex, player)
pos_b, kind_b = room_b.doorList[d2.doorListPos]
valid = kind in okay_normals and kind_b in okay_normals and valid_key_door_pair(d, d2)
else:
valid = kind in okay_normals
valid &= kind_b in okay_normals and valid_key_door_pair(d, d2)
if valid and 0 <= d2.doorListPos < 4:
candidates.append(d2)
else:
valid = True
if valid and d not in candidates:
candidates.append(d)
connected = ext.connected_region
@@ -2384,13 +2479,14 @@ def find_big_key_door_candidates(region, checked, used, world, player):
def find_valid_bk_combination(builder, suggested, start_regions, world, player, drop=True):
bk_door_pool = builder.candidates.big
bk_doors_needed = suggested
if player in world.custom_door_types:
if player in world.custom_door_types and 'Big Key Door' in world.custom_door_types[player]:
custom_bk_doors = world.custom_door_types[player]['Big Key Door'][builder.name]
else:
custom_bk_doors = []
if custom_bk_doors:
bk_door_pool = filter_key_door_pool(bk_door_pool, custom_bk_doors)
bk_doors_needed -= len(custom_bk_doors)
bk_doors_needed = max(0, bk_doors_needed)
if len(bk_door_pool) < bk_doors_needed:
if not drop:
return None, 0
@@ -2401,7 +2497,7 @@ def find_valid_bk_combination(builder, suggested, start_regions, world, player,
proposal = kth_combination(sample_list[itr], bk_door_pool, bk_doors_needed)
proposal.extend(custom_bk_doors)
start_regions = filter_start_regions(builder, start_regions, world, player)
start_regions, event_starts = filter_start_regions(builder, start_regions, world, player)
while not validate_bk_layout(proposal, builder, start_regions, world, player):
itr += 1
if itr >= len(sample_list):
@@ -2452,17 +2548,20 @@ def reassign_big_key_doors(bk_map, world, player):
if d1.type is DoorType.Interior:
change_door_to_big_key(d1, world, player)
d2.bigKey = True # ensure flag is set
if d2.smallKey:
d2.smallKey = False
else:
world.paired_doors[player].append(PairedDoor(d1.name, d2.name))
change_door_to_big_key(d1, world, player)
change_door_to_big_key(d2, world, player)
world.spoiler.set_door_type(d1.name+' <-> '+d2.name, 'Big Key Door', player)
logger.debug(f'Big Key Door: {d1.name} <-> {d2.name}')
world.spoiler.set_door_type(f'{d1.name} <-> {d2.name} ({d1.dungeon_name()})', 'Big Key Door', player)
logger.debug(f'Big Key Door: {d1.name} <-> {d2.name} ({d1.dungeon_name()})')
else:
d = obj
if d.type is DoorType.Interior:
change_door_to_big_key(d, world, player)
d.dest.bigKey = True # ensure flag is set
if world.door_type_mode[player] != 'original':
d.dest.bigKey = True # ensure flag is set when bk doors are double sided
elif d.type is DoorType.SpiralStairs:
pass # we don't have spiral stairs candidates yet that aren't already key doors
elif d.type is DoorType.Normal:
@@ -2473,12 +2572,14 @@ def reassign_big_key_doors(bk_map, world, player):
if stateful_door(d.dest, dest_room.kind(d.dest)):
change_door_to_big_key(d.dest, world, player)
add_pair(d, d.dest, world, player)
world.spoiler.set_door_type(d.name, 'Big Key Door', player)
logger.debug(f'Big Key Door: {d.name}')
world.spoiler.set_door_type(f'{d.name} ({d.dungeon_name()})', 'Big Key Door', player)
logger.debug(f'Big Key Door: {d.name} ({d.dungeon_name()})')
def change_door_to_big_key(d, world, player):
d.bigKey = True
if d.smallKey:
d.smallKey = False
room = world.get_room(d.roomIndex, player)
if room.doorList[d.doorListPos][1] != DoorKind.BigKey:
verify_door_list_pos(d, room, world, player)
@@ -2522,14 +2623,14 @@ def find_valid_combination(builder, target, start_regions, world, player, drop_k
logger = logging.getLogger('')
key_door_pool = list(builder.candidates.small)
key_doors_needed = target
if player in world.custom_door_types:
if player in world.custom_door_types and 'Key Door' in world.custom_door_types[player]:
custom_key_doors = world.custom_door_types[player]['Key Door'][builder.name]
else:
custom_key_doors = []
if custom_key_doors: # could validate that each custom item is in the candidates
key_door_pool = filter_key_door_pool(key_door_pool, custom_key_doors)
key_doors_needed -= len(custom_key_doors)
key_doors_needed = max(0, key_doors_needed)
# find valid combination of candidates
if len(key_door_pool) < key_doors_needed:
if not drop_keys:
@@ -2544,9 +2645,10 @@ def find_valid_combination(builder, target, start_regions, world, player, drop_k
sample_list = build_sample_list(combinations)
proposal = kth_combination(sample_list[itr], key_door_pool, key_doors_needed)
proposal.extend(custom_key_doors)
start_regions = filter_start_regions(builder, start_regions, world, player)
builder.key_doors_num = len(proposal)
start_regions, event_starts = filter_start_regions(builder, start_regions, world, player)
key_layout = build_key_layout(builder, start_regions, proposal, world, player)
key_layout = build_key_layout(builder, start_regions, proposal, event_starts, world, player)
determine_prize_lock(key_layout, world, player)
while not validate_key_layout(key_layout, world, player):
itr += 1
@@ -2572,12 +2674,11 @@ def find_valid_combination(builder, target, start_regions, world, player, drop_k
# make changes
if player not in world.key_logic.keys():
world.key_logic[player] = {}
builder.total_keys = builder.key_doors_num
analyze_dungeon(key_layout, world, player)
builder.key_door_proposal = proposal
world.key_logic[player][builder.name] = key_layout.key_logic
world.key_layout[player][builder.name] = key_layout
return builder.key_door_proposal, key_doors_needed
return builder.key_door_proposal, key_doors_needed + len(custom_key_doors)
def find_bd_candidates(builder, start_regions, used, world, player):
@@ -2618,25 +2719,21 @@ def find_bd_door_candidates(region, checked, used, world, player):
room = world.get_room(d.roomIndex, player)
position, kind = room.doorList[d.doorListPos]
if d.type == DoorType.Interior:
valid = kind in okay_interiors
if valid and d.dest not in candidates: # interior doors are not separable yet
# interior doors are not separable yet
valid = kind in okay_interiors and d.dest not in used
if valid and d.dest not in candidates:
candidates.append(d.dest)
elif d.type == DoorType.Normal:
if decoupled:
valid = kind in okay_normals
else:
valid = kind in okay_normals
if valid and not decoupled:
d2 = d.dest
if d2 not in candidates and d2 not in used:
if d2.type == DoorType.Normal:
room_b = world.get_room(d2.roomIndex, player)
pos_b, kind_b = room_b.doorList[d2.doorListPos]
valid = kind in okay_normals and kind_b in okay_normals and valid_key_door_pair(d, d2)
else:
valid = kind in okay_normals
valid &= kind_b in okay_normals and valid_key_door_pair(d, d2)
if valid and 0 <= d2.doorListPos < 4:
candidates.append(d2)
else:
valid = True
if valid and d not in candidates:
candidates.append(d)
connected = ext.connected_region
@@ -2654,7 +2751,7 @@ def find_valid_bd_combination(builder, suggested, world, player):
bd_door_pool = builder.candidates.bomb_dash
bomb_doors_needed, dash_doors_needed = suggested
ttl_needed = bomb_doors_needed + dash_doors_needed
if player in world.custom_door_types:
if player in world.custom_door_types and 'Bomb Door' in world.custom_door_types[player]:
custom_bomb_doors = world.custom_door_types[player]['Bomb Door'][builder.name]
custom_dash_doors = world.custom_door_types[player]['Dash Door'][builder.name]
else:
@@ -2667,8 +2764,15 @@ def find_valid_bd_combination(builder, suggested, world, player):
bd_door_pool = filter_key_door_pool(bd_door_pool, custom_dash_doors)
dash_doors_needed -= len(custom_dash_doors)
while len(bd_door_pool) < bomb_doors_needed + dash_doors_needed:
bomb_doors_needed = round(len(bd_door_pool) * bomb_doors_needed/ttl_needed)
dash_doors_needed = round(len(bd_door_pool) * dash_doors_needed/ttl_needed)
test = random.choice([True, False])
if test:
bomb_doors_needed -= 1
if bomb_doors_needed < 0:
bomb_doors_needed = 0
else:
dash_doors_needed -= 1
if dash_doors_needed < 0:
dash_doors_needed = 0
bomb_proposal = random.sample(bd_door_pool, k=bomb_doors_needed)
bomb_proposal.extend(custom_bomb_doors)
dash_pool = [x for x in bd_door_pool if x not in bomb_proposal]
@@ -2696,8 +2800,8 @@ def reassign_bd_doors(bd_map, world, player):
elif d.type is DoorType.Normal and not_in_proposal(d):
if not d.entranceFlag:
world.get_room(d.roomIndex, player).change(d.doorListPos, DoorKind.Normal)
do_bombable_dashable(flat_bomb_proposal, DoorKind.Bombable, world, player)
do_bombable_dashable(flat_dash_proposal, DoorKind.Dashable, world, player)
do_bombable_dashable(pair[0], DoorKind.Bombable, world, player)
do_bombable_dashable(pair[1], DoorKind.Dashable, world, player)
def do_bombable_dashable(proposal, kind, world, player):
@@ -2723,7 +2827,7 @@ def do_bombable_dashable(proposal, kind, world, player):
change_door_to_kind(d1, kind, world, player)
change_door_to_kind(d2, kind, world, player)
spoiler_type = 'Bomb Door' if kind == DoorKind.Bombable else 'Dash Door'
world.spoiler.set_door_type(d1.name+' <-> '+d2.name, spoiler_type, player)
world.spoiler.set_door_type(f'{d1.name} <-> {d2.name} ({d1.dungeon_name()})', spoiler_type, player)
else:
d = obj
if d.type is DoorType.Interior:
@@ -2737,7 +2841,7 @@ def do_bombable_dashable(proposal, kind, world, player):
change_door_to_kind(d.dest, kind, world, player)
add_pair(d, d.dest, world, player)
spoiler_type = 'Bomb Door' if kind == DoorKind.Bombable else 'Dash Door'
world.spoiler.set_door_type(d.name, spoiler_type, player)
world.spoiler.set_door_type(f'{d.name} ({d.dungeon_name()})', spoiler_type, player)
def find_current_bd_doors(builder, world):
@@ -2852,27 +2956,23 @@ def find_key_door_candidates(region, checked, used, world, player):
room = world.get_room(d.roomIndex, player)
position, kind = room.doorList[d.doorListPos]
if d.type == DoorType.Interior:
valid = kind in okay_interiors
if valid and d.dest not in candidates: # interior doors are not separable yet
valid = kind in okay_interiors and d.dest not in used
# interior doors are not separable yet
if valid and d.dest not in candidates:
candidates.append(d.dest)
elif d.type == DoorType.SpiralStairs:
valid = kind in [DoorKind.StairKey, DoorKind.StairKey2, DoorKind.StairKeyLow]
elif d.type == DoorType.Normal:
if decoupled:
valid = kind in okay_normals
else:
valid = kind in okay_normals
if valid and not decoupled:
d2 = d.dest
if d2 not in candidates and d2 not in used:
if d2.type == DoorType.Normal:
room_b = world.get_room(d2.roomIndex, player)
pos_b, kind_b = room_b.doorList[d2.doorListPos]
valid = kind in okay_normals and kind_b in okay_normals and valid_key_door_pair(d, d2)
else:
valid = kind in okay_normals
valid &= kind_b in okay_normals and valid_key_door_pair(d, d2)
if valid and 0 <= d2.doorListPos < 4:
candidates.append(d2)
else:
valid = True
if valid and d not in candidates:
candidates.append(d)
connected = ext.connected_region
@@ -2944,8 +3044,8 @@ def reassign_key_doors(small_map, world, player):
world.paired_doors[player].append(PairedDoor(d1.name, d2.name))
change_door_to_small_key(d1, world, player)
change_door_to_small_key(d2, world, player)
world.spoiler.set_door_type(d1.name+' <-> '+d2.name, 'Key Door', player)
logger.debug('Key Door: %s', d1.name+' <-> '+d2.name)
world.spoiler.set_door_type(f'{d1.name} <-> {d2.name} ({d1.dungeon_name()})', 'Key Door', player)
logger.debug(f'Key Door: {d1.name} <-> {d2.name} ({d1.dungeon_name()})')
else:
d = obj
if d.type is DoorType.Interior:
@@ -2961,8 +3061,8 @@ def reassign_key_doors(small_map, world, player):
if stateful_door(d.dest, dest_room.kind(d.dest)):
change_door_to_small_key(d.dest, world, player)
add_pair(d, d.dest, world, player)
world.spoiler.set_door_type(d.name, 'Key Door', player)
logger.debug('Key Door: %s', d.name)
world.spoiler.set_door_type(f'{d.name} ({d.dungeon_name()})', 'Key Door', player)
logger.debug(f'Key Door: {d.name} ({d.dungeon_name()})')
def change_door_to_small_key(d, world, player):
@@ -3152,7 +3252,7 @@ def change_pair_type(door, new_type, world, player):
room_b.change(door.dest.doorListPos, new_type)
add_pair(door, door.dest, world, player)
spoiler_type = 'Bomb Door' if new_type == DoorKind.Bombable else 'Dash Door'
world.spoiler.set_door_type(door.name + ' <-> ' + door.dest.name, spoiler_type, player)
world.spoiler.set_door_type(f'{door.name} <-> {door.dest.name} ({door.dungeon_name()})', spoiler_type, player)
def remove_pair_type_if_present(door, world, player):
@@ -3245,6 +3345,14 @@ def find_accessible_entrances(world, player, builder):
return visited_entrances
def find_possible_entrances(world, player, builder):
entrances = [region.name for region in
(portal.door.entrance.parent_region for portal in world.dungeon_portals[player])
if region.dungeon.name == builder.name]
entrances.extend(drop_entrances[builder.name])
return entrances
def valid_inaccessible_region(r):
return r.type is not RegionType.Cave or (len(r.exits) > 0 and r.name not in ['Links House', 'Chris Houlihan Room'])
@@ -4216,7 +4324,7 @@ default_door_connections = [
('Eastern Map Valley SW', 'Eastern Dark Square NW'),
('Eastern Attic Start WS', 'Eastern False Switches ES'),
('Eastern Cannonball Hell WS', 'Eastern Single Eyegore ES'),
('Desert Compass NW', 'Desert Cannonball S'),
('Desert Compass NE', 'Desert Cannonball S'),
('Desert Beamos Hall NE', 'Desert Tiles 2 SE'),
('PoD Middle Cage N', 'PoD Pit Room S'),
('PoD Pit Room NW', 'PoD Arena Main SW'),
@@ -4503,7 +4611,7 @@ door_type_counts = {
'Swamp Palace': (6, 0, 0, 2, 0, 0, 0),
'Palace of Darkness': (6, 1, 1, 3, 2, 0, 0),
'Misery Mire': (6, 3, 5, 2, 0, 0, 0),
'Skull Woods': (5, 0, 2, 2, 0, 1, 0),
'Skull Woods': (5, 0, 1, 2, 0, 1, 0),
'Ice Palace': (6, 1, 3, 0, 0, 0, 0),
'Tower of Hera': (1, 1, 0, 0, 0, 0, 0),
'Thieves Town': (3, 1, 2, 1, 1, 0, 0),
+6 -3
View File
@@ -66,7 +66,7 @@ def create_doors(world, player):
create_door(player, 'Hyrule Castle Back Hall Down Stairs', Sprl).dir(Dn, 0x01, 0, HTL).ss(A, 0x2a, 0x00),
create_door(player, 'Hyrule Castle Throne Room Tapestry', Lgcl),
create_door(player, 'Hyrule Castle Tapestry Backwards', Lgcl),
create_door(player, 'Hyrule Castle Throne Room N', Nrml).dir(No, 0x51, Mid, High).pos(1),
create_door(player, 'Hyrule Castle Throne Room N', Nrml).dir(No, 0x51, Mid, High).pos(0),
create_door(player, 'Hyrule Castle Throne Room South Stairs', StrS).dir(So, 0x51, Mid, Low),
# hyrule dungeon level
@@ -207,7 +207,7 @@ def create_doors(world, player):
create_door(player, 'Desert East Wing ES', Intr).dir(Ea, 0x85, Bot, High).pos(3),
create_door(player, 'Desert East Wing Key Door EN', Intr).dir(Ea, 0x85, Top, High).small_key().pos(1),
create_door(player, 'Desert Compass Key Door WN', Intr).dir(We, 0x85, Top, High).small_key().pos(1),
create_door(player, 'Desert Compass NW', Nrml).dir(No, 0x85, Right, High).trap(0x4).pos(0),
create_door(player, 'Desert Compass NE', Nrml).dir(No, 0x85, Right, High).trap(0x4).pos(0),
create_door(player, 'Desert Cannonball S', Nrml).dir(So, 0x75, Right, High).pos(1).portal(X, 0x02),
create_door(player, 'Desert Arrow Pot Corner S Edge', Open).dir(So, 0x75, None, High).edge(6, Z, 0x20),
create_door(player, 'Desert Arrow Pot Corner W Edge', Open).dir(We, 0x75, None, High).edge(2, Z, 0x20),
@@ -780,7 +780,7 @@ def create_doors(world, player):
create_door(player, 'Ice Freezors Hole', Hole),
create_door(player, 'Ice Freezors Bomb Hole', Hole), # combine these two? -- they have to lead to the same spot
create_door(player, 'Ice Freezors Ledge Hole', Hole),
create_door(player, 'Ice Freezors Ledge ES', Intr).dir(Ea, 0x7e, Bot, High).pos(2),
create_door(player, 'Ice Freezors Ledge ES', Intr).dir(Ea, 0x7e, Bot, High).pos(1),
create_door(player, 'Ice Tall Hint WS', Intr).dir(We, 0x7e, Bot, High).pos(1),
create_door(player, 'Ice Tall Hint EN', Nrml).dir(Ea, 0x7e, Top, High).pos(2),
create_door(player, 'Ice Tall Hint SE', Nrml).dir(So, 0x7e, Right, High).small_key().pos(0).portal(X, 0x02),
@@ -1478,6 +1478,8 @@ def create_doors(world, player):
world.get_door('GT Spike Crystal Right to Left Barrier - Orange', player).barrier(CrystalBarrier.Orange)
world.get_door('GT Spike Crystal Left to Right Bypass', player).barrier(CrystalBarrier.Blue)
world.get_door('Sanctuary Mirror Route', player).barrier(CrystalBarrier.Orange)
# kill certain doors
if world.intensity[player] == 1: # due to ladder & warp being fixed
world.get_door('PoD Mimics 2 SW', player).kill()
@@ -1524,6 +1526,7 @@ def create_doors(world, player):
world.get_door("GT Bob\'s Room SE", player).passage = False
world.get_door('Desert Tiles 2 SE', player).bk_shuffle_req = True # key-drop note: allows this to be a portal
world.get_door('Swamp Lobby S', player).standard_restricted = True
world.get_door('Sanctuary S', player).standard_restricted = True
world.get_door('PoD Mimics 2 SW', player).rupee_bow_restricted = True # bow statue
# enemizer logic could get rid of the following restriction
world.get_door('PoD Pit Room S', player).rupee_bow_restricted = True # so mimics 1 shouldn't be required
+53 -19
View File
@@ -10,9 +10,9 @@ import time
from typing import List
from BaseClasses import DoorType, Direction, CrystalBarrier, RegionType, Polarity, PolSlot, flooded_keys, Sector
from BaseClasses import Hook, hook_from_door
from BaseClasses import Hook, hook_from_door, Door
from Regions import dungeon_events, flooded_keys_reverse
from Dungeons import dungeon_regions, split_region_starts
from Dungeons import split_region_starts
from RoomData import DoorKind
from source.dungeon.DungeonStitcher import generate_dungeon_find_proposal
@@ -772,6 +772,13 @@ def connect_simple_door(exit_door, region):
special_big_key_doors = ['Hyrule Dungeon Cellblock Door', "Thieves Blind's Cell Door"]
std_special_big_key_doors = ['Hyrule Castle Throne Room Tapestry'] + special_big_key_doors
def get_special_big_key_doors(world, player):
if world.mode[player] == 'standard':
return std_special_big_key_doors
return special_big_key_doors
class ExplorationState(object):
@@ -846,13 +853,21 @@ class ExplorationState(object):
ret.prize_received = self.prize_received
return ret
def init_zelda_event_doors(self, event_starts, player):
for entrance in event_starts:
event_door = Door(player, entrance.name, DoorType.Logical)
event_door.req_event = 'Zelda Drop Off'
event_door.entrance = entrance
event_door.crystal = CrystalBarrier.Orange # always start in orange
self.append_door_to_list(event_door, self.event_doors)
def next_avail_door(self):
self.avail_doors.sort(key=lambda x: 0 if x.flag else 1 if x.door.bigKey else 2)
exp_door = self.avail_doors.pop()
self.crystal = exp_door.crystal
return exp_door
def visit_region(self, region, key_region=None, key_checks=False, bk_Flag=False):
def visit_region(self, region, key_region=None, key_checks=False, bk_flag=False):
if region.type != RegionType.Dungeon:
self.crystal = CrystalBarrier.Orange
if self.crystal == CrystalBarrier.Either:
@@ -873,7 +888,7 @@ class ExplorationState(object):
self.ttl_locations += 1
if location not in self.found_locations:
self.found_locations.append(location)
if not bk_Flag and (not location.forced_item or 'Big Key' in location.item.name):
if not bk_flag and (not location.forced_item or 'Big Key' in location.item.name):
self.bk_found.add(location)
if location.name in dungeon_events and location.name not in self.events:
if self.flooded_key_check(location):
@@ -994,7 +1009,8 @@ class ExplorationState(object):
if self.can_traverse(door):
if door.controller:
door = door.controller
if (door in big_key_door_proposal or door.name in special_big_key_doors) and not self.big_key_opened:
if (door in big_key_door_proposal
or door.name in get_special_big_key_doors(world, player)) and not self.big_key_opened:
if not self.in_door_list(door, self.big_doors):
self.append_door_to_list(door, self.big_doors)
elif door.req_event is not None and door.req_event not in self.events:
@@ -1327,8 +1343,9 @@ def create_dungeon_builders(all_sectors, connections_tuple, world, player, dunge
sector = find_sector(r_name, all_sectors)
reverse_d_map[sector] = key
if world.mode[player] == 'standard':
current_dungeon = dungeon_map['Hyrule Castle']
standard_stair_check(dungeon_map, current_dungeon, candidate_sectors, global_pole)
if 'Hyrule Castle' in dungeon_map:
current_dungeon = dungeon_map['Hyrule Castle']
standard_stair_check(dungeon_map, current_dungeon, candidate_sectors, global_pole)
complete_dungeons = {x: y for x, y in dungeon_map.items() if sum(len(sector.outstanding_doors) for sector in y.sectors) <= 0}
[dungeon_map.pop(key) for key in complete_dungeons.keys()]
@@ -1356,7 +1373,8 @@ def create_dungeon_builders(all_sectors, connections_tuple, world, player, dunge
sanc_builder = random.choice(lw_builders)
assign_sector(sanc, sanc_builder, candidate_sectors, global_pole)
bow_sectors, retro_std_flag = {}, world.bow_mode[player].startswith('retro') and world.mode[player] == 'standard'
retro_std_flag = world.bow_mode[player].startswith('retro') and world.mode[player] == 'standard'
non_hc_sectors = {}
free_location_sectors = {}
crystal_switches = {}
crystal_barriers = {}
@@ -1364,7 +1382,9 @@ def create_dungeon_builders(all_sectors, connections_tuple, world, player, dunge
neutral_sectors = {}
for sector in candidate_sectors:
if retro_std_flag and 'Bow' in sector.item_logic: # these need to be distributed outside of HC
bow_sectors[sector] = None
non_hc_sectors[sector] = None
elif world.mode[player] == 'standard' and 'Open Floodgate' in sector.item_logic:
non_hc_sectors[sector] = None
elif sector.chest_locations > 0:
free_location_sectors[sector] = None
elif sector.c_switch:
@@ -1375,8 +1395,8 @@ def create_dungeon_builders(all_sectors, connections_tuple, world, player, dunge
neutral_sectors[sector] = None
else:
polarized_sectors[sector] = None
if bow_sectors:
assign_bow_sectors(dungeon_map, bow_sectors, global_pole)
if non_hc_sectors:
assign_non_hc_sectors(dungeon_map, non_hc_sectors, global_pole)
leftover = assign_location_sectors_minimal(dungeon_map, free_location_sectors, global_pole, world, player)
free_location_sectors = scatter_extra_location_sectors(dungeon_map, leftover, global_pole)
for sector in free_location_sectors:
@@ -1420,7 +1440,8 @@ def create_dungeon_builders(all_sectors, connections_tuple, world, player, dunge
def standard_stair_check(dungeon_map, dungeon, candidate_sectors, global_pole):
# this is because there must be at least one non-dead stairway in hc to get out
# this check may not be necessary
filtered_sectors = [x for x in candidate_sectors if any(y for y in x.outstanding_doors if not y.dead and y.type == DoorType.SpiralStairs)]
filtered_sectors = [x for x in candidate_sectors if 'Open Floodgate' not in x.item_logic and
any(y for y in x.outstanding_doors if not y.dead and y.type == DoorType.SpiralStairs)]
valid = False
while not valid:
chosen_sector = random.choice(filtered_sectors)
@@ -1550,7 +1571,7 @@ def define_sector_features(sectors):
sector.bk_required = True
for ext in region.exits:
door = ext.door
if door is not None:
if door is not None and not door.blocked:
if door.crystal == CrystalBarrier.Either:
sector.c_switch = True
elif door.crystal == CrystalBarrier.Orange:
@@ -1562,6 +1583,8 @@ def define_sector_features(sectors):
if region.name in ['PoD Mimics 2', 'PoD Bow Statue Right', 'PoD Mimics 1', 'GT Mimics 1', 'GT Mimics 2',
'Eastern Single Eyegore', 'Eastern Duo Eyegores']:
sector.item_logic.add('Bow')
if region.name in ['Swamp Lobby', 'Swamp Entrance']:
sector.item_logic.add('Open Floodgate')
def assign_sector(sector, dungeon, candidate_sectors, global_pole):
@@ -1613,8 +1636,8 @@ def find_sector(r_name, sectors):
return None
def assign_bow_sectors(dungeon_map, bow_sectors, global_pole):
sector_list = list(bow_sectors)
def assign_non_hc_sectors(dungeon_map, non_hc_sectors, global_pole):
sector_list = list(non_hc_sectors)
random.shuffle(sector_list)
population = []
for name in dungeon_map:
@@ -1623,7 +1646,7 @@ def assign_bow_sectors(dungeon_map, bow_sectors, global_pole):
choices = random.choices(population, k=len(sector_list))
for i, choice in enumerate(choices):
builder = dungeon_map[choice]
assign_sector(sector_list[i], builder, bow_sectors, global_pole)
assign_sector(sector_list[i], builder, non_hc_sectors, global_pole)
def scatter_extra_location_sectors(dungeon_map, free_location_sectors, global_pole):
@@ -1801,6 +1824,7 @@ def ensure_crystal_switches_reachable(dungeon_map, crystal_switches, polarized_s
for name, builder in dungeon_map.items():
if builder.c_switch_present and builder.c_switch_required and not builder.c_locked:
invalid_builders.append(builder)
random.shuffle(invalid_builders)
while len(invalid_builders) > 0:
valid_builders = []
for builder in invalid_builders:
@@ -1826,6 +1850,7 @@ def ensure_crystal_switches_reachable(dungeon_map, crystal_switches, polarized_s
if eq.c_switch:
reachable_crystals[hook_from_door(eq.door)] = True
valid_ent_sectors = []
random.shuffle(entrance_sectors)
for entrance_sector in entrance_sectors:
other_sectors = [x for x in builder.sectors if x != entrance_sector]
reachable, access = is_c_switch_reachable(entrance_sector, reachable_crystals, other_sectors)
@@ -1843,7 +1868,12 @@ def ensure_crystal_switches_reachable(dungeon_map, crystal_switches, polarized_s
while not valid:
if len(candidates) <= 0:
raise GenerationException(f'need to provide more sophisticated crystal connection for {entrance_sector}')
sector, which_list = random.choice(list(candidates.items()))
# prioritize candidates
if any(x == 'Crystals' for x in candidates.values()):
cand_list = [x for x in candidates.items() if x[1] == 'Crystals']
else:
cand_list = list(candidates.items())
sector, which_list = random.choice(cand_list)
del candidates[sector]
valid = global_pole.is_valid_choice(dungeon_map, builder, [sector])
if which_list == 'Polarized':
@@ -3494,6 +3524,8 @@ def identify_branching_issues(dungeon_map, builder_info):
unconnected_builders[name] = builder
for hook, door_list in unreached_doors.items():
builder.unfulfilled[hook] += len(door_list)
elif package:
builder.throne_door, builder.throne_sector, builder.chosen_lobby = package
return unconnected_builders
@@ -3511,7 +3543,8 @@ def check_for_valid_layout(builder, sector_list, builder_info):
for portal in world.dungeon_portals[player]:
if not portal.destination and portal.name in dungeon_portals[builder.name]:
possible_regions.add(portal.door.entrance.parent_region.name)
if builder.name in dungeon_drops.keys():
if builder.name in dungeon_drops.keys() and (builder.name != 'Hyrule Castle'
or world.mode[player] != 'standard'):
possible_regions.update(dungeon_drops[builder.name])
independents = find_independent_entrances(possible_regions, world, player)
for name, split_build in builder.split_dungeon_map.items():
@@ -3538,7 +3571,8 @@ def check_for_valid_layout(builder, sector_list, builder_info):
split_list['Sewers'].remove(temp_builder.throne_door.entrance.parent_region.name)
builder.exception_list = list(sector_list)
return True, {}, package
except (GenerationException, NeutralizingException, OtherGenException):
except (GenerationException, NeutralizingException, OtherGenException) as e:
logging.getLogger('').debug(f'Bailing on this layout for {builder.name}', exc_info=1)
builder.split_dungeon_map = None
builder.valid_proposal = None
if temp_builder.name == 'Hyrule Castle' and temp_builder.throne_door:
+8 -4
View File
@@ -2626,6 +2626,8 @@ mandatory_connections = [('Links House S&Q', 'Links House'),
('Bumper Cave Entrance Mirror Spot', 'Death Mountain Entrance'),
('Bumper Cave Ledge Drop', 'West Dark World'),
('Bumper Cave Ledge Mirror Spot', 'Death Mountain Return Ledge'),
('Bumper Cave Bottom to Top', 'Bumper Cave (top)'),
('Bumper Cave Top To Bottom', 'Bumper Cave (bottom)'),
('Skull Woods Forest', 'Skull Woods Forest'),
('Desert Ledge Mirror Spot', 'Desert Ledge'),
('Desert Ledge (Northeast) Mirror Spot', 'Desert Ledge (Northeast)'),
@@ -2754,6 +2756,8 @@ inverted_mandatory_connections = [('Links House S&Q', 'Inverted Links House'),
('Bumper Cave Entrance Rock', 'Bumper Cave Entrance'),
('Bumper Cave Entrance Drop', 'West Dark World'),
('Bumper Cave Ledge Drop', 'West Dark World'),
('Bumper Cave Bottom to Top', 'Bumper Cave (top)'),
('Bumper Cave Top To Bottom', 'Bumper Cave (bottom)'),
('Skull Woods Forest', 'Skull Woods Forest'),
('Paradox Cave Push Block Reverse', 'Paradox Cave Chest Area'),
('Paradox Cave Push Block', 'Paradox Cave Front'),
@@ -2977,8 +2981,8 @@ default_connections = [('Links House', 'Links House'),
('C-Shaped House', 'C-Shaped House'),
('Chest Game', 'Chest Game'),
('Dark World Hammer Peg Cave', 'Dark World Hammer Peg Cave'),
('Bumper Cave (Bottom)', 'Bumper Cave'),
('Bumper Cave (Top)', 'Bumper Cave'),
('Bumper Cave (Bottom)', 'Bumper Cave (bottom)'),
('Bumper Cave (Top)', 'Bumper Cave (top)'),
('Red Shield Shop', 'Red Shield Shop'),
('Dark Sanctuary Hint', 'Dark Sanctuary Hint'),
('Fortune Teller (Dark)', 'Fortune Teller (Dark)'),
@@ -3140,7 +3144,7 @@ inverted_default_connections = [('Waterfall of Wishing', 'Waterfall of Wishing'
('Inverted Big Bomb Shop', 'Inverted Big Bomb Shop'),
('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 (bottom)'),
('Old Man Cave (East)', 'Death Mountain Return Cave (left)'),
('Old Man Cave Exit (West)', 'West Dark World'),
('Old Man Cave Exit (East)', 'Dark Death Mountain'),
@@ -3149,7 +3153,7 @@ inverted_default_connections = [('Waterfall of Wishing', 'Waterfall of Wishing'
('Bumper Cave (Top)', 'Dark Death Mountain Healer Fairy'),
('Bumper Cave Exit (Top)', 'Death Mountain Return Ledge'),
('Bumper Cave Exit (Bottom)', 'Light World'),
('Death Mountain Return Cave (West)', 'Bumper Cave'),
('Death Mountain Return Cave (West)', 'Bumper Cave (top)'),
('Death Mountain Return Cave (East)', 'Death Mountain Return Cave (right)'),
('Death Mountain Return Cave Exit (West)', 'Death Mountain'),
('Death Mountain Return Cave Exit (East)', 'Death Mountain'),
+99 -19
View File
@@ -24,7 +24,6 @@ def promote_dungeon_items(world):
item.advancement = True
elif item.map or item.compass:
item.priority = True
dungeon_tracking(world)
def dungeon_tracking(world):
@@ -35,7 +34,6 @@ def dungeon_tracking(world):
def fill_dungeons_restrictive(world, shuffled_locations):
dungeon_tracking(world)
# with shuffled dungeon items they are distributed as part of the normal item pool
for item in world.get_items():
@@ -73,11 +71,13 @@ def fill_dungeons_restrictive(world, shuffled_locations):
def fill_restrictive(world, base_state, locations, itempool, key_pool=None, single_player_placement=False,
vanilla=False):
def sweep_from_pool():
def sweep_from_pool(placing_item=None):
new_state = base_state.copy()
for item in itempool:
new_state.collect(item, True)
new_state.placing_item = placing_item
new_state.sweep_for_events()
new_state.placing_item = None
return new_state
unplaced_items = []
@@ -94,7 +94,7 @@ def fill_restrictive(world, base_state, locations, itempool, key_pool=None, sing
while any(player_items.values()) and locations:
items_to_place = [[itempool.remove(items[-1]), items.pop()][-1] for items in player_items.values() if items]
maximum_exploration_state = sweep_from_pool()
maximum_exploration_state = sweep_from_pool(placing_item=items_to_place[0])
has_beaten_game = world.has_beaten_game(maximum_exploration_state)
for item_to_place in items_to_place:
@@ -105,6 +105,8 @@ def fill_restrictive(world, base_state, locations, itempool, key_pool=None, sing
spot_to_fill = None
item_locations = filter_locations(item_to_place, locations, world, vanilla)
verify(item_to_place, item_locations, maximum_exploration_state, single_player_placement,
perform_access_check, key_pool, world)
for location in item_locations:
spot_to_fill = verify_spot_to_fill(location, item_to_place, maximum_exploration_state,
single_player_placement, perform_access_check, key_pool, world)
@@ -128,9 +130,6 @@ def fill_restrictive(world, base_state, locations, itempool, key_pool=None, sing
raise FillError('No more spots to place %s' % item_to_place)
world.push_item(spot_to_fill, item_to_place, False)
if item_to_place.smallkey:
with suppress(ValueError):
key_pool.remove(item_to_place)
track_outside_keys(item_to_place, spot_to_fill, world)
track_dungeon_items(item_to_place, spot_to_fill, world)
locations.remove(spot_to_fill)
@@ -143,21 +142,29 @@ def verify_spot_to_fill(location, item_to_place, max_exp_state, single_player_pl
key_pool, world):
if item_to_place.smallkey or item_to_place.bigkey: # a better test to see if a key can go there
location.item = item_to_place
location.event = True
if item_to_place.smallkey:
with suppress(ValueError):
key_pool.remove(item_to_place)
test_state = max_exp_state.copy()
test_state.stale[item_to_place.player] = True
else:
test_state = max_exp_state
if not single_player_placement or location.player == item_to_place.player:
test_state.sweep_for_events()
if location.can_fill(test_state, item_to_place, perform_access_check):
if valid_key_placement(item_to_place, location, key_pool, world):
if valid_key_placement(item_to_place, location, key_pool, test_state, world):
if item_to_place.crystal or valid_dungeon_placement(item_to_place, location, world):
return location
if item_to_place.smallkey or item_to_place.bigkey:
location.item = None
location.event = False
if item_to_place.smallkey:
key_pool.append(item_to_place)
return None
def valid_key_placement(item, location, key_pool, world):
def valid_key_placement(item, location, key_pool, collection_state, world):
if not valid_reserved_placement(item, location, world):
return False
if ((not item.smallkey and not item.bigkey) or item.player != location.player
@@ -174,7 +181,15 @@ def valid_key_placement(item, location, key_pool, world):
prize_loc = world.get_location(key_logic.prize_location, location.player)
cr_count = world.crystals_needed_for_gt[location.player]
wild_keys = world.keyshuffle[item.player] != 'none'
return key_logic.check_placement(unplaced_keys, wild_keys, location if item.bigkey else None, prize_loc, cr_count)
if wild_keys:
reached_keys = {x for x in collection_state.locations_checked
if x.item and x.item.name == key_logic.small_key_name and x.item.player == item.player}
else:
reached_keys = set() # will be calculated using key logic in a moment
self_locking_keys = sum(1 for d, rule in key_logic.door_rules.items() if rule.allow_small
and rule.small_location.item and rule.small_location.item.name == key_logic.small_key_name)
return key_logic.check_placement(unplaced_keys, wild_keys, reached_keys, self_locking_keys,
location if item.bigkey else None, prize_loc, cr_count)
else:
return not item.is_inside_dungeon_item(world)
@@ -205,6 +220,7 @@ def track_outside_keys(item, location, world):
if loc_dungeon and loc_dungeon.name == item_dungeon:
return # this is an inside key
world.key_logic[item.player][item_dungeon].outside_keys += 1
world.key_logic[item.player][item_dungeon].outside_keys_locations.add(location)
def track_dungeon_items(item, location, world):
@@ -289,7 +305,7 @@ def last_ditch_placement(item_to_place, locations, world, state, base_state, ite
possible_swaps = [x for x in state.locations_checked if x.item.type == 'Crystal']
else:
possible_swaps = [x for x in state.locations_checked
if x.item.type not in ['Event', 'Crystal'] and not x.forced_item]
if x.item.type not in ['Event', 'Crystal'] and not x.forced_item and not x.locked]
swap_locations = sorted(possible_swaps, key=location_preference)
return try_possible_swaps(swap_locations, item_to_place, locations, world, base_state, itempool,
key_pool, single_player_placement)
@@ -345,7 +361,9 @@ def find_spot_for_item(item_to_place, locations, world, base_state, pool,
test_state = maximum_exploration_state
if (not single_player_placement or location.player == item_to_place.player) \
and location.can_fill(test_state, item_to_place, perform_access_check) \
and valid_key_placement(item_to_place, location, pool if (keys_in_itempool and keys_in_itempool[item_to_place.player]) else world.itempool, world):
and valid_key_placement(item_to_place, location,
pool if (keys_in_itempool and keys_in_itempool[item_to_place.player]) else world.itempool,
test_state, world):
return location
if item_to_place.smallkey or item_to_place.bigkey:
location.item = old_item
@@ -363,6 +381,7 @@ def distribute_items_restrictive(world, gftower_trash=False, fill_locations=None
# handle pot/drop shuffle
random.shuffle(world.itempool)
config_sort(world)
progitempool = [item for item in world.itempool if item.advancement]
prioitempool = [item for item in world.itempool if not item.advancement and item.priority]
restitempool = [item for item in world.itempool if not item.advancement and not item.priority]
@@ -383,7 +402,7 @@ def distribute_items_restrictive(world, gftower_trash=False, fill_locations=None
else:
max_trash = gt_count
scaled_trash = math.floor(max_trash * scale_factor)
if world.goal[player] in ['triforcehunt', 'trinity']:
if world.goal[player] in ['triforcehunt', 'trinity', 'ganonhunt']:
gftower_trash_count = random.randint(scaled_trash, max_trash)
else:
gftower_trash_count = random.randint(0, scaled_trash)
@@ -402,10 +421,16 @@ def distribute_items_restrictive(world, gftower_trash=False, fill_locations=None
random.shuffle(fill_locations)
fill_locations.reverse()
# Make sure the escape small key is placed first in standard with key shuffle to prevent running out of spots
# todo: crossed
progitempool.sort(key=lambda item: 1 if item.name == 'Small Key (Escape)'
and world.keyshuffle[item.player] != 'none' and world.mode[item.player] == 'standard' else 0)
# Make sure the escape keys ire placed first in standard to prevent running out of spots
def std_item_sort(item):
if world.mode[item.player] == 'standard':
if item.name == 'Small Key (Escape)':
return 1
if item.name == 'Big Key (Escape)':
return 2
return 0
progitempool.sort(key=std_item_sort)
key_pool = [x for x in progitempool if x.smallkey]
# sort maps and compasses to the back -- this may not be viable in equitable & ambrosia
@@ -455,6 +480,20 @@ def distribute_items_restrictive(world, gftower_trash=False, fill_locations=None
ensure_good_items(world)
def config_sort(world):
if world.item_pool_config.verify:
config_sort_helper(world, world.item_pool_config.verify)
elif world.item_pool_config.preferred:
config_sort_helper(world, world.item_pool_config.preferred)
def config_sort_helper(world, sort_dict):
pref = list(sort_dict.keys())
pref_len = len(pref)
world.itempool.sort(key=lambda i: pref_len - pref.index((i.name, i.player))
if (i.name, i.player) in sort_dict else 0)
def calc_trash_locations(world, player):
total_count, gt_count = 0, 0
for loc in world.get_locations():
@@ -499,10 +538,17 @@ def fast_fill_helper(world, item_pool, fill_locations):
def fast_fill(world, item_pool, fill_locations):
while item_pool and fill_locations:
config = world.item_pool_config
fast_pool = [x for x in item_pool if (x.name, x.player) not in config.restricted]
filtered_pool = [x for x in item_pool if (x.name, x.player) in config.restricted]
filtered_fill(world, filtered_pool, fill_locations)
while fast_pool and fill_locations:
spot_to_fill = fill_locations.pop()
item_to_place = item_pool.pop()
item_to_place = fast_pool.pop()
world.push_item(spot_to_fill, item_to_place, False)
item_pool.clear()
item_pool.extend(filtered_pool)
item_pool.extend(fast_pool)
def fast_fill_pot_for_multiworld(world, item_pool, fill_locations):
@@ -612,6 +658,40 @@ def sell_keys(world, player):
world.itempool.remove(universal_key)
def verify(item_to_place, item_locations, state, spp, pac, key_pool, world):
if world.item_pool_config.verify:
logger = logging.getLogger('')
item_name = 'Bottle' if item_to_place.name.startswith('Bottle') else item_to_place.name
item_player = item_to_place.player
config = world.item_pool_config
if (item_name, item_player) in config.verify:
tests = config.verify[(item_name, item_player)]
issues = []
for location in item_locations:
if location.name in tests:
expected = tests[location.name]
spot = verify_spot_to_fill(location, item_to_place, state, spp, pac, key_pool, world)
if spot and (item_to_place.smallkey or item_to_place.bigkey):
location.item = None
location.event = False
if item_to_place.smallkey:
key_pool.append(item_to_place)
if (expected and spot) or (not expected and spot is None):
logger.debug(f'Placing {item_name} ({item_player}) at {location.name} was {expected}')
config.verify_count += 1
if config.verify_count >= config.verify_target:
exit()
else:
issues.append((item_name, item_player, location.name, expected))
if len(issues) > 0:
for name, player, loc, expected in issues:
if expected:
logger.error(f'Could not place {name} ({player}) at {loc}')
else:
logger.error(f'{name} ({player}) should not be allowed at {loc}')
raise Exception(f'Test failed placing {name}')
def balance_multiworld_progression(world):
state = CollectionState(world)
checked_locations = set()
+2 -2
View File
@@ -142,8 +142,8 @@ class InitialSram:
'Big Key (Misery Mire)': (0x367, 0x01), 'Compass (Misery Mire)': (0x365, 0x01), 'Map (Misery Mire)': (0x369, 0x01),
'Big Key (Turtle Rock)': (0x366, 0x08), 'Compass (Turtle Rock)': (0x364, 0x08), 'Map (Turtle Rock)': (0x368, 0x08),
'Big Key (Ganons Tower)': (0x366, 0x04), 'Compass (Ganons Tower)': (0x364, 0x04), 'Map (Ganons Tower)': (0x368, 0x04)}
set_or_table = {'Flippers': (0x356, 1, 0x379, 0x02),'Pegasus Boots': (0x355, 1, 0x379, 0x04),
'Shovel': (0x34C, 1, 0x38C, 0x04), 'Ocarina': (0x34C, 3, 0x38C, 0x01),
set_or_table = {'Flippers': (0x356, 1, 0x379, 0x02), 'Pegasus Boots': (0x355, 1, 0x379, 0x04),
'Shovel': (0x34C, 1, 0x38C, 0x04), 'Ocarina': (0x34C, 2, 0x38C, 0x02),
'Ocarina (Activated)': (0x34C, 3, 0x38C, 0x01),
'Mushroom': (0x344, 1, 0x38C, 0x20 | 0x08), 'Magic Powder': (0x344, 2, 0x38C, 0x10),
'Blue Boomerang': (0x341, 1, 0x38C, 0x80), 'Red Boomerang': (0x341, 2, 0x38C, 0x40)}
+2 -1
View File
@@ -184,7 +184,8 @@ def create_inverted_regions(world, player):
create_cave_region(player, 'Chest Game', 'a game of 16 chests', ['Chest Game']),
create_cave_region(player, 'Red Shield Shop', 'the rare shop', ['Red Shield Shop - Left', 'Red Shield Shop - Middle', 'Red Shield Shop - Right']),
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 (bottom)', 'a connector', None, ['Bumper Cave Exit (Bottom)', 'Bumper Cave Bottom to Top']),
create_cave_region(player, 'Bumper Cave (top)', 'a connector', None, ['Bumper Cave Exit (Top)', 'Bumper Cave Top To Bottom']),
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)']),
create_dw_region(player, 'Skull Woods Forest (West)', None, ['Skull Woods Second Section Hole', 'Skull Woods Second Section Door (West)', 'Skull Woods Final Section']),
+261 -71
View File
@@ -6,7 +6,7 @@ import RaceRandom as random
from BaseClasses import Region, RegionType, Shop, ShopType, Location, CollectionState, PotItem
from EntranceShuffle import connect_entrance
from Regions import shop_to_location_table, retro_shops, shop_table_by_location, valid_pot_location
from Fill import FillError, fill_restrictive, get_dungeon_item_pool, is_dungeon_item
from Fill import FillError, fill_restrictive, get_dungeon_item_pool, track_dungeon_items, track_outside_keys
from PotShuffle import vanilla_pots
from Items import ItemFactory
@@ -182,8 +182,12 @@ def get_custom_array_key(item):
def generate_itempool(world, player):
if (world.difficulty[player] not in ['normal', 'hard', 'expert'] or world.goal[player] not in ['ganon', 'pedestal', 'dungeons', 'triforcehunt', 'trinity', 'crystals']
or world.mode[player] not in ['open', 'standard', 'inverted'] or world.timer not in ['none', 'display', 'timed', 'timed-ohko', 'ohko', 'timed-countdown'] or world.progressive not in ['on', 'off', 'random']):
if (world.difficulty[player] not in ['normal', 'hard', 'expert']
or world.goal[player] not in ['ganon', 'pedestal', 'dungeons', 'triforcehunt', 'trinity', 'crystals',
'ganonhunt', 'completionist']
or world.mode[player] not in ['open', 'standard', 'inverted']
or world.timer not in ['none', 'display', 'timed', 'timed-ohko', 'ohko', 'timed-countdown']
or world.progressive not in ['on', 'off', 'random']):
raise NotImplementedError('Not supported yet')
if world.timer in ['ohko', 'timed-ohko']:
@@ -283,6 +287,7 @@ def generate_itempool(world, player):
for _ in range(0, amt):
pool.append('Rupees (20)')
start_inventory = list(world.precollected_items)
for item in precollected_items:
world.push_precollected(ItemFactory(item, player))
@@ -345,7 +350,7 @@ def generate_itempool(world, player):
world.clock_mode = clock_mode
goal = world.goal[player]
if goal in ['triforcehunt', 'trinity']:
if goal in ['triforcehunt', 'trinity', 'ganonhunt']:
g, t = set_default_triforce(goal, world.treasure_hunt_count[player], world.treasure_hunt_total[player])
world.treasure_hunt_count[player], world.treasure_hunt_total[player] = g, t
world.treasure_hunt_icon[player] = 'Triforce Piece'
@@ -368,21 +373,7 @@ def generate_itempool(world, player):
for i in range(4):
next(adv_heart_pieces).advancement = True
beeweights = {'0': {None: 100},
'1': {None: 75, 'trap': 25},
'2': {None: 40, 'trap': 40, 'bee': 20},
'3': {'trap': 50, 'bee': 50},
'4': {'trap': 100}}
def beemizer(item):
if world.beemizer[item.player] and not item.advancement and not item.priority and not item.type:
choice = random.choices(list(beeweights[world.beemizer[item.player]].keys()), weights=list(beeweights[world.beemizer[item.player]].values()))[0]
return item if not choice else ItemFactory("Bee Trap", player) if choice == 'trap' else ItemFactory("Bee", player)
return item
if not skip_pool_adjustments:
world.itempool += [beemizer(item) for item in items]
else:
world.itempool += items
world.itempool += items
# shuffle medallions
mm_medallion, tr_medallion = None, None
@@ -392,8 +383,16 @@ def generate_itempool(world, player):
custom_medallions = medal_map[player]
if 'Misery Mire' in custom_medallions:
mm_medallion = custom_medallions['Misery Mire']
if isinstance(mm_medallion, dict):
mm_medallion = random.choices(list(mm_medallion.keys()), list(mm_medallion.values()), k=1)[0]
if mm_medallion == 'Random':
mm_medallion = None
if 'Turtle Rock' in custom_medallions:
tr_medallion = custom_medallions['Turtle Rock']
if isinstance(tr_medallion, dict):
tr_medallion = random.choices(list(tr_medallion.keys()), list(tr_medallion.values()), k=1)[0]
if tr_medallion == 'Random':
tr_medallion = None
if not mm_medallion:
mm_medallion = ['Ether', 'Quake', 'Bombos'][random.randint(0, 2)]
if not tr_medallion:
@@ -427,6 +426,38 @@ def generate_itempool(world, player):
if world.dropshuffle[player] == 'underworld' and not skip_pool_adjustments:
add_drop_contents(world, player)
# modfiy based on start inventory, if any
modify_pool_for_start_inventory(start_inventory, world, player)
beeweights = {'0': {None: 100},
'1': {None: 75, 'trap': 25},
'2': {None: 40, 'trap': 40, 'bee': 20},
'3': {'trap': 50, 'bee': 50},
'4': {'trap': 100}}
def beemizer(item):
if world.beemizer[item.player] and not item.advancement and not item.priority and not item.type:
choice = random.choices(list(beeweights[world.beemizer[item.player]].keys()), weights=list(beeweights[world.beemizer[item.player]].values()))[0]
return item if not choice else ItemFactory("Bee Trap", player) if choice == 'trap' else ItemFactory("Bee", player)
return item
if not skip_pool_adjustments:
world.itempool = [beemizer(item) for item in world.itempool]
# increase pool if not enough items
ttl_locations = sum(1 for x in world.get_unfilled_locations(player) if '- Prize' not in x.name)
pool_size = count_player_dungeon_item_pool(world, player)
pool_size += sum(1 for x in world.itempool if x.player == player)
if pool_size < ttl_locations:
retro_bow = world.bow_mode[player].startswith('retro')
amount_to_add = ttl_locations - pool_size
filler_additions = random.choices(list(filler_items.keys()), filler_items.values(), k=amount_to_add)
for item in filler_additions:
item_name = 'Rupees (5)' if retro_bow and item == 'Arrows (10)' else item
world.itempool.append(ItemFactory(item_name, player))
take_any_locations = [
'Snitch Lady (East)', 'Snitch Lady (West)', 'Bush Covered House', 'Light World Bomb Hut',
@@ -813,15 +844,15 @@ def add_pot_contents(world, player):
world.itempool.append(ItemFactory(item, player))
def get_pool_core(world, player, progressive, shuffle, difficulty, treasure_hunt_total, timer, goal, mode, swords, bombbag,
door_shuffle, logic, flute_activated):
def get_pool_core(world, player, progressive, shuffle, difficulty, treasure_hunt_total, timer, goal, mode, swords,
bombbag, door_shuffle, logic, flute_activated):
pool = []
placed_items = {}
precollected_items = []
clock_mode = None
if goal in ['triforcehunt', 'trinity']:
if goal in ['triforcehunt', 'trinity', 'ganonhunt']:
if treasure_hunt_total == 0:
treasure_hunt_total = 30 if goal == 'triforcehunt' else 10
treasure_hunt_total = 30 if goal in ['triforcehunt', 'ganonhunt'] else 10
# triforce pieces max out
triforcepool = ['Triforce Piece'] * min(treasure_hunt_total, max_goal)
@@ -924,7 +955,7 @@ def get_pool_core(world, player, progressive, shuffle, difficulty, treasure_hunt
elif timer == 'timed-ohko':
pool.extend(diff.timedohko)
clock_mode = 'countdown-ohko'
if goal in ['triforcehunt', 'trinity']:
if goal in ['triforcehunt', 'trinity', 'ganonhunt']:
pool.extend(triforcepool)
for extra in diff.extras:
@@ -957,6 +988,80 @@ def get_pool_core(world, player, progressive, shuffle, difficulty, treasure_hunt
return (pool, placed_items, precollected_items, clock_mode, lamps_needed_for_dark_rooms)
item_alternates = {
# Bows
'Progressive Bow (Alt)': ('Progressive Bow', 1),
'Bow': ('Progressive Bow', 1),
'Silver Arrows': ('Progressive Bow', 2),
# Gloves
'Power Glove': ('Progressive Glove', 1),
'Titans Mitts': ('Progressive Glove', 2),
# Swords
'Sword and Shield': ('Progressive Sword', 1), # could find a way to also remove a shield, but mostly not impactful
'Fighter Sword': ('Progressive Sword', 1),
'Master Sword': ('Progressive Sword', 2),
'Tempered Sword': ('Progressive Sword', 3),
'Golden Sword': ('Progressive Sword', 4),
# Shields
'Blue Shield': ('Progressive Shield', 1),
'Red Shield': ('Progressive Shield', 2),
'Mirror Shield': ('Progressive Shield', 3),
# Armors
'Blue Mail': ('Progressive Armor', 1),
'Red Mail': ('Progressive Armor', 2),
'Magic Upgrade (1/4)': ('Magic Upgrade (1/2)', 2),
'Ocarina': ('Ocarina (Activated)', 1),
'Ocarina (Activated)': ('Ocarina', 1),
'Boss Heart Container': ('Sanctuary Heart Container', 1),
'Sanctuary Heart Container': ('Boss Heart Container', 1),
'Power Star': ('Triforce Piece', 1)
}
def modify_pool_for_start_inventory(start_inventory, world, player):
if (world.customizer and world.customizer.get_item_pool()) or world.custom:
# custom item pools only adjust in dungeon items
for item in start_inventory:
if item.dungeon:
d = world.get_dungeon(item.dungeon, item.player)
match = next((i for i in d.all_items if i.name == item.name), None)
if match:
if match.map or match.compass:
d.dungeon_items.remove(match)
elif match.smallkey:
d.small_keys.remove(match)
elif match.bigkey and d.big_key == match:
d.big_key = None
return
for item in start_inventory:
if item.player == player:
if item in world.itempool:
world.itempool.remove(item)
elif item.name in item_alternates:
alt = item_alternates[item.name]
i = alt[1]
while i > 0:
alt_item = ItemFactory([alt[0]], player)[0]
if alt_item in world.itempool:
world.itempool.remove(alt_item)
i = i-1
elif 'Bottle' in item.name:
bottle_item = next((x for x in world.itempool if 'Bottle' in item.name and x.player == player), None)
if bottle_item is not None:
world.itempool.remove(bottle_item)
if item.dungeon:
d = world.get_dungeon(item.dungeon, item.player)
match = next((i for i in d.all_items if i.name == item.name), None)
if match:
if match.map or match.compass:
d.dungeon_items.remove(match)
elif match.smallkey:
d.small_keys.remove(match)
elif match.bigkey and d.big_key == match:
d.big_key = None
def make_custom_item_pool(world, player, progressive, shuffle, difficulty, timer, goal, mode, swords, bombbag, customitemarray):
if isinstance(customitemarray,dict) and 1 in customitemarray:
customitemarray = customitemarray[1]
@@ -983,7 +1088,7 @@ def make_custom_item_pool(world, player, progressive, shuffle, difficulty, timer
customitemarray["triforce"] = total_items_to_place
# Triforce Pieces
if goal in ['triforcehunt', 'trinity']:
if goal in ['triforcehunt', 'trinity', 'ganonhunt']:
g, t = set_default_triforce(goal, customitemarray["triforcepiecesgoal"], customitemarray["triforcepieces"])
customitemarray["triforcepiecesgoal"], customitemarray["triforcepieces"] = g, t
@@ -1021,8 +1126,8 @@ def make_custom_item_pool(world, player, progressive, shuffle, difficulty, timer
treasure_hunt_count = max(min(customitemarray["triforcepiecesgoal"], max_goal), 1)
treasure_hunt_icon = 'Triforce Piece'
# Ensure game is always possible to complete here, force sufficient pieces if the player is unwilling.
if ((customitemarray["triforcepieces"] < treasure_hunt_count) and (goal in ['triforcehunt', 'trinity'])
and (customitemarray["triforce"] == 0)):
if ((customitemarray["triforcepieces"] < treasure_hunt_count)
and (goal in ['triforcehunt', 'trinity', 'ganonhunt']) and (customitemarray["triforce"] == 0)):
extrapieces = treasure_hunt_count - customitemarray["triforcepieces"]
pool.extend(['Triforce Piece'] * extrapieces)
itemtotal = itemtotal + extrapieces
@@ -1060,6 +1165,22 @@ def make_custom_item_pool(world, player, progressive, shuffle, difficulty, timer
# print("Placing " + str(nothings) + " Nothings")
pool.extend(['Nothing'] * nothings)
start_inventory = [x for x in world.precollected_items if x.player == player]
if not start_inventory:
if world.logic[player] in ['owglitches', 'nologic']:
precollected_items.append('Pegasus Boots')
if 'Pegasus Boots' in pool:
pool.remove('Pegasus Boots')
pool.append('Rupees (20)')
if world.swords[player] == 'assured':
precollected_items.append('Progressive Sword')
if 'Progressive Sword' in pool:
pool.remove('Progressive Sword')
pool.append('Rupees (50)')
elif 'Fighter Sword' in pool:
pool.remove('Fighter Sword')
pool.append('Rupees (50)')
return (pool, placed_items, precollected_items, clock_mode, treasure_hunt_count, treasure_hunt_icon, lamps_needed_for_dark_rooms)
@@ -1144,37 +1265,35 @@ def make_customizer_pool(world, player):
place_item('Master Sword Pedestal', 'Triforce')
guaranteed_items = alwaysitems + ['Magic Mirror', 'Moon Pearl']
missing_items = []
if world.shopsanity[player]:
guaranteed_items.extend(['Blue Potion', 'Green Potion', 'Red Potion'])
if world.keyshuffle[player] == 'universal':
guaranteed_items.append('Small Key (Universal)')
for item in guaranteed_items:
if item not in pool:
pool.append(item)
missing_items.append(item)
glove_count = sum(1 for i in pool if i == 'Progressive Glove')
glove_count = 2 if next((i for i in pool if i == 'Titans Glove'), None) is not None else glove_count
for i in range(glove_count, 2):
pool.append('Progressive Glove')
missing_items.append('Progressive Glove')
if world.bombbag[player]:
if 'Bomb Upgrade (+10)' not in pool:
pool.append('Bomb Upgrade (+10)')
missing_items.append('Bomb Upgrade (+10)')
if world.swords[player] != 'swordless':
beam_swords = {'Master Sword', 'Tempered Sword', 'Golden Sword'}
sword_count = sum(1 for i in pool if i in 'Progressive Sword')
sword_count = 2 if next((i for i in pool if i in beam_swords), None) is not None else sword_count
for i in range(sword_count, 2):
pool.append('Progressive Sword')
missing_items.append('Progressive Sword')
bow_found = next((i for i in pool if i in {'Bow', 'Progressive Bow'}), None)
if not bow_found:
pool.append('Progressive Bow')
heart_found = next((i for i in pool if i in {'Boss Heart Container', 'Sanctuary Heart Container'}), None)
if heart_found is None:
pool.append('Boss Heart Container')
missing_items.append('Progressive Bow')
logging.getLogger('').warning(f'The following items are not in the custom item pool {", ".join(missing_items)}')
g, t = set_default_triforce(world.goal[player], world.treasure_hunt_count[player],
world.treasure_hunt_total[player])
@@ -1183,12 +1302,20 @@ def make_customizer_pool(world, player):
if pieces < t:
pool.extend(['Triforce Piece'] * (t - pieces))
ttl_locations = sum(1 for x in world.get_unfilled_locations(player) if '- Prize' not in x.name)
pool_size = len(get_player_dungeon_item_pool(world, player)) + len(pool)
if pool_size < ttl_locations:
amount_to_add = ttl_locations - pool_size
pool.extend(random.choices(list(filler_items.keys()), filler_items.values(), k=amount_to_add))
if not world.customizer.get_start_inventory():
if world.logic[player] in ['owglitches', 'nologic']:
precollected_items.append('Pegasus Boots')
if 'Pegasus Boots' in pool:
pool.remove('Pegasus Boots')
pool.append('Rupees (20)')
if world.swords[player] == 'assured':
precollected_items.append('Progressive Sword')
if 'Progressive Sword' in pool:
pool.remove('Progressive Sword')
pool.append('Rupees (50)')
elif 'Fighter Sword' in pool:
pool.remove('Fighter Sword')
pool.append('Rupees (50)')
return pool, placed_items, precollected_items, clock_mode, 1
@@ -1204,15 +1331,15 @@ filler_items = {
}
def get_player_dungeon_item_pool(world, player):
return [item for dungeon in world.dungeons for item in dungeon.all_items
if dungeon.player == player and item.location is None]
def count_player_dungeon_item_pool(world, player):
return sum(1 for dungeon in world.dungeons for item in dungeon.all_items
if dungeon.player == player and item.location is None and is_dungeon_item(item.name, world, player))
# location pool doesn't support larger values at this time
def set_default_triforce(goal, custom_goal, custom_total):
triforce_goal, triforce_total = 0, 0
if goal == 'triforcehunt':
if goal in ['triforcehunt', 'ganonhunt']:
triforce_goal, triforce_total = 20, 30
elif goal == 'trinity':
triforce_goal, triforce_total = 8, 10
@@ -1267,31 +1394,94 @@ def fill_specific_items(world):
for player, placement_list in placements.items():
for location, item in placement_list.items():
loc = world.get_location(location, player)
item_parts = item.split('#')
item_player = player if len(item_parts) < 2 else int(item_parts[1])
item_name = item_parts[0]
event_flag = False
if is_dungeon_item(item_name, world, item_player):
item_to_place = next(x for x in dungeon_pool
if x.name == item_name and x.player == item_player)
dungeon_pool.remove(item_to_place)
event_flag = True
elif item_name in prize_set:
item_player = player # prizes must be for that player
item_to_place = ItemFactory(item_name, item_player)
prize_pool.remove(item_name)
event_flag = True
else:
item_to_place = next((x for x in world.itempool
if x.name == item_name and x.player == item_player), None)
if item_to_place is None:
item_to_place = ItemFactory(item_name, player)
else:
world.itempool.remove(item_to_place)
world.push_item(loc, item_to_place, False)
# track_outside_keys(item_to_place, spot_to_fill, world)
# track_dungeon_items(item_to_place, spot_to_fill, world)
loc.event = event_flag or item_to_place.advancement
item_to_place, event_flag = get_item_and_event_flag(item, world, player,
dungeon_pool, prize_set, prize_pool)
if item_to_place:
world.push_item(loc, item_to_place, False)
track_outside_keys(item_to_place, loc, world)
track_dungeon_items(item_to_place, loc, world)
loc.event = (event_flag or item_to_place.advancement
or item_to_place.bigkey or item_to_place.smallkey)
advanced_placements = world.customizer.get_advanced_placements()
if advanced_placements:
for player, placement_list in advanced_placements.items():
for placement in placement_list:
if placement['type'] == 'LocationGroup':
item = placement['item']
item_to_place, event_flag = get_item_and_event_flag(item, world, player,
dungeon_pool, prize_set, prize_pool)
if not item_to_place:
continue
locations = placement['locations']
handled = False
while not handled:
if isinstance(locations, dict):
chosen_loc = random.choices(list(locations.keys()), list(locations.values()), k=1)[0]
else: # if isinstance(locations, list):
chosen_loc = random.choice(locations)
if chosen_loc == 'Random':
if is_dungeon_item(item_to_place.name, world, item_to_place.player):
dungeon_pool.append(item_to_place)
elif item_to_place.name in prize_set:
prize_pool.append(item_to_place.name)
else:
world.itempool.append(item_to_place)
else:
loc = world.get_location(chosen_loc, player)
if loc.item:
continue
world.push_item(loc, item_to_place, False)
track_outside_keys(item_to_place, loc, world)
track_dungeon_items(item_to_place, loc, world)
loc.event = (event_flag or item_to_place.advancement
or item_to_place.bigkey or item_to_place.smallkey)
handled = True
elif placement['type'] == 'NotLocationGroup':
item = placement['item']
item_parts = item.split('#')
item_player = player if len(item_parts) < 2 else int(item_parts[1])
item_name = item_parts[0]
world.item_pool_config.restricted[(item_name, item_player)] = placement['locations']
elif placement['type'] == 'PreferredLocationGroup':
item = placement['item']
item_parts = item.split('#')
item_player = player if len(item_parts) < 2 else int(item_parts[1])
item_name = item_parts[0]
world.item_pool_config.preferred[(item_name, item_player)] = placement['locations']
elif placement['type'] == 'Verification':
item = placement['item']
item_parts = item.split('#')
item_player = player if len(item_parts) < 2 else int(item_parts[1])
item_name = item_parts[0]
world.item_pool_config.verify[(item_name, item_player)] = placement['locations']
world.item_pool_config.verify_target += len(placement['locations'])
def get_item_and_event_flag(item, world, player, dungeon_pool, prize_set, prize_pool):
item_parts = item.split('#')
item_player = player if len(item_parts) < 2 else int(item_parts[1])
item_name = item_parts[0]
event_flag = False
if is_dungeon_item(item_name, world, item_player):
item_to_place = next(x for x in dungeon_pool
if x.name == item_name and x.player == item_player)
dungeon_pool.remove(item_to_place)
event_flag = True
elif item_name in prize_set:
item_player = player # prizes must be for that player
item_to_place = ItemFactory(item_name, item_player)
prize_pool.remove(item_name)
event_flag = True
else:
matcher = lambda x: x.name == item_name and x.player == item_player
if item_name == 'Bottle':
matcher = lambda x: x.name.startswith(item_name) and x.player == item_player
item_to_place = next((x for x in world.itempool if matcher(x)), None)
if item_to_place is None:
return None, event_flag
else:
world.itempool.remove(item_to_place)
return item_to_place, event_flag
def is_dungeon_item(item, world, player):
+1 -1
View File
@@ -81,7 +81,7 @@ item_table = {'Bow': (True, False, None, 0x0B, 200, 'You have\nchosen the\narche
'Arrows (5)': (False, False, None, 0x5A, 15, 'This will give\nyou five shots\nwith your bow!', 'and the arrow pack', 'stick-collecting kid', 'sewing kit for sale', 'fungus for arrows', 'archer boy sews again', 'five arrows'),
'Small Magic': (False, False, None, 0x45, 5, 'A bit of magic', 'and the bit of magic', 'bit-o-magic kid', 'magic bit for sale', 'fungus for magic', 'magic boy conjures again', 'a bit of magic'),
'Big Magic': (False, False, None, 0xB4, 40, 'A lot of magic', 'and lots of magic', 'lot-o-magic kid', 'magic refill for sale', 'fungus for magic', 'magic boy conjures again', 'a magic refill'),
'Chicken': (False, False, None, 0xB3, 999, 'Cucco of Legend', 'and the legendary cucco', 'chicken kid', 'fried chicken for sale', 'fungus for chicken', 'cucco boy clucks again', 'a cucco'),
'Chicken': (False, False, None, 0xB3, 5, 'Cucco of Legend', 'and the legendary cucco', 'chicken kid', 'fried chicken for sale', 'fungus for chicken', 'cucco boy clucks again', 'a cucco'),
'Bombs (3)': (False, False, None, 0x28, 15, 'I make things\ngo triple\nBOOM!!!', 'and the explosions', 'the bomb-holding kid', 'firecrackers for sale', 'blend fungus into bombs', '\'splosion boy explodes again', 'three bombs'),
'Bombs (10)': (False, False, None, 0x31, 50, 'I make things\ngo BOOM! Ten\ntimes!', 'and the explosions', 'the bomb-holding kid', 'firecrackers for sale', 'blend fungus into bombs', '\'splosion boy explodes again', 'ten bombs'),
'Bomb Upgrade (+10)': (False, False, None, 0x52, 100, 'Increase bomb\nstorage, low\nlow price', 'and the bomb bag', 'boom-enlarging kid', 'bomb boost for sale', 'the shroom goes boom', 'upgrade boy explodes more again', 'bomb capacity'),
+49 -24
View File
@@ -5,7 +5,7 @@ from collections import defaultdict, deque
from BaseClasses import DoorType, dungeon_keys, KeyRuleType, RegionType
from Regions import dungeon_events
from Dungeons import dungeon_keys, dungeon_bigs, dungeon_table
from DungeonGenerator import ExplorationState, special_big_key_doors, count_locations_exclude_big_chest, prize_or_event
from DungeonGenerator import ExplorationState, get_special_big_key_doors, count_locations_exclude_big_chest, prize_or_event
from DungeonGenerator import reserved_location, blind_boss_unavail
@@ -14,6 +14,7 @@ class KeyLayout(object):
def __init__(self, sector, starts, proposal):
self.sector = sector
self.start_regions = starts
self.event_starts = []
self.proposal = proposal
self.key_logic = KeyLogic(sector.name)
@@ -58,13 +59,16 @@ class KeyLogic(object):
self.placement_rules = []
self.location_rules = {}
self.outside_keys = 0
self.outside_keys_locations = set()
self.dungeon = dungeon_name
self.sm_doors = {}
self.prize_location = None
def check_placement(self, unplaced_keys, wild_keys, big_key_loc=None, prize_loc=None, cr_count=7):
def check_placement(self, unplaced_keys, wild_keys, reached_keys, self_locking_keys,
big_key_loc=None, prize_loc=None, cr_count=7):
for rule in self.placement_rules:
if not rule.is_satisfiable(self.outside_keys, wild_keys, unplaced_keys, big_key_loc, prize_loc, cr_count):
if not rule.is_satisfiable(self.outside_keys_locations, wild_keys, reached_keys, self_locking_keys,
unplaced_keys, big_key_loc, prize_loc, cr_count):
return False
if big_key_loc:
for rule_a, rule_b in itertools.combinations(self.placement_rules, 2):
@@ -158,7 +162,8 @@ class PlacementRule(object):
left -= rule_needed
return False
def is_satisfiable(self, outside_keys, wild_keys, unplaced_keys, big_key_loc, prize_location, cr_count):
def is_satisfiable(self, outside_keys_locations, wild_keys, reached_keys, self_locking_keys, unplaced_keys,
big_key_loc, prize_location, cr_count):
if self.prize_relevance and prize_location:
if self.prize_relevance == 'BigBomb':
if prize_location.item.name not in ['Crystal 5', 'Crystal 6']:
@@ -185,10 +190,11 @@ class PlacementRule(object):
check_locations = self.check_locations_wo_bk if bk_blocked else self.check_locations_w_bk
if not bk_blocked and check_locations is None:
return True
available_keys = outside_keys
available_keys = len(outside_keys_locations)
# todo: sometimes we need an extra empty chest to accomodate the big key too
# dungeon bias seed 563518200 for example
threshold = self.needed_keys_wo_bk if bk_blocked else self.needed_keys_w_bk
threshold -= self_locking_keys
if not wild_keys:
empty_chests = 0
for loc in check_locations:
@@ -199,7 +205,8 @@ class PlacementRule(object):
place_able_keys = min(empty_chests, unplaced_keys)
available_keys += place_able_keys
else:
available_keys += unplaced_keys
available_keys += len(reached_keys.difference(outside_keys_locations)) # already placed small keys
available_keys += unplaced_keys # small keys not yet placed
return available_keys >= threshold
@@ -223,13 +230,14 @@ class KeyCounter(object):
return max(self.used_keys + reserve - len(self.key_only_locations), 0)
def build_key_layout(builder, start_regions, proposal, world, player):
def build_key_layout(builder, start_regions, proposal, event_starts, world, player):
key_layout = KeyLayout(builder.master_sector, start_regions, proposal)
key_layout.flat_prop = flatten_pair_list(key_layout.proposal)
key_layout.max_drops = count_key_drops(key_layout.sector)
key_layout.max_chests = calc_max_chests(builder, key_layout, world, player)
key_layout.big_key_special = check_bk_special(key_layout.sector.region_set(), world, player)
key_layout.all_locations = find_all_locations(key_layout.sector)
key_layout.event_starts = list(event_starts.keys())
return key_layout
@@ -301,10 +309,10 @@ def analyze_dungeon(key_layout, world, player):
key_logic.bk_restricted.update(filter_big_chest(key_counter.free_locations))
# note to self: this is due to the enough_small_locations function in validate_key_layout_sub_loop
# I don't like this exception here or there
elif available < possible_smalls and avail_bigs and non_big_locs > 0:
max_ctr = find_max_counter(key_layout)
bk_lockdown = [x for x in max_ctr.free_locations if x not in key_counter.free_locations]
key_logic.bk_restricted.update(filter_big_chest(bk_lockdown))
# elif available < possible_smalls and avail_bigs and non_big_locs > 0:
# max_ctr = find_max_counter(key_layout)
# bk_lockdown = [x for x in max_ctr.free_locations if x not in key_counter.free_locations]
# key_logic.bk_restricted.update(filter_big_chest(bk_lockdown))
# try to relax the rules here? - smallest requirement that doesn't force a softlock
child_queue = deque()
for child in key_counter.child_doors.keys():
@@ -1000,7 +1008,7 @@ def find_worst_counter_wo_bk(small_key_num, accessible_set, door, odd_ctr, key_c
def open_a_door(door, child_state, flat_proposal, world, player):
if door.bigKey or door.name in special_big_key_doors:
if door.bigKey or door.name in get_special_big_key_doors(world, player):
child_state.big_key_opened = True
child_state.avail_doors.extend(child_state.big_doors)
child_state.opened_doors.extend(set([d.door for d in child_state.big_doors]))
@@ -1443,7 +1451,7 @@ def validate_bk_layout(proposal, builder, start_regions, world, player):
if loc.forced_big_key():
return True
else:
return len(state.bk_found) > 0
return state.count_locations_exclude_specials(world, player) > 0
return False
@@ -1455,6 +1463,7 @@ def validate_key_layout(key_layout, world, player):
return True
flat_proposal = key_layout.flat_prop
state = ExplorationState(dungeon=key_layout.sector.name)
state.init_zelda_event_doors(key_layout.event_starts, player)
state.key_locations = key_layout.max_chests
state.big_key_special = check_bk_special(key_layout.sector.regions, world, player)
for region in key_layout.start_regions:
@@ -1482,14 +1491,16 @@ def validate_key_layout_sub_loop(key_layout, state, checked_states, flat_proposa
else:
ttl_locations = count_locations_exclude_big_chest(state.found_locations, world, player)
ttl_small_key_only = count_small_key_only_locations(state)
available_small_locations = cnt_avail_small_locations(ttl_locations, ttl_small_key_only, state, world, player)
available_small_locations = cnt_avail_small_locations(ttl_locations, ttl_small_key_only, state,
key_layout, world, player)
available_big_locations = cnt_avail_big_locations(ttl_locations, state, world, player)
if invalid_self_locking_key(key_layout, state, prev_state, prev_avail, world, player):
return False
# todo: allow more key shuffles - refine placement rules
# if (not smalls_avail or available_small_locations == 0) and (state.big_key_opened or num_bigs == 0 or available_big_locations == 0):
found_forced_bk = state.found_forced_bk()
smalls_done = not smalls_avail or not enough_small_locations(state, available_small_locations)
smalls_done = not smalls_avail or available_small_locations == 0
# or not enough_small_locations(state, available_small_locations)
bk_done = state.big_key_opened or num_bigs == 0 or (available_big_locations == 0 and not found_forced_bk)
# prize door should not be opened if the boss is reachable - but not reached yet
allow_for_prize_lock = (key_layout.prize_can_lock and
@@ -1611,18 +1622,24 @@ def determine_prize_lock(key_layout, world, player):
key_layout.prize_can_lock = True
def cnt_avail_small_locations(free_locations, key_only, state, world, player):
if world.keyshuffle[player] == 'none':
def cnt_avail_small_locations(free_locations, key_only, state, key_layout, world, player):
std_flag = world.mode[player] == 'standard' and key_layout.sector.name == 'Hyrule Castle'
if world.keyshuffle[player] == 'none' or std_flag:
bk_adj = 1 if state.big_key_opened and not state.big_key_special else 0
avail_chest_keys = min(free_locations - bk_adj, state.key_locations - key_only)
# this is the secret passage, could expand to Uncle/Links House with appropriate logic
std_adj = 1 if std_flag and world.keyshuffle[player] != 'none' else 0
avail_chest_keys = min(free_locations + std_adj - bk_adj, state.key_locations - key_only)
return max(0, avail_chest_keys + key_only - state.used_smalls)
return state.key_locations - state.used_smalls
def cnt_avail_small_locations_by_ctr(free_locations, counter, layout, world, player):
if world.keyshuffle[player] == 'none':
std_flag = world.mode[player] == 'standard' and layout.sector.name == 'Hyrule Castle'
if world.keyshuffle[player] == 'none' or std_flag:
bk_adj = 1 if counter.big_key_opened and not layout.big_key_special else 0
avail_chest_keys = min(free_locations - bk_adj, layout.max_chests)
# this is the secret passage, could expand to Uncle/Links House with appropriate logic
std_adj = 1 if std_flag and world.keyshuffle[player] != 'none' else 0
avail_chest_keys = min(free_locations + std_adj - bk_adj, layout.max_chests)
return max(0, avail_chest_keys + len(counter.key_only_locations) - counter.used_keys)
return layout.max_chests + len(counter.key_only_locations) - counter.used_keys
@@ -1646,6 +1663,7 @@ def create_key_counters(key_layout, world, player):
key_layout.found_doors.clear()
flat_proposal = key_layout.flat_prop
state = ExplorationState(dungeon=key_layout.sector.name)
state.init_zelda_event_doors(key_layout.event_starts, player)
if world.doorShuffle[player] == 'vanilla':
builder = world.dungeon_layouts[player][key_layout.sector.name]
state.key_locations = len(builder.key_door_proposal) - builder.key_drop_cnt
@@ -1678,10 +1696,10 @@ def create_key_counters(key_layout, world, player):
if door.dest in flat_proposal and door.type != DoorType.SpiralStairs:
key_layout.found_doors.add(door.dest)
child_state = parent_state.copy()
if door.bigKey or door.name in special_big_key_doors:
if door.bigKey or door.name in get_special_big_key_doors(world, player):
key_layout.key_logic.bk_doors.add(door)
# open the door, if possible
if can_open_door(door, child_state, world, player):
if can_open_door(door, child_state, key_layout, world, player):
open_a_door(door, child_state, flat_proposal, world, player)
expand_key_state(child_state, flat_proposal, world, player)
code = state_id(child_state, key_layout.flat_prop)
@@ -1702,14 +1720,15 @@ def find_outside_connection(region):
return None, None
def can_open_door(door, state, world, player):
def can_open_door(door, state, key_layout, world, player):
if state.big_key_opened:
ttl_locations = count_free_locations(state, world, player)
else:
ttl_locations = count_locations_exclude_big_chest(state.found_locations, world, player)
if door.smallKey:
ttl_small_key_only = count_small_key_only_locations(state)
available_small_locations = cnt_avail_small_locations(ttl_locations, ttl_small_key_only, state, world, player)
available_small_locations = cnt_avail_small_locations(ttl_locations, ttl_small_key_only, state,
key_layout, world, player)
return available_small_locations > 0
elif door.bigKey:
available_big_locations = cnt_avail_big_locations(ttl_locations, state, world, player)
@@ -2075,6 +2094,12 @@ def validate_key_placement(key_layout, world, player):
if world.bigkeyshuffle[player]:
max_counter = find_max_counter(key_layout)
big_key_outside = bigkey_name not in (l.item.name for l in max_counter.free_locations if l.item)
for i in world.precollected_items:
if i.player == player and i.name == bigkey_name:
big_key_outside = True
break
if i.player == player and i.name == smallkey_name:
keys_outside += 1
for code, counter in key_layout.key_counters.items():
if len(counter.child_doors) == 0:
+18 -12
View File
@@ -24,6 +24,7 @@ from RoomData import create_rooms
from Rules import set_rules
from Dungeons import create_dungeons
from Fill import distribute_items_restrictive, promote_dungeon_items, fill_dungeons_restrictive, ensure_good_items
from Fill import dungeon_tracking
from Fill import sell_potions, sell_keys, balance_multiworld_progression, balance_money_progression, lock_shop_locations
from ItemList import generate_itempool, difficulties, fill_prizes, customize_shops, fill_specific_items
from Utils import output_path, parse_player_names
@@ -35,7 +36,7 @@ from source.classes.CustomSettings import CustomSettings
from source.rom.DataTables import init_data_tables
__version__ = '1.0.1.3-x'
__version__ = '1.3.0.0-x'
from source.classes.BabelFish import BabelFish
@@ -71,9 +72,8 @@ def main(args, seed=None, fish=None):
if args.customizer:
customized = CustomSettings()
customized.load_yaml(args.customizer)
seed = customized.determine_seed()
if seed:
seeded = True
seed = customized.determine_seed(seed)
seeded = True
customized.adjust_args(args)
world = World(args.multi, args.shuffle, args.door_shuffle, args.logic, args.mode, args.swords,
args.difficulty, args.item_functionality, args.timer, args.progressive, args.goal, args.algorithm,
@@ -91,6 +91,7 @@ def main(args, seed=None, fish=None):
if args.securerandom:
world.seed = ''.join(random.choice(string.ascii_uppercase + string.ascii_lowercase + string.digits) for _ in range(9))
world.boots_hint = args.boots_hint.copy()
world.remote_items = args.remote_items.copy()
world.mapshuffle = args.mapshuffle.copy()
world.compassshuffle = args.compassshuffle.copy()
@@ -111,6 +112,8 @@ def main(args, seed=None, fish=None):
world.beemizer = args.beemizer.copy()
world.intensity = {player: random.randint(1, 3) if args.intensity[player] == 'random' else int(args.intensity[player]) for player in range(1, world.players + 1)}
world.door_type_mode = args.door_type_mode.copy()
world.trap_door_mode = args.trap_door_mode.copy()
world.key_logic_algorithm = args.key_logic_algorithm.copy()
world.decoupledoors = args.decoupledoors.copy()
world.experimental = args.experimental.copy()
world.dungeon_counters = args.dungeon_counters.copy()
@@ -151,7 +154,7 @@ def main(args, seed=None, fish=None):
world.player_names[player].append(name)
logger.info('')
world.settings = CustomSettings()
world.settings.create_from_world(world)
world.settings.create_from_world(world, args.race)
outfilebase = f'DR_{args.outputname if args.outputname else world.seed}'
@@ -261,6 +264,7 @@ def main(args, seed=None, fish=None):
massage_item_pool(world)
if args.print_custom_yaml:
world.settings.record_item_pool(world)
dungeon_tracking(world)
fill_specific_items(world)
logger.info(world.fish.translate("cli", "cli", "placing.dungeon.prizes"))
@@ -299,6 +303,7 @@ def main(args, seed=None, fish=None):
balance_multiworld_progression(world)
# if we only check for beatable, we can do this sanity check first before creating the rom
world.clear_exp_cache()
if not world.can_beat_game(log_error=True):
raise RuntimeError(world.fish.translate("cli", "cli", "cannot.beat.game"))
@@ -518,10 +523,8 @@ def copy_world(world):
new_location.item = item
item.location = new_location
item.world = ret
if location.event:
new_location.event = True
if location.locked:
new_location.locked = True
new_location.event = location.event
new_location.locked = location.locked
# these need to be modified properly by set_rules
new_location.access_rule = lambda state: True
new_location.item_rule = lambda state: True
@@ -590,7 +593,8 @@ def create_playthrough(world):
world = copy_world(world)
# get locations containing progress items
prog_locations = [location for location in world.get_filled_locations() if location.item.advancement]
prog_locations = [location for location in world.get_filled_locations() if location.item.advancement
or world.goal[location.player] == 'completionist']
optional_locations = ['Trench 1 Switch', 'Trench 2 Switch', 'Ice Block Drop', 'Skull Star Tile']
state_cache = [None]
collection_spheres = []
@@ -627,13 +631,15 @@ def create_playthrough(world):
for num, sphere in reversed(list(enumerate(collection_spheres))):
to_delete = set()
for location in sphere:
if world.goal[location.player] == 'completionist':
continue # every location for that player is required
# we remove the item at location and check if game is still beatable
logging.getLogger('').debug('Checking if %s (Player %d) is required to beat the game.', location.item.name, location.item.player)
old_item = location.item
location.item = None
# todo: this is not very efficient, but I'm not sure how else to do it for this backwards logic
# world.clear_exp_cache()
if world.can_beat_game(state_cache[num]):
world.clear_exp_cache()
if world.can_beat_game(state_cache[max(num-1, 0)]):
logging.getLogger('').debug(f'{old_item.name} (Player {old_item.player}) is not required')
to_delete.add(location)
else:
+7 -1
View File
@@ -46,6 +46,9 @@ class Context:
self.lookup_name_to_id = {}
self.lookup_id_to_name = {}
self.disable_client_forfeit = False
async def send_msgs(websocket, msgs):
if not websocket or not websocket.open or websocket.closed:
return
@@ -281,7 +284,10 @@ async def process_client_cmd(ctx : Context, client : Client, cmd, args):
if args.startswith('!players'):
notify_all(ctx, get_connected_players_string(ctx))
if args.startswith('!forfeit'):
forfeit_player(ctx, client.team, client.slot)
if ctx.disable_client_forfeit:
notify_client(client, 'Client-initiated forfeits are disabled. Please ask the host of this game to forfeit on your behalf.')
else:
forfeit_player(ctx, client.team, client.slot)
if args.startswith('!countdown'):
try:
timer = int(args.split()[1])
+9 -9
View File
@@ -745,11 +745,11 @@ vanilla_pots = {
0xE7: [Pot(68, 5, PotItem.OneRupee, 'Death Mountain Return Cave (right)', obj=RoomObject(0x0AB389, [0x8B, 0x2B, 0xFA])),
Pot(72, 5, PotItem.OneRupee, 'Death Mountain Return Cave (right)', obj=RoomObject(0x0AB38C, [0x93, 0x2B, 0xFA]))],
0xE8: [Pot(96, 4, PotItem.Heart, 'Superbunny Cave (Bottom)', obj=RoomObject(0x0AA98E, [0xC3, 0x23, 0xFA]))],
0xEB: [Pot(206, 8, PotItem.FiveRupees, 'Bumper Cave', obj=RoomObject(0x0AADE7, [0x9F, 0x47, 0xFA])),
Pot(210, 8, PotItem.FiveRupees, 'Bumper Cave', obj=RoomObject(0x0AADEA, [0xA7, 0x47, 0xFA])),
Pot(88, 14, PotItem.SmallMagic, 'Bumper Cave', obj=RoomObject(0x0AADED, [0xB3, 0x73, 0xFA])),
Pot(92, 14, PotItem.Heart, 'Bumper Cave', obj=RoomObject(0x0AADF0, [0xBB, 0x73, 0xFA])),
Pot(96, 14, PotItem.SmallMagic, 'Bumper Cave', obj=RoomObject(0x0AADF3, [0xC3, 0x73, 0xFA]))],
0xEB: [Pot(206, 8, PotItem.FiveRupees, 'Bumper Cave (bottom)', obj=RoomObject(0x0AADE7, [0x9F, 0x47, 0xFA])),
Pot(210, 8, PotItem.FiveRupees, 'Bumper Cave (bottom)', obj=RoomObject(0x0AADEA, [0xA7, 0x47, 0xFA])),
Pot(88, 14, PotItem.SmallMagic, 'Bumper Cave (bottom)', obj=RoomObject(0x0AADED, [0xB3, 0x73, 0xFA])),
Pot(92, 14, PotItem.Heart, 'Bumper Cave (bottom)', obj=RoomObject(0x0AADF0, [0xBB, 0x73, 0xFA])),
Pot(96, 14, PotItem.SmallMagic, 'Bumper Cave (bottom)', obj=RoomObject(0x0AADF3, [0xC3, 0x73, 0xFA]))],
0xF1: [Pot(64, 5, PotItem.Heart, 'Old Man Cave', obj=RoomObject(0x0AA6B2, [0x83, 0x2B, 0xFA]))],
0xF3: [Pot(0x28, 0x14, PotItem.Nothing, 'Elder House', obj=RoomObject(0x0AA76F, [0x53, 0xA3, 0xFA])),
Pot(0x2C, 0x14, PotItem.Nothing, 'Elder House', obj=RoomObject(0x0AA772, [0x5B, 0xA3, 0xFA])),
@@ -851,10 +851,10 @@ vanilla_pots = {
Pot(100, 22, PotItem.Heart, 'Dark Lake Hylia Ledge Spike Cave', obj=RoomObject(0x0AB62A, [0xCB, 0xB3, 0xFA])),
Pot(88, 28, PotItem.Heart, 'Dark Lake Hylia Ledge Spike Cave', obj=RoomObject(0x0AB633, [0xB3, 0xE3, 0xFA])),
Pot(100, 28, PotItem.Heart, 'Dark Lake Hylia Ledge Spike Cave', obj=RoomObject(0x0AB636, [0xCB, 0xE3, 0xFA]))],
0x127: [Pot(24, 25, PotItem.Nothing, 'Dark World Hammer Peg Cave', obj=RoomObject(0x2A801A, [0x33, 0xCB, 0xFA])),
Pot(28, 25, PotItem.Nothing, 'Dark World Hammer Peg Cave', obj=RoomObject(0x2A801D, [0x3B, 0xCB, 0xFA])),
Pot(32, 25, PotItem.Nothing, 'Dark World Hammer Peg Cave', obj=RoomObject(0x2A8020, [0x43, 0xCB, 0xFA])),
Pot(36, 25, PotItem.Nothing, 'Dark World Hammer Peg Cave', obj=RoomObject(0x2A8023, [0x4B, 0xCB, 0xFA]))],
0x127: [Pot(24, 25, PotItem.Nothing, 'Dark World Hammer Peg Cave', obj=RoomObject(0x2B801A, [0x33, 0xCB, 0xFA])),
Pot(28, 25, PotItem.Nothing, 'Dark World Hammer Peg Cave', obj=RoomObject(0x2B801D, [0x3B, 0xCB, 0xFA])),
Pot(32, 25, PotItem.Nothing, 'Dark World Hammer Peg Cave', obj=RoomObject(0x2B8020, [0x43, 0xCB, 0xFA])),
Pot(36, 25, PotItem.Nothing, 'Dark World Hammer Peg Cave', obj=RoomObject(0x2B8023, [0x4B, 0xCB, 0xFA]))],
}
+398 -195
View File
@@ -4,15 +4,47 @@ This is a door randomizer for _The Legend of Zelda: A Link to the Past_ for the
based on the Entrance Randomizer found at [KevinCathcart's Github Project.](https://github.com/KevinCathcart/ALttPEntranceRandomizer)
See https://alttpr.com/ for more details on the normal randomizer.
# Known Issues
# Documentation
1. [Setup and Installation](#setup-and-installation)
2. [Commonly Missed Things](#commonly-missed-things) (** **Read This If New** **)
3. [Settings](#settings)
1. [Dungeon Randomization](#dungeon-settings)
1. [Dungeon Door Shuffle](#door-shuffle)
2. [Intensity Level](#intensity---intensity-number)
3. [Key Drop Shuffle (Legacy)](#key-drop-shuffle-legacy---keydropshuffle)
4. [Door Type Shuffle](#door-type-shuffle)
5. [Trap Door Removal](#trap-door-removal)
6. [Key Logic Algorithm](#key-logic-algorithm)
7. [Decouple Doors](#decouple-doors)
8. [Pottery](#pottery)
9. [Small Key Shuffle](#small-key-shuffle)
10. [Shuffle Enemy Key Drops](#shuffle-enemy-key-drops)
11. [Experimental Features](#experimental-features)
12. [Crossed Dungeon Specific Settings](#crossed-dungeon-specific-settings)
2. [Item Randomization Changes](#item-randomization)
1. [New "Items"](#new-items)
2. [Shopsanity](#shopsanity)
3. [Logic Level](#logic-level)
4. [Goal](#goal)
5. [Item Sorting](#item-sorting)
6. [Forbidden Boss Items](#forbidden-boss-items)
3. [Customizer](#customizer)
4. [Entrance Randomization](#entrance-randomization)
1. [Shuffle Links House](#shuffle-links-house)
2. [Overworld Map](#overworld-map)
5. [Enemizer](#enemizer)
6. [Retro Changes](#retro-changes)
7. [Standard Changes](#standard-changes)
8. [Game Options](#game-options)
9. [Generation Setup & Miscellaneous](#generation-setup--miscellaneous)
[List of Known Issues and Their Status](https://docs.google.com/document/d/1Bk-m-QRvH5iF60ndptKYgyaV7P93D3TiG8xmdxp_bdQ/edit?usp=sharing)
## Setup and Installation
# Feedback and Bug Reports
### Feedback and Bug Reports
Please just DM me on discord for now. I (Aerinon) can be found at the [ALTTP Randomizer discord](https://discordapp.com/invite/alttprandomizer).
You can use the #bug-reports or #door-rando channel at the [ALTTP Randomizer discord](https://discordapp.com/invite/alttprandomizer) to provide feedback or bug reports.
# Installation
### Installation
Click on
@@ -22,7 +54,7 @@ Go down to Assets and find a build for your system (Windows, Mac, or Linux)
Download and unzip. Find the DungeonRandomizer.exe or equivalent
# Installation from source
### Installation from source
See these instructions.
@@ -36,21 +68,21 @@ To use the CLI, run ```DungeonRandomizer.py```.
Alternatively, run ```Gui.py``` for a simple graphical user interface.
# Commonly Missed Things and Differences from other Randomizers
# Commonly Missed Things
#### and Differences from other Randomizers
Most of these apply only when the door shuffle is not vanilla.
### Starting Item
You start with a “Mirror Scroll”, a dumbed-down mirror that only works in dungeons, not the overworld and cant erase blocks like the Mirror.
You start with a “Mirror Scroll” (it looks like a map), a dumbed-down mirror that only works in dungeons, not the overworld, and cant erase blocks like the Mirror.
### Navigation
* The Pinball Rooms trap door can be removed in the case where it is required to go through to get to the back of Skull Woods.
* Holes in Mire Torches Top and Mire Torches Bottom fall through to rooms below (you only need fire to get the chest)
* You can Hookshot from the left Mire wooden Bridge to the right one.
* In the PoD Arena, you can bonk with Boots between the two blue crystal barriers against the ladder to reach the Arena Bridge chest and door. (Bomb Jump also possible but not in logic - Boots are required)
* Flooded Rooms in Swamp can be traversed backward and may be required.
* Flooded Rooms in Swamp can be traversed backward and may be required. The flippers are needed to get out of the water.
### Other Logic
@@ -76,69 +108,189 @@ You start with a “Mirror Scroll”, a dumbed-down mirror that only works in du
# Settings
## Dungeon Settings
Only extra settings are found here. All entrance randomizer settings are supported. See their [readme](https://github.com/KevinCathcart/ALttPEntranceRandomizer/blob/master/README.md)
## Door Shuffle (--doorShuffle)
### Door Shuffle
### Basic
* Vanilla - Doors are not shuffled
* Basic - Doors are shuffled only within a single dungeon.
* Paritioned - Dungeons are shuffled in 3 pools: Light World, Early Dark World, Late Dark World. (Late Dark are the four dungeons that require Mitts in vanilla, including Ganons Tower)
* Crossed - Doors are shuffled between dungeons as well.
Doors are shuffled only within a single dungeon.
CLI: `--doorShuffle [vanilla|basic|partitioned|crossed]`
### Crossed
### Intensity (--intensity number)
Doors are shuffled between dungeons as well.
* Level 1 - Normal door and spiral staircases are shuffled
* Level 2 - Same as Level 1 plus open edges and both types of straight staircases are shuffled.
* Level 3 - Same as Level 2 plus Dungeon Lobbies are shuffled
### Vanilla
Doors are not shuffled.
## Intensity (--intensity number)
#### Level 1
Normal door and spiral staircases are shuffled
#### Level 2
Same as Level 1 plus open edges and both types of straight staircases are shuffled.
#### Level 3
Same as Level 2 plus Dungeon Lobbies are shuffled
## KeyDropShuffle (--keydropshuffle)
### Key Drop Shuffle (Legacy) (--keydropshuffle)
Adds 33 new locations to the randomization pool. The 32 small keys found under pots and dropped by enemies and the Big
Key drop location are added to the pool. The keys normally found there are added to the item pool. Retro adds
32 generic keys to the pool instead.
Key drop location are added to the pool. The keys normally found there are added to the item pool. Retro adds
32 generic keys to the pool instead. This has been can be controlled more granularly with the [Pottery](#pottery) and
[Shuffle Enemy Key Drops](#shuffle-enemy-key-drops)
## Crossed Dungeon Specific Settings
### Door Type Shuffle
### Mixed Travel (--mixed_travel value)
Four options here, and all of them only take effect if Dungeon Door Shuffle is not vanilla:
Due to Hammerjump, Hovering in PoD Arena, and the Mire Big Key Chest bomb jump two sections of a supertile that are
* Small Key Doors, Bomb Doors, Dash Doors: This is what was normally shuffled previously
* Adds Big Keys Doors: Big key doors are now shuffled in addition to those above, and Big Key doors are enabled to be on in both vertical directions thanks to a graphic that ended up on the cutting room floor. This does change
* Adds Trap Doors: All trap doors that are permanently shut in vanilla are shuffled, excluding those by bosses.
* Increases all Door Types: This is a chaos mode where each door type per dungeon is randomized between 1 less and 4 more.
CLI: `--door_type_mode [original|big|all|chaos]`
### Trap Door Removal
Options here for making dungeon traversal nicer. Only applies if door shuffle is not vanilla.
* No Removal: This does not remove any trap doors.
* Removed If Blocking Path: Dungeon generation is relaxed to allow annoying trap doors to be removed if necessary. Note that boss trap doors are never shuffled in this mode.
* Remove Boss Traps: Boss traps are removed, this includes the one near Mothula.
* Remove All Annoying Traps: This removes all trap doors that are annoying, including boss traps.
If trap doors are shuffled the first two option behave the same. The last option overrides the shuffle because there is nothing left to shuffle. Boss traps are never shuffled.
In all cases, that the trap door near the mire cutscene chest (Mire Warping Pool ES) is left alone because it enforces the use of fire to get to the chest.
CLI: `--trap_door_mode [vanilla|optional|boss|oneway]`
### Key Logic Algorithm
Determines how small key door logic works.
* Default: Current key logic. Assumes worse case usage, placement checks, but assumes you can't get to a chest until you have sufficient keys. (May assume items are unreachable)
* Partial Protection: Assumes you always have full inventory and worse case usage. This should account for dark room and bunny revival glitches.
* Strict: For those would like to glitch and be protected from yourselves. Small keys door require all small keys to be available to be in logic.
CLI: `--key_logic [default|partial|strict]`
### Decouple Doors
This is similar to insanity mode in ER where door entrances and exits are not paired anymore. Tends to remove more logic from dungeons as many rooms will not be required to traverse to explore. Hope you like transitions.
CLI `--decoupledoors`
### Pottery
New pottery option that control which pots (and large blocks) are in the locations pool:
* None: No pots are in the pool, like normal randomizer
* Key Pots: The pots that have keys are in the pool. This is about half of the old keydropshuffle option
* Cave Pots: The pots that are not found in dungeons are in the pool. (Includes the large block in Spike Cave). Does
not include key pots.
* Cave + Keys Pots: Both non-dungeon pots and pots that used to have keys are in the pool.
* Reduced Dungeon Pots: Same as Cave+Keys but also roughly a quarter of dungeon pots are added to the location pool picked at random. This is a dynamic mode so pots in the pool will be colored. Pots out of the pool will have vanilla contents.
* Clustered Dungeon Pots: Like reduced but pots are grouped by logical sets and roughly 50% of pots are chosen from those groups. This is a dynamic mode like the above.
* Excludes Empty Pots: All pots that had some sort of objects under them are chosen to be in the location pool. This excludes most large blocks and some pots out of dungeons.
* Dungeon Pots: The pots that are in dungeons are in the pool. (Includes serveral large blocks)
* Lottery: All pots and large blocks are in the pool.
By default, switches remain in their vanilla location (unless you turn on the legacy option below)
CLI `--pottery <option>` from `none, keys, cave, cavekeys, reduced, clustered, nonempty, dungeon, lottery`
Note for multiworld: due to the design of the pottery lottery, only 256 items for other players can be under pots in your world.
#### Colorize Pots
If the pottery mode is dynamic, this option is forced to be on (clustered and reduced). It is allowed to be on in all other pottery modes. Exceptions include "none" where no pots would be colored, and "lottery" where all pots would be. This option colors the pots differently that have been chosen to be part of the location pool. If not specified, you are expected to remember the pottery setting you chose. Note that Mystery will colorize all pots if lottery is chosen randomly.
CLI `--colorizepots`
#### Pot Shuffle (Legacy)
This continues to works the same by shuffling all pots on a supertile. It works with the lottery option as well to move the switches to any valid pot on the supertile regardless of the pots chosen in the pottery mode. This may increase the number of pot locations slightly depending on the mode.
### Small Key Shuffle
There are three options now available:
* In Dungeon: The small key will be in their own dungeon
* Randomized: Small keys can be shuffled outside their own dungeon
* Universal: Retro keys without the other options
CLI: `--keyshuffle [none|wild|universal]`
### Shuffle Enemy Key Drops
Enemies that drop keys can have their drop shuffled into the pool. This is the one part of the keydropshuffle option.
See the pottery option for more options involving pots.
CLI: `--dropshuffle`
### Experimental Features
You will start as a bunny if your spawn point is in the dark world. CLI: `--experimental`
### Crossed Dungeon Specific Settings
#### Dungeon Chest Counters
* Auto - picks an appropriate setting based on other settings. Generally will be pickup if the number of item in a dungeon changes.
* On - Dungeon counters on hud always displayed
* Off - Dungeon counters on hud never displayer
* On Compass Pickup - Dungeons with a compass item will display the counter once the compass is found. Dungeons without a compass item will display always unless the number of items in the dungeon is completely vanilla.
CLI: `--dungeon_counters` from `auto, on, off, pickup`
#### Mixed Travel (--mixed_travel value)
Due to Hammerjump, Hovering in PoD Arena, and the Mire Big Key Chest bomb jump, two sections of a supertile that are
otherwise unconnected logically can be reached using these glitches. To prevent the player from unintentionally changing
dungeons while doing these tricks, you may use one of the following options.
dungeons while doing these tricks, you may use one of the following options:
#### Prevent (default)
* Prevent (default):
Rails are added the 3 spots to prevent these tricks. This setting is recommend for those learning crossed dungeon mode to learn what is dangerous and what is not. No logic seeds ignore this setting.
* Allow: The rooms are left alone and it is up to the discretion of the player whether to use these tricks or not.
* Force: The two disjointed sections are forced to be in the same dungeon but the glitches are never logically required to complete that game.
Rails are added the 3 spots to prevent this tricks. This setting is recommend for those learning crossed dungeon mode to
learn what is dangerous and what is not. No logic seeds ignore this setting.
#### Allow
The rooms are left alone and it is up to the discretion of the player whether to use these tricks or not.
#### Force
The two disjointed sections are forced to be in the same dungeon but the glitches are never logically required to complete that game.cause then you would need time to check the map in a d
### Standardize Palettes (--standardize_palettes)
#### Standardize Palettes (--standardize_palettes)
No effect if door shuffle is not on crossed
#### Standardize (default)
Rooms in the same dungeon have their palettes changed to match. Hyrule Castle is split between Sewer and HC palette.
* Standardize (default): Rooms in the same dungeon have their palettes changed to match. Hyrule Castle is split between Sewer and HC palette.
Rooms adjacent to sanctuary get their coloring to match the Sanctuary's original palette.
* Original: Rooms/supertiles keep their original palettes.
#### Original
Rooms/supertiles keep their original palettes.
## Item Randomization
## Shopsanity
### New "Items"
#### Bombbag
Two bomb bags are added to the item pool (They look like +10 Capacity upgrades). Bombs are unable to be used until one is found. Bomb capacity upgrades are otherwise unavailable.
CLI `--bombbag`
#### Pseudo Boots
Dashing is allowed without the boots item however doors and certain rocks remain un-openable until boots are found. Items that require boots are still unattainable. Specific sequence breaks like hovering and water-walking are not allowed until boots are found. Bonk distance is shortened to prevent certain pits from being crossed. Finding boots restores all normal behavior.
CLI `--pseudoboots`
#### Flute Mode
Normal mode for flute means you need to activate it at the village statue after finding it like usual. Activated flute mode mean you can use it immediately upon finding it. The flute SFX plays to let you know this is the case.
CLI:`--flute_mode`
#### Bow Mode
Four options here :
* Progressive. Standard progressive bows.
* Silvers separate. One bow in the pool and silvers are a separate item.
* Retro (progressive). Arrows cost rupees. You need to purchase the single arrow item at a shop and there are two progressive bows places.
* Retro + Silvers. Arrows cost rupees. You need to purchase the single arrow item or find the silvers, there is only one bow, and silvers are a separate item (but count for the quiver if found).
CLI: `--bow_mode [progressive|silvers|retro|retro_silvers]`
### Shopsanity
This adds 32 shop locations (9 more in retro) to the general location pool.
@@ -192,34 +344,34 @@ Detailed explanation: It is calculated how much money is available in the item p
All prices range approx. from half the base price to twice the base price (as a max) in increments of 5, the exact price is chosen randomly within the range subject to adjustments by the sphere effects above.
| Category | Items | Base Price | Typical Range |
| ----------------- | ------- |:----------:|:-------------:|
| Major Progression | Hammer, Hookshot, Mirror, Ocarina, Boots, Somaria, Fire Rod, Ice Rod | 250 | 125-500
| | Moon Pearl | 200 | 100-400
| | Lamp, Progressive Bows, Gloves, & Swords | 150 | 75-300
| | Triforce Piece | 100 | 50-200
| Medallions | Bombos, Ether, Quake | 100 | 50-200
| Safety/Fetch | Cape, Mushroom, Shovel, Powder, Bug Net, Byrna, Progressive Armor & Shields, Half Magic | 50 | 25-100
| Bottles | Empty Bottle or Bee Bottle | 50 | 25-100
| | Green Goo or Good Bee | 60 | 30-120
| | Red Goo or Fairy | 70 | 35-140
| | Blue Goo | 80 | 40-160
| Health | Heart Container | 40 | 20-80
| | Sanctuary Heart | 50 | 25-100
| | Piece of Heart | 10 | 5-20
| Dungeon | Big Keys | 60 | 30-120
| | Small Keys | 40 | 20-80
| | Info Maps | 20 | 10-40
| | Other Maps & Compasses | 10 | 5-20
| Rupees | Green | Free | Free
| | Blue | 2 | 2-4
| | Red | 10 | 5-20
| | Fifty | 25 | 15-50
| | One Hundred | 50 | 25-100
| | Three Hundred | 150 | 75-300
| Ammo | Three Bombs | 15 | 10-30
| | Single Arrow | 3 | 3-6
| Original Shop Items | Other Ammo, Refills, Non-Progressive Shields, Capacity Upgrades, Small Hearts, Retro Quiver, Universal Key | Original | .5 - 2 * Original
| Category | Items | Base Price | Typical Range |
|---------------------|------------------------------------------------------------------------------------------------------------|:----------:|:-----------------:|
| Major Progression | Hammer, Hookshot, Mirror, Ocarina, Boots, Somaria, Fire Rod, Ice Rod | 250 | 125-500 |
| | Moon Pearl | 200 | 100-400 |
| | Lamp, Progressive Bows, Gloves, & Swords | 150 | 75-300 |
| | Triforce Piece | 100 | 50-200 |
| Medallions | Bombos, Ether, Quake | 100 | 50-200 |
| Safety/Fetch | Cape, Mushroom, Shovel, Powder, Bug Net, Byrna, Progressive Armor & Shields, Half Magic | 50 | 25-100 |
| Bottles | Empty Bottle or Bee Bottle | 50 | 25-100 |
| | Green Goo or Good Bee | 60 | 30-120 |
| | Red Goo or Fairy | 70 | 35-140 |
| | Blue Goo | 80 | 40-160 |
| Health | Heart Container | 40 | 20-80 |
| | Sanctuary Heart | 50 | 25-100 |
| | Piece of Heart | 10 | 5-20 |
| Dungeon | Big Keys | 60 | 30-120 |
| | Small Keys | 40 | 20-80 |
| | Info Maps | 20 | 10-40 |
| | Other Maps & Compasses | 10 | 5-20 |
| Rupees | Green | Free | Free |
| | Blue | 2 | 2-4 |
| | Red | 10 | 5-20 |
| | Fifty | 25 | 15-50 |
| | One Hundred | 50 | 25-100 |
| | Three Hundred | 150 | 75-300 |
| Ammo | Three Bombs | 15 | 10-30 |
| | Single Arrow | 3 | 3-6 |
| Original Shop Items | Other Ammo, Refills, Non-Progressive Shields, Capacity Upgrades, Small Hearts, Retro Quiver, Universal Key | Original | .5 - 2 * Original |
#### Rupee Balancing Algorithm
@@ -241,39 +393,187 @@ Arrow Capacity upgrades are now replaced by Rupees wherever it might end up.
The Ten Arrows and 5 randomly selected Small Hearts or Blue Shields are replaced by the quiver item (represented by the Single Arrow in game.) 5 Red Potion refills are replaced by the Universal small key. It is assured that at least one shop sells Universal Small Keys. The quiver may thus not be found in shops. The quiver and small keys retain their original base price, but may be discounted.
## Logic Level
### Logic Level
### Overworld Glitches
Overworld Glitches is now supported.
Set `--logic` to `owglitches` to make overworld glitches required in the logic.
CLI: `--logic owglitches`
## Shuffle Links House
### Goal
New supported goals:
* Trinity: Find one of 3 triforces to win. One is at pedestal. One is with Ganon. One is with Murahdahla who wants you to find 8 of 10 triforce pieces to complete.
* Triforce Hunt + Ganon: Collect the requisite triforce pieces, then defeat Ganon. (Aga2 not required). Use `ganonhunt` on CLI
* Completionist: All dungeons not enough for you? You have to obtain every item in the game too. This option turns on the collection rate counter and forces accessibility to be 100% locations. Finish by defeating Ganon.
### Item Sorting
The "Item Sorting" option or ```--algorithm``` has been updated with new placement algorithms. Older algorithms have been removed.
When referenced below, Major Items include all Y items, all A items, all equipment (swords, shields, & armor) and Heart Containers. Dungeon items are considered major if shuffled outside of dungeons. Bomb and arrows upgrades are Major if shopsanity is turned on. The arrow quiver and universal small keys are Major if retro is turned on. Triforce Pieces are Major if that is the goal, and the Bomb Bag is Major if that is enabled.
Here are the current fill options:
#### Balanced
This one stays the same as before and is recommended for the most random distribution of items.
#### Vanilla Fill
This fill attempts to place all items in their vanilla locations when possible. Obviously shuffling entrances or the dungeon interiors will often prevent items from being placed in their vanilla location. If the vanilla fill is not possible, then other locations are tried in sequence preferring "major" locations (see below), then heart piece locations, then the rest except for GT locations which are preferred last. Note the PoD small key that is normally found in the dark maze in vanilla is move to Harmless Hellway due to the placement algorithm limitation.
#### Major Location Restriction
This fill attempts to place major items in major locations. Major locations are where the major items are found in the vanilla game. This includes the spot next to Uncle in the Sewers, and the Boomerang chest in Hyrule Castle.
This location pool is expanded to where dungeon items are locations if those dungeon items are shuffled. The Capacity Fairy locations are included if Shopsanity is on. If retro is enabled in addition to shopsanity, then the Old Man Sword Cave and one location in each retro cave is included. Key drop locations can be included if small or big key shuffle is on. This gives a very good balance between overworld and underworld locations though the dungeons ones will be on bosses and in big chests generally. Seeds do become more linear but usually easier to figure out.
#### Dungeon Restriction
The fill attempts to place all major items in dungeons. It will overflow to the overworld if there are more items than locations (e.g. Triforce hunt.) This fill does attempt to run the GT trash fill when possible. Seeds are typically very linear but tend to be more difficult.
#### District Restriction
The world is divided up into different regions or districts. Each dungeon is its own district. The overworld consists of the following districts:
Light world:
* Kakariko (The main screen, blacksmith screen, and library/maze race screens)
* Northwest Hyrule (The lost woods and fortune teller screens all the way to the river west of the potion shop)
* Central Hyrule (Hyrule castle, Link's House, the marsh, and the haunted grove)
* Desert (From the thief to the main desert screen)
* Lake Hylia (Around the lake)
* Eastern Hyrule (The eastern wild, the potion shop, and Zora's Domain)
* Death Mountain
Dark world:
* East Dark World (The pyramid, Palace of darkness, and Catfish)
* South Dark World (The dark lake, swamp area, to the dig game)
* Northwest Dark World (Village of Outcasts, to the Dark Sanctuary and screens in between)
* The Mire
* Dark Death Mountain
These districts are chosen at random and then filled with major items. If a location is part of a chosen district, but there are no more major items to place, a single green rupee is placed in the extra to indicate that as a placeholder. All other single green rupees are changed to be a blue rupee in order to not give false positives.
In entrance shuffle, what is shuffled to the entrances is considered instead of where the interior was originally. For example, if Blind's Hut is shuffled to the Dam, then the 5 chests in Blind's Hut are part of Central Hyrule instead of Kakariko.
Note: Bombos Tablet, Lake Hylia Island, Bumper Cave Ledge, the Floating Island, Cave 45, the Graveyard Cave, Checkerboard Cave and Mimic Cave are considered part of the light world region rather than the dark world region you mirror from.
In multiworld, the districts chosen apply to all players.
#### CLI values:
```balanced, vanilla_fill, major_only, dungeon_only, district```
#### New Hints
Based on the district algorithm above (whether it is enabled or not,) new hints can appear about that district or dungeon. For each district and dungeon, it is evaluated whether it contains vital items and how many. If it has not any vital item, items then it moves onto useful items. Useful items are generally safeties or convenience items: shields, mails, half magic, bottles, medallions that aren't required, etc. If it contains none of those and is an overworld district, then it checks for a couple more things. First, if dungeons are shuffled, it looks to see if any are in the district, if so, one of those dungeons is picked for the hint. Then, if connectors are shuffled, it checks to see if you can get to unique region through a connector in that district. If none of the above apply, the district or dungeon is considered completely foolish.
### Forbidden Boss Items
You may now restrict the items that can appear on the boss, like the popular ambrosia preset does.
CLI: ```--restrict_boss_items <option>```
#### Options
* none: As before, the boss may have any item including any dungeon item that could occur there.
* mapcompass: ~~The map and compass are logically required to defeat a boss. This prevents both of those from appearing on the dungeon boss. Note that this does affect item placement logic and the placement algorithm as maps and compasses are considered as required items to beat a boss.~~ Currently bugged, not recommended for use.
* dungeon: Same as above but both small keys and bigs keys of the dungeon are not allowed on a boss. (Note: this does not affect universal keys as they are not dungeon-specific)
## Customizer
Please see [Customizer documentation](docs/Customizer.md) on how to create custom seeds.
## Entrance Randomization
### New Modes
Lite and Lean ER is now available when Experimental Features are turned on. (todo: bring over documenation of these modes)
### Shuffle Links House
In certain ER shuffles, (not dungeonssimple or dungeonsfulls), you can now control whether Links House is shuffled or remains vanilla. Previously, inverted seeds had this behavior and would shuffle links house, but now if will only do so if this is specified. Now, also works for open modes, but links house is never shuffled in standard mode.
## Bomb Logic (--bombbag)
### Shuffle Back of Tavern
When enabling this option, you do not start with bomb capacity but rather you must find 1 of 2 bomb bags. (They are represented by the +10 capacity item.) Bomb capacity upgrades are otherwise unavailable.
You may shuffle the back of tavern entrance in ER modes when Experimental Features are turned on.
## Reduce Flashing
### Overworld Map
Accessibility option to reducing some flashing animations in the game.
Option to move indicators on overworld map to reference dungeon location. The non-default options include indicators for Hyrule Castle, Agahnim's Tower, and Ganon's Tower.
## Pseudo-boots
* Default: Status quo. Showing only the prize markers on the vanilla dungeon locations.
* Compass:
The compass item controls whether the marker is moved to the dungeons locations. If you possess the compass but not the map, only a glowing X will be present regardless of dungeon prize type, if you only possess the map, the prizes will be shown in predicable locations at the bottom of the overworld map instead of the vanilla location. Light world dungeons on the light world map and dark world dungeons on the dark world map. If you posses both map and compass, then the prize of the dungeon and the location will be on the map.
* Map: The map item plays double duty in this mode and only possession of the map will show both prize and location of the dungeon. If you do not shuffle maps or the dungeon does not have a map, the information will be displayed without needing to find any items.
Option to start with ability to dash, but not able to make any boots required logical checks or traversal.
If you do not shuffle the compass or map outside of the dungeon, the non-shuffled items are not needed to display the information. If a dungeon does not have a map or compass, it is not needed for the information. Talking to the bomb shop or Sahasrahla furnishes you with complete information as well as map information.
## SFX Shuffle (--shuffle_sfx)
CLI ```--overworld_map [default|compass|map]```
## Enemizer
### Boss Shuffle: Unique
At least one boss each of the prize bosses will be present guarding the prizes. GT bosses can be anything.
### Blind Note
If bosses are shuffled and Blind is chosen to be the boss of Thieves Town, then bombing the attic and delivering the maiden is still required.
## Standard Changes
When dungeon door shuffle is on, the Sanctuary is guaranteed to be behind the Throne Room and the Mirror Scroll works like death warping instead of the mirror during the escape sequence.
## Retro Changes
Retro can be partially enabled: see Small Key Shuffle and Bow Mode. Retro checkbox or Retro mode still enable all 3 options.
New supported option:
### Take Any Caves
These are now independent of retro mode and have three options: None, Random, and Fixed. None disables the caves. Random works as take-any caves did before. Fixed means that the take any caves replace specific fairy caves in the pool and will be at those entrances unless ER is turned on (then they can be shuffled wherever). The fixed entrances are:
* Desert Healer Fairy
* Swamp Healer Fairy (aka Light Hype Cave)
* Dark Death Mountain Healer Fairy
* Dark Lake Hylia Ledge Healer Fairy (aka Shopping Mall Bomb)
* Bonk Fairy (Dark)
## Game Options
### MSU Resume
Turns on msu resume support. Found on "Game Options" tab, the "Adjust/Patch" tab, or use the `--msu_resume` CLI option.
### Collection Rate
Display the collection rate unless the triforce piece counter is needed. If the game was generated as a mystery, then the total count is not displayed. `--collection_rate`
### Reduce Flashing
Accessibility option to reducing some flashing animations in the game. `--reduce_flashing`
### Shuffle Sound Effects (--shuffle_sfx)
Shuffles a large portion of the sounds effects. Can be used with the adjuster.
## Experimental Features
## Generation Setup & Miscellaneous
You will start as a bunny if your spawn point is in the dark world.
### Create BPS Patches
## Triforce Hunt Settings
Create bps patch(es) instead of generating rom(s) for distribution. `--bps`
A collection of settings to control the triforce piece pool.
### Triforce Hunt Settings
A collection of settings to control the triforce piece pool for the CLI/Mystery
* --triforce_goal_min: Minimum number of pieces to collect to win
* --triforce_goal_max: Maximum number of pieces to collect to win
@@ -281,110 +581,13 @@ A collection of settings to control the triforce piece pool.
* --triforce_pool_max: Maximum number of pieces in item pool
* --triforce_min_difference: Minimum difference between pool and goal to win
## Seed
### Seed
Can be used to set a seed number to generate. Using the same seed with same settings on the same version of the entrance randomizer will always yield an identical output.
## Count
### Count
Use to batch generate multiple seeds with same settings. If a seed number is provided, it will be used for the first seed, then used to derive the next seed (i.e. generating 10 seeds with the same seed number given will produce the same 10 (different) roms each time).
# Command Line Options
```
-h, --help
```
Show the help message and exit.
```
--door_shuffle <mode>
```
For specifying the door shuffle you want as above. (default: basic)
```
--intensity <number>
```
For specifying the door shuffle intensity level you want as above. (default: 2)
```
--keydropshuffle
```
Include mobs and pots drop in the item pool. (default: not enabled)
```
--shopsanity
```
Includes shop locations in the item pool.
```
--pseudoboots
```
Start with dash ability, but no way to use boots to accomplish checks
```
--shufflelinks
```
Whether to shuffle links house in most ER modes.
```
--shuffletavern
```
Whether to shuffle the back of the tavern in most ER modes.
```
--bombbag
```
Need to find the bombbag upgrade to used bombs
```
--experimental
```
Enables experimental features
```
--mixed_travel <mode>
```
How to handle certain glitches in crossed dungeon mode. (default: prevent)
```
--standardize_palettes (mode)
```
Whether to standardize dungeon palettes in crossed dungeon mode. (default: standardize)
```
--reduce_flashing
```
Reduces amount of flashing in some animations
```
--shuffle_sfx
```
Shuffles a bunch of the sounds effects
```
--msu_resume
```
Enable MSU resume feature
```
--collection_rate
```
Total collection rate is displayed unless Triforce Hunt is on. If the game was designed as a mystery, then the total count is not displayed.
+145 -252
View File
@@ -1,297 +1,190 @@
## New Features
# New Features
## Pottery Lottery and Key Drop Shuffle Changes
One major change with this update is that big key doors and certain trap doors are no longer guaranteed to be vanilla in Dungeon Door Shuffle modes even if you choose not to shuffle those types. A newer algorithm for putting dungeons together has been written and it will remove big key doors and trap doors when necessary to ensure progress can be made.
### Pottery
Please note that retro features are now independently customizable as referenced below. Selecting Retro mode or World State: Retro will change Bow Mode to Retro (Progressive). Take Anys to Random, and Small Keys to Universal.
New pottery option that control which pots (and large blocks) are in the locations pool:
## Flute Mode
* None: No pots are in the pool, like normal randomizer
* Key Pots: The pots that have keys are in the pool. This is about half of the old keydropshuffle option
* Cave Pots: The pots that are not found in dungeons are in the pool. (Includes the large block in Spike Cave). Does
not include key pots.
* CaveKeys: Both non-dungeon pots and pots that used to have keys are in the pool.
* Reduced: Same as CaveKeys but also roughly a quarter of dungeon pots are added to the location pool picked at random. This is a dynamic mode so pots in the pool will be colored. Pots out of the pool will have vanilla contents.
* Clustered: Like reduced but pots are grouped by logical sets and roughly 50% of pots are chosen from those group. This is a dynamic mode like the above.
* Nonempty: All pots that had some sort of objects under them are chosen to be in the location pool. This excludes most large blocks and some pots out of dungeons.
* Dungeon Pots: The pots that are in dungeons are in the pool. (Includes serveral large blocks)
* Lottery: All pots and large blocks are in the pool
Normal mode for flute means you need to activate it at the village statue after finding it like usual.
Activated flute mode mean you can use it immediately upon finding it. the flute SFX plays to let you know this is the case.
By default, switches remain in their vanilla location (unless you turn on the legacy option below)
## Bow Mode
CLI `--pottery <option>` from `none, keys, cave, cavekeys, reduced, clustered, nonempty, dungeon, lottery`
Four options here:
Note for multiworld: due to the design of the pottery lottery, only 256 items for other players can be under pots in your world.
* Progressive. Standard progressive bows.
* Silvers separate. One bow in the pool and silvers are a separate item.
* Retro (progressive). Arrows cost rupees. You need to purchase the single arrow item at a shop and there are two progressive bows places.
* Retro + Silvers. Arrows cost rupees. You need to purchase the single arrow item or find the silvers, there is only one bow, and silvers are a separate item (but count for the quiver if found).
### Colorize Pots
## Dungeon Shuffle Features
If the pottery mode is dynamic, this option is forced to be on (clustered and reduced). It is allowed to be on in all other pottery modes. Exceptions include "none" where no pots would be colored, and "lottery" where all pots would be. This option colors the pots differently that have been chosen to be part of the location pool. If not specified, you are expected to remember the pottery setting you chose. Note that Mystery will colorize all pots if lottery is chosen randomly.
### Small Keys
CLI `--colorizepots`
There are three options now available:
### Shuffle key drops
* In Dungeon: The small key will be in their own dungeon
* Randomized: Small keys can be shuffled outside their own dungeon
* Universal: Retro keys without the other options
Enemies that drop keys can have their drop shuffled into the pool. This is the other half of the keydropshuffle option.
### Dungeon Door Shuffle
CLI `--dropshuffle`
New mode: Partitioned. Partly between basic and crossed, dungeons are shuffled in 3 pools:
#### Legacy options
* Light World dungeons, Hyrule Castle and Aga Tower are mixed together
* Palace of Darkness, Swamp Palace, Skull Woods, and Thieves Town are mixed together
* The other dark world dungeons including Ganons Tower are mixed together
"Drop and Pot Keys" or `--keydropshuffle` is still availabe for use. This simply sets the pottery to keys and turns dropshuffle on as well to have the same behavior as the old setting.
### Door Types to Shuffle
The old "Pot Shuffle" option is still available under "Pot Shuffle (Legacy)" or `--shufflepots` and works the same by shuffling all pots on a supertile. It works with the lottery option as well to move the switches to any valid pot on the supertile regardless of the pots chosen in the pottery mode. This may increase the number of pot locations slightly depending on the mode.
Four options here, and all of them only take effect if Dungeon Door Shuffle is not Vanilla:
#### Tracking Notes
* Small Key Doors, Bomb Doors, Dash Doors: This is what was normally shuffled previously
* Adds Big Keys Doors: Big key doors are now shuffled in addition to those above, and Big Key doors are enabled to be on in both vertical directions thanks to a graphic that ended up on the cutting room floor. This does change
* Adds Trap Doors: All trap doors that are permanently shut in vanilla are shuffled.
* Increases all Door Types: This is a chaos mode where each door type per dungeon is randomized between 1 less and 4 more.
The sram locations for pots and sprite drops are now final, please reach out for assistance or investigate the rom changes if needed.
Note: Boss Trap doors are removed currently and not added into the trap door pool as extra trap doors. This may not be a permanent change
## New Options
### Decouple Doors
### Collection Rate
You can set the collection rate counter on using the "Display Collection Rate" on the Game Options tab are using the CLI option `--collection_rate`. Mystery seeds will not display the total.
### Goal: Trinity
Triforces are placed behind Ganon, on the pedestal, and on Murahdahla with 8/10 triforce pieces required. Recommend to run with 4-5 Crystal requirement for Ganon. Automatically pre-opens the pyramid.
### Boss Shuffle: Unique
At least one boss each of the prize bosses will be present guarding the prizes. GT bosses can be anything.
### MSU Resume
Turns on msu resume support. Found on "Game Options" tab, the "Adjust/Patch" tab, or use the `--msu_resume` CLI option.
### BPS Patch
Creates a bps patch for the seed. Found on the "Generation Setup" tab called "Create BPS Patches" or `--bps`. Can turn off generating a rom using the existing "Create Patched ROM" option or `--suppress_rom`. There is an option on the Adjust/Patch tab to select a bps file to apply to the Base Rom selected on the Generation Setup tab using the Patch Rom button. Selected adjustments will be applied during patching.
## New Font
Font updated to support lowercase English. Lowercase vs. uppercase typos may exist. Note, you can use lowercase English letters on the file name.
This is similar to insanity mode in ER where door entrances and exits are not paired anymore. Tends to remove more logic from dungeons as many rooms will not be required to traverse to explore. Hope you like transitions.
## Customizer
Please refer to [the documentation](docs/Customizer.md) and examples of customizer [here](docs/customizer_example.yaml) and [here](docs/multi_mystery_example.yaml)
note that entrance customization is only available with experimental features turned on.
Please see [Customizer documentation](docs/Customizer.md) on how to create custom seeds.
## Experimental Entrance Shuffle
## New Goals
To support customizer and future entrance shuffle modes (perhaps even customizable ones), the entrance shuffle algorithm has been re-written. It is currently in an unstable state, and will use the old method unless you turn experimental features on. I'm currently in the process of evaluating most modes with different combinations of settings and checking the distribution of entrances. Entrance customization is only supported with this new experimental entrance shuffle. The experimental entrance shuffle includes prototypes of Lean and Lite entrance shuffles from the OWR branch.
### Triforce Hunt + Ganon
Collect the requisite triforce pieces, then defeat Ganon. (Aga2 not required). Use `ganonhunt` on CLI
## Restricted Item Placement Algorithm
### Completionist
All dungeons not enough for you? You have to obtain every item in the game too. This option turns on the collection rate counter and forces accessibility to be 100% locations. Finish by defeating Ganon.
The "Item Sorting" option or ```--algorithm``` has been updated with new placement algorithms. Older algorithms have been removed.
When referenced below, Major Items include all Y items, all A items, all equipment (swords, shields, & armor) and Heart Containers. Dungeon items are considered major if shuffled outside of dungeons. Bomb and arrows upgrades are Major if shopsanity is turned on. The arrow quiver and universal small keys are Major if retro is turned on. Triforce Pieces are Major if that is the goal, and the Bomb Bag is Major if that is enabled.
Here are the current fill options:
### Balanced
## Standard Generation Change
This one stays the same as before and is recommended for the most random distribution of items.
Hyrule Castle in standard mode is generated a little differently now. The throne room is guaranteed to be in Hyrule Castle and the Sanctuary is guaranteed to be beyond that. Additionally, the Mirror Scroll will bring you back to Zelda's Cell or the Throne Room depending on what save point you last obtained, this is to make it consistent with where you end up if you die. If you are lucky enough to find the Mirror, it behaves differently and brings you the last entrance used - giving you more options for exploration in Hyrule Castle.
### Vanilla Fill
## ER Features
This fill attempts to place all items in their vanilla locations when possible. Obviously shuffling entrances or the dungeon interiors will often prevent items from being placed in their vanilla location. If the vanilla fill is not possible, then other locations are tried in sequence preferring "major" locations (see below), then heart piece locations, then the rest except for GT locations which are preferred last. Note the PoD small key that is normally found in the dark maze in vanilla is move to Harmless Hellway due to the placement algorithm limitation.
### New Experimental Algorithm
### Major Location Restriction
To accommodate future flexibility the ER algorithm was rewritten for easy of use. This allows future modes to be added more easily. This new algorithm is only used when the experimental flag is turned on.
This fill attempts to place major items in major locations. Major locations are where the major items are found in the vanilla game. This includes the spot next to Uncle in the Sewers, and the Boomerang chest in Hyrule Castle.
### Lite/Lean ER (Experimental required)
This location pool is expanded to where dungeon items are locations if those dungeon items are shuffled. The Capacity Fairy locations are included if Shopsanity is on. If retro is enabled in addition to shopsanity, then the Old Man Sword Cave and one location in each retro cave is included. Key drop locations can be included if small or big key shuffle is on. This gives a very good balance between overworld and underworld locations though the dungeons ones will be on bosses and in big chests generally. Seeds do become more linear but usually easier to figure out.
Designed by Codemann, these are available now (only with experimental turned on - they otherwise fail)
### Dungeon Restriction
#### Lite
- Dungeon and multi-entrance caves can only lead to dungeon and multi-entrance caves
- Dropdowns can only lead to dropdowns, with them staying coupled to their appropriate exits
- Cave entrances that normally lead to items can only lead to caves that have items (this includes Potion Shop and Big Bomb Shop)
- All remaining entrances remain vanilla
- Multi-entrance caves are connected same-world only
- LW is guaranteed to have HC/EP/DP/ToH/AT and DW: IP/MM/TR/GT
- Shop locations are included in the Item Cave pool if Shopsanity is enabled
- Houses with pots are included in the Item Cave pool if Pottery is enabled
The fill attempts to place all major items in dungeons. It will overflow to the overworld if there are more items than locations (e.g. Triforce hunt.) This fill does attempt to run the GT trash fill when possible. Seeds are typically very linear but tend to be more difficult.
#### Lean
- Same grouping/pooling mechanism as in Lite ER
- Both dungeons and connectors can be cross-world connections
- No dungeon guarantees like in Lite ER
### District Restriction
The world is divided up into different regions or districts. Each dungeon is its own district. The overworld consists of the following districts:
### Back of Tavern Shuffle (Experimental required)
Light world:
Thanks goes to Catobat which now allows the back of tavern to be shuffled anywhere and any valid cave can be at the back of tavern with this option checked. Available in experimental only for now as it requires the new algorithm to be shuffled properly.
* Kakariko (The main screen, blacksmith screen, and library/maze race screens)
* Northwest Hyrule (The lost woods and fortune teller screens all the way to the river west of the potion shop)
* Central Hyrule (Hyrule castle, Link's House, the marsh, and the haunted grove)
* Desert (From the thief to the main desert screen)
* Lake Hylia (Around the lake)
* Eastern Hyrule (The eastern wild, the potion shop, and Zora's Domain)
* Death Mountain
#### Take Any Caves
Dark world:
These are now independent of retro mode and have three options: None, Random, and Fixed. None disables the caves. Random works as take-any caves did before. Fixed means that the take any caves replace specific fairy caves in the pool and will be at those entrances unless ER is turned on (then they can be shuffled wherever). The fixed entrances are:
* East Dark World (The pyramid, Palace of darkness, and Catfish)
* South Dark World (The dark lake, swamp area, to the dig game)
* Northwest Dark World (Village of Outcasts, to the Dark Sanctuary and screens in between)
* The Mire
* Dark Death Mountain
* Desert Healer Fairy
* Swamp Healer Fairy (aka Light Hype Cave)
* Dark Death Mountain Healer Fairy
* Dark Lake Hylia Ledge Healer Fairy (aka Shopping Mall Bomb)
* Bonk Fairy (Dark)
These districts are chosen at random and then filled with major items. If a location is part of a chosen district, but there are no more major items to place, a single green rupee is placed in the extra to indicate that as a placeholder. All other single green rupees are changed to be a blue rupee in order to not give false positives.
# Bug Fixes and Notes
In entrance shuffle, what is shuffled to the entrances is considered instead of where the interior was originally. For example, if Blind's Hut is shuffled to the Dam, then the 5 chests in Blind's Hut are part of Central Hyrule instead of Kakariko.
* Unreleased Version
* MultiServer can not disable forfeits if desired
* 1.2.0.12u
* Fix for mirror portal in inverted
* Yet another fix for blocked door in Standard ER
* 1.2.0.11u
* Fixed an issue with lower layer doors in Standard
* Fix for doors in cave state (will no longer be vanilla)
* Added a logic rule for th murderdactyl near bumper ledge for OHKO purposes
* Enemizer alteration for Hovers and normal enemies in shallow water
* Fix for beemizer including modes with an increased item pool
* Fix for district algorithm
* 1.2.0.10u
* Fixed overrun issues with edge transitions
* Better support for customized start_inventory with dungeon items
* Colorized pots now available with lottery. Default is on.
* Dungeon_only support pottery
* Fix AllowAccidentalGlitches flag in OWG
* Potential fix for mirror portal and entering cave on same frame
* A few other minor issues, generation and graphical
* 1.2.0.9-u
* Disallowed standard exits (due to ER) are now graphically half blocked instead of missing
* Graphical issues with Sanctuary and Swamp Hub lobbies are fixed
* Fixes an issue surrounding door state and decoupled doors leading to blocked doors
* Customizer improvements:
* Better logic around customized lobbies
* Better logic around customized door types
* Fix to key doors that was causing extra key doors
* Generation improvement around crystal switches
* Fix bug in dungeon_only that wasn't using pot key locations (known issue still exists in pottery modes)
* Fixes for multiworld:
* Fixes an issue when keys are found in own dungeon for another player when using the bizhawk plugin.
* Fixes an issue with absorbables for another player also being received by the player picking it up.
* 1.2.0.8-u
* New Features: trap_door_mode and key_logic_algorithm
* Change S&Q in door shuffle + standard during escape to spawn as Uncle
* Fix for vanilla doors + certain ER modes
* Fix for unintentional decoupled door in standard
* Fix a problem with BK doors being one-sided
* Change to how wilds keys are placed in standard, better randomization
* Removed a Triforce text
* Fix for Desert Tiles 1 key door
* 1.2.0.7-u
* Fix for some misery mire key logic
* Minor standard generation fix
* Fix for inactive flute start
* Settingsfile for multiworld generation support
* Fix for duped HC/AT Maps/Compasses
* 1.2.0.6-u
* Fix for light cone in Escape when entering from Dark World post-zelda
* Fix for light cone in Escape when lighting a torch with fire rod
* 1.2.0.5.u
* Logic fix for Sanctuary mirror (it wasn't resetting the crystal state)
* Minor bugfixes for customizer
* 1.2.0.4-u
* Starting inventory fixes if item not present in the item pool.
* Support for Assured sword setting and OWG Boots when using a custom item pool. (Customizer or GUI)
* Logic fix for the skull woods star tile that lets you into the X pot room. Now accounts for small key or big key door there blocking the way from the star tile. A trap door is not allowed there.
* Standard logic improvement that requires a path from Zelda to the start so that you cannot get softlocked by rescuing Zelda. Standard mirror scroll change may need to be reverted if impossible seed are still generated.
* 1.2.0.3-u
* Starting inventory taken into account with default item pool. (Custom pools must do this themselves)
* Fast ROM update
* Fix for restricted boss item counting maps & compasses as vital
* Bug fix for vanilla ER + inverted + experimental
* 1.2.0.2-u
* Fixed a bug with certain trap doors missing
* Added a hint reference for district hints
* 1.2.0.1-u
* Added new ganonhunt and completionist goals
* Fixed the issue when defeating Agahnim and standing in the doorway can cause door state to linger.
* Fix for Inverted Lean/Lite ER
* Fix for vanilla Doors + Standard + ER
* Added a limit per dungeon on small key doors to ensure reasonable generation
* Fixed many small bugs
Note: Bombos Tablet, Lake Hylia Island, Bumper Cave Ledge, the Floating Island, Cave 45, the Graveyard Cave, Checkerboard Cave and Mimic Cave are considered part of the light world region rather than the dark world region you mirror from.
# Known Issues
In multiworld, the districts chosen apply to all players.
### CLI values:
```balanced, vanilla_fill, major_only, dungeon_only, district```
## New Hints
Based on the district algorithm above (whether it is enabled or not,) new hints can appear about that district or dungeon. For each district and dungeon, it is evaluated whether it contains vital items and how many. If it has not any vital item, items then it moves onto useful items. Useful items are generally safeties or convenience items: shields, mails, half magic, bottles, medallions that aren't required, etc. If it contains none of those and is an overworld district, then it checks for a couple more things. First, if dungeons are shuffled, it looks to see if any are in the district, if so, one of those dungeons is picked for the hint. Then, if connectors are shuffled, it checks to see if you can get to unique region through a connector in that district. If none of the above apply, the district or dungeon is considered completely foolish.
## Overworld Map shows Dungeon Entrances
Option to move indicators on overworld map to reference dungeon location. The non-default options include indicators for Hyrule Castle, Agahnim's Tower, and Ganon's Tower.
CLI ```--overworld_map```
#### Options
##### default
Status quo. Showing only the prize markers on the vanilla dungeon locations.
##### compass
The compass item controls whether the marker is moved to the dungeons locations. If you possess the compass but not the map, only a glowing X will be present regardless of dungeon prize type, if you only possess the map, the prizes will be shown in predicable locations at the bottom of the overworld map instead of the vanilla location. Light world dungeons on the light world map and dark world dungeons on the dark world map. If you posses both map and compass, then the prize of the dungeon and the location will be on the map.
If you do not shuffle the compass or map outside of the dungeon, the non-shuffled items are not needed to display the information. If a dungeon does not have a map or compass, it is not needed for the information. Talking to the bomb shop or Sahasrahla furnishes you with complete information as well as map information.
##### map
The map item plays double duty in this mode and only possession of the map will show both prize and location of the dungeon. If you do not shuffle maps or the dungeon does not have a map, the information will be displayed without needing to find any items.
## Restricted Dungeon Items on Bosses
You may now restrict the items that can appear on the boss, like the popular ambrosia preset does.
CLI: ```--restrict_boss_items <option>```
#### Options
##### none
As before, the boss may have any item including any dungeon item that could occur there.
##### mapcompass
~~The map and compass are logically required to defeat a boss. This prevents both of those from appearing on the dungeon boss. Note that this does affect item placement logic and the placement algorithm as maps and compasses are considered as required items to beat a boss.~~
Currently bugged, not recommended for use.
##### dungeon
Same as above but both small keys and bigs keys of the dungeon are not allowed on a boss. (Note: this does not affect universal keys as they are not dungeon-specific)
## Notes and Bug Fixes
#### StandardThrone
* Bug fixes
* Fix HC Ledge entrances after rescuing Zelda, also includes the throne spawn point now.
* Original Release
* Changed standard dungeon generation to always have Throne Room in hyrule castle and always have sanctuary behind it
* S&Q/death in standard after moving the tapestry but before delivering Zelda will result in spawning at the tapestry
* Mirror scroll will return you to Zelda's cell instead of last entrance. This reverts to normal behavior once the tapestry open trigger is reached
#### Customizer
* Fixed an issue where Interior Key Doors were missing from custom yaml output
* Updated lite/lean ER for pottery settings
* Fixed an issue with lite/lean ER not generating
* Fixed up the GUI selection of the customizer file.
* Fixed up the item_pool section to skip a lot of pool manipulations. Key items will be added (like the bow) if not detected. Extra dungeon items can be added to the pool and will be confined to the dungeon if possible (and not shuffled). If the pool isn't full, junk items are added to the pool to fill it out.
#### Unstable
* 1.0.1.3
* Fix for rain prevented doors fouling up key doors
* Couple minor issues
* 1.0.1.2
* Removed "good bee" as an in-logic way of killing Mothula
* Fixed an issue with Mystery generation and Windows path
* Fixed an issue with small key bias rework
* Fixed an issue where trinity goal would open pyramid unexpectedly. (No longer does so if ER mdoe is shuffling holes). Crystals goal updated to match that behavior.
* Fixed a playthrough issue that was not respecting pot rules
* Fixed an issue that was conflicting with downstream OWR project
* Fixed an issue with inverted and certain pottery settings
* Fixed an issue with small keys being shuffled and big keys not (key distribution)
* 1.0.1.1
* Fixed the pots in Mire Storyteller/ Dark Desert Hint to be colorized when they should be
* Certain pot items no longer reload when reloading the supertile (matches original pot behavior better)
* Changed the key distribution that made small keys placement more random when keys are in their own dungeon
* Unique boss shuffle no longer allows repeat bosses in GT (e.g. only one Trinexx in GT, so exactly 3 bosses are repeated in the seed. This is a difference process than full which does affect the probability distribution.)
* Removed text color in hints due to vanilla bug
* 1.0.1.0
* Large features
* New pottery modes - see notes above
* Pot substitutions added for red rupees, 10 bomb packs, 3 bomb packs, and 10 arrows have been added. They use objects that can result from a tree pull or other drop. The 3 bomb pack becomes a 4 bomb pack and the 10 bomb pack becomes an 8 pack. These substitutions are repeatable like all other normal pot contents.
* Updated TFH to support up to 850 pieces
* New font support
* Trinity goal added
* Separated Collection Rate counter from experimental
* Added MSU Resume option
* Support for BPS patch creation and applying patches during adjustment
* New option for Boss Shuffle: Unique (Prize bosses will be one of each, but GT bosses can be anything)
* Logic Notes
* Skull X Room requires Boots or access to Skull Back Drop
* GT Falling Torches requires Boots to get over the falling tile gap (this is a stop-gap measure until more sophisticated crystal switch traversal is possible)
* Waterfall of Wishing logic in open. You must have flippers to exit the Waterfall (flippers also required in glitched modes as well)
* Fix for GT Crystal Conveyor not requiring Somaria/Bombs to get through
* Pedestal goal + vanilla swords places a random sword in the pool
* Added a few more places Links House shouldn't go when shuffled
* Small features
* Added a check for python package requirements before running code. GUI and console both for better error messages. Thanks to mtrethewey for the idea.
* Refactored spoiler to generate in stages for better error collection. A meta file will be generated additionally for mystery seeds. Some random settings moved later in the spoiler to have the meta section at the top not spoil certain things. (GT/Ganon requirements.) Thanks to codemann and OWR for most of this work.
* Updated tourney winners (included Doors Async League winners)
* Some textual changes for hints (capitalization standardization)
* Reworked GT Trash Fill. Base rate is 0-75% of locations fill with 7 crystals entrance requirements. Triforce hunt is 75%-100% of locations. The 75% number will decrease based on the crystal entrance requirement. Dungeon_only algorithm caps it based on how many items need to be placed in dungeons. Cross dungeon shuffle will now work with the trash fill.
* Expanded Mystery logic options (e.g. owglitches)
* Updated indicators on keysanity menu for overworld map option
* Bug fixes:
* Fix for Zelda (or any follower) going to the maiden cell supertile and the boss is not Blind. The follower will not despawn unless the boss is Blind, then the maiden will spawn as normal.
* Bug with 0 GT crystals not opening GT
* Fixed a couple issues with dungeon counters and the DungeonCompletion field for autotracking
* Settings code fix
* Fix for forbidding certain dashable doors (it actually does something this time)
* Fix for major item algorithm and pottery
* Updated map display on keysanity menu to work better with overworld_map option
* Minor bug in crossed doors
* Fix for Multiworld forfeits, shops and pot items now included
* MultiServer fix for ssl certs and python
* forbid certain doors from being dashable when you either can't dash them open (but bombs would work) or you'd fall into a pit from the bonk recoil in OHKO
* Fixed a couple rain state issues
* Add major_only algorithm to settings code
* Fixes for Links House being at certain entrances (did not generate)
* Fix for vanilla_fill, it now prioritizes heart container placements
* Fix for dungeon counter showing up in AT/HC in crossed dungeon mode
* Fixed usestartinventory with mystery
* Added double click fix for install.py
* Fix for SFX shuffle
* Fix for districting + shopsanity
* Fix for multiworld progression balancing would place Nothing or Arrow items
* Fixed a bug with shopsanity + district algorithm where pre-placed potions messed up the placeholder count
* Fixed usestartinventory flag (can be use on a per player basis)
* Sprite selector fix for systems with SSL issues
* Fix for Standard ER where locations in rain state could be in logic
* 1.0.0.3
* overworld_map=map mode fixed. Location of dungeons with maps are not shown until map is retrieved. (Dungeon that do not have map like Castle Tower are simply never shown)
* Aga2 completion on overworld_map now tied to boss defeat flag instead of pyramid hole being opened (fast ganon fix)
* Minor issue in dungeon_only algorithm fixed (minorly affected major_only keyshuffle and vanilla fallbacks)
* 1.0.0.2
* Include 1.0.1 fixes
* District hint rework
* 1.0.0.1
* Add Light Hype Fairy to bombbag mode as needing bombs
### From stable DoorDev
* 1.0.1
* Fixed a bug with key doors not detecting one side of an interior door
* Sprite selector fix for systems with SSL issues
* Decoupled doors can lead to situations where you aren't logically supposed to go back through a door without a big key or small key, but you can if you press the correct direction back through the door first. There are some transitions where you may get stuck without a bomb. These problems are planned to be fixed.
* Logic getting to Skull X room may be wrong if a trap door, big key door, or bombable wall is shuffled there. A bomb jump to get to those pot may be required if you don't have boots to bonk across.
+3 -2
View File
@@ -183,7 +183,8 @@ def create_regions(world, player):
create_cave_region(player, 'Chest Game', 'a game of 16 chests', ['Chest Game']),
create_cave_region(player, 'Red Shield Shop', 'the rare shop', ['Red Shield Shop - Left', 'Red Shield Shop - Middle', 'Red Shield Shop - Right']),
create_cave_region(player, 'Dark Sanctuary Hint', 'a storyteller'),
create_cave_region(player, 'Bumper Cave', 'a connector', None, ['Bumper Cave Exit (Bottom)', 'Bumper Cave Exit (Top)']),
create_cave_region(player, 'Bumper Cave (bottom)', 'a connector', None, ['Bumper Cave Exit (Bottom)', 'Bumper Cave Bottom to Top']),
create_cave_region(player, 'Bumper Cave (top)', 'a connector', None, ['Bumper Cave Exit (Top)', 'Bumper Cave Top To Bottom']),
create_dw_region(player, 'Bumper Cave Ledge', ['Bumper Cave Ledge'], ['Bumper Cave Ledge Drop', 'Bumper Cave (Top)', 'Bumper Cave Ledge Mirror Spot'], 'a ledge with an item'),
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)']),
@@ -339,7 +340,7 @@ def create_dungeon_regions(world, player):
create_dungeon_region(player, 'Desert Dead End', 'Desert Palace', None, ['Desert Dead End Edge']),
create_dungeon_region(player, 'Desert East Lobby', 'Desert Palace', None, ['Desert East Lobby WS', 'Desert East Lobby S']),
create_dungeon_region(player, 'Desert East Wing', 'Desert Palace', None, ['Desert East Wing ES', 'Desert East Wing Key Door EN', 'Desert East Wing W Edge', 'Desert East Wing N Edge']),
create_dungeon_region(player, 'Desert Compass Room', 'Desert Palace', ['Desert Palace - Compass Chest'], ['Desert Compass Key Door WN', 'Desert Compass NW']),
create_dungeon_region(player, 'Desert Compass Room', 'Desert Palace', ['Desert Palace - Compass Chest'], ['Desert Compass Key Door WN', 'Desert Compass NE']),
create_dungeon_region(player, 'Desert Cannonball', 'Desert Palace', ['Desert Palace - Big Key Chest'], ['Desert Cannonball S']),
create_dungeon_region(player, 'Desert Arrow Pot Corner', 'Desert Palace', None, ['Desert Arrow Pot Corner S Edge', 'Desert Arrow Pot Corner W Edge', 'Desert Arrow Pot Corner NW']),
create_dungeon_region(player, 'Desert Trap Room', 'Desert Palace', None, ['Desert Trap Room SW']),
+89 -25
View File
@@ -15,7 +15,7 @@ try:
except ImportError:
raise Exception('Could not load BPS module')
from BaseClasses import ShopType, Region, Location, Door, DoorType, RegionType, LocationType, Item
from BaseClasses import ShopType, Region, Location, Door, DoorType, RegionType, LocationType
from DoorShuffle import compass_data, DROptions, boss_indicator, dungeon_portals
from Dungeons import dungeon_music_addresses, dungeon_table
from Regions import location_table, shop_to_location_table, retro_shops
@@ -561,7 +561,7 @@ class Sprite(object):
def handle_native_dungeon(location, itemid):
# Keys in their native dungeon should use the original item code for keys
if location.parent_region.dungeon:
if location.parent_region.dungeon and location.player == location.item.player:
if location.parent_region.dungeon.name == location.item.dungeon:
if location.item.bigkey:
return 0x32
@@ -708,7 +708,8 @@ def patch_rom(world, rom, player, team, is_mystery=False):
dr_flags = DROptions.Eternal_Mini_Bosses if world.doorShuffle[player] == 'vanilla' else DROptions.Town_Portal
if world.doorShuffle[player] not in ['vanilla', 'basic']:
dr_flags |= DROptions.Map_Info
if world.collection_rate[player] and world.goal[player] not in ['triforcehunt', 'trinity']:
if ((world.collection_rate[player] or world.goal[player] == 'completionist')
and world.goal[player] not in ['triforcehunt', 'trinity', 'ganonhunt']):
dr_flags |= DROptions.Debug
if world.doorShuffle[player] not in ['vanilla', 'basic'] and world.logic[player] != 'nologic'\
and world.mixed_travel[player] == 'prevent':
@@ -1062,9 +1063,14 @@ def patch_rom(world, rom, player, team, is_mystery=False):
rom.write_byte(0x178000 + i, random.randint(0, 255))
# shuffle prize packs
prizes = [0xD8, 0xD8, 0xD8, 0xD8, 0xD9, 0xD8, 0xD8, 0xD9, 0xDA, 0xD9, 0xDA, 0xDB, 0xDA, 0xD9, 0xDA, 0xDA, 0xE0, 0xDF, 0xDF, 0xDA, 0xE0, 0xDF, 0xD8, 0xDF,
0xDC, 0xDC, 0xDC, 0xDD, 0xDC, 0xDC, 0xDE, 0xDC, 0xE1, 0xD8, 0xE1, 0xE2, 0xE1, 0xD8, 0xE1, 0xE2, 0xDF, 0xD9, 0xD8, 0xE1, 0xDF, 0xDC, 0xD9, 0xD8,
0xD8, 0xE3, 0xE0, 0xDB, 0xDE, 0xD8, 0xDB, 0xE2, 0xD9, 0xDA, 0xDB, 0xD9, 0xDB, 0xD9, 0xDB]
prizes = [0xD8, 0xD8, 0xD8, 0xD8, 0xD9, 0xD8, 0xD8, 0xD9,
0xDA, 0xD9, 0xDA, 0xDB, 0xDA, 0xD9, 0xDA, 0xDA,
0xE0, 0xDF, 0xDF, 0xDA, 0xE0, 0xDF, 0xD8, 0xDF,
0xDC, 0xDC, 0xDC, 0xDD, 0xDC, 0xDC, 0xDE, 0xDC,
0xE1, 0xD8, 0xE1, 0xE2, 0xE1, 0xD8, 0xE1, 0xE2,
0xDF, 0xD9, 0xD8, 0xE1, 0xDF, 0xDC, 0xD9, 0xD8,
0xD8, 0xE3, 0xE0, 0xDB, 0xDE, 0xD8, 0xDB, 0xE2,
0xD9, 0xDA, 0xDB, 0xD9, 0xDB, 0xD9, 0xDB]
dig_prizes = [0xB2, 0xD8, 0xD8, 0xD8, 0xD8, 0xD8, 0xD8, 0xD8, 0xD8,
0xD9, 0xD9, 0xD9, 0xD9, 0xD9, 0xDA, 0xDA, 0xDA, 0xDA, 0xDA,
0xDB, 0xDB, 0xDB, 0xDB, 0xDB, 0xDC, 0xDC, 0xDC, 0xDC, 0xDC,
@@ -1077,13 +1083,41 @@ def patch_rom(world, rom, player, team, is_mystery=False):
return [l[i:i+n] for i in range(0, len(l), n)]
# randomize last 7 slots
prizes [-7:] = random.sample(prizes, 7)
possible_prizes = {
'Small Heart': 0xD8, 'Fairy': 0xE3,
'Rupee (1)': 0xD9, 'Rupees (5)': 0xDA, 'Rupees (20)': 0xDB,
'Big Magic': 0xE0, 'Small Magic': 0xDF,
'Single Bomb': 0xDC, 'Bombs (4)': 0xDD,
'Bombs (8)': 0xDE, 'Arrows (5)': 0xE1, 'Arrows (10)': 0xE2
} #weights, if desired 13, 1, 9, 7, 6, 3, 6, 7, 1, 2, 5, 3
uniform_prizes = list(possible_prizes.values())
prizes[-7:] = random.sample(prizes, 7)
#shuffle order of 7 main packs
packs = chunk(prizes[:56], 8)
random.shuffle(packs)
prizes[:56] = [drop for pack in packs for drop in pack]
if world.customizer:
drops = world.customizer.get_drops()
if drops:
for player, drop_config in drops.items():
for pack_num in range(1, 8):
if f'Pack {pack_num}' in drop_config:
for prize, idx in enumerate(drop_config[f'Pack {pack_num}']):
chosen = random.choice(uniform_prizes) if prize == 'Random' else possible_prizes[prize]
prizes[(pack_num-1)*8 + idx] = chosen
for tree_pull_tier in range(1, 4):
if f'Tree Pull Tier {tree_pull_tier}' in drop_config:
prize = drop_config[f'Tree Pull Tier {tree_pull_tier}']
chosen = random.choice(uniform_prizes) if prize == 'Random' else possible_prizes[prize]
prizes[63-tree_pull_tier] = chosen # (62 through 60 in reverse)
for key, pos in {'Crab Normal': 59, 'Crab Special': 58, 'Stun Prize': 57, 'Fish': 56}.items():
if key in drop_config:
prize = drop_config[key]
chosen = random.choice(uniform_prizes) if prize == 'Random' else possible_prizes[prize]
prizes[pos] = chosen
if world.difficulty_adjustments[player] in ['hard', 'expert']:
prize_replacements = {0xE0: 0xDF, # Fairy -> heart
0xE3: 0xD8} # Big magic -> small magic
@@ -1220,7 +1254,7 @@ def patch_rom(world, rom, player, team, is_mystery=False):
# set up goals for treasure hunt
rom.write_bytes(0x180165, [0x0E, 0x28] if world.treasure_hunt_icon[player] == 'Triforce Piece' else [0x0D, 0x28])
if world.goal[player] in ['triforcehunt', 'trinity']:
if world.goal[player] in ['triforcehunt', 'trinity', 'ganonhunt']:
rom.write_bytes(0x180167, int16_as_bytes(world.treasure_hunt_count[player]))
rom.write_byte(0x180194, 1) # Must turn in triforced pieces (instant win not enabled)
@@ -1283,6 +1317,10 @@ def patch_rom(world, rom, player, team, is_mystery=False):
rom.write_byte(0x18003E, 0x02) # make ganon invincible until all dungeons are beat
elif world.goal[player] in ['crystals', 'trinity']:
rom.write_byte(0x18003E, 0x04) # make ganon invincible until all crystals
elif world.goal[player] in ['ganonhunt']:
rom.write_byte(0x18003E, 0x05) # make ganon invincible until all triforce pieces collected
elif world.goal[player] in ['completionist']:
rom.write_byte(0x18003E, 0x0a) # make ganon invincible until everything is collected
else:
rom.write_byte(0x18003E, 0x03) # make ganon invincible until all crystals and aga 2 are collected
@@ -1427,7 +1465,7 @@ def patch_rom(world, rom, player, team, is_mystery=False):
rom.write_byte(0x1800A3, 0x01) # enable correct world setting behaviour after agahnim kills
rom.write_byte(0x1800A4, 0x01 if world.logic[player] != 'nologic' else 0x00) # enable POD EG fix
rom.write_byte(0x180042, 0x01 if world.save_and_quit_from_boss else 0x00) # Allow Save and Quit after boss kill
rom.write_byte(0x180358, 0x01 if world.logic[player] == 'nologic' else 0x00)
rom.write_byte(0x180358, 0x01 if (world.logic[player] in ['owglitches', 'nologic']) else 0x00)
# remove shield from uncle
rom.write_bytes(0x6D253, [0x00, 0x00, 0xf6, 0xff, 0x00, 0x0E])
@@ -1468,8 +1506,10 @@ def patch_rom(world, rom, player, team, is_mystery=False):
if world.doorShuffle[player] not in ['vanilla', 'basic']:
# Uncle respawn refills (magic, bombs, arrows)
rom.write_bytes(0x180185, [max(0x20, magic_max), max(3, bomb_max), max(10, bow_max)])
rom.write_bytes(0x180188, [0x20, 3, 10]) # Zelda respawn refills (magic, bombs, arrows)
rom.write_bytes(0x18018B, [0x20, 3, 10]) # Mantle respawn refills (magic, bombs, arrows)
# Zelda respawn refills (magic, bombs, arrows)
rom.write_bytes(0x180188, [max(0x20, magic_max), max(3, bomb_max), max(10, bow_max)])
# Mantle respawn refills (magic, bombs, arrows)
rom.write_bytes(0x18018B, [max(0x20, magic_max), max(3, bomb_max), max(10, bow_max)])
elif world.doorShuffle[player] == 'basic': # just in case a bomb is needed to get to a chest
rom.write_bytes(0x180185, [max(0x00, magic_max), max(3, bomb_max), max(0, bow_max)])
rom.write_bytes(0x180188, [magic_small, 3, bow_small]) # Zelda respawn refills (magic, bombs, arrows)
@@ -1524,11 +1564,11 @@ def patch_rom(world, rom, player, team, is_mystery=False):
rom.write_bytes(room.address(), room.rom_data())
if world.data_tables[player]:
colorize_pots = is_mystery or (world.pottery[player] not in ['vanilla', 'lottery']
and (world.colorizepots[player]
or world.pottery[player] in ['reduced', 'clustered']))
colorize_pots = (world.pottery[player] != 'vanilla', 'lottery'
and (world.colorizepots[player] or world.pottery[player] in ['reduced', 'clustered']))
world.data_tables[player].write_to_rom(rom, colorize_pots)
write_enemizer_tweaks(rom, world, player)
write_strings(rom, world, player, team)
# write initial sram
@@ -1618,6 +1658,11 @@ def write_custom_shops(rom, world, player):
rom.write_bytes(0x184900, items_data)
def write_enemizer_tweaks(rom, world, player):
if world.enemy_shuffle[player] != 'none':
rom.write_byte(snes_to_pc(0x1DF6D8), 0) # lets enemies walk on water instead of clipping into infinity?
rom.write_byte(snes_to_pc(0x0DB6B3), 0x82) # hovers don't need water necessarily?
def hud_format_text(text):
output = bytes()
for char in text.lower():
@@ -2157,44 +2202,42 @@ def write_strings(rom, world, player, team):
hint_candidates = []
for name, district in world.districts[player].items():
hint_type = 'foolish'
choice_set = set()
choices = []
item_count, item_type = 0, 'useful'
for loc_name in district.locations:
location_item = world.get_location(loc_name, player).item
if location_item.advancement:
if 'Heart Container' in location_item.name:
if 'Heart Container' in location_item.name or location_item.compass or location_item.map:
continue
itm_type = 'useful' if useful_item_for_hint(location_item, world) else 'vital'
hint_type = 'path'
if item_type == itm_type:
choice_set.add(location_item)
choices.append(location_item)
item_count += 1
elif itm_type == 'vital':
item_type = 'vital'
item_count = 1
choice_set.clear()
choice_set.add(location_item)
choices.clear()
choices.append(location_item)
if hint_type == 'foolish':
if district.dungeons and world.shuffle[player] != 'vanilla':
choice_set.update(district.dungeons)
choices.extend(district.dungeons)
hint_type = 'dungeon_path'
elif district.access_points and world.shuffle[player] not in ['vanilla', 'dungeonssimple',
'dungeonsfull']:
choice_set.update([x.hint_text for x in district.access_points])
choices.extend([x.hint_text for x in district.access_points])
hint_type = 'connector'
if hint_type == 'foolish':
hint_candidates.append((hint_type, f'{name} is a foolish choice'))
elif hint_type == 'dungeon_path':
choices = sorted(list(choice_set))
dungeon_choice = random.choice(choices) # prefer required dungeons...
hint_candidates.append((hint_type, f'{name} is on the path to {dungeon_choice}'))
elif hint_type == 'connector':
choices = sorted(list(choice_set))
access_point = random.choice(choices) # prefer required access...
hint_candidates.append((hint_type, f'{name} can reach {access_point}'))
elif hint_type == 'path':
if item_count == 1:
the_item = text_for_item(next(iter(choice_set)), world, player, team)
the_item = text_for_item(next(iter(choices)), world, player, team)
hint_candidates.append((hint_type, f'{name} conceals only {the_item}'))
else:
hint_candidates.append((hint_type, f'{name} conceals {item_count} {item_type} items'))
@@ -2264,7 +2307,24 @@ def write_strings(rom, world, player, team):
if world.goal[player] in ['dungeons']:
tt['sign_ganon'] = 'You need to complete all the dungeons.'
tt['uncle_leaving_text'] = Uncle_texts[random.randint(0, len(Uncle_texts) - 1)]
if world.boots_hint[player]:
starting_boots = next((i for i in world.precollected_items if i.player == player
and i.name == 'Pegasus Boots'), None)
if starting_boots:
uncle_text = 'Lonk! Boots\nare on\nyour feet.'
else:
boots_location = next((l for l in world.get_locations()
if l.player == player and l.item and l.item.name == 'Pegasus Boots'), None)
if boots_location:
district = next((d for k, d in world.districts[player].items()
if boots_location.name in d.locations), 'Zebes')
uncle_text = f'Lonk! Boots\nare in {district.name}'
else:
uncle_text = "I couldn't\nfind the Boots\ntoday.\nRIP me."
tt['uncle_leaving_text'] = uncle_text
else:
tt['uncle_leaving_text'] = Uncle_texts[random.randint(0, len(Uncle_texts) - 1)]
tt['end_triforce'] = "{NOBORDER}\n" + Triforce_texts[random.randint(0, len(Triforce_texts) - 1)]
tt['bomb_shop_big_bomb'] = BombShop2_texts[random.randint(0, len(BombShop2_texts) - 1)]
@@ -2286,6 +2346,10 @@ def write_strings(rom, world, player, team):
trinity_crystal_text = ('%d crystal to beat Ganon.' if world.crystals_needed_for_ganon[player] == 1 else '%d crystals to beat Ganon.') % world.crystals_needed_for_ganon[player]
tt['sign_ganon'] = 'Three ways to victory! %s Get to it!' % trinity_crystal_text
tt['murahdahla'] = "Hello @. I\nam Murahdahla, brother of\nSahasrahla and Aginah. Behold the power of\ninvisibility.\n\n\n\n… … …\n\nWait! you can see me? I knew I should have\nhidden in a hollow tree. If you bring\n%d triforce pieces, I can reassemble it." % int(world.treasure_hunt_count[player])
elif world.goal[player] == 'ganonhunt':
tt['sign_ganon'] = 'Go find the Triforce pieces to beat Ganon'
elif world.goal[player] == 'completionist':
tt['sign_ganon'] = 'Ganon only respects those who have done everything'
tt['ganon_fall_in'] = Ganon1_texts[random.randint(0, len(Ganon1_texts) - 1)]
tt['ganon_fall_in_alt'] = 'You cannot defeat me until you finish your goal!'
tt['ganon_phase_3_alt'] = 'Got wax in\nyour ears?\nI can not die!'
+4 -2
View File
@@ -103,7 +103,7 @@ def create_rooms(world, player):
Room(player, 0x60, 0x51309).door(Position.NorthE2, DoorKind.NormalLow2).door(Position.East2, DoorKind.NormalLow2).door(Position.East2, DoorKind.ToggleFlag).door(Position.EastN, DoorKind.Normal).door(Position.SouthE, DoorKind.Normal).door(Position.SouthE, DoorKind.IncognitoEntrance),
Room(player, 0x61, 0x51454).door(Position.West2, DoorKind.NormalLow).door(Position.West2, DoorKind.ToggleFlag).door(Position.East2, DoorKind.NormalLow).door(Position.East2, DoorKind.ToggleFlag).door(Position.South2, DoorKind.NormalLow).door(Position.South2, DoorKind.IncognitoEntrance).door(Position.WestN, DoorKind.Normal),
Room(player, 0x62, 0x51577).door(Position.West2, DoorKind.NormalLow2).door(Position.West2, DoorKind.ToggleFlag).door(Position.NorthW2, DoorKind.NormalLow2).door(Position.North, DoorKind.Normal).door(Position.SouthW, DoorKind.Normal).door(Position.SouthW, DoorKind.IncognitoEntrance),
Room(player, 0x63, 0xf88ed).door(Position.NorthE, DoorKind.StairKey).door(Position.InteriorW, DoorKind.TrapTriggerable).door(Position.SouthW, DoorKind.DungeonEntrance), # looked like a huge typo - I had to guess on StairKey
Room(player, 0x63, 0xf88ed).door(Position.NorthW, DoorKind.StairKey).door(Position.InteriorW, DoorKind.TrapTriggerable).door(Position.SouthW, DoorKind.DungeonEntrance), # looked like a huge typo - I had to guess on StairKey
Room(player, 0x64, 0xfda53).door(Position.InteriorS, DoorKind.Trap2),
Room(player, 0x65, 0xfdac5).door(Position.InteriorS, DoorKind.Normal),
Room(player, 0x66, 0xfa01b).door(Position.InteriorE2, DoorKind.Waterfall).door(Position.SouthW2, DoorKind.NormalLow2).door(Position.SouthW2, DoorKind.ToggleFlag).door(Position.InteriorW2, DoorKind.NormalLow2),
@@ -244,7 +244,9 @@ def create_rooms(world, player):
# Room(player, 0xff, 0x52c9a).door(Position.InteriorW, DoorKind.Bombable).door(Position.InteriorE, DoorKind.Bombable).door(Position.SouthE, DoorKind.CaveEntrance),
]
# fix some wonky things
world.get_room(0x51, player).change(1, DoorKind.Normal) # fix the dungeon changer
# should I put back the dungeon changer for certain logic - like no logic? maybe in basic
if world.doorShuffle[player] != 'vanilla':
world.get_room(0x51, player).delete(1) # remove the dungeon changer
world.get_room(0x60, player).swap(2, 4) # puts the exit at pos 2 - enables pos 3
world.get_room(0x61, player).swap(1, 6) # puts the WN door at pos 1 - enables it
world.get_room(0x61, player).swap(5, 6) # puts the Incognito Entrance at the end, so it can be deleted
+63 -17
View File
@@ -63,6 +63,10 @@ def set_rules(world, player):
add_rule(world.get_location('Ganon', player), lambda state: state.has('Beat Agahnim 2', player))
elif world.goal[player] in ['triforcehunt', 'trinity']:
add_rule(world.get_location('Murahdahla', player), lambda state: state.item_count('Triforce Piece', player) + state.item_count('Power Star', player) >= int(state.world.treasure_hunt_count[player]))
elif world.goal[player] == 'ganonhunt':
add_rule(world.get_location('Ganon', player), lambda state: state.item_count('Triforce Piece', player) + state.item_count('Power Star', player) >= int(state.world.treasure_hunt_count[player]))
elif world.goal[player] == 'completionist':
add_rule(world.get_location('Ganon', player), lambda state: state.everything(player))
if world.mode[player] != 'inverted':
set_big_bomb_rules(world, player)
@@ -123,6 +127,10 @@ def or_rule(rule1, rule2):
return lambda state: rule1(state) or rule2(state)
def and_rule(rule1, rule2):
return lambda state: rule1(state) and rule2(state)
def add_lamp_requirement(spot, player):
add_rule(spot, lambda state: state.has('Lamp', player, state.world.lamps_needed_for_dark_rooms))
@@ -276,8 +284,22 @@ def global_rules(world, player):
set_rule(world.get_entrance('Skull Big Chest Hookpath', player), lambda state: state.has('Hookshot', player))
set_rule(world.get_entrance('Skull Torch Room WN', player), lambda state: state.has('Fire Rod', player))
set_rule(world.get_entrance('Skull Vines NW', player), lambda state: state.has_sword(player))
set_rule(world.get_entrance('Skull 2 West Lobby Pits', player), lambda state: state.has_Boots(player) or state.has('Hidden Pits', player))
set_rule(world.get_entrance('Skull 2 West Lobby Ledge Pits', player), lambda state: state.has('Hidden Pits', player))
hidden_pits_door = world.get_door('Skull Small Hall WS', player)
def hidden_pits_rule(state):
return state.has('Hidden Pits', player)
if hidden_pits_door.bigKey:
key_logic = world.key_logic[player][hidden_pits_door.entrance.parent_region.dungeon.name]
hidden_pits_rule = and_rule(hidden_pits_rule, create_rule(key_logic.bk_name, player))
elif hidden_pits_door.smallKey:
d_name = hidden_pits_door.entrance.parent_region.dungeon.name
hidden_pits_rule = and_rule(hidden_pits_rule, eval_small_key_door('Skull Small Hall WS', d_name, player))
set_rule(world.get_entrance('Skull 2 West Lobby Pits', player), lambda state: state.has_Boots(player)
or hidden_pits_rule(state))
set_rule(world.get_entrance('Skull 2 West Lobby Ledge Pits', player), hidden_pits_rule)
set_defeat_dungeon_boss_rule(world.get_location('Skull Woods - Boss', player))
set_defeat_dungeon_boss_rule(world.get_location('Skull Woods - Prize', player))
@@ -389,7 +411,8 @@ def global_rules(world, player):
set_rule(world.get_entrance('GT Mimics 1 ES', player), lambda state: state.can_shoot_arrows(player))
set_rule(world.get_entrance('GT Mimics 2 WS', player), lambda state: state.can_shoot_arrows(player))
set_rule(world.get_entrance('GT Mimics 2 NE', player), lambda state: state.can_shoot_arrows(player))
# consider access to refill room
# consider access to refill room - interior doors would need a change
set_rule(world.get_entrance('GT Cannonball Bridge SE', player), lambda state: state.has_Boots(player))
set_rule(world.get_entrance('GT Gauntlet 1 WN', player), lambda state: state.can_kill_most_things(player))
set_rule(world.get_entrance('GT Gauntlet 2 EN', player), lambda state: state.can_kill_most_things(player))
set_rule(world.get_entrance('GT Gauntlet 2 SW', player), lambda state: state.can_kill_most_things(player))
@@ -649,7 +672,7 @@ def bomb_rules(world, player):
('Hyrule Dungeon Armory S', True), # One green guard
('Hyrule Dungeon Armory ES', True), # One green guard
('Hyrule Dungeon Armory Boomerang WS', True), # One blue guard
('Desert Compass NW', True), # Three popos
('Desert Compass NE', True), # Three popos
('Desert Four Statues NW', True), # Four popos
('Desert Four Statues ES', True), # Four popos
('Hera Beetles WS', False), # Three blue beetles and only two pots, and bombs don't work.
@@ -869,14 +892,17 @@ def default_rules(world, player):
set_rule(world.get_entrance('Graveyard Ledge Mirror Spot', player), lambda state: state.has_Pearl(player) and state.has_Mirror(player))
set_rule(world.get_entrance('Bumper Cave Entrance Rock', player), lambda state: state.has_Pearl(player) and state.can_lift_rocks(player))
set_rule(world.get_entrance('Bumper Cave Ledge Mirror Spot', player), lambda state: state.has_Mirror(player))
# this more like an ohko rule - dependent on bird being present too - so enemizer could turn this off?
set_rule(world.get_entrance('Bumper Cave Ledge Drop', player), lambda state: state.has_Pearl(player) and
(state.has('Cape', player) or state.has('Cane of Byrna', player) or state.has_sword(player)))
set_rule(world.get_entrance('Bat Cave Drop Ledge Mirror Spot', player), lambda state: state.has_Mirror(player))
set_rule(world.get_entrance('Dark World Hammer Peg Cave', player), lambda state: state.has_Pearl(player) and state.has('Hammer', player))
set_rule(world.get_entrance('Village of Outcasts Eastern Rocks', player), lambda state: state.has_Pearl(player) and state.can_lift_heavy_rocks(player))
set_rule(world.get_entrance('Peg Area Rocks', player), lambda state: state.has_Pearl(player) and state.can_lift_heavy_rocks(player))
set_rule(world.get_entrance('Village of Outcasts Pegs', player), lambda state: state.has_Pearl(player) and state.has('Hammer', player))
set_rule(world.get_entrance('Grassy Lawn Pegs', player), lambda state: state.has_Pearl(player) and state.has('Hammer', player))
set_rule(world.get_entrance('Bumper Cave Exit (Top)', player), lambda state: state.has('Cape', player))
set_rule(world.get_entrance('Bumper Cave Exit (Bottom)', player), lambda state: state.has('Cape', player) or state.has('Hookshot', player))
set_rule(world.get_entrance('Bumper Cave Bottom to Top', player), lambda state: state.has('Cape', player))
set_rule(world.get_entrance('Bumper Cave Top To Bottom', player), lambda state: state.has('Cape', player) or state.has('Hookshot', player))
set_rule(world.get_entrance('Skull Woods Final Section', player), lambda state: state.has('Fire Rod', player) and state.has_Pearl(player)) # bunny cannot use fire rod
set_rule(world.get_entrance('Misery Mire', player), lambda state: state.has_Pearl(player) and state.has_sword(player) and state.has_misery_mire_medallion(player)) # sword required to cast magic (!)
@@ -1233,7 +1259,7 @@ std_kill_rooms = {
'Hyrule Dungeon Armory Main': ['Hyrule Dungeon Armory S', 'Hyrule Dungeon Armory ES'], # One green guard
'Hyrule Dungeon Armory Boomerang': ['Hyrule Dungeon Armory Boomerang WS'], # One blue guard
'Eastern Stalfos Spawn': ['Eastern Stalfos Spawn ES', 'Eastern Stalfos Spawn NW'], # Can use pots
'Desert Compass Room': ['Desert Compass NW'], # Three popos
'Desert Compass Room': ['Desert Compass NE'], # Three popos
'Desert Four Statues': ['Desert Four Statues NW', 'Desert Four Statues ES'], # Four popos
'Hera Beetles': ['Hera Beetles WS'], # Three blue beetles and only two pots, and bombs don't work.
'Tower Gold Knights': ['Tower Gold Knights SW', 'Tower Gold Knights EN'], # Two ball and chain
@@ -1284,7 +1310,7 @@ def standard_rules(world, player):
# zelda should be saved before agahnim is in play
add_rule(world.get_location('Agahnim 1', player), lambda state: state.has('Zelda Delivered', player))
# too restrictive for crossed?
# uncle can't have keys generally because unplaced items aren't used here
def uncle_item_rule(item):
copy_state = CollectionState(world)
copy_state.collect(item)
@@ -1803,8 +1829,8 @@ def set_bunny_rules(world, player, inverted):
# regions for the exits of multi-entrace caves/drops that bunny cannot pass
# Note spiral cave may be technically passible, but it would be too absurd to require since OHKO mode is a thing.
bunny_impassable_caves = ['Bumper Cave', 'Two Brothers House', 'Hookshot Cave (Middle)',
'Pyramid', 'Spiral Cave (Top)', 'Fairy Ascension Cave (Drop)']
bunny_impassable_caves = ['Bumper Cave (top)', 'Bumper Cave (bottom)', 'Two Brothers House',
'Hookshot Cave (Middle)', 'Pyramid', 'Spiral Cave (Top)', 'Fairy Ascension Cave (Drop)']
bunny_accessible_locations = ['Link\'s Uncle', 'Sahasrahla', 'Sick Kid', 'Lost Woods Hideout', 'Lumberjack Tree',
'Checkerboard Cave', 'Potion Shop', 'Spectacle Rock Cave', 'Pyramid',
'Hype Cave - Generous Guy', 'Peg Cave', 'Bumper Cave Ledge', 'Dark Blacksmith Ruins',
@@ -2008,7 +2034,7 @@ bunny_impassible_doors = {
'Eastern Map Balcony Hook Path', 'Eastern Stalfos Spawn ES', 'Eastern Stalfos Spawn NW',
'Eastern Darkness S', 'Eastern Darkness NE', 'Eastern Darkness Up Stairs',
'Eastern Attic Start WS', 'Eastern Single Eyegore NE', 'Eastern Duo Eyegores NE', 'Desert Main Lobby Left Path',
'Desert Main Lobby Right Path', 'Desert Left Alcove Path', 'Desert Right Alcove Path', 'Desert Compass NW',
'Desert Main Lobby Right Path', 'Desert Left Alcove Path', 'Desert Right Alcove Path', 'Desert Compass NE',
'Desert West Lobby NW', 'Desert Back Lobby NW', 'Desert Four Statues NW', 'Desert Four Statues ES',
'Desert Beamos Hall WS', 'Desert Beamos Hall NE', 'Desert Wall Slide NW',
'Hera Lobby to Front Barrier - Blue', 'Hera Front to Lobby Barrier - Blue', 'Hera Front to Down Stairs Barrier - Blue',
@@ -2073,13 +2099,16 @@ bunny_impassible_doors = {
def add_key_logic_rules(world, player):
key_logic = world.key_logic[player]
eval_func = eval_small_key_door
if world.key_logic_algorithm[player] == 'strict' and world.keyshuffle[player] == 'wild':
eval_func = eval_small_key_door_strict
for d_name, d_logic in key_logic.items():
for door_name, rule in d_logic.door_rules.items():
door_entrance = world.get_entrance(door_name, player)
add_rule(door_entrance, eval_small_key_door(door_name, d_name, player))
add_rule(door_entrance, eval_func(door_name, d_name, player))
if door_entrance.door.dependents:
for dep in door_entrance.door.dependents:
add_rule(dep.entrance, eval_small_key_door(door_name, d_name, player))
add_rule(dep.entrance, eval_func(door_name, d_name, player))
for location in d_logic.bk_restricted:
if not location.forced_item:
forbid_item(location, d_logic.bk_name, player)
@@ -2090,7 +2119,8 @@ def add_key_logic_rules(world, player):
for chest in d_logic.bk_chests:
big_chest = world.get_location(chest.name, player)
add_rule(big_chest, create_rule(d_logic.bk_name, player))
if len(d_logic.bk_doors) == 0 and len(d_logic.bk_chests) <= 1:
if (len(d_logic.bk_doors) == 0 and len(d_logic.bk_chests) <= 1
and world.accessibility[player] != 'locations'):
set_always_allow(big_chest, allow_big_key_in_big_chest(d_logic.bk_name, player))
if world.keyshuffle[player] == 'universal':
for d_name, layout in world.key_layout[player].items():
@@ -2103,6 +2133,8 @@ def eval_small_key_door_main(state, door_name, dungeon, player):
if state.is_door_open(door_name, player):
return True
key_logic = state.world.key_logic[player][dungeon]
if door_name not in key_logic.door_rules:
return False
door_rule = key_logic.door_rules[door_name]
door_openable = False
for ruleType, number in door_rule.new_rules.items():
@@ -2111,9 +2143,9 @@ def eval_small_key_door_main(state, door_name, dungeon, player):
if ruleType == KeyRuleType.WorstCase:
door_openable |= state.has_sm_key(key_logic.small_key_name, player, number)
elif ruleType == KeyRuleType.AllowSmall:
if (door_rule.small_location.item and door_rule.small_location.item.name == key_logic.small_key_name
and door_rule.small_location.item.player == player):
return True # always okay if allow small is on
small_loc_item = door_rule.small_location.item
if small_loc_item and small_loc_item.name == key_logic.small_key_name and small_loc_item.player == player:
door_openable |= state.has_sm_key(key_logic.small_key_name, player, number)
elif isinstance(ruleType, tuple):
lock, lock_item = ruleType
# this doesn't track logical locks yet, i.e. hammer locks the item and hammer is there, but the item isn't
@@ -2125,10 +2157,24 @@ def eval_small_key_door_main(state, door_name, dungeon, player):
return door_openable
def eval_small_key_door_strict_main(state, door_name, dungeon, player):
if state.is_door_open(door_name, player):
return True
key_layout = state.world.key_layout[player][dungeon]
number = key_layout.max_chests
if number <= 0:
return True
return state.has_sm_key_strict(key_layout.key_logic.small_key_name, player, number)
def eval_small_key_door(door_name, dungeon, player):
return lambda state: eval_small_key_door_main(state, door_name, dungeon, player)
def eval_small_key_door_strict(door_name, dungeon, player):
return lambda state: eval_small_key_door_strict_main(state, door_name, dungeon, player)
def allow_big_key_in_big_chest(bk_name, player):
return lambda state, item: item.name == bk_name and item.player == player
+1 -2
View File
@@ -91,7 +91,6 @@ Triforce_texts = [
'Who stole the fourth triangle?',
'Trifource?\nMore Like Tritrice, am I right?'
'\n Well Done!',
'You just wasted 2 hours of your life.',
'This was meant to be a trapezoid',
# these ones are from web randomizer
"\n G G",
@@ -1716,7 +1715,7 @@ class TextTable(object):
text['game_shooting_choice'] = CompressedTextMapper.convert("20 rupees.\n5 arrows.\nWin rupees!\nWant to play?\n ≥ Yes\n No\n{CHOICE}")
text['game_shooting_yes'] = CompressedTextMapper.convert("Let's do this!")
text['game_shooting_no'] = CompressedTextMapper.convert("Where are you going? Straight up!")
text['game_shooting_continue'] = CompressedTextMapper.convert("Keep playing?\nyes\n no\n{CHOICE}")
text['game_shooting_continue'] = CompressedTextMapper.convert("Keep playing?\nYes\n No\n{CHOICE}")
text['pond_of_wishing'] = CompressedTextMapper.convert("-Wishing Pond-\n\n On Vacation")
text['pond_item_select'] = CompressedTextMapper.convert("Pick something\nto throw in.\n{ITEMSELECT}")
text['pond_item_test'] = CompressedTextMapper.convert("You toss this?\n ≥ Yup\n Wrong\n{CHOICE}")
+40 -3
View File
@@ -38,7 +38,7 @@ in each player section you can set up default settings for that player or you ca
door_shuffle: basic
```
Player 1's settings will be determined by rolling the mystery weights and player 2's setting will be default except for those two specified in his section. Each settings should be consistent with the CLI arguments.
Player 1's settings will be determined by rolling the mystery weights and player 2's setting will be default except for those two specified in his section. Each setting should be consistent with the CLI arguments. Simple weighted settings are supported here. If you need sub-weights though, use a separate yaml file.
Start inventory is not supported here. It has a separate section.
@@ -71,7 +71,21 @@ You may list each location for a player and the item you wish to place there. A
Example:
`Pegasus Boots#3` means the boots for player 3.
### advanced_placements
This must be defined by player. Each player number should be listed with the appropriate section. Each section is a list of placement rules. Each placement rule has a specific type.
Supported Types: PlacementGroup, NotPlacmentGroup
#### PlacementGroup
You may define an item, and a list of locations. The locations may be weighted if desired. The item will be placed at one of the listed locations - this currently ignores logic. The item will be placed there. The special location 'Random' indicates that the item should be placed randomly, without any other consideration. This may be repeated for placement of multiple items like multiple bows or swords.
#### NotPlacementGroup
You may define an item and a list of locations that an item should not be placed at. This will apply to all items of that type. The logic is considered for this. If it is otherwise impossible, the item will be considered for the listed locations. This is important for small key layouts mostly, but it will try other locations first.
### entrances
@@ -144,7 +158,7 @@ Misery Mire: Ether
Turtle Rock: Quake
```
Leave blank or omit if you wish it to be random.
Leave blank or omit if you wish it to be random. Alternatively, a weighted dictionary is supported and a 'Random' option
### bosses
@@ -173,4 +187,27 @@ To start with multiple copies of progressive items, list them more than once.
This conflicts with the mystery yaml, if specified. These start inventory items will be added after those are added.
### drops
This must be defined by player. You may have prize packs, tree pulls, crab drops, stun prizes, and the fish prize defined using the following keys:
```
drops:
1:
Pack 1
- Small Heart
- Bombs (4)
- Random
- etc
Pack 2: (list)
...
Pack 7: (list)
Tree Pull Tier 1: Single Bomb
Tree Pull Tier 2: Arrows (10)
Tree Pull Tier 3: Fairy
Crab Normal: Rupees (20)
Crab Special: Small Magic
Stun Prize: Bombs (8)
Fish: Big Magic
```
Prize packs expect a list of eight items each (anything not specified will be whatever randomization would have normally occurred). The special drops expect a single item. Packs 1 through 7 are supported. Prize pack 0 is not customizable.
+96
View File
@@ -0,0 +1,96 @@
Hint description:
Hints will appear in the following ratios across the 15 telepathic tiles that have hints and the five storyteller locations:
Hint distribution for crossed entrance / crossed dungeon:
* 4 hints for inconvenient entrances.
* 2 hints for random entrances (this can by coincidence pick inconvenient entrances that aren't used for the first set of hints).
* 1 hints for inconvenient item locations.
* 7 hints for valuable items
* 1 hint for the Thieves Town attic
* 5 district hints
More vanilla shuffles of entrances reduces the number of inconvenient and random entrances hinted and generally follows adjustments in the ER_hint_reference.
Non-crossed door shuffles removes the attic hint and reduces the number of interesting items hinted at.
### Inconvenient entrance list
* Skull Woods Final
* Ice Palace
* Misery Mire
* Turtle Rock
* Ganon's Tower
* Mimic Ledge
* SW DM Foothills Cave (mirror from upper Bumper ledge)
* Hammer Pegs (near purple chest)
* Super Bomb cracked wall
### Inconvenient location list
* Purple Chest
* Spike Cave
* Magic Bat
* Sahasrahla (Green Pendant)
The following are included only if door shuffle is vanilla:
* Swamp left (two chests)
* Mire left (two chests)
* Hera basement
* Eastern Palace Big Key chest (protected by anti-fairies)
* Thieves' Town Big Chest
* Ice Palace Big Chest
* Ganon's Tower Big Chest
In the vanilla, dungeonssimple, and dungeonsfull shuffles, the following two locations are added to the inconvenient locations list:
* Graveyard Cave
* Mimic Cave
### Valuable items
Valuable Items are simply all items that are shown on the pause subscreen (Y, B, or A sections) minus Silver Arrows and plus Triforce Pieces, Magic Upgrades (1/2 or 1/4), and the Single Arrow. If key shuffle is being used, you can additionally get hints for Small Keys or Big Keys but not hints for Maps or Compasses.
### District Hints
These replace all junk/joke hints.
Each dungeon is considered its own district. The overworld is divided into districts as according to this picture:
![District image](Districts.png "OW Districts")
Each district is classified in one of five categories:
* Foolish
* Connector Only
* Dungeon Only
* Useful
* Vital
A vital district contains one or more vital items. This includes:
* Y Items: Bows, Hookshot, Both Rods, Bombos, Lamp, Hammer, Ocarina, Book, Cane of Somaria, Mirror
* A Items: Gloves, Moon Pearl, Boots, Flippers
* All Swords, Triforce Pieces, and the Silver Arrows
* Ether and Quake if used to open an entrance
* Big Keys, Small Keys, Bomb Bags, Arrow Quivers in the appropriate mode (wild big keys, wild small keys, bomb bag, retro + shopsanity)
The hint will name the item if there is only one vital one, or give the number otherwise.
Useful district contain one or more useful items, but no vital items.
* Y Items: Boomerangs, Magic Powder, Mushroom, Shovel, Bug Net, Bottles, Cane of Byrna, Cape
* Ether and Quake if not used to open an entrance
* Shields, Mails, Magic Upgrades
Notably, the fetch items may technically be required depending on what is located at the fetch quest, but are considered useful still.
The hint will name the item if there is only one, or give the number of useful items otherwise.
A "Dungeon Only" district has no vital or useful items but contains one dungeon or more. The hint will pick one of the dungeon and state: "{district} is on the path to {dungeon}"
A "Connector Only" district has no vital or useful items but contains a connector that reaches a potentially inaccessible area like
the Turtle Rock upper ledge or the Maze Race (you have no mirror yet). The hint will state "{district} can reach {region}"
Foolish districts have no dungeons, no potentially useful connector and no useful or vital items. These districts, if they exist, are preferred for hints. The others are chosen randomly.
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

File diff suppressed because it is too large Load Diff
+41
View File
@@ -0,0 +1,41 @@
meta:
algorithm: balanced
players: 1
settings:
1:
mode: inverted
mapshuffle: True
keyshuffle: wild
placements:
1:
Link's Uncle: Progressive Sword
Blacksmith: Progressive Sword
Master Sword Pedestal: Progressive Sword
Pyramid Fairy - Left: Progressive Sword
Desert Palace - Boss: Compass (Desert Palace)
Eastern Palace - Boss: Compass (Eastern Palace)
Tower of Hera - Boss: Compass (Tower of Hera)
Swamp Palace - Boss: Compass (Swamp Palace)
Thieves' Town - Boss: Compass (Thieves Town)
Skull Woods - Boss: Compass (Skull Woods)
Ice Palace - Boss: Compass (Ice Palace)
Misery Mire - Boss: Compass (Misery Mire)
Turtle Rock - Boss: Compass (Turtle Rock)
Palace of Darkness - Boss: Compass (Palace of Darkness)
Eastern Palace - Big Key Chest: Big Key (Eastern Palace)
Desert Palace - Big Key Chest: Big Key (Desert Palace)
Tower of Hera - Big Key Chest: Big Key (Tower of Hera)
Palace of Darkness - Big Key Chest: Big Key (Palace of Darkness)
Thieves' Town - Big Key Chest: Big Key (Thieves Town)
Skull Woods - Big Key Chest: Big Key (Skull Woods)
Swamp Palace - Big Key Chest: Big Key (Swamp Palace)
Ice Palace - Big Key Chest: Big Key (Ice Palace)
Misery Mire - Big Key Chest: Big Key (Misery Mire)
Turtle Rock - Big Key Chest: Big Key (Turtle Rock)
Ganons Tower - Big Key Chest: Big Key (Ganons Tower)
start_inventory:
1:
- Ocarina (Activated)
+73
View File
@@ -0,0 +1,73 @@
meta:
algorithm: balanced
players: 1
settings:
1:
mode: inverted
goal: dungeons
shuffleenemies: shuffled
shufflebosses: full
item_pool:
1:
Progressive Bow: 8
Blue Boomerang: 4
Red Boomerang: 4
Hookshot: 4
Magic Powder: 4
Mushroom: 4
Ice Rod: 4
Fire Rod: 4
Bombos: 4
Ether: 4
Quake: 4
Lamp: 4
Hammer: 4
Ocarina: 4
Shovel: 4
Bug Catching Net: 4
Book of Mudora: 4
Bottle (Random): 16
Cane of Byrna: 4
Cane of Somaria: 4
Cape: 4
Magic Mirror: 4
Flippers: 4
Moon Pearl: 4
Pegasus Boots: 4
Progressive Glove: 8
Progressive Armor: 8
Progressive Shield: 12
Progressive Sword: 16
Magic Upgrade (1/2): 4
start_inventory:
1:
- Boss Heart Container
- Boss Heart Container
- Boss Heart Container
- Pegasus Boots
- Rupees (5)
- Rupees (5)
- Rupees (5)
- Rupee (1)
- Rupee (1)
- Rupee (1)
- Rupee (1)
- Compass (Desert Palace)
- Compass (Eastern Palace)
- Compass (Tower of Hera)
- Compass (Swamp Palace)
- Compass (Thieves Town)
- Compass (Skull Woods)
- Compass (Ice Palace)
- Compass (Misery Mire)
- Compass (Turtle Rock)
- Compass (Palace of Darkness)
- Compass (Ganons Tower)
drops:
1:
Tree Pull Tier 1: Single Bomb
Tree Pull Tier 2: Bombs (4)
Tree Pull Tier 3: Bombs (8)
Crab Normal: Rupees (20)
Crab Special: Big Magic
Stun Prize: Rupees (20)
+443
View File
@@ -0,0 +1,443 @@
meta:
algorithm: balanced
players: 1
settings:
1:
mode:
open: 1
standard: 1
mapshuffle: True
compassshuffle: True
boots_hint:
True: 1
False: 1
advanced_placements:
1:
- item: Green Pendant
locations:
- Eastern Palace - Prize
- Random
type: LocationGroup
- item: Crystal 5
locations:
- Ice Palace - Prize
- Random
type: LocationGroup
- item: Crystal 6
locations:
- Misery Mire - Prize
- Random
type: LocationGroup
- item: Progressive Bow
locations:
- Eastern Palace - Big Chest
- Random
type: LocationGroup
- item: Progressive Bow
locations:
- Pyramid Fairy - Right
- Random
type: LocationGroup
- item: Book of Mudora
locations:
- Library
- Random
type: LocationGroup
- item: Hammer
locations:
- Palace of Darkness - Big Chest
- Random
type: LocationGroup
- item: Hookshot
locations:
- Swamp Palace - Big Chest
- Random
type: LocationGroup
- item: Magic Mirror
locations:
- Old Man
- Random
type: LocationGroup
- item: Ocarina
locations:
- Flute Spot
- Random
type: LocationGroup
- item: Pegasus Boots
locations:
- Sahasrahla
- Random
type: LocationGroup
- item: Cape
locations:
- King's Tomb
- Random
type: LocationGroup
- item: Mushroom
locations:
- Mushroom
- Random
type: LocationGroup
- item: Shovel
locations:
- Stumpy
- Random
type: LocationGroup
- item: Lamp
locations:
Link's House: 1
Secret Passage: 1
Hyrule Castle - Zelda's Chest: 1
Random: 3
type: LocationGroup
- item: Magic Powder
locations:
- Potion Shop
- Random
type: LocationGroup
- item: Moon Pearl
locations:
- Tower of Hera - Big Chest
- Random
type: LocationGroup
- item: Cane of Somaria
locations:
- Misery Mire - Big Chest
- Random
type: LocationGroup
- item: Fire Rod
locations:
- Skull Woods - Big Chest
- Random
type: LocationGroup
- item: Flippers
locations:
- King Zora
- Random
type: LocationGroup
- item: Ice Rod
locations:
- Ice Rod Cave
- Random
type: LocationGroup
- item: Bombos
locations:
- Bombos Tablet
- Random
type: LocationGroup
- item: Ether
locations:
- Ether Tablet
- Random
type: LocationGroup
- item: Quake
locations:
- Catfish
- Random
type: LocationGroup
- item: Bottle
locations:
- Bottle Merchant
- Random
type: LocationGroup
- item: Bottle
locations:
- Kakariko Tavern
- Random
type: LocationGroup
- item: Bottle
locations:
- Purple Chest
- Random
type: LocationGroup
- item: Bottle
locations:
- Hobo
- Random
type: LocationGroup
- item: Progressive Sword
locations:
- Link's Uncle
- Random
type: LocationGroup
- item: Progressive Sword
locations:
- Blacksmith
- Random
type: LocationGroup
- item: Progressive Sword
locations:
- Master Sword Pedestal
- Random
type: LocationGroup
- item: Progressive Sword
locations:
- Pyramid Fairy - Left
- Random
type: LocationGroup
- item: Progressive Glove
locations:
- Desert Palace - Big Chest
- Random
type: LocationGroup
- item: Progressive Glove
locations:
- Thieves' Town - Big Chest
- Random
type: LocationGroup
- item: Progressive Armor
locations:
- Ice Palace - Big Chest
- Random
type: LocationGroup
- item: Progressive Armor
locations:
- Ganons Tower - Big Chest
- Random
type: LocationGroup
- item: Blue Boomerang
locations:
- Hyrule Castle - Boomerang Chest
- Random
type: LocationGroup
- item: Red Boomerang
locations:
- Waterfall Fairy - Left
- Random
type: LocationGroup
- item: Progressive Shield
locations:
- Secret Passage
- Random
type: LocationGroup
- item: Progressive Shield
locations:
- Waterfall Fairy - Right
- Random
type: LocationGroup
- item: Progressive Shield
locations:
- Turtle Rock - Big Chest
- Random
type: LocationGroup
- item: Bug Catching Net
locations:
- Sick Kid
- Random
type: LocationGroup
- item: Cane of Byrna
locations:
- Spike Cave
- Random
type: LocationGroup
- item: Boss Heart Container
locations:
- Desert Palace - Boss
- Random
type: LocationGroup
- item: Boss Heart Container
locations:
- Eastern Palace - Boss
- Random
type: LocationGroup
- item: Boss Heart Container
locations:
- Tower of Hera - Boss
- Random
type: LocationGroup
- item: Boss Heart Container
locations:
- Swamp Palace - Boss
- Random
type: LocationGroup
- item: Boss Heart Container
locations:
- Thieves' Town - Boss
- Random
type: LocationGroup
- item: Boss Heart Container
locations:
- Skull Woods - Boss
- Random
type: LocationGroup
- item: Boss Heart Container
locations:
- Ice Palace - Boss
- Random
type: LocationGroup
- item: Boss Heart Container
locations:
- Misery Mire - Boss
- Random
type: LocationGroup
- item: Boss Heart Container
locations:
- Turtle Rock - Boss
- Random
type: LocationGroup
- item: Boss Heart Container
locations:
- Palace of Darkness - Boss
- Random
type: LocationGroup
- item: Sanctuary Heart Container
locations:
- Sanctuary
- Random
type: LocationGroup
- item: Rupees (300)
locations:
- Mini Moldorm Cave - Generous Guy
- Random
type: LocationGroup
- item: Rupees (300)
locations:
- Sewers - Secret Room - Middle
- Random
type: LocationGroup
- item: Rupees (300)
locations:
- Hype Cave - Generous Guy
- Random
type: LocationGroup
- item: Rupees (300)
locations:
- Brewery
- Random
type: LocationGroup
- item: Rupees (300)
locations:
- C-Shaped House
- Random
type: LocationGroup
- item: Magic Upgrade (1/2)
locations:
- Magic Bat
- Random
type: LocationGroup
- item: Compass (Eastern Palace)
locations:
- Eastern Palace - Compass Chest
- Random
type: LocationGroup
- item: Map (Eastern Palace)
locations:
- Eastern Palace - Map Chest
- Random
type: LocationGroup
- item: Compass (Desert Palace)
locations:
- Desert Palace - Compass Chest
- Random
type: LocationGroup
- item: Map (Desert Palace)
locations:
- Desert Palace - Map Chest
- Random
type: LocationGroup
- item: Compass (Tower of Hera)
locations:
- Tower of Hera - Compass Chest
- Random
type: LocationGroup
- item: Map (Tower of Hera)
locations:
- Tower of Hera - Map Chest
- Random
type: LocationGroup
- item: Map (Escape)
locations:
- Hyrule Castle - Map Chest
- Random
type: LocationGroup
- item: Compass (Palace of Darkness)
locations:
- Palace of Darkness - Compass Chest
- Random
type: LocationGroup
- item: Map (Palace of Darkness)
locations:
- Palace of Darkness - Map Chest
- Random
type: LocationGroup
- item: Compass (Thieves Town)
locations:
- Thieves' Town - Compass Chest
- Random
type: LocationGroup
- item: Map (Thieves Town)
locations:
- Thieves' Town - Map Chest
- Random
type: LocationGroup
- item: Compass (Skull Woods)
locations:
- Skull Woods - Compass Chest
- Random
type: LocationGroup
- item: Map (Skull Woods)
locations:
- Skull Woods - Map Chest
- Random
type: LocationGroup
- item: Compass (Swamp Palace)
locations:
- Swamp Palace - Compass Chest
- Random
type: LocationGroup
- item: Map (Swamp Palace)
locations:
- Swamp Palace - Map Chest
- Random
type: LocationGroup
- item: Compass (Ice Palace)
locations:
- Ice Palace - Compass Chest
- Random
type: LocationGroup
- item: Map (Ice Palace)
locations:
- Ice Palace - Map Chest
- Random
type: LocationGroup
- item: Compass (Misery Mire)
locations:
- Misery Mire - Compass Chest
- Random
type: LocationGroup
- item: Map (Misery Mire)
locations:
- Misery Mire - Map Chest
- Random
type: LocationGroup
- item: Compass (Turtle Rock)
locations:
- Turtle Rock - Compass Chest
- Random
type: LocationGroup
- item: Map (Turtle Rock)
locations:
- Turtle Rock - Roller Room - Left
- Random
type: LocationGroup
- item: Compass (Ganons Tower)
locations:
- Ganons Tower - Compass Room - Top Left
- Random
type: LocationGroup
- item: Map (Ganons Tower)
locations:
- Ganons Tower - Map Chest
- Random
type: LocationGroup
medallions:
1:
Misery Mire:
Ether: 1
Random: 1
Turtle Rock:
Quake: 1
Random: 1
+21
View File
@@ -0,0 +1,21 @@
meta:
algorithm: balanced
players: 1
settings:
1:
goal: triforcehunt
shuffle: crossed
keysanity: True
shopsanity: True
keydropshuffle: True
pottery: lottery
dungeon_counters: 'on'
triforce_goal: 420
triforce_pool: 420
start_inventory:
1:
- Pegasus Boots
- Lamp
- Rupees (300)
- Rupees (100)
- Rupees (20)
+21
View File
@@ -0,0 +1,21 @@
meta:
algorithm: balanced
players: 1
settings:
1:
goal: triforcehunt
shuffle: crossed
keysanity: True
shopsanity: True
keydropshuffle: True
pottery: lottery
dungeon_counters: 'on'
triforce_goal: 213
triforce_pool: 420
start_inventory:
1:
- Pegasus Boots
- Lamp
- Rupees (300)
- Rupees (100)
- Rupees (20)
+259
View File
@@ -0,0 +1,259 @@
meta:
algorithm: balanced
players: 1
settings:
1:
bigkeyshuffle: True
placements:
1:
Potion Shop: Shovel
Flute Spot: Magic Powder
advanced_placements:
1:
- item: Progressive Bow
locations:
- Palace of Darkness - Boss
- Palace of Darkness - Map Chest
- Palace of Darkness - The Arena - Ledge
- Eastern Palace - Boss
- Ganons Tower - Pre-Moldorm Chest
- Ganons Tower - Mini Helmasaur Room - Left
- Ganons Tower - Mini Helmasaur Room - Right
- Ganons Tower - Validation Chest
type: LocationGroup
- item: Fire Rod
locations:
- Ice Palace - Big Chest
- Ice Palace - Boss
- Ice Palace - Big Key Chest
- Ice Palace - Compass Chest
- Ice Palace - Map Chest
- Ice Palace - Spike Room
- Ice Palace - Iced T Room
- Ice Palace - Freezor Chest
- Desert Palace - Boss
- Tower of Hera - Big Key Chest
- Misery Mire - Big Key Chest
- Misery Mire - Compass Chest
type: LocationGroup
- item: Pegasus Boots
locations:
- Hookshot Cave - Top Right
- Hookshot Cave - Top Left
- Hookshot Cave - Bottom Right
- Hookshot Cave - Bottom Left
- Mushroom Spot
- Zora Ledge
- Bombos Tablet
- Ether Tablet
- Floating Island
- Lake Hylia Island
- Maze Race
- Spectacle Rock
- Spectacle Rock Cave
- Pyramid
- Desert Ledge
- Bumper Cave Ledge
type: LocationGroup
- item: Progressive Sword
locations:
- Swamp Palace - Big Chest
- Swamp Palace - Boss
- Swamp Palace - Big Key Chest
- Swamp Palace - Compass Chest
- Swamp Palace - Map Chest
- Swamp Palace - Entrance
- Swamp Palace - Flooded Room - Left
- Swamp Palace - Flooded Room - Right
- Swamp Palace - Waterfall Room
- Swamp Palace - West Chest
type: LocationGroup
- item: Titans Mitts
locations:
- Swamp Palace - Big Chest
- Swamp Palace - Boss
- Swamp Palace - Big Key Chest
- Swamp Palace - Compass Chest
- Swamp Palace - Map Chest
- Swamp Palace - Entrance
- Swamp Palace - Flooded Room - Left
- Swamp Palace - Flooded Room - Right
- Swamp Palace - Waterfall Room
- Swamp Palace - West Chest
type: LocationGroup
- item: Ice Rod
type: LocationGroup
locations:
- Turtle Rock - Big Chest
- Turtle Rock - Boss
- Turtle Rock - Big Key Chest
- Turtle Rock - Compass Chest
- Turtle Rock - Roller Room - Left
- Turtle Rock - Chain Chomps
- Turtle Rock - Crystaroller Room
- Turtle Rock - Roller Room - Right
- Turtle Rock - Eye Bridge - Bottom Left
- Turtle Rock - Eye Bridge - Bottom Right
- Turtle Rock - Eye Bridge - Top Left
- Turtle Rock - Eye Bridge - Top Right
- Mimic Cave
- Link's House
- Link's Uncle
- Secret Passage
- Hyrule Castle - Map Chest
- Sanctuary
- Mushroom Spot
- Lost Woods Hideout
- Blind's Hideout - Left
- Blind's Hideout - Right
- Blind's Hideout - Far Left
- Blind's Hideout - Far Right
- Blind's Hideout - Top
- Kakariko Well - Right
- Kakariko Well - Left
- Kakariko Well - Middle
- Kakariko Well - Top
- Kakariko Well - Bottom
- Chicken House
- Bottle Merchant
- Kakariko Tavern
- Maze Race
- Floodgate Chest
- Sunken Treasure
- Mini Moldorm Cave - Left
- Mini Moldorm Cave - Right
- Mini Moldorm Cave - Generous Guy
- Mini Moldorm Cave - Far Left
- Mini Moldorm Cave - Far Right
- Sahasrahla's Hut - Middle
- Sahasrahla's Hut - Right
- Sahasrahla's Hut - Left
- Ice Rod Cave
- Eastern Palace - Compass Chest
- Eastern Palace - Map Chest
- Eastern Palace - Cannonball Chest
- Aginah's Cave
- item: Big Key (Eastern Palace)
locations:
- Eastern Palace - Cannonball Chest
- Eastern Palace - Big Key Chest
- Eastern Palace - Compass Chest
- Eastern Palace - Map Chest
type: LocationGroup
- item: Big Key (Desert Palace)
locations:
- Desert Palace - Big Key Chest
- Desert Palace - Compass Chest
- Desert Palace - Map Chest
- Desert Palace - Torch
type: LocationGroup
- item: Big Key (Palace of Darkness)
locations:
- Palace of Darkness - Big Key Chest
- Palace of Darkness - Compass Chest
- Palace of Darkness - Map Chest
- Palace of Darkness - Stalfos Basement
- Palace of Darkness - Dark Basement - Right
- Palace of Darkness - Harmless Hellway
- Palace of Darkness - Shooter Room
- Palace of Darkness - The Arena - Bridge
- Palace of Darkness - The Arena - Ledge
- Palace of Darkness - Dark Basement - Left
- Palace of Darkness - Dark Maze - Bottom
- Palace of Darkness - Dark Maze - Top
type: LocationGroup
- item: Big Key (Thieves Town)
locations:
- Thieves' Town - Big Key Chest
- Thieves' Town - Compass Chest
- Thieves' Town - Map Chest
- Thieves' Town - Ambush Chest
type: LocationGroup
- item: Big Key (Misery Mire)
locations:
- Misery Mire - Big Key Chest
- Misery Mire - Compass Chest
- Misery Mire - Map Chest
- Misery Mire - Spike Chest
- Misery Mire - Main Lobby
- Misery Mire - Bridge Chest
type: LocationGroup
- item: Big Key (Turtle Rock)
locations:
- Turtle Rock - Big Key Chest
- Turtle Rock - Compass Chest
- Turtle Rock - Roller Room - Left
- Turtle Rock - Chain Chomps
- Turtle Rock - Roller Room - Right
type: LocationGroup
- item: Big Key (Ganons Tower)
locations:
- Ganons Tower - Big Key Chest
- Ganons Tower - Compass Room - Top Left
- Ganons Tower - Map Chest
- Ganons Tower - Bob's Torch
- Ganons Tower - Tile Room
- Ganons Tower - Firesnake Room
- Ganons Tower - DMs Room - Top Right
- Ganons Tower - DMs Room - Top Left
- Ganons Tower - DMs Room - Bottom Left
- Ganons Tower - DMs Room - Bottom Right
- Ganons Tower - Compass Room - Top Right
- Ganons Tower - Compass Room - Bottom Right
- Ganons Tower - Compass Room - Bottom Left
- Ganons Tower - Hope Room - Left
- Ganons Tower - Hope Room - Right
- Ganons Tower - Randomizer Room - Top Left
- Ganons Tower - Randomizer Room - Top Right
- Ganons Tower - Randomizer Room - Bottom Right
- Ganons Tower - Randomizer Room - Bottom Left
- Ganons Tower - Bob's Chest
- Ganons Tower - Big Key Room - Left
- Ganons Tower - Big Key Room - Right
type: LocationGroup
item_pool:
1:
Arrows (10): 12
Blue Boomerang: 1
Bombos: 1
Bombs (10): 1
Bombs (3): 16
Book of Mudora: 1
Boss Heart Container: 10
Bottle (Random): 4
Bug Catching Net: 1
Cane of Byrna: 1
Cane of Somaria: 1
Cape: 1
Ether: 1
Fire Rod: 1
Flippers: 1
Hammer: 1
Hookshot: 1
Ice Rod: 1
Lamp: 1
Magic Mirror: 1
Magic Powder: 1
Magic Upgrade (1/2): 1
Moon Pearl: 1
Mushroom: 1
Ocarina: 1
Pegasus Boots: 1
Piece of Heart: 24
Progressive Armor: 2
Progressive Bow: 2
Progressive Glove: 2
Progressive Shield: 3
Progressive Sword: 4
Titans Mitts: 1
Quake: 1
Red Boomerang: 1
Rupee (1): 2
Rupees (100): 1
Rupees (20): 27
Rupees (300): 5
Rupees (5): 4
Rupees (50): 7
Sanctuary Heart Container: 1
Shovel: 1
Single Arrow: 1
@@ -0,0 +1,24 @@
meta:
players: 1
race: true
settings:
1:
shopsanity: true
pseudoboots: true
goal: crystals
crystals_gt: random
bombbag: true
shuffle: crossed
shufflelinks: true
keysanity: true
door_shuffle: crossed
intensity: 3
door_type_mode: big
pottery: keys
dropshuffle: true
experimental: true
dungeon_counters: 'on'
hints: true
msu_resume: true
collection_rate: true
quickswap: true
@@ -0,0 +1,23 @@
meta:
players: 1
race: true
settings:
1:
shopsanity: true
pseudoboots: true
goal: crystals
crystals_gt: random
shuffle: crossed
shufflelinks: true
keysanity: true
door_shuffle: crossed
intensity: 3
door_type_mode: big
pottery: keys
dropshuffle: true
experimental: true
dungeon_counters: 'on'
hints: true
msu_resume: true
collection_rate: true
quickswap: true
@@ -0,0 +1,23 @@
meta:
players: 1
race: true
settings:
1:
shopsanity: true
pseudoboots: true
goal: crystals
crystals_gt: random
shuffle: crossed
shufflelinks: true
keysanity: true
door_shuffle: crossed
intensity: 3
door_type_mode: big
pottery: lottery
dropshuffle: true
experimental: true
dungeon_counters: 'on'
hints: true
msu_resume: true
collection_rate: true
quickswap: true
@@ -0,0 +1,25 @@
meta:
players: 1
race: true
settings:
1:
shopsanity: true
pseudoboots: true
mode: standard
swords: assured
goal: crystals
crystals_gt: random
shuffle: crossed
shufflelinks: true
keysanity: true
door_shuffle: crossed
intensity: 3
door_type_mode: big
pottery: keys
dropshuffle: true
experimental: true
dungeon_counters: 'on'
hints: true
msu_resume: true
collection_rate: true
quickswap: true
+11 -1
View File
@@ -22,6 +22,14 @@
big: 2
all: 1
chaos: 1
trap_door_mode:
vanilla: 1
boss: 0
oneway: 0
key_logic_algorithm:
default: 1
partial: 0
strict: 0
decoupledoors: off
dropshuffle:
none: 4
@@ -83,6 +91,8 @@
pedestal: 2
triforce-hunt: 2
trinity: 2
ganonhunt: 2
completionist: 1
triforce_goal_min: 10
triforce_goal_max: 30
triforce_pool_min: 20
@@ -96,7 +106,7 @@
mcu: 1 # map, compass, universal smalls
# for use when you aren't using the dungeon_items above
# smallkey_shuffle:
# standard: 5
# none: 5
# wild: 1
# universal: 1
dungeon_counters:
+19 -1
View File
@@ -7,12 +7,30 @@ algorithm:
district: 1
door_shuffle:
vanilla: 1
basic: 2
basic: 1
partitioned: 1
crossed: 3 # crossed yield more errors so is preferred
intensity:
1: 1
2: 1
3: 2 # intensity 3 usually yield more errors
door_type_mode:
original: 2
big: 2
all: 1
chaos: 1
trap_door_mode:
vanilla: 3 # more errors
optional: 1
boss: 1
oneway: 1
key_logic_algorithm:
default: 1
partial: 0
strict: 0
decoupledoors:
off: 9 # more strict
on: 1
dropshuffle:
none: 10 # fewer locations
keys: 1
+22 -1
View File
@@ -34,6 +34,10 @@
"retro"
]
},
"boots_hint": {
"action": "store_true",
"type": "bool"
},
"swords": {
"choices": [
"random",
@@ -63,7 +67,9 @@
"dungeons",
"triforcehunt",
"trinity",
"crystals"
"crystals",
"ganonhunt",
"completionist"
]
},
"difficulty": {
@@ -190,6 +196,21 @@
"chaos"
]
},
"trap_door_mode": {
"choices": [
"vanilla",
"optional",
"boss",
"oneway"
]
},
"key_logic_algorithm": {
"choices": [
"default",
"partial",
"strict"
]
},
"decoupledoors": {
"action": "store_true",
"type": "bool"
+17 -1
View File
@@ -107,7 +107,10 @@
"Triforce Hunt: Places 30 Triforce Pieces in the world, collect",
" 20 of them to beat the game.",
"Trinity: Can beat the game by defeating Ganon, pulling",
" Pedestal, or delivering Triforce Pieces."
" Pedestal, or delivering Triforce Pieces.",
"Ganon Hunt: Places 30 Triforce Pieces in the world, collect",
" 20 of them then defeat Ganon.",
"Completionist: Find everything then defeat Ganon."
],
"difficulty": [
"Select game difficulty. Affects available itempool. (default: %(default)s)",
@@ -236,6 +239,19 @@
"all: Adds traps doors (and any future supported door types)",
"chaos: Increases the number of door types in all dungeon pools"
],
"trap_door_mode" : [
"Trap Door Removal (default: %(default)s)",
"vanilla: No trap door removal",
"optional: Trap doors removed if blocking",
"boss: Also remove boss traps",
"oneway: Remove all annoying trap doors"
],
"key_logic_algorithm": [
"Key Logic Algorithm (default: %(default)s)",
"default: Balance between safety and randomization",
"partial: Partial protection when using certain minor glitches",
"strict: Ensure small keys are available"
],
"decoupledoors" : [ "Door entrances and exits are decoupled" ],
"experimental": [ "Enable experimental features. (default: %(default)s)" ],
"dungeon_counters": [ "Enable dungeon chest counters. (default: %(default)s)" ],
+14
View File
@@ -94,6 +94,17 @@
"randomizer.dungeon.door_type_mode.all": "Adds Trap Doors",
"randomizer.dungeon.door_type_mode.chaos": "Increases all door types",
"randomizer.dungeon.trap_door_mode": "Trap Door Removal",
"randomizer.dungeon.trap_door_mode.vanilla": "No Removal",
"randomizer.dungeon.trap_door_mode.optional": "Removed If Blocking Path",
"randomizer.dungeon.trap_door_mode.boss": "Also Remove Boss Traps",
"randomizer.dungeon.trap_door_mode.oneway": "Remove All Annoying Traps",
"randomizer.dungeon.key_logic_algorithm": "Key Logic Algorithm",
"randomizer.dungeon.key_logic_algorithm.default": "Default",
"randomizer.dungeon.key_logic_algorithm.partial": "Partial Protection",
"randomizer.dungeon.key_logic_algorithm.strict": "Strict",
"randomizer.dungeon.experimental": "Enable Experimental Features",
"randomizer.dungeon.dungeon_counters": "Dungeon Chest Counters",
@@ -216,6 +227,7 @@
"randomizer.generation.print_custom_yaml": "Print Customizer File",
"randomizer.generation.usestartinventory": "Use Starting Inventory",
"randomizer.generation.usecustompool": "Use Custom Item Pool",
"randomizer.generation.race": "Generate \"Race\" ROM",
"randomizer.generation.saveonexit": "Save Settings on Exit",
"randomizer.generation.saveonexit.ask": "Ask Me",
@@ -251,6 +263,8 @@
"randomizer.item.goal.triforcehunt": "Triforce Hunt",
"randomizer.item.goal.trinity": "Trinity",
"randomizer.item.goal.crystals": "Crystals",
"randomizer.item.goal.ganonhunt": "Triforce Hunt + Ganon",
"randomizer.item.goal.completionist": "Completionist",
"randomizer.item.crystals_gt": "Crystals to open GT",
"randomizer.item.crystals_gt.0": "0",
@@ -1,6 +1,6 @@
{
"widgets": {
"keyshuffle": {
"smallkeyshuffle": {
"type": "selectbox",
"options": [
"none",
@@ -33,7 +33,7 @@
},
"door_type_mode": {
"type": "selectbox",
"default": "basic",
"default": "original",
"options": [
"original",
"big",
@@ -44,6 +44,28 @@
"width": 45
}
},
"trap_door_mode": {
"type": "selectbox",
"default": "optional",
"options": [
"vanilla",
"optional",
"boss",
"oneway"
],
"config": {
"width": 30
}
},
"key_logic_algorithm": {
"type": "selectbox",
"default": "default",
"options": [
"default",
"partial",
"strict"
]
},
"decoupledoors": { "type": "checkbox" },
"keydropshuffle": { "type": "checkbox" },
"pottery": {
@@ -6,6 +6,7 @@
"calcplaythrough": { "type": "checkbox" },
"print_custom_yaml": { "type": "checkbox" },
"usestartinventory": { "type": "checkbox" },
"usecustompool": { "type": "checkbox" }
"usecustompool": { "type": "checkbox" },
"race": { "type": "checkbox" }
}
}
@@ -36,7 +36,9 @@
"dungeons",
"triforcehunt",
"trinity",
"crystals"
"crystals",
"ganonhunt",
"completionist"
]
},
"crystals_gt": {
+1 -1
View File
@@ -6,7 +6,7 @@ import subprocess # do stuff at the shell level
env = common.prepare_env()
pip_requirements = os.path.join("..","resources","app","meta","manifests","pip_requirements.txt")
pip_requirements = os.path.join(".","resources","app","meta","manifests","pip_requirements.txt")
if not os.path.isfile(pip_requirements):
pip_requirements = os.path.join("..","..","..","resources","app","meta","manifests","pip_requirements.txt")
+49 -14
View File
@@ -24,15 +24,20 @@ class CustomSettings(object):
head, filename = os.path.split(file)
self.relative_dir = head
def determine_seed(self):
if 'meta' not in self.file_source:
return None
meta = defaultdict(lambda: None, self.file_source['meta'])
seed = meta['seed']
if seed:
random.seed(seed)
return seed
return None
def determine_seed(self, default_seed):
if 'meta' in self.file_source:
meta = defaultdict(lambda: None, self.file_source['meta'])
seed = meta['seed']
if seed:
random.seed(seed)
return seed
if default_seed is None:
random.seed(None)
seed = random.randint(0, 999999999)
else:
seed = default_seed
random.seed(seed)
return seed
def determine_players(self):
if 'meta' not in self.file_source:
@@ -43,7 +48,10 @@ class CustomSettings(object):
def adjust_args(self, args):
def get_setting(value, default):
if value:
return value
if isinstance(value, dict):
return random.choices(list(value.keys()), list(value.values()), k=1)[0]
else:
return value
return default
if 'meta' in self.file_source:
meta = defaultdict(lambda: None, self.file_source['meta'])
@@ -53,6 +61,7 @@ class CustomSettings(object):
args.bps = get_setting(meta['bps'], args.bps)
args.suppress_rom = get_setting(meta['suppress_rom'], args.suppress_rom)
args.names = get_setting(meta['names'], args.names)
args.race = get_setting(meta['race'], args.race)
self.player_range = range(1, args.multi + 1)
if 'settings' in self.file_source:
for p in self.player_range:
@@ -67,6 +76,7 @@ class CustomSettings(object):
args.door_shuffle[p] = get_setting(settings['door_shuffle'], args.door_shuffle[p])
args.logic[p] = get_setting(settings['logic'], args.logic[p])
args.mode[p] = get_setting(settings['mode'], args.mode[p])
args.boots_hint[p] = get_setting(settings['boots_hint'], args.boots_hint[p])
args.swords[p] = get_setting(settings['swords'], args.swords[p])
args.flute_mode[p] = get_setting(settings['flute_mode'], args.flute_mode[p])
args.bow_mode[p] = get_setting(settings['bow_mode'], args.bow_mode[p])
@@ -87,11 +97,21 @@ class CustomSettings(object):
if args.pottery[p] == 'none':
args.pottery[p] = 'keys'
if args.retro[p] or args.mode[p] == 'retro':
if args.bow_mode[p] == 'progressive':
args.bow_mode[p] = 'retro'
elif args.bow_mode[p] == 'silvers':
args.bow_mode[p] = 'retro_silvers'
args.take_any[p] = 'random' if args.take_any[p] == 'none' else args.take_any[p]
args.keyshuffle[p] = 'universal'
args.mixed_travel[p] = get_setting(settings['mixed_travel'], args.mixed_travel[p])
args.standardize_palettes[p] = get_setting(settings['standardize_palettes'],
args.standardize_palettes[p])
args.intensity[p] = get_setting(settings['intensity'], args.intensity[p])
args.door_type_mode[p] = get_setting(settings['door_type_mode'], args.door_type_mode[p])
args.trap_door_mode[p] = get_setting(settings['trap_door_mode'], args.trap_door_mode[p])
args.key_logic_algorithm[p] = get_setting(settings['key_logic_algorithm'], args.key_logic_algorithm[p])
args.decoupledoors[p] = get_setting(settings['decoupledoors'], args.decoupledoors[p])
args.dungeon_counters[p] = get_setting(settings['dungeon_counters'], args.dungeon_counters[p])
args.crystals_gt[p] = get_setting(settings['crystals_gt'], args.crystals_gt[p])
@@ -105,12 +125,13 @@ class CustomSettings(object):
if get_setting(settings['keysanity'], args.keysanity):
args.bigkeyshuffle[p] = True
args.keyshuffle[p] = True
if args.keyshuffle[p] == 'none':
args.keyshuffle[p] = 'wild'
args.mapshuffle[p] = True
args.compassshuffle[p] = True
args.shufflebosses[p] = get_setting(settings['shufflebosses'], args.shufflebosses[p])
args.shuffleenemies[p] = get_setting(settings['shuffleenemies'], args.shuffleenemies[p])
args.shufflebosses[p] = get_setting(settings['boss_shuffle'], args.shufflebosses[p])
args.shuffleenemies[p] = get_setting(settings['enemy_shuffle'], args.shuffleenemies[p])
args.enemy_health[p] = get_setting(settings['enemy_health'], args.enemy_health[p])
args.enemy_damage[p] = get_setting(settings['enemy_damage'], args.enemy_damage[p])
args.shufflepots[p] = get_setting(settings['shufflepots'], args.shufflepots[p])
@@ -139,6 +160,7 @@ class CustomSettings(object):
args.ow_palettes[p] = get_setting(settings['ow_palettes'], args.ow_palettes[p])
args.uw_palettes[p] = get_setting(settings['uw_palettes'], args.uw_palettes[p])
args.shuffle_sfx[p] = get_setting(settings['shuffle_sfx'], args.shuffle_sfx[p])
args.msu_resume[p] = get_setting(settings['msu_resume'], args.msu_resume[p])
def get_item_pool(self):
if 'item_pool' in self.file_source:
@@ -150,6 +172,11 @@ class CustomSettings(object):
return self.file_source['placements']
return None
def get_advanced_placements(self):
if 'advanced_placements' in self.file_source:
return self.file_source['advanced_placements']
return None
def get_entrances(self):
if 'entrances' in self.file_source:
return self.file_source['entrances']
@@ -175,13 +202,19 @@ class CustomSettings(object):
return self.file_source['medallions']
return None
def create_from_world(self, world):
def get_drops(self):
if 'drops' in self.file_source:
return self.file_source['drops']
return None
def create_from_world(self, world, race):
self.player_range = range(1, world.players + 1)
settings_dict, meta_dict = {}, {}
self.world_rep['meta'] = meta_dict
meta_dict['players'] = world.players
meta_dict['algorithm'] = world.algorithm
meta_dict['seed'] = world.seed
meta_dict['race'] = race
self.world_rep['settings'] = settings_dict
for p in self.player_range:
settings_dict[p] = {}
@@ -189,6 +222,8 @@ class CustomSettings(object):
settings_dict[p]['door_shuffle'] = world.doorShuffle[p]
settings_dict[p]['intensity'] = world.intensity[p]
settings_dict[p]['door_type_mode'] = world.door_type_mode[p]
settings_dict[p]['trap_door_mode'] = world.trap_door_mode[p]
settings_dict[p]['key_logic_algorithm'] = world.key_logic_algorithm[p]
settings_dict[p]['decoupledoors'] = world.decoupledoors[p]
settings_dict[p]['logic'] = world.logic[p]
settings_dict[p]['mode'] = world.mode[p]
+3
View File
@@ -98,6 +98,8 @@ SETTINGSTOPROCESS = {
"dungeondoorshuffle": "door_shuffle",
"dungeonintensity": "intensity",
"door_type_mode": "door_type_mode",
"trap_door_mode": "trap_door_mode",
"key_logic_algorithm": "key_logic_algorithm",
"decoupledoors": "decoupledoors",
"keydropshuffle": "keydropshuffle",
"dropshuffle": "dropshuffle",
@@ -130,6 +132,7 @@ SETTINGSTOPROCESS = {
"print_custom_yaml": "print_custom_yaml",
"usestartinventory": "usestartinventory",
"usecustompool": "custom",
"race": "race",
"saveonexit": "saveonexit"
}
},
+55 -14
View File
@@ -58,15 +58,17 @@ def generate_dungeon_find_proposal(builder, entrance_region_names, split_dungeon
excluded[region] = None
elif split_dungeon and builder.sewers_access and builder.sewers_access.entrance.parent_region == region:
continue
elif len(region.entrances) == 1: # for holes
access_region = next(x.parent_region for x in region.entrances
if x.parent_region.type in [RegionType.LightWorld, RegionType.DarkWorld]
or x.parent_region.name == 'Sewer Drop')
if access_region.name == 'Sewer Drop':
access_region = next(x.parent_region for x in access_region.entrances)
if (access_region.name in world.inaccessible_regions[player] and
region.name not in world.enabled_entrances[player]):
drop_region = next((x.parent_region for x in region.entrances
if x.parent_region.type in [RegionType.LightWorld, RegionType.DarkWorld]
or x.parent_region.name == 'Sewer Drop'), None)
if drop_region: # for holes
if drop_region.name == 'Sewer Drop':
drop_region = next(x.parent_region for x in drop_region.entrances)
if (drop_region.name in world.inaccessible_regions[player] and
region.name not in world.enabled_entrances[player]):
excluded[region] = None
elif region in excluded:
del excluded[region]
entrance_regions = [x for x in entrance_regions if x not in excluded.keys()]
doors_to_connect, idx = {}, 0
all_regions = set()
@@ -216,7 +218,6 @@ def modify_proposal(proposed_map, explored_state, doors_to_connect, hash_code_se
else:
logger.debug(f' Re-Linking {attempt.name} -> {new_door.name}')
logger.debug(f' Re-Linking {old_attempt.name} -> {old_target.name}')
hash_code_set.add(hash_code)
return proposed_map, hash_code
@@ -315,9 +316,14 @@ def determine_paths_for_dungeon(world, player, all_regions, name):
non_hole_portals.append(portal.door.entrance.parent_region.name)
if portal.destination:
paths.append(portal.door.entrance.parent_region.name)
if world.mode[player] == 'standard' and name == 'Hyrule Castle':
if world.mode[player] == 'standard' and name == 'Hyrule Castle Dungeon':
paths.append('Hyrule Dungeon Cellblock')
paths.append(('Hyrule Dungeon Cellblock', 'Sanctuary'))
paths.append(('Hyrule Dungeon Cellblock', 'Hyrule Castle Throne Room'))
entrance = next(x for x in world.dungeon_portals[player] if x.name == 'Hyrule Castle South')
# todo: in non-er, we can use the other portals too
paths.append(('Hyrule Dungeon Cellblock', entrance.door.entrance.parent_region.name))
paths.append(('Hyrule Castle Throne Room', [entrance.door.entrance.parent_region.name,
'Hyrule Dungeon Cellblock']))
if world.doorShuffle[player] in ['basic'] and name == 'Thieves Town':
paths.append('Thieves Attic Window')
elif 'Thieves Attic Window' in all_r_names:
@@ -431,6 +437,13 @@ def connect_simple_door(exit_door, region):
special_big_key_doors = ['Hyrule Dungeon Cellblock Door', "Thieves Blind's Cell Door"]
std_special_big_key_doors = ['Hyrule Castle Throne Room Tapestry'] + special_big_key_doors
def get_special_big_key_doors(world, player):
if world.mode[player] == 'standard':
return std_special_big_key_doors
return special_big_key_doors
class ExplorationState(object):
@@ -618,6 +631,7 @@ class ExplorationState(object):
elif not self.in_door_list(door, self.avail_doors):
self.append_door_to_list(door, self.avail_doors, flag)
# same as above but traps are ignored, and flag is not used
def add_all_doors_check_proposed_2(self, region, proposed_map, valid_doors, world, player):
for door in get_doors(world, region, player):
if door in proposed_map and door.name in valid_doors:
@@ -638,6 +652,27 @@ class ExplorationState(object):
elif not self.in_door_list(door, self.avail_doors):
self.append_door_to_list(door, self.avail_doors)
# same as above but traps are checked for
def add_all_doors_check_proposed_3(self, region, proposed_map, valid_doors, world, player):
for door in get_doors(world, region, player):
if door in proposed_map and door.name in valid_doors:
self.visited_doors.add(door)
if self.can_traverse(door):
if door.controller is not None:
door = door.controller
if door.dest is None and door not in proposed_map.keys() and door.name in valid_doors:
if not self.in_door_list_ic(door, self.unattached_doors):
self.append_door_to_list(door, self.unattached_doors)
else:
other = self.find_door_in_list(door, self.unattached_doors)
if self.crystal != other.crystal:
other.crystal = CrystalBarrier.Either
elif door.req_event is not None and door.req_event not in self.events and not self.in_door_list(door,
self.event_doors):
self.append_door_to_list(door, self.event_doors)
elif not self.in_door_list(door, self.avail_doors):
self.append_door_to_list(door, self.avail_doors)
def add_all_doors_check_proposed_traps(self, region, proposed_traps, world, player):
for door in get_doors(world, region, player):
if self.can_traverse_ignore_traps(door) and door not in proposed_traps:
@@ -671,7 +706,7 @@ class ExplorationState(object):
if door in key_door_proposal and door not in self.opened_doors:
if not self.in_door_list(door, self.small_doors):
self.append_door_to_list(door, self.small_doors)
elif (door.bigKey or door.name in special_big_key_doors) and not self.big_key_opened:
elif (door.bigKey or door.name in get_special_big_key_doors(world, player)) and not self.big_key_opened:
if not self.in_door_list(door, self.big_doors):
self.append_door_to_list(door, self.big_doors)
elif door.req_event is not None and door.req_event not in self.events:
@@ -824,7 +859,10 @@ def extend_reachable_state_lenient(search_regions, state, proposed_map, all_regi
local_state = state.copy()
for region in search_regions:
local_state.visit_region(region)
local_state.add_all_doors_check_proposed_2(region, proposed_map, valid_doors, world, player)
if world.trap_door_mode[player] == 'vanilla':
local_state.add_all_doors_check_proposed_3(region, proposed_map, valid_doors, world, player)
else:
local_state.add_all_doors_check_proposed_2(region, proposed_map, valid_doors, world, player)
while len(local_state.avail_doors) > 0:
explorable_door = local_state.next_avail_door()
if explorable_door.door in proposed_map:
@@ -835,7 +873,10 @@ def extend_reachable_state_lenient(search_regions, state, proposed_map, all_regi
if (valid_region_to_explore_in_regions(connect_region, all_regions, world, player)
and not local_state.visited(connect_region)):
local_state.visit_region(connect_region)
local_state.add_all_doors_check_proposed_2(connect_region, proposed_map, valid_doors, world, player)
if world.trap_door_mode[player] == 'vanilla':
local_state.add_all_doors_check_proposed_3(connect_region, proposed_map, valid_doors, world, player)
else:
local_state.add_all_doors_check_proposed_2(connect_region, proposed_map, valid_doors, world, player)
return local_state
+67 -16
View File
@@ -3,7 +3,7 @@ import logging
from collections import defaultdict
from source.item.District import resolve_districts
from BaseClasses import PotItem, PotFlags
from BaseClasses import PotItem, PotFlags, LocationType
from DoorShuffle import validate_vanilla_reservation
from Dungeons import dungeon_table
from Items import item_table, ItemFactory
@@ -18,6 +18,11 @@ class ItemPoolConfig(object):
self.item_pool = None
self.placeholders = None
self.reserved_locations = defaultdict(set)
self.restricted = {}
self.preferred = {}
self.verify = {}
self.verify_count = 0
self.verify_target = 0
self.recorded_choices = []
@@ -77,8 +82,8 @@ def create_item_pool_config(world):
if pot.item not in [PotItem.Key, PotItem.Hole, PotItem.Switch]:
item = pot_items[pot.item]
descriptor = 'Large Block' if pot.flags & PotFlags.Block else f'Pot #{pot_index+1}'
location = f'{pot.room} {descriptor}'
config.static_placement[player][item].append(location)
loc = f'{pot.room} {descriptor}'
config.static_placement[player][item].append(loc)
if world.shopsanity[player]:
for item, locs in shop_vanilla_mapping.items():
config.static_placement[player][item].extend(locs)
@@ -158,7 +163,11 @@ def create_item_pool_config(world):
dungeon_set = (mode_grouping['Big Chests'] + mode_grouping['Dungeon Trash'] + mode_grouping['Big Keys'] +
mode_grouping['Heart Containers'] + mode_grouping['GT Trash'] + mode_grouping['Small Keys'] +
mode_grouping['Compasses'] + mode_grouping['Maps'] + mode_grouping['Key Drops'] +
mode_grouping['Big Key Drops'])
mode_grouping['Pot Keys'] + mode_grouping['Big Key Drops'])
dungeon_set = set(dungeon_set)
for loc in world.get_locations():
if loc.parent_region.dungeon and loc.type in [LocationType.Pot, LocationType.Drop]:
dungeon_set.add(loc.name)
for player in range(1, world.players + 1):
config.item_pool[player] = determine_major_items(world, player)
config.location_groups[0].locations = set(dungeon_set)
@@ -187,26 +196,27 @@ def district_item_pool_config(world):
if district.dungeon:
adjustment = len([i for i in world.get_dungeon(name, p).all_items
if i.is_inside_dungeon_item(world)])
dist_len = len(district.locations) - adjustment
dist_adj = adjustment
if name not in district_choices:
district_choices[name] = (district.sphere_one, dist_len)
district_choices[name] = (district.sphere_one, dist_adj)
else:
so, amt = district_choices[name]
district_choices[name] = (so or district.sphere_one, amt + dist_len)
district_choices[name] = (so or district.sphere_one, amt + dist_adj)
chosen_locations = defaultdict(set)
location_cnt = 0
adjustment_cnt = 0
# choose a sphere one district
sphere_one_choices = [d for d, info in district_choices.items() if info[0]]
sphere_one = random.choice(sphere_one_choices)
so, amt = district_choices[sphere_one]
location_cnt += amt
so, adj = district_choices[sphere_one]
for player in range(1, world.players + 1):
for location in world.districts[player][sphere_one].locations:
chosen_locations[location].add(player)
del district_choices[sphere_one]
config.recorded_choices.append(sphere_one)
adjustment_cnt += adj
location_cnt = len(chosen_locations) - adjustment_cnt
scale_factors = defaultdict(int)
scale_total = 0
@@ -215,8 +225,9 @@ def district_item_pool_config(world):
dungeon = world.get_entrance(ent, p).connected_region.dungeon
if dungeon:
scale = world.crystals_needed_for_gt[p]
scale_total += scale
scale_factors[dungeon.name] += scale
if scale > 0:
scale_total += scale
scale_factors[dungeon.name] += scale
scale_total = max(1, scale_total)
scale_divisors = defaultdict(lambda: 1)
scale_divisors.update(scale_factors)
@@ -224,13 +235,15 @@ def district_item_pool_config(world):
while location_cnt < item_cnt:
weights = [scale_total / scale_divisors[d] for d in district_choices.keys()]
choice = random.choices(list(district_choices.keys()), weights=weights, k=1)[0]
so, amt = district_choices[choice]
location_cnt += amt
so, adj = district_choices[choice]
for player in range(1, world.players + 1):
for location in world.districts[player][choice].locations:
chosen_locations[location].add(player)
del district_choices[choice]
config.recorded_choices.append(choice)
adjustment_cnt += adj
location_cnt = len(chosen_locations) - adjustment_cnt
config.placeholders = location_cnt - item_cnt
config.location_groups[0].locations = chosen_locations
@@ -380,8 +393,13 @@ def vanilla_fallback(item_to_place, locations, world):
def filter_locations(item_to_place, locations, world, vanilla_skip=False, potion=False):
config = world.item_pool_config
if not isinstance(item_to_place, str):
item_name = 'Bottle' if item_to_place.name.startswith('Bottle') else item_to_place.name
else:
item_name = item_to_place
if world.algorithm == 'vanilla_fill':
config, filtered = world.item_pool_config, []
filtered = []
item_name = 'Bottle' if item_to_place.name.startswith('Bottle') else item_to_place.name
if item_name in config.static_placement[item_to_place.player]:
restricted = config.static_placement[item_to_place.player][item_name]
@@ -408,7 +426,8 @@ def filter_locations(item_to_place, locations, world, vanilla_skip=False, potion
return filtered if len(filtered) > 0 else locations
if world.algorithm == 'district':
config = world.item_pool_config
if item_to_place == 'Placeholder' or item_to_place.name in config.item_pool[item_to_place.player]:
if ((isinstance(item_to_place,str) and item_to_place == 'Placeholder')
or item_to_place.name in config.item_pool[item_to_place.player]):
restricted = config.location_groups[0].locations
filtered = [l for l in locations if l.name in restricted and l.player in restricted[l.name]]
return filtered if len(filtered) > 0 else locations
@@ -418,6 +437,15 @@ def filter_locations(item_to_place, locations, world, vanilla_skip=False, potion
if len(filtered) == 0:
raise RuntimeError('Can\'t sell potion of a certain type due to district restriction')
return filtered
if (item_name, item_to_place.player) in config.restricted:
locs = config.restricted[(item_name, item_to_place.player)]
return sorted(locations, key=lambda l: 1 if l.name in locs else 0)
if (item_name, item_to_place.player) in config.preferred:
locs = config.preferred[(item_name, item_to_place.player)]
return sorted(locations, key=lambda l: 0 if l.name in locs else 1)
if (item_name, item_to_place.player) in config.verify:
locs = config.verify[(item_name, item_to_place.player)].keys()
return sorted(locations, key=lambda l: 0 if l.name in locs else 1)
return locations
@@ -815,3 +843,26 @@ pot_items = {
}
valid_pot_items = {y: x for x, y in pot_items.items()}
if __name__ == '__main__':
import yaml
from yaml.representer import Representer
advanced_placements = {'advanced_placements': {}}
player_map = advanced_placements['advanced_placements']
placement_list = []
player_map[1] = placement_list
for item, location_list in vanilla_mapping.items():
for location in location_list:
placement = {}
placement['type'] = 'LocationGroup'
placement['item'] = item
locations = placement['locations'] = []
locations.append(location)
locations.append('Random')
placement_list.append(placement)
yaml.add_representer(defaultdict, Representer.represent_dict)
with open('fillgen.yaml', 'w') as file:
yaml.dump(advanced_placements, file)
+3 -2
View File
@@ -1,6 +1,4 @@
import importlib.util
import webbrowser
from tkinter import Tk, Label, Button, Frame
def check_requirements(console=False):
@@ -26,6 +24,9 @@ def check_requirements(console=False):
logger.error('See the step about "Installing Platform-specific dependencies":')
logger.error('https://github.com/aerinon/ALttPDoorRandomizer/blob/DoorDev/docs/BUILDING.md')
else:
import webbrowser
from tkinter import Tk, Label, Button, Frame
master = Tk()
master.title('Error')
frame = Frame(master)
+41 -23
View File
@@ -66,6 +66,8 @@ def link_entrances_new(world, player):
default_map['Old Man Cave (East)'] = 'Death Mountain Return Cave Exit (West)'
one_way_map['Bumper Cave (Top)'] = 'Dark Death Mountain Healer Fairy'
del default_map['Bumper Cave (Top)']
del one_way_map['Big Bomb Shop']
one_way_map['Inverted Big Bomb Shop'] = 'Inverted Big Bomb Shop'
avail_pool.default_map = default_map
avail_pool.one_way_map = one_way_map
@@ -273,7 +275,7 @@ def do_main_shuffle(entrances, exits, avail, mode_def):
# OM Cave entrance in lw/dw if cross_world off
if 'Old Man Cave Exit (West)' in rem_exits:
world_limiter = DW_Entrances if avail.inverted else LW_Entrances
om_cave_options = [x for x in rem_entrances if x in world_limiter and bonk_fairy_exception(x)]
om_cave_options = sorted([x for x in rem_entrances if x in world_limiter and bonk_fairy_exception(x)])
om_cave_choice = random.choice(om_cave_options)
if not avail.coupled:
connect_exit('Old Man Cave Exit (West)', om_cave_choice, avail)
@@ -302,14 +304,15 @@ def do_main_shuffle(entrances, exits, avail, mode_def):
unused_entrances = set()
if not cross_world:
lw_entrances, dw_entrances = [], []
for x in rem_entrances:
left = sorted(rem_entrances)
for x in left:
if bonk_fairy_exception(x):
lw_entrances.append(x) if x in LW_Entrances else dw_entrances.append(x)
do_same_world_connectors(lw_entrances, dw_entrances, multi_exit_caves, avail)
unused_entrances.update(lw_entrances)
unused_entrances.update(dw_entrances)
else:
entrance_list = [x for x in rem_entrances if bonk_fairy_exception(x)]
entrance_list = sorted([x for x in rem_entrances if bonk_fairy_exception(x)])
do_cross_world_connectors(entrance_list, multi_exit_caves, avail)
unused_entrances.update(entrance_list)
@@ -630,7 +633,7 @@ def do_fixed_shuffle(avail, entrance_list):
rules = Restrictions()
rules.size = size
if ('Hyrule Castle Entrance (South)' in entrances and
avail.world.doorShuffle[avail.player] in ['basic', 'crossed']):
avail.world.doorShuffle[avail.player] != 'vanilla'):
rules.must_exit_to_lw = True
if 'Inverted Ganons Tower' in entrances and not avail.world.shuffle_ganon:
rules.fixed = True
@@ -774,12 +777,19 @@ def do_vanilla_connect(pool_def, avail):
if avail.world.pottery[avail.player] not in ['none', 'keys', 'dungeon']:
return
defaults = inverted_default_connections if avail.inverted else default_connections
if avail.inverted:
if 'Dark Death Mountain Fairy' in pool_def['entrances']:
pool_def['entrances'].remove('Dark Death Mountain Fairy')
pool_def['entrances'].append('Bumper Cave (top)')
for entrance in pool_def['entrances']:
if entrance in avail.entrances:
target = defaults[entrance]
connect_simple(avail.world, entrance, target, avail.player)
avail.entrances.remove(entrance)
avail.exits.remove(target)
if entrance in avail.default_map:
connect_vanilla_two_way(entrance, avail.default_map[entrance], avail)
else:
connect_simple(avail.world, entrance, target, avail.player)
avail.entrances.remove(entrance)
avail.exits.remove(target)
def do_mandatory_connections(avail, entrances, cave_options, must_exit):
@@ -1013,12 +1023,15 @@ def connect_custom(avail_pool, world, player):
if world.customizer and world.customizer.get_entrances():
custom_entrances = world.customizer.get_entrances()
player_key = player
for ent_name, exit_name in custom_entrances[player_key]['two-way'].items():
connect_two_way(ent_name, exit_name, avail_pool)
for ent_name, exit_name in custom_entrances[player_key]['entrances'].items():
connect_entrance(ent_name, exit_name, avail_pool)
for ent_name, exit_name in custom_entrances[player_key]['exits'].items():
connect_exit(exit_name, ent_name, avail_pool)
if 'two-way' in custom_entrances[player_key]:
for ent_name, exit_name in custom_entrances[player_key]['two-way'].items():
connect_two_way(ent_name, exit_name, avail_pool)
if 'entrances' in custom_entrances[player_key]:
for ent_name, exit_name in custom_entrances[player_key]['entrances'].items():
connect_entrance(ent_name, exit_name, avail_pool)
if 'exits' in custom_entrances[player_key]:
for ent_name, exit_name in custom_entrances[player_key]['exits'].items():
connect_exit(exit_name, ent_name, avail_pool)
def connect_simple(world, exit_name, region_name, player):
@@ -1228,7 +1241,7 @@ modes = {
'Light World Bomb Hut', '20 Rupee Cave', '50 Rupee Cave', 'Hookshot Fairy',
'Palace of Darkness Hint', 'Dark Lake Hylia Ledge Spike Cave',
'Dark Desert Hint',
'Links House', 'Inverted Links House', 'Tavern North']
'Links House', 'Tavern North']
},
'old_man_cave': { # have to do old man cave first so lw dungeon don't use up everything
'special': 'old_man_cave_east',
@@ -1304,7 +1317,7 @@ modes = {
'Light World Bomb Hut', '20 Rupee Cave', '50 Rupee Cave', 'Hookshot Fairy',
'Palace of Darkness Hint', 'Dark Lake Hylia Ledge Spike Cave',
'Dark Desert Hint',
'Links House', 'Inverted Links House', 'Tavern North']
'Links House', 'Tavern North'] # inverted links house gets substituted
}
}
},
@@ -1572,6 +1585,7 @@ entrance_map = {
'Paradox Cave (Bottom)': 'Paradox Cave Exit (Bottom)',
'Paradox Cave (Middle)': 'Paradox Cave Exit (Middle)',
'Paradox Cave (Top)': 'Paradox Cave Exit (Top)',
'Inverted Dark Sanctuary': 'Inverted Dark Sanctuary Exit',
}
@@ -1607,7 +1621,7 @@ single_entrance_map = {
'Lake Hylia Fortune Teller': 'Lake Hylia Fortune Teller', 'Lake Hylia Fairy': 'Lake Hylia Healer Fairy',
'Bonk Fairy (Light)': 'Bonk Fairy (Light)', 'Lumberjack House': 'Lumberjack House', 'Dam': 'Dam',
'Blinds Hideout': 'Blinds Hideout', 'Waterfall of Wishing': 'Waterfall of Wishing',
'Inverted Bomb Shop': 'Inverted Bomb Shop', 'Inverted Dark Sanctuary': 'Inverted Dark Sanctuary',
'Inverted Bomb Shop': 'Inverted Bomb Shop'
}
default_dw = {
@@ -1616,7 +1630,7 @@ default_dw = {
'Palace of Darkness Exit', 'Swamp Palace Exit', 'Turtle Rock Exit (Front)', 'Turtle Rock Ledge Exit (West)',
'Turtle Rock Ledge Exit (East)', 'Turtle Rock Isolated Ledge Exit', 'Bumper Cave Exit (Top)',
'Bumper Cave Exit (Bottom)', 'Superbunny Cave Exit (Top)', 'Superbunny Cave Exit (Bottom)',
'Hookshot Cave Front Exit', 'Hookshot Cave Back Exit', 'Ganons Tower Exit', 'Pyramid Exit',
'Hookshot Cave Front Exit', 'Hookshot Cave Back Exit', 'Ganons Tower Exit', 'Pyramid Exit', 'Bonk Fairy (Dark)',
'Dark Lake Hylia Healer Fairy', 'Dark Lake Hylia Ledge Healer Fairy', 'Dark Desert Healer Fairy',
'Dark Death Mountain Healer Fairy', 'Cave Shop (Dark Death Mountain)', 'Pyramid Fairy', 'East Dark World Hint',
'Palace of Darkness Hint', 'Big Bomb Shop', 'Village of Outcasts Shop', 'Dark Lake Hylia Shop',
@@ -1639,7 +1653,7 @@ default_lw = {
'Spectacle Rock Cave Exit (Top)', 'Spectacle Rock Cave Exit (Peak)', 'Paradox Cave Exit (Bottom)',
'Paradox Cave Exit (Middle)', 'Paradox Cave Exit (Top)', 'Fairy Ascension Cave Exit (Bottom)',
'Fairy Ascension Cave Exit (Top)', 'Spiral Cave Exit', 'Spiral Cave Exit (Top)', 'Waterfall of Wishing', 'Dam',
'Blinds Hideout', 'Lumberjack House', 'Bonk Fairy (Light)', 'Bonk Fairy (Dark)', 'Lake Hylia Healer Fairy',
'Blinds Hideout', 'Lumberjack House', 'Bonk Fairy (Light)', 'Lake Hylia Healer Fairy',
'Swamp Healer Fairy', 'Desert Healer Fairy', 'Fortune Teller (Light)', 'Lake Hylia Fortune Teller', 'Kings Grave', 'Tavern',
'Chicken House', 'Aginahs Cave', 'Sahasrahlas Hut', 'Cave Shop (Lake Hylia)', 'Capacity Upgrade', 'Blacksmiths Hut',
'Sick Kids House', 'Lost Woods Gamble', 'Snitch Lady (East)', 'Snitch Lady (West)', 'Bush Covered House',
@@ -1684,7 +1698,7 @@ DW_Entrances = ['Bumper Cave (Bottom)', 'Superbunny Cave (Top)', 'Superbunny Ca
'Dark Lake Hylia Ledge Hint', 'Chest Game', 'Dark Desert Fairy', 'Dark Lake Hylia Ledge Fairy',
'Fortune Teller (Dark)', 'Dark World Hammer Peg Cave', 'Pyramid Entrance',
'Skull Woods First Section Door', 'Skull Woods Second Section Door (East)',
'Skull Woods Second Section Door (West)',
'Skull Woods Second Section Door (West)', 'Ganons Tower',
'Inverted Dark Sanctuary', 'Inverted Links House', 'Inverted Agahnims Tower']
LW_Must_Exit = ['Desert Palace Entrance (East)']
@@ -1938,6 +1952,8 @@ mandatory_connections = [('Links House S&Q', 'Links House'),
('Bumper Cave Entrance Mirror Spot', 'Death Mountain Entrance'),
('Bumper Cave Ledge Drop', 'West Dark World'),
('Bumper Cave Ledge Mirror Spot', 'Death Mountain Return Ledge'),
('Bumper Cave Top To Bottom', 'Bumper Cave (bottom)'),
('Bumper Cave Bottom to Top', 'Bumper Cave (top)'),
('Skull Woods Forest', 'Skull Woods Forest'),
('Desert Ledge Mirror Spot', 'Desert Ledge'),
('Desert Ledge (Northeast) Mirror Spot', 'Desert Ledge (Northeast)'),
@@ -2066,6 +2082,8 @@ inverted_mandatory_connections = [('Links House S&Q', 'Inverted Links House'),
('Bumper Cave Entrance Rock', 'Bumper Cave Entrance'),
('Bumper Cave Entrance Drop', 'West Dark World'),
('Bumper Cave Ledge Drop', 'West Dark World'),
('Bumper Cave Top To Bottom', 'Bumper Cave (bottom)'),
('Bumper Cave Bottom to Top', 'Bumper Cave (top)'),
('Skull Woods Forest', 'Skull Woods Forest'),
('Paradox Cave Push Block Reverse', 'Paradox Cave Chest Area'),
('Paradox Cave Push Block', 'Paradox Cave Front'),
@@ -2290,8 +2308,8 @@ default_connections = {'Links House': 'Links House',
'C-Shaped House': 'C-Shaped House',
'Chest Game': 'Chest Game',
'Dark World Hammer Peg Cave': 'Dark World Hammer Peg Cave',
'Bumper Cave (Bottom)': 'Bumper Cave',
'Bumper Cave (Top)': 'Bumper Cave',
'Bumper Cave (Bottom)': 'Bumper Cave (bottom)',
'Bumper Cave (Top)': 'Bumper Cave (top)',
'Red Shield Shop': 'Red Shield Shop',
'Dark Sanctuary Hint': 'Dark Sanctuary Hint',
'Fortune Teller (Dark)': 'Fortune Teller (Dark)',
@@ -2452,7 +2470,7 @@ inverted_default_connections = {'Waterfall of Wishing': 'Waterfall of Wishing',
'Inverted Big Bomb Shop': 'Inverted Big Bomb Shop',
'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 (bottom)',
'Old Man Cave (East)': 'Death Mountain Return Cave (left)',
'Old Man Cave Exit (West)': 'West Dark World',
'Old Man Cave Exit (East)': 'Dark Death Mountain',
@@ -2461,7 +2479,7 @@ inverted_default_connections = {'Waterfall of Wishing': 'Waterfall of Wishing',
'Bumper Cave (Top)': 'Dark Death Mountain Healer Fairy',
'Bumper Cave Exit (Top)': 'Death Mountain Return Ledge',
'Bumper Cave Exit (Bottom)': 'Light World',
'Death Mountain Return Cave (West)': 'Bumper Cave',
'Death Mountain Return Cave (West)': 'Bumper Cave (top)',
'Death Mountain Return Cave (East)': 'Death Mountain Return Cave (right)',
'Death Mountain Return Cave Exit (West)': 'Death Mountain',
'Death Mountain Return Cave Exit (East)': 'Death Mountain',
+7 -2
View File
@@ -84,6 +84,8 @@ def roll_settings(weights):
ret.door_shuffle = door_shuffle if door_shuffle != 'none' else 'vanilla'
ret.intensity = get_choice('intensity')
ret.door_type_mode = get_choice('door_type_mode')
ret.trap_door_mode = get_choice('trap_door_mode')
ret.key_logic_algorithm = get_choice('key_logic_algorithm')
ret.decoupledoors = get_choice('decoupledoors') == 'on'
ret.experimental = get_choice('experimental') == 'on'
ret.collection_rate = get_choice('collection_rate') == 'on'
@@ -101,7 +103,7 @@ def roll_settings(weights):
ret.dropshuffle = 'keys' if ret.dropshuffle == 'none' and keydropshuffle else ret.dropshuffle
ret.pottery = get_choice('pottery') if 'pottery' in weights else 'none'
ret.pottery = 'keys' if ret.pottery == 'none' and keydropshuffle else ret.pottery
ret.colorizepots = get_choice('colorizepots') == 'on'
ret.colorizepots = get_choice_default('colorizepots', default='on') == 'on'
ret.shufflepots = get_choice('pot_shuffle') == 'on'
ret.mixed_travel = get_choice('mixed_travel') if 'mixed_travel' in weights else 'prevent'
ret.standardize_palettes = (get_choice('standardize_palettes') if 'standardize_palettes' in weights
@@ -114,7 +116,9 @@ def roll_settings(weights):
'dungeons': 'dungeons',
'pedestal': 'pedestal',
'triforce-hunt': 'triforcehunt',
'trinity': 'trinity'
'trinity': 'trinity',
'ganonhunt': 'ganonhunt',
'completionist': 'completionist'
}[goal]
ret.openpyramid = goal in ['fast_ganon', 'trinity'] if ret.shuffle in ['vanilla', 'dungeonsfull', 'dungeonssimple'] else False
@@ -202,5 +206,6 @@ def roll_settings(weights):
ret.ow_palettes = get_choice('ow_palettes', romweights)
ret.uw_palettes = get_choice('uw_palettes', romweights)
ret.shuffle_sfx = get_choice('shuffle_sfx', romweights) == 'on'
ret.msu_resume = get_choice('msu_resume', romweights) == 'on'
return ret
+126
View File
@@ -0,0 +1,126 @@
import fnmatch
import os
import subprocess
import sys
import multiprocessing
import concurrent.futures
import argparse
from collections import OrderedDict
cpu_threads = multiprocessing.cpu_count()
py_version = f"{sys.version_info.major}.{sys.version_info.minor}"
def main(args=None):
successes = []
errors = []
task_mapping = []
tests = OrderedDict()
successes.append(f"Testing DR (NewTestSuite)")
print(successes[0])
# max_attempts = args.count
pool = concurrent.futures.ThreadPoolExecutor(max_workers=cpu_threads)
dead_or_alive = 0
alive = 0
def test(test_name: str, command: str, test_file: str):
tests[test_name] = [command]
base_command = f"python3.8 DungeonRandomizer.py --suppress_rom --suppress_spoiler"
def gen_seed():
task_command = base_command + " " + command
return subprocess.run(task_command, capture_output=True, shell=True, text=True)
task = pool.submit(gen_seed)
task.success = False
task.name = test_name
task.test_file = test_file
task.cmd = base_command + " " + command
task_mapping.append(task)
for test_suite, test_files in args.test_suite.items():
for test_file in test_files:
test(test_suite, f'--customizer {os.path.join(test_suite, test_file)}', test_file)
from tqdm import tqdm
with tqdm(concurrent.futures.as_completed(task_mapping),
total=len(task_mapping), unit="seed(s)",
desc=f"Success rate: 0.00%") as progressbar:
for task in progressbar:
dead_or_alive += 1
try:
result = task.result()
if result.returncode:
errors.append([task.name + ' ' + task.test_file, task.cmd, result.stderr])
else:
alive += 1
task.success = True
except Exception as e:
raise e
progressbar.set_description(f"Success rate: {(alive/dead_or_alive)*100:.2f}% - {task.name}")
def get_results(testname: str):
result = ""
dead_or_alive = [task.success for task in task_mapping if task.name == testname]
alive = [x for x in dead_or_alive if x]
success = f"{testname} Rate: {(len(alive) / len(dead_or_alive)) * 100:.2f}%"
successes.append(success)
print(success)
result += f"{(len(alive)/len(dead_or_alive))*100:.2f}%\t"
return result.strip()
results = []
for t in tests.keys():
results.append(get_results(t))
for result in results:
print(result)
successes.append(result)
return successes, errors
if __name__ == "__main__":
successes = []
parser = argparse.ArgumentParser(add_help=False)
# parser.add_argument('--count', default=0, type=lambda value: max(int(value), 0))
parser.add_argument('--cpu_threads', default=cpu_threads, type=lambda value: max(int(value), 1))
parser.add_argument('--help', default=False, action='store_true')
args = parser.parse_args()
if args.help:
parser.print_help()
exit(0)
cpu_threads = args.cpu_threads
test_suites = {}
# not sure if it supports subdirectories properly yet
for root, dirnames, filenames in os.walk('test/suite'):
test_suites[root] = fnmatch.filter(filenames, '*.yaml')
args = argparse.Namespace()
args.test_suite = test_suites
s, errors = main(args=args)
if successes:
successes += [""] * 2
successes += s
print()
if errors:
with open(f"new-test-suite-errors.txt", 'w') as stream:
for error in errors:
stream.write(error[0] + "\n")
stream.write(error[1] + "\n")
stream.write(error[2] + "\n\n")
with open("new-test-suite-success.txt", "w") as stream:
stream.write(str.join("\n", successes))
input("Press enter to continue")
+23
View File
@@ -0,0 +1,23 @@
meta:
players: 2
settings:
1:
pottery: cavekeys
keysanity: True
2:
keysanity: True
placements:
1:
Sanctuary: Small Key (Escape)#2
'Links House Pot #3': Rupees (20)#2
start_inventory:
1:
- Pegasus Boots
- Ocarina (Activated)
- Magic Mirror
- Boss Heart Container
- Boss Heart Container
- Boss Heart Container
- Boss Heart Container
- Red Mail
- Golden Sword
+46
View File
@@ -0,0 +1,46 @@
meta:
players: 1
algorithm: dungeon_only
settings:
1:
goal: dungeons
pseudoboots: true
door_shuffle: crossed
decoupledoors: true
intensity: 3
door_type_mode: all
experimental: true
dungeon_counters: 'on'
compassshuffle: true
mapshuffle: true
keydropshuffle: true
doors:
1:
lobbies:
Hyrule Castle South: Thieves Lobby S
doors:
Thieves Lobby NE Edge:
dest: Hyrule Castle Throne Room South Stairs
one-way: True
Hyrule Castle Throne Room South Stairs:
dest: Desert Beamos Hall NE
one-way: True
Desert Beamos Hall NE:
dest: Ice Spike Cross SE
type: Key Door
one-way: True
Ice Spike Cross SE:
dest: Thieves Lobby NE Edge
one-way: True
Hyrule Castle Throne Room N:
dest: Ice Firebar Down Ladder
type: Key Door
Swamp Lobby S:
dest: Thieves Lobby N Edge
type: Bomb Door
Thieves Lobby E: Thieves Compass Room W
Thieves Compass Room NW Edge: Ice Tall Hint SE
Thieves Compass Room N Edge: PoD Bow Statue Down Ladder
+4 -2
View File
@@ -1,5 +1,7 @@
import os
import sys
sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..'))
import RaceRandom as random
import logging
import time
from collections import Counter, defaultdict
@@ -106,7 +108,7 @@ def test_loop(tests, entrance_set, exit_set, ctr, shuffle_mode, main_mode, links
# seed = 635441530
random.seed(seed)
world = World(1, {1: shuffle_mode}, {1: 'vanilla'}, {1: 'noglitches'}, {1: main_mode}, {}, {}, {},
{}, {}, {}, {}, {}, True, {}, {}, [], {})
{}, {}, {}, {}, {}, True, {}, [], {})
world.customizer = False
world.shufflelinks = {1: links}
if world.mode[1] != 'inverted':
+44
View File
@@ -0,0 +1,44 @@
# Possible improvements: account for items that are possibly in logic
# Example: Mire Big Key in harmless means all 6 mire smalls required for fire-locked side,
# if you have access to harmless via:
# 2 pod smalls + bow, hammer or 3 pod small
meta:
players: 1
settings:
1:
key_logic_algorithm: default
keysanity: True
crystals_needed_for_gt: 0 # to skip trash fill
placements:
1:
Hobo: Big Key (Misery Mire)
Waterfall Fairy - Left: Small Key (Misery Mire)
Waterfall Fairy - Right: Small Key (Misery Mire)
Palace of Darkness - Big Chest: Hammer
advanced_placements:
1:
# Contrast with partial_2
- type: Verification
item: Flippers
locations:
Misery Mire - Map Chest: True
Misery Mire - Main Lobby: True
Misery Mire - Bridge Chest: True
Misery Mire - Spike Chest: True
Misery Mire - Compass Chest: False
Misery Mire - Big Key Chest: False
Misery Mire - Boss: False
# Contrast with partial_3
- type: Verification
item: Big Key (Ganons Tower)
locations:
Ganons Tower - Big Key Chest: True
Ganons Tower - Big Key Room - Left: True
Ganons Tower - Big Key Room - Right: True
Ganons Tower - Bob's Chest: True
# Normal logic doesn't allow this placement
# unless hammer is placed before it - no algorithm does this in non-keysanity, but possible in keysanity
- type: Verification
item: Small Key (Palace of Darkness)
locations:
Palace of Darkness - Dark Maze - Bottom: True
+24
View File
@@ -0,0 +1,24 @@
# Even though Lamp is Flipper-locked, this logic considers that a key could be wasted in the dark in mire
# Only fire locked mire is off limits
meta:
players: 1
settings:
1:
key_logic_algorithm: partial
keysanity: true
placements:
1:
Hobo: Lamp
Waterfall Fairy - Left: Small Key (Misery Mire)
advanced_placements:
1:
- type: Verification
item: Flippers
locations:
Misery Mire - Map Chest: True
Misery Mire - Main Lobby: True
Misery Mire - Bridge Chest: True
Misery Mire - Spike Chest: True
Misery Mire - Compass Chest: False
Misery Mire - Big Key Chest: False
Misery Mire - Boss: False
+26
View File
@@ -0,0 +1,26 @@
# For contrast with default logic
# This logic is not yet smart enough to allow the crystal blocked chests with two keys (Spike Pot and one other)
meta:
players: 1
settings:
1:
key_logic_algorithm: partial
keysanity: True
placements:
1:
Hobo: Lamp
Waterfall Fairy - Left: Small Key (Misery Mire)
Waterfall Fairy - Right: Small Key (Misery Mire)
Swamp Palace - Entrance: Big Key (Misery Mire)
advanced_placements:
1:
- type: Verification
item: Flippers
locations:
Misery Mire - Map Chest: False
Misery Mire - Main Lobby: False
Misery Mire - Bridge Chest: True
Misery Mire - Spike Chest: True
Misery Mire - Compass Chest: False
Misery Mire - Big Key Chest: False
Misery Mire - Boss: False
+160
View File
@@ -0,0 +1,160 @@
# For contrast with default logic
# Examples of valid big key placement that doesn't work with pure worst case scenarios
# Basically chests that are obtainable two ways+ of spending keys
# (Possible fix: access to the extra door grants access to the mini helma key too)
meta:
players: 1
settings:
1:
key_logic_algorithm: partial
crystals_needed_for_gt: 0 # to skip trash fill
advanced_placements:
1:
- type: Verification
item: Big Key (Desert Palace)
locations:
Desert Palace - Big Chest: False
Desert Palace - Big Key Chest: True
Desert Palace - Boss: False
Desert Palace - Compass Chest: True
Desert Palace - Map Chest: True
Desert Palace - Torch: True
- type: Verification
item: Big Key (Eastern Palace)
locations:
Eastern Palace - Big Chest: False
Eastern Palace - Big Key Chest: True
Eastern Palace - Boss: False
Eastern Palace - Cannonball Chest: True
Eastern Palace - Compass Chest: True
Eastern Palace - Map Chest: True
- type: Verification
item: Big Key (Ganons Tower)
locations:
# These four require not wasting keys upstairs because the big key is down here
Ganons Tower - Big Key Chest: False
Ganons Tower - Big Key Room - Left: False
Ganons Tower - Big Key Room - Right: False
Ganons Tower - Bob's Chest: False
# These are normal
Ganons Tower - Big Chest: False
Ganons Tower - Bob's Torch: True
Ganons Tower - Compass Room - Bottom Left: True
Ganons Tower - Compass Room - Bottom Right: True
Ganons Tower - Compass Room - Top Left: True
Ganons Tower - Compass Room - Top Right: True
Ganons Tower - DMs Room - Bottom Left: True
Ganons Tower - DMs Room - Bottom Right: True
Ganons Tower - DMs Room - Top Left: True
Ganons Tower - DMs Room - Top Right: True
Ganons Tower - Firesnake Room: True
Ganons Tower - Hope Room - Left: True
Ganons Tower - Hope Room - Right: True
Ganons Tower - Map Chest: True
Ganons Tower - Mini Helmasaur Room - Left: False
Ganons Tower - Mini Helmasaur Room - Right: False
Ganons Tower - Pre-Moldorm Chest: False
Ganons Tower - Randomizer Room - Bottom Left: True
Ganons Tower - Randomizer Room - Bottom Right: True
Ganons Tower - Randomizer Room - Top Left: True
Ganons Tower - Randomizer Room - Top Right: True
Ganons Tower - Tile Room: True
Ganons Tower - Validation Chest: False
- type: Verification
item: Big Key (Ice Palace)
locations:
Ice Palace - Big Chest: False
Ice Palace - Big Key Chest: True
Ice Palace - Boss: False
Ice Palace - Compass Chest: True
Ice Palace - Freezor Chest: True
Ice Palace - Iced T Room: True
Ice Palace - Map Chest: True
Ice Palace - Spike Room: True
- type: Verification
item: Big Key (Misery Mire)
locations:
Misery Mire - Big Chest: False
Misery Mire - Big Key Chest: True
Misery Mire - Boss: False
Misery Mire - Bridge Chest: True
Misery Mire - Compass Chest: True
Misery Mire - Main Lobby: True
Misery Mire - Map Chest: True
Misery Mire - Spike Chest: True
- type: Verification
item: Big Key (Palace of Darkness)
locations:
Palace of Darkness - Big Chest: False
Palace of Darkness - Big Key Chest: True
Palace of Darkness - Boss: False
Palace of Darkness - Compass Chest: True
Palace of Darkness - Dark Basement - Left: True
Palace of Darkness - Dark Basement - Right: True
Palace of Darkness - Dark Maze - Bottom: True
Palace of Darkness - Dark Maze - Top: True
Palace of Darkness - Harmless Hellway: True
Palace of Darkness - Map Chest: True
Palace of Darkness - Shooter Room: True
Palace of Darkness - Stalfos Basement: True
Palace of Darkness - The Arena - Bridge: True
Palace of Darkness - The Arena - Ledge: True
- type: Verification
item: Big Key (Skull Woods)
locations:
Skull Woods - Big Chest: True
Skull Woods - Big Key Chest: True
Skull Woods - Boss: True
Skull Woods - Bridge Room: True
Skull Woods - Compass Chest: True
Skull Woods - Map Chest: True
Skull Woods - Pinball Room: True
Skull Woods - Pot Prison: True
- type: Verification
item: Big Key (Swamp Palace)
locations:
Swamp Palace - Big Chest: True
Swamp Palace - Big Key Chest: True
Swamp Palace - Boss: True
Swamp Palace - Compass Chest: True
Swamp Palace - Entrance: False
Swamp Palace - Flooded Room - Left: True
Swamp Palace - Flooded Room - Right: True
Swamp Palace - Map Chest: True
Swamp Palace - Waterfall Room: True
Swamp Palace - West Chest: True
- type: Verification
item: Big Key (Thieves Town)
locations:
Thieves' Town - Ambush Chest: True
Thieves' Town - Attic: False
Thieves' Town - Big Chest: False
Thieves' Town - Big Key Chest: True
Thieves' Town - Blind's Cell: False
Thieves' Town - Boss: False
Thieves' Town - Compass Chest: True
Thieves' Town - Map Chest: True
- type: Verification
item: Big Key (Tower of Hera)
locations:
Tower of Hera - Basement Cage: True
Tower of Hera - Big Chest: False
Tower of Hera - Big Key Chest: True
Tower of Hera - Boss: False
Tower of Hera - Compass Chest: False
Tower of Hera - Map Chest: True
- type: Verification
item: Big Key (Turtle Rock)
locations:
Turtle Rock - Big Chest: False
Turtle Rock - Big Key Chest: True
Turtle Rock - Boss: False
Turtle Rock - Chain Chomps: True
Turtle Rock - Compass Chest: True
Turtle Rock - Crystaroller Room: False
Turtle Rock - Eye Bridge - Bottom Left: False
Turtle Rock - Eye Bridge - Bottom Right: False
Turtle Rock - Eye Bridge - Top Left: False
Turtle Rock - Eye Bridge - Top Right: False
Turtle Rock - Roller Room - Left: True
Turtle Rock - Roller Room - Right: True
+22
View File
@@ -0,0 +1,22 @@
meta:
players: 1
settings:
1:
key_logic_algorithm: strict
keysanity: true
placements:
1:
Hobo: Big Key (Misery Mire)
Waterfall Fairy - Left: Small Key (Misery Mire)
advanced_placements:
1:
- type: Verification
item: Flippers
locations:
Misery Mire - Map Chest: False
Misery Mire - Main Lobby: False
Misery Mire - Bridge Chest: True
Misery Mire - Spike Chest: True
Misery Mire - Compass Chest: False
Misery Mire - Big Key Chest: False
Misery Mire - Boss: False