Merged DR v1.2.0.0

This commit is contained in:
codemann8
2022-12-03 01:52:22 -06:00
68 changed files with 10253 additions and 1200 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
+3
View File
@@ -4,6 +4,9 @@
*.bmbp
*.log
*_Spoiler.json
*_custom.yaml
*_meta.txt
*.bps
*.pyc
*.sfc
*.srm
+1
View File
@@ -0,0 +1 @@
<randall.rupper@gmail.com> <randall.rupper@sirsidynix.com>
+115 -53
View File
@@ -21,7 +21,7 @@ from source.dungeon.RoomObject import RoomObject
class World(object):
def __init__(self, players, owShuffle, owCrossed, owMixed, shuffle, doorShuffle, logic, mode, swords, difficulty, difficulty_adjustments,
timer, progressive, goal, algorithm, accessibility, shuffle_ganon, retro, custom, customitemarray, hints):
timer, progressive, goal, algorithm, accessibility, shuffle_ganon, custom, customitemarray, hints):
self.players = players
self.teams = 1
self.owShuffle = owShuffle.copy()
@@ -34,6 +34,7 @@ class World(object):
self.shuffle = shuffle.copy()
self.doorShuffle = doorShuffle.copy()
self.intensity = {}
self.door_type_mode = {}
self.logic = logic.copy()
self.mode = mode.copy()
self.swords = swords.copy()
@@ -71,7 +72,6 @@ class World(object):
self.fix_trock_exit = {}
self.shuffle_ganon = shuffle_ganon
self.fix_gtower_exit = self.shuffle_ganon
self.retro = retro.copy()
self.custom = custom
self.customitemarray = customitemarray
self.can_take_damage = True
@@ -105,10 +105,6 @@ class World(object):
self.pot_contents = {}
for player in range(1, players + 1):
# If World State is Retro, set to Open and set Retro flag
if self.mode[player] == "retro":
self.mode[player] = "open"
self.retro[player] = True
def set_player_attr(attr, val):
self.__dict__.setdefault(attr, {})[player] = val
set_player_attr('_region_cache', {})
@@ -132,13 +128,15 @@ 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 ['lean', 'crossed', 'insanity', 'madness_legacy'])
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', False)
set_player_attr('keyshuffle', 'none')
set_player_attr('bigkeyshuffle', False)
set_player_attr('restrict_boss_items', 'none')
set_player_attr('bombbag', False)
set_player_attr('flute_mode', False)
set_player_attr('bow_mode', False)
set_player_attr('difficulty_requirements', None)
set_player_attr('boss_shuffle', 'none')
set_player_attr('enemy_shuffle', 'none')
@@ -151,6 +149,7 @@ class World(object):
set_player_attr('crystals_ganon_orig', {})
set_player_attr('crystals_gt_orig', {})
set_player_attr('open_pyramid', 'auto')
set_player_attr('take_any', 'none')
set_player_attr('treasure_hunt_icon', 'Triforce Piece')
set_player_attr('treasure_hunt_count', 0)
set_player_attr('treasure_hunt_total', 0)
@@ -160,16 +159,23 @@ class World(object):
set_player_attr('collection_rate', False)
set_player_attr('colorizepots', False)
set_player_attr('pot_pool', {})
set_player_attr('decoupledoors', False)
set_player_attr('door_type_mode', 'original')
set_player_attr('shopsanity', False)
set_player_attr('mixed_travel', 'prevent')
set_player_attr('standardize_palettes', 'standardize')
set_player_attr('force_fix', {'gt': False, 'sw': False, 'pod': False, 'tr': False})
set_player_attr('prizes', {'pull': [0, 0, 0], 'crab': [0, 0], 'stun': 0, 'fish': 0})
set_player_attr('prizes', {'dig;': [], 'pull': [0, 0, 0], 'crab': [0, 0], 'stun': 0, 'fish': 0, 'enemies': []})
set_player_attr('exp_cache', defaultdict(dict))
set_player_attr('enabled_entrances', {})
def finish_init(self):
for player in range(1, self.players + 1):
if self.mode[player] == 'retro':
self.mode[player] == 'open'
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)})'
@@ -451,7 +457,8 @@ class World(object):
def push_precollected(self, item):
item.world = self
if (item.smallkey and self.keyshuffle[item.player]) or (item.bigkey and self.bigkeyshuffle[item.player]):
if ((item.smallkey and self.keyshuffle[item.player] != 'none')
or (item.bigkey and self.bigkeyshuffle[item.player])):
item.advancement = True
self.precollected_items.append(item)
self.state.collect(item, True)
@@ -586,6 +593,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):
@@ -655,7 +663,7 @@ class CollectionState(object):
if key_logic.sm_doors[door]:
self.reached_doors[player].add(key_logic.sm_doors[door].name)
if not connection.can_reach(self):
checklist_key = 'Universal' if self.world.retro[player] else dungeon_name
checklist_key = 'Universal' if self.world.keyshuffle[player] == 'universal' else dungeon_name
checklist = self.dungeons_to_check[player][checklist_key]
checklist[connection.name] = (connection, crystal_state)
elif door.name not in self.opened_doors[player]:
@@ -827,7 +835,7 @@ class CollectionState(object):
return None
def set_dungeon_limits(self, player, dungeon_name):
if self.world.retro[player] and self.world.mode[player] == 'standard':
if self.world.keyshuffle[player] == 'universal' and self.world.mode[player] == 'standard':
self.dungeon_limits = ['Hyrule Castle', 'Agahnims Tower']
else:
self.dungeon_limits = [dungeon_name]
@@ -851,14 +859,16 @@ class CollectionState(object):
door_candidates.append(door.name)
return door_candidates
door_candidates, skip = [], set()
if state.world.accessibility[player] != 'locations' and remaining_keys == 0 and dungeon_name in state.world.key_logic[player]:
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 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)
@@ -894,6 +904,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):
@@ -977,7 +988,7 @@ class CollectionState(object):
'Golden Sword', 'Progressive Sword', 'Progressive Glove', 'Silver Arrows', 'Green Pendant',
'Blue Pendant', 'Red Pendant', 'Crystal 1', 'Crystal 2', 'Crystal 3', 'Crystal 4', 'Crystal 5',
'Crystal 6', 'Crystal 7', 'Blue Boomerang', 'Red Boomerang', 'Blue Shield', 'Red Shield',
'Mirror Shield', 'Progressive Shield', 'Bug Catching Net', 'Cane of Byrna', 'Ocarina (Activated)'
'Mirror Shield', 'Progressive Shield', 'Bug Catching Net', 'Cane of Byrna', 'Ocarina (Activated)',
'Boss Heart Container', 'Sanctuary Heart Container', 'Piece of Heart', 'Magic Upgrade (1/2)',
'Magic Upgrade (1/4)']
or item_name.startswith(('Bottle', 'Small Key', 'Big Key'))
@@ -1018,7 +1029,7 @@ class CollectionState(object):
new_locations = True
while new_locations:
reachable_events = [location for location in locations if location.event and
(not key_only or (not self.world.keyshuffle[location.item.player] and location.item.smallkey) or (not self.world.bigkeyshuffle[location.item.player] and location.item.bigkey))
(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
@@ -1078,7 +1089,7 @@ class CollectionState(object):
return self.prog_items[item, player] >= count
def has_sm_key(self, item, player, count=1):
if self.world.retro[player]:
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)
@@ -1207,7 +1218,7 @@ class CollectionState(object):
or self.has('Cane of Somaria', player))
def can_shoot_arrows(self, player):
if self.world.retro[player]:
if self.world.bow_mode[player] in ['retro', 'retro_silvers']:
#todo: Non-progressive silvers grant wooden arrows, but progressive bows do not. Always require shop arrows to be safe
return self.has('Bow', player) and (self.can_buy_unlimited('Single Arrow', player) or self.has('Single Arrow', player))
return self.has('Bow', player)
@@ -1253,7 +1264,8 @@ class CollectionState(object):
if any(map(lambda i: i.name in ['Ocarina', 'Ocarina (Activated)'], self.world.precollected_items)):
return True
lw = self.world.get_region('Kakariko Area', player)
return self.has('Ocarina (Activated)', player) or (self.has('Ocarina', player) and lw.can_reach(self) and self.is_not_bunny(lw, player))
return self.has('Ocarina (Activated)', player) or (self.has('Ocarina', player) and lw.can_reach(self)
and self.is_not_bunny(lw, player))
def can_melt_things(self, player):
return self.has('Fire Rod', player) or (self.has('Bombos', player) and self.has_sword(player))
@@ -1496,7 +1508,7 @@ class Region(object):
return False
def can_fill(self, item):
inside_dungeon_item = ((item.smallkey and not self.world.keyshuffle[item.player])
inside_dungeon_item = ((item.smallkey and self.world.keyshuffle[item.player] == 'none')
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]))
@@ -2276,7 +2288,6 @@ class Sector(object):
self.item_logic = set()
self.chest_location_set = set()
def region_set(self):
if self.r_name_set is None:
self.r_name_set = dict.fromkeys(map(lambda r: r.name, self.regions))
@@ -2558,7 +2569,7 @@ class Location(object):
def gen_name(self):
name = self.name
world = self.parent_region.world if self.parent_region and self.parent_region.world else None
if self.parent_region.dungeon and world and world.doorShuffle[self.player] == 'crossed':
if self.parent_region.dungeon and world and world.doorShuffle[self.player] not in ['basic', 'vanilla']:
name += f' @ {self.parent_region.dungeon.name}'
if world and world.players > 1:
name += f' ({world.get_player_names(self.player)})'
@@ -2643,7 +2654,7 @@ class Item(object):
return item_dungeon
def is_inside_dungeon_item(self, world):
return ((self.smallkey and not world.keyshuffle[self.player])
return ((self.smallkey and world.keyshuffle[self.player] == 'none')
or (self.bigkey and not world.bigkeyshuffle[self.player])
or (self.compass and not world.compassshuffle[self.player])
or (self.map and not world.mapshuffle[self.player]))
@@ -2794,9 +2805,10 @@ class Spoiler(object):
'versions': {'Door':ERVersion, 'Overworld':ORVersion},
'logic': self.world.logic,
'mode': self.world.mode,
'retro': self.world.retro,
'bombbag': self.world.bombbag,
'weapons': self.world.swords,
'flute_mode': self.world.flute_mode,
'bow_mode': self.world.bow_mode,
'goal': self.world.goal,
'ow_shuffle': self.world.owShuffle,
'ow_terrain': self.world.owTerrain,
@@ -2809,9 +2821,13 @@ class Spoiler(object):
'shuffle': self.world.shuffle,
'shuffleganon': self.world.shuffle_ganon,
'shufflelinks': self.world.shufflelinks,
'shuffletavern': self.world.shuffletavern,
'take_any': self.world.take_any,
'overworld_map': self.world.overworld_map,
'door_shuffle': self.world.doorShuffle,
'intensity': self.world.intensity,
'door_type_mode': self.world.door_type_mode,
'decoupledoors': self.world.decoupledoors,
'dungeon_counters': self.world.dungeon_counters,
'item_pool': self.world.difficulty,
'item_functionality': self.world.difficulty_adjustments,
@@ -2996,7 +3012,6 @@ class Spoiler(object):
outfile.write('Settings Code:'.ljust(line_width) + '%s\n' % self.metadata["code"][player])
outfile.write('Logic:'.ljust(line_width) + '%s\n' % self.metadata['logic'][player])
outfile.write('Mode:'.ljust(line_width) + '%s\n' % self.metadata['mode'][player])
outfile.write('Retro:'.ljust(line_width) + '%s\n' % yn(self.metadata['retro'][player]))
outfile.write('Swords:'.ljust(line_width) + '%s\n' % self.metadata['weapons'][player])
outfile.write('Goal:'.ljust(line_width) + '%s\n' % self.metadata['goal'][player])
if self.metadata['goal'][player] in ['triforcehunt', 'trinity']:
@@ -3008,6 +3023,9 @@ class Spoiler(object):
outfile.write('Restricted Boss Items:'.ljust(line_width) + '%s\n' % self.metadata['restricted_boss_items'][player])
outfile.write('Difficulty:'.ljust(line_width) + '%s\n' % self.metadata['item_pool'][player])
outfile.write('Item Functionality:'.ljust(line_width) + '%s\n' % self.metadata['item_functionality'][player])
outfile.write('Flute Mode:'.ljust(line_width) + '%s\n' % self.metadata['flute_mode'][player])
outfile.write('Bow Mode:'.ljust(line_width) + '%s\n' % self.metadata['bow_mode'][player])
outfile.write('Take Any Caves:'.ljust(line_width) + '%s\n' % self.metadata['take_any'][player])
outfile.write('Shopsanity:'.ljust(line_width) + '%s\n' % yn(self.metadata['shopsanity'][player]))
outfile.write('Bombbag:'.ljust(line_width) + '%s\n' % yn(self.metadata['bombbag'][player]))
outfile.write('Pseudoboots:'.ljust(line_width) + '%s\n' % yn(self.metadata['pseudoboots'][player]))
@@ -3025,12 +3043,15 @@ class Spoiler(object):
if self.metadata['shuffle'][player] != 'vanilla':
outfile.write('Shuffle GT/Ganon:'.ljust(line_width) + '%s\n' % yn(self.metadata['shuffleganon'][player]))
outfile.write('Shuffle Links:'.ljust(line_width) + '%s\n' % yn(self.metadata['shufflelinks'][player]))
outfile.write('Shuffle Tavern:'.ljust(line_width) + '%s\n' % yn(self.metadata['shuffletavern'][player]))
if self.metadata['shuffle'][player] != 'vanilla' or self.metadata['ow_mixed'][player]:
outfile.write('Overworld Map:'.ljust(line_width) + '%s\n' % self.metadata['overworld_map'][player])
outfile.write('Pyramid Hole Pre-opened:'.ljust(line_width) + '%s\n' % self.metadata['open_pyramid'][player])
outfile.write('Door Shuffle:'.ljust(line_width) + '%s\n' % self.metadata['door_shuffle'][player])
if self.metadata['door_shuffle'][player] != 'vanilla':
outfile.write('Intensity:'.ljust(line_width) + '%s\n' % self.metadata['intensity'][player])
outfile.write('Door Type Mode:'.ljust(line_width) + '%s\n' % self.metadata['door_type_mode'][player])
outfile.write('Decouple Doors:'.ljust(line_width) + '%s\n' % yn(self.metadata['decoupledoors'][player]))
outfile.write('Experimental:'.ljust(line_width) + '%s\n' % yn(self.metadata['experimental'][player]))
outfile.write('Dungeon Counters:'.ljust(line_width) + '%s\n' % self.metadata['dungeon_counters'][player])
outfile.write('Enemy Drop Shuffle:'.ljust(line_width) + '%s\n' % yn(self.metadata['dropshuffle'][player]))
@@ -3038,7 +3059,7 @@ class Spoiler(object):
outfile.write('Pot Shuffle (Legacy):'.ljust(line_width) + '%s\n' % yn(self.metadata['potshuffle'][player]))
outfile.write('Map Shuffle:'.ljust(line_width) + '%s\n' % yn(self.metadata['mapshuffle'][player]))
outfile.write('Compass Shuffle:'.ljust(line_width) + '%s\n' % yn(self.metadata['compassshuffle'][player]))
outfile.write('Small Key Shuffle:'.ljust(line_width) + '%s\n' % yn(self.metadata['keyshuffle'][player]))
outfile.write('Small Key Shuffle:'.ljust(line_width) + '%s\n' % self.metadata['keyshuffle'][player])
outfile.write('Big Key Shuffle:'.ljust(line_width) + '%s\n' % yn(self.metadata['bigkeyshuffle'][player]))
outfile.write('Boss Shuffle:'.ljust(line_width) + '%s\n' % self.metadata['boss_shuffle'][player])
outfile.write('Enemy Shuffle:'.ljust(line_width) + '%s\n' % self.metadata['enemy_shuffle'][player])
@@ -3311,12 +3332,12 @@ class Pot(object):
return hash((self.x, self.y, self.room))
# byte 0: DDOO OEEE (DR, OR, ER)
dr_mode = {"basic": 1, "crossed": 2, "vanilla": 0}
or_mode = {"vanilla": 0, "parallel": 1, "full": 1}
er_mode = {"vanilla": 0, "simple": 1, "restricted": 2, "full": 3, "lite": 4, "lean": 5, "crossed": 6, "insanity": 7, "dungeonsfull": 8, "dungeonssimple": 9}
# byte 0: DDDE EEEE (DR, ER)
dr_mode = {"basic": 1, "crossed": 2, "vanilla": 0, "partitioned": 3}
er_mode = {"vanilla": 0, "simple": 1, "restricted": 2, "full": 3, "crossed": 4, "insanity": 5, 'lite': 8,
'lean': 9, "dungeonsfull": 7, "dungeonssimple": 6}
# byte 1: LLLW WSSR (logic, mode, sword, retro)
# byte 1: LLLW WSS? (logic, mode, sword)
logic_mode = {"noglitches": 0, "minorglitches": 1, "nologic": 2, "owglitches": 3, "majorglitches": 4}
world_mode = {"open": 0, "standard": 1, "inverted": 2}
sword_mode = {"random": 0, "assured": 1, "swordless": 2, "vanilla": 3}
@@ -3326,13 +3347,13 @@ goal_mode = {"ganon": 0, "pedestal": 1, "dungeons": 2, "triforcehunt": 3, "cryst
diff_mode = {"normal": 0, "hard": 1, "expert": 2}
func_mode = {"normal": 0, "hard": 1, "expert": 2}
# byte 3: S?MM PIII (shop, unused, mixed, palettes, intensity)
# byte 3: SDMM PIII (shop, decouple doors, mixed, palettes, intensity)
# keydrop now has it's own byte
mixed_travel_mode = {"prevent": 0, "allow": 1, "force": 2}
# intensity is 3 bits (reserves 4-7 levels)
# new byte 4: ?DDD PPPP (unused, drop, pottery)
# dropshuffle reserves 2 bits, pottery needs 2 but reserves 2 for future modes)
# new byte 4: TDDD PPPP (tavern shuffle, drop, pottery)
# dropshuffle reserves 2 bits, pottery needs 4)
pottery_mode = {'none': 0, 'keys': 2, 'lottery': 3, 'dungeon': 4, 'cave': 5, 'cavekeys': 6, 'reduced': 7,
'clustered': 8, 'nonempty': 9}
@@ -3342,7 +3363,8 @@ counter_mode = {"default": 0, "off": 1, "on": 2, "pickup": 3}
# byte 6: CCCC CPAA (crystals ganon, pyramid, access
access_mode = {"items": 0, "locations": 1, "none": 2}
# byte 7: BSMC ??EE (big, small, maps, compass, bosses, enemies)
# byte 7: B?MC DDEE (big, ?, maps, compass, door_type, enemies)
door_type_mode = {'original': 0, 'big': 1, 'all': 2, 'chaos': 3}
enemy_mode = {"none": 0, "shuffled": 1, "chaos": 2, "random": 2, "legacy": 3}
# byte 8: HHHD DPBS (enemy_health, enemy_dmg, potshuffle, bomb logic, shuffle links)
@@ -3356,12 +3378,27 @@ rb_mode = {"none": 0, "mapcompass": 1, "dungeon": 2}
algo_mode = {"balanced": 0, "equitable": 1, "vanilla_fill": 2, "dungeon_only": 3, "district": 4, 'major_only': 5}
boss_mode = {"none": 0, "simple": 1, "full": 2, "chaos": 3, 'random': 3, 'unique': 4}
# byte 10: settings_version
# byte 11: OOOT WCCC (OWR layout, free terrain, whirlpools, OWR crossed)
or_mode = {"vanilla": 0, "parallel": 1, "full": 2}
orcrossed_mode = {"none": 0, "polar": 1, "grouped": 2, "limited": 3, "chaos": 4}
# byte 12: KMB? FF?? (keep similar, mixed/tile flip, bonk drops, flute spots)
flutespot_mode = {"vanilla": 0, "balanced": 1, "random": 2}
# byte 13: 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}
# additions
# psuedoboots does not effect code
# sfx_shuffle and other adjust items does not effect settings code
# Bump this when making changes that are not backwards compatible (nearly all of them)
settings_version = 0
settings_version = 1
class Settings(object):
@@ -3369,43 +3406,52 @@ class Settings(object):
@staticmethod
def make_code(w, p):
code = bytes([
(dr_mode[w.doorShuffle[p]] << 6) | (or_mode[w.owShuffle[p]] << 5) | (0x10 if w.owCrossed[p] != 'none' else 0) | (0x08 if w.owMixed[p] else 0) | er_mode[w.shuffle[p]],
(dr_mode[w.doorShuffle[p]] << 5) | er_mode[w.shuffle[p]],
(logic_mode[w.logic[p]] << 5) | (world_mode[w.mode[p]] << 3)
| (sword_mode[w.swords[p]] << 1) | (1 if w.retro[p] else 0),
| (sword_mode[w.swords[p]] << 1),
(goal_mode[w.goal[p]] << 5) | (diff_mode[w.difficulty[p]] << 3)
| (func_mode[w.difficulty_adjustments[p]] << 1) | (1 if w.hints[p] else 0),
(0x80 if w.shopsanity[p] else 0) | (mixed_travel_mode[w.mixed_travel[p]] << 4)
(0x80 if w.shopsanity[p] else 0) | (0x40 if w.decoupledoors[p] else 0)
| (mixed_travel_mode[w.mixed_travel[p]] << 4)
| (0x8 if w.standardize_palettes[p] == "original" else 0)
| (0 if w.intensity[p] == "random" else w.intensity[p]),
(0x10 if w.dropshuffle[p] else 0) | (pottery_mode[w.pottery[p]]),
(0x80 if w.shuffletavern[p] else 0) | (0x10 if w.dropshuffle[p] else 0) | (pottery_mode[w.pottery[p]]),
((8 if w.crystals_gt_orig[p] == "random" else int(w.crystals_gt_orig[p])) << 3)
| (counter_mode[w.dungeon_counters[p]] << 1) | (1 if w.experimental[p] else 0),
((8 if w.crystals_ganon_orig[p] == "random" else int(w.crystals_ganon_orig[p])) << 3)
| (0x4 if w.is_pyramid_open(p) else 0) | access_mode[w.accessibility[p]],
| (0x4 if w.open_pyramid[p] else 0) | access_mode[w.accessibility[p]],
(0x80 if w.bigkeyshuffle[p] else 0) | (0x40 if w.keyshuffle[p] else 0)
(0x80 if w.bigkeyshuffle[p] else 0)
| (0x20 if w.mapshuffle[p] else 0) | (0x10 if w.compassshuffle[p] else 0)
| (enemy_mode[w.enemy_shuffle[p]]),
| (door_type_mode[w.door_type_mode[p]] << 2) | (enemy_mode[w.enemy_shuffle[p]]),
(e_health[w.enemy_health[p]] << 5) | (e_dmg[w.enemy_damage[p]] << 3) | (0x4 if w.potshuffle[p] else 0)
| (0x2 if w.bombbag[p] else 0) | (1 if w.shufflelinks[p] else 0),
(rb_mode[w.restrict_boss_items[p]] << 6) | (algo_mode[w.algorithm] << 3) | (boss_mode[w.boss_shuffle[p]]),
settings_version])
settings_version,
(or_mode[w.owShuffle[p]] << 5) | (0x10 if w.owTerrain[p] else 0) | (0x08 if w.owWhirlpoolShuffle[p] else 0) | orcrossed_mode[w.owCrossed[p]],
(0x80 if w.owKeepSimilar[p] else 0) | (0x40 if w.owMixed[p] else 0) | (0x20 if w.shuffle_bonk_drops[p] else 0) | (flutespot_mode[w.owFluteShuffle[p]] << 4),
(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]])
])
return base64.b64encode(code, "+-".encode()).decode()
@staticmethod
def adjust_args_from_code(code, player, args):
settings, p = base64.b64decode(code.encode(), "+-".encode()), player
if len(settings) < 11:
if len(settings) < 14:
raise Exception('Provided code is incompatible with this version')
if settings[10] != settings_version:
raise Exception('Provided code is incompatible with this version')
@@ -3413,9 +3459,8 @@ class Settings(object):
def r(d):
return {y: x for x, y in d.items()}
args.shuffle[p] = r(er_mode)[settings[0] & 0x0F]
args.ow_shuffle[p] = r(or_mode)[(settings[0] & 0x30) >> 4]
args.door_shuffle[p] = r(dr_mode)[(settings[0] & 0xC0) >> 6]
args.shuffle[p] = r(er_mode)[settings[0] & 0x1F]
args.door_shuffle[p] = r(dr_mode)[(settings[0] & 0xE0) >> 5]
args.logic[p] = r(logic_mode)[(settings[1] & 0xE0) >> 5]
args.mode[p] = r(world_mode)[(settings[1] & 0x18) >> 3]
args.swords[p] = r(sword_mode)[(settings[1] & 0x6) >> 1]
@@ -3423,16 +3468,16 @@ class Settings(object):
args.item_functionality[p] = r(func_mode)[(settings[2] & 0x6) >> 1]
args.goal[p] = r(goal_mode)[(settings[2] & 0xE0) >> 5]
args.accessibility[p] = r(access_mode)[settings[6] & 0x3]
args.retro[p] = True if settings[1] & 0x01 else False
# args.retro[p] = True if settings[1] & 0x01 else False
args.hints[p] = True if settings[2] & 0x01 else False
args.shopsanity[p] = True if settings[3] & 0x80 else False
# args.keydropshuffle[p] = True if settings[3] & 0x40 else False
args.decoupledoors[p] = True if settings[3] & 0x40 else False
args.mixed_travel[p] = r(mixed_travel_mode)[(settings[3] & 0x30) >> 4]
args.standardize_palettes[p] = "original" if settings[3] & 0x8 else "standardize"
intensity = settings[3] & 0x7
args.intensity[p] = "random" if intensity == 0 else intensity
# args.shuffleswitches[p] = True if settings[4] & 0x80 else False
args.shuffletavern[p] = True if settings[4] & 0x80 else False
args.dropshuffle[p] = True if settings[4] & 0x10 else False
args.pottery[p] = r(pottery_mode)[settings[4] & 0x0F]
@@ -3446,10 +3491,10 @@ class Settings(object):
args.openpyramid[p] = True if settings[6] & 0x4 else False
args.bigkeyshuffle[p] = True if settings[7] & 0x80 else False
args.keyshuffle[p] = True if settings[7] & 0x40 else False
# args.keyshuffle[p] = True if settings[7] & 0x40 else False
args.mapshuffle[p] = True if settings[7] & 0x20 else False
args.compassshuffle[p] = True if settings[7] & 0x10 else False
# args.shufflebosses[p] = r(boss_mode)[(settings[7] & 0xc) >> 2]
args.door_type_mode[p] = r(door_type_mode)[(settings[7] & 0xc) >> 2]
args.shuffleenemies[p] = r(enemy_mode)[settings[7] & 0x3]
args.enemy_health[p] = r(e_health)[(settings[8] & 0xE0) >> 5]
@@ -3457,11 +3502,28 @@ class Settings(object):
args.shufflepots[p] = True if settings[8] & 0x4 else False
args.bombbag[p] = True if settings[8] & 0x2 else False
args.shufflelinks[p] = True if settings[8] & 0x1 else False
if len(settings) > 9:
args.restrict_boss_items[p] = r(rb_mode)[(settings[9] & 0xC0) >> 6]
args.algorithm = r(algo_mode)[(settings[9] & 0x38) >> 3]
args.shufflebosses[p] = r(boss_mode)[(settings[9] & 0x07)]
args.ow_shuffle[p] = r(or_mode)[(settings[11] & 0xE0) >> 5]
args.ow_terrain[p] = True if settings[11] & 0x10 else False
args.ow_whirlpool[p] = True if settings[11] & 0x08 else False
args.ow_crossed[p] = r(orcrossed_mode)[(settings[11] & 0x07)]
args.ow_keepsimilar[p] = True if settings[12] & 0x80 else False
args.ow_mixed[p] = True if settings[12] & 0x40 else False
args.bonk_drops[p] = True if settings[12] & 0x20 else False
args.ow_fluteshuffle[p] = r(flutespot_mode)[(settings[12] & 0x0C) >> 2]
if len(settings) > 13:
args.flute_mode[p] = r(flute_mode)[(settings[13] & 0x80) >> 7]
args.bow_mode[p] = r(bow_mode)[(settings[13] & 0x70) >> 4]
args.take_any[p] = r(take_any_mode)[(settings[13] & 0xC) >> 2]
args.keyshuffle[p] = r(keyshuffle_mode)[settings[13] & 0x3]
class KeyRuleType(FastEnum):
WorstCase = 0
+21
View File
@@ -164,6 +164,22 @@ def place_bosses(world, player):
all_bosses = sorted(boss_table.keys()) #s orted to be deterministic on older pythons
placeable_bosses = [boss for boss in all_bosses if boss not in ['Agahnim', 'Agahnim2', 'Ganon']]
used_bosses = []
if world.customizer and world.customizer.get_bosses():
custom_bosses = world.customizer.get_bosses()
if player in custom_bosses:
for location, boss in custom_bosses[player].items():
level = None
if '(' in location:
i = location.find('(')
level = location[i+1:location.find(')')]
location = location[:i-1]
if can_place_boss(world, player, boss, location, level):
loc_text = location + (' ('+level+')' if level else '')
place_boss(boss, level, location, loc_text, world, player)
boss_locations.remove([location, level])
used_bosses.append((boss, level))
# temporary hack for swordless kholdstare:
if world.boss_shuffle[player] in ["simple", "full", "unique"]:
@@ -178,6 +194,8 @@ def place_bosses(world, player):
bosses = placeable_bosses + ['Armos Knights', 'Lanmolas', 'Moldorm']
else: # all bosses present, the three duplicates chosen at random
bosses = placeable_bosses + random.sample(placeable_bosses, 3)
for u, level in used_bosses:
placeable_bosses.remove(u)
logging.getLogger('').debug('Bosses chosen %s', bosses)
@@ -201,6 +219,9 @@ def place_bosses(world, player):
place_boss(boss, level, loc, loc_text, world, player)
elif world.boss_shuffle[player] == 'unique':
bosses = list(placeable_bosses)
for u, level in used_bosses:
if not level:
bosses.remove(u)
gt_bosses = []
for [loc, level] in boss_locations:
+52 -18
View File
@@ -9,6 +9,7 @@ import sys
from source.classes.BabelFish import BabelFish
from Utils import update_deprecated_args
from source.classes.CustomSettings import CustomSettings
class ArgumentDefaultsHelpFormatter(argparse.RawTextHelpFormatter):
@@ -32,11 +33,25 @@ def parse_cli(argv, no_defaults=False):
parser = argparse.ArgumentParser(add_help=False)
parser.add_argument('--settingsfile', help="input json file of settings", type=str)
parser.add_argument('--multi', default=defval(settings["multi"]), type=lambda value: min(max(int(value), 1), 255))
parser.add_argument('--customizer', help='input yaml file for customizations', type=str)
parser.add_argument('--print_custom_yaml', help='print example yaml for current settings',
default=False, action="store_true")
parser.add_argument('--mystery', dest="mystery", default=False, action="store_true")
multiargs, _ = parser.parse_known_args(argv)
if multiargs.settingsfile:
settings = apply_settings_file(settings, multiargs.settingsfile)
player_num = multiargs.multi
if multiargs.customizer:
custom = CustomSettings()
custom.load_yaml(multiargs.customizer)
cp = custom.determine_players()
if cp:
player_num = cp
parser = argparse.ArgumentParser(formatter_class=ArgumentDefaultsHelpFormatter)
# get args
@@ -78,38 +93,50 @@ def parse_cli(argv, no_defaults=False):
parser.add_argument('--securerandom', default=defval(settings["securerandom"]), action='store_true')
parser.add_argument('--teams', default=defval(1), type=lambda value: max(int(value), 1))
parser.add_argument('--settingsfile', dest="filename", help="input json file of settings", type=str)
parser.add_argument('--customizer', dest="customizer", help='input yaml file for customizations', type=str)
parser.add_argument('--print_custom_yaml', dest="print_custom_yaml", default=False, action="store_true")
if multiargs.multi:
for player in range(1, multiargs.multi + 1):
if player_num:
for player in range(1, player_num + 1):
parser.add_argument(f'--p{player}', default=defval(''), help=argparse.SUPPRESS)
ret = parser.parse_args(argv)
if ret.keysanity:
ret.mapshuffle, ret.compassshuffle, ret.keyshuffle, ret.bigkeyshuffle = [True] * 4
ret.mapshuffle, ret.compassshuffle, ret.bigkeyshuffle = [True] * 3
ret.keyshuffle = 'wild'
if ret.keydropshuffle:
ret.dropshuffle = True
ret.pottery = 'keys' if ret.pottery == 'none' else ret.pottery
if multiargs.multi:
if ret.retro or ret.mode == 'retro':
if ret.bow_mode == 'progressive':
ret.bow_mode = 'retro'
elif ret.bow_mode == 'silvers':
ret.bow_mode = 'retro_silvers'
ret.take_any = 'random' if ret.take_any == 'none' else ret.take_any
ret.keyshuffle = 'universal'
if player_num:
defaults = copy.deepcopy(ret)
for player in range(1, multiargs.multi + 1):
for player in range(1, player_num + 1):
playerargs = parse_cli(shlex.split(getattr(ret, f"p{player}")), True)
for name in ['logic', 'mode', 'swords', 'goal', 'difficulty', 'item_functionality', 'ow_shuffle',
'ow_terrain', 'ow_crossed', 'ow_keepsimilar', 'ow_mixed', 'ow_whirlpool', 'ow_fluteshuffle',
'flute_mode', 'bow_mode', 'take_any', 'boots_hint',
'shuffle', 'door_shuffle', 'intensity', 'crystals_ganon', 'crystals_gt', 'openpyramid',
'mapshuffle', 'compassshuffle', 'keyshuffle', 'bigkeyshuffle', 'startinventory',
'usestartinventory', 'bombbag', 'shuffleganon', 'overworld_map', 'restrict_boss_items',
'triforce_pool_min', 'triforce_pool_max', 'triforce_goal_min', 'triforce_goal_max',
'triforce_min_difference', 'triforce_goal', 'triforce_pool', 'shufflelinks', 'pseudoboots',
'retro', 'accessibility', 'hints', 'beemizer', 'experimental', 'dungeon_counters',
'triforce_min_difference', 'triforce_goal', 'triforce_pool', 'shufflelinks', 'shuffletavern',
'pseudoboots', 'retro', 'accessibility', 'hints', 'beemizer', 'experimental', 'dungeon_counters',
'shufflebosses', 'shuffleenemies', 'enemy_health', 'enemy_damage', 'shufflepots',
'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', 'bonk_drops']:
'msu_resume', 'collection_rate', 'colorizepots', 'decoupledoors', 'door_type_mode', 'bonk_drops']:
value = getattr(defaults, name) if getattr(playerargs, name) is None else getattr(playerargs, name)
if player == 1:
setattr(ret, name, {1: value})
@@ -135,19 +162,22 @@ def parse_settings():
"retro": False,
"bombbag": False,
"mode": "open",
"boots_hint": False,
"logic": "noglitches",
"goal": "ganon",
"crystals_gt": "7",
"crystals_ganon": "7",
"swords": "random",
"flute_mode": "normal",
"bow_mode": "progressive",
"difficulty": "normal",
"item_functionality": "normal",
"timer": "none",
"progressive": "on",
"accessibility": "items",
"algorithm": "balanced",
'mystery': False,
'suppress_meta': False,
"mystery": False,
"suppress_meta": False,
"restrict_boss_items": "none",
# Shuffle Ganon defaults to TRUE
@@ -163,7 +193,9 @@ def parse_settings():
"bonk_drops": False,
"shuffle": "vanilla",
"shufflelinks": False,
"shuffletavern": False,
"overworld_map": "default",
"take_any": "none",
"pseudoboots": False,
"shuffleenemies": "none",
@@ -173,18 +205,20 @@ def parse_settings():
"enemizercli": os.path.join(".", "EnemizerCLI", "EnemizerCLI.Core"),
"shopsanity": False,
'keydropshuffle': False,
'dropshuffle': False,
'pottery': 'none',
'colorizepots': False,
'shufflepots': False,
"keydropshuffle": False,
"dropshuffle": False,
"pottery": "none",
"colorizepots": False,
"shufflepots": False,
"mapshuffle": False,
"compassshuffle": False,
"keyshuffle": False,
"keyshuffle": "none",
"bigkeyshuffle": False,
"keysanity": False,
"door_shuffle": "vanilla",
"intensity": 3,
"door_type_mode": "original",
"decoupledoors": False,
"experimental": False,
"dungeon_counters": "default",
"mixed_travel": "prevent",
@@ -214,8 +248,8 @@ def parse_settings():
"uw_palettes": "default",
"reduce_flashing": False,
"shuffle_sfx": False,
'msu_resume': False,
'collection_rate': False,
"msu_resume": False,
"collection_rate": False,
# Spoiler defaults to TRUE
# Playthrough defaults to TRUE
+1593 -238
View File
File diff suppressed because it is too large Load Diff
+24 -16
View File
@@ -104,8 +104,10 @@ def create_doors(world, player):
create_door(player, 'Sewers Dark Cross Key Door N', Nrml).dir(No, 0x32, Mid, High).small_key().pos(0),
create_door(player, 'Sewers Water S', Nrml).dir(So, 0x22, Mid, High).small_key().pos(0).portal(Z, 0x22),
create_door(player, 'Sewers Water W', Nrml).dir(We, 0x22, Bot, High).pos(1),
create_door(player, 'Sewers Key Rat E', Nrml).dir(Ea, 0x21, Bot, High).pos(1),
create_door(player, 'Sewers Key Rat Key Door N', Nrml).dir(No, 0x21, Right, High).small_key().pos(0),
create_door(player, 'Sewers Dark Aquabats ES', Nrml).dir(Ea, 0x21, Bot, High).pos(2),
create_door(player, 'Sewers Dark Aquabats N', Intr).dir(No, 0x21, Mid, High).pos(1),
create_door(player, 'Sewers Key Rat S', Intr).dir(So, 0x21, Mid, High).pos(1),
create_door(player, 'Sewers Key Rat NE', Nrml).dir(No, 0x21, Right, High).small_key().pos(0),
create_door(player, 'Sewers Secret Room Key Door S', Nrml).dir(So, 0x11, Right, High).small_key().pos(2).portal(X, 0x02),
create_door(player, 'Sewers Rat Path WS', Intr).dir(We, 0x11, Bot, High).pos(1),
create_door(player, 'Sewers Rat Path WN', Intr).dir(We, 0x11, Top, High).pos(0),
@@ -348,7 +350,7 @@ def create_doors(world, player):
create_door(player, 'Tower Catwalk North Stairs', StrS).dir(No, 0x40, Left, High),
create_door(player, 'Tower Antechamber South Stairs', StrS).dir(So, 0x30, Left, High),
create_door(player, 'Tower Antechamber NW', Intr).dir(No, 0x30, Left, High).pos(1),
create_door(player, 'Tower Altar SW', Intr).dir(So, 0x30, Left, High).no_exit().pos(1),
create_door(player, 'Tower Altar SW', Intr).dir(So, 0x30, Left, High).no_exit().trap(0x2).pos(1),
create_door(player, 'Tower Altar NW', Nrml).dir(No, 0x30, Left, High).pos(0),
create_door(player, 'Tower Agahnim 1 SW', Nrml).dir(So, 0x20, Left, High).no_exit().trap(0x4).pos(0),
@@ -667,7 +669,7 @@ def create_doors(world, player):
create_door(player, 'Thieves Conveyor Maze SW', Intr).dir(So, 0xbc, Left, High).pos(6),
create_door(player, 'Thieves Pot Alcove Top NW', Intr).dir(No, 0xbc, Left, High).pos(6),
create_door(player, 'Thieves Conveyor Maze EN', Intr).dir(Ea, 0xbc, Top, High).pos(2),
create_door(player, 'Thieves Hallway WN', Intr).dir(We, 0xbc, Top, High).no_exit().pos(2),
create_door(player, 'Thieves Hallway WN', Intr).dir(We, 0xbc, Top, High).no_exit().trap(0x1).pos(2),
create_door(player, 'Thieves Conveyor Maze Down Stairs', Sprl).dir(Dn, 0xbc, 0, HTH).ss(A, 0x11, 0x80, True, True),
create_door(player, 'Thieves Boss SE', Nrml).dir(So, 0xac, Right, High).no_exit().trap(0x4).pos(0),
create_door(player, 'Thieves Spike Track ES', Nrml).dir(Ea, 0xbb, Bot, High).pos(5),
@@ -742,7 +744,7 @@ def create_doors(world, player):
create_door(player, 'Ice Big Key Push Block', Lgcl),
create_door(player, 'Ice Big Key Down Ladder', Lddr).dir(So, 0x1f, 3, High),
create_door(player, 'Ice Stalfos Hint SE', Intr).dir(So, 0x3e, Right, High).pos(0),
create_door(player, 'Ice Conveyor NE', Intr).dir(No, 0x3e, Right, High).no_exit().pos(0),
create_door(player, 'Ice Conveyor NE', Intr).dir(No, 0x3e, Right, High).no_exit().trap(0x4).pos(0),
create_door(player, 'Ice Conveyor to Crystal', Lgcl),
create_door(player, 'Ice Conveyor Crystal Exit', Lgcl),
create_door(player, 'Ice Conveyor SW', Nrml).dir(So, 0x3e, Left, High).small_key().pos(1).portal(Z, 0x20),
@@ -760,7 +762,7 @@ def create_doors(world, player):
create_door(player, 'Ice Firebar ES', Intr).dir(Ea, 0x5e, Bot, High).pos(3),
create_door(player, 'Ice Firebar Down Ladder', Lddr).dir(So, 0x5e, 5, High),
create_door(player, 'Ice Spike Cross NE', Intr).dir(No, 0x5e, Right, High).pos(1),
create_door(player, 'Ice Falling Square SE', Intr).dir(So, 0x5e, Right, High).no_exit().pos(1),
create_door(player, 'Ice Falling Square SE', Intr).dir(So, 0x5e, Right, High).no_exit().trap(0x1).pos(1),
create_door(player, 'Ice Falling Square Hole', Hole),
create_door(player, 'Ice Spike Room WS', Nrml).dir(We, 0x5f, Bot, High).small_key().pos(0),
create_door(player, 'Ice Spike Room Down Stairs', Sprl).dir(Dn, 0x5f, 3, HTH).ss(Z, 0x11, 0x48, True, True),
@@ -840,12 +842,16 @@ def create_doors(world, player):
create_door(player, 'Mire Hub Top NW', Nrml).dir(No, 0xc2, Left, High).pos(2),
create_door(player, 'Mire Lone Shooter WS', Nrml).dir(We, 0xc3, Bot, High).pos(6),
create_door(player, 'Mire Lone Shooter ES', Intr).dir(Ea, 0xc3, Bot, High).pos(3),
create_door(player, 'Mire Falling Bridge WS', Intr).dir(We, 0xc3, Bot, High).no_exit().pos(3),
create_door(player, 'Mire Falling Bridge WS', Intr).dir(We, 0xc3, Bot, High).no_exit().trap(0x8).pos(3),
create_door(player, 'Mire Falling Bridge W', Intr).dir(We, 0xc3, Mid, High).pos(2),
create_door(player, 'Mire Failure Bridge E', Intr).dir(Ea, 0xc3, Mid, High).no_exit().pos(2),
create_door(player, 'Mire Failure Bridge E', Intr).dir(Ea, 0xc3, Mid, High).no_exit().trap(0x1).pos(2),
create_door(player, 'Mire Failure Bridge W', Nrml).dir(We, 0xc3, Mid, High).pos(5),
create_door(player, 'Mire Falling Bridge WN', Intr).dir(We, 0xc3, Top, High).pos(1),
create_door(player, 'Mire Map Spike Side EN', Intr).dir(Ea, 0xc3, Top, High).no_exit().pos(1),
create_door(player, 'Mire Falling Bridge Hook Path', Lgcl),
create_door(player, 'Mire Falling Bridge Hook Only Path', Lgcl),
create_door(player, 'Mire Falling Bridge Primary Path', Lgcl), # dynamic
create_door(player, 'Mire Falling Bridge Failure Path', Lgcl),
create_door(player, 'Mire Map Spike Side EN', Intr).dir(Ea, 0xc3, Top, High).no_exit().trap(0x2).pos(1),
create_door(player, 'Mire Map Spot WN', Nrml).dir(We, 0xc3, Top, High).small_key().pos(0),
create_door(player, 'Mire Crystal Dead End NW', Nrml).dir(No, 0xc3, Left, High).pos(4),
create_door(player, 'Mire Map Spike Side Drop Down', Lgcl),
@@ -903,7 +909,7 @@ def create_doors(world, player):
create_door(player, 'Mire Tile Room NW', Intr).dir(No, 0xc1, Left, High).pos(3),
create_door(player, 'Mire Compass Room SW', Intr).dir(So, 0xc1, Left, High).pos(3),
create_door(player, 'Mire Compass Room EN', Intr).dir(Ea, 0xc1, Top, High).pos(2),
create_door(player, 'Mire Wizzrobe Bypass WN', Intr).dir(We, 0xc1, Top, High).no_exit().pos(2),
create_door(player, 'Mire Wizzrobe Bypass WN', Intr).dir(We, 0xc1, Top, High).no_exit().trap(0x1).pos(2),
create_door(player, 'Mire Compass Blue Barrier', Lgcl),
create_door(player, 'Mire Compass Chest Exit', Lgcl),
create_door(player, 'Mire Neglected Room NE', Nrml).dir(No, 0xd1, Right, High).pos(2),
@@ -912,7 +918,7 @@ def create_doors(world, player):
create_door(player, 'Mire Neglected Room SE', Intr).dir(So, 0xd1, Right, High).pos(3),
create_door(player, 'Mire Chest View NE', Intr).dir(No, 0xd1, Right, High).pos(3),
create_door(player, 'Mire BK Chest Ledge WS', Intr).dir(We, 0xd1, Bot, High).pos(0),
create_door(player, 'Mire Warping Pool ES', Intr).dir(Ea, 0xd1, Bot, High).no_exit().pos(0),
create_door(player, 'Mire Warping Pool ES', Intr).dir(Ea, 0xd1, Bot, High).no_exit().trap(0x4).pos(0),
create_door(player, 'Mire Warping Pool Warp', Warp),
create_door(player, 'Mire Torches Top Down Stairs', Sprl).dir(Dn, 0x97, 0, HTH).ss(A, 0x11, 0xb0, True).kill(),
create_door(player, 'Mire Torches Top SW', Intr).dir(So, 0x97, Left, High).pos(1),
@@ -1011,7 +1017,7 @@ def create_doors(world, player):
create_door(player, 'TR Big Chest Entrance SE', Nrml).dir(So, 0x24, Right, High).pos(4).kill().portal(X, 0x00),
create_door(player, 'TR Big Chest Entrance Gap', Lgcl),
create_door(player, 'TR Big Chest NE', Intr).dir(No, 0x24, Right, High).pos(3),
create_door(player, 'TR Dodgers SE', Intr).dir(So, 0x24, Right, High).no_exit().pos(3),
create_door(player, 'TR Dodgers SE', Intr).dir(So, 0x24, Right, High).no_exit().trap(0x8).pos(3),
create_door(player, 'TR Dodgers NE', Nrml).dir(No, 0x24, Right, High).big_key().pos(0),
create_door(player, 'TR Lazy Eyes SE', Nrml).dir(So, 0x23, Right, High).pos(0).portal(X, 0x00),
create_door(player, 'TR Lazy Eyes ES', Nrml).dir(Ea, 0x23, Bot, High).pos(1),
@@ -1073,7 +1079,7 @@ def create_doors(world, player):
create_door(player, 'GT Hope Room EN', Nrml).dir(Ea, 0x8c, Top, High).trap(0x4).pos(0),
create_door(player, 'GT Torch EN', Intr).dir(Ea, 0x8c, Top, High).small_key().pos(2),
create_door(player, 'GT Hope Room WN', Intr).dir(We, 0x8c, Top, High).small_key().pos(2),
create_door(player, 'GT Torch SW', Intr).dir(So, 0x8c, Left, High).no_exit().pos(1),
create_door(player, 'GT Torch SW', Intr).dir(So, 0x8c, Left, High).no_exit().trap(0x2).pos(1),
create_door(player, 'GT Big Chest NW', Intr).dir(No, 0x8c, Left, High).pos(1),
create_door(player, 'GT Blocked Stairs Down Stairs', Sprl).dir(Dn, 0x8c, 3, HTH).ss(Z, 0x12, 0x40, True, True).kill(),
create_door(player, 'GT Blocked Stairs Block Path', Lgcl),
@@ -1179,7 +1185,7 @@ def create_doors(world, player):
create_door(player, 'GT Ice Armos NE', Intr).dir(No, 0x1c, Right, High).pos(0),
create_door(player, 'GT Big Key Room SE', Intr).dir(So, 0x1c, Right, High).pos(0),
create_door(player, 'GT Ice Armos WS', Intr).dir(We, 0x1c, Bot, High).pos(1),
create_door(player, 'GT Four Torches ES', Intr).dir(Ea, 0x1c, Bot, High).no_exit().pos(1),
create_door(player, 'GT Four Torches ES', Intr).dir(Ea, 0x1c, Bot, High).no_exit().trap(0x2).pos(1),
create_door(player, 'GT Four Torches NW', Intr).dir(No, 0x1c, Left, High).pos(2),
create_door(player, 'GT Fairy Abyss SW', Intr).dir(So, 0x1c, Left, High).pos(2),
create_door(player, 'GT Four Torches Up Stairs', Sprl).dir(Up, 0x1c, 0, HTH).ss(Z, 0x1b, 0x2c, True, True),
@@ -1211,7 +1217,7 @@ def create_doors(world, player):
create_door(player, 'GT Beam Dash WS', Intr).dir(We, 0x6c, Bot, High).pos(0),
create_door(player, 'GT Lanmolas 2 ES', Intr).dir(Ea, 0x6c, Bot, High).pos(0),
create_door(player, 'GT Lanmolas 2 NW', Intr).dir(No, 0x6c, Left, High).pos(1),
create_door(player, 'GT Quad Pot SW', Intr).dir(So, 0x6c, Left, High).no_exit().pos(1),
create_door(player, 'GT Quad Pot SW', Intr).dir(So, 0x6c, Left, High).no_exit().trap(0x2).pos(1),
create_door(player, 'GT Quad Pot Up Stairs', Sprl).dir(Up, 0x6c, 0, HTH).ss(A, 0x1b, 0x6c, True, True),
create_door(player, 'GT Wizzrobes 1 Down Stairs', Sprl).dir(Dn, 0xa5, 0, HTH).ss(A, 0x12, 0x80, True, True),
create_door(player, 'GT Wizzrobes 1 SW', Intr).dir(So, 0xa5, Left, High).pos(2),
@@ -1480,6 +1486,8 @@ def create_doors(world, player):
controller_door(south_controller, world.get_door('Ice Cross Top Push Block Bottom', player))
controller_door(east_controller, world.get_door('Ice Cross Bottom Push Block Right', player))
controller_door(east_controller, world.get_door('Ice Cross Top Push Block Right', player))
primary_controller = world.get_door('Mire Falling Bridge WS', player)
controller_door(primary_controller, world.get_door('Mire Falling Bridge Primary Path', player))
assign_entrances(world, player)
@@ -1572,7 +1580,7 @@ def reset_portals(world, player):
def create_paired_doors(world, player):
world.paired_doors[player] = [
PairedDoor('Sewers Secret Room Key Door S', 'Sewers Key Rat Key Door N', True),
PairedDoor('Sewers Secret Room Key Door S', 'Sewers Key Rat NE', True),
PairedDoor('TR Pokey 2 ES', 'TR Lava Island WS', True), # TR Pokey Key
PairedDoor('TR Dodgers NE', 'TR Lava Escape SE', True), # TR Big key door by pipes
PairedDoor('PoD Falling Bridge WN', 'PoD Dark Maze EN', True), # Pod Dark maze door
+204 -71
View File
@@ -10,11 +10,14 @@ 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 RoomData import DoorKind
from source.dungeon.DungeonStitcher import generate_dungeon_find_proposal
from source.dungeon.DungeonStitcher import GenerationException as OtherGenException
class GraphPiece:
@@ -43,7 +46,10 @@ def pre_validate(builder, entrance_region_names, split_dungeon, world, player):
for sector in builder.sectors:
for door in sector.outstanding_doors:
doors_to_connect[door.name] = door
all_regions.update(sector.regions)
if world.mode[player] == 'standard' and builder.name == 'Hyrule Castle Dungeon':
all_regions.update([x for x in sector.regions if x.name != 'Hyrule Castle Behind Tapestry'])
else:
all_regions.update(sector.regions)
bk_special |= check_for_special(sector.regions)
bk_needed = False
for sector in builder.sectors:
@@ -59,7 +65,7 @@ def generate_dungeon(builder, entrance_region_names, split_dungeon, world, playe
if builder.valid_proposal: # we made this earlier in gen, just use it
proposed_map = builder.valid_proposal
else:
proposed_map = generate_dungeon_find_proposal(builder, entrance_region_names, split_dungeon, world, player)
proposed_map = generate_dungeon_find_proposal_old(builder, entrance_region_names, split_dungeon, world, player)
builder.valid_proposal = proposed_map
queue = collections.deque(proposed_map.items())
while len(queue) > 0:
@@ -77,7 +83,7 @@ def generate_dungeon(builder, entrance_region_names, split_dungeon, world, playe
return master_sector
def generate_dungeon_find_proposal(builder, entrance_region_names, split_dungeon, world, player):
def generate_dungeon_find_proposal_old(builder, entrance_region_names, split_dungeon, world, player):
logger = logging.getLogger('')
name = builder.name
entrance_regions = convert_regions(entrance_region_names, world, player)
@@ -91,18 +97,27 @@ def generate_dungeon_find_proposal(builder, entrance_region_names, split_dungeon
p_region = portal.door.entrance.connected_region
access_region = next(x.parent_region for x in p_region.entrances
if x.parent_region.type in [RegionType.LightWorld, RegionType.DarkWorld])
if ((access_region.name in world.inaccessible_regions[player] and
region.name not in world.enabled_entrances[player])
or (world.mode[player] == 'standard' and access_region.name != 'Hyrule Castle Courtyard'
and 'Hyrule Castle' in builder.name)):
excluded[region] = None
else: # for non-portals, holes and sewers in std
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'), None)
if access_region is None:
if builder.sewers_access is None:
excluded[region] = None
else:
if access_region.name == 'Sewer Drop':
if world.mode[player] == 'standard' and (builder.sewers_access is None
or builder.sewers_access.entrance.parent_region != region):
excluded[region] = None
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]):
excluded[region] = None
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]):
excluded[region] = None
entrance_regions = [x for x in entrance_regions if x not in excluded.keys()]
doors_to_connect = {}
all_regions = set()
@@ -143,7 +158,11 @@ def generate_dungeon_find_proposal(builder, entrance_region_names, split_dungeon
dungeon, hangers, hooks = gen_dungeon_info(name, builder.sectors, entrance_regions, all_regions, proposed_map,
doors_to_connect, bk_needed, bk_special, world, player)
dungeon_cache[depth] = dungeon, hangers, hooks
valid = check_valid(name, dungeon, hangers, hooks, proposed_map, doors_to_connect, all_regions,
if len(proposed_map) != len(doors_to_connect) and builder.name == 'Hyrule Castle Dungeon':
check_regions = all_regions.difference({world.get_region('Hyrule Castle Behind Tapestry', player)})
else:
check_regions = all_regions
valid = check_valid(name, dungeon, hangers, hooks, proposed_map, doors_to_connect, check_regions,
bk_needed, bk_special, paths, entrance_regions, world, player)
else:
dungeon, hangers, hooks = dungeon_cache[depth]
@@ -565,9 +584,15 @@ 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':
paths.append('Hyrule Dungeon Cellblock')
paths.append(('Hyrule Dungeon Cellblock', 'Sanctuary'))
if world.mode[player] == 'standard':
if name == 'Hyrule Castle':
paths.append('Hyrule Dungeon Cellblock')
paths.append(('Hyrule Dungeon Cellblock', 'Sanctuary'))
if name == 'Hyrule Castle Sewers':
paths.append('Sanctuary')
if name == 'Hyrule Castle Dungeon':
paths.append('Hyrule Dungeon Cellblock')
paths.append(('Hyrule Dungeon Cellblock', 'Hyrule Castle Throne Room'))
if world.doorShuffle[player] in ['basic'] and name == 'Thieves Town':
paths.append('Thieves Attic Window')
elif 'Thieves Attic Window' in all_r_names:
@@ -786,6 +811,7 @@ class ExplorationState(object):
self.prize_door_set = {}
self.prize_doors = []
self.prize_doors_opened = False
self.prize_received = False
def copy(self):
ret = ExplorationState(dungeon=self.dungeon)
@@ -817,8 +843,17 @@ class ExplorationState(object):
ret.prize_door_set = dict(self.prize_door_set)
ret.prize_doors = list(self.prize_doors)
ret.prize_doors_opened = self.prize_doors_opened
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()
@@ -844,9 +879,9 @@ class ExplorationState(object):
self.key_locations += 1
if location.name not in dungeon_events and '- Prize' not in location.name and location.name not in ['Agahnim 1', 'Agahnim 2']:
self.ttl_locations += 1
if location not in self.found_locations: # todo: special logic for TT Boss?
if location not in self.found_locations:
self.found_locations.append(location)
if not bk_Flag:
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):
@@ -962,6 +997,20 @@ class ExplorationState(object):
elif not self.in_door_list(door, self.avail_doors):
self.append_door_to_list(door, self.avail_doors)
def add_all_doors_check_big_keys(self, region, big_key_door_proposal, world, player):
for door in get_doors(world, region, player):
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 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:
if 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 visited(self, region):
if self.crystal == CrystalBarrier.Either:
return region in self.visited_blue and region in self.visited_orange
@@ -1161,6 +1210,7 @@ class DungeonBuilder(object):
self.name = name
self.sectors = []
self.location_cnt = 0
self.location_set = set()
self.key_drop_cnt = 0
self.dungeon_items = None # during fill how many dungeon items are left
self.free_items = None # during fill how many dungeon items are left
@@ -1191,6 +1241,8 @@ class DungeonBuilder(object):
self.combo_size = None
self.flex = 0
self.key_door_proposal = None
self.bk_door_proposal = None
self.trap_door_proposal = None
self.allowance = None
if 'Stonewall' in name:
@@ -1206,6 +1258,11 @@ class DungeonBuilder(object):
self.split_dungeon_map = None
self.exception_list = []
self.throne_door = None
self.throne_sector = None
self.chosen_lobby = None
self.sewers_access = None
def polarity_complement(self):
pol = Polarity()
for sector in self.sectors:
@@ -1235,7 +1292,7 @@ def simple_dungeon_builder(name, sector_list):
return builder
def create_dungeon_builders(all_sectors, connections_tuple, world, player,
def create_dungeon_builders(all_sectors, connections_tuple, world, player, dungeon_pool,
dungeon_entrances=None, split_dungeon_entrances=None):
logger = logging.getLogger('')
logger.info('Shuffling Dungeon Sectors')
@@ -1251,14 +1308,12 @@ def create_dungeon_builders(all_sectors, connections_tuple, world, player,
global_pole = GlobalPolarity(candidate_sectors)
dungeon_map = {}
for key in dungeon_regions.keys():
dungeon_map[key] = DungeonBuilder(key)
for key in dungeon_boss_sectors.keys():
current_dungeon = dungeon_map[key]
for key in dungeon_pool:
current_dungeon = dungeon_map[key] = DungeonBuilder(key)
for r_name in dungeon_boss_sectors[key]:
assign_sector(find_sector(r_name, candidate_sectors), current_dungeon, candidate_sectors, global_pole)
if key == 'Hyrule Castle' and world.mode[player] == 'standard':
for r_name in ['Hyrule Dungeon Cellblock', 'Sanctuary']: # need to deliver zelda
for r_name in ['Hyrule Dungeon Cellblock', 'Sanctuary', 'Hyrule Castle Throne Room']: # need to deliver zelda
assign_sector(find_sector(r_name, candidate_sectors), current_dungeon,
candidate_sectors, global_pole)
if key == 'Thieves Town' and world.get_dungeon("Thieves Town", player).boss.enemizer_name == 'Blind':
@@ -1266,7 +1321,7 @@ def create_dungeon_builders(all_sectors, connections_tuple, world, player,
candidate_sectors, global_pole)
entrances_map, potentials, connections = connections_tuple
accessible_sectors, reverse_d_map = set(), {}
for key in dungeon_entrances.keys():
for key in dungeon_pool:
current_dungeon = dungeon_map[key]
current_dungeon.all_entrances = dungeon_entrances[key]
for r_name in current_dungeon.all_entrances:
@@ -1286,6 +1341,10 @@ def create_dungeon_builders(all_sectors, connections_tuple, world, player,
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()]
if not dungeon_map:
dungeon_map.update(complete_dungeons)
return dungeon_map
# categorize sectors
identify_destination_sectors(accessible_sectors, reverse_d_map, dungeon_map, connections,
dungeon_entrances, split_dungeon_entrances)
@@ -1296,8 +1355,8 @@ def create_dungeon_builders(all_sectors, connections_tuple, world, player,
sanc = find_sector('Sanctuary', candidate_sectors)
if sanc: # only run if sanc if a candidate
lw_builders = []
for name, portal_list in dungeon_portals.items():
for portal_name in portal_list:
for name in dungeon_pool:
for portal_name in dungeon_portals[name]:
if world.get_portal(portal_name, player).light_world:
lw_builders.append(dungeon_map[name])
break
@@ -1305,7 +1364,8 @@ def create_dungeon_builders(all_sectors, connections_tuple, world, player,
sanc_builder = random.choice(lw_builders)
assign_sector(sanc, sanc_builder, candidate_sectors, global_pole)
bow_sectors, retro_std_flag = {}, world.retro[player] 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 = {}
@@ -1313,7 +1373,9 @@ def create_dungeon_builders(all_sectors, connections_tuple, world, player,
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:
@@ -1324,8 +1386,8 @@ def create_dungeon_builders(all_sectors, connections_tuple, world, player,
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:
@@ -1369,7 +1431,8 @@ def create_dungeon_builders(all_sectors, connections_tuple, world, player,
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)
@@ -1388,6 +1451,8 @@ def identify_destination_sectors(accessible_sectors, reverse_d_map, dungeon_map,
if ent_name in found_connections:
continue
sector = find_sector(ent_name, reverse_d_map.keys())
if sector is None:
continue
if sector in accessible_sectors:
found_connections.add(ent_name)
accessible_overworld.add(region) # todo: drops don't give ow access
@@ -1398,6 +1463,8 @@ def identify_destination_sectors(accessible_sectors, reverse_d_map, dungeon_map,
explored = False
else:
d_name = reverse_d_map[sector]
if d_name not in dungeon_map:
return
if d_name not in split_dungeon_entrances:
for r_name in dungeon_entrances[d_name]:
ent_sector = find_sector(r_name, dungeon_map[d_name].sectors)
@@ -1451,6 +1518,8 @@ def calc_allowance_and_dead_ends(builder, connections_tuple, world, player):
builder.branches -= 1
if entrance not in drop_entrances_allowance:
needed_connections.append(entrance)
if builder.sewers_access:
starting_allowance += 1
builder.allowance = starting_allowance
for entrance in needed_connections:
sector = find_sector(entrance, builder.sectors)
@@ -1493,7 +1562,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:
@@ -1505,6 +1574,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):
@@ -1518,6 +1589,7 @@ def assign_sector_helper(sector, builder):
builder.sectors.append(sector)
builder.location_cnt += sector.chest_locations
builder.key_drop_cnt += sector.key_only_locations
builder.location_set.update(sector.chest_location_set)
if sector.c_switch:
builder.c_switch_present = True
if sector.blue_barrier:
@@ -1555,8 +1627,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:
@@ -1565,7 +1637,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):
@@ -2077,16 +2149,18 @@ def assign_polarized_sectors(dungeon_map, polarized_sectors, global_pole, builde
while len(problem_builders) > 0:
for name, builder in problem_builders.items():
candidates = find_branching_candidates(builder, neutral_choices, builder_info)
valid, choice = False, None
valid, choice, package = False, None, None
while not valid:
if len(candidates) <= 0:
raise GenerationException('Cross Dungeon Builder: Complex branch problems: %s' % name)
choice = random.choice(candidates)
candidates.remove(choice)
choice, package = random.choice(candidates)
candidates.remove((choice, package))
valid = global_pole.is_valid_choice(dungeon_map, builder, choice) and valid_polarized_assignment(builder, choice)
neutral_choices.remove(choice)
for sector in choice:
assign_sector(sector, builder, polarized_sectors, global_pole)
if package:
builder.throne_door, builder.throne_sector, builder.chosen_lobby = package
builder.unfulfilled.clear()
problem_builders = identify_branching_issues(problem_builders, builder_info)
@@ -2107,16 +2181,21 @@ def assign_polarized_sectors(dungeon_map, polarized_sectors, global_pole, builde
chosen_sectors = defaultdict(list)
for i, choice in enumerate(choices):
chosen_sectors[choice].extend(neutral_choices[i])
all_valid = True
all_valid, package_map = True, {}
for name, sector_list in chosen_sectors.items():
if not valid_assignment(dungeon_map[name], sector_list, builder_info):
flag, package = valid_assignment(dungeon_map[name], sector_list, builder_info)
if not flag:
all_valid = False
break
if package:
package_map[dungeon_map[name]] = package
if all_valid:
for i, choice in enumerate(choices):
builder = dungeon_map[choice]
for sector in neutral_choices[i]:
assign_sector(sector, builder, polarized_sectors, global_pole)
if builder in package_map:
builder.throne_door, builder.throne_sector, builder.chosen_lobby = package_map[builder]
tries += 1
@@ -2629,9 +2708,9 @@ def weed_candidates(builder, candidates, best_charge):
def find_branching_candidates(builder, neutral_choices, builder_info):
candidates = []
for choice in neutral_choices:
resolved, problem_list = check_for_valid_layout(builder, choice, builder_info)
resolved, problem_list, package = check_for_valid_layout(builder, choice, builder_info)
if resolved:
candidates.append(choice)
candidates.append((choice, package))
return candidates
@@ -2786,13 +2865,13 @@ def categorize_groupings(sectors):
def valid_assignment(builder, sector_list, builder_info):
if not valid_entrance(builder, sector_list, builder_info):
return False
return False, None
if not valid_c_switch(builder, sector_list):
return False
return False, None
if not valid_polarized_assignment(builder, sector_list):
return False
resolved, problems = check_for_valid_layout(builder, sector_list, builder_info)
return resolved
return False, None
resolved, problems, package = check_for_valid_layout(builder, sector_list, builder_info)
return resolved, package
def valid_entrance(builder, sector_list, builder_info):
@@ -2898,31 +2977,56 @@ def assign_the_rest(dungeon_map, neutral_sectors, global_pole, builder_info):
chosen_sectors = defaultdict(list)
for i, choice in enumerate(choices):
chosen_sectors[choice].append(neutral_sector_list[i])
all_valid = True
all_valid, package_map = True, {}
for name, sector_list in chosen_sectors.items():
if not valid_assignment(dungeon_map[name], sector_list, builder_info):
flag, package = valid_assignment(dungeon_map[name], sector_list, builder_info)
if not flag:
all_valid = False
break
if package:
package_map[dungeon_map[name]] = package
if all_valid:
for name, sector_list in chosen_sectors.items():
builder = dungeon_map[name]
for sector in sector_list:
assign_sector(sector, builder, neutral_sectors, global_pole)
if builder in package_map:
builder.throne_door, builder.throne_sector, builder.chosen_lobby = package_map[builder]
tries += 1
def split_dungeon_builder(builder, split_list, builder_info):
ents, splits, c_tuple, world, player = builder_info
if builder.split_dungeon_map and len(builder.exception_list) == 0:
for name, proposal in builder.valid_proposal.items():
builder.split_dungeon_map[name].valid_proposal = proposal
if builder.name == 'Hyrule Castle':
builder.chosen_lobby.outstanding_doors.remove(builder.throne_door)
builder.throne_sector.outstanding_doors.remove(world.get_door('Hyrule Castle Throne Room N', player))
return builder.split_dungeon_map # we made this earlier in gen, just use it
attempts, comb_w_replace, merge_attempt, merge_limit = 0, None, 0, len(split_list) - 1
while attempts < 5: # does not solve coin flips 3% of the time
try:
candidate_sectors = dict.fromkeys(builder.sectors)
global_pole = GlobalPolarity(candidate_sectors)
if builder.name == 'Hyrule Castle':
throne_sector = find_sector('Hyrule Castle Throne Room', candidate_sectors)
chosen_lobbies = {r_name for x in split_list.values() for r_name in x}
choices = {}
for sector in candidate_sectors:
if sector.adj_outflow() > 1 and sector != throne_sector:
for door in sector.outstanding_doors:
if door.direction == Direction.South and door.entrance.parent_region not in chosen_lobbies:
choices[door] = sector
chosen_door = random.choice(list(choices.keys()))
split_list['Sewers'].append(chosen_door.entrance.parent_region.name)
choices[chosen_door].outstanding_doors.remove(chosen_door)
builder.throne_door = chosen_door
builder.throne_sector = throne_sector
builder.chosen_lobby = choices[chosen_door]
throne_sector.outstanding_doors.remove(world.get_door('Hyrule Castle Throne Room N', player))
global_pole = GlobalPolarity(candidate_sectors)
dungeon_map, sub_builder, merge_keys = {}, None, []
if merge_attempt > 0:
candidates = []
@@ -2932,7 +3036,6 @@ def split_dungeon_builder(builder, split_list, builder_info):
continue
elif len(split_entrances) <= 0:
continue
ents, splits, c_tuple, world, player = builder_info
r_name = split_entrances[0]
p = next((x for x in world.dungeon_portals[player] if x.door.entrance.parent_region.name == r_name), None)
if p and not p.deadEnd:
@@ -2953,14 +3056,29 @@ def split_dungeon_builder(builder, split_list, builder_info):
sub_builder.all_entrances = list(split_entrances)
for r_name in split_entrances:
assign_sector(find_sector(r_name, candidate_sectors), sub_builder, candidate_sectors, global_pole)
if builder.name == 'Hyrule Castle':
assign_sector(find_sector('Hyrule Castle Throne Room', candidate_sectors),
dungeon_map['Hyrule Castle Dungeon'], candidate_sectors, global_pole)
assign_sector(find_sector('Hyrule Dungeon Cellblock', candidate_sectors),
dungeon_map['Hyrule Castle Dungeon'], candidate_sectors, global_pole)
dungeon_map['Hyrule Castle Dungeon'].throne_door = world.get_door('Hyrule Castle Throne Room N', player)
dungeon_map['Hyrule Castle Sewers'].sewers_access = builder.throne_door
if len(candidate_sectors) == 0:
return dungeon_map
comb_w_replace = len(dungeon_map) ** len(candidate_sectors)
return balance_split(candidate_sectors, dungeon_map, global_pole, builder_info)
except (GenerationException, NeutralizingException):
if comb_w_replace and comb_w_replace <= 10000:
if comb_w_replace and comb_w_replace <= 10000 and not builder.throne_door:
attempts += 5 # all the combinations were tried already, no use repeating
else:
attempts += 1
if attempts >= 5 and merge_attempt < merge_limit:
if builder.throne_door:
previous = find_sector(builder.throne_door.entrance.parent_region.name, builder.sectors)
previous.outstanding_doors.append(builder.throne_door)
builder.throne_sector.outstanding_doors.append(world.get_door('Hyrule Castle Throne Room N', player))
split_list['Sewers'].remove(builder.throne_door.entrance.parent_region.name)
builder.throne_door = None
if attempts >= 5 and merge_attempt < merge_limit and builder.name != 'Hyrule Castle':
merge_attempt, attempts = merge_attempt + 1, 0
raise GenerationException('Unable to resolve in 5 attempts')
@@ -2981,16 +3099,21 @@ def balance_split(candidate_sectors, dungeon_map, global_pole, builder_info):
chosen_sectors = defaultdict(list)
for i, choice in enumerate(choices):
chosen_sectors[choice].append(main_sector_list[i])
all_valid = True
all_valid, package_map = True, {}
for name, builder in dungeon_map.items():
if not valid_assignment(builder, chosen_sectors[name], builder_info):
flag, package = valid_assignment(builder, chosen_sectors[name], builder_info)
if not flag:
all_valid = False
break
if package:
package_map[builder] = package
if all_valid:
for name, sector_list in chosen_sectors.items():
builder = dungeon_map[name]
for sector in sector_list:
assign_sector(sector, builder, candidate_sectors, global_pole)
if builder in package_map:
builder.throne_door, builder.throne_sector, builder.chosen_lobby = package_map[builder]
return dungeon_map
tries += 1
raise GenerationException('Split Dungeon Builder: Impossible dungeon. Ref %s' % next(iter(dungeon_map.keys())))
@@ -3380,7 +3503,7 @@ class DungeonAccess:
def identify_branching_issues(dungeon_map, builder_info):
unconnected_builders = {}
for name, builder in dungeon_map.items():
resolved, unreached_doors = check_for_valid_layout(builder, [], builder_info)
resolved, unreached_doors, package = check_for_valid_layout(builder, [], builder_info)
if not resolved:
unconnected_builders[name] = builder
for hook, door_list in unreached_doors.items():
@@ -3402,7 +3525,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():
@@ -3421,16 +3545,27 @@ def check_for_valid_layout(builder, sector_list, builder_info):
proposal = generate_dungeon_find_proposal(split_build, entrance_regions, split, world, player)
# record split proposals
builder.valid_proposal[name] = proposal
package = None
if temp_builder.name == 'Hyrule Castle':
temp_builder.chosen_lobby.outstanding_doors.append(temp_builder.throne_door)
temp_builder.throne_sector.outstanding_doors.append(world.get_door('Hyrule Castle Throne Room N', player))
package = temp_builder.throne_door, temp_builder.throne_sector, temp_builder.chosen_lobby
split_list['Sewers'].remove(temp_builder.throne_door.entrance.parent_region.name)
builder.exception_list = list(sector_list)
return True, {}
except (GenerationException, NeutralizingException):
return True, {}, package
except (GenerationException, NeutralizingException, OtherGenException):
builder.split_dungeon_map = None
builder.valid_proposal = None
if temp_builder.name == 'Hyrule Castle' and temp_builder.throne_door:
temp_builder.chosen_lobby.outstanding_doors.append(temp_builder.throne_door)
temp_builder.throne_sector.outstanding_doors.append(world.get_door('Hyrule Castle Throne Room N', player))
old_entrance = temp_builder.throne_door.entrance.parent_region.name
split_dungeon_entrances[builder.name]['Sewers'].remove(old_entrance)
unreached_doors = resolve_equations(builder, sector_list)
return False, unreached_doors
return False, unreached_doors, None
else:
unreached_doors = resolve_equations(builder, sector_list)
return len(unreached_doors) == 0, unreached_doors
return len(unreached_doors) == 0, unreached_doors, None
def find_independent_entrances(entrance_regions, world, player):
@@ -3831,22 +3966,20 @@ def find_free_equation(equations):
def copy_door_equations(builder, sector_list):
equations = {}
for sector in builder.sectors + sector_list:
if sector.equations is None:
# todo: sort equations?
sector.equations = calc_sector_equations(sector)
sector.equations = calc_sector_equations(sector, builder.sewers_access)
curr_list = equations[sector] = []
for equation in sector.equations:
curr_list.append(equation.copy())
return equations
def calc_sector_equations(sector):
def calc_sector_equations(sector, sewers_flag=False):
equations = []
is_entrance = sector.is_entrance_sector() and not sector.destination_entrance
is_entrance = (sector.is_entrance_sector() and not sector.destination_entrance) or sewers_flag
if is_entrance:
flagged_equations = []
for door in sector.outstanding_doors:
equation, flag = calc_door_equation(door, sector, True)
equation, flag = calc_door_equation(door, sector, True, sewers_flag)
if flag:
flagged_equations.append(equation)
equations.append(equation)
@@ -3862,9 +3995,9 @@ def calc_sector_equations(sector):
return equations
def calc_door_equation(door, sector, look_for_entrance):
def calc_door_equation(door, sector, look_for_entrance, sewers_flag=None):
if look_for_entrance and not door.blocked:
flag = sector.is_entrance_sector()
flag = sector.is_entrance_sector() or sewers_flag
if flag:
eq = DoorEquation(door)
eq.benefit[hook_from_door(door)].append(door)
+15 -13
View File
@@ -5,7 +5,7 @@ from Items import ItemFactory
def create_dungeons(world, player):
def make_dungeon(name, id, default_boss, dungeon_regions, big_key, small_keys, dungeon_items):
dungeon = Dungeon(name, dungeon_regions, big_key, [] if world.retro[player] else small_keys,
dungeon = Dungeon(name, dungeon_regions, big_key, [] if world.keyshuffle[player] == 'universal' else small_keys,
dungeon_items, player, id)
dungeon.boss = BossFactory(default_boss, player)
for region in dungeon.regions:
@@ -52,10 +52,10 @@ hyrule_castle_regions = [
'Hyrule Dungeon South Abyss', 'Hyrule Dungeon South Abyss Catwalk', 'Hyrule Dungeon Guardroom',
'Hyrule Dungeon Armory Main', 'Hyrule Dungeon Armory Boomerang', 'Hyrule Dungeon Armory North Branch',
'Hyrule Dungeon Staircase', 'Hyrule Dungeon Cellblock', 'Hyrule Dungeon Cell', 'Sewers Behind Tapestry',
'Sewers Rope Room', 'Sewers Dark Cross', 'Sewers Water', 'Sewers Key Rat', 'Sewers Rat Path',
'Sewers Secret Room Blocked Path', 'Sewers Secret Room', 'Sewers Yet More Rats', 'Sewers Pull Switch', 'Sanctuary',
'Sanctuary Portal', 'Hyrule Castle West Portal', 'Hyrule Castle South Portal', 'Hyrule Castle East Portal'
'Sewers Rope Room', 'Sewers Dark Cross', 'Sewers Water', 'Sewers Dark Aquabats', 'Sewers Key Rat',
'Sewers Rat Path', 'Sewers Secret Room Blocked Path', 'Sewers Secret Room', 'Sewers Yet More Rats',
'Sewers Pull Switch', 'Sanctuary', 'Sanctuary Portal', 'Hyrule Castle West Portal', 'Hyrule Castle South Portal',
'Hyrule Castle East Portal'
]
eastern_regions = [
@@ -155,14 +155,16 @@ ice_regions = [
mire_regions = [
'Mire Lobby', 'Mire Post-Gap', 'Mire 2', 'Mire Hub', 'Mire Hub Right', 'Mire Hub Top', 'Mire Hub Switch',
'Mire Lone Shooter', 'Mire Failure Bridge', 'Mire Falling Bridge', 'Mire Map Spike Side', 'Mire Map Spot',
'Mire Crystal Dead End', 'Mire Hidden Shooters', 'Mire Hidden Shooters Blocked', 'Mire Cross', 'Mire Minibridge',
'Mire BK Door Room', 'Mire Spikes', 'Mire Ledgehop', 'Mire Bent Bridge', 'Mire Over Bridge', 'Mire Right Bridge',
'Mire Left Bridge', 'Mire Fishbone', 'Mire South Fish', 'Mire Spike Barrier', 'Mire Square Rail', 'Mire Lone Warp',
'Mire Wizzrobe Bypass', 'Mire Conveyor Crystal', 'Mire Conveyor - Crystal', 'Mire Tile Room', 'Mire Compass Room', 'Mire Compass Chest',
'Mire Neglected Room', 'Mire Chest View', 'Mire Conveyor Barrier', 'Mire BK Chest Ledge', 'Mire Warping Pool',
'Mire Torches Top', 'Mire Torches Bottom', 'Mire Attic Hint', 'Mire Dark Shooters', 'Mire Key Rupees',
'Mire Block X', 'Mire Tall Dark and Roomy', 'Mire Tall Dark and Roomy - Ranged Crystal', 'Mire Crystal Right', 'Mire Crystal Mid', 'Mire Crystal Left',
'Mire Lone Shooter', 'Mire Failure Bridge', 'Mire Falling Bridge - Primary', 'Mire Falling Bridge - Failure',
'Mire Falling Bridge - Chest', 'Mire Map Spike Side', 'Mire Map Spot', 'Mire Crystal Dead End',
'Mire Hidden Shooters', 'Mire Hidden Shooters Blocked', 'Mire Cross', 'Mire Minibridge', 'Mire BK Door Room',
'Mire Spikes', 'Mire Ledgehop', 'Mire Bent Bridge', 'Mire Over Bridge', 'Mire Right Bridge', 'Mire Left Bridge',
'Mire Fishbone', 'Mire South Fish', 'Mire Spike Barrier', 'Mire Square Rail', 'Mire Lone Warp',
'Mire Wizzrobe Bypass', 'Mire Conveyor Crystal', 'Mire Conveyor - Crystal', 'Mire Tile Room', 'Mire Compass Room',
'Mire Compass Chest', 'Mire Neglected Room', 'Mire Chest View', 'Mire Conveyor Barrier', 'Mire BK Chest Ledge',
'Mire Warping Pool', 'Mire Torches Top', 'Mire Torches Bottom', 'Mire Attic Hint', 'Mire Dark Shooters',
'Mire Key Rupees', 'Mire Block X', 'Mire Tall Dark and Roomy', 'Mire Tall Dark and Roomy - Ranged Crystal',
'Mire Crystal Right', 'Mire Crystal Mid', 'Mire Crystal Left',
'Mire Crystal Top', 'Mire Shooter Rupees', 'Mire Falling Foes', 'Mire Firesnake Skip', 'Mire Antechamber',
'Mire Boss', 'Mire Portal'
]
+2
View File
@@ -91,6 +91,7 @@ While the exact verbage of location names and item names can be found in the sou
Overworld Entrance naming:
Links House: The hero's old residence
Turtle Rock: Turtle Rock Main
Misery Mire: Misery Mire
Ice Palace: Ice Palace
@@ -141,6 +142,7 @@ Lake Hylia Fairy: A cave NE of Lake Hylia
Light Hype Fairy: The cave south of your house
Desert Fairy: The cave near the desert
Chicken House: The chicken lady's house
Tavern North: A backdoor
Aginahs Cave: The open desert cave
Sahasrahlas Hut: The house near armos
Cave Shop (Lake Hylia): The cave NW Lake Hylia
+4 -3
View File
@@ -2039,7 +2039,8 @@ Exit_Pool_Base = ['Links House Exit',
'Skull Pot Circle',
'Pyramid']
# these are connections that cannot be shuffled and always exist. They link together separate parts of the world we need to divide into regions
# these are connections that cannot be shuffled and always exist.
# They link together separate parts of the world we need to divide into regions
mandatory_connections = [('Old Man S&Q', 'Old Man House'),
# UW Connections
@@ -2378,7 +2379,7 @@ door_addresses = {'Links House': (0x00, (0x0104, 0x2c
'Light Hype Fairy': (0x6B, (0x0115, 0x34, 0x00a0, 0x0c04, 0x0900, 0x0c58, 0x0988, 0x0c73, 0x0985, 0x0a, 0xf6, 0x0000, 0x0000), 0x02),
'Desert Fairy': (0x71, (0x0115, 0x3a, 0x0000, 0x0e00, 0x0400, 0x0e26, 0x0468, 0x0e6d, 0x0485, 0x00, 0x00, 0x0000, 0x0000), 0x00),
'Kings Grave': (0x5A, (0x0113, 0x14, 0x0320, 0x0456, 0x0900, 0x04a6, 0x0998, 0x04c3, 0x097d, 0x0a, 0xf6, 0x0000, 0x0000), 0x20),
'Tavern North': (0x42, (0x0103, 0x18, 0x1440, 0x08a7, 0x0206, 0x08f9, 0x0288, 0x0914, 0x0293, 0xf7, 0x09, 0xFFFF, 0x0000), 0x00), # do not use, buggy
'Tavern North': (0x42, (0x0103, 0x18, 0x1440, 0x08a7, 0x0206, 0x091b, 0x0288, 0x0914, 0x0293, 0xf7, 0x09, 0xFFFF, 0x0000), 0x00),
'Chicken House': (0x4A, (0x0108, 0x18, 0x1120, 0x0837, 0x0106, 0x0888, 0x0188, 0x08a4, 0x0193, 0x07, 0xf9, 0x1530, 0x0000), 0x00),
'Aginahs Cave': (0x70, (0x010a, 0x30, 0x0656, 0x0cc6, 0x02aa, 0x0d18, 0x0328, 0x0d33, 0x032f, 0x08, 0xf8, 0x0000, 0x0000), 0x00),
'Sahasrahlas Hut': (0x44, (0x0105, 0x1e, 0x0610, 0x06d4, 0x0c76, 0x0727, 0x0cf0, 0x0743, 0x0cfb, 0x0a, 0xf6, 0x0000, 0x0000), 0x00),
@@ -2659,7 +2660,7 @@ ow_prize_table = {'Links House': (0x8b1, 0xb2d),
'Light Hype Fairy': (0x940, 0xc80),
'Desert Fairy': (0x420, 0xe00),
'Kings Grave': (0x920, 0x520),
'Tavern North': None, # can't mark this one technically
'Tavern North': (0x270, 0x900),
'Chicken House': (0x120, 0x880),
'Aginahs Cave': (0x2e0, 0xd00),
'Sahasrahlas Hut': (0xcf0, 0x6c0),
+101 -35
View File
@@ -13,7 +13,7 @@ from source.item.FillUtil import filter_pot_locations, valid_pot_items
def get_dungeon_item_pool(world):
return [item for dungeon in world.dungeons for item in dungeon.all_items]
return [item for dungeon in world.dungeons for item in dungeon.all_items if item.location is None]
def promote_dungeon_items(world):
@@ -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,11 +34,11 @@ 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():
if (item.smallkey and world.keyshuffle[item.player]) or (item.bigkey and world.bigkeyshuffle[item.player]):
if ((item.smallkey and world.keyshuffle[item.player] != 'none')
or (item.bigkey and world.bigkeyshuffle[item.player])):
item.advancement = True
elif (item.map and world.mapshuffle[item.player]) or (item.compass and world.compassshuffle[item.player]):
item.priority = True
@@ -50,7 +49,7 @@ def fill_dungeons_restrictive(world, shuffled_locations):
(bigs if i.bigkey else smalls if i.smallkey else others).append(i)
unplaced_smalls = list(smalls)
for i in world.itempool:
if i.smallkey and world.keyshuffle[i.player]:
if i.smallkey and world.keyshuffle[i.player] != 'none':
unplaced_smalls.append(i)
def fill(base_state, items, key_pool):
@@ -72,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 = []
@@ -93,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:
@@ -160,7 +161,7 @@ def valid_key_placement(item, location, key_pool, world):
if not valid_reserved_placement(item, location, world):
return False
if ((not item.smallkey and not item.bigkey) or item.player != location.player
or world.retro[item.player] or world.logic[item.player] == 'nologic'):
or world.keyshuffle[item.player] == 'universal' or world.logic[item.player] == 'nologic'):
return True
dungeon = location.parent_region.dungeon
if dungeon:
@@ -172,7 +173,7 @@ def valid_key_placement(item, location, key_pool, world):
if key_logic.prize_location:
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]
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)
else:
return not item.is_inside_dungeon_item(world)
@@ -216,7 +217,7 @@ def track_dungeon_items(item, location, world):
def is_dungeon_item(item, world):
return ((item.smallkey and not world.keyshuffle[item.player])
return ((item.smallkey and world.keyshuffle[item.player] == 'none')
or (item.bigkey and not world.bigkeyshuffle[item.player])
or (item.compass and not world.compassshuffle[item.player])
or (item.map and not world.mapshuffle[item.player]))
@@ -288,7 +289,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)
@@ -426,7 +427,8 @@ def distribute_items_restrictive(world, gftower_trash=False, fill_locations=None
# 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] and world.mode[item.player] == 'standard' else 0)
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)
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
@@ -490,14 +492,15 @@ def calc_trash_locations(world, player):
def ensure_good_pots(world, write_skips=False):
for loc in world.get_locations():
# # convert Arrows 5 when necessary
# if (loc.item.name in {'Arrows (5)'}
# and loc.type not in [LocationType.Pot, LocationType.Bonk]):
# loc.item = ItemFactory(invalid_location_replacement[loc.item.name], loc.item.player)
# convert Nothing when necessary
if loc.item is None:
loc.item = ItemFactory('Nothing', loc.player)
# convert Arrows 5 and Nothing when necessary
if (loc.item.name in {'Nothing'}
and (loc.type != LocationType.Pot or loc.item.player != loc.player)):
loc.item = ItemFactory(invalid_location_replacement[loc.item.name], loc.item.player)
if (loc.item.name in {'Arrows (5)'}
and (loc.type not in [LocationType.Pot, LocationType.Bonk] or loc.item.player != loc.player)):
loc.item = ItemFactory(invalid_location_replacement[loc.item.name], loc.item.player)
# # can be placed here by multiworld balancing or shop balancing
# # change it to something normal for the player it got swapped to
# elif (loc.item.name in {'Chicken', 'Big Magic'}
@@ -507,7 +510,7 @@ def ensure_good_pots(world, write_skips=False):
# else:
# loc.item = ItemFactory(invalid_location_replacement[loc.item.name], loc.player)
# do the arrow retro check
if world.retro[loc.item.player] and loc.item.name in {'Arrows (5)', 'Arrows (10)'}:
if world.bow_mode[loc.item.player].startswith('retro') and loc.item.name in {'Arrows (5)', 'Arrows (10)'}:
loc.item = ItemFactory('Rupees (5)', loc.item.player)
# don't write out all pots to spoiler
if write_skips:
@@ -528,10 +531,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):
@@ -672,7 +682,7 @@ def balance_multiworld_progression(world):
candidate_items = collections.defaultdict(set)
while True:
for location in balancing_sphere:
if location.event and (world.keyshuffle[location.item.player] or not location.item.smallkey) and (world.bigkeyshuffle[location.item.player] or not location.item.bigkey):
if location.event and (world.keyshuffle[location.item.player] != 'none' or not location.item.smallkey) and (world.bigkeyshuffle[location.item.player] or not location.item.bigkey):
balancing_state.collect(location.item, True, location)
player = location.item.player
if player in balancing_players and not location.locked and location.player != player:
@@ -747,7 +757,7 @@ def balance_multiworld_progression(world):
sphere_locations.add(location)
for location in sphere_locations:
if location.event and (world.keyshuffle[location.item.player] or not location.item.smallkey) and (world.bigkeyshuffle[location.item.player] or not location.item.bigkey):
if location.event and (world.keyshuffle[location.item.player] != 'none' or not location.item.smallkey) and (world.bigkeyshuffle[location.item.player] or not location.item.bigkey):
state.collect(location.item, True, location)
checked_locations |= sphere_locations
@@ -823,7 +833,9 @@ def balance_money_progression(world):
return True
if item.name in ['Progressive Armor', 'Blue Mail', 'Red Mail']:
return True
if world.retro[player] and (item.name in ['Single Arrow', 'Small Key (Universal)']):
if world.keyshuffle[player] == 'universal' and item.name == 'Small Key (Universal)':
return True
if world.bow_mode[player].startswith('retro') and item.name == 'Single Arrow':
return True
if location.name in pay_for_locations:
return True
@@ -963,31 +975,85 @@ def balance_money_progression(world):
wallet[location.item.player] += rupee_chart[location.item.name]
def set_prize_drops(world, player):
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,
0xDD, 0xDD, 0xDD, 0xDD, 0xDD, 0xDE, 0xDE, 0xDE, 0xDE, 0xDE,
0xDF, 0xDF, 0xDF, 0xDF, 0xDF, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0,
0xE1, 0xE1, 0xE1, 0xE1, 0xE1, 0xE2, 0xE2, 0xE2, 0xE2, 0xE2,
0xE3, 0xE3, 0xE3, 0xE3, 0xE3]
# randomize last 7 slots
new_prizes = random.sample(prizes, 7)
def chunk(l,n):
return [l[i:i+n] for i in range(0, len(l), n)]
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
new_prizes = [prize_replacements.get(prize, prize) for prize in new_prizes]
prizes = [prize_replacements.get(prize, prize) for prize in prizes]
dig_prizes = [prize_replacements.get(prize, prize) for prize in dig_prizes]
if world.retro[player]:
if world.bow_mode[player].startswith('retro'):
prize_replacements = {0xE1: 0xDA, #5 Arrows -> Blue Rupee
0xE2: 0xDB} #10 Arrows -> Red Rupee
new_prizes = [prize_replacements.get(prize, prize) for prize in new_prizes]
prizes = [prize_replacements.get(prize, prize) for prize in prizes]
dig_prizes = [prize_replacements.get(prize, prize) for prize in dig_prizes]
# write tree pull prizes
world.prizes[player]['pull'] = [ new_prizes.pop(), new_prizes.pop(), new_prizes.pop() ]
world.prizes[player]['dig'] = dig_prizes
# write tree pull prizes
world.prizes[player]['pull'] = [ prizes.pop(), prizes.pop(), prizes.pop() ]
# rupee crab prizes
world.prizes[player]['crab'] = [ new_prizes.pop(), new_prizes.pop() ]
world.prizes[player]['crab'] = [ prizes.pop(), prizes.pop() ]
# stunned enemy prize
world.prizes[player]['stun'] = new_prizes.pop()
world.prizes[player]['stun'] = prizes.pop()
# saved fish prize
world.prizes[player]['fish'] = new_prizes.pop()
world.prizes[player]['fish'] = prizes.pop()
world.prizes[player]['enemies'] = prizes
+1 -1
View File
@@ -190,7 +190,7 @@ def guiMain(args=None):
self.pages["startinventory"].content.pack(side=TOP, fill=BOTH, expand=True)
# Custom Controls
self.pages["custom"].content = custom_page(self,self.pages["custom"])
self.pages["custom"].content = custom_page(self, self.pages["custom"])
self.pages["custom"].content.pack(side=TOP, fill=BOTH, expand=True)
def validation(P):
+3 -2
View File
@@ -76,7 +76,7 @@ class InitialSram:
if startingstate.has('Bow', player):
equip[0x340] = 3 if startingstate.has('Silver Arrows', player) else 1
equip[0x38E] |= 0x20 # progressive flag to get the correct hint in all cases
if not world.retro[player]:
if not world.bow_mode[player].startswith('retro'):
equip[0x38E] |= 0x80
if startingstate.has('Silver Arrows', player):
equip[0x38E] |= 0x40
@@ -147,6 +147,7 @@ class InitialSram:
'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),
'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)}
keys = {'Small Key (Eastern Palace)': [0x37E], 'Small Key (Desert Palace)': [0x37F],
@@ -190,7 +191,7 @@ class InitialSram:
elif item.name in bombs:
starting_bombs += bombs[item.name]
elif item.name in arrows:
if world.retro[player]:
if world.bow_mode[player].startswith('retro'):
equip[0x38E] |= 0x80
starting_arrows = 1
else:
+349 -109
View File
@@ -1,4 +1,4 @@
from collections import namedtuple
from collections import namedtuple, defaultdict
import logging
import math
import RaceRandom as random
@@ -6,7 +6,7 @@ import RaceRandom as random
from BaseClasses import LocationType, 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, fast_fill, get_dungeon_item_pool
from Fill import FillError, fill_restrictive, get_dungeon_item_pool, track_dungeon_items, track_outside_keys
from PotShuffle import vanilla_pots
from Tables import bonk_prize_lookup
from Items import ItemFactory
@@ -279,13 +279,17 @@ def generate_itempool(world, player):
world.get_location('Zelda Drop Off', player).locked = True
# set up item pool
if world.custom and player in world.customitemarray:
(pool, placed_items, precollected_items, clock_mode, treasure_hunt_count, treasure_hunt_total, treasure_hunt_icon, lamps_needed_for_dark_rooms) = make_custom_item_pool(world.progressive, world.shuffle[player], world.difficulty[player], world.timer, world.goal[player], world.mode[player], world.swords[player], world.retro[player], world.bombbag[player], world.customitemarray[player])
skip_pool_adjustments = False
if world.customizer and world.customizer.get_item_pool():
(pool, placed_items, precollected_items, clock_mode, lamps_needed_for_dark_rooms) = make_customizer_pool(world, player)
skip_pool_adjustments = True
elif world.custom and player in world.customitemarray:
(pool, placed_items, precollected_items, clock_mode, treasure_hunt_count, treasure_hunt_total, treasure_hunt_icon, lamps_needed_for_dark_rooms) = make_custom_item_pool(world, player, world.progressive, world.shuffle[player], world.difficulty[player], world.timer, world.goal[player], world.mode[player], world.swords[player], world.bombbag[player], world.customitemarray[player])
world.rupoor_cost = min(world.customitemarray[player]["rupoorcost"], 9999)
else:
(pool, placed_items, precollected_items, clock_mode, lamps_needed_for_dark_rooms) = get_pool_core(world.progressive, world.shuffle[player], world.difficulty[player], world.treasure_hunt_total[player], world.timer, world.goal[player], world.mode[player], world.swords[player], world.retro[player], world.bombbag[player], world.doorShuffle[player], world.logic[player], world.is_tile_swapped(0x18, player))
(pool, placed_items, precollected_items, clock_mode, lamps_needed_for_dark_rooms) = get_pool_core(world, player, world.progressive, world.shuffle[player], world.difficulty[player], world.treasure_hunt_total[player], world.timer, world.goal[player], world.mode[player], world.swords[player], world.bombbag[player], world.doorShuffle[player], world.logic[player], world.flute_mode[player] == 'active' or world.is_tile_swapped(0x18, player))
if player in world.pool_adjustment.keys():
if player in world.pool_adjustment.keys() and not skip_pool_adjustments:
amt = world.pool_adjustment[player]
if amt < 0:
trash_options = [x for x in pool if x in trash_items]
@@ -311,7 +315,8 @@ def generate_itempool(world, player):
if not found_sword and world.swords[player] != 'swordless':
found_sword = True
possible_weapons.append(item)
if item in ['Progressive Bow', 'Bow'] and not found_bow and not world.retro[player]:
if (item in ['Progressive Bow', 'Bow'] and not found_bow
and not world.bow_mode[player].startswith('retro')):
found_bow = True
possible_weapons.append(item)
if item in ['Hammer', 'Fire Rod', 'Cane of Somaria', 'Cane of Byrna']:
@@ -331,7 +336,7 @@ def generate_itempool(world, player):
world.get_location(location, player).event = True
world.get_location(location, player).locked = True
if world.shopsanity[player]:
if world.shopsanity[player] and not skip_pool_adjustments:
for shop in world.shops[player]:
if shop.region.name in shop_to_location_table:
for index, slot in enumerate(shop.inventory):
@@ -365,7 +370,7 @@ def generate_itempool(world, player):
world.treasure_hunt_icon[player] = 'Triforce Piece'
world.itempool.extend([item for item in get_dungeon_item_pool(world) if item.player == player
and ((item.smallkey and world.keyshuffle[player])
and ((item.smallkey and world.keyshuffle[player] != 'none')
or (item.bigkey and world.bigkeyshuffle[player])
or (item.map and world.mapshuffle[player])
or (item.compass and world.compassshuffle[player]))])
@@ -374,7 +379,9 @@ def generate_itempool(world, player):
# rather than making all hearts/heart pieces progression items (which slows down generation considerably)
# We mark one random heart container as an advancement item (or 4 heart pieces in expert mode)
if world.difficulty[player] in ['normal', 'hard'] and not (world.custom and player in world.customitemarray and world.customitemarray[player]["heartcontainer"] == 0):
next(item for item in items if item.name == 'Boss Heart Container').advancement = True
container = next((item for item in items if item.name == 'Boss Heart Container'), None)
if container:
container.advancement = True
elif world.difficulty[player] in ['expert'] and not (world.custom and player in world.customitemarray and world.customitemarray[player]["heartpiece"] < 4):
adv_heart_pieces = (item for item in items if item.name == 'Piece of Heart')
for i in range(4):
@@ -391,11 +398,33 @@ def generate_itempool(world, player):
return item if not choice else ItemFactory("Bee Trap", player) if choice == 'trap' else ItemFactory("Bee", player)
return item
world.itempool += [beemizer(item) for item in items]
if not skip_pool_adjustments:
world.itempool += [beemizer(item) for item in items]
else:
world.itempool += items
# shuffle medallions
mm_medallion = ['Ether', 'Quake', 'Bombos'][random.randint(0, 2)]
tr_medallion = ['Ether', 'Quake', 'Bombos'][random.randint(0, 2)]
mm_medallion, tr_medallion = None, None
if world.customizer and world.customizer.get_medallions() and player in world.customizer.get_medallions():
medal_map = world.customizer.get_medallions()
if player in medal_map:
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:
tr_medallion = ['Ether', 'Quake', 'Bombos'][random.randint(0, 2)]
world.required_medallions[player] = (mm_medallion, tr_medallion)
# shuffle bottle refills
@@ -409,16 +438,17 @@ def generate_itempool(world, player):
set_up_shops(world, player)
if world.retro[player]:
set_up_take_anys(world, player)
if world.dropshuffle[player]:
if world.take_any[player] != 'none':
set_up_take_anys(world, player, skip_pool_adjustments)
if world.keyshuffle[player] == 'universal':
if world.dropshuffle[player] and not skip_pool_adjustments:
world.itempool += [ItemFactory('Small Key (Universal)', player)] * 13
if world.pottery[player] not in ['none', 'cave']:
if world.pottery[player] not in ['none', 'cave'] and not skip_pool_adjustments:
world.itempool += [ItemFactory('Small Key (Universal)', player)] * 19
create_dynamic_shop_locations(world, player)
if world.pottery[player] not in ['none', 'keys']:
if world.pottery[player] not in ['none', 'keys'] and not skip_pool_adjustments:
add_pot_contents(world, player)
if world.shuffle_bonk_drops[player]:
@@ -436,8 +466,12 @@ take_any_locations = [
'Palace of Darkness Hint', 'East Dark World Hint', 'Archery Game', 'Dark Lake Hylia Ledge Hint',
'Dark Lake Hylia Ledge Spike Cave', 'Fortune Teller (Dark)', 'Dark Sanctuary Hint', 'Dark Desert Hint']
fixed_take_anys = [
'Desert Healer Fairy', 'Swamp Healer Fairy', 'Dark Death Mountain Healer Fairy',
'Dark Lake Hylia Ledge Healer Fairy', 'Bonk Fairy (Dark)']
def set_up_take_anys(world, player):
def set_up_take_anys(world, player, skip_adjustments=False):
if world.mode[player] == 'inverted':
if 'Dark Sanctuary Hint' in take_any_locations:
take_any_locations.remove('Dark Sanctuary Hint')
@@ -445,9 +479,12 @@ def set_up_take_anys(world, player):
if 'Archery Game' in take_any_locations:
take_any_locations.remove('Archery Game')
take_any_candidates = [x for x in take_any_locations if len(world.get_region(x, player).locations) == 0]
regions = random.sample(take_any_candidates, 5)
if world.take_any[player] == 'random':
take_any_candidates = [x for x in take_any_locations if len(world.get_region(x, player).locations) == 0]
regions = random.sample(take_any_candidates, 5)
elif world.take_any[player] == 'fixed':
regions = list(fixed_take_anys)
random.shuffle(regions)
old_man_take_any = Region("Old Man Sword Cave", RegionType.Cave, 'the sword cave', player)
world.regions.append(old_man_take_any)
@@ -462,12 +499,13 @@ def set_up_take_anys(world, player):
sword = next((item for item in world.itempool if item.type == 'Sword' and item.player == player), None)
if sword:
world.itempool.append(ItemFactory('Rupees (20)', player))
if not world.shopsanity[player]:
world.itempool.remove(sword)
if not skip_adjustments:
world.itempool.append(ItemFactory('Rupees (20)', player))
if not world.shopsanity[player]:
world.itempool.remove(sword)
old_man_take_any.shop.add_inventory(0, sword.name, 0, 0, create_location=True)
else:
if world.shopsanity[player]:
if world.shopsanity[player] and not skip_adjustments:
world.itempool.append(ItemFactory('Rupees (300)', player))
old_man_take_any.shop.add_inventory(0, 'Rupees (300)', 0, 0, create_location=world.shopsanity[player])
@@ -485,7 +523,7 @@ def set_up_take_anys(world, player):
world.shops[player].append(take_any.shop)
take_any.shop.add_inventory(0, 'Blue Potion', 0, 0, create_location=world.shopsanity[player])
take_any.shop.add_inventory(1, 'Boss Heart Container', 0, 0, create_location=world.shopsanity[player])
if world.shopsanity[player]:
if world.shopsanity[player] and not skip_adjustments:
world.itempool.append(ItemFactory('Blue Potion', player))
world.itempool.append(ItemFactory('Boss Heart Container', player))
@@ -668,38 +706,47 @@ def fill_prizes(world, attempts=15):
def set_up_shops(world, player):
if world.retro[player]:
retro_bow = world.bow_mode[player].startswith('retro')
universal_keys = world.keyshuffle[player] == 'universal'
if retro_bow or universal_keys:
if world.shopsanity[player]:
removals = [next(item for item in world.itempool if item.name == 'Arrows (10)' and item.player == player)]
red_pots = [item for item in world.itempool if item.name == 'Red Potion' and item.player == player][:5]
shields_n_hearts = [item for item in world.itempool if item.name in ['Blue Shield', 'Small Heart'] and item.player == player]
removals.extend([item for item in world.itempool if item.name == 'Arrow Upgrade (+5)' and item.player == player])
removals.extend(red_pots)
removals.extend(random.sample(shields_n_hearts, 5))
removals = []
if retro_bow:
removals = [next(item for item in world.itempool if item.name == 'Arrows (10)' and item.player == player)]
removals.extend([item for item in world.itempool if item.name == 'Arrow Upgrade (+5)' and item.player == player])
shields_n_hearts = [item for item in world.itempool if item.name in ['Blue Shield', 'Small Heart'] and item.player == player]
removals.extend(random.sample(shields_n_hearts, 5))
if universal_keys:
red_pots = [item for item in world.itempool if item.name == 'Red Potion' and item.player == player][:5]
removals.extend(red_pots)
for remove in removals:
world.itempool.remove(remove)
for i in range(6): # replace the Arrows (10) and randomly selected hearts/blue shield
arrow_item = ItemFactory('Single Arrow', player)
arrow_item.advancement = True
world.itempool.append(arrow_item)
for i in range(5): # replace the red potions
world.itempool.append(ItemFactory('Small Key (Universal)', player))
world.itempool.append(ItemFactory('Rupees (50)', player)) # replaces the arrow upgrade
if retro_bow:
for i in range(6): # replace the Arrows (10) and randomly selected hearts/blue shield
arrow_item = ItemFactory('Single Arrow', player)
arrow_item.advancement = True
world.itempool.append(arrow_item)
world.itempool.append(ItemFactory('Rupees (50)', player)) # replaces the arrow upgrade
if universal_keys:
for i in range(5): # replace the red potions
world.itempool.append(ItemFactory('Small Key (Universal)', player))
# TODO: move hard+ mode changes for shields here, utilizing the new shops
else:
rss = world.get_region('Red Shield Shop', player).shop
if not rss.locked:
rss.custom = True
rss.add_inventory(2, 'Single Arrow', 80)
if retro_bow:
rss = world.get_region('Red Shield Shop', player).shop
if not rss.locked:
rss.custom = True
rss.add_inventory(2, 'Single Arrow', 80)
rss.locked = True
cap_shop = world.get_region('Capacity Upgrade', player).shop
cap_shop.inventory[1] = None # remove arrow capacity upgrades in retro
for shop in random.sample([s for s in world.shops[player] if not s.locked and s.region.player == player], 5):
shop.custom = True
shop.locked = True
shop.add_inventory(0, 'Single Arrow', 80)
shop.add_inventory(1, 'Small Key (Universal)', 100)
shop.add_inventory(2, 'Bombs (10)', 50)
rss.locked = True
cap_shop = world.get_region('Capacity Upgrade', player).shop
cap_shop.inventory[1] = None # remove arrow capacity upgrades in retro
if retro_bow:
shop.add_inventory(0, 'Single Arrow', 80)
if universal_keys:
shop.add_inventory(1, 'Small Key (Universal)', 100)
if world.bombbag[player]:
if world.shopsanity[player]:
removals = [item for item in world.itempool if item.name == 'Bomb Upgrade (+5)' and item.player == player]
@@ -712,10 +759,11 @@ def set_up_shops(world, player):
def customize_shops(world, player):
found_bomb_upgrade, found_arrow_upgrade = False, world.retro[player]
retro_bow = world.bow_mode[player].startswith('retro')
found_bomb_upgrade, found_arrow_upgrade = False, retro_bow
possible_replacements = []
shops_to_customize = shop_to_location_table.copy()
if world.retro[player]:
if world.take_any[player] != 'none':
shops_to_customize.update(retro_shops)
for shop_name, loc_list in shops_to_customize.items():
shop = world.get_region(shop_name, player).shop
@@ -738,7 +786,7 @@ def customize_shops(world, player):
price = 0
else:
price = 120 if shop_name == 'Potion Shop' and item.name == 'Red Potion' else item.price
if world.retro[player] and item.name == 'Single Arrow':
if retro_bow and item.name == 'Single Arrow':
price = 80
# randomize price
shop.add_inventory(idx, item.name, randomize_price(price), max_repeat, player=item.player)
@@ -765,7 +813,7 @@ def customize_shops(world, player):
if not found_arrow_upgrade and len(possible_replacements) > 0:
choices = []
for shop, idx, loc, item in possible_replacements:
if item.name == 'Arrows (10)' or (item.name == 'Single Arrow' and not world.retro[player]):
if item.name == 'Arrows (10)' or (item.name == 'Single Arrow' and not retro_bow):
choices.append((shop, idx, loc, item))
if len(choices) > 0:
shop, idx, loc, item = random.choice(choices)
@@ -909,11 +957,12 @@ rupee_chart = {'Rupee (1)': 1, 'Rupees (5)': 5, 'Rupees (20)': 20, 'Rupees (50)'
def add_pot_contents(world, player):
retro_bow = world.bow_mode[player].startswith('retro')
for super_tile, pot_list in vanilla_pots.items():
for pot in pot_list:
if pot.item not in [PotItem.Hole, PotItem.Key, PotItem.Switch]:
if valid_pot_location(pot, world.pot_pool[player], world, player):
item = ('Rupees (5)' if world.retro[player] and pot_items[pot.item] == 'Arrows (5)'
item = ('Rupees (5)' if retro_bow and pot_items[pot.item] == 'Arrows (5)'
else pot_items[pot.item])
world.itempool.append(ItemFactory(item, player))
@@ -929,7 +978,8 @@ def add_bonkdrop_contents(world, player):
count -= 1
def get_pool_core(progressive, shuffle, difficulty, treasure_hunt_total, timer, goal, mode, swords, retro, 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 = []
@@ -965,12 +1015,8 @@ def get_pool_core(progressive, shuffle, difficulty, treasure_hunt_total, timer,
lamps_needed_for_dark_rooms = 1
# insanity shuffle doesn't have fake LW/DW logic so for now guaranteed Mirror and Moon Pearl at the start
if shuffle == 'insanity_legacy':
place_item('Link\'s House', 'Magic Mirror')
place_item('Sanctuary', 'Moon Pearl')
else:
pool.extend(['Magic Mirror', 'Moon Pearl'])
# old insanity shuffle didn't have fake LW/DW logic so this used to be conditional
pool.extend(['Magic Mirror', 'Moon Pearl'])
if timer == 'display':
clock_mode = 'stopwatch'
@@ -1004,7 +1050,7 @@ def get_pool_core(progressive, shuffle, difficulty, treasure_hunt_total, timer,
else:
pool.extend(diff.basicarmor)
if want_progressives():
if 'silvers' not in world.bow_mode[player]:
pool.extend(['Progressive Bow'] * 2)
elif swords != 'swordless':
pool.extend(diff.basicbow)
@@ -1052,13 +1098,14 @@ def get_pool_core(progressive, shuffle, difficulty, treasure_hunt_total, timer,
if goal in ['pedestal', 'trinity'] and swords != 'vanilla':
place_item('Master Sword Pedestal', 'Triforce')
if retro:
pool = [item.replace('Single Arrow','Rupees (5)') for item in pool]
pool = [item.replace('Arrows (10)','Rupees (5)') for item in pool]
pool = [item.replace('Arrow Upgrade (+5)','Rupees (5)') for item in pool]
pool = [item.replace('Arrow Upgrade (+10)','Rupees (5)') for item in pool]
if world.bow_mode[player].startswith('retro'):
pool = [item.replace('Single Arrow', 'Rupees (5)') for item in pool]
pool = [item.replace('Arrows (10)', 'Rupees (5)') for item in pool]
pool = [item.replace('Arrow Upgrade (+5)', 'Rupees (5)') for item in pool]
pool = [item.replace('Arrow Upgrade (+10)', 'Rupees (5)') for item in pool]
if world.keyshuffle[player] == 'universal':
pool.extend(diff.retro)
if door_shuffle != 'vanilla': # door shuffle needs more keys for retro
if door_shuffle != 'vanilla': # door shuffle needs more keys for universal keys
replace = 'Rupees (20)' if difficulty == 'normal' else 'Rupees (5)'
indices = [i for i, x in enumerate(pool) if x == replace]
for i in range(0, min(10, len(indices))):
@@ -1074,7 +1121,7 @@ def get_pool_core(progressive, shuffle, difficulty, treasure_hunt_total, timer,
return (pool, placed_items, precollected_items, clock_mode, lamps_needed_for_dark_rooms)
def make_custom_item_pool(progressive, shuffle, difficulty, timer, goal, mode, swords, retro, bombbag, customitemarray):
def make_custom_item_pool(world, player, progressive, shuffle, difficulty, timer, goal, mode, swords, bombbag, customitemarray):
pool = []
placed_items = {}
precollected_items = []
@@ -1155,7 +1202,7 @@ def make_custom_item_pool(progressive, shuffle, difficulty, timer, goal, mode, s
itemtotal = itemtotal + 1
if mode == 'standard':
if retro:
if world.keyshuffle[player] == 'universal':
key_location = random.choice(['Secret Passage', 'Hyrule Castle - Boomerang Chest', 'Hyrule Castle - Map Chest', 'Hyrule Castle - Zelda\'s Chest', 'Sewers - Dark Cross'])
place_item(key_location, 'Small Key (Universal)')
pool.extend(['Small Key (Universal)'] * max((customitemarray["generickeys"] - 1), 0))
@@ -1166,17 +1213,10 @@ def make_custom_item_pool(progressive, shuffle, difficulty, timer, goal, mode, s
pool.extend(['Fighter Sword'] * customitemarray["sword1"])
pool.extend(['Progressive Sword'] * customitemarray["progressivesword"])
pool.extend(['Magic Mirror'] * customitemarray["mirror"])
pool.extend(['Moon Pearl'] * customitemarray["pearl"])
if shuffle == 'insanity_legacy':
place_item('Link\'s House', 'Magic Mirror')
place_item('Sanctuary', 'Moon Pearl')
pool.extend(['Magic Mirror'] * max((customitemarray["mirror"] -1 ), 0))
pool.extend(['Moon Pearl'] * max((customitemarray["pearl"] - 1), 0))
else:
pool.extend(['Magic Mirror'] * customitemarray["mirror"])
pool.extend(['Moon Pearl'] * customitemarray["pearl"])
if retro:
if world.keyshuffle[player] == 'universal':
itemtotal = itemtotal - 28 # Corrects for small keys not being in item pool in Retro Mode
if itemtotal < total_items_to_place:
nothings = total_items_to_place - itemtotal
@@ -1208,12 +1248,62 @@ def make_customizer_pool(world, player):
assert loc not in placed_items
placed_items[loc] = item
dungeon_locations, dungeon_count = defaultdict(set), defaultdict(int)
for l in world.get_unfilled_locations(player):
if l.parent_region.dungeon:
dungeon = l.parent_region.dungeon
dungeon_locations[dungeon.name].add(l)
if dungeon.name not in dungeon_count:
d_count = 1 if dungeon.big_key else 0
d_count += len(dungeon.small_keys) + len(dungeon.dungeon_items)
dungeon_count[dungeon.name] = d_count
diff = difficulties[world.difficulty[player]]
for item_name, amount in world.customizer.get_item_pool()[player].items():
if isinstance(amount, int):
if item_name == 'Bottle (Random)':
for _ in range(amount):
pool.append(random.choice(diff.bottles))
elif item_name.startswith('Small Key') and item_name != 'Small Key (Universal)':
d_item = ItemFactory(item_name, player)
if world.keyshuffle[player] == 'none':
d_name = d_item.dungeon
dungeon = world.get_dungeon(d_name, player)
target_amount = max(amount, len(dungeon.small_keys))
additional_amount = target_amount - len(dungeon.small_keys)
possible_fit = min(additional_amount, len(dungeon_locations[d_name])-dungeon_count[d_name])
if possible_fit > 0:
dungeon_count[d_name] += possible_fit
dungeon.small_keys.extend([d_item] * amount)
additional_amount -= possible_fit
if additional_amount > 0:
pool.extend([item_name] * amount)
else:
dungeon = world.get_dungeon(d_item.dungeon, player)
target_amount = max(amount, len(dungeon.small_keys))
additional_amount = target_amount - len(dungeon.small_keys)
dungeon.small_keys.extend([d_item] * additional_amount)
elif item_name.startswith('Big Key') or item_name.startswith('Map') or item_name.startswith('Compass'):
d_item = ItemFactory(item_name, player)
if ((d_item.bigkey and not world.bigkeyshuffle[player])
or (d_item.compass and not world.compassshuffle[player])
or (d_item.map and not world.mapshuffle[player])):
d_name = d_item.dungeon
dungeon = world.get_dungeon(d_name, player)
current_amount = 1 if d_item == dungeon.big_key or d_item in dungeon.dungeon_items else 0
additional_amount = amount - current_amount
possible_fit = min(additional_amount, len(dungeon_locations[d_name])-dungeon_count[d_name])
if possible_fit > 0:
dungeon_count[d_name] += possible_fit
dungeon.dungeon_items.extend([d_item] * amount)
additional_amount -= possible_fit
if additional_amount > 0:
pool.extend([item_name] * amount)
else:
dungeon = world.get_dungeon(d_item.dungeon, player)
current_amount = 1 if d_item == dungeon.big_key or d_item in dungeon.dungeon_items else 0
additional_amount = amount - current_amount
dungeon.dungeon_items.extend([d_item] * additional_amount)
else:
pool.extend([item_name] * amount)
@@ -1225,12 +1315,73 @@ def make_customizer_pool(world, player):
elif timer == 'ohko':
clock_mode = 'ohko'
if world.goal[player] == 'pedestal':
if world.goal[player] in ['pedestal', 'trinity']:
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:
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):
missing_items.append('Progressive Glove')
if world.bombbag[player]:
if 'Bomb Upgrade (+10)' not in pool:
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):
missing_items.append('Progressive Sword')
bow_found = next((i for i in pool if i in {'Bow', 'Progressive Bow'}), None)
if not bow_found:
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])
if t != 0:
pieces = sum(1 for i in pool if i == 'Triforce Piece')
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))
return pool, placed_items, precollected_items, clock_mode, 1
filler_items = {
'Arrows (10)': 12,
'Bombs (3)': 16,
'Rupees (300)': 5,
'Rupees (100)': 1,
'Rupees (50)': 7,
'Rupees (20)': 28,
'Rupees (5)': 4,
}
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]
# 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
@@ -1246,31 +1397,120 @@ def set_default_triforce(goal, custom_goal, custom_total):
# A quick test to ensure all combinations generate the correct amount of items.
def test():
for difficulty in ['normal', 'hard', 'expert']:
for goal in ['ganon', 'triforcehunt', 'pedestal', 'trinity']:
for timer in ['none', 'display', 'timed', 'timed-ohko', 'ohko', 'timed-countdown']:
for mode in ['open', 'standard', 'inverted', 'retro']:
for swords in ['random', 'assured', 'swordless', 'vanilla']:
for progressive in ['on', 'off']:
for shuffle in ['vanilla', 'full', 'crossed', 'insanity']:
for logic in ['noglitches', 'minorglitches', 'owglitches', 'nologic']:
for retro in [True, False]:
for door_shuffle in ['basic', 'crossed', 'vanilla']:
for bombbag in [True, False]:
out = get_pool_core(progressive, shuffle, difficulty, 30, timer, goal, mode, swords, retro, bombbag, door_shuffle, logic, False)
count = len(out[0]) + len(out[1])
# def test():
# for difficulty in ['normal', 'hard', 'expert']:
# for goal in ['ganon', 'triforcehunt', 'pedestal', 'trinity']:
# for timer in ['none', 'display', 'timed', 'timed-ohko', 'ohko', 'timed-countdown']:
# for mode in ['open', 'standard', 'inverted', 'retro']:
# for swords in ['random', 'assured', 'swordless', 'vanilla']:
# for progressive in ['on', 'off']:
# for shuffle in ['vanilla', 'full', 'crossed', 'insanity']:
# for logic in ['noglitches', 'minorglitches', 'owglitches', 'nologic']:
# for retro in [True, False]:
# for door_shuffle in ['basic', 'crossed', 'vanilla']:
# for bombbag in [True, False]:
# out = get_pool_core(progressive, shuffle, difficulty, 30, timer, goal, mode, swords, retro, bombbag, door_shuffle, logic, False)
# count = len(out[0]) + len(out[1])
correct_count = total_items_to_place
if goal in ['pedestal', 'trinity'] and swords != 'vanilla':
# pedestal goals generate one extra item
correct_count += 1
if retro:
correct_count += 28
try:
assert count == correct_count, "expected {0} items but found {1} items for {2}".format(correct_count, count, (progressive, shuffle, difficulty, timer, goal, mode, swords, retro, bombbag))
except AssertionError as e:
print(e)
if __name__ == '__main__':
test()
def fill_specific_items(world):
if world.customizer:
placements = world.customizer.get_placements()
dungeon_pool = get_dungeon_item_pool(world)
prize_pool = []
prize_set = {'Red Pendant', 'Blue Pendant', 'Green Pendant', 'Crystal 1', 'Crystal 2',
'Crystal 3', 'Crystal 4', 'Crystal 7', 'Crystal 5', 'Crystal 6'}
for p in range(1, world.players + 1):
prize_pool.extend(prize_set)
if placements:
for player, placement_list in placements.items():
for location, item in placement_list.items():
loc = world.get_location(location, player)
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
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']
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):
return ((item.startswith('Small Key') and world.keyshuffle[player] == 'none')
or (item.startswith('Big Key') and not world.bigkeyshuffle[player])
or (item.startswith('Compass') and not world.compassshuffle[player])
or (item.startswith('Map') and not world.mapshuffle[player]))
+1 -2
View File
@@ -14,8 +14,7 @@ def ItemFactory(items, player):
advancement, priority, type, code, price, pedestal_hint, pedestal_credit, sickkid_credit, zora_credit, witch_credit, fluteboy_credit, hint_text = item_table[item]
ret.append(Item(item, advancement, priority, type, code, price, pedestal_hint, pedestal_credit, sickkid_credit, zora_credit, witch_credit, fluteboy_credit, hint_text, player))
else:
logging.getLogger('').warning('Unknown Item: %s', item)
return None
raise RuntimeError(f'Unknown Item: {item}')
if singleton:
return ret[0]
+72 -20
View File
@@ -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)
@@ -223,13 +224,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
@@ -251,7 +253,7 @@ def find_all_locations(sector):
def calc_max_chests(builder, key_layout, world, player):
if world.doorShuffle[player] != 'crossed':
if world.doorShuffle[player] in ['basic', 'vanilla']:
return len(world.get_dungeon(key_layout.sector.name, player).small_keys)
return max(0, builder.key_doors_num - key_layout.max_drops)
@@ -275,7 +277,7 @@ def analyze_dungeon(key_layout, world, player):
key_logic.bk_chests.update(find_big_chest_locations(key_layout.all_chest_locations))
key_logic.bk_chests.update(find_big_key_locked_locations(key_layout.all_chest_locations))
key_logic.prize_location = dungeon_table[key_layout.sector.name].prize
if world.retro[player] and world.mode[player] != 'standard':
if world.keyshuffle[player] == 'universal' and world.mode[player] != 'standard':
return
original_key_counter = find_counter({}, False, key_layout, False)
@@ -301,10 +303,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():
@@ -926,7 +928,7 @@ def self_lock_possible(counter):
def available_chest_small_keys(key_counter, world, player):
if not world.keyshuffle[player] and not world.retro[player]:
if world.keyshuffle[player] == 'none':
cnt = 0
for loc in key_counter.free_locations:
if key_counter.big_key_opened or '- Big Chest' not in loc.name:
@@ -937,7 +939,7 @@ def available_chest_small_keys(key_counter, world, player):
def available_chest_small_keys_logic(key_counter, world, player, sm_restricted):
if not world.keyshuffle[player] and not world.retro[player]:
if world.keyshuffle[player] == 'none':
cnt = 0
for loc in key_counter.free_locations:
if loc not in sm_restricted and (key_counter.big_key_opened or '- Big Chest' not in loc.name):
@@ -1172,6 +1174,16 @@ def expand_key_state(state, flat_proposal, world, player):
state.add_all_doors_check_keys(connect_region, flat_proposal, world, player)
def expand_big_key_state(state, flat_proposal, world, player):
while len(state.avail_doors) > 0:
exp_door = state.next_avail_door()
door = exp_door.door
connect_region = world.get_entrance(door.name, player).connected_region
if state.validate(door, connect_region, world, player):
state.visit_region(connect_region, key_checks=True)
state.add_all_doors_check_big_keys(connect_region, flat_proposal, world, player)
def flatten_pair_list(paired_list):
flat_list = []
for d in paired_list:
@@ -1401,13 +1413,51 @@ def prize_relevance(key_layout, dungeon_entrance, is_atgt_swapped):
return None
def prize_relevance_sig2(start_regions, d_name, dungeon_entrance, world, player):
if len(start_regions) > 1 and dungeon_entrance and dungeon_table[d_name].prize:
if dungeon_entrance.name == ('Agahmins Tower' if world.is_atgt_swapped(player) else 'Ganons Tower'):
return 'GT'
elif dungeon_entrance.name == 'Pyramid Fairy':
return 'BigBomb'
return None
def validate_bk_layout(proposal, builder, start_regions, world, player):
bk_special = check_bk_special(builder.master_sector.regions, world, player)
if world.bigkeyshuffle[player] and (world.dropshuffle[player] or not bk_special):
return True
flat_proposal = flatten_pair_list(proposal)
state = ExplorationState(dungeon=builder.name)
state.big_key_special = bk_special
for region in start_regions:
dungeon_entrance, portal_door = find_outside_connection(region)
prize_relevant_flag = prize_relevance_sig2(start_regions, builder.name, dungeon_entrance, world, player)
if prize_relevant_flag:
state.append_door_to_list(portal_door, state.prize_doors)
state.prize_door_set[portal_door] = dungeon_entrance
# key_layout.prize_relevant = prize_relevant_flag
else:
state.visit_region(region, key_checks=True)
state.add_all_doors_check_big_keys(region, flat_proposal, world, player)
expand_big_key_state(state, flat_proposal, world, player)
if bk_special:
for loc in state.found_locations:
if loc.forced_big_key():
return True
else:
return len(state.bk_found) > 0
return False
# Soft lock stuff
def validate_key_layout(key_layout, world, player):
# retro is all good - except for hyrule castle in standard mode
if (world.retro[player] and (world.mode[player] != 'standard' or key_layout.sector.name != 'Hyrule Castle')) or world.logic[player] == 'nologic':
# universal key is all good - except for hyrule castle in standard mode
if world.logic[player] == 'nologic' or (world.keyshuffle[player] == 'universal' and
(world.mode[player] != 'standard' or key_layout.sector.name != 'Hyrule Castle')):
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:
@@ -1442,7 +1492,8 @@ def validate_key_layout_sub_loop(key_layout, state, checked_states, flat_proposa
# 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
@@ -1532,8 +1583,8 @@ def enough_small_locations(state, avail_small_loc):
def determine_prize_lock(key_layout, world, player):
if ((world.retro[player] and (world.mode[player] != 'standard' or key_layout.sector.name != 'Hyrule Castle'))
or world.logic[player] == 'nologic'):
if world.logic[player] == 'nologic' or (world.keyshuffle[player] == 'universal' and
(world.mode[player] != 'standard' or key_layout.sector.name != 'Hyrule Castle')):
return # done, doesn't matter what
flat_proposal = key_layout.flat_prop
state = ExplorationState(dungeon=key_layout.sector.name)
@@ -1565,7 +1616,7 @@ def determine_prize_lock(key_layout, world, player):
def cnt_avail_small_locations(free_locations, key_only, state, world, player):
if not world.keyshuffle[player] and not world.retro[player]:
if world.keyshuffle[player] == 'none':
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)
return max(0, avail_chest_keys + key_only - state.used_smalls)
@@ -1573,7 +1624,7 @@ def cnt_avail_small_locations(free_locations, key_only, state, world, player):
def cnt_avail_small_locations_by_ctr(free_locations, counter, layout, world, player):
if not world.keyshuffle[player] and not world.retro[player]:
if world.keyshuffle[player] == 'none':
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)
return max(0, avail_chest_keys + len(counter.key_only_locations) - counter.used_keys)
@@ -1599,12 +1650,13 @@ 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
else:
builder = world.dungeon_layouts[player][key_layout.sector.name]
state.key_locations = builder.total_keys - builder.key_drop_cnt
state.key_locations = max(0, builder.total_keys - builder.key_drop_cnt)
state.big_key_special = False
for region in key_layout.sector.regions:
for location in region.locations:
@@ -1865,7 +1917,7 @@ def val_hyrule(key_logic, world, player):
val_rule(key_logic.door_rules['Hyrule Dungeon Map Room Key Door S'], 1)
val_rule(key_logic.door_rules['Hyrule Dungeon Armory Interior Key Door N'], 2)
val_rule(key_logic.door_rules['Sewers Dark Cross Key Door N'], 3)
val_rule(key_logic.door_rules['Sewers Key Rat Key Door N'], 4)
val_rule(key_logic.door_rules['Sewers Key Rat NE'], 4)
else:
val_rule(key_logic.door_rules['Sewers Secret Room Key Door S'], 2)
val_rule(key_logic.door_rules['Sewers Dark Cross Key Door N'], 2)
@@ -2016,14 +2068,14 @@ def val_rule(rule, skn, allow=False, loc=None, askn=None, setCheck=None):
# Soft lock stuff
def validate_key_placement(key_layout, world, player):
if world.retro[player] or world.accessibility[player] == 'none':
if world.keyshuffle[player] == 'universal' or world.accessibility[player] == 'none':
return True # Can't keylock in retro. Expected if beatable only.
max_counter = find_max_counter(key_layout)
keys_outside = 0
big_key_outside = False
smallkey_name = dungeon_keys[key_layout.sector.name]
bigkey_name = dungeon_bigs[key_layout.sector.name]
if world.keyshuffle[player]:
if world.keyshuffle[player] != 'none':
keys_outside = key_layout.max_chests - sum(1 for i in max_counter.free_locations if i.item is not None and i.item.name == smallkey_name and i.item.player == player)
if world.bigkeyshuffle[player]:
max_counter = find_max_counter(key_layout)
+75 -20
View File
@@ -25,14 +25,17 @@ 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_pots
from Fill import dungeon_tracking
from Fill import sell_potions, sell_keys, balance_multiworld_progression, balance_money_progression, lock_shop_locations, set_prize_drops
from ItemList import generate_itempool, difficulties, fill_prizes, customize_shops, create_farm_locations
from ItemList import generate_itempool, difficulties, fill_prizes, customize_shops, fill_specific_items, create_farm_locations
from Utils import output_path, parse_player_names
from source.item.FillUtil import create_item_pool_config, massage_item_pool, district_item_pool_config
from source.overworld.EntranceShuffle2 import link_entrances_new
from source.tools.BPS import create_bps_from_data
from source.classes.CustomSettings import CustomSettings
__version__ = '1.0.1.3-u'
__version__ = '1.2.0.0-u'
from source.classes.BabelFish import BabelFish
@@ -58,32 +61,44 @@ def main(args, seed=None, fish=None):
if args.securerandom:
random.use_secure()
seeded = False
# initialize the world
if args.code:
for player, code in args.code.items():
if code:
Settings.adjust_args_from_code(code, player, args)
customized = None
if args.customizer:
customized = CustomSettings()
customized.load_yaml(args.customizer)
seed = customized.determine_seed(seed)
seeded = True
customized.adjust_args(args)
world = World(args.multi, args.ow_shuffle, args.ow_crossed, args.ow_mixed, args.shuffle, args.door_shuffle, args.logic, args.mode, args.swords,
args.difficulty, args.item_functionality, args.timer, args.progressive, args.goal, args.algorithm,
args.accessibility, args.shuffleganon, args.retro, args.custom, args.customitemarray, args.hints)
args.accessibility, args.shuffleganon, args.custom, args.customitemarray, args.hints)
world.customizer = customized if customized else None
logger = logging.getLogger('')
if seed is None:
random.seed(None)
world.seed = random.randint(0, 999999999)
else:
world.seed = int(seed)
random.seed(world.seed)
if not seeded:
random.seed(world.seed)
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()
world.keyshuffle = args.keyshuffle.copy()
world.bigkeyshuffle = args.bigkeyshuffle.copy()
world.bombbag = args.bombbag.copy()
world.flute_mode = args.flute_mode.copy()
world.bow_mode = args.bow_mode.copy()
world.crystals_needed_for_ganon = {player: random.randint(0, 7) if args.crystals_ganon[player] == 'random' else int(args.crystals_ganon[player]) for player in range(1, world.players + 1)}
world.crystals_needed_for_gt = {player: random.randint(0, 7) if args.crystals_gt[player] == 'random' else int(args.crystals_gt[player]) for player in range(1, world.players + 1)}
world.crystals_ganon_orig = args.crystals_ganon.copy()
@@ -100,6 +115,8 @@ def main(args, seed=None, fish=None):
world.enemy_damage = args.enemy_damage.copy()
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.decoupledoors = args.decoupledoors.copy()
world.experimental = args.experimental.copy()
world.dungeon_counters = args.dungeon_counters.copy()
world.fish = fish
@@ -112,13 +129,16 @@ def main(args, seed=None, fish=None):
world.treasure_hunt_count = {k: int(v) for k, v in args.triforce_goal.items()}
world.treasure_hunt_total = {k: int(v) for k, v in args.triforce_pool.items()}
world.shufflelinks = args.shufflelinks.copy()
world.shuffletavern = args.shuffletavern.copy()
world.pseudoboots = args.pseudoboots.copy()
world.overworld_map = args.overworld_map.copy()
world.take_any = args.take_any.copy()
world.restrict_boss_items = args.restrict_boss_items.copy()
world.collection_rate = args.collection_rate.copy()
world.colorizepots = args.colorizepots.copy()
world.rom_seeds = {player: random.randint(0, 999999999) for player in range(1, world.players + 1)}
world.finish_init()
from OverworldShuffle import __version__ as ORVersion
logger.info(
@@ -139,6 +159,8 @@ def main(args, seed=None, fish=None):
for player, name in enumerate(team, 1):
world.player_names[player].append(name)
logger.info('')
world.settings = CustomSettings()
world.settings.create_from_world(world)
outfilebase = f'OR_{args.outputname if args.outputname else world.seed}'
@@ -151,7 +173,9 @@ def main(args, seed=None, fish=None):
if args.usestartinventory[player]:
for tok in filter(None, args.startinventory[player].split(',')):
item = ItemFactory(tok.strip(), player)
name = tok.strip()
name = name if name != 'Ocarina' or world.flute_mode[player] != 'active' else 'Ocarina (Activated)'
item = ItemFactory(name, player)
if item:
world.push_precollected(item)
@@ -172,6 +196,25 @@ def main(args, seed=None, fish=None):
adjust_locations(world, player)
place_bosses(world, player)
if world.customizer and world.customizer.get_start_inventory():
for p, inv_list in world.customizer.get_start_inventory().items():
for inv_item in inv_list:
item = ItemFactory(inv_item.strip(), p)
if item:
world.push_precollected(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:
d.big_key.remove(match)
if args.print_custom_yaml:
world.settings.record_info(world)
if any(world.potshuffle.values()):
logger.info(world.fish.translate("cli", "cli", "shuffling.pots"))
for player in range(1, world.players + 1):
@@ -196,9 +239,12 @@ def main(args, seed=None, fish=None):
link_entrances(world, player)
logger.info(world.fish.translate("cli", "cli", "shuffling.prep"))
for player in range(1, world.players + 1):
link_doors_prep(world, player)
if args.print_custom_yaml:
world.settings.record_entrances(world)
create_item_pool_config(world)
logger.info(world.fish.translate("cli", "cli", "shuffling.dungeons"))
@@ -206,6 +252,9 @@ def main(args, seed=None, fish=None):
for player in range(1, world.players + 1):
link_doors(world, player)
mark_light_dark_world_regions(world, player)
if args.print_custom_yaml:
world.settings.record_doors(world)
logger.info(world.fish.translate("cli", "cli", "generating.itempool"))
for player in range(1, world.players + 1):
@@ -224,19 +273,21 @@ def main(args, seed=None, fish=None):
for player in range(1, world.players + 1):
if world.shopsanity[player]:
sell_potions(world, player)
if world.retro[player]:
if world.keyshuffle[player] == 'universal':
sell_keys(world, player)
else:
lock_shop_locations(world, player)
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"))
fill_prizes(world)
# used for debugging
# fill_specific_items(world)
logger.info(world.fish.translate("cli","cli","placing.dungeon.items"))
if args.algorithm != 'equitable':
@@ -270,6 +321,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"))
@@ -280,6 +332,10 @@ def main(args, seed=None, fish=None):
balance_money_progression(world)
ensure_good_pots(world, True)
if args.print_custom_yaml:
world.settings.record_item_placements(world)
world.settings.write_to_file(output_path(f'{outfilebase}_custom.yaml'))
rom_names = []
jsonout = {}
enemized = False
@@ -398,7 +454,7 @@ def copy_world(world):
# ToDo: Not good yet
ret = World(world.players, world.owShuffle, world.owCrossed, world.owMixed, world.shuffle, world.doorShuffle, world.logic, world.mode, world.swords,
world.difficulty, world.difficulty_adjustments, world.timer, world.progressive, world.goal, world.algorithm,
world.accessibility, world.shuffle_ganon, world.retro, world.custom, world.customitemarray, world.hints)
world.accessibility, world.shuffle_ganon, world.custom, world.customitemarray, world.hints)
ret.teams = world.teams
ret.player_names = copy.deepcopy(world.player_names)
ret.remote_items = world.remote_items.copy()
@@ -427,6 +483,7 @@ def copy_world(world):
ret.keyshuffle = world.keyshuffle.copy()
ret.bigkeyshuffle = world.bigkeyshuffle.copy()
ret.bombbag = world.bombbag.copy()
ret.flute_mode = world.flute_mode.copy()
ret.crystals_needed_for_ganon = world.crystals_needed_for_ganon.copy()
ret.crystals_needed_for_gt = world.crystals_needed_for_gt.copy()
ret.crystals_ganon_orig = world.crystals_ganon_orig.copy()
@@ -445,6 +502,7 @@ def copy_world(world):
ret.enemy_damage = world.enemy_damage.copy()
ret.beemizer = world.beemizer.copy()
ret.intensity = world.intensity.copy()
ret.decoupledoors = world.decoupledoors.copy()
ret.experimental = world.experimental.copy()
ret.shopsanity = world.shopsanity.copy()
ret.dropshuffle = world.dropshuffle.copy()
@@ -517,12 +575,9 @@ 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
if location.skip:
new_location.skip = True
new_location.event = location.event
new_location.locked = location.locked
new_location.skip = location.skip
# these need to be modified properly by set_rules
new_location.access_rule = lambda state: True
new_location.item_rule = lambda state: True
@@ -576,7 +631,7 @@ def copy_world_premature(world, player):
# ToDo: Not good yet
ret = World(world.players, world.owShuffle, world.owCrossed, world.owMixed, world.shuffle, world.doorShuffle, world.logic, world.mode, world.swords,
world.difficulty, world.difficulty_adjustments, world.timer, world.progressive, world.goal, world.algorithm,
world.accessibility, world.shuffle_ganon, world.retro, world.custom, world.customitemarray, world.hints)
world.accessibility, world.shuffle_ganon, world.custom, world.customitemarray, world.hints)
ret.teams = world.teams
ret.player_names = copy.deepcopy(world.player_names)
ret.remote_items = world.remote_items.copy()
@@ -775,8 +830,8 @@ def create_playthrough(world):
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:
+4 -197
View File
@@ -1,22 +1,21 @@
import argparse
import logging
from pathlib import Path
import os
import RaceRandom as random
import urllib.request
import urllib.parse
import yaml
from DungeonRandomizer import parse_cli
from Main import main as DRMain
from source.classes.BabelFish import BabelFish
from yaml.constructor import SafeConstructor
from source.tools.MysteryUtils import roll_settings, get_weights
def add_bool(self, node):
return self.construct_scalar(node)
SafeConstructor.add_constructor(u'tag:yaml.org,2002:bool', add_bool)
def main():
parser = argparse.ArgumentParser(add_help=False)
parser.add_argument('--multi', default=1, type=lambda value: min(max(int(value), 1), 255))
@@ -107,198 +106,6 @@ def main():
DRMain(erargs, seed, BabelFish())
def get_weights(path):
if os.path.exists(Path(path)):
with open(path, "r", encoding="utf-8") as f:
return yaml.load(f, Loader=yaml.SafeLoader)
elif urllib.parse.urlparse(path).scheme in ['http', 'https']:
return yaml.load(urllib.request.urlopen(path), Loader=yaml.FullLoader)
def roll_settings(weights):
def get_choice(option, root=None):
root = weights if root is None else root
if option not in root:
return None
if type(root[option]) is not dict:
return root[option]
if not root[option]:
return None
return random.choices(list(root[option].keys()), weights=list(map(int,root[option].values())))[0]
def get_choice_default(option, root=weights, default=None):
choice = get_choice(option, root)
if choice is None and default is not None:
return default
return choice
while True:
subweights = weights.get('subweights', {})
if len(subweights) == 0:
break
chances = ({k: int(v['chance']) for (k, v) in subweights.items()})
subweight_name = random.choices(list(chances.keys()), weights=list(chances.values()))[0]
subweights = weights.get('subweights', {}).get(subweight_name, {}).get('weights', {})
subweights['subweights'] = subweights.get('subweights', {})
weights = {**weights, **subweights}
ret = argparse.Namespace()
ret.algorithm = get_choice('algorithm')
glitch_map = {'none': 'noglitches', 'no_logic': 'nologic', 'owglitches': 'owglitches',
'owg': 'owglitches', 'minorglitches': 'minorglitches'}
glitches_required = get_choice('glitches_required')
if glitches_required is not None:
if glitches_required not in glitch_map.keys():
print(f'Logic did not match one of: {", ".join(glitch_map.keys())}')
glitches_required = 'none'
ret.logic = glitch_map[glitches_required]
item_placement = get_choice('item_placement')
# not supported in ER
dungeon_items = get_choice('dungeon_items')
ret.mapshuffle = get_choice('map_shuffle') == 'on' if 'map_shuffle' in weights else dungeon_items in ['mc', 'mcs', 'full']
ret.compassshuffle = get_choice('compass_shuffle') == 'on' if 'compass_shuffle' in weights else dungeon_items in ['mc', 'mcs', 'full']
ret.keyshuffle = get_choice('smallkey_shuffle') == 'on' if 'smallkey_shuffle' in weights else dungeon_items in ['mcs', 'full']
ret.bigkeyshuffle = get_choice('bigkey_shuffle') == 'on' if 'bigkey_shuffle' in weights else dungeon_items in ['full']
ret.accessibility = get_choice('accessibility')
ret.restrict_boss_items = get_choice('restrict_boss_items')
overworld_shuffle = get_choice('overworld_shuffle')
ret.ow_shuffle = overworld_shuffle if overworld_shuffle != 'none' else 'vanilla'
ret.ow_terrain = get_choice('overworld_terrain') == 'on'
valid_options = {'none', 'polar', 'grouped', 'limited', 'chaos'}
ret.ow_crossed = get_choice('overworld_crossed')
ret.ow_crossed = ret.ow_crossed if ret.ow_crossed in valid_options else 'none'
ret.ow_keepsimilar = get_choice('overworld_keepsimilar') == 'on'
ret.ow_mixed = get_choice('overworld_swap') == 'on'
ret.ow_whirlpool = get_choice('whirlpool_shuffle') == 'on'
overworld_flute = get_choice('flute_shuffle')
ret.ow_fluteshuffle = overworld_flute if overworld_flute != 'none' else 'vanilla'
ret.bonk_drops = get_choice('bonk_drops') == 'on'
entrance_shuffle = get_choice('entrance_shuffle')
ret.shuffle = entrance_shuffle if entrance_shuffle != 'none' else 'vanilla'
overworld_map = get_choice('overworld_map')
ret.overworld_map = overworld_map if overworld_map != 'default' else 'default'
door_shuffle = get_choice('door_shuffle')
ret.door_shuffle = door_shuffle if door_shuffle != 'none' else 'vanilla'
ret.intensity = get_choice('intensity')
ret.experimental = get_choice('experimental') == 'on'
ret.collection_rate = get_choice('collection_rate') == 'on'
ret.dungeon_counters = get_choice('dungeon_counters') if 'dungeon_counters' in weights else 'default'
if ret.dungeon_counters == 'default':
ret.dungeon_counters = 'pickup' if ret.door_shuffle != 'vanilla' or ret.compassshuffle == 'on' else 'off'
ret.pseudoboots = get_choice('pseudoboots') == 'on'
ret.shopsanity = get_choice('shopsanity') == 'on'
ret.dropshuffle = get_choice('dropshuffle') == 'on'
ret.pottery = get_choice('pottery') if 'pottery' in weights else 'none'
ret.colorizepots = get_choice('colorizepots') == '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 else 'standardize'
goal = get_choice('goals')
if goal is not None:
ret.goal = {'ganon': 'ganon',
'fast_ganon': 'crystals',
'dungeons': 'dungeons',
'pedestal': 'pedestal',
'triforce-hunt': 'triforcehunt',
'trinity': 'trinity'
}[goal]
ret.openpyramid = get_choice('open_pyramid') if 'open_pyramid' in weights else 'auto'
ret.shuffleganon = get_choice('shuffleganon') == 'on'
ret.shufflelinks = get_choice('shufflelinks') == 'on'
ret.crystals_gt = get_choice('tower_open')
ret.crystals_ganon = get_choice('ganon_open')
from ItemList import set_default_triforce
default_tf_goal, default_tf_pool = set_default_triforce(ret.goal, 0, 0)
goal_min = get_choice_default('triforce_goal_min', default=default_tf_goal)
goal_max = get_choice_default('triforce_goal_max', default=default_tf_goal)
pool_min = get_choice_default('triforce_pool_min', default=default_tf_pool)
pool_max = get_choice_default('triforce_pool_max', default=default_tf_pool)
ret.triforce_goal = random.randint(int(goal_min), int(goal_max))
min_diff = get_choice_default('triforce_min_difference', default=(default_tf_pool-default_tf_goal))
ret.triforce_pool = random.randint(max(int(pool_min), ret.triforce_goal + int(min_diff)), int(pool_max))
ret.mode = get_choice('world_state')
if ret.mode == 'retro':
ret.mode = 'open'
ret.retro = True
ret.retro = get_choice('retro') == 'on' # this overrides world_state if used
ret.bombbag = get_choice('bombbag') == 'on'
ret.hints = get_choice('hints') == 'on'
swords = get_choice('weapons')
if swords is not None:
ret.swords = {'randomized': 'random',
'assured': 'assured',
'vanilla': 'vanilla',
'swordless': 'swordless'
}[swords]
ret.difficulty = get_choice('item_pool')
ret.item_functionality = get_choice('item_functionality')
old_style_bosses = {'basic': 'simple',
'normal': 'full',
'chaos': 'random'}
boss_choice = get_choice('boss_shuffle')
if boss_choice in old_style_bosses.keys():
boss_choice = old_style_bosses[boss_choice]
ret.shufflebosses = boss_choice
enemy_choice = get_choice('enemy_shuffle')
if enemy_choice == 'chaos':
enemy_choice = 'random'
ret.shuffleenemies = enemy_choice
old_style_damage = {'none': 'default',
'chaos': 'random'}
damage_choice = get_choice('enemy_damage')
if damage_choice in old_style_damage:
damage_choice = old_style_damage[damage_choice]
ret.enemy_damage = damage_choice
ret.enemy_health = get_choice('enemy_health')
ret.beemizer = get_choice('beemizer') if 'beemizer' in weights else '0'
inventoryweights = weights.get('startinventory', {})
startitems = []
for item in inventoryweights.keys():
if get_choice(item, inventoryweights) == 'on':
startitems.append(item)
ret.startinventory = ','.join(startitems)
if len(startitems) > 0:
ret.usestartinventory = True
if 'rom' in weights:
romweights = weights['rom']
ret.sprite = get_choice('sprite', romweights)
ret.disablemusic = get_choice('disablemusic', romweights) == 'on'
ret.quickswap = get_choice('quickswap', romweights) == 'on'
ret.reduce_flashing = get_choice('reduce_flashing', romweights) == 'on'
ret.msu_resume = get_choice('msu_resume', romweights) == 'on'
ret.fastmenu = get_choice('menuspeed', romweights)
ret.heartcolor = get_choice('heartcolor', romweights)
ret.heartbeep = get_choice('heartbeep', romweights)
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'
return ret
if __name__ == '__main__':
main()
+10 -9
View File
@@ -93,12 +93,12 @@ vanilla_pots = {
Pot(28, 23, PotItem.Nothing, 'Ice Pengator Switch', obj=RoomObject(0x1FC388, [0x3B, 0xBB, 0xFA])),
Pot(86, 26, PotItem.Nothing, 'Ice Big Key', obj=RoomObject(0x1FC397, [0xAF, 0xD3, 0xFA])),
Pot(86, 27, PotItem.Nothing, 'Ice Big Key', obj=RoomObject(0x1FC39A, [0xAF, 0xDB, 0xFA]))],
0x21: [Pot(160, 20, PotItem.Nothing, 'Sewers Key Rat', obj=RoomObject(0x0A8C71, [0x43, 0xA7, 0xFA])),
Pot(168, 24, PotItem.SmallMagic, 'Sewers Key Rat', obj=RoomObject(0x0A8C7A, [0x53, 0xC7, 0xFA])),
Pot(48, 28, PotItem.Heart, 'Sewers Key Rat', obj=RoomObject(0x0A8C80, [0x63, 0xE3, 0xFA])),
Pot(82, 28, PotItem.SmallMagic, 'Sewers Key Rat', obj=RoomObject(0x0A8C7D, [0xA7, 0xE3, 0xFA])),
Pot(100, 28, PotItem.Nothing, 'Sewers Key Rat', obj=RoomObject(0x0A8C74, [0xCB, 0xE3, 0xFA])),
Pot(104, 28, PotItem.Nothing, 'Sewers Key Rat', obj=RoomObject(0x0A8C77, [0xD3, 0xE3, 0xFA]))],
0x21: [Pot(160, 20, PotItem.Nothing, 'Sewers Dark Aquabats', obj=RoomObject(0x0A8C71, [0x43, 0xA7, 0xFA])),
Pot(168, 24, PotItem.SmallMagic, 'Sewers Dark Aquabats', obj=RoomObject(0x0A8C7A, [0x53, 0xC7, 0xFA])),
Pot(48, 28, PotItem.Heart, 'Sewers Dark Aquabats', obj=RoomObject(0x0A8C80, [0x63, 0xE3, 0xFA])),
Pot(82, 28, PotItem.SmallMagic, 'Sewers Dark Aquabats', obj=RoomObject(0x0A8C7D, [0xA7, 0xE3, 0xFA])),
Pot(100, 28, PotItem.Nothing, 'Sewers Dark Aquabats', obj=RoomObject(0x0A8C74, [0xCB, 0xE3, 0xFA])),
Pot(104, 28, PotItem.Nothing, 'Sewers Dark Aquabats', obj=RoomObject(0x0A8C77, [0xD3, 0xE3, 0xFA]))],
0x23: [Pot(86, 26, PotItem.OneRupee, 'TR Lazy Eyes', obj=RoomObject(0x1FED09, [0xAF, 0xD3, 0xFA])),
Pot(90, 26, PotItem.Heart, 'TR Lazy Eyes', obj=RoomObject(0x1FED0C, [0xB7, 0xD3, 0xFA])),
Pot(94, 26, PotItem.OneRupee, 'TR Lazy Eyes', obj=RoomObject(0x1FED0F, [0xBF, 0xD3, 0xFA])),
@@ -879,7 +879,8 @@ def shuffle_pots(world, player):
elif old_pot.item == PotItem.Switch:
available_pots = (pot for pot in new_pots if (pot.room == old_pot.room or pot.room in movable_switch_rooms[old_pot.room]) and not (pot.flags & PotFlags.NoSwitch))
elif old_pot.item == PotItem.Key:
if world.doorShuffle[player] == 'vanilla' and not world.retro[player] and world.pottery[player] == 'none' and world.logic[player] != 'nologic':
if (world.doorShuffle[player] == 'vanilla' and world.keyshuffle[player] != 'universal'
and world.pottery[player] == 'none' and world.logic[player] != 'nologic'):
available_pots = (pot for pot in new_pots if pot.room not in invalid_key_rooms)
else:
available_pots = new_pots
@@ -890,7 +891,7 @@ def shuffle_pots(world, player):
new_pot = random.choice(available_pots)
new_pot.item = old_pot.item
if world.retro[player] and new_pot.item == PotItem.FiveArrows:
if world.bow_mode[player].startswith('retro') and new_pot.item == PotItem.FiveArrows:
new_pot.item = PotItem.FiveRupees
if new_pot.item == PotItem.Key:
@@ -938,7 +939,7 @@ def shuffle_pot_switches(world, player):
new_pot = random.choice(available_pots)
new_pot.item, old_pot.item = old_pot.item, new_pot.item
if world.retro[player] and new_pot.item == PotItem.FiveArrows:
if world.bow_mode[player].startswith('retro') and new_pot.item == PotItem.FiveArrows:
new_pot.item = PotItem.FiveRupees
if new_pot.item == PotItem.Switch and (new_pot.flags & PotFlags.SwitchLogicChange):
+69 -230
View File
@@ -1,270 +1,109 @@
## 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 pot 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
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.
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.
## Customizer
### Goal: Trinity
Please see [Customizer documentation](docs/Customizer.md) on how to create custom seeds.
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.
## Standard Generation Change
### Boss Shuffle: Unique
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.
At least one boss each of the prize bosses will be present guarding the prizes. GT bosses can be anything.
## ER Features
### MSU Resume
### New Experimental Algorithm
Turns on msu resume support. Found on "Game Options" tab, the "Adjust/Patch" tab, or use the `--msu_resume` CLI option.
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.
### BPS Patch
### Lite/Lean ER (Experimental required)
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.
Designed by Codemann, these are available now (only with experimental turned on - they otherwise fail)
## New Font
#### 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
Font updated to support lowercase English. Lowercase vs. uppercase typos may exist. Note, you can use lowercase English letters on the file name.
#### 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
## Restricted Item Placement Algorithm
### Back of Tavern Shuffle (Experimental required)
The "Item Sorting" option or ```--algorithm``` has been updated with new placement algorithms. Older algorithms have been removed.
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.
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.
#### Take Any Caves
Here are the current fill options:
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:
### Balanced
* 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)
This one stays the same as before and is recommended for the most random distribution of items.
# Bug Fixes and Notes
### Vanilla Fill
None yet
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.
# Known Issues
### 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.
## 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
#### 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
* Standing in the doorway when defeating Aga 1 and being teleported to the Dark World will not clear door state. It may cause issues requiring a Save & Quit to fix.
* 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. A bomb jump to get to those pot may be required if you don't have boots to bonk across.
+11 -3
View File
@@ -421,7 +421,10 @@ def create_dungeon_regions(world, player):
create_dungeon_region(player, 'Sewers Rope Room', 'Hyrule Castle', None, ['Sewers Rope Room Up Stairs', 'Sewers Rope Room North Stairs']),
create_dungeon_region(player, 'Sewers Dark Cross', 'Hyrule Castle', ['Sewers - Dark Cross'], ['Sewers Dark Cross Key Door N', 'Sewers Dark Cross South Stairs']),
create_dungeon_region(player, 'Sewers Water', 'Hyrule Castle', None, ['Sewers Water S', 'Sewers Water W']),
create_dungeon_region(player, 'Sewers Key Rat', 'Hyrule Castle', ['Hyrule Castle - Key Rat Key Drop'], ['Sewers Key Rat E', 'Sewers Key Rat Key Door N']),
create_dungeon_region(player, 'Sewers Dark Aquabats', 'Hyrule Castle', None,
['Sewers Dark Aquabats ES', 'Sewers Dark Aquabats N']),
create_dungeon_region(player, 'Sewers Key Rat', 'Hyrule Castle', ['Hyrule Castle - Key Rat Key Drop'],
['Sewers Key Rat S', 'Sewers Key Rat NE']),
create_dungeon_region(player, 'Sewers Secret Room Blocked Path', 'Hyrule Castle', None, ['Sewers Secret Room Up Stairs']),
create_dungeon_region(player, 'Sewers Rat Path', 'Hyrule Castle', None, ['Sewers Secret Room Key Door S', 'Sewers Secret Room Push Block', 'Sewers Rat Path WS', 'Sewers Rat Path WN']),
create_dungeon_region(player, 'Sewers Secret Room', 'Hyrule Castle', ['Sewers - Secret Room - Left', 'Sewers - Secret Room - Middle', 'Sewers - Secret Room - Right'],
@@ -770,7 +773,12 @@ def create_dungeon_regions(world, player):
create_dungeon_region(player, 'Mire Hub Switch', 'Misery Mire', ['Misery Mire - Main Lobby'], ['Mire Hub Switch Blue Barrier N', 'Mire Hub Switch Blue Barrier S']),
create_dungeon_region(player, 'Mire Lone Shooter', 'Misery Mire', None, ['Mire Lone Shooter WS', 'Mire Lone Shooter ES']),
create_dungeon_region(player, 'Mire Failure Bridge', 'Misery Mire', None, ['Mire Failure Bridge W', 'Mire Failure Bridge E']),
create_dungeon_region(player, 'Mire Falling Bridge', 'Misery Mire', ['Misery Mire - Big Chest'], ['Mire Falling Bridge WS', 'Mire Falling Bridge W', 'Mire Falling Bridge WN']),
create_dungeon_region(player, 'Mire Falling Bridge - Failure', 'Misery Mire', None,
['Mire Falling Bridge W', 'Mire Falling Bridge Hook Only Path', 'Mire Falling Bridge Primary Path']),
create_dungeon_region(player, 'Mire Falling Bridge - Primary', 'Misery Mire', None,
['Mire Falling Bridge WS', 'Mire Falling Bridge Hook Path', 'Mire Falling Bridge Failure Path']),
create_dungeon_region(player, 'Mire Falling Bridge - Chest', 'Misery Mire', ['Misery Mire - Big Chest'],
['Mire Falling Bridge WN']),
create_dungeon_region(player, 'Mire Map Spike Side', 'Misery Mire', None, ['Mire Map Spike Side EN', 'Mire Map Spike Side Drop Down', 'Mire Map Spike Side Blue Barrier']),
create_dungeon_region(player, 'Mire Map Spot', 'Misery Mire', ['Misery Mire - Map Chest'], ['Mire Map Spot WN', 'Mire Map Spot Blue Barrier']),
create_dungeon_region(player, 'Mire Crystal Dead End', 'Misery Mire', None, ['Mire Crystal Dead End Left Barrier', 'Mire Crystal Dead End Right Barrier', 'Mire Crystal Dead End NW']),
@@ -1174,7 +1182,7 @@ def adjust_locations(world, player):
loc.event = False
item_dungeon = key_item.dungeon
dungeon = world.get_dungeon(item_dungeon, player)
if key_item.smallkey and not world.retro[player]:
if key_item.smallkey and world.keyshuffle[player] != 'universal':
dungeon.small_keys.append(key_item)
elif key_item.bigkey:
dungeon.big_key = key_item
+74 -81
View File
@@ -38,7 +38,7 @@ from source.dungeon.RoomList import Room0127
JAP10HASH = '03a63945398191337e896e5771f77173'
RANDOMIZERBASEHASH = '3954c4e449c908a9e086a1fd5797c98b'
RANDOMIZERBASEHASH = '7a6619830c3521f2a09c4c64cd8d7d75'
class JsonRom(object):
@@ -829,7 +829,7 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False):
# Thanks to Zarby89 for originally finding these values
# todo fix screen scrolling
if world.shuffle[player] not in ['insanity', 'insanity_legacy', 'madness_legacy'] and \
if world.shuffle[player] not in ['insanity'] and \
exit.name in ['Eastern Palace Exit', 'Tower of Hera Exit', 'Thieves Town Exit', 'Ice Palace Exit', 'Misery Mire Exit',
'Palace of Darkness Exit', 'Swamp Palace Exit', 'Ganons Tower Exit', 'Desert Palace Exit (North)', 'Agahnims Tower Exit', 'Spiral Cave Exit (Top)',
'Superbunny Cave Exit (Bottom)', 'Turtle Rock Ledge Exit (East)']:
@@ -860,16 +860,18 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False):
else:
# patch door table
rom.write_byte(0xDBB73 + exit.addresses, exit.target)
if exit.name == 'Tavern North':
rom.write_byte(0x157D0, exit.target)
if world.mode[player] == 'inverted':
patch_shuffled_dark_sanc(world, rom, player)
# setup dr option flags based on experimental, etc.
dr_flags = DROptions.Eternal_Mini_Bosses if world.doorShuffle[player] == 'vanilla' else DROptions.Town_Portal
if world.doorShuffle[player] == 'crossed':
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']:
dr_flags |= DROptions.Debug
if world.doorShuffle[player] == 'crossed' and world.logic[player] != 'nologic'\
if world.doorShuffle[player] not in ['vanilla', 'basic'] and world.logic[player] != 'nologic'\
and world.mixed_travel[player] == 'prevent':
# PoD Falling Bridge or Hammjump
# 1FA607: db $2D, $79, $69 ; 0x0069: Vertical Rail ↕ | { 0B, 1E } | Size: 05
@@ -890,6 +892,8 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False):
dr_flags |= DROptions.DarkWorld_Spawns
if world.logic[player] != 'nologic':
dr_flags |= DROptions.Fix_EG
if world.door_type_mode[player] in ['big', 'all', 'chaos']:
dr_flags |= DROptions.BigKeyDoor_Shuffle
my_locations = world.get_filled_locations(player)
valid_locations = [l for l in my_locations if ((l.type == LocationType.Pot and not l.forced_item)
@@ -900,7 +904,8 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False):
valid_loc_by_dungeon = valid_dungeon_locations(valid_locations)
# fix hc big key problems (map and compass too)
if world.doorShuffle[player] == 'crossed' or world.dropshuffle[player] or world.pottery[player] not in ['none', 'cave']:
if (world.doorShuffle[player] not in ['vanilla', 'basic'] or world.dropshuffle[player]
or world.pottery[player] not in ['none', 'cave']):
rom.write_byte(0x151f1, 2)
rom.write_byte(0x15270, 2)
sanctuary = world.get_region('Sanctuary', player)
@@ -929,19 +934,19 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False):
rom.write_bytes(0x13fff4, [0xe4, 0x00])
# patch doors
if world.doorShuffle[player] == 'crossed':
if world.doorShuffle[player] not in ['vanilla', 'basic']:
rom.write_byte(0x138002, 2)
for name, layout in world.key_layout[player].items():
offset = compass_data[name][4]//2
if world.retro[player]:
if world.keyshuffle[player] == 'universal':
rom.write_byte(0x13f030+offset, layout.max_chests + layout.max_drops)
else:
rom.write_byte(0x13f020+offset, layout.max_chests + layout.max_drops) # not currently used
rom.write_byte(0x13f030+offset, layout.max_chests)
builder = world.dungeon_layouts[player][name]
valid_cnt = len(valid_loc_by_dungeon[name])
if valid_cnt > 99:
logging.getLogger('').warning(f'{name} exceeds 99 in locations ({valid_cnt})')
if valid_cnt > 256:
logging.getLogger('').warning(f'{name} exceeds 256 in locations ({valid_cnt})')
rom.write_byte(0x13f080+offset, valid_cnt % 10)
rom.write_byte(0x13f090+offset, valid_cnt // 10)
rom.write_byte(0x13f0a0+offset, valid_cnt)
@@ -969,7 +974,8 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False):
if world.doorShuffle[player] != 'vanilla':
for name, pair in boss_indicator.items():
dungeon_id, boss_door = pair
opposite_door = world.get_door(boss_door, player).dest
boss_region = world.get_door(boss_door, player).entrance.parent_region
opposite_door = next(iter(x for x in boss_region.entrances if x.name != 'Skull Final Drop WS')).door
if opposite_door and isinstance(opposite_door, Door) and opposite_door.roomIndex > -1:
dungeon_name = opposite_door.entrance.parent_region.dungeon.name
dungeon_id = boss_indicator[dungeon_name][0]
@@ -1033,7 +1039,7 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False):
rom.write_byte(0x142A50, 1) # StandingItemsOn
multiClientFlags = ((0x1 if world.dropshuffle[player] else 0)
| (0x2 if world.shopsanity[player] else 0)
| (0x4 if world.retro[player] else 0)
| (0x4 if world.take_any[player] != 'none' else 0)
| (0x8 if world.pottery[player] != 'none' else 0)
| (0x10 if is_mystery else 0))
rom.write_byte(0x142A51, multiClientFlags)
@@ -1219,42 +1225,8 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False):
for i in range(1024):
rom.write_byte(0x178000 + i, random.randint(0, 255))
# shuffle prize packs
pack_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]
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,
0xDD, 0xDD, 0xDD, 0xDD, 0xDD, 0xDE, 0xDE, 0xDE, 0xDE, 0xDE,
0xDF, 0xDF, 0xDF, 0xDF, 0xDF, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0,
0xE1, 0xE1, 0xE1, 0xE1, 0xE1, 0xE2, 0xE2, 0xE2, 0xE2, 0xE2,
0xE3, 0xE3, 0xE3, 0xE3, 0xE3]
def chunk(l,n):
return [l[i:i+n] for i in range(0, len(l), n)]
#shuffle order of 7 main packs
packs = chunk(pack_prizes, 8)
random.shuffle(packs)
pack_prizes = [drop for pack in packs for drop in pack]
if world.difficulty_adjustments[player] in ['hard', 'expert']:
prize_replacements = {0xE0: 0xDF, # Fairy -> heart
0xE3: 0xD8} # Big magic -> small magic
pack_prizes = [prize_replacements.get(prize, prize) for prize in pack_prizes]
dig_prizes = [prize_replacements.get(prize, prize) for prize in dig_prizes]
if world.retro[player]:
prize_replacements = {0xE1: 0xDA, #5 Arrows -> Blue Rupee
0xE2: 0xDB} #10 Arrows -> Red Rupee
pack_prizes = [prize_replacements.get(prize, prize) for prize in pack_prizes]
dig_prizes = [prize_replacements.get(prize, prize) for prize in dig_prizes]
rom.write_bytes(0x180100, dig_prizes)
# write dig prizes
rom.write_bytes(0x180100, world.prizes[player]['dig'])
# write tree pull prizes
rom.write_byte(0xEFBD4, world.prizes[player]['pull'][0])
@@ -1272,7 +1244,7 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False):
rom.write_byte(0xE82CC, world.prizes[player]['fish'])
# fill enemy prize packs
rom.write_bytes(0x37A78, pack_prizes)
rom.write_bytes(0x37A78, world.prizes[player]['enemies'])
# Fill in item substitutions table
rom.write_bytes(0x184000, [
@@ -1280,7 +1252,7 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False):
0x12, 0x01, 0x35, 0xFF, # lamp -> 5 rupees
0x51, 0x00 if world.bombbag[player] else 0x06, 0x31 if world.bombbag[player] else 0x52, 0xFF, # 6 +5 bomb upgrades -> +10 bomb upgrade. If bombbag -> turns into Bombs (10)
0x53, 0x06, 0x54, 0xFF, # 6 +5 arrow upgrades -> +10 arrow upgrade
0x58, 0x01, 0x36 if world.retro[player] else 0x43, 0xFF, # silver arrows -> single arrow (red 20 in retro mode)
0x58, 0x01, 0x36 if world.bow_mode[player].startswith('retro') else 0x43, 0xFF, # silver arrows -> single arrow (red 20 in retro mode)
0x3E, difficulty.boss_heart_container_limit, 0x47, 0xff, # boss heart -> green 20
0x17, difficulty.heart_piece_limit, 0x47, 0xff, # piece of heart -> green 20
0xFF, 0xFF, 0xFF, 0xFF, # end of table sentinel
@@ -1329,7 +1301,7 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False):
ERtimeincrease = 10
else:
ERtimeincrease = 20
if world.keyshuffle[player] or world.bigkeyshuffle[player] or world.mapshuffle[player]:
if world.keyshuffle[player] != 'none' or world.bigkeyshuffle[player] or world.mapshuffle[player]:
ERtimeincrease = ERtimeincrease + 15
if world.clock_mode == 'none':
rom.write_bytes(0x180190, [0x00, 0x00, 0x00]) # turn off clock mode
@@ -1469,7 +1441,7 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False):
# m - enabled for inside maps
# c - enabled for inside compasses
# s - enabled for inside small keys
rom.write_byte(0x18016A, 0x10 | ((0x01 if world.keyshuffle[player] else 0x00)
rom.write_byte(0x18016A, 0x10 | ((0x01 if world.keyshuffle[player] == 'wild' else 0x00)
| (0x02 if world.compassshuffle[player] else 0x00)
| (0x04 if world.mapshuffle[player] else 0x00)
| (0x08 if world.bigkeyshuffle[player] else 0x00))) # free roaming item text boxes
@@ -1520,7 +1492,7 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False):
if len(portal_list) == 1:
portal_idx = 0
else:
if world.doorShuffle[player] == 'crossed':
if world.doorShuffle[player] not in ['vanilla', 'basic']:
# the random choice excludes sanctuary
portal_idx = next((i for i, elem in enumerate(portal_list)
if world.get_portal(elem, player).chosen), random.choice([1, 2, 3]))
@@ -1544,7 +1516,7 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False):
rom.write_byte(0x53EA6+ow_map_index, world_indicator)
# in crossed doors - flip the compass exists flags
if world.doorShuffle[player] == 'crossed':
if world.doorShuffle[player] not in ['vanilla', 'basic']:
for dungeon, portal_list in dungeon_portals.items():
ow_map_index = dungeon_table[dungeon].map_index
exists_flag = any(x for x in world.get_dungeon(dungeon, player).dungeon_items if x.type == 'Compass')
@@ -1561,7 +1533,7 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False):
# a - Small Key
#
enable_menu_map_check = world.overworld_map[player] != 'default' and world.shuffle[player] != 'none'
rom.write_byte(0x180045, ((0x01 if world.keyshuffle[player] else 0x00)
rom.write_byte(0x180045, ((0x01 if world.keyshuffle[player] == 'wild' else 0x00)
| (0x02 if world.bigkeyshuffle[player] else 0x00)
| (0x04 if world.mapshuffle[player] or enable_menu_map_check else 0x00)
| (0x08 if world.compassshuffle[player] else 0x00))) # free roaming items in menu
@@ -1592,17 +1564,17 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False):
write_int16(rom, 0x18017A, get_reveal_bytes('Green Pendant') if world.mapshuffle[player] else 0x0000) # Sahasrahla reveal
write_int16(rom, 0x18017C, get_reveal_bytes('Crystal 5')|get_reveal_bytes('Crystal 6') if world.mapshuffle[player] else 0x0000) # Bomb Shop Reveal
rom.write_byte(0x180172, 0x01 if world.retro[player] else 0x00) # universal keys
rom.write_byte(0x180175, 0x01 if world.retro[player] else 0x00) # rupee bow
rom.write_byte(0x180176, 0x0A if world.retro[player] else 0x00) # wood arrow cost
rom.write_byte(0x180178, 0x32 if world.retro[player] else 0x00) # silver arrow cost
rom.write_byte(0x301FC, 0xDA if world.retro[player] else 0xE1) # rupees replace arrows under pots
rom.write_byte(0x180172, 0x01 if world.keyshuffle[player] == 'universal' else 0x00) # universal keys
rom.write_byte(0x180175, 0x01 if world.bow_mode[player].startswith('retro') else 0x00) # rupee bow
rom.write_byte(0x180176, 0x0A if world.bow_mode[player].startswith('retro') else 0x00) # wood arrow cost
rom.write_byte(0x180178, 0x32 if world.bow_mode[player].startswith('retro') else 0x00) # silver arrow cost
rom.write_byte(0x301FC, 0xDA if world.bow_mode[player].startswith('retro') else 0xE1) # rupees replace arrows under pots
if enemized:
rom.write_byte(0x1B152e, 0xDA if world.retro[player] else 0xE1)
rom.write_byte(0x30052, 0xDB if world.retro[player] else 0xE2) # replace arrows in fish prize from bottle merchant
rom.write_bytes(0xECB4E, [0xA9, 0x00, 0xEA, 0xEA] if world.retro[player] else [0xAF, 0x77, 0xF3, 0x7E]) # Thief steals rupees instead of arrows
rom.write_bytes(0xF0D96, [0xA9, 0x00, 0xEA, 0xEA] if world.retro[player] else [0xAF, 0x77, 0xF3, 0x7E]) # Pikit steals rupees instead of arrows
rom.write_bytes(0xEDA5, [0x35, 0x41] if world.retro[player] else [0x43, 0x44]) # Chest game gives rupees instead of arrows
rom.write_byte(0x1B152e, 0xDA if world.bow_mode[player].startswith('retro') else 0xE1)
rom.write_byte(0x30052, 0xDB if world.bow_mode[player].startswith('retro') else 0xE2) # replace arrows in fish prize from bottle merchant
rom.write_bytes(0xECB4E, [0xA9, 0x00, 0xEA, 0xEA] if world.bow_mode[player].startswith('retro') else [0xAF, 0x77, 0xF3, 0x7E]) # Thief steals rupees instead of arrows
rom.write_bytes(0xF0D96, [0xA9, 0x00, 0xEA, 0xEA] if world.bow_mode[player].startswith('retro') else [0xAF, 0x77, 0xF3, 0x7E]) # Pikit steals rupees instead of arrows
rom.write_bytes(0xEDA5, [0x35, 0x41] if world.bow_mode[player].startswith('retro') else [0x43, 0x44]) # Chest game gives rupees instead of arrows
digging_game_rng = random.randint(1, 30) # set rng for digging game
rom.write_byte(0x180020, digging_game_rng)
rom.write_byte(0xEFD95, digging_game_rng)
@@ -1647,11 +1619,13 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False):
rom.write_bytes(0x180188, [0x20, 0, 0]) # Zelda respawn refills (magic, bombs, arrows)
rom.write_bytes(0x18018B, [0x20, 0, 0]) # Mantle respawn refills (magic, bombs, arrows)
magic_max, magic_small = 0x80, 0x20
if world.doorShuffle[player] == 'crossed':
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)
@@ -2175,7 +2149,7 @@ def write_strings(rom, world, player, team):
entrances_to_hint.update({'Agahnims Tower': 'The sealed castle door'})
else:
entrances_to_hint.update({'Ganons Tower': 'Ganon\'s Tower'})
if world.shuffle[player] in ['simple', 'restricted', 'restricted_legacy']:
if world.shuffle[player] in ['simple', 'restricted']:
for entrance in all_entrances:
if entrance.name in entrances_to_hint:
this_hint = entrances_to_hint[entrance.name] + ' leads to ' + hint_text(entrance.connected_region) + '.'
@@ -2187,7 +2161,7 @@ def write_strings(rom, world, player, team):
entrances_to_hint.update(InconvenientOtherEntrances)
if world.shuffle[player] in ['vanilla', 'dungeonssimple', 'dungeonsfull', 'lite', 'lean']:
hint_count = 0
elif world.shuffle[player] in ['simple', 'restricted', 'restricted_legacy']:
elif world.shuffle[player] in ['simple', 'restricted']:
hint_count = 2
else:
hint_count = 4
@@ -2202,7 +2176,7 @@ def write_strings(rom, world, player, team):
break
# Next we handle hints for randomly selected other entrances, curating the selection intelligently based on shuffle.
if world.shuffle[player] not in ['simple', 'restricted', 'restricted_legacy']:
if world.shuffle[player] not in ['simple', 'restricted']:
entrances_to_hint.update(ConnectorEntrances)
entrances_to_hint.update(DungeonEntrances)
if world.shuffle_ganon[player]:
@@ -2232,8 +2206,10 @@ def write_strings(rom, world, player, team):
entrances_to_hint.update({'Big Bomb Shop': 'The old bomb shop'})
else:
entrances_to_hint.update({'Links House': 'The hero\'s old residence'})
if world.shuffletavern[player]:
entrances_to_hint.update({'Tavern North': 'A backdoor'})
entrances_to_hint.update({'Dark Sanctuary Hint': 'The dark sanctuary cave'})
if world.shuffle[player] in ['insanity', 'madness_legacy', 'insanity_legacy']:
if world.shuffle[player] in ['insanity']:
entrances_to_hint.update(InsanityEntrances)
if world.shuffle_ganon[player]:
if world.is_tile_swapped(0x1b, player):
@@ -2254,13 +2230,13 @@ def write_strings(rom, world, player, team):
# Next we write a few hints for specific inconvenient locations. We don't make many because in entrance this is highly unpredictable.
locations_to_hint = InconvenientLocations.copy()
if world.doorShuffle[player] != 'crossed':
if world.doorShuffle[player] == 'vanilla':
locations_to_hint.extend(InconvenientDungeonLocations)
if world.shuffle[player] in ['vanilla', 'dungeonssimple', 'dungeonsfull']:
locations_to_hint.extend(InconvenientVanillaLocations)
random.shuffle(locations_to_hint)
hint_count = 3 if world.shuffle[player] not in ['vanilla', 'dungeonssimple', 'dungeonsfull'] else 5
hint_count -= 2 if world.doorShuffle[player] == 'crossed' else 0
hint_count -= 2 if world.doorShuffle[player] not in ['vanilla', 'basic'] else 0
del locations_to_hint[hint_count:]
for location in locations_to_hint:
if location == 'Swamp Left':
@@ -2330,27 +2306,27 @@ def write_strings(rom, world, player, team):
this_hint = this_hint[0].upper() + this_hint[1:]
tt[hint_locations.pop(0)] = this_hint
items_to_hint.remove(flute_item)
if world.keyshuffle[player]:
if world.keyshuffle[player] == 'wild':
items_to_hint.extend(SmallKeys)
if world.bigkeyshuffle[player]:
items_to_hint.extend(BigKeys)
random.shuffle(items_to_hint)
hint_count = 5 if world.shuffle[player] not in ['vanilla', 'dungeonssimple', 'dungeonsfull'] else 8
hint_count += 2 if world.doorShuffle[player] == 'crossed' else 0
hint_count += 2 if world.doorShuffle[player] not in ['vanilla', 'basic'] else 0
hint_count += 1 if world.owShuffle[player] != 'vanilla' or world.owCrossed[player] != 'none' or world.owMixed[player] else 0
while hint_count > 0:
while hint_count > 0 and len(items_to_hint) > 0:
this_item = items_to_hint.pop(0)
this_location = world.find_items_not_key_only(this_item, player)
random.shuffle(this_location)
if this_location:
random.shuffle(this_location)
item_name = this_location[0].item.hint_text
item_name = item_name[0].upper() + item_name[1:]
this_hint = f'{item_name} can be found {hint_text(this_location[0])}.'
tt[hint_locations.pop(0)] = this_hint
hint_count -= 1
# Adding a hint for the Thieves' Town Attic location in Crossed door shuffle.
if world.doorShuffle[player] in ['crossed']:
# Adding a hint for the Thieves' Town Attic location in mixed door shuffles.
if world.doorShuffle[player] not in ['vanilla', 'basic']:
attic_hint = world.get_location("Thieves' Town - Attic", player).parent_region.dungeon.name
this_hint = 'A cracked floor can be found in ' + attic_hint + '.'
if world.intensity[player] < 2 and hint_locations[0] == 'telepathic_tile_thieves_town_upstairs':
@@ -2468,7 +2444,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)]
@@ -2509,7 +2502,7 @@ def write_strings(rom, world, player, team):
tt['tablet_bombos_book'] = bombos_text
# attic hint
if world.doorShuffle[player] in ['crossed']:
if world.doorShuffle[player] not in ['vanilla', 'basic']:
attic_hint = world.get_location("Thieves' Town - Attic", player).parent_region.dungeon.name
tt['blind_not_that_way'] = f'{attic_hint} is too bright for my eyes'
# see tagalog.asm tables at 957,967 or Follower_HandleTrigger in JPDASM
+14 -2
View File
@@ -316,6 +316,18 @@ class Room(object):
byte_array.append(kind.value)
return byte_array
def next_free(self, pos=4):
for i, door in enumerate(self.doorList):
if i >= pos:
return None
position, kind = door
if kind not in [DoorKind.SmallKey, DoorKind.Dashable, DoorKind.Bombable, DoorKind.TrapTriggerable,
DoorKind.Trap, DoorKind.Trap2, DoorKind.TrapTriggerableLow, DoorKind.TrapLowE3,
DoorKind.BigKey, DoorKind.StairKey, DoorKind.StairKey2, DoorKind.StairKeyLow,
DoorKind.BlastWall, DoorKind.BombableEntrance]:
return i
return None
def __str__(self):
return str(self.__unicode__())
@@ -383,8 +395,8 @@ class DoorKind(Enum):
Bombable = 0x2E
BlastWall = 0x30
Hidden = 0x32
TrapTriggerable = 0x36 # right side trap or south side trap
Trap2 = 0x38 # left side trap or north side trap
TrapTriggerable = 0x36 # right side trap or bottom side trap (door directions: West, North)
Trap2 = 0x38 # left side trap or top side trap (door directions: East, South)
NormalLow2 = 0x40
TrapTriggerableLow = 0x44
Warp = 0x46
+12 -9
View File
@@ -355,7 +355,8 @@ def global_rules(world, player):
set_rule(world.get_entrance('Mire Lobby Gap', player), lambda state: state.has_Boots(player) or state.has('Hookshot', player))
set_rule(world.get_entrance('Mire Post-Gap Gap', player), lambda state: state.has_Boots(player) or state.has('Hookshot', player))
set_rule(world.get_entrance('Mire Falling Bridge WN', player), lambda state: state.has_Boots(player) or state.has('Hookshot', player)) # this is due to the fact the the door opposite is blocked
set_rule(world.get_entrance('Mire Falling Bridge Hook Path', player), lambda state: state.has_Boots(player) or state.has('Hookshot', player))
set_rule(world.get_entrance('Mire Falling Bridge Hook Only Path', player), lambda state: state.has('Hookshot', player))
set_rule(world.get_entrance('Mire 2 NE', player), lambda state: state.has_sword(player) or
(state.has('Fire Rod', player) and (state.can_use_bombs(player) or state.can_extend_magic(player, 9))) or # 9 fr shots or 8 with some bombs
(state.has('Ice Rod', player) and state.can_use_bombs(player)) or # freeze popo and throw, bomb to finish
@@ -425,7 +426,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))
@@ -1604,7 +1606,8 @@ def add_conditional_lamps(world, player):
'Sewers Behind Tapestry': {'sewer': True, 'entrances': ['Sewers Behind Tapestry S', 'Sewers Behind Tapestry Down Stairs'], 'locations': []},
'Sewers Rope Room': {'sewer': True, 'entrances': ['Sewers Rope Room Up Stairs', 'Sewers Rope Room North Stairs'], 'locations': []},
'Sewers Water': {'sewer': True, 'entrances': ['Sewers Water S', 'Sewers Water W'], 'locations': []},
'Sewers Key Rat': {'sewer': True, 'entrances': ['Sewers Key Rat E', 'Sewers Key Rat Key Door N'], 'locations': ['Hyrule Castle - Key Rat Key Drop']},
'Sewers Dark Aquabats': {'sewer': True, 'entrances': ['Sewers Dark Aquabats N', 'Sewers Dark Aquabats ES'], 'locations': []},
'Sewers Key Rat': {'sewer': True, 'entrances': ['Sewers Key Rat S', 'Sewers Key Rat NE'], 'locations': ['Hyrule Castle - Key Rat Key Drop']},
'Old Man Cave': {'sewer': False, 'entrances': ['Old Man Cave Exit (East)']},
'Old Man House Back': {'sewer': False, 'entrances': ['Old Man House Back to Front', 'Old Man House Exit (Top)']},
'Death Mountain Return Cave (left)': {'sewer': False, 'entrances': ['Death Mountain Return Cave E', 'Death Mountain Return Cave Exit (West)']},
@@ -2041,9 +2044,9 @@ bunny_impassible_doors = {
'Ice Backwards Room Hole', 'Ice Switch Room SE', 'Ice Antechamber NE', 'Ice Antechamber Hole', 'Mire Lobby Gap',
'Mire Post-Gap Gap', 'Mire 2 NE', 'Mire Hub Upper Blue Barrier', 'Mire Hub Lower Blue Barrier',
'Mire Hub Right Blue Barrier', 'Mire Hub Top Blue Barrier', 'Mire Hub Switch Blue Barrier N',
'Mire Hub Switch Blue Barrier S', 'Mire Falling Bridge WN', 'Mire Map Spike Side Blue Barrier',
'Mire Map Spot Blue Barrier', 'Mire Crystal Dead End Left Barrier', 'Mire Crystal Dead End Right Barrier',
'Mire Cross ES', 'Mire Left Bridge Hook Path', 'Mire Fishbone Blue Barrier',
'Mire Hub Switch Blue Barrier S', 'Mire Falling Bridge Hook Path', 'Mire Falling Bridge Hook Only Path',
'Mire Map Spike Side Blue Barrier', 'Mire Map Spot Blue Barrier', 'Mire Crystal Dead End Left Barrier',
'Mire Crystal Dead End Right Barrier', 'Mire Cross ES', 'Mire Left Bridge Hook Path', 'Mire Fishbone Blue Barrier',
'Mire South Fish Blue Barrier', 'Mire Tile Room NW', 'Mire Compass Blue Barrier', 'Mire Attic Hint Hole',
'Mire Dark Shooters SW', 'Mire Crystal Mid Blue Barrier', 'Mire Crystal Left Blue Barrier', 'TR Main Lobby Gap',
'TR Lobby Ledge Gap', 'TR Hub SW', 'TR Hub SE', 'TR Hub ES', 'TR Hub EN', 'TR Hub NW', 'TR Hub NE', 'TR Hub Path',
@@ -2089,7 +2092,7 @@ def add_key_logic_rules(world, 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:
set_always_allow(big_chest, allow_big_key_in_big_chest(d_logic.bk_name, player))
if world.retro[player]:
if world.keyshuffle[player] == 'universal':
for d_name, layout in world.key_layout[player].items():
for door in layout.flat_prop:
if world.mode[player] != 'standard' or not retro_in_hc(door.entrance):
@@ -2108,8 +2111,8 @@ 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):
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
+1 -1
View File
@@ -45,7 +45,7 @@ def main(args=None):
test("Vanilla ", "--shuffle vanilla")
test("Retro ", "--retro --shuffle vanilla")
test("Keysanity ", "--shuffle vanilla --dropshuffle --keysanity")
test("Keysanity ", "--shuffle vanilla --keydropshuffle --keysanity")
test("Shopsanity", "--shuffle vanilla --shopsanity")
test("Simple ", "--shuffle simple")
test("Full ", "--shuffle full")
Binary file not shown.
+213
View File
@@ -0,0 +1,213 @@
## Customizer Files
A customizer file is [yaml file](https://yaml.org/) with different settings. This documentation is intended to be read with an [example customizer file](customizer_example.yaml).
This can also be used to roll a mystery or mutli-mystery seed via the GUI. [Example](multi_mystery_example.yaml)
The cli includes a couple arguments to help:
`--print_custom_yaml` will create a yaml file based on the seed rolled. Treat it like a spoiler.
`--customizer` takes a file as an argument.
Present on the GUI as `Print Customizer File` and `Customizer File` on the Generation Setup tab.
### meta
Supported values:
* `players`: number of players
* `seed`: you can set the seed number for a set experience on things that are randomized instead of specified
* `algorithm`: fill algorithm
* `names`: for naming mutliworld players
###### Not Yet Implemented
* `mystery`: indicates the seed should be treated as a mystery (hides total collection_rate) (This may end up player specific)
### settings
This must be defined by player. Each player number should be listed with the appropriate settings.
in each player section you can set up default settings for that player or you can specify a mystery yaml file.
```
1: player1.yaml
2:
shuffle: crossed
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 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.
###### Not Yet Implemented
Rom/Adjust flags like sprite, quickswap are not outputing with the print_custom_yaml settings
### item_pool
This must be defined by player. Each player number should be listed with the appropriate pool.
Then each player can have the entire item pool defined. The name of item should be followed by the number of that item in the pool. Many key items will be added to the pool if not detected.
`Bottle (Random)` is supported to randomize bottle contents according to those allowed by difficulty. Pendants and crystals are supported here.
##### Caveat
Dungeon items amount can be increased (but not decreased as the minimum of each dungeon item is either pre-determined or calculated by door rando) if the type of dungeon item is not shuffled then it is attempted to be placed in the dungeon. Extra item beyond dungeon capacity not be confined to the dungeon.
### placements
This must be defined by player. Each player number should be listed with the appropriate placement list.
You may list each location for a player and the item you wish to place there. A location name requires to be enclosed by single quotes if the location name contains a `#` (Most pot locations have the `#`). (Currently no location names have both a `'` and a `#` so you don't need to worry about escaping the `'`)
For multiworld you can specify which player the item is for using this syntax:
`<item name>#<player number>`
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
This must be defined by player. Each player number should be listed with the appropriate sections. This section has three primary subsections: `entrances`, `exits`, and `two-way`.
#### two-way
`two-way` should be used for connectors, dungeons that you wish to couple. (as opposite to decoupled in the insanity shuffle). Links house should be placed using this method as is can be decoupled logically. (Haven't tested to see if it works in game). The overworld entrance is listed first, followed by the interior exit that it leads to. (The exit will then be linked to exit at that entrance).
`50 Rupee Cave: Desert Palace Exit (North)` The 50 Rupee entrance leads to Desert North entrance, and leaving there will spit you out at the same place.
#### exits
`exits` is used for the Chris Houlihan Room Exit and connectors and dungeons that you wish to be decoupled from their entrances. Perhaps counter-intuitively, the exit is listed after the entrance from which it emerges.
`Light Hype Fairy: Chris Houlihan Room Exit` leaving Chris Houlihan Room will spit you out at the Light Hype Fairy.
(I can easily switch this syntax around if people would like me too)
#### entrances
`entrances` is used for single entrances caves, houses, shops, etc. and drops. Single entrance caves always exit to where you enter, they cannot be decoupled. Dungeons and connectors which are decoupled can also be listed here.
`Chicken House: Kakariko Shop` if you walk into Chicken House door, you will in the Kakariko Shop.
##### Known Issues
Chris Houlihan and Links House should be specified together or not at all.
### doors
This must be defined by player. Each player number should be listed with the appropriate sections. This section has three primary subsections: `lobbies` and `doors`.
`lobbies` lists the doors by which each dungeon is entered
`<lobby name>: <door name>` Ex. `Turtle Rock Chest: TR Lava Escape SE`
`doors` lists pairs of doors. The first door name is listed is the key. The value of this object may be the paired door name or optionally it can have two properties: `dest` and `type`. If you want a type, you must use the second option.
The destination door is listed under `dest`
Supported `type`s are `Key Door`, `Bomb Door`, and `Dash Door`
Here are the two examples of the syntax:
```Hyrule Dungeon Guardroom Abyss Edge: Hyrule Castle Lobby W```
```
Sewers Rat Path WS:
dest: Sewers Secret Room ES
two-way: true
type: Key Door
```
You'll note that sub-tile door do not need to be listed, but if you want them to be key doors you will have to list them.
###### Not Yet Implemented
`one-way` to indicate decoupled doors
##### Known Issue
If you specify a door type and those doors cannot be a stateful door due to the nature of the supertile (or you've placed too many on the supertile) an exception is thrown.
### medallions
This must be defined by player. Each player number should be listed with the appropriate info.
Example:
```
Misery Mire: Ether
Turtle Rock: Quake
```
Leave blank or omit if you wish it to be random. Alternatively, a weighted dictionary is supported and a 'Random' option
### bosses
This must be defined by player. Each player number should be listed with the appropriate boss list.
This is done as `<dungeon>: <boss>`
E.g. `Skull Woods: Helmasaur King` for helmacopter. Be sure to turn on at least one enemizer setting for the bosses to actually be randomized.
### start_inventory
This must be defined by player. Each player number should be listed with a list of items to start with.
This is a yaml list (note the hyphens):
```
start_inventory:
1:
- Pegasus Boots
- Progressive Sword
```
To start with multiple copies of progressive items, list them more than once.
##### Known Issue
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.
+125
View File
@@ -0,0 +1,125 @@
meta:
algorithm: balanced
players: 1
settings:
1:
door_shuffle: vanilla
dropshuffle: true
experimental: true
goal: ganon
hints: false
intensity: 1
pseudoboots: false
pottery: lottery
shopsanity: false
shuffle: vanilla
shufflelinks: false
shufflebosses: false
item_pool:
1:
Ice Rod: 1
Progressive Sword: 1
placements:
1:
Turtle Rock - Boss: Triforce
start_inventory:
1:
- Progressive Bow
- Progressive Bow
- Blue Boomerang
- Red Boomerang
- Hookshot
- Mushroom
- Magic Powder
- Fire Rod
- Bombos
- Ether
- Quake
- Lamp
- Hammer
- Ocarina
- Bug Catching Net
- Book of Mudora
- Shovel
- Cane of Somaria
- Cane of Byrna
- Cape
- Magic Mirror
- Moon Pearl
- Titans Mitts
- Tempered Sword
- Pegasus Boots
- Flippers
- Red Mail
- Progressive Shield
- Progressive Shield
- Bottle (Red Potion)
- Bottle (Green Potion)
- Bottle (Blue Potion)
- Bottle (Fairy)
- Magic Upgrade (1/2)
- Magic Upgrade (1/2)
- Boss Heart Container
- Boss Heart Container
- Boss Heart Container
- Boss Heart Container
- Boss Heart Container
- Boss Heart Container
- Boss Heart Container
- Boss Heart Container
- Boss Heart Container
- Boss Heart Container
- Boss Heart Container
- Boss Heart Container
- Boss Heart Container
- Boss Heart Container
- Boss Heart Container
- Boss Heart Container
- Boss Heart Container
- Arrows (10)
- Arrows (10)
- Arrows (10)
- Bombs (10)
- Rupees (300)
- Rupees (300)
- Rupees (300)
- Rupees (300)
- Rupees (300)
- Rupees (300)
- Rupees (300)
- Rupees (300)
- Rupees (300)
- Rupees (300)
- Rupees (300)
- Rupees (300)
- Rupees (300)
- Rupees (300)
- Rupees (300)
- Rupees (300)
- Rupees (300)
- Rupees (300)
- Rupees (300)
- Rupees (300)
- Rupees (300)
- Rupees (300)
- Rupees (300)
- Rupees (300)
- Rupees (300)
- Rupees (300)
- Rupees (300)
- Rupees (300)
- Rupees (300)
- Rupees (300)
- Rupees (300)
- Rupees (300)
- Rupees (300)
- Rupees (50)
- Rupees (20)
- Rupees (20)
- Rupees (5)
- Rupee (1)
- Rupee (1)
- Rupee (1)
- Rupee (1)
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
+108
View File
@@ -0,0 +1,108 @@
meta:
algorithm: balanced
players: 1
seed: 42
names: Lonk
settings:
1:
door_shuffle: basic
dropshuffle: true
experimental: true
goal: ganon
hints: true
intensity: 3
overworld_map: compass
pseudoboots: true
pottery: keys
shopsanity: true
shuffle: crossed
shufflelinks: true
shufflebosses: unique
item_pool:
1:
Blue Boomerang: 3
Bombos: 3
Book of Mudora: 3
Boss Heart Container: 30
Bottle (Random): 12
Bug Catching Net: 3
Cane of Byrna: 3
Cane of Somaria: 3
Cape: 3
Ether: 3
Fire Rod: 3
Flippers: 3
Hammer: 3
Hookshot: 3
Ice Rod: 3
Lamp: 3
Magic Mirror: 3
Magic Powder: 3
Magic Upgrade (1/2): 3
Moon Pearl: 3
Mushroom: 3
Ocarina: 3
Pegasus Boots: 3
Piece of Heart: 24
Progressive Armor: 6
Progressive Bow: 6
Progressive Glove: 6
Progressive Shield: 9
Progressive Sword: 12
Quake: 3
Red Boomerang: 3
Rupees (300): 10
Bombs (3): 20
Sanctuary Heart Container: 3
Shovel: 3
Single Arrow: 1
placements:
1:
Palace of Darkness - Big Chest: Hammer
Capacity Upgrade - Left: Moon Pearl
Turtle Rock - Pokey 2 Key Drop: Ice Rod
entrances:
1:
entrances:
Bat Cave Drop: Pyramid
Dam: Chicken House
Snitch Lady (West): Cave 45
exits:
Misery Mire: Chris Houlihan Room Exit
two-way:
Bat Cave Cave: Pyramid Exit
Bumper Cave (Bottom): Bumper Cave Exit (Bottom)
Bumper Cave (Top): Bumper Cave Exit (Top)
Skull Woods Final Section: Ganons Tower Exit
Misery Mire: Links House Exit
doors:
1:
lobbies:
Sanctuary: Sewers Pull Switch S
Skull 2 East: Skull 1 Lobby S
Skull 3: Skull 2 West Lobby S
Turtle Rock Lazy Eyes: TR Roller Room SW
doors:
Hyrule Dungeon Guardroom Abyss Edge: Hyrule Castle Lobby W
Sewers Rat Path WS:
dest: Sewers Secret Room ES
type: Key Door
Sewers Rat Path WN:
dest: Sewers Secret Room EN
type: Key Door
Tower Pacifist Run WS:
dest: Tower Circle of Pots ES
type: Key Door
Hera Lobby Up Stairs: Hera 4F Down Stairs
medallions:
1:
Misery Mire: Bombos
bosses:
1:
Palace of Darkness: Arrghus
Thieves Town: Blind
Ganons Tower (top): Vitreous
start_inventory:
1:
- Pegasus Boots
+17
View File
@@ -0,0 +1,17 @@
meta:
algorithm: balanced
players: 3
seed: 722495389
names: Link,Zelda,Ganon
settings:
1: player1.yml
2: player2.yml
3: player3.yml
start_inventory:
1:
- Progressive Sword
- Pegasus Boots
2:
- Lamp
3:
- Flippers
+19
View File
@@ -0,0 +1,19 @@
description: I like my keysanity distributed weirdly, but no doors please
door_shuffle: vanilla
dungeon_items:
standard: 12
m: 3
c: 3
s: 3
b: 3
mc: 2
ms: 2
mb: 2
cs: 2
cb: 2
sb: 2
mcs: 3
mcb: 3
msb: 3
csb: 3
full: 12
+16
View File
@@ -0,0 +1,16 @@
description: Some sort of er with unknown goals
door_shuffle: basic
entrance_shuffle:
dungeonssimple: 3
dungeonsfull: 2
simple: 2
restricted: 2
full: 2
crossed: 3
insanity: 1
goals:
ganon: 2
fast_ganon: 2
dungeons: 1
pedestal: 2
triforce-hunt: 1
+25
View File
@@ -0,0 +1,25 @@
description: Door feature fan
door_shuffle: crossed
world_state:
standard: 2
open: 2
inverted: 1
intensity:
1: 1
2: 1
3: 2
keydropshuffle:
on: 1
off: 1
shopsanity:
on: 1
off: 4
restrict_boss_items:
none: 1
dungeon: 1
boss_shuffle:
none: 4
simple: 1
full: 1
unique: 1
random: 1
+24 -3
View File
@@ -36,6 +36,12 @@
1: 2
2: 2
3: 4
door_type_mode:
original: 2
big: 2
all: 1
chaos: 1
decoupledoors: off
dropshuffle:
on: 1
off: 1
@@ -58,6 +64,14 @@
bombbag:
on: 1
off: 4
flute_mode:
normal: 3
active: 1
bow_mode:
progressive: 5
silvers: 5
retro: 1
retro_silvers: 1
entrance_shuffle:
none: 15
dungeonssimple: 3
@@ -88,9 +102,10 @@
open: 1
inverted: 1
retro: 0
retro:
on: 1
off: 4
take_any:
random: 1
fixed: 3
none: 16
goals:
ganon: 2
fast_ganon: 2
@@ -118,6 +133,12 @@
mc: 3
mcs: 2
full: 5
mcu: 1 # map, compass, universal smalls
# for use when you aren't using the dungeon_items above
# smallkey_shuffle:
# standard: 5
# wild: 1
# universal: 1
dungeon_counters:
on: 5
off: 0
+3 -1
View File
@@ -43,11 +43,13 @@ entrance_shuffle:
shufflelinks:
on: 1
off: 1
shuffletavern:
on: 1
off: 1
world_state:
standard: 1
open: 1
inverted: 1
retro: 0
retro:
on: 1
off: 1
+47 -2
View File
@@ -34,6 +34,10 @@
"retro"
]
},
"boots_hint": {
"action": "store_true",
"type": "bool"
},
"swords": {
"choices": [
"random",
@@ -42,6 +46,20 @@
"vanilla"
]
},
"flute_mode": {
"choices": [
"normal",
"active"
]
},
"bow_mode": {
"choices": [
"progressive",
"silvers",
"retro",
"retro_silvers"
]
},
"goal": {
"choices": [
"ganon",
@@ -198,6 +216,7 @@
"door_shuffle": {
"choices": [
"basic",
"partitioned",
"crossed",
"vanilla"
]
@@ -210,6 +229,18 @@
"random"
]
},
"door_type_mode": {
"choices":[
"original",
"big",
"all",
"chaos"
]
},
"decoupledoors": {
"action": "store_true",
"type": "bool"
},
"experimental": {
"action": "store_true",
"type": "bool"
@@ -312,8 +343,11 @@
"type": "bool"
},
"keyshuffle": {
"action": "store_true",
"type": "bool"
"choices": [
"none",
"wild",
"universal"
]
},
"bigkeyshuffle": {
"action": "store_true",
@@ -332,6 +366,13 @@
"action": "store_true",
"type": "bool"
},
"take_any": {
"choices": [
"none",
"random",
"fixed"
]
},
"startinventory": {},
"usestartinventory": {
"type": "bool"
@@ -429,6 +470,10 @@
"action": "store_true",
"type": "bool"
},
"shuffletavern": {
"action": "store_true",
"type": "bool"
},
"overworld_map": {
"choices": [
"default",
+32 -1
View File
@@ -122,6 +122,18 @@
"Hard: Reduced functionality.",
"Expert: Greatly reduced functionality."
],
"flute_mode": [
"Determines if you need to wake up the bird or not on flute pickup (default: %(default)s)",
"Normal: Normal functionality.",
"Active: Flute is activated on pickup."
],
"bow_mode": [
"Determines how the bow acts in the pool (default: %(default)s)",
"Progressive: Two progressive bows placed. First picked up is the bow. Second is silvers.",
"Silvers Separate: Bow and silvers are completely separate items.",
"Retro: Z1 Bow where arrows cost money and the Single Arrow must be bought or found to shoot",
"Retro + Silvers: Bow and silvers are completely separate items."
],
"timer": [
"Select game timer setting. Affects available itempool. (default: %(default)s)",
"None: No timer.",
@@ -250,6 +262,7 @@
"door_shuffle": [
"Select Door Shuffling Algorithm. (default: %(default)s)",
"Basic: Doors are mixed within a single dungeon.",
"Partitioned Doors are mixed in 3 partitions: L1-3+HC+AT, D1-4, D5-8",
"Crossed: Doors are mixed between all dungeons.",
"Vanilla: All doors are connected the same way they were in the",
" base game."
@@ -261,6 +274,14 @@
"3: And shuffles dungeon lobbies",
"random: Picks one of those at random"
],
"door_type_mode" : [
"Door Types to Shuffle (default: %(default)s)",
"original: Shuffles key doors, bombable, and dashable doors",
"big: Adds big key doors",
"all: Adds traps doors (and any future supported door types)",
"chaos: Increases the number of door types in all dungeon pools"
],
"decoupledoors" : [ "Door entrances and exits are decoupled" ],
"experimental": [ "Enable experimental features. (default: %(default)s)" ],
"dungeon_counters": [ "Enable dungeon chest counters. (default: %(default)s)" ],
"crystals_ganon": [
@@ -331,10 +352,17 @@
"Keys are universal, shooting arrows costs rupees,",
"and a few other little things make this more like Zelda-1. (default: %(default)s)"
],
"pseudoboots": [ " Players starts with pseudo boots that allow dashing but no item checks (default: %(default)s)"],
"take_any": [
"Take Any caves from Zelda 1 (default: %(default)s)",
"None: No take any caves",
"Random: Take any caves can replace a random set of un-interesting caves. See documentation for full list",
"Fixed: Take any caves will replace certain location. See documentation for full list"
],
"pseudoboots": [ " Start with pseudo boots that allow dashing but no item checks (default: %(default)s)"],
"bombbag": ["Start with 0 bomb capacity. Two capacity upgrades (+10) are added to the pool (default: %(default)s)" ],
"startinventory": [ "Specifies a list of items that will be in your starting inventory (separated by commas). (default: %(default)s)" ],
"usestartinventory": [ "Toggle usage of Starting Inventory." ],
"customizer": ["Path to a customizer file."],
"custom": [ "Not supported." ],
"customitemarray": [ "Not supported." ],
"accessibility": [
@@ -359,6 +387,9 @@
"shufflelinks": [
"Include Link's House in the entrance shuffle pool. (default: %(default)s)"
],
"shuffletavern": [
"Include the back of the tavern in the entrance shuffle pool. (default: %(default)s)"
],
"overworld_map": [
"Control if and how the overworld map indicators show the locations of dungeons (default: %(default)s)"
],
+29 -12
View File
@@ -53,8 +53,12 @@
"randomizer.dungeon.mapshuffle": "Maps",
"randomizer.dungeon.compassshuffle": "Compasses",
"randomizer.dungeon.smallkeyshuffle": "Small Keys",
"randomizer.dungeon.smallkeyshuffle.none": "In Dungeon",
"randomizer.dungeon.smallkeyshuffle.wild": "Randomized",
"randomizer.dungeon.smallkeyshuffle.universal": "Universal",
"randomizer.dungeon.bigkeyshuffle": "Big Keys",
"randomizer.dungeon.keydropshuffle": "Key Drop Shuffle (Legacy)",
"randomizer.dungeon.decoupledoors": "Decouple Doors",
"randomizer.dungeon.dropshuffle": "Shuffle Enemy Key Drops",
"randomizer.dungeon.potshuffle": "Pot Shuffle (Legacy)",
"randomizer.dungeon.pottery": "Pottery",
@@ -72,6 +76,7 @@
"randomizer.dungeon.dungeondoorshuffle": "Dungeon Door Shuffle",
"randomizer.dungeon.dungeondoorshuffle.vanilla": "Vanilla",
"randomizer.dungeon.dungeondoorshuffle.basic": "Basic",
"randomizer.dungeon.dungeondoorshuffle.partitioned": "Partitioned",
"randomizer.dungeon.dungeondoorshuffle.crossed": "Crossed",
"randomizer.dungeon.dungeonintensity": "Intensity Level",
@@ -80,6 +85,12 @@
"randomizer.dungeon.dungeonintensity.3": "3: Dungeon Lobbies",
"randomizer.dungeon.dungeonintensity.random": "Random",
"randomizer.dungeon.door_type_mode": "Door Types to Shuffle",
"randomizer.dungeon.door_type_mode.original": "Small Key Doors, Bomb Doors, Dash Doors",
"randomizer.dungeon.door_type_mode.big": "Adds Big Key Doors",
"randomizer.dungeon.door_type_mode.all": "Adds Trap Doors",
"randomizer.dungeon.door_type_mode.chaos": "Increases all door types",
"randomizer.dungeon.experimental": "Enable Experimental Features",
"randomizer.dungeon.dungeon_counters": "Dungeon Chest Counters",
@@ -160,6 +171,7 @@
"randomizer.entrance.openpyramid.no": "No",
"randomizer.entrance.shuffleganon": "Include Ganon's Tower and Pyramid Hole in shuffle pool",
"randomizer.entrance.shufflelinks": "Include Link's House in the shuffle pool",
"randomizer.entrance.shuffletavern": "Include the back of the tavern in the entrance shuffle pool",
"randomizer.entrance.overworld_map": "Overworld Map",
"randomizer.entrance.overworld_map.default": "Default (no location display)",
"randomizer.entrance.overworld_map.compass": "Compass indicates location",
@@ -167,21 +179,20 @@
"randomizer.entrance.entranceshuffle": "Entrance Shuffle",
"randomizer.entrance.entranceshuffle.vanilla": "Vanilla",
"randomizer.entrance.entranceshuffle.lite": "Lite",
"randomizer.entrance.entranceshuffle.simple": "Simple",
"randomizer.entrance.entranceshuffle.restricted": "Restricted",
"randomizer.entrance.entranceshuffle.full": "Full",
"randomizer.entrance.entranceshuffle.lite": "Lite",
"randomizer.entrance.entranceshuffle.lean": "Lean",
"randomizer.entrance.entranceshuffle.crossed": "Crossed",
"randomizer.entrance.entranceshuffle.insanity": "Insanity",
"randomizer.entrance.entranceshuffle.restricted_legacy": "Restricted (Legacy)",
"randomizer.entrance.entranceshuffle.full_legacy": "Full (Legacy)",
"randomizer.entrance.entranceshuffle.madness_legacy": "Madness (Legacy)",
"randomizer.entrance.entranceshuffle.insanity_legacy": "Insanity (Legacy)",
"randomizer.entrance.entranceshuffle.dungeonsfull": "Dungeons + Full",
"randomizer.entrance.entranceshuffle.dungeonssimple": "Dungeons + Simple",
"randomizer.entrance.take_any": "Take Any Caves",
"randomizer.entrance.take_any.none": "None",
"randomizer.entrance.take_any.random": "Random",
"randomizer.entrance.take_any.fixed": "Fixed",
"randomizer.gameoptions.nobgm": "Disable Music & MSU-1",
"randomizer.gameoptions.quickswap": "L/R Quickswapping",
@@ -230,6 +241,7 @@
"randomizer.generation.createspoiler": "Create Spoiler Log",
"randomizer.generation.createrom": "Create Patched ROM",
"randomizer.generation.calcplaythrough": "Calculate Playthrough",
"randomizer.generation.print_custom_yaml": "Print Customizer File",
"randomizer.generation.usestartinventory": "Use Starting Inventory",
"randomizer.generation.usecustompool": "Use Custom Item Pool",
@@ -244,7 +256,7 @@
"randomizer.generation.rom.dialog.allfiles": "All Files",
"randomizer.item.hints": "Include Helpful Hints",
"randomizer.item.retro": "Retro mode (universal keys)",
"randomizer.item.retro": "Retro mode",
"randomizer.item.pseudoboots": "Start with Pseudo Boots",
"randomizer.item.bombbag": "Bombbag",
@@ -315,6 +327,16 @@
"randomizer.item.itemfunction.hard": "Hard",
"randomizer.item.itemfunction.expert": "Expert",
"randomizer.item.flute_mode": "Flute Mode",
"randomizer.item.flute_mode.normal": "Normal",
"randomizer.item.flute_mode.active": "Pre-Activated",
"randomizer.item.bow_mode": "Bow Mode",
"randomizer.item.bow_mode.progressive": "Progressive",
"randomizer.item.bow_mode.silvers": "Silvers Separate",
"randomizer.item.bow_mode.retro": "Retro (Progressive)",
"randomizer.item.bow_mode.retro_silvers": "Retro + Silvers",
"randomizer.item.timer": "Timer Setting",
"randomizer.item.timer.none": "No Timer",
"randomizer.item.timer.display": "Stopwatch",
@@ -323,11 +345,6 @@
"randomizer.item.timer.ohko": "OHKO",
"randomizer.item.timer.timed-countdown": "Timed Countdown",
"randomizer.item.progressives": "Progressive Items",
"randomizer.item.progressives.on": "On",
"randomizer.item.progressives.off": "Off",
"randomizer.item.progressives.random": "Random",
"randomizer.item.accessibility": "Accessibility",
"randomizer.item.accessibility.items": "100% Inventory",
"randomizer.item.accessibility.locations": "100% Locations",
@@ -2,7 +2,6 @@
"keysanity": {
"mapshuffle": { "type": "checkbox" },
"compassshuffle": { "type": "checkbox" },
"smallkeyshuffle": { "type": "checkbox" },
"bigkeyshuffle": { "type": "checkbox" }
}
}
@@ -1,11 +1,20 @@
{
"widgets": {
"smallkeyshuffle": {
"type": "selectbox",
"options": [
"none",
"wild",
"universal"
]
},
"dungeondoorshuffle": {
"type": "selectbox",
"default": "vanilla",
"options": [
"vanilla",
"basic",
"partitioned",
"crossed"
]
},
@@ -22,6 +31,20 @@
"width": 45
}
},
"door_type_mode": {
"type": "selectbox",
"default": "basic",
"options": [
"original",
"big",
"all",
"chaos"
],
"config": {
"width": 45
}
},
"decoupledoors": { "type": "checkbox" },
"keydropshuffle": { "type": "checkbox" },
"pottery": {
"type": "selectbox",
@@ -11,8 +11,17 @@
"width": 10
}
},
"take_any": {
"type": "selectbox",
"options": [
"none",
"random",
"fixed"
]
},
"shuffleganon": { "type": "checkbox" },
"shufflelinks": { "type": "checkbox" },
"shuffletavern": { "type": "checkbox" },
"overworld_map": {
"type": "selectbox",
"options": [
@@ -4,6 +4,7 @@
"bps": { "type": "checkbox" },
"createspoiler": { "type": "checkbox" },
"calcplaythrough": { "type": "checkbox" },
"print_custom_yaml": { "type": "checkbox" },
"usestartinventory": { "type": "checkbox" },
"usecustompool": { "type": "checkbox" }
}
+16 -8
View File
@@ -86,6 +86,22 @@
"expert"
]
},
"flute_mode": {
"type": "selectbox",
"options": [
"normal",
"active"
]
},
"bow_mode": {
"type": "selectbox",
"options": [
"progressive",
"silvers",
"retro",
"retro_silvers"
]
},
"timer": {
"type": "selectbox",
"options": [
@@ -97,14 +113,6 @@
"timed-countdown"
]
},
"progressives": {
"type": "selectbox",
"options": [
"on",
"off",
"random"
]
},
"accessibility": {
"type": "selectbox",
"options": [
+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")
+381
View File
@@ -0,0 +1,381 @@
import os
import urllib.request
import urllib.parse
import yaml
from yaml.representer import Representer
from collections import defaultdict
from pathlib import Path
import RaceRandom as random
from BaseClasses import LocationType, DoorType
from source.tools.MysteryUtils import roll_settings, get_weights
class CustomSettings(object):
def __init__(self):
self.file_source = None
self.relative_dir = None
self.world_rep = {}
self.player_range = None
def load_yaml(self, file):
self.file_source = load_yaml(file)
head, filename = os.path.split(file)
self.relative_dir = head
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:
return None
meta = defaultdict(lambda: None, self.file_source['meta'])
return meta['players']
def adjust_args(self, args):
def get_setting(value, default):
if 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'])
args.multi = get_setting(meta['players'], args.multi)
args.algorithm = get_setting(meta['algorithm'], args.algorithm)
args.outputname = get_setting(meta['name'], args.outputname)
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)
self.player_range = range(1, args.multi + 1)
if 'settings' in self.file_source:
for p in self.player_range:
player_setting = self.file_source['settings'][p]
if isinstance(player_setting, str):
weights = get_weights(os.path.join(self.relative_dir, player_setting))
settings = defaultdict(lambda: None, vars(roll_settings(weights)))
args.mystery = True
else:
settings = defaultdict(lambda: None, player_setting)
args.shuffle[p] = get_setting(settings['shuffle'], args.shuffle[p])
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])
args.item_functionality[p] = get_setting(settings['item_functionality'], args.item_functionality[p])
args.goal[p] = get_setting(settings['goal'], args.goal[p])
args.difficulty[p] = get_setting(settings['difficulty'], args.difficulty[p])
args.accessibility[p] = get_setting(settings['accessibility'], args.accessibility[p])
args.retro[p] = get_setting(settings['retro'], args.retro[p])
args.take_any[p] = get_setting(settings['take_any'], args.take_any[p])
args.hints[p] = get_setting(settings['hints'], args.hints[p])
args.shopsanity[p] = get_setting(settings['shopsanity'], args.shopsanity[p])
args.dropshuffle[p] = get_setting(settings['dropshuffle'], args.dropshuffle[p])
args.pottery[p] = get_setting(settings['pottery'], args.pottery[p])
if get_setting(settings['keydropshuffle'], args.keydropshuffle[p]):
args.dropshuffle[p] = True
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.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])
args.crystals_ganon[p] = get_setting(settings['crystals_ganon'], args.crystals_ganon[p])
args.experimental[p] = get_setting(settings['experimental'], args.experimental[p])
args.openpyramid[p] = get_setting(settings['openpyramid'], args.openpyramid[p])
args.bigkeyshuffle[p] = get_setting(settings['bigkeyshuffle'], args.bigkeyshuffle[p])
args.keyshuffle[p] = get_setting(settings['keyshuffle'], args.keyshuffle[p])
args.mapshuffle[p] = get_setting(settings['mapshuffle'], args.mapshuffle[p])
args.compassshuffle[p] = get_setting(settings['compassshuffle'], args.compassshuffle[p])
if get_setting(settings['keysanity'], args.keysanity):
args.bigkeyshuffle[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.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])
args.bombbag[p] = get_setting(settings['bombbag'], args.bombbag[p])
args.shufflelinks[p] = get_setting(settings['shufflelinks'], args.shufflelinks[p])
args.shuffletavern[p] = get_setting(settings['shuffletavern'], args.shuffletavern[p])
args.restrict_boss_items[p] = get_setting(settings['restrict_boss_items'], args.restrict_boss_items[p])
args.overworld_map[p] = get_setting(settings['overworld_map'], args.overworld_map[p])
args.pseudoboots[p] = get_setting(settings['pseudoboots'], args.pseudoboots[p])
args.triforce_goal[p] = get_setting(settings['triforce_goal'], args.triforce_goal[p])
args.triforce_pool[p] = get_setting(settings['triforce_pool'], args.triforce_pool[p])
args.beemizer[p] = get_setting(settings['beemizer'], args.beemizer[p])
# mystery usage
args.usestartinventory[p] = get_setting(settings['usestartinventory'], args.usestartinventory[p])
args.startinventory[p] = get_setting(settings['startinventory'], args.startinventory[p])
# rom adjust stuff
args.sprite[p] = get_setting(settings['sprite'], args.sprite[p])
args.disablemusic[p] = get_setting(settings['disablemusic'], args.disablemusic[p])
args.quickswap[p] = get_setting(settings['quickswap'], args.quickswap[p])
args.reduce_flashing[p] = get_setting(settings['reduce_flashing'], args.reduce_flashing[p])
args.fastmenu[p] = get_setting(settings['fastmenu'], args.fastmenu[p])
args.heartcolor[p] = get_setting(settings['heartcolor'], args.heartcolor[p])
args.heartbeep[p] = get_setting(settings['heartbeep'], args.heartbeep[p])
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])
def get_item_pool(self):
if 'item_pool' in self.file_source:
return self.file_source['item_pool']
return None
def get_placements(self):
if 'placements' in self.file_source:
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']
return None
def get_doors(self):
if 'doors' in self.file_source:
return self.file_source['doors']
return None
def get_bosses(self):
if 'bosses' in self.file_source:
return self.file_source['bosses']
return None
def get_start_inventory(self):
if 'start_inventory' in self.file_source:
return self.file_source['start_inventory']
return None
def get_medallions(self):
if 'medallions' in self.file_source:
return self.file_source['medallions']
return None
def get_drops(self):
if 'drops' in self.file_source:
return self.file_source['drops']
return None
def create_from_world(self, world):
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
self.world_rep['settings'] = settings_dict
for p in self.player_range:
settings_dict[p] = {}
settings_dict[p]['shuffle'] = world.shuffle[p]
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]['decoupledoors'] = world.decoupledoors[p]
settings_dict[p]['logic'] = world.logic[p]
settings_dict[p]['mode'] = world.mode[p]
settings_dict[p]['swords'] = world.swords[p]
settings_dict[p]['flute_mode'] = world.flute_mode[p]
settings_dict[p]['bow_mode'] = world.bow_mode[p]
settings_dict[p]['difficulty'] = world.difficulty[p]
settings_dict[p]['goal'] = world.goal[p]
settings_dict[p]['accessibility'] = world.accessibility[p]
settings_dict[p]['item_functionality'] = world.difficulty_adjustments[p]
settings_dict[p]['take_any'] = world.take_any[p]
settings_dict[p]['hints'] = world.hints[p]
settings_dict[p]['shopsanity'] = world.shopsanity[p]
settings_dict[p]['dropshuffle'] = world.dropshuffle[p]
settings_dict[p]['pottery'] = world.pottery[p]
settings_dict[p]['mixed_travel'] = world.mixed_travel[p]
settings_dict[p]['standardize_palettes'] = world.standardize_palettes[p]
settings_dict[p]['dungeon_counters'] = world.dungeon_counters[p]
settings_dict[p]['crystals_gt'] = world.crystals_gt_orig[p]
settings_dict[p]['crystals_ganon'] = world.crystals_ganon_orig[p]
settings_dict[p]['experimental'] = world.experimental[p]
settings_dict[p]['openpyramid'] = world.open_pyramid[p]
settings_dict[p]['bigkeyshuffle'] = world.bigkeyshuffle[p]
settings_dict[p]['keyshuffle'] = world.keyshuffle[p]
settings_dict[p]['mapshuffle'] = world.mapshuffle[p]
settings_dict[p]['compassshuffle'] = world.compassshuffle[p]
settings_dict[p]['shufflebosses'] = world.boss_shuffle[p]
settings_dict[p]['shuffleenemies'] = world.enemy_shuffle[p]
settings_dict[p]['enemy_health'] = world.enemy_health[p]
settings_dict[p]['enemy_damage'] = world.enemy_damage[p]
settings_dict[p]['shufflepots'] = world.potshuffle[p]
settings_dict[p]['bombbag'] = world.bombbag[p]
settings_dict[p]['shufflelinks'] = world.shufflelinks[p]
settings_dict[p]['shuffletavern'] = world.shuffletavern[p]
settings_dict[p]['overworld_map'] = world.overworld_map[p]
settings_dict[p]['pseudoboots'] = world.pseudoboots[p]
settings_dict[p]['triforce_goal'] = world.treasure_hunt_count[p]
settings_dict[p]['triforce_pool'] = world.treasure_hunt_total[p]
settings_dict[p]['beemizer'] = world.beemizer[p]
# rom adjust stuff
# settings_dict[p]['sprite'] = world.sprite[p]
# settings_dict[p]['disablemusic'] = world.disablemusic[p]
# settings_dict[p]['quickswap'] = world.quickswap[p]
# settings_dict[p]['reduce_flashing'] = world.reduce_flashing[p]
# settings_dict[p]['fastmenu'] = world.fastmenu[p]
# settings_dict[p]['heartcolor'] = world.heartcolor[p]
# settings_dict[p]['heartbeep'] = world.heartbeep[p]
# settings_dict[p]['ow_palettes'] = world.ow_palettes[p]
# settings_dict[p]['uw_palettes'] = world.uw_palettes[p]
# settings_dict[p]['shuffle_sfx'] = world.shuffle_sfx[p]
# more settings?
def record_info(self, world):
self.world_rep['bosses'] = bosses = {}
self.world_rep['start_inventory'] = start_inv = {}
for p in self.player_range:
bosses[p] = {}
start_inv[p] = []
for dungeon in world.dungeons:
for level, boss in dungeon.bosses.items():
location = dungeon.name if level is None else f'{dungeon.name} ({level})'
if boss and 'Agahnim' not in boss.name:
bosses[dungeon.player][location] = boss.name
for item in world.precollected_items:
start_inv[item.player].append(item.name)
def record_item_pool(self, world):
self.world_rep['item_pool'] = item_pool = {}
self.world_rep['medallions'] = medallions = {}
for p in self.player_range:
item_pool[p] = defaultdict(int)
medallions[p] = {}
for item in world.itempool:
item_pool[item.player][item.name] += 1
for p, req_medals in world.required_medallions.items():
medallions[p]['Misery Mire'] = req_medals[0]
medallions[p]['Turtle Rock'] = req_medals[1]
def record_item_placements(self, world):
self.world_rep['placements'] = placements = {}
for p in self.player_range:
placements[p] = {}
for location in world.get_locations():
if location.type != LocationType.Logical:
if location.player != location.item.player:
placements[location.player][location.name] = f'{location.item.name}#{location.item.player}'
else:
placements[location.player][location.name] = location.item.name
def record_entrances(self, world):
self.world_rep['entrances'] = entrances = {}
world.custom_entrances = {}
for p in self.player_range:
connections = entrances[p] = {}
connections['entrances'] = {}
connections['exits'] = {}
connections['two-way'] = {}
for key, data in world.spoiler.entrances.items():
player = data['player'] if 'player' in data else 1
connections = entrances[player]
sub = 'two-way' if data['direction'] == 'both' else 'exits' if data['direction'] == 'exit' else 'entrances'
connections[sub][data['entrance']] = data['exit']
def record_doors(self, world):
self.world_rep['doors'] = doors = {}
for p in self.player_range:
meta_doors = doors[p] = {}
lobbies = meta_doors['lobbies'] = {}
door_map = meta_doors['doors'] = {}
for portal in world.dungeon_portals[p]:
lobbies[portal.name] = portal.door.name
door_types = {DoorType.Normal, DoorType.SpiralStairs, DoorType.Interior}
if world.intensity[p] > 1:
door_types.update([DoorType.Open, DoorType.StraightStairs, DoorType.Ladder])
door_kinds, skip = {}, set()
for key, info in world.spoiler.doorTypes.items():
if key[1] == p:
if ' <-> ' in info['doorNames']:
dns = info['doorNames'].split(' <-> ')
for dn in dns:
door_kinds[dn] = info['type'] # Key Door, Bomb Door, Dash Door
else:
door_kinds[info['doorNames']] = info['type']
for door in world.doors:
if door.player == p and not door.entranceFlag and door.type in door_types and door not in skip:
if door.type == DoorType.Interior:
if door.name in door_kinds:
door_value = {'type': door_kinds[door.name]}
door_map[door.name] = door_value # intra-tile note
skip.add(door.dest)
elif door.dest:
if door.dest.dest == door:
door_value = door.dest.name
skip.add(door.dest)
if door.name in door_kinds:
door_value = {'dest': door_value, 'type': door_kinds[door.name]}
if door.name not in door_kinds and door.dest.name in door_kinds:
# tricky swap thing
door_value = {'dest': door.name, 'type': door_kinds[door.dest.name]}
door = door.dest # this is weird
elif door.name in door_kinds:
door_value = {'dest': door.dest.name, 'one-way': True, 'type': door_kinds[door.name]}
else:
door_value = {'dest': door.dest.name, 'one-way': True}
door_map[door.name] = door_value
def record_medallions(self):
pass
def write_to_file(self, destination):
yaml.add_representer(defaultdict, Representer.represent_dict)
with open(destination, 'w') as file:
yaml.dump(self.world_rep, file)
def load_yaml(path):
if os.path.exists(Path(path)):
with open(path, "r", encoding="utf-8") as f:
return yaml.load(f, Loader=yaml.SafeLoader)
elif urllib.parse.urlparse(path).scheme in ['http', 'https']:
return yaml.load(urllib.request.urlopen(path), Loader=yaml.FullLoader)
+7 -1
View File
@@ -68,8 +68,9 @@ SETTINGSTOPROCESS = {
"weapons": "swords",
"itempool": "difficulty",
"itemfunction": "item_functionality",
"flute_mode": "flute_mode",
"bow_mode": "bow_mode",
"timer": "timer",
"progressives": "progressive",
"accessibility": "accessibility",
"sortingalgo": "algorithm",
"beemizer": "beemizer",
@@ -89,8 +90,10 @@ SETTINGSTOPROCESS = {
"openpyramid": "openpyramid",
"shuffleganon": "shuffleganon",
"shufflelinks": "shufflelinks",
"shuffletavern": "shuffletavern",
"entranceshuffle": "shuffle",
"overworld_map": "overworld_map",
"take_any": "take_any",
},
"enemizer": {
"enemyshuffle": "shuffleenemies",
@@ -105,6 +108,8 @@ SETTINGSTOPROCESS = {
"bigkeyshuffle": "bigkeyshuffle",
"dungeondoorshuffle": "door_shuffle",
"dungeonintensity": "intensity",
"door_type_mode": "door_type_mode",
"decoupledoors": "decoupledoors",
"keydropshuffle": "keydropshuffle",
"dropshuffle": "dropshuffle",
"pottery": "pottery",
@@ -133,6 +138,7 @@ SETTINGSTOPROCESS = {
"createspoiler": "create_spoiler",
"createrom": "create_rom",
"calcplaythrough": "calc_playthrough",
"print_custom_yaml": "print_custom_yaml",
"usestartinventory": "usestartinventory",
"usecustompool": "custom",
"saveonexit": "saveonexit"
+888
View File
@@ -0,0 +1,888 @@
import RaceRandom as random
import collections
import logging
import time
from BaseClasses import CrystalBarrier, DoorType, Hook, RegionType, Sector
from BaseClasses import hook_from_door, flooded_keys
from Regions import dungeon_events, flooded_keys_reverse
def pre_validate(builder, entrance_region_names, split_dungeon, world, player):
pass
# todo: determine the part of check_valid that are necessary here
def generate_dungeon(builder, entrance_region_names, split_dungeon, world, player):
if builder.valid_proposal: # we made this earlier in gen, just use it
proposed_map = builder.valid_proposal
else:
proposed_map = generate_dungeon_find_proposal(builder, entrance_region_names, split_dungeon, world, player)
builder.valid_proposal = proposed_map
queue = collections.deque(proposed_map.items())
while len(queue) > 0:
a, b = queue.popleft()
if world.decoupledoors[player]:
connect_doors_one_way(a, b)
else:
connect_doors(a, b)
queue.remove((b, a))
if len(builder.sectors) == 0:
return Sector()
available_sectors = list(builder.sectors)
master_sector = available_sectors.pop()
for sub_sector in available_sectors:
master_sector.regions.extend(sub_sector.regions)
master_sector.outstanding_doors.clear()
master_sector.r_name_set = None
return master_sector
def generate_dungeon_find_proposal(builder, entrance_region_names, split_dungeon, world, player):
logger = logging.getLogger('')
name = builder.name
logger.debug(f'Generating Dungeon: {name}')
entrance_regions = convert_regions(entrance_region_names, world, player)
excluded = {}
for region in entrance_regions:
portal = next((x for x in world.dungeon_portals[player] if x.door.entrance.parent_region == region), None)
if portal:
if portal.destination:
excluded[region] = None
elif len(entrance_regions) > 1:
p_region = portal.door.entrance.connected_region
access_region = next(x.parent_region for x in p_region.entrances
if x.parent_region.type in [RegionType.LightWorld, RegionType.DarkWorld])
if (access_region.name in world.inaccessible_regions[player] and
region.name not in world.enabled_entrances[player]):
excluded[region] = None
elif split_dungeon and builder.sewers_access and builder.sewers_access.entrance.parent_region == region:
continue
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()
for sector in builder.sectors:
for door in sector.outstanding_doors:
doors_to_connect[door.name] = door, idx
idx += 1
all_regions.update(sector.regions)
finished = False
# flag if standard and this is hyrule castle
paths = determine_paths_for_dungeon(world, player, all_regions, name)
proposed_map = create_random_proposal(doors_to_connect, world, player)
itr = 0
hash_code = proposal_hash(doors_to_connect, proposed_map)
hash_code_set = set()
start = time.time()
while not finished:
if itr > 1000:
elasped = time.time() - start
raise GenerationException(f'Generation taking too long. {elasped}. Ref {name}')
if hash_code in hash_code_set:
proposed_map = create_random_proposal(doors_to_connect, world, player)
hash_code = proposal_hash(doors_to_connect, proposed_map)
if hash_code not in hash_code_set:
hash_code_set.add(hash_code)
explored_state = explore_proposal(name, entrance_regions, all_regions, proposed_map, doors_to_connect,
world, player)
if check_valid(name, explored_state, proposed_map, doors_to_connect, all_regions,
paths, entrance_regions, world, player):
finished = True
else:
proposed_map, hash_code = modify_proposal(proposed_map, explored_state, doors_to_connect,
hash_code_set, world, player)
itr += 1
return proposed_map
def create_random_proposal(doors_to_connect, world, player):
logger = logging.getLogger('')
hooks = [Hook.North, Hook.South, Hook.East, Hook.West, Hook.Stairs]
primary_bucket = collections.defaultdict(list)
secondary_bucket = collections.defaultdict(list)
for name, door in doors_to_connect.items():
door, idx = door
primary_bucket[hook_from_door(door)].append(door)
secondary_bucket[hook_from_door(door)].append(door)
proposal = {}
while True:
hooks_left, left = [], 0
for hook in hooks:
hook_len = len(primary_bucket[hook])
if hook_len > 0:
hooks_left.append(hook)
left += hook_len
if left == 0:
return proposal
next_hook = random.choice(hooks_left)
primary_door = random.choice(primary_bucket[next_hook])
opp_hook, secondary_door = type_map[next_hook], None
while (secondary_door is None or secondary_door == primary_door
or decouple_check(primary_bucket[next_hook], secondary_bucket[opp_hook],
primary_door, secondary_door, world, player)):
secondary_door = random.choice(secondary_bucket[opp_hook])
proposal[primary_door] = secondary_door
primary_bucket[next_hook].remove(primary_door)
secondary_bucket[opp_hook].remove(secondary_door)
if not world.decoupledoors[player]:
proposal[secondary_door] = primary_door
primary_bucket[opp_hook].remove(secondary_door)
secondary_bucket[next_hook].remove(primary_door)
logger.debug(f' Linking {primary_door.name} <-> {secondary_door.name}')
else:
logger.debug(f' Linking {primary_door.name} -> {secondary_door.name}')
def decouple_check(primary_list, secondary_list, primary_door, secondary_door, world, player):
if world.decoupledoors[player] and len(primary_list) == 2 and len(secondary_list) == 2:
primary_alone = next(d for d in primary_list if d != primary_door)
secondary_alone = next(d for d in secondary_list if d != secondary_door)
return primary_alone == secondary_alone
return False
def proposal_hash(doors_to_connect, proposed_map):
hash_code = ''
for name, door_pair in doors_to_connect.items():
door, idx = door_pair
hash_code += str(idx) + str(doors_to_connect[proposed_map[door].name][1])
return hash_code
def modify_proposal(proposed_map, explored_state, doors_to_connect, hash_code_set, world, player):
logger = logging.getLogger('')
hash_code, itr = None, 0
while hash_code is None or hash_code in hash_code_set:
if itr > 10:
proposed_map = create_random_proposal(doors_to_connect, world, player)
hash_code = proposal_hash(doors_to_connect, proposed_map)
return proposed_map, hash_code
visited_bucket = collections.defaultdict(list)
unvisted_bucket = collections.defaultdict(list)
visited_choices = []
unvisted_count = 0
for door_one, door_two in proposed_map.items():
if door_one in explored_state.visited_doors:
visited_bucket[hook_from_door(door_one)].append(door_one)
visited_choices.append(door_one)
else:
unvisted_bucket[hook_from_door(door_one)].append(door_one)
unvisted_count += 1
if unvisted_count == 0:
# something is wrong beyond connectedness, crystal switch puzzle or bk layout - reshuffle
proposed_map = create_random_proposal(doors_to_connect, world, player)
hash_code = proposal_hash(doors_to_connect, proposed_map)
return proposed_map, hash_code
attempt, opp_hook = None, None
opp_hook_len, possible_swaps = 0, list(visited_choices)
while opp_hook_len == 0:
if len(possible_swaps) == 0:
break
attempt = random.choice(possible_swaps)
possible_swaps.remove(attempt)
opp_hook = type_map[hook_from_door(attempt)]
opp_hook_len = len(unvisted_bucket[opp_hook])
if opp_hook_len == 0:
itr += 1
continue
unvisted_bucket[opp_hook].sort(key=lambda d: d.name)
new_door = random.choice(unvisted_bucket[opp_hook])
old_target = proposed_map[attempt]
proposed_map[attempt] = new_door
if not world.decoupledoors[player]:
old_attempt = proposed_map[new_door]
else:
old_attempt = next(x for x in proposed_map if proposed_map[x] == new_door)
proposed_map[old_attempt] = old_target
if not world.decoupledoors[player]:
proposed_map[old_target] = old_attempt
proposed_map[new_door] = attempt
hash_code = proposal_hash(doors_to_connect, proposed_map)
itr += 1
if not world.decoupledoors[player]:
logger.debug(f' Re-linking {attempt.name} <-> {new_door.name}')
logger.debug(f' Re-linking {old_attempt.name} <-> {old_target.name}')
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
def explore_proposal(name, entrance_regions, all_regions, proposed_map, valid_doors, world, player):
start = ExplorationState(dungeon=name)
original_state = extend_reachable_state_lenient(entrance_regions, start, proposed_map,
all_regions, valid_doors, world, player)
return original_state
def check_valid(name, exploration_state, proposed_map, doors_to_connect, all_regions,
paths, entrance_regions, world, player):
all_visited = set()
all_visited.update(exploration_state.visited_blue)
all_visited.update(exploration_state.visited_orange)
if len(all_regions.difference(all_visited)) > 0:
return False
if not valid_paths(name, paths, entrance_regions, doors_to_connect, all_regions, proposed_map, world, player):
return False
return True
def determine_if_bk_needed(sector, split_dungeon, bk_special, world, player):
if not split_dungeon or bk_special:
for region in sector.regions:
for ext in region.exits:
door = world.check_for_door(ext.name, player)
if door is not None and door.bigKey:
return True
return False
def check_for_special(regions):
for region in regions:
for loc in region.locations:
if loc.forced_big_key():
return True
return False
def valid_paths(name, paths, entrance_regions, valid_doors, all_regions, proposed_map, world, player):
for path in paths:
if type(path) is tuple:
target = path[1]
start_regions = []
for region in all_regions:
if path[0] == region.name:
start_regions.append(region)
break
else:
target = path
start_regions = entrance_regions
if not valid_path(name, start_regions, target, valid_doors, proposed_map, all_regions, world, player):
return False
return True
def valid_path(name, starting_regions, target, valid_doors, proposed_map, all_regions, world, player):
target_regions = set()
if type(target) is not list:
for region in all_regions:
if target == region.name:
target_regions.add(region)
break
else:
for region in all_regions:
if region.name in target:
target_regions.add(region)
start = ExplorationState(dungeon=name)
original_state = extend_reachable_state_lenient(starting_regions, start, proposed_map, all_regions,
valid_doors, world, player)
for exp_door in original_state.unattached_doors:
if not exp_door.door.blocked or exp_door.door.trapFlag != 0:
return True # outstanding connection possible
for target in target_regions:
if original_state.visited_at_all(target):
return True
return False # couldn't find an outstanding door or the target
boss_path_checks = ['Eastern Boss', 'Desert Boss', 'Hera Boss', 'Tower Agahnim 1', 'PoD Boss', 'Swamp Boss',
'Skull Boss', 'Ice Boss', 'Mire Boss', 'TR Boss', 'GT Agahnim 2']
# pinball is allowed to orphan you
drop_path_checks = ['Skull Pot Circle', 'Skull Left Drop', 'Skull Back Drop', 'Sewers Rat Path']
def determine_paths_for_dungeon(world, player, all_regions, name):
all_r_names = set(x.name for x in all_regions)
paths = []
non_hole_portals = []
for portal in world.dungeon_portals[player]:
if portal.door.entrance.parent_region in all_regions:
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 Dungeon':
paths.append('Hyrule Dungeon Cellblock')
paths.append(('Hyrule Dungeon Cellblock', 'Hyrule Castle Throne Room'))
if world.doorShuffle[player] in ['basic'] and name == 'Thieves Town':
paths.append('Thieves Attic Window')
elif 'Thieves Attic Window' in all_r_names:
paths.append('Thieves Attic Window')
for boss in boss_path_checks:
if boss in all_r_names:
paths.append(boss)
if 'Thieves Boss' in all_r_names:
paths.append('Thieves Boss')
if world.get_dungeon("Thieves Town", player).boss.enemizer_name == 'Blind':
paths.append(('Thieves Blind\'s Cell', 'Thieves Boss'))
for drop_check in drop_path_checks:
if drop_check in all_r_names:
paths.append((drop_check, non_hole_portals))
return paths
def convert_regions(region_names, world, player):
region_list = []
for name in region_names:
region_list.append(world.get_region(name, player))
return region_list
type_map = {
Hook.Stairs: Hook.Stairs,
Hook.North: Hook.South,
Hook.South: Hook.North,
Hook.West: Hook.East,
Hook.East: Hook.West
}
def connect_doors(a, b):
# Return on unsupported types.
if a.type in [DoorType.Hole, DoorType.Warp, DoorType.Interior, DoorType.Logical]:
return
# Connect supported types
if a.type in [DoorType.Normal, DoorType.SpiralStairs, DoorType.Open, DoorType.StraightStairs, DoorType.Ladder]:
connect_two_way(a.entrance, b.entrance)
dep_doors, target = [], None
if len(a.dependents) > 0:
dep_doors, target = a.dependents, b
elif len(b.dependents) > 0:
dep_doors, target = b.dependents, a
if target is not None:
target_region = target.entrance.parent_region
for dep in dep_doors:
connect_simple_door(dep, target_region)
return
# If we failed to account for a type, panic
raise RuntimeError('Unknown door type ' + a.type.name)
def connect_doors_one_way(a, b):
# Return on unsupported types.
if a.type in [DoorType.Hole, DoorType.Warp, DoorType.Interior, DoorType.Logical]:
return
# Connect supported types
if a.type in [DoorType.Normal, DoorType.SpiralStairs, DoorType.Open, DoorType.StraightStairs, DoorType.Ladder]:
connect_one_way(a.entrance, b.entrance)
dep_doors, target = [], None
if len(a.dependents) > 0:
dep_doors, target = a.dependents, b
if target is not None:
target_region = target.entrance.parent_region
for dep in dep_doors:
connect_simple_door(dep, target_region)
return
# If we failed to account for a type, panic
raise RuntimeError('Unknown door type ' + a.type.name)
def connect_two_way(entrance, ext):
# if these were already connected somewhere, remove the backreference
if entrance.connected_region is not None:
entrance.connected_region.entrances.remove(entrance)
if ext.connected_region is not None:
ext.connected_region.entrances.remove(ext)
entrance.connect(ext.parent_region)
ext.connect(entrance.parent_region)
if entrance.parent_region.dungeon:
ext.parent_region.dungeon = entrance.parent_region.dungeon
x = entrance.door
y = ext.door
if x is not None:
x.dest = y
if y is not None:
y.dest = x
def connect_one_way(entrance, ext):
# if these were already connected somewhere, remove the backreference
if entrance.connected_region is not None:
entrance.connected_region.entrances.remove(entrance)
entrance.connect(ext.parent_region)
if entrance.parent_region.dungeon:
ext.parent_region.dungeon = entrance.parent_region.dungeon
x = entrance.door
if x is not None:
x.dest = ext.door
def connect_simple_door(exit_door, region):
exit_door.entrance.connect(region)
exit_door.dest = region
special_big_key_doors = ['Hyrule Dungeon Cellblock Door', "Thieves Blind's Cell Door"]
class ExplorationState(object):
def __init__(self, init_crystal=CrystalBarrier.Orange, dungeon=None):
self.unattached_doors = []
self.avail_doors = []
self.event_doors = []
self.visited_orange = []
self.visited_blue = []
self.visited_doors = set()
self.events = set()
self.crystal = init_crystal
# key region stuff
self.door_krs = {}
# key validation stuff
self.small_doors = []
self.big_doors = []
self.opened_doors = []
self.big_key_opened = False
self.big_key_special = False
self.found_locations = []
self.ttl_locations = 0
self.used_locations = 0
self.key_locations = 0
self.used_smalls = 0
self.bk_found = set()
self.non_door_entrances = []
self.dungeon = dungeon
self.pinball_used = False
self.prize_door_set = {}
self.prize_doors = []
self.prize_doors_opened = False
def copy(self):
ret = ExplorationState(dungeon=self.dungeon)
ret.unattached_doors = list(self.unattached_doors)
ret.avail_doors = list(self.avail_doors)
ret.event_doors = list(self.event_doors)
ret.visited_orange = list(self.visited_orange)
ret.visited_blue = list(self.visited_blue)
ret.events = set(self.events)
ret.crystal = self.crystal
ret.door_krs = self.door_krs.copy()
ret.small_doors = list(self.small_doors)
ret.big_doors = list(self.big_doors)
ret.opened_doors = list(self.opened_doors)
ret.big_key_opened = self.big_key_opened
ret.big_key_special = self.big_key_special
ret.ttl_locations = self.ttl_locations
ret.key_locations = self.key_locations
ret.used_locations = self.used_locations
ret.used_smalls = self.used_smalls
ret.found_locations = list(self.found_locations)
ret.bk_found = set(self.bk_found)
ret.non_door_entrances = list(self.non_door_entrances)
ret.dungeon = self.dungeon
ret.pinball_used = self.pinball_used
ret.prize_door_set = dict(self.prize_door_set)
ret.prize_doors = list(self.prize_doors)
ret.prize_doors_opened = self.prize_doors_opened
return ret
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):
if region.type != RegionType.Dungeon:
self.crystal = CrystalBarrier.Orange
if self.crystal == CrystalBarrier.Either:
if region not in self.visited_blue:
self.visited_blue.append(region)
if region not in self.visited_orange:
self.visited_orange.append(region)
elif self.crystal == CrystalBarrier.Orange:
self.visited_orange.append(region)
elif self.crystal == CrystalBarrier.Blue:
self.visited_blue.append(region)
if region.type == RegionType.Dungeon:
for location in region.locations:
if key_checks and location not in self.found_locations:
if location.forced_item and 'Small Key' in location.item.name:
self.key_locations += 1
if location.name not in dungeon_events and '- Prize' not in location.name and location.name not in ['Agahnim 1', 'Agahnim 2']:
self.ttl_locations += 1
if location not in self.found_locations:
self.found_locations.append(location)
if not bk_flag:
self.bk_found.add(location)
if location.name in dungeon_events and location.name not in self.events:
if self.flooded_key_check(location):
self.perform_event(location.name, key_region)
if location.name in flooded_keys_reverse.keys() and self.location_found(
flooded_keys_reverse[location.name]):
self.perform_event(flooded_keys_reverse[location.name], key_region)
if '- Prize' in location.name:
self.prize_received = True
def flooded_key_check(self, location):
if location.name not in flooded_keys.keys():
return True
return flooded_keys[location.name] in [x.name for x in self.found_locations]
def location_found(self, location_name):
for l in self.found_locations:
if l.name == location_name:
return True
return False
def perform_event(self, location_name, key_region):
self.events.add(location_name)
queue = collections.deque(self.event_doors)
while len(queue) > 0:
exp_door = queue.popleft()
if exp_door.door.req_event == location_name:
self.avail_doors.append(exp_door)
self.event_doors.remove(exp_door)
if key_region is not None:
d_name = exp_door.door.name
if d_name not in self.door_krs.keys():
self.door_krs[d_name] = key_region
def add_all_entrance_doors_check_unattached(self, region, world, player):
door_list = [x for x in get_doors(world, region, player) if x.type in [DoorType.Normal, DoorType.SpiralStairs]]
door_list.extend(get_entrance_doors(world, region, player))
for door in door_list:
if self.can_traverse(door):
if door.dest is None and not self.in_door_list_ic(door, self.unattached_doors):
self.append_door_to_list(door, self.unattached_doors)
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)
for entrance in region.entrances:
door = world.check_for_door(entrance.name, player)
if door is None:
self.non_door_entrances.append(entrance)
def add_all_doors_check_unattached(self, region, world, player):
for door in get_doors(world, region, player):
if self.can_traverse(door):
if door.controller is not None:
door = door.controller
if door.dest is None and not self.in_door_list_ic(door, self.unattached_doors):
self.append_door_to_list(door, self.unattached_doors)
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(self, region, proposed_map, valid_doors, flag, world, player, exception):
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 door.blocked and exception(door):
self.pinball_used = True
if self.can_traverse(door, exception):
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, flag)
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, flag)
elif not self.in_door_list(door, self.avail_doors):
self.append_door_to_list(door, self.avail_doors, flag)
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:
self.visited_doors.add(door)
if self.can_traverse_ignore_traps(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:
if door.controller is not None:
door = door.controller
if 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, False)
elif not self.in_door_list(door, self.avail_doors):
self.append_door_to_list(door, self.avail_doors, False)
def add_all_doors_check_key_region(self, region, key_region, world, player):
for door in get_doors(world, region, player):
if self.can_traverse(door):
if 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)
if door.name not in self.door_krs.keys():
self.door_krs[door.name] = key_region
else:
if door.name not in self.door_krs.keys():
self.door_krs[door.name] = key_region
def add_all_doors_check_keys(self, region, key_door_proposal, world, player):
for door in get_doors(world, region, player):
if self.can_traverse(door):
if door.controller:
door = door.controller
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:
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:
if 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 visited(self, region):
if self.crystal == CrystalBarrier.Either:
return region in self.visited_blue and region in self.visited_orange
elif self.crystal == CrystalBarrier.Orange:
return region in self.visited_orange
elif self.crystal == CrystalBarrier.Blue:
return region in self.visited_blue
return False
def visited_at_all(self, region):
return region in self.visited_blue or region in self.visited_orange
def found_forced_bk(self):
for location in self.found_locations:
if location.forced_big_key():
return True
return False
def can_traverse(self, door, exception=None):
if door.blocked:
return exception(door) if exception else False
if door.crystal not in [CrystalBarrier.Null, CrystalBarrier.Either]:
return self.crystal == CrystalBarrier.Either or door.crystal == self.crystal
return True
def can_traverse_ignore_traps(self, door):
if door.blocked and door.trapFlag == 0:
return False
if door.crystal not in [CrystalBarrier.Null, CrystalBarrier.Either]:
return self.crystal == CrystalBarrier.Either or door.crystal == self.crystal
return True
def count_locations_exclude_specials(self, world, player):
return count_locations_exclude_big_chest(self.found_locations, world, player)
def validate(self, door, region, world, player):
return self.can_traverse(door) and not self.visited(region) and valid_region_to_explore(region, self.dungeon,
world, player)
def in_door_list(self, door, door_list):
for d in door_list:
if d.door == door and d.crystal == self.crystal:
return True
return False
@staticmethod
def in_door_list_ic(door, door_list):
for d in door_list:
if d.door == door:
return True
return False
@staticmethod
def find_door_in_list(door, door_list):
for d in door_list:
if d.door == door:
return d
return None
def append_door_to_list(self, door, door_list, flag=False):
if door.crystal == CrystalBarrier.Null:
door_list.append(ExplorableDoor(door, self.crystal, flag))
else:
door_list.append(ExplorableDoor(door, door.crystal, flag))
def key_door_sort(self, d):
if d.door.smallKey:
if d.door in self.opened_doors:
return 1
else:
return 0
return 2
def count_locations_exclude_big_chest(locations, world, player):
cnt = 0
for loc in locations:
if ('- Big Chest' not in loc.name and not loc.forced_item and not reserved_location(loc, world, player)
and not prize_or_event(loc) and not blind_boss_unavail(loc, locations, world, player)):
cnt += 1
return cnt
def prize_or_event(loc):
return loc.name in dungeon_events or '- Prize' in loc.name or loc.name in ['Agahnim 1', 'Agahnim 2']
def reserved_location(loc, world, player):
return hasattr(world, 'item_pool_config') and loc.name in world.item_pool_config.reserved_locations[player]
def blind_boss_unavail(loc, locations, world, player):
if loc.name == "Thieves' Town - Boss":
return (loc.parent_region.dungeon.boss.name == 'Blind' and
(not any(x for x in locations if x.name == 'Suspicious Maiden') or
(world.get_region('Thieves Attic Window', player).dungeon.name == 'Thieves Town' and
not any(x for x in locations if x.name == 'Attic Cracked Floor'))))
return False
class ExplorableDoor(object):
def __init__(self, door, crystal, flag):
self.door = door
self.crystal = crystal
self.flag = flag
def __str__(self):
return str(self.__unicode__())
def __unicode__(self):
return '%s (%s)' % (self.door.name, self.crystal.name)
def extend_reachable_state_improved(search_regions, state, proposed_map, all_regions, valid_doors, bk_flag, world, player, exception):
local_state = state.copy()
for region in search_regions:
local_state.visit_region(region)
local_state.add_all_doors_check_proposed(region, proposed_map, valid_doors, False, world, player, exception)
while len(local_state.avail_doors) > 0:
explorable_door = local_state.next_avail_door()
if explorable_door.door.bigKey:
if bk_flag:
big_not_found = (not special_big_key_found(local_state) if local_state.big_key_special
else local_state.count_locations_exclude_specials(world, player) == 0)
if big_not_found:
continue # we can't open this door
if explorable_door.door in proposed_map:
connect_region = world.get_entrance(proposed_map[explorable_door.door].name, player).parent_region
else:
connect_region = world.get_entrance(explorable_door.door.name, player).connected_region
if connect_region is not None:
if valid_region_to_explore_in_regions(connect_region, all_regions, world, player) and not local_state.visited(
connect_region):
flag = explorable_door.flag or explorable_door.door.bigKey
local_state.visit_region(connect_region, bk_flag=flag)
local_state.add_all_doors_check_proposed(connect_region, proposed_map, valid_doors, flag, world, player, exception)
return local_state
def extend_reachable_state_lenient(search_regions, state, proposed_map, all_regions, valid_doors, world, player):
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)
while len(local_state.avail_doors) > 0:
explorable_door = local_state.next_avail_door()
if explorable_door.door in proposed_map:
connect_region = world.get_entrance(proposed_map[explorable_door.door].name, player).parent_region
else:
connect_region = world.get_entrance(explorable_door.door.name, player).connected_region
if connect_region is not None:
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)
return local_state
def special_big_key_found(state):
for location in state.found_locations:
if location.forced_item and location.forced_item.bigkey:
return True
return False
def valid_region_to_explore_in_regions(region, all_regions, world, player):
if region is None:
return False
return ((region.type == RegionType.Dungeon and region in all_regions)
or region.name in world.inaccessible_regions[player]
or (region.name == 'Hyrule Castle Ledge' and world.mode[player] == 'standard'))
def valid_region_to_explore(region, name, world, player):
if region is None:
return False
return ((region.type == RegionType.Dungeon and region.dungeon and region.dungeon.name in name)
or region.name in world.inaccessible_regions[player]
or (region.name == 'Hyrule Castle Ledge' and world.mode[player] == 'standard'))
def get_doors(world, region, player):
res = []
for ext in region.exits:
door = world.check_for_door(ext.name, player)
if door is not None:
res.append(door)
return res
def get_entrance_doors(world, region, player):
res = []
for ext in region.entrances:
door = world.check_for_door(ext.name, player)
if door is not None:
res.append(door)
return res
class GenerationException(Exception):
pass
+1 -1
View File
@@ -157,8 +157,8 @@ def adjust_page(top, parent, settings):
"quickswap": "quickswap",
"nobgm": "disablemusic",
"reduce_flashing": "reduce_flashing",
'msu_resume': 'msu_resume',
"shuffle_sfx": "shuffle_sfx",
"msu_resume": "msu_resume"
}
guiargs = Namespace()
for option in options:
+13
View File
@@ -220,6 +220,11 @@ def create_guiargs(parent):
# Get baserom path
guiargs.rom = parent.pages["randomizer"].pages["generation"].widgets["rom"].storageVar.get()
# Get customizer path
customizer_value = parent.pages["randomizer"].pages["generation"].widgets["customizer"].storageVar.get()
if customizer_value and customizer_value != 'None':
guiargs.customizer = customizer_value
# Get if we're using the Custom Item Pool
guiargs.custom = bool(parent.pages["randomizer"].pages["generation"].widgets["usecustompool"].storageVar.get())
@@ -280,4 +285,12 @@ def create_guiargs(parent):
guiargs.dropshuffle = 1
guiargs.pottery = 'keys' if guiargs.pottery == 'none' else guiargs.pottery
if guiargs.retro or guiargs.mode == 'retro':
if guiargs.bow_mode == 'progressive':
guiargs.bow_mode = 'retro'
elif guiargs.bow_mode == 'silvers':
guiargs.bow_mode = 'retro_silvers'
guiargs.take_any = 'random' if guiargs.take_any == 'none' else guiargs.take_any
guiargs.keyshuffle = 'universal'
return guiargs
+1 -1
View File
@@ -27,7 +27,7 @@ def entrando_page(parent):
for key in dictWidgets:
self.widgets[key] = dictWidgets[key]
packAttrs = {"anchor":E}
if self.widgets[key].type == "checkbox" or key == "openpyramid":
if self.widgets[key].type == "checkbox" or key in ["openpyramid", "take_any"]:
packAttrs["anchor"] = W
self.widgets[key].pack(packAttrs)
+42
View File
@@ -79,6 +79,48 @@ def generation_page(parent,settings):
# frame: pack
self.widgets[widget].pieces["frame"].pack(fill=X)
self.frames["customizer"] = Frame(self)
self.frames["customizer"].pack(anchor=W, fill=X)
## Customizer file
# This one's more-complicated, build it and stuff it
# widget ID
widget = "customizer"
# Empty object
self.widgets[widget] = Empty()
# pieces
self.widgets[widget].pieces = {}
# frame
self.widgets[widget].pieces["frame"] = Frame(self.frames["customizer"])
# frame: label
self.widgets[widget].pieces["frame"].label = Label(self.widgets[widget].pieces["frame"], text='Customizer File: ')
# storage var
self.widgets[widget].storageVar = StringVar()
# textbox
self.widgets[widget].pieces["textbox"] = Entry(self.widgets[widget].pieces["frame"],
textvariable=self.widgets[widget].storageVar)
self.widgets[widget].storageVar.set(settings["customizer"])
# FIXME: Translate these
def FileSelect():
file = filedialog.askopenfilename(filetypes=[("Yaml Files", (".yaml", ".yml")), ("All Files", "*")],
initialdir=os.path.join("."))
self.widgets["customizer"].storageVar.set(file)
# dialog button
self.widgets[widget].pieces["button"] = Button(self.widgets[widget].pieces["frame"],
text='Select File', command=FileSelect)
# frame label: pack
self.widgets[widget].pieces["frame"].label.pack(side=LEFT)
# textbox: pack
self.widgets[widget].pieces["textbox"].pack(side=LEFT, fill=X, expand=True)
# button: pack
self.widgets[widget].pieces["button"].pack(side=LEFT)
# frame: pack
self.widgets[widget].pieces["frame"].pack(fill=X)
## Run Diagnostics
# This one's more-complicated, build it and stuff it
# widget ID
+46 -10
View File
@@ -18,6 +18,8 @@ class ItemPoolConfig(object):
self.item_pool = None
self.placeholders = None
self.reserved_locations = defaultdict(set)
self.restricted = {}
self.preferred = {}
self.recorded_choices = []
@@ -82,10 +84,11 @@ def create_item_pool_config(world):
if world.shopsanity[player]:
for item, locs in shop_vanilla_mapping.items():
config.static_placement[player][item].extend(locs)
if world.retro[player]:
if world.take_any[player] != 'none':
for item, locs in retro_vanilla_mapping.items():
config.static_placement[player][item].extend(locs)
# universal keys
if world.keyshuffle[player] == 'universal':
universal_key_locations = []
for item, locs in vanilla_mapping.items():
if 'Small Key' in item:
@@ -98,12 +101,13 @@ def create_item_pool_config(world):
for item, locs in potkeys_vanilla_mapping.items():
universal_key_locations.extend(locs)
if world.shopsanity[player]:
single_arrow_placement = list(shop_vanilla_mapping['Red Potion'])
single_arrow_placement.append('Red Shield Shop - Right')
config.static_placement[player]['Single Arrow'] = single_arrow_placement
universal_key_locations.extend(shop_vanilla_mapping['Small Heart'])
universal_key_locations.extend(shop_vanilla_mapping['Blue Shield'])
config.static_placement[player]['Small Key (Universal)'] = universal_key_locations
if world.bow_mode[player].startswith('retro') and world.shopsanity[player]:
single_arrow_placement = list(shop_vanilla_mapping['Red Potion'])
single_arrow_placement.append('Red Shield Shop - Right')
config.static_placement[player]['Single Arrow'] = single_arrow_placement
config.location_groups[player] = [
LocationGroup('Major').locs(mode_grouping['Overworld Major'] + mode_grouping['Big Chests'] + mode_grouping['Heart Containers']),
LocationGroup('bkhp').locs(mode_grouping['Heart Pieces']),
@@ -124,7 +128,7 @@ def create_item_pool_config(world):
groups.locations.extend(mode_grouping['Big Keys'])
if world.dropshuffle[player] != 'none':
groups.locations.extend(mode_grouping['Big Key Drops'])
if world.keyshuffle[player]:
if world.keyshuffle[player] != 'none':
groups.locations.extend(mode_grouping['Small Keys'])
if world.dropshuffle[player] != 'none':
groups.locations.extend(mode_grouping['Key Drops'])
@@ -137,7 +141,7 @@ def create_item_pool_config(world):
if world.shopsanity[player]:
groups.locations.append('Capacity Upgrade - Left')
groups.locations.append('Capacity Upgrade - Right')
if world.retro[player]:
if world.take_any[player] != 'none':
if world.shopsanity[player]:
groups.locations.extend(retro_vanilla_mapping['Heart Container'])
groups.locations.append('Old Man Sword Cave Item 1')
@@ -249,7 +253,7 @@ def previously_reserved(location, world, player):
if world.restrict_boss_items[player] == 'dungeon' and (not world.compassshuffle[player]
or not world.mapshuffle[player]
or not world.bigkeyshuffle[player]
or not (world.keyshuffle[player] or world.retro[player])):
or world.keyshuffle[player] == 'none'):
return True
return False
@@ -335,7 +339,7 @@ def determine_major_items(world, player):
pass # now what?
if world.bigkeyshuffle[player]:
major_item_set.update({x for x, y in item_table.items() if y[2] == 'BigKey'})
if world.keyshuffle[player]:
if world.keyshuffle[player] != 'none':
major_item_set.update({x for x, y in item_table.items() if y[2] == 'SmallKey'})
if world.compassshuffle[player]:
major_item_set.update({x for x, y in item_table.items() if y[2] == 'Compass'})
@@ -344,8 +348,9 @@ def determine_major_items(world, player):
if world.shopsanity[player]:
major_item_set.add('Bomb Upgrade (+5)')
major_item_set.add('Arrow Upgrade (+5)')
if world.retro[player]:
if world.bow_mode[player].startswith('retro'):
major_item_set.add('Single Arrow')
if world.keyshuffle[player] == 'universal':
major_item_set.add('Small Key (Universal)')
if world.goal in ['triforcehunt', 'trinity']:
major_item_set.add('Triforce Piece')
@@ -377,8 +382,10 @@ 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
item_name = 'Bottle' if item_to_place.name.startswith('Bottle') else item_to_place.name
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]
@@ -415,6 +422,12 @@ 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)
return locations
@@ -811,3 +824,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)
File diff suppressed because it is too large Load Diff
+219
View File
@@ -0,0 +1,219 @@
import argparse
import RaceRandom as random
import os
from pathlib import Path
import urllib.request
import urllib.parse
import yaml
def get_weights(path):
if os.path.exists(Path(path)):
with open(path, "r", encoding="utf-8") as f:
return yaml.load(f, Loader=yaml.SafeLoader)
elif urllib.parse.urlparse(path).scheme in ['http', 'https']:
return yaml.load(urllib.request.urlopen(path), Loader=yaml.FullLoader)
def roll_settings(weights):
def get_choice(option, root=None):
root = weights if root is None else root
if option not in root:
return None
if type(root[option]) is not dict:
return root[option]
if not root[option]:
return None
return random.choices(list(root[option].keys()), weights=list(map(int, root[option].values())))[0]
def get_choice_default(option, root=weights, default=None):
choice = get_choice(option, root)
if choice is None and default is not None:
return default
return choice
while True:
subweights = weights.get('subweights', {})
if len(subweights) == 0:
break
chances = ({k: int(v['chance']) for (k, v) in subweights.items()})
subweight_name = random.choices(list(chances.keys()), weights=list(chances.values()))[0]
subweights = weights.get('subweights', {}).get(subweight_name, {}).get('weights', {})
subweights['subweights'] = subweights.get('subweights', {})
weights = {**weights, **subweights}
ret = argparse.Namespace()
ret.algorithm = get_choice('algorithm')
glitch_map = {'none': 'noglitches', 'no_logic': 'nologic', 'owglitches': 'owglitches',
'owg': 'owglitches', 'minorglitches': 'minorglitches'}
glitches_required = get_choice('glitches_required')
if glitches_required is not None:
if glitches_required not in glitch_map.keys():
print(f'Logic did not match one of: {", ".join(glitch_map.keys())}')
glitches_required = 'none'
ret.logic = glitch_map[glitches_required]
# item_placement = get_choice('item_placement')
# not supported in ER
dungeon_items = get_choice('dungeon_items')
dungeon_items = '' if dungeon_items == 'standard' or dungeon_items is None else dungeon_items
dungeon_items = 'mcsb' if dungeon_items == 'full' else dungeon_items
ret.mapshuffle = get_choice('map_shuffle') == 'on' if 'map_shuffle' in weights else 'm' in dungeon_items
ret.compassshuffle = get_choice('compass_shuffle') == 'on' if 'compass_shuffle' in weights else 'c' in dungeon_items
if 'smallkey_shuffle' in weights:
ret.keyshuffle = get_choice('smallkey_shuffle')
else:
if 's' in dungeon_items:
ret.keyshuffle = 'wild'
if 'u' in dungeon_items:
ret.keyshuffle = 'universal'
ret.bigkeyshuffle = get_choice('bigkey_shuffle') == 'on' if 'bigkey_shuffle' in weights else 'b' in dungeon_items
ret.accessibility = get_choice('accessibility')
ret.restrict_boss_items = get_choice('restrict_boss_items')
overworld_shuffle = get_choice('overworld_shuffle')
ret.ow_shuffle = overworld_shuffle if overworld_shuffle != 'none' else 'vanilla'
ret.ow_terrain = get_choice('overworld_terrain') == 'on'
valid_options = {'none', 'polar', 'grouped', 'limited', 'chaos'}
ret.ow_crossed = get_choice('overworld_crossed')
ret.ow_crossed = ret.ow_crossed if ret.ow_crossed in valid_options else 'none'
ret.ow_keepsimilar = get_choice('overworld_keepsimilar') == 'on'
ret.ow_mixed = get_choice('overworld_swap') == 'on'
ret.ow_whirlpool = get_choice('whirlpool_shuffle') == 'on'
overworld_flute = get_choice('flute_shuffle')
ret.ow_fluteshuffle = overworld_flute if overworld_flute != 'none' else 'vanilla'
ret.bonk_drops = get_choice('bonk_drops') == 'on'
entrance_shuffle = get_choice('entrance_shuffle')
ret.shuffle = entrance_shuffle if entrance_shuffle != 'none' else 'vanilla'
overworld_map = get_choice('overworld_map')
ret.overworld_map = overworld_map if overworld_map != 'default' else 'default'
door_shuffle = get_choice('door_shuffle')
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.decoupledoors = get_choice('decoupledoors') == 'on'
ret.experimental = get_choice('experimental') == 'on'
ret.collection_rate = get_choice('collection_rate') == 'on'
ret.dungeon_counters = get_choice('dungeon_counters') if 'dungeon_counters' in weights else 'default'
if ret.dungeon_counters == 'default':
ret.dungeon_counters = 'pickup' if ret.door_shuffle != 'vanilla' or ret.compassshuffle == 'on' else 'off'
ret.pseudoboots = get_choice('pseudoboots') == 'on'
ret.shopsanity = get_choice('shopsanity') == 'on'
keydropshuffle = get_choice('keydropshuffle') == 'on'
ret.dropshuffle = get_choice('dropshuffle') == 'on' or keydropshuffle
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.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
else 'standardize')
goal = get_choice('goals')
if goal is not None:
ret.goal = {'ganon': 'ganon',
'fast_ganon': 'crystals',
'dungeons': 'dungeons',
'pedestal': 'pedestal',
'triforce-hunt': 'triforcehunt',
'trinity': 'trinity'
}[goal]
ret.openpyramid = get_choice('open_pyramid') if 'open_pyramid' in weights else 'auto'
ret.shuffleganon = get_choice('shuffleganon') == 'on'
ret.shufflelinks = get_choice('shufflelinks') == 'on'
ret.shuffletavern = get_choice('shuffletavern') == 'on'
ret.crystals_gt = get_choice('tower_open')
ret.crystals_ganon = get_choice('ganon_open')
from ItemList import set_default_triforce
default_tf_goal, default_tf_pool = set_default_triforce(ret.goal, 0, 0)
goal_min = get_choice_default('triforce_goal_min', default=default_tf_goal)
goal_max = get_choice_default('triforce_goal_max', default=default_tf_goal)
pool_min = get_choice_default('triforce_pool_min', default=default_tf_pool)
pool_max = get_choice_default('triforce_pool_max', default=default_tf_pool)
ret.triforce_goal = random.randint(int(goal_min), int(goal_max))
min_diff = get_choice_default('triforce_min_difference', default=(default_tf_pool-default_tf_goal))
ret.triforce_pool = random.randint(max(int(pool_min), ret.triforce_goal + int(min_diff)), int(pool_max))
ret.mode = get_choice('world_state')
if ret.mode == 'retro':
ret.mode = 'open'
ret.retro = True
ret.retro = get_choice('retro') == 'on' # this overrides world_state if used
ret.take_any = get_choice_default('take_any', default='none')
ret.bombbag = get_choice('bombbag') == 'on'
ret.hints = get_choice('hints') == 'on'
swords = get_choice('weapons')
if swords is not None:
ret.swords = {'randomized': 'random',
'assured': 'assured',
'vanilla': 'vanilla',
'swordless': 'swordless'
}[swords]
ret.difficulty = get_choice('item_pool')
ret.flute_mode = get_choice_default('flute_mode', default='normal')
ret.bow_mode = get_choice_default('bow_mode', default='progressive')
ret.item_functionality = get_choice('item_functionality')
old_style_bosses = {'basic': 'simple',
'normal': 'full',
'chaos': 'random'}
boss_choice = get_choice('boss_shuffle')
if boss_choice in old_style_bosses.keys():
boss_choice = old_style_bosses[boss_choice]
ret.shufflebosses = boss_choice
enemy_choice = get_choice('enemy_shuffle')
if enemy_choice == 'chaos':
enemy_choice = 'random'
ret.shuffleenemies = enemy_choice
old_style_damage = {'none': 'default',
'chaos': 'random'}
damage_choice = get_choice('enemy_damage')
if damage_choice in old_style_damage:
damage_choice = old_style_damage[damage_choice]
ret.enemy_damage = damage_choice
ret.enemy_health = get_choice('enemy_health')
ret.beemizer = get_choice('beemizer') if 'beemizer' in weights else '0'
inventoryweights = weights.get('startinventory', {})
startitems = []
for item in inventoryweights.keys():
if get_choice(item, inventoryweights) == 'on':
startitems.append(item)
ret.startinventory = ','.join(startitems)
if len(startitems) > 0:
ret.usestartinventory = True
if 'rom' in weights:
romweights = weights['rom']
ret.sprite = get_choice('sprite', romweights)
ret.disablemusic = get_choice('disablemusic', romweights) == 'on'
ret.quickswap = get_choice('quickswap', romweights) == 'on'
ret.reduce_flashing = get_choice('reduce_flashing', romweights) == 'on'
ret.msu_resume = get_choice('msu_resume', romweights) == 'on'
ret.fastmenu = get_choice('menuspeed', romweights)
ret.heartcolor = get_choice('heartcolor', romweights)
ret.heartbeep = get_choice('heartbeep', romweights)
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'
return ret
+13
View File
@@ -0,0 +1,13 @@
meta:
players: 1
settings:
1:
door_shuffle: crossed
mode: standard
shuffle: crossed
doors:
1:
doors:
Hyrule Castle East Lobby N:
dest: Sewers Secret Room Key Door S
type: Key Door
+144
View File
@@ -0,0 +1,144 @@
import RaceRandom as random
import logging
import time
from collections import Counter, defaultdict
from source.overworld.EntranceShuffle2 import link_entrances_new
from EntranceShuffle import link_entrances
from BaseClasses import World
from Regions import create_regions, create_dungeon_regions
# tested: open + crossed (lh) Mar. 17 (made changes)
# tested: open + simple (lh) Mar. 22
def run_stats():
# logging.basicConfig(format='%(message)s', level=logging.DEBUG)
random.seed(None)
tests = 10000
for main_mode in ['open', 'standard', 'inverted']:
for shuffle_mode in ['dungeonssimple', 'dungeonsfull', 'lite', 'lean',
'simple', 'restricted', 'full', 'crossed']:
for ls in [True, False]:
if ls and (main_mode == 'standard' or shuffle_mode in ['dungeonssimple', 'dungeonsfull']):
continue
def runner_new(world):
link_entrances_new(world, 1)
def runner_old(world):
link_entrances(world, 1)
compare_tests(tests, shuffle_mode, main_mode, ls, runner_old, runner_new)
def run_test_stats():
# logging.basicConfig(format='%(message)s', level=logging.DEBUG)
random.seed(None)
tests = 10000
for main_mode in ['open']:
for shuffle_mode in [#'dungeonssimple', 'dungeonsfull',
'simple']:#, 'restricted', 'full', 'crossed']:
for ls in [True, False]:
if ls and (main_mode == 'standard' or shuffle_mode in ['dungeonssimple', 'dungeonsfull']):
continue
def runner_new(world):
link_entrances_new(world, 1)
run_tests(tests, shuffle_mode, main_mode, ls, runner_new)
def run_old_stats():
# logging.basicConfig(format='%(message)s', level=logging.DEBUG)
random.seed(None)
tests = 10000
for main_mode in ['open']:
for shuffle_mode in [#'dungeonssimple', 'dungeonsfull',
'simple']:#, 'restricted', 'full', 'crossed']:
for ls in [True, False]:
if ls and (main_mode == 'standard' or shuffle_mode in ['dungeonssimple', 'dungeonsfull']):
continue
def runner(world):
link_entrances(world, 1)
run_tests(tests, shuffle_mode, main_mode, ls, runner)
def compare_tests(tests, shuffle_mode, main_mode, links=False, runner_old=None, runner_new=None):
print(f'Testing {shuffle_mode} {main_mode}')
entrance_set = set()
exit_set = set()
ctr = defaultdict(Counter)
start = time.time()
test_loop(tests, entrance_set, exit_set, ctr, shuffle_mode, main_mode, links, runner_old)
print(f'Old test took {time.time() - start}s')
start = time.time()
test_loop(tests, entrance_set, exit_set, ctr, shuffle_mode, main_mode, links, runner_new, -1)
# test_loop(tests, entrance_set, exit_set, ctr, shuffle_mode, main_mode, links, runner_new)
print(f'New test took {time.time() - start}s')
dump_file(tests, entrance_set, exit_set, ctr, shuffle_mode, main_mode, links)
def run_tests(tests, shuffle_mode, main_mode, links=False, runner=None):
print(f'Testing {shuffle_mode} {main_mode}')
entrance_set = set()
exit_set = set()
ctr = defaultdict(Counter)
test_loop(tests, entrance_set, exit_set, ctr, shuffle_mode, main_mode, links, runner)
def test_loop(tests, entrance_set, exit_set, ctr, shuffle_mode, main_mode, links, runner, value=1):
for i in range(0, tests):
if i % 1000 == 0:
print(f'Test {i}')
seed = random.randint(0, 999999999)
# seed = 635441530
random.seed(seed)
world = World(1, {1: shuffle_mode}, {1: 'vanilla'}, {1: 'noglitches'}, {1: main_mode}, {}, {}, {},
{}, {}, {}, {}, {}, True, {}, {}, [], {})
world.customizer = False
world.shufflelinks = {1: links}
create_regions(world, 1)
create_dungeon_regions(world, 1)
# print(f'Linking seed {seed}')
# try:
runner(world)
# except Exception as e:
# print(f'Failure during seed {seed} with {e}')
for data in world.spoiler.entrances.values():
ent = data['entrance']
ext = data['exit']
# drc = data['direction']
entrance_set.add(ent)
exit_set.add(ext)
ctr[ent][ext] += value
def dump_file(tests, entrance_set, exit_set, ctr, shuffle_mode, main_mode, links):
columns = sorted(list(exit_set))
rows = sorted(list(entrance_set))
filename = f'er_stats_{shuffle_mode}_{main_mode}_{"ls" if links else "v"}.csv'
with open(filename, 'w') as stat_file:
stat_file.write(',')
stat_file.write(','.join(columns))
stat_file.write('\n')
for r in rows:
stat_file.write(f'{r},')
formatted = []
for c in columns:
occurance = ctr[r][c] / tests
formatted.append(f'{occurance:.5%}')
stat_file.write(','.join(formatted))
stat_file.write('\n')
if __name__ == "__main__":
# logging.basicConfig(format='%(message)s', level=logging.DEBUG)
# run_tests(1, 'restricted', 'inverted', True, lambda world: link_entrances_new(world, 1))
# run_test_stats()
# run_old_stats()
run_stats()
View File