Merge branch 'DoorDevUnstable' into DoorDevVolatile

# Conflicts:
#	CLI.py
#	Main.py
#	RELEASENOTES.md
#	Rom.py
#	Rules.py
#	data/base2current.bps
#	source/classes/CustomSettings.py
#	source/tools/MysteryUtils.py
This commit is contained in:
aerinon
2023-09-29 11:44:23 -06:00
25 changed files with 516 additions and 157 deletions

View File

@@ -78,6 +78,7 @@ class World(object):
self.rooms = [] self.rooms = []
self._room_cache = {} self._room_cache = {}
self.dungeon_layouts = {} self.dungeon_layouts = {}
self.dungeon_pool = {}
self.inaccessible_regions = {} self.inaccessible_regions = {}
self.enabled_entrances = {} self.enabled_entrances = {}
self.key_logic = {} self.key_logic = {}
@@ -145,6 +146,7 @@ class World(object):
set_player_attr('colorizepots', True) set_player_attr('colorizepots', True)
set_player_attr('pot_pool', {}) set_player_attr('pot_pool', {})
set_player_attr('decoupledoors', False) set_player_attr('decoupledoors', False)
set_player_attr('door_self_loops', False)
set_player_attr('door_type_mode', 'original') set_player_attr('door_type_mode', 'original')
set_player_attr('trap_door_mode', 'optional') set_player_attr('trap_door_mode', 'optional')
set_player_attr('key_logic_algorithm', 'default') set_player_attr('key_logic_algorithm', 'default')
@@ -1764,6 +1766,7 @@ class Door(object):
self.dest = None self.dest = None
self.blocked = False # Indicates if the door is normally blocked off as an exit. (Sanc door or always closed) self.blocked = False # Indicates if the door is normally blocked off as an exit. (Sanc door or always closed)
self.blocked_orig = False self.blocked_orig = False
self.trapped = False
self.stonewall = False # Indicate that the door cannot be enter until exited (Desert Torches, PoD Eye Statue) self.stonewall = False # Indicate that the door cannot be enter until exited (Desert Torches, PoD Eye Statue)
self.smallKey = False # There's a small key door on this side self.smallKey = False # There's a small key door on this side
self.bigKey = False # There's a big key door on this side self.bigKey = False # There's a big key door on this side
@@ -1874,7 +1877,7 @@ class Door(object):
return self return self
def no_exit(self): def no_exit(self):
self.blocked = self.blocked_orig = True self.blocked = self.blocked_orig = self.trapped = True
return self return self
def no_entrance(self): def no_entrance(self):
@@ -2483,6 +2486,7 @@ class Spoiler(object):
'trap_door_mode': self.world.trap_door_mode, 'trap_door_mode': self.world.trap_door_mode,
'key_logic': self.world.key_logic_algorithm, 'key_logic': self.world.key_logic_algorithm,
'decoupledoors': self.world.decoupledoors, 'decoupledoors': self.world.decoupledoors,
'door_self_loops': self.world.door_self_loops,
'dungeon_counters': self.world.dungeon_counters, 'dungeon_counters': self.world.dungeon_counters,
'item_pool': self.world.difficulty, 'item_pool': self.world.difficulty,
'item_functionality': self.world.difficulty_adjustments, 'item_functionality': self.world.difficulty_adjustments,
@@ -2512,7 +2516,9 @@ class Spoiler(object):
'triforcegoal': self.world.treasure_hunt_count, 'triforcegoal': self.world.treasure_hunt_count,
'triforcepool': self.world.treasure_hunt_total, 'triforcepool': self.world.treasure_hunt_total,
'race': self.world.settings.world_rep['meta']['race'], 'race': self.world.settings.world_rep['meta']['race'],
'code': {p: Settings.make_code(self.world, p) for p in range(1, self.world.players + 1)} 'user_notes': self.world.settings.world_rep['meta']['user_notes'],
'code': {p: Settings.make_code(self.world, p) for p in range(1, self.world.players + 1)},
'seed': self.world.seed
} }
for p in range(1, self.world.players + 1): for p in range(1, self.world.players + 1):
@@ -2653,6 +2659,8 @@ class Spoiler(object):
self.parse_meta() self.parse_meta()
with open(filename, 'w') as outfile: with open(filename, 'w') as outfile:
outfile.write('ALttP Dungeon Randomizer Version %s - Seed: %s\n\n' % (self.metadata['version'], self.world.seed)) outfile.write('ALttP Dungeon Randomizer Version %s - Seed: %s\n\n' % (self.metadata['version'], self.world.seed))
if self.metadata['user_notes']:
outfile.write('User Notes: %s\n' % self.metadata['user_notes'])
outfile.write('Filling Algorithm: %s\n' % self.world.algorithm) outfile.write('Filling Algorithm: %s\n' % self.world.algorithm)
outfile.write('Players: %d\n' % self.world.players) outfile.write('Players: %d\n' % self.world.players)
outfile.write('Teams: %d\n' % self.world.teams) outfile.write('Teams: %d\n' % self.world.teams)
@@ -2694,6 +2702,7 @@ class Spoiler(object):
outfile.write(f"Trap Door Mode: {self.metadata['trap_door_mode'][player]}\n") outfile.write(f"Trap Door Mode: {self.metadata['trap_door_mode'][player]}\n")
outfile.write(f"Key Logic Algorithm: {self.metadata['key_logic'][player]}\n") outfile.write(f"Key Logic Algorithm: {self.metadata['key_logic'][player]}\n")
outfile.write(f"Decouple Doors: {yn(self.metadata['decoupledoors'][player])}\n") outfile.write(f"Decouple Doors: {yn(self.metadata['decoupledoors'][player])}\n")
outfile.write(f"Spiral Stairs can self-loop: {yn(self.metadata['door_self_loops'][player])}\n")
outfile.write(f"Experimental: {yn(self.metadata['experimental'][player])}\n") outfile.write(f"Experimental: {yn(self.metadata['experimental'][player])}\n")
outfile.write(f"Dungeon Counters: {self.metadata['dungeon_counters'][player]}\n") outfile.write(f"Dungeon Counters: {self.metadata['dungeon_counters'][player]}\n")
outfile.write(f"Drop Shuffle: {self.metadata['dropshuffle'][player]}\n") outfile.write(f"Drop Shuffle: {self.metadata['dropshuffle'][player]}\n")
@@ -2952,7 +2961,7 @@ class Pot(object):
# byte 0: DDDE EEEE (DR, ER) # byte 0: DDDE EEEE (DR, ER)
dr_mode = {"basic": 1, "crossed": 2, "vanilla": 0, "partitioned": 3} dr_mode = {"basic": 1, "crossed": 2, "vanilla": 0, "partitioned": 3, 'paired': 4}
er_mode = {"vanilla": 0, "simple": 1, "restricted": 2, "full": 3, "crossed": 4, "insanity": 5, 'lite': 8, er_mode = {"vanilla": 0, "simple": 1, "restricted": 2, "full": 3, "crossed": 4, "insanity": 5, 'lite': 8,
'lean': 9, "dungeonsfull": 7, "dungeonssimple": 6} 'lean': 9, "dungeonsfull": 7, "dungeonssimple": 6}
@@ -2978,10 +2987,10 @@ drop_shuffle_mode = {'none': 0, 'keys': 1, 'underworld': 2}
pottery_mode = {'none': 0, 'keys': 2, 'lottery': 3, 'dungeon': 4, 'cave': 5, 'cavekeys': 6, 'reduced': 7, pottery_mode = {'none': 0, 'keys': 2, 'lottery': 3, 'dungeon': 4, 'cave': 5, 'cavekeys': 6, 'reduced': 7,
'clustered': 8, 'nonempty': 9} 'clustered': 8, 'nonempty': 9}
# byte 5: CCCC CTTX (crystals gt, ctr2, experimental) # byte 5: SCCC CTTX (self-loop doors, crystals gt, ctr2, experimental)
counter_mode = {"default": 0, "off": 1, "on": 2, "pickup": 3} counter_mode = {"default": 0, "off": 1, "on": 2, "pickup": 3}
# byte 6: CCCC CPAA (crystals ganon, pyramid, access # byte 6: ?CCC CPAA (crystals ganon, pyramid, access
access_mode = {"items": 0, "locations": 1, "none": 2} access_mode = {"items": 0, "locations": 1, "none": 2}
# byte 7: B?MC DDEE (big, ?, maps, compass, door_type, enemies) # byte 7: B?MC DDEE (big, ?, maps, compass, door_type, enemies)
@@ -3039,7 +3048,8 @@ class Settings(object):
(0x80 if w.shuffletavern[p] else 0) | (drop_shuffle_mode[w.dropshuffle[p]] << 4) | (pottery_mode[w.pottery[p]]), (0x80 if w.shuffletavern[p] else 0) | (drop_shuffle_mode[w.dropshuffle[p]] << 4) | (pottery_mode[w.pottery[p]]),
((8 if w.crystals_gt_orig[p] == "random" else int(w.crystals_gt_orig[p])) << 3) (0x80 if w.door_self_loops[p] else 0)
| ((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), | (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) ((8 if w.crystals_ganon_orig[p] == "random" else int(w.crystals_ganon_orig[p])) << 3)
@@ -3059,8 +3069,8 @@ class Settings(object):
(flute_mode[w.flute_mode[p]] << 7 | bow_mode[w.bow_mode[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]]), | take_any_mode[w.take_any[p]] << 2 | keyshuffle_mode[w.keyshuffle[p]]),
((0x80 if w.pseudoboots[p] else 0) | overworld_map_mode[w.overworld_map[p]] << 6 ((0x80 if w.pseudoboots[p] else 0) | overworld_map_mode[w.overworld_map[p]] << 5
| trap_door_mode[w.trap_door_mode[p]] << 4 | key_logic_algo[w.key_logic_algorithm[p]]), | trap_door_mode[w.trap_door_mode[p]] << 3 | key_logic_algo[w.key_logic_algorithm[p]]),
]) ])
return base64.b64encode(code, "+-".encode()).decode() return base64.b64encode(code, "+-".encode()).decode()
@@ -3098,12 +3108,13 @@ class Settings(object):
args.dropshuffle[p] = r(drop_shuffle_mode)[(settings[4] & 0x70) >> 4] args.dropshuffle[p] = r(drop_shuffle_mode)[(settings[4] & 0x70) >> 4]
args.pottery[p] = r(pottery_mode)[settings[4] & 0x0F] args.pottery[p] = r(pottery_mode)[settings[4] & 0x0F]
args.door_self_loops[p] = True if settings[5] & 0x80 else False
args.dungeon_counters[p] = r(counter_mode)[(settings[5] & 0x6) >> 1] args.dungeon_counters[p] = r(counter_mode)[(settings[5] & 0x6) >> 1]
cgt = (settings[5] & 0xf8) >> 3 cgt = (settings[5] & 0x78) >> 3
args.crystals_gt[p] = "random" if cgt == 8 else cgt args.crystals_gt[p] = "random" if cgt == 8 else cgt
args.experimental[p] = True if settings[5] & 0x1 else False args.experimental[p] = True if settings[5] & 0x1 else False
cgan = (settings[6] & 0xf8) >> 3 cgan = (settings[6] & 0x78) >> 3
args.crystals_ganon[p] = "random" if cgan == 8 else cgan args.crystals_ganon[p] = "random" if cgan == 8 else cgan
args.openpyramid[p] = True if settings[6] & 0x4 else False args.openpyramid[p] = True if settings[6] & 0x4 else False
@@ -3130,8 +3141,8 @@ class Settings(object):
args.keyshuffle[p] = r(keyshuffle_mode)[settings[11] & 0x3] args.keyshuffle[p] = r(keyshuffle_mode)[settings[11] & 0x3]
if len(settings) > 12: if len(settings) > 12:
args.pseudoboots[p] = True if settings[12] & 0x80 else False args.pseudoboots[p] = True if settings[12] & 0x80 else False
args.overworld_map[p] = r(overworld_map_mode)[(settings[12] & 0x60) >> 6] args.overworld_map[p] = r(overworld_map_mode)[(settings[12] & 0x60) >> 5]
args.trap_door_mode[p] = r(trap_door_mode)[(settings[12] & 0x14) >> 4] args.trap_door_mode[p] = r(trap_door_mode)[(settings[12] & 0x18) >> 3]
args.key_logic_algorithm[p] = r(key_logic_algo)[settings[12] & 0x07] args.key_logic_algorithm[p] = r(key_logic_algo)[settings[12] & 0x07]

6
CLI.py
View File

@@ -141,7 +141,7 @@ def parse_cli(argv, no_defaults=False):
'heartbeep', 'remote_items', 'shopsanity', 'dropshuffle', 'pottery', 'keydropshuffle', 'heartbeep', 'remote_items', 'shopsanity', 'dropshuffle', 'pottery', 'keydropshuffle',
'mixed_travel', 'standardize_palettes', 'code', 'reduce_flashing', 'shuffle_sfx', 'mixed_travel', 'standardize_palettes', 'code', 'reduce_flashing', 'shuffle_sfx',
'msu_resume', 'collection_rate', 'colorizepots', 'decoupledoors', 'door_type_mode', 'msu_resume', 'collection_rate', 'colorizepots', 'decoupledoors', 'door_type_mode',
'trap_door_mode', 'key_logic_algorithm', 'any_enemy_logic']: 'trap_door_mode', 'key_logic_algorithm', 'door_self_loops', 'any_enemy_logic']:
value = getattr(defaults, name) if getattr(playerargs, name) is None else getattr(playerargs, name) value = getattr(defaults, name) if getattr(playerargs, name) is None else getattr(playerargs, name)
if player == 1: if player == 1:
setattr(ret, name, {1: value}) setattr(ret, name, {1: value})
@@ -218,6 +218,7 @@ def parse_settings():
'trap_door_mode': 'optional', 'trap_door_mode': 'optional',
'key_logic_algorithm': 'default', 'key_logic_algorithm': 'default',
'decoupledoors': False, 'decoupledoors': False,
'door_self_loops': False,
'experimental': False, 'experimental': False,
'dungeon_counters': 'default', 'dungeon_counters': 'default',
'mixed_travel': 'prevent', 'mixed_travel': 'prevent',
@@ -346,7 +347,8 @@ def parse_settings():
"outputpath": os.path.join("."), "outputpath": os.path.join("."),
"saveonexit": "ask", "saveonexit": "ask",
"outputname": "", "outputname": "",
"startinventoryarray": {} "startinventoryarray": {},
"notes": ""
} }
# read saved settings file if it exists and set these # read saved settings file if it exists and set these

View File

@@ -88,7 +88,8 @@ def link_doors_prep(world, player):
find_inaccessible_regions(world, player) find_inaccessible_regions(world, player)
if world.intensity[player] >= 3 and world.doorShuffle[player] != 'vanilla': create_dungeon_pool(world, player)
if world.intensity[player] >= 3 and world.doorShuffle[player] != 'vanilla':
choose_portals(world, player) choose_portals(world, player)
else: else:
if world.shuffle[player] == 'vanilla': if world.shuffle[player] == 'vanilla':
@@ -132,6 +133,20 @@ def create_dungeon_pool(world, player):
pool = None pool = None
if world.doorShuffle[player] == 'basic': if world.doorShuffle[player] == 'basic':
pool = [([name], regions) for name, regions in dungeon_regions.items()] pool = [([name], regions) for name, regions in dungeon_regions.items()]
elif world.doorShuffle[player] == 'paired':
dungeon_pool = list(dungeon_regions.keys())
groups = []
while dungeon_pool:
if len(dungeon_pool) == 3:
groups.append(list(dungeon_pool))
dungeon_pool.clear()
else:
choice_a = random.choice(dungeon_pool)
dungeon_pool.remove(choice_a)
choice_b = random.choice(dungeon_pool)
dungeon_pool.remove(choice_b)
groups.append([choice_a, choice_b])
pool = [(group, list(chain.from_iterable([dungeon_regions[d] for d in group]))) for group in groups]
elif world.doorShuffle[player] == 'partitioned': elif world.doorShuffle[player] == 'partitioned':
groups = [['Hyrule Castle', 'Eastern Palace', 'Desert Palace', 'Tower of Hera', 'Agahnims Tower'], groups = [['Hyrule Castle', 'Eastern Palace', 'Desert Palace', 'Tower of Hera', 'Agahnims Tower'],
['Palace of Darkness', 'Swamp Palace', 'Skull Woods', 'Thieves Town'], ['Palace of Darkness', 'Swamp Palace', 'Skull Woods', 'Thieves Town'],
@@ -142,38 +157,17 @@ def create_dungeon_pool(world, player):
elif world.doorShuffle[player] != 'vanilla': elif world.doorShuffle[player] != 'vanilla':
logging.getLogger('').error('Invalid door shuffle setting: %s' % world.doorShuffle[player]) logging.getLogger('').error('Invalid door shuffle setting: %s' % world.doorShuffle[player])
raise Exception('Invalid door shuffle setting: %s' % world.doorShuffle[player]) raise Exception('Invalid door shuffle setting: %s' % world.doorShuffle[player])
return pool world.dungeon_pool[player] = pool
def link_doors_main(world, player): def link_doors_main(world, player):
pool = create_dungeon_pool(world, player) pool = world.dungeon_pool[player]
if pool: if pool:
main_dungeon_pool(pool, world, player) main_dungeon_pool(pool, world, player)
if world.doorShuffle[player] != 'vanilla': if world.doorShuffle[player] != 'vanilla':
create_door_spoiler(world, player) create_door_spoiler(world, player)
# todo: I think this function is not necessary
def mark_regions(world, player):
# traverse dungeons and make sure dungeon property is assigned
player_dungeons = [dungeon for dungeon in world.dungeons if dungeon.player == player]
for dungeon in player_dungeons:
queue = deque(dungeon.regions)
while len(queue) > 0:
region = world.get_region(queue.popleft(), player)
if region.name not in dungeon.regions:
dungeon.regions.append(region.name)
region.dungeon = dungeon
for ext in region.exits:
d = world.check_for_door(ext.name, player)
connected = ext.connected_region
if d is not None and connected is not None:
if d.dest is not None and connected.name not in dungeon.regions and connected.type == RegionType.Dungeon and connected.name not in queue:
queue.append(connected) # needs to be added
elif connected is not None and connected.name not in dungeon.regions and connected.type == RegionType.Dungeon and connected.name not in queue:
queue.append(connected) # needs to be added
def create_door_spoiler(world, player): def create_door_spoiler(world, player):
logger = logging.getLogger('') logger = logging.getLogger('')
shuffled_door_types = [DoorType.Normal, DoorType.SpiralStairs] shuffled_door_types = [DoorType.Normal, DoorType.SpiralStairs]
@@ -183,6 +177,7 @@ def create_door_spoiler(world, player):
queue = deque(world.dungeon_layouts[player].values()) queue = deque(world.dungeon_layouts[player].values())
while len(queue) > 0: while len(queue) > 0:
builder = queue.popleft() builder = queue.popleft()
std_flag = world.mode[player] == 'standard' and builder.name == 'Hyrule Castle' and world.shuffle[player] == 'vanilla'
done = set() done = set()
start_regions = set(convert_regions(builder.layout_starts, world, player)) # todo: set all_entrances for basic start_regions = set(convert_regions(builder.layout_starts, world, player)) # todo: set all_entrances for basic
reg_queue = deque(start_regions) reg_queue = deque(start_regions)
@@ -211,11 +206,16 @@ def create_door_spoiler(world, player):
logger.warning('This is a bug during door spoiler') logger.warning('This is a bug during door spoiler')
elif not isinstance(door_b, Region): elif not isinstance(door_b, Region):
logger.warning('Door not connected: %s', door_a.name) logger.warning('Door not connected: %s', door_a.name)
if connect and connect.type == RegionType.Dungeon and connect not in visited: if valid_connection(connect, std_flag, world, player) and connect not in visited:
visited.add(connect) visited.add(connect)
reg_queue.append(connect) reg_queue.append(connect)
def valid_connection(region, std_flag, world, player):
return region and (region.type == RegionType.Dungeon or region.name in world.inaccessible_regions[player] or
(std_flag and region.name == 'Hyrule Castle Ledge'))
def vanilla_key_logic(world, player): def vanilla_key_logic(world, player):
builders = [] builders = []
world.dungeon_layouts[player] = {} world.dungeon_layouts[player] = {}
@@ -437,17 +437,7 @@ def pair_existing_key_doors(world, player, door_a, door_b):
def choose_portals(world, player): def choose_portals(world, player):
if world.doorShuffle[player] != ['vanilla']: if world.doorShuffle[player] != ['vanilla']:
shuffle_flag = world.doorShuffle[player] != 'basic' shuffle_flag = world.doorShuffle[player] != 'basic'
allowed = {} allowed = {name: set(group[0]) for group in world.dungeon_pool[player] for name in group[0]}
if world.doorShuffle[player] == 'basic':
allowed = {name: {name} for name in dungeon_regions}
elif world.doorShuffle[player] == 'partitioned':
groups = [['Hyrule Castle', 'Eastern Palace', 'Desert Palace', 'Tower of Hera', 'Agahnims Tower'],
['Palace of Darkness', 'Swamp Palace', 'Skull Woods', 'Thieves Town'],
['Ice Palace', 'Misery Mire', 'Turtle Rock', 'Ganons Tower']]
allowed = {name: set(group) for group in groups for name in group}
elif world.doorShuffle[player] == 'crossed':
all_dungeons = set(dungeon_regions.keys())
allowed = {name: all_dungeons for name in dungeon_regions}
# key drops allow the big key in the right place in Desert Tiles 2 # key drops allow the big key in the right place in Desert Tiles 2
bk_shuffle = world.bigkeyshuffle[player] or world.pottery[player] not in ['none', 'cave'] bk_shuffle = world.bigkeyshuffle[player] or world.pottery[player] not in ['none', 'cave']
@@ -592,7 +582,7 @@ def customizer_portals(master_door_list, world, player):
assigned_doors.add(door) assigned_doors.add(door)
# restricts connected doors to the customized portals # restricts connected doors to the customized portals
if assigned_doors: if assigned_doors:
pool = create_dungeon_pool(world, player) pool = world.dungeon_pool[player]
if pool: if pool:
pool_map = {} pool_map = {}
for pool, region_list in pool: for pool, region_list in pool:
@@ -1859,12 +1849,12 @@ def shuffle_trap_doors(door_type_pools, paths, start_regions_map, all_custom, wo
builder.candidates.trap = filter_key_door_pool(builder.candidates.trap, all_custom[dungeon]) builder.candidates.trap = filter_key_door_pool(builder.candidates.trap, all_custom[dungeon])
remaining -= len(custom_trap_doors[dungeon]) remaining -= len(custom_trap_doors[dungeon])
ttl += len(builder.candidates.trap) ttl += len(builder.candidates.trap)
if ttl == 0: if ttl == 0 and all(len(custom_trap_doors[dungeon]) == 0 for dungeon in pool):
continue continue
for dungeon in pool: for dungeon in pool:
builder = world.dungeon_layouts[player][dungeon] builder = world.dungeon_layouts[player][dungeon]
proportion = len(builder.candidates.trap) proportion = len(builder.candidates.trap)
calc = int(round(proportion * door_type_pool.traps/ttl)) calc = 0 if ttl == 0 else int(round(proportion * door_type_pool.traps/ttl))
suggested = min(proportion, calc) suggested = min(proportion, calc)
remaining -= suggested remaining -= suggested
suggestion_map[dungeon] = suggested suggestion_map[dungeon] = suggested
@@ -1996,7 +1986,10 @@ def shuffle_small_key_doors(door_type_pools, used_doors, start_regions_map, all_
remaining = max(0, remaining) remaining = max(0, remaining)
for dungeon in pool: for dungeon in pool:
builder = world.dungeon_layouts[player][dungeon] builder = world.dungeon_layouts[player][dungeon]
calculated = int(round(builder.key_doors_num*total_keys/ttl)) if ttl == 0:
calculated = 0
else:
calculated = int(round(builder.key_doors_num*total_keys/ttl))
max_keys = max(0, builder.location_cnt - calc_used_dungeon_items(builder, world, player)) max_keys = max(0, builder.location_cnt - calc_used_dungeon_items(builder, world, player))
cand_len = max(0, len(builder.candidates.small) - builder.key_drop_cnt) cand_len = max(0, len(builder.candidates.small) - builder.key_drop_cnt)
limit = min(max_keys, cand_len, max_computation) limit = min(max_keys, cand_len, max_computation)
@@ -2226,9 +2219,10 @@ def find_valid_trap_combination(builder, suggested, start_regions, paths, world,
sample_list = build_sample_list(combinations, 1000) sample_list = build_sample_list(combinations, 1000)
proposal = kth_combination(sample_list[itr], trap_door_pool, trap_doors_needed) proposal = kth_combination(sample_list[itr], trap_door_pool, trap_doors_needed)
proposal.extend(custom_trap_doors) proposal.extend(custom_trap_doors)
filtered_proposal = [x for x in proposal if x.name not in trap_door_exceptions]
start_regions, event_starts = filter_start_regions(builder, start_regions, world, player) start_regions, event_starts = filter_start_regions(builder, start_regions, world, player)
while not validate_trap_layout(proposal, builder, start_regions, paths, world, player): while not validate_trap_layout(filtered_proposal, builder, start_regions, paths, world, player):
itr += 1 itr += 1
if itr >= len(sample_list): if itr >= len(sample_list):
if not drop: if not drop:
@@ -2241,6 +2235,7 @@ def find_valid_trap_combination(builder, suggested, start_regions, paths, world,
itr = 0 itr = 0
proposal = kth_combination(sample_list[itr], trap_door_pool, trap_doors_needed) proposal = kth_combination(sample_list[itr], trap_door_pool, trap_doors_needed)
proposal.extend(custom_trap_doors) proposal.extend(custom_trap_doors)
filtered_proposal = [x for x in proposal if x.name not in trap_door_exceptions]
builder.trap_door_proposal = proposal builder.trap_door_proposal = proposal
return proposal, trap_doors_needed return proposal, trap_doors_needed
@@ -2263,6 +2258,12 @@ def filter_start_regions(builder, start_regions, world, player):
portal_entrance_region = portal.door.entrance.parent_region.name portal_entrance_region = portal.door.entrance.parent_region.name
if portal_entrance_region not in builder.path_entrances: if portal_entrance_region not in builder.path_entrances:
excluded[region] = None excluded[region] = None
if not portal:
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 and drop_region.name in world.inaccessible_regions[player]:
excluded[region] = None
if std_flag and (not portal or portal.find_portal_entrance().parent_region.name != 'Hyrule Castle Courtyard'): if std_flag and (not portal or portal.find_portal_entrance().parent_region.name != 'Hyrule Castle Courtyard'):
excluded[region] = None excluded[region] = None
if portal is None: if portal is None:
@@ -2358,10 +2359,12 @@ def reassign_trap_doors(trap_map, world, player):
elif kind in [DoorKind.Trap2, DoorKind.TrapTriggerable]: elif kind in [DoorKind.Trap2, DoorKind.TrapTriggerable]:
room.change(d.doorListPos, DoorKind.Normal) room.change(d.doorListPos, DoorKind.Normal)
d.blocked = False d.blocked = False
d.trapped = False
# connect_one_way(world, d.name, d.dest.name, player) # connect_one_way(world, d.name, d.dest.name, player)
elif d.type is DoorType.Normal and d not in traps: elif d.type is DoorType.Normal and d not in traps:
world.get_room(d.roomIndex, player).change(d.doorListPos, DoorKind.Normal) world.get_room(d.roomIndex, player).change(d.doorListPos, DoorKind.Normal)
d.blocked = False d.blocked = False
d.trapped = False
for d in traps: for d in traps:
change_door_to_trap(d, world, player) change_door_to_trap(d, world, player)
world.spoiler.set_door_type(f'{d.name} ({d.dungeon_name()})', 'Trap Door', player) world.spoiler.set_door_type(f'{d.name} ({d.dungeon_name()})', 'Trap Door', player)
@@ -2399,24 +2402,45 @@ def change_door_to_trap(d, world, player):
elif d.direction in [Direction.North, Direction.West]: elif d.direction in [Direction.North, Direction.West]:
new_kind = DoorKind.TrapTriggerable new_kind = DoorKind.TrapTriggerable
if new_kind: if new_kind:
d.blocked = True d.blocked = is_trap_door_blocked(d)
d.trapped = True
pos = 3 if d.type == DoorType.Normal else 4 pos = 3 if d.type == DoorType.Normal else 4
verify_door_list_pos(d, room, world, player, pos) verify_door_list_pos(d, room, world, player, pos)
d.trapFlag = {0: 0x4, 1: 0x2, 2: 0x1, 3: 0x8}[d.doorListPos] d.trapFlag = {0: 0x4, 1: 0x2, 2: 0x1, 3: 0x8}[d.doorListPos]
room.change(d.doorListPos, new_kind) room.change(d.doorListPos, new_kind)
if d.entrance.connected_region is not None: if d.entrance.connected_region is not None and d.blocked:
d.entrance.connected_region.entrances.remove(d.entrance) d.entrance.connected_region.entrances.remove(d.entrance)
d.entrance.connected_region = None d.entrance.connected_region = None
elif d.type is DoorType.Normal: elif d.type is DoorType.Normal:
d.blocked = True d.blocked = is_trap_door_blocked(d)
d.trapped = True
verify_door_list_pos(d, room, world, player, pos=3) verify_door_list_pos(d, room, world, player, pos=3)
d.trapFlag = {0: 0x4, 1: 0x2, 2: 0x1}[d.doorListPos] d.trapFlag = {0: 0x4, 1: 0x2, 2: 0x1}[d.doorListPos]
room.change(d.doorListPos, DoorKind.Trap) room.change(d.doorListPos, DoorKind.Trap)
if d.entrance.connected_region is not None: if d.entrance.connected_region is not None and d.blocked:
d.entrance.connected_region.entrances.remove(d.entrance) d.entrance.connected_region.entrances.remove(d.entrance)
d.entrance.connected_region = None d.entrance.connected_region = None
trap_door_exceptions = {
'PoD Mimics 2 SW', 'TR Twin Pokeys NW', 'Thieves Blocked Entry SW', 'Hyrule Dungeon Armory Interior Key Door N',
'Desert Compass Key Door WN', 'TR Tile Room SE', 'Mire Cross SW', 'Tower Circle of Pots ES',
'Eastern Single Eyegore ES', 'Eastern Duo Eyegores SE', 'Swamp Push Statue S',
'Skull 2 East Lobby WS', 'GT Hope Room WN', 'Eastern Courtyard Ledge S', 'Ice Lobby SE', 'GT Speed Torch WN',
'Ice Switch Room ES', 'Ice Switch Room NE', 'Skull Torch Room WS', 'GT Speed Torch NE', 'GT Speed Torch WS',
'GT Torch Cross WN', 'Mire Tile Room SW', 'Mire Tile Room ES', 'TR Torches WN', 'PoD Lobby N', 'PoD Middle Cage S',
'Ice Bomb Jump NW', 'GT Hidden Spikes SE', 'Ice Tall Hint EN', 'GT Conveyor Cross EN', 'Eastern Pot Switch WN',
'Thieves Conveyor Maze WN', 'Thieves Conveyor Maze SW', 'Eastern Dark Square Key Door WN', 'Eastern Lobby NW',
'Eastern Lobby NE', 'Ice Cross Bottom SE', 'Desert Back Lobby S', 'Desert West S',
'Desert West Lobby ES', 'Mire Hidden Shooters SE', 'Mire Hidden Shooters ES', 'Mire Hidden Shooters WS',
'Tower Dark Pits EN', 'Tower Dark Maze ES', 'TR Tongue Pull WS',
}
def is_trap_door_blocked(door):
return door.name not in trap_door_exceptions
def find_big_key_candidates(builder, start_regions, used, world, player): def find_big_key_candidates(builder, start_regions, used, world, player):
if world.door_type_mode[player] != 'original': # big, all, chaos if world.door_type_mode[player] != 'original': # big, all, chaos
# traverse dungeon and find candidates # traverse dungeon and find candidates
@@ -3310,6 +3334,9 @@ def find_inaccessible_regions(world, player):
ledge = world.get_region('Hyrule Castle Ledge', player) ledge = world.get_region('Hyrule Castle Ledge', player)
if any(x for x in ledge.exits if x.connected_region.name == 'Agahnims Tower Portal'): if any(x for x in ledge.exits if x.connected_region.name == 'Agahnims Tower Portal'):
world.inaccessible_regions[player].append('Hyrule Castle Ledge') world.inaccessible_regions[player].append('Hyrule Castle Ledge')
# this should be considered as part of the inaccessible regions, dungeonssimple?
if world.mode[player] == 'standard' and world.shuffle[player] == 'vanilla':
world.inaccessible_regions[player].append('Hyrule Castle Ledge')
logger = logging.getLogger('') logger = logging.getLogger('')
logger.debug('Inaccessible Regions:') logger.debug('Inaccessible Regions:')
for r in world.inaccessible_regions[player]: for r in world.inaccessible_regions[player]:

View File

@@ -1677,10 +1677,24 @@ def assign_location_sectors_minimal(dungeon_map, free_location_sectors, global_p
random.shuffle(sector_list) random.shuffle(sector_list)
orig_location_set = build_orig_location_set(dungeon_map) orig_location_set = build_orig_location_set(dungeon_map)
num_dungeon_items = requested_dungeon_items(world, player) num_dungeon_items = requested_dungeon_items(world, player)
locations_to_distribute = sum(sector.chest_locations for sector in free_location_sectors.keys())
reserved_per_dungeon = {d_name: count_reserved_locations(world, player, orig_location_set[d_name])
for d_name in dungeon_map.keys()}
base_free, found_enough = 2, False
while not found_enough:
needed = sum(max(0, max(base_free, reserved_per_dungeon[d]) + num_dungeon_items - len(orig_location_set[d]))
for d in dungeon_map.keys())
if needed > locations_to_distribute:
if base_free == 0:
raise Exception('Unable to meet minimum requirements, check for customizer problems')
base_free -= 1
else:
found_enough = True
d_idx = {builder.name: i for i, builder in enumerate(dungeon_map.values())} d_idx = {builder.name: i for i, builder in enumerate(dungeon_map.values())}
next_sector = sector_list.pop() next_sector = sector_list.pop()
while not valid: while not valid:
choice, totals, location_set = weighted_random_location(dungeon_map, choices, orig_location_set, world, player) choice, totals, location_set = weighted_random_location(dungeon_map, choices, orig_location_set,
base_free, world, player)
if not choice: if not choice:
break break
choices[choice].append(next_sector) choices[choice].append(next_sector)
@@ -1691,7 +1705,7 @@ def assign_location_sectors_minimal(dungeon_map, free_location_sectors, global_p
valid = True valid = True
for d_name, idx in d_idx.items(): for d_name, idx in d_idx.items():
free_items = count_reserved_locations(world, player, location_set[d_name]) free_items = count_reserved_locations(world, player, location_set[d_name])
target = max(free_items, 2) + num_dungeon_items target = max(free_items, base_free) + num_dungeon_items
if totals[idx] < target: if totals[idx] < target:
valid = False valid = False
break break
@@ -1699,8 +1713,7 @@ def assign_location_sectors_minimal(dungeon_map, free_location_sectors, global_p
if len(sector_list) == 0: if len(sector_list) == 0:
choices = defaultdict(list) choices = defaultdict(list)
sector_list = list(free_location_sectors) sector_list = list(free_location_sectors)
else: next_sector = sector_list.pop()
next_sector = sector_list.pop()
else: else:
choices[choice].remove(next_sector) choices[choice].remove(next_sector)
for builder, choice_list in choices.items(): for builder, choice_list in choices.items():
@@ -1709,7 +1722,7 @@ def assign_location_sectors_minimal(dungeon_map, free_location_sectors, global_p
return free_location_sectors return free_location_sectors
def weighted_random_location(dungeon_map, choices, orig_location_set, world, player): def weighted_random_location(dungeon_map, choices, orig_location_set, base_free, world, player):
population = [] population = []
totals = [] totals = []
location_set = {x: set(y) for x, y in orig_location_set.items()} location_set = {x: set(y) for x, y in orig_location_set.items()}
@@ -1720,7 +1733,7 @@ def weighted_random_location(dungeon_map, choices, orig_location_set, world, pla
builder_set = location_set[dungeon_builder.name] builder_set = location_set[dungeon_builder.name]
builder_set.update(set().union(*(s.chest_location_set for s in choices[dungeon_builder]))) builder_set.update(set().union(*(s.chest_location_set for s in choices[dungeon_builder])))
free_items = count_reserved_locations(world, player, builder_set) free_items = count_reserved_locations(world, player, builder_set)
target = max(free_items, 2) + num_dungeon_items target = max(free_items, base_free) + num_dungeon_items
if ttl < target: if ttl < target:
population.append(dungeon_builder) population.append(dungeon_builder)
choice = random.choice(population) if len(population) > 0 else None choice = random.choice(population) if len(population) > 0 else None
@@ -1775,7 +1788,7 @@ def count_reserved_locations(world, player, proposed_set):
return 2 return 2
def assign_crystal_switch_sectors(dungeon_map, crystal_switches, crystal_barriers, global_pole, assign_one=False): def assign_crystal_switch_sectors(dungeon_map, crystal_switches, crystal_barriers, global_pole):
population = [] population = []
some_c_switches_present = False some_c_switches_present = False
for name, builder in dungeon_map.items(): for name, builder in dungeon_map.items():
@@ -1784,7 +1797,7 @@ def assign_crystal_switch_sectors(dungeon_map, crystal_switches, crystal_barrier
if builder.c_switch_present and not builder.c_locked: if builder.c_switch_present and not builder.c_locked:
some_c_switches_present = True some_c_switches_present = True
if len(population) == 0: # nothing needs a switch if len(population) == 0: # nothing needs a switch
if assign_one and not some_c_switches_present: # something should have one if len(crystal_barriers) > 0 and not some_c_switches_present: # something should have one
if len(crystal_switches) == 0: if len(crystal_switches) == 0:
raise GenerationException('No crystal switches to assign. Ref %s' % next(iter(dungeon_map.keys()))) raise GenerationException('No crystal switches to assign. Ref %s' % next(iter(dungeon_map.keys())))
valid, builder_choice, switch_choice = False, None, None valid, builder_choice, switch_choice = False, None, None
@@ -3139,8 +3152,7 @@ def balance_split(candidate_sectors, dungeon_map, global_pole, builder_info):
check_for_forced_assignments(dungeon_map, candidate_sectors, global_pole) check_for_forced_assignments(dungeon_map, candidate_sectors, global_pole)
check_for_forced_crystal(dungeon_map, candidate_sectors, global_pole) check_for_forced_crystal(dungeon_map, candidate_sectors, global_pole)
crystal_switches, crystal_barriers, neutral_sectors, polarized_sectors = categorize_sectors(candidate_sectors) crystal_switches, crystal_barriers, neutral_sectors, polarized_sectors = categorize_sectors(candidate_sectors)
leftover = assign_crystal_switch_sectors(dungeon_map, crystal_switches, crystal_barriers, leftover = assign_crystal_switch_sectors(dungeon_map, crystal_switches, crystal_barriers, global_pole)
global_pole, len(crystal_barriers) > 0)
ensure_crystal_switches_reachable(dungeon_map, leftover, polarized_sectors, crystal_barriers, global_pole) ensure_crystal_switches_reachable(dungeon_map, leftover, polarized_sectors, crystal_barriers, global_pole)
for sector in leftover: for sector in leftover:
if sector.polarity().is_neutral(): if sector.polarity().is_neutral():

View File

@@ -459,7 +459,7 @@ def refine_placement_rules(key_layout, max_ctr):
changed = True changed = True
while changed: while changed:
changed = False changed = False
rules_to_remove = [] rules_to_remove = {}
for rule in key_logic.placement_rules: for rule in key_logic.placement_rules:
if rule.check_locations_w_bk: if rule.check_locations_w_bk:
rule.check_locations_w_bk.difference_update(key_logic.sm_restricted) rule.check_locations_w_bk.difference_update(key_logic.sm_restricted)
@@ -468,7 +468,7 @@ def refine_placement_rules(key_layout, max_ctr):
rule.check_locations_w_bk.difference_update(key_onlys) rule.check_locations_w_bk.difference_update(key_onlys)
rule.needed_keys_w_bk -= len(key_onlys) rule.needed_keys_w_bk -= len(key_onlys)
if rule.needed_keys_w_bk == 0: if rule.needed_keys_w_bk == 0:
rules_to_remove.append(rule) rules_to_remove[rule] = None
# todo: evaluate this usage # todo: evaluate this usage
# if rule.bk_relevant and len(rule.check_locations_w_bk) == rule.needed_keys_w_bk + 1: # if rule.bk_relevant and len(rule.check_locations_w_bk) == rule.needed_keys_w_bk + 1:
# new_restricted = set(max_ctr.free_locations) - rule.check_locations_w_bk # new_restricted = set(max_ctr.free_locations) - rule.check_locations_w_bk
@@ -481,13 +481,13 @@ def refine_placement_rules(key_layout, max_ctr):
# changed = True # changed = True
if rule.needed_keys_w_bk > key_layout.max_chests or len(rule.check_locations_w_bk) < rule.needed_keys_w_bk: if rule.needed_keys_w_bk > key_layout.max_chests or len(rule.check_locations_w_bk) < rule.needed_keys_w_bk:
logging.getLogger('').warning('Invalid rule - what went wrong here??') logging.getLogger('').warning('Invalid rule - what went wrong here??')
rules_to_remove.append(rule) rules_to_remove[rule] = None
changed = True changed = True
if rule.bk_conditional_set is not None: if rule.bk_conditional_set is not None:
rule.bk_conditional_set.difference_update(key_logic.bk_restricted) rule.bk_conditional_set.difference_update(key_logic.bk_restricted)
rule.bk_conditional_set.difference_update(max_ctr.key_only_locations) rule.bk_conditional_set.difference_update(max_ctr.key_only_locations)
if len(rule.bk_conditional_set) == 0: if len(rule.bk_conditional_set) == 0:
rules_to_remove.append(rule) rules_to_remove[rule] = None
if rule.check_locations_wo_bk: if rule.check_locations_wo_bk:
rule.check_locations_wo_bk.difference_update(key_logic.sm_restricted) rule.check_locations_wo_bk.difference_update(key_logic.sm_restricted)
key_onlys = rule.check_locations_wo_bk.intersection(max_ctr.key_only_locations) key_onlys = rule.check_locations_wo_bk.intersection(max_ctr.key_only_locations)
@@ -495,11 +495,11 @@ def refine_placement_rules(key_layout, max_ctr):
rule.check_locations_wo_bk.difference_update(key_onlys) rule.check_locations_wo_bk.difference_update(key_onlys)
rule.needed_keys_wo_bk -= len(key_onlys) rule.needed_keys_wo_bk -= len(key_onlys)
if rule.needed_keys_wo_bk == 0: if rule.needed_keys_wo_bk == 0:
rules_to_remove.append(rule) rules_to_remove[rule] = None
if len(rule.check_locations_wo_bk) < rule.needed_keys_wo_bk or rule.needed_keys_wo_bk > key_layout.max_chests: if len(rule.check_locations_wo_bk) < rule.needed_keys_wo_bk or rule.needed_keys_wo_bk > key_layout.max_chests:
if not rule.prize_relevance and len(rule.bk_conditional_set) > 0: if not rule.prize_relevance and len(rule.bk_conditional_set) > 0:
key_logic.bk_restricted.update(rule.bk_conditional_set) key_logic.bk_restricted.update(rule.bk_conditional_set)
rules_to_remove.append(rule) rules_to_remove[rule] = None
changed = True # impossible for bk to be here, I think changed = True # impossible for bk to be here, I think
for rule_a, rule_b in itertools.combinations([x for x in key_logic.placement_rules if x not in rules_to_remove], 2): for rule_a, rule_b in itertools.combinations([x for x in key_logic.placement_rules if x not in rules_to_remove], 2):
if rule_b.bk_conditional_set and rule_a.check_locations_w_bk: if rule_b.bk_conditional_set and rule_a.check_locations_w_bk:
@@ -511,25 +511,25 @@ def refine_placement_rules(key_layout, max_ctr):
common_locs = len(rule_b.check_locations_w_bk & rule_a.check_locations_wo_bk) common_locs = len(rule_b.check_locations_w_bk & rule_a.check_locations_wo_bk)
if (common_needed - common_locs) * 2 > key_layout.max_chests: if (common_needed - common_locs) * 2 > key_layout.max_chests:
key_logic.bk_restricted.update(rule_a.bk_conditional_set) key_logic.bk_restricted.update(rule_a.bk_conditional_set)
rules_to_remove.append(rule_a) rules_to_remove[rule_a] = None
changed = True changed = True
break break
equivalent_rules = [] equivalent_rules = []
for rule in key_logic.placement_rules: for rule in key_logic.placement_rules:
for rule2 in key_logic.placement_rules: for rule2 in key_logic.placement_rules:
if rule != rule2: if rule != rule2 and rule not in rules_to_remove and rule2 not in rules_to_remove:
if rule.check_locations_w_bk and rule2.check_locations_w_bk: if rule.check_locations_w_bk and rule2.check_locations_w_bk:
if rule2.check_locations_w_bk == rule.check_locations_w_bk and rule2.needed_keys_w_bk > rule.needed_keys_w_bk: if rule2.check_locations_w_bk == rule.check_locations_w_bk and rule2.needed_keys_w_bk > rule.needed_keys_w_bk:
rules_to_remove.append(rule) rules_to_remove[rule] = None
elif rule2.needed_keys_w_bk == rule.needed_keys_w_bk and rule2.check_locations_w_bk < rule.check_locations_w_bk: elif rule2.needed_keys_w_bk == rule.needed_keys_w_bk and rule2.check_locations_w_bk < rule.check_locations_w_bk:
rules_to_remove.append(rule) rules_to_remove[rule] = None
elif rule2.check_locations_w_bk == rule.check_locations_w_bk and rule2.needed_keys_w_bk == rule.needed_keys_w_bk: elif rule2.check_locations_w_bk == rule.check_locations_w_bk and rule2.needed_keys_w_bk == rule.needed_keys_w_bk:
equivalent_rules.append((rule, rule2)) equivalent_rules.append((rule, rule2))
if rule.check_locations_wo_bk and rule2.check_locations_wo_bk and rule.bk_conditional_set == rule2.bk_conditional_set: if rule.check_locations_wo_bk and rule2.check_locations_wo_bk and rule.bk_conditional_set == rule2.bk_conditional_set:
if rule2.check_locations_wo_bk == rule.check_locations_wo_bk and rule2.needed_keys_wo_bk > rule.needed_keys_wo_bk: if rule2.check_locations_wo_bk == rule.check_locations_wo_bk and rule2.needed_keys_wo_bk > rule.needed_keys_wo_bk:
rules_to_remove.append(rule) rules_to_remove[rule] = None
elif rule2.needed_keys_wo_bk == rule.needed_keys_wo_bk and rule2.check_locations_wo_bk < rule.check_locations_wo_bk: elif rule2.needed_keys_wo_bk == rule.needed_keys_wo_bk and rule2.check_locations_wo_bk < rule.check_locations_wo_bk:
rules_to_remove.append(rule) rules_to_remove[rule] = None
elif rule2.check_locations_wo_bk == rule.check_locations_wo_bk and rule2.needed_keys_wo_bk == rule.needed_keys_wo_bk: elif rule2.check_locations_wo_bk == rule.check_locations_wo_bk and rule2.needed_keys_wo_bk == rule.needed_keys_wo_bk:
equivalent_rules.append((rule, rule2)) equivalent_rules.append((rule, rule2))
if len(rules_to_remove) > 0: if len(rules_to_remove) > 0:

View File

@@ -119,6 +119,7 @@ def main(args, seed=None, fish=None):
world.trap_door_mode = args.trap_door_mode.copy() world.trap_door_mode = args.trap_door_mode.copy()
world.key_logic_algorithm = args.key_logic_algorithm.copy() world.key_logic_algorithm = args.key_logic_algorithm.copy()
world.decoupledoors = args.decoupledoors.copy() world.decoupledoors = args.decoupledoors.copy()
world.door_self_loops = args.door_self_loops.copy()
world.experimental = args.experimental.copy() world.experimental = args.experimental.copy()
world.dungeon_counters = args.dungeon_counters.copy() world.dungeon_counters = args.dungeon_counters.copy()
world.fish = fish world.fish = fish
@@ -176,7 +177,7 @@ def main(args, seed=None, fish=None):
world.player_names[player].append(name) world.player_names[player].append(name)
logger.info('') logger.info('')
world.settings = CustomSettings() world.settings = CustomSettings()
world.settings.create_from_world(world, args.race) world.settings.create_from_world(world, args)
outfilebase = f'DR_{args.outputname if args.outputname else world.seed}' outfilebase = f'DR_{args.outputname if args.outputname else world.seed}'
@@ -475,6 +476,7 @@ def copy_world(world):
ret.beemizer = world.beemizer.copy() ret.beemizer = world.beemizer.copy()
ret.intensity = world.intensity.copy() ret.intensity = world.intensity.copy()
ret.decoupledoors = world.decoupledoors.copy() ret.decoupledoors = world.decoupledoors.copy()
ret.door_self_loops = world.door_self_loops.copy()
ret.experimental = world.experimental.copy() ret.experimental = world.experimental.copy()
ret.shopsanity = world.shopsanity.copy() ret.shopsanity = world.shopsanity.copy()
ret.dropshuffle = world.dropshuffle.copy() ret.dropshuffle = world.dropshuffle.copy()

View File

@@ -1022,7 +1022,7 @@ key_drop_data = {
'Ice Palace - Jelly Key Drop': ['Drop', (0x09DA21, 0xE, 3), 'dropped in Ice Palace', 'Small Key (Ice Palace)'], 'Ice Palace - Jelly Key Drop': ['Drop', (0x09DA21, 0xE, 3), 'dropped in Ice Palace', 'Small Key (Ice Palace)'],
'Ice Palace - Conveyor Key Drop': ['Drop', (0x09DE08, 0x3E, 9), 'dropped in Ice Palace', 'Small Key (Ice Palace)'], 'Ice Palace - Conveyor Key Drop': ['Drop', (0x09DE08, 0x3E, 9), 'dropped in Ice Palace', 'Small Key (Ice Palace)'],
'Ice Palace - Hammer Block Key Drop': ['Pot', 0x3F, 'under a block in Ice Palace', 'Small Key (Ice Palace)'], 'Ice Palace - Hammer Block Key Drop': ['Pot', 0x3F, 'under a block in Ice Palace', 'Small Key (Ice Palace)'],
'Ice Palace - Many Pots Pot Key': ['Pot', 0x9F, 'int a pot in Ice Palace', 'Small Key (Ice Palace)'], 'Ice Palace - Many Pots Pot Key': ['Pot', 0x9F, 'in a pot in Ice Palace', 'Small Key (Ice Palace)'],
'Misery Mire - Spikes Pot Key': ['Pot', 0xB3, 'in a pot in Misery Mire', 'Small Key (Misery Mire)'], 'Misery Mire - Spikes Pot Key': ['Pot', 0xB3, 'in a pot in Misery Mire', 'Small Key (Misery Mire)'],
'Misery Mire - Fishbone Pot Key': ['Pot', 0xA1, 'in a pot in forgotten Mire', 'Small Key (Misery Mire)'], 'Misery Mire - Fishbone Pot Key': ['Pot', 0xA1, 'in a pot in forgotten Mire', 'Small Key (Misery Mire)'],
'Misery Mire - Conveyor Crystal Key Drop': ['Drop', (0x09E7FB, 0xC1, 10), 'dropped in Misery Mire', 'Small Key (Misery Mire)'], 'Misery Mire - Conveyor Crystal Key Drop': ['Drop', (0x09E7FB, 0xC1, 10), 'dropped in Misery Mire', 'Small Key (Misery Mire)'],

View File

@@ -174,7 +174,13 @@ CLI: `--key_logic [default|partial|strict]`
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. This is similar to insanity mode in ER where door entrances and exits are not paired anymore. Tends to remove more logic from dungeons as many rooms will not be required to traverse to explore. Hope you like transitions.
CLI `--decoupledoors` CLI: `--decoupledoors`
### Self-Looping Spiral Stairs
If enabled, spiral stairs are allowed to lead to themselves.
CLI: `--door_self_loops`
### Pottery ### Pottery

View File

@@ -176,6 +176,19 @@ These are now independent of retro mode and have three options: None, Random, an
* Forbid bumpers on OW water * Forbid bumpers on OW water
* Forbid Stal on pits * Forbid Stal on pits
* Text fix on sprite author (thanks Synack) * Text fix on sprite author (thanks Synack)
* 1.2.0.21u
* Fix that should force items needed for leaving Zelda's cell to before the throne room, so S&Q isn't mandatory
* Small fix for Tavern Shuffle (thanks Catobat)
* Several small generation fixes
* 1.2.0.20u
* New generation feature that allows Spiral Stair to link to themselves (thank Catobat)
* Added logic for trap doors that could be opened using existing room triggers
* Fixed a problem with inverted generation and the experimental flag
* Added a notes field for user added notes either via CLI or Customizer (thanks Hiimcody and Codemann)
* Fixed a typo for a specific pot hint
* Fix for Hera Boss music (thanks Codemann)
* 1.1.6 (from Stable)
* Minor issue with dungeon counter hud interfering with timer
* 1.2.0.19u * 1.2.0.19u
* Added min/max for triforce pool, goal, and difference for CLI and Customizer. (Thanks Catobat) * Added min/max for triforce pool, goal, and difference for CLI and Customizer. (Thanks Catobat)
* Fixed a bug with dungeon generation * Fixed a bug with dungeon generation

147
Rules.py
View File

@@ -293,6 +293,9 @@ def global_rules(world, player):
# Start of door rando rules # Start of door rando rules
# TODO: Do these need to flag off when door rando is off? - some of them, yes # TODO: Do these need to flag off when door rando is off? - some of them, yes
def is_trapped(entrance):
return world.get_entrance(entrance, player).door.trapped
# Eastern Palace # Eastern Palace
# Eyegore room needs a bow # Eyegore room needs a bow
# set_rule(world.get_entrance('Eastern Duo Eyegores NE', player), lambda state: state.can_shoot_arrows(player)) # set_rule(world.get_entrance('Eastern Duo Eyegores NE', player), lambda state: state.can_shoot_arrows(player))
@@ -370,6 +373,8 @@ def global_rules(world, player):
set_rule(world.get_entrance('Skull Big Chest Hookpath', player), lambda state: state.has('Hookshot', player)) set_rule(world.get_entrance('Skull Big Chest Hookpath', player), lambda state: state.has('Hookshot', player))
set_rule(world.get_entrance('Skull Torch Room WN', player), lambda state: state.has('Fire Rod', player)) set_rule(world.get_entrance('Skull Torch Room WN', player), lambda state: state.has('Fire Rod', player))
if is_trapped('Skull Torch Room WS'):
set_rule(world.get_entrance('Skull Torch Room WS', player), lambda state: state.has('Fire Rod', player))
set_rule(world.get_entrance('Skull Vines NW', player), lambda state: state.has_sword(player)) set_rule(world.get_entrance('Skull Vines NW', player), lambda state: state.has_sword(player))
hidden_pits_door = world.get_door('Skull Small Hall WS', player) hidden_pits_door = world.get_door('Skull Small Hall WS', player)
@@ -407,6 +412,8 @@ def global_rules(world, player):
set_rule(world.get_location('Thieves\' Town - Prize', player), lambda state: state.has('Maiden Unmasked', player) and world.get_location('Thieves\' Town - Prize', player).parent_region.dungeon.boss.can_defeat(state)) set_rule(world.get_location('Thieves\' Town - Prize', player), lambda state: state.has('Maiden Unmasked', player) and world.get_location('Thieves\' Town - Prize', player).parent_region.dungeon.boss.can_defeat(state))
set_rule(world.get_entrance('Ice Lobby WS', player), lambda state: state.can_melt_things(player)) set_rule(world.get_entrance('Ice Lobby WS', player), lambda state: state.can_melt_things(player))
if is_trapped('Ice Lobby SE'):
set_rule(world.get_entrance('Ice Lobby SE', player), lambda state: state.can_melt_things(player))
set_rule(world.get_entrance('Ice Hammer Block ES', player), lambda state: state.can_lift_rocks(player) and state.has('Hammer', player)) set_rule(world.get_entrance('Ice Hammer Block ES', player), lambda state: state.can_lift_rocks(player) and state.has('Hammer', player))
set_rule(world.get_entrance('Ice Right H Path', player), lambda state: state.can_lift_rocks(player) and state.has('Hammer', player)) set_rule(world.get_entrance('Ice Right H Path', player), lambda state: state.can_lift_rocks(player) and state.has('Hammer', player))
set_rule(world.get_location('Ice Palace - Hammer Block Key Drop', player), lambda state: state.can_lift_rocks(player) and state.has('Hammer', player)) set_rule(world.get_location('Ice Palace - Hammer Block Key Drop', player), lambda state: state.can_lift_rocks(player) and state.has('Hammer', player))
@@ -422,6 +429,12 @@ def global_rules(world, player):
set_rule(world.get_entrance('Ice Hookshot Balcony Path', player), lambda state: state.has('Hookshot', player)) set_rule(world.get_entrance('Ice Hookshot Balcony Path', player), lambda state: state.has('Hookshot', player))
if not world.get_door('Ice Switch Room SE', player).entranceFlag: if not world.get_door('Ice Switch Room SE', player).entranceFlag:
set_rule(world.get_entrance('Ice Switch Room SE', player), lambda state: state.has('Cane of Somaria', player) or state.has('Convenient Block', player)) set_rule(world.get_entrance('Ice Switch Room SE', player), lambda state: state.has('Cane of Somaria', player) or state.has('Convenient Block', player))
if is_trapped('Ice Switch Room ES'):
set_rule(world.get_entrance('Ice Switch Room ES', player),
lambda state: state.has('Cane of Somaria', player) or state.has('Convenient Block', player))
if is_trapped('Ice Switch Room NE'):
set_rule(world.get_entrance('Ice Switch Room NE', player),
lambda state: state.has('Cane of Somaria', player) or state.has('Convenient Block', player))
set_defeat_dungeon_boss_rule(world.get_location('Ice Palace - Boss', player)) set_defeat_dungeon_boss_rule(world.get_location('Ice Palace - Boss', player))
set_defeat_dungeon_boss_rule(world.get_location('Ice Palace - Prize', player)) set_defeat_dungeon_boss_rule(world.get_location('Ice Palace - Prize', player))
@@ -442,8 +455,16 @@ def global_rules(world, player):
or state.has('Cane of Byrna', player) or state.has('Cape', player)) or state.has('Cane of Byrna', player) or state.has('Cape', player))
set_rule(world.get_entrance('Mire Left Bridge Hook Path', player), lambda state: state.has('Hookshot', player)) set_rule(world.get_entrance('Mire Left Bridge Hook Path', player), lambda state: state.has('Hookshot', player))
set_rule(world.get_entrance('Mire Tile Room NW', player), lambda state: state.has_fire_source(player)) set_rule(world.get_entrance('Mire Tile Room NW', player), lambda state: state.has_fire_source(player))
if is_trapped('Mire Tile Room SW'):
set_rule(world.get_entrance('Mire Tile Room SW', player), lambda state: state.has_fire_source(player))
if is_trapped('Mire Tile Room ES'):
set_rule(world.get_entrance('Mire Tile Room ES', player), lambda state: state.has_fire_source(player))
set_rule(world.get_entrance('Mire Attic Hint Hole', player), lambda state: state.has_fire_source(player)) set_rule(world.get_entrance('Mire Attic Hint Hole', player), lambda state: state.has_fire_source(player))
set_rule(world.get_entrance('Mire Dark Shooters SW', player), lambda state: state.has('Cane of Somaria', player)) set_rule(world.get_entrance('Mire Dark Shooters SW', player), lambda state: state.has('Cane of Somaria', player))
# Not: somaria doesn't work here, so this cannot be opened if trapped
# if is_trapped('Mire Dark Shooters SE'):
# set_rule(world.get_entrance('Mire Dark Shooters SE', player),
# lambda state: state.has('Cane of Somaria', player))
set_defeat_dungeon_boss_rule(world.get_location('Misery Mire - Boss', player)) set_defeat_dungeon_boss_rule(world.get_location('Misery Mire - Boss', player))
set_defeat_dungeon_boss_rule(world.get_location('Misery Mire - Prize', player)) set_defeat_dungeon_boss_rule(world.get_location('Misery Mire - Prize', player))
@@ -459,6 +480,9 @@ def global_rules(world, player):
set_rule(world.get_entrance('TR Hub Path', player), lambda state: state.has('Cane of Somaria', player)) set_rule(world.get_entrance('TR Hub Path', player), lambda state: state.has('Cane of Somaria', player))
set_rule(world.get_entrance('TR Hub Ledges Path', player), lambda state: state.has('Cane of Somaria', player)) set_rule(world.get_entrance('TR Hub Ledges Path', player), lambda state: state.has('Cane of Somaria', player))
set_rule(world.get_entrance('TR Torches NW', player), lambda state: state.has('Cane of Somaria', player) and state.has('Fire Rod', player)) set_rule(world.get_entrance('TR Torches NW', player), lambda state: state.has('Cane of Somaria', player) and state.has('Fire Rod', player))
if is_trapped('TR Torches WN'):
set_rule(world.get_entrance('TR Torches WN', player),
lambda state: state.has('Cane of Somaria', player) and state.has('Fire Rod', player))
set_rule(world.get_entrance('TR Big Chest Entrance Gap', player), lambda state: state.has('Cane of Somaria', player) or state.has('Hookshot', player)) set_rule(world.get_entrance('TR Big Chest Entrance Gap', player), lambda state: state.has('Cane of Somaria', player) or state.has('Hookshot', player))
set_rule(world.get_entrance('TR Big Chest Gap', player), lambda state: state.has('Cane of Somaria', player) or state.has_Boots(player)) set_rule(world.get_entrance('TR Big Chest Gap', player), lambda state: state.has('Cane of Somaria', player) or state.has_Boots(player))
set_rule(world.get_entrance('TR Dark Ride Up Stairs', player), lambda state: state.has('Cane of Somaria', player)) set_rule(world.get_entrance('TR Dark Ride Up Stairs', player), lambda state: state.has('Cane of Somaria', player))
@@ -478,10 +502,20 @@ def global_rules(world, player):
set_rule(world.get_location('Ganons Tower - Bob\'s Torch', player), lambda state: state.has_Boots(player)) set_rule(world.get_location('Ganons Tower - Bob\'s Torch', player), lambda state: state.has_Boots(player))
set_rule(world.get_entrance('GT Hope Room EN', player), lambda state: state.has('Cane of Somaria', player)) set_rule(world.get_entrance('GT Hope Room EN', player), lambda state: state.has('Cane of Somaria', player))
if is_trapped('GT Hope Room WN'):
set_rule(world.get_entrance('GT Hope Room WN', player), lambda state: state.has('Cane of Somaria', player))
set_rule(world.get_entrance('GT Conveyor Cross Hammer Path', player), lambda state: state.has('Hammer', player)) set_rule(world.get_entrance('GT Conveyor Cross Hammer Path', player), lambda state: state.has('Hammer', player))
set_rule(world.get_entrance('GT Conveyor Cross Hookshot Path', player), lambda state: state.has('Hookshot', player)) set_rule(world.get_entrance('GT Conveyor Cross Hookshot Path', player), lambda state: state.has('Hookshot', player))
if is_trapped('GT Conveyor Cross EN'):
set_rule(world.get_entrance('GT Conveyor Cross EN', player), lambda state: state.has('Hammer', player))
if not world.get_door('GT Speed Torch SE', player).entranceFlag: if not world.get_door('GT Speed Torch SE', player).entranceFlag:
set_rule(world.get_entrance('GT Speed Torch SE', player), lambda state: state.has('Fire Rod', player)) set_rule(world.get_entrance('GT Speed Torch SE', player), lambda state: state.has('Fire Rod', player))
if is_trapped('GT Speed Torch NE'):
set_rule(world.get_entrance('GT Speed Torch NE', player), lambda state: state.has('Fire Rod', player))
if is_trapped('GT Speed Torch WS'):
set_rule(world.get_entrance('GT Speed Torch WS', player), lambda state: state.has('Fire Rod', player))
if is_trapped('GT Speed Torch WN'):
set_rule(world.get_entrance('GT Speed Torch WN', player), lambda state: state.has('Fire Rod', player))
set_rule(world.get_entrance('GT Hookshot South-Mid Path', player), lambda state: state.has('Hookshot', player)) set_rule(world.get_entrance('GT Hookshot South-Mid Path', player), lambda state: state.has('Hookshot', player))
set_rule(world.get_entrance('GT Hookshot Mid-North Path', player), lambda state: state.has('Hookshot', player)) set_rule(world.get_entrance('GT Hookshot Mid-North Path', player), lambda state: state.has('Hookshot', player))
set_rule(world.get_entrance('GT Hookshot East-Mid Path', player), lambda state: state.has('Hookshot', player) or state.has_Boots(player)) set_rule(world.get_entrance('GT Hookshot East-Mid Path', player), lambda state: state.has('Hookshot', player) or state.has_Boots(player))
@@ -503,6 +537,8 @@ def global_rules(world, player):
set_rule(world.get_entrance('GT Lanmolas 2 ES', player), lambda state: world.get_region('GT Lanmolas 2', player).dungeon.bosses['middle'].can_defeat(state)) set_rule(world.get_entrance('GT Lanmolas 2 ES', player), lambda state: world.get_region('GT Lanmolas 2', player).dungeon.bosses['middle'].can_defeat(state))
set_rule(world.get_entrance('GT Lanmolas 2 NW', player), lambda state: world.get_region('GT Lanmolas 2', player).dungeon.bosses['middle'].can_defeat(state)) set_rule(world.get_entrance('GT Lanmolas 2 NW', player), lambda state: world.get_region('GT Lanmolas 2', player).dungeon.bosses['middle'].can_defeat(state))
set_rule(world.get_entrance('GT Torch Cross ES', player), lambda state: state.has_fire_source(player)) set_rule(world.get_entrance('GT Torch Cross ES', player), lambda state: state.has_fire_source(player))
if is_trapped('GT Torch Cross WN'):
set_rule(world.get_entrance('GT Torch Cross WN', player), lambda state: state.has_fire_source(player))
set_rule(world.get_entrance('GT Falling Torches NE', player), lambda state: state.has_fire_source(player)) set_rule(world.get_entrance('GT Falling Torches NE', player), lambda state: state.has_fire_source(player))
# todo: the following only applies to crystal state propagation from this supertile # todo: the following only applies to crystal state propagation from this supertile
# you can also reset the supertile, but I'm not sure how to model that # you can also reset the supertile, but I'm not sure how to model that
@@ -780,12 +816,16 @@ def challenge_room_rules(world, player):
room_map = world.data_tables[player].uw_enemy_table.room_map room_map = world.data_tables[player].uw_enemy_table.room_map
stats = world.data_tables[player].enemy_stats stats = world.data_tables[player].enemy_stats
for region, data in std_kill_rooms.items(): for region, data in std_kill_rooms.items():
entrances, room_id, enemy_list = data entrances, trap_ables, room_id, enemy_list = data
rule = get_challenge_rule(world, player, room_map, stats, room_id, enemy_list, region) rule = get_challenge_rule(world, player, room_map, stats, room_id, enemy_list, region)
for ent in entrances: for ent in entrances:
entrance = world.get_entrance(ent, player) entrance = world.get_entrance(ent, player)
if not entrance.door or not entrance.door.entranceFlag: if not entrance.door or not entrance.door.entranceFlag:
add_rule_new(world.get_entrance(ent, player), rule) add_rule_new(entrance, rule)
for ent in trap_ables:
entrance = world.get_entrance(ent, player)
if entrance.door.trapped and not entrance.door.entranceFlag:
add_rule_new(entrance, rule)
for region, data in kill_chests.items(): for region, data in kill_chests.items():
locations, room_id, enemy_list = data locations, room_id, enemy_list = data
rule = get_challenge_rule(world, player, room_map, stats, room_id, enemy_list, region) rule = get_challenge_rule(world, player, room_map, stats, room_id, enemy_list, region)
@@ -1144,6 +1184,9 @@ def swordless_rules(world, player):
set_rule(world.get_entrance('Tower Altar NW', player), lambda state: True) set_rule(world.get_entrance('Tower Altar NW', player), lambda state: True)
set_rule(world.get_entrance('Skull Vines NW', player), lambda state: True) set_rule(world.get_entrance('Skull Vines NW', player), lambda state: True)
set_rule(world.get_entrance('Ice Lobby WS', player), lambda state: state.has('Fire Rod', player) or state.has('Bombos', player)) set_rule(world.get_entrance('Ice Lobby WS', player), lambda state: state.has('Fire Rod', player) or state.has('Bombos', player))
if world.get_entrance('Ice Lobby SE', player).door.trapped:
set_rule(world.get_entrance('Ice Lobby SE', player),
lambda state: state.has('Fire Rod', player) or state.has('Bombos', player))
set_rule(world.get_location('Ice Palace - Freezor Chest', player), lambda state: state.has('Fire Rod', player) or state.has('Bombos', player)) set_rule(world.get_location('Ice Palace - Freezor Chest', player), lambda state: state.has('Fire Rod', player) or state.has('Bombos', player))
set_rule(world.get_location('Ether Tablet', player), lambda state: state.has('Book of Mudora', player) and state.has('Hammer', player)) set_rule(world.get_location('Ether Tablet', player), lambda state: state.has('Book of Mudora', player) and state.has('Hammer', player))
@@ -1157,76 +1200,76 @@ def swordless_rules(world, player):
if world.mode[player] != 'inverted': if world.mode[player] != 'inverted':
set_rule(world.get_entrance('Agahnims Tower', player), lambda state: state.has('Cape', player) or state.has('Hammer', player)) set_rule(world.get_entrance('Agahnims Tower', player), lambda state: state.has('Cape', player) or state.has('Hammer', player))
std_kill_rooms = { std_kill_rooms = {
'Hyrule Dungeon Armory Main': # One green guard 'Hyrule Dungeon Armory Main': # One green guard
(['Hyrule Dungeon Armory S', 'Hyrule Dungeon Armory ES'], 0x71, [0]), (['Hyrule Dungeon Armory S', 'Hyrule Dungeon Armory ES'], ['Hyrule Dungeon Armory Interior Key Door N'],
0x71, [0]),
'Hyrule Dungeon Armory Boomerang': # One blue guard 'Hyrule Dungeon Armory Boomerang': # One blue guard
(['Hyrule Dungeon Armory Boomerang WS'], 0x71, [1]), (['Hyrule Dungeon Armory Boomerang WS'], [], 0x71, [1]),
'Eastern Stalfos Spawn': # Can use pots up to a point see stalfos_spawn_exception 'Eastern Stalfos Spawn': # Can use pots up to a point see stalfos_spawn_exception
(['Eastern Stalfos Spawn ES', 'Eastern Stalfos Spawn NW'], 0xa8, []), (['Eastern Stalfos Spawn ES', 'Eastern Stalfos Spawn NW'], [], 0xa8, []),
'Eastern Single Eyegore': 'Eastern Single Eyegore':
(['Eastern Single Eyegore NE'], 0xd8, [8, 9, 10]), (['Eastern Single Eyegore NE'], ['Eastern Single Eyegore ES'], 0xd8, [8, 9, 10]),
'Eastern Duo Eyegores': 'Eastern Duo Eyegores':
(['Eastern Duo Eyegores NE'], 0xd8, [0, 1, 2, 3, 4, 5, 6, 7]), (['Eastern Duo Eyegores NE'], ['Eastern Duo Eyegores S'], 0xd8, [0, 1, 2, 3, 4, 5, 6, 7]),
'Desert Compass Room': # Three popos (beamos) 'Desert Compass Room': # Three popos (beamos)
(['Desert Compass NE'], 0x085, [2, 3, 4, 5]), (['Desert Compass NE'], ['Desert Compass Key Door WN'], 0x085, [2, 3, 4, 5]),
'Desert Four Statues': # Four popos (beamos) 'Desert Four Statues': # Four popos (beamos)
(['Desert Four Statues NW', 'Desert Four Statues ES'], 0x53, [5, 6, 8, 9, 10]), (['Desert Four Statues NW', 'Desert Four Statues ES'], [], 0x53, [5, 6, 8, 9, 10]),
'Hera Beetles': # Three blue beetles and only two pots, and bombs don't work. 'Hera Beetles': # Three blue beetles and only two pots, and bombs don't work.
(['Hera Beetles WS'], 0x31, [7, 8, 10]), (['Hera Beetles WS'], [], 0x31, [7, 8, 10]),
'Tower Gold Knights': # Two ball and chain 'Tower Gold Knights': # Two ball and chain
(['Tower Gold Knights SW', 'Tower Gold Knights EN'], 0xe0, [0, 1]), (['Tower Gold Knights SW', 'Tower Gold Knights EN'], [], 0xe0, [0, 1]),
'Tower Dark Archers': # Backwards kill room 'Tower Dark Archers': # Backwards kill room
(['Tower Dark Archers WN'], 0xc0, [0, 1, 3]), (['Tower Dark Archers WN'], [], 0xc0, [0, 1, 3]),
'Tower Red Spears': # Two spear soldiers 'Tower Red Spears': # Two spear soldiers
(['Tower Red Spears WN'], 0xb0, [1, 2, 3, 4]), (['Tower Red Spears WN'], [], 0xb0, [1, 2, 3, 4]),
'Tower Red Guards': # Two usain bolts 'Tower Red Guards': # Two usain bolts
(['Tower Red Guards EN', 'Tower Red Guards SW'], 0xb0, [0, 5]), (['Tower Red Guards EN', 'Tower Red Guards SW'], [], 0xb0, [0, 5]),
'Tower Circle of Pots': # Two spear soldiers. Plenty of pots. 'Tower Circle of Pots': # Two spear soldiers. Plenty of pots.
(['Tower Circle of Pots NW'], 0xb0, [7, 8, 9, 10]), (['Tower Circle of Pots NW'], ['Tower Circle of Pots ES'], 0xb0, [7, 8, 9, 10]),
'PoD Mimics 1': 'PoD Mimics 1':
(['PoD Mimics 1 NW'], 0x4b, [0, 3, 4]), (['PoD Mimics 1 NW'], ['PoD Mimics 1 SW'], 0x4b, [0, 3, 4]),
'PoD Mimics 2': 'PoD Mimics 2':
(['PoD Mimics 2 NW'], 0x1b, [3, 4, 5]), (['PoD Mimics 2 NW'], ['PoD Mimics 2 SW'], 0x1b, [3, 4, 5]),
'PoD Turtle Party': # Lots of turtles. 'PoD Turtle Party': # Lots of turtles.
(['PoD Turtle Party ES', 'PoD Turtle Party NW'], 0x0b, [4, 5, 6, 7, 8, 9]), (['PoD Turtle Party ES', 'PoD Turtle Party NW'], [], 0x0b, [4, 5, 6, 7, 8, 9]),
'Thieves Basement Block': # One blue and one red zazak and one Stalfos. Two pots. Need weapon. 'Thieves Basement Block': # One blue and one red zazak and one Stalfos. Two pots. Need weapon.
(['Thieves Basement Block WN'], 0x45, [1, 2, 3]), (['Thieves Basement Block WN'], ['Thieves Blocked Entry SW'], 0x45, [1, 2, 3]),
'Ice Jelly Key': 'Ice Jelly Key':
(['Ice Jelly Key ES'], 0x0e, [1, 2, 3]), (['Ice Jelly Key ES'], [], 0x0e, [1, 2, 3]),
'Ice Stalfos Hint': # Need bombs for big stalfos knights 'Ice Stalfos Hint': # Need bombs for big stalfos knights
(['Ice Stalfos Hint SE'], 0x3e, [1, 2]), (['Ice Stalfos Hint SE'], [], 0x3e, [1, 2]),
'Ice Pengator Trap': # Five pengators. Bomb-doable? 'Ice Pengator Trap': # Five pengators. Bomb-doable?
(['Ice Pengator Trap NE'], 0x6e, [0, 1, 2, 3, 4]), (['Ice Pengator Trap NE'], [], 0x6e, [0, 1, 2, 3, 4]),
'Mire 2': # Wizzrobes. Bombs dont work. 'Mire 2': # Wizzrobes. Bombs dont work.
(['Mire 2 NE'], 0xd2, [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]), (['Mire 2 NE'], [], 0xd2, [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]),
'Mire Cross': # 4 Sluggulas. Bombs don't work 'Mire Cross': # 4 Sluggulas. Bombs don't work
(['Mire Cross ES'], 0xb2, [5, 6, 7, 10, 11]), (['Mire Cross ES'], ['Mire Cross SW'], 0xb2, [5, 6, 7, 10, 11]),
'TR Twin Pokeys': # Two pokeys 'TR Twin Pokeys': # Two pokeys
(['TR Twin Pokeys EN', 'TR Twin Pokeys SW'], 0x24, [3, 4, 5, 6]), (['TR Twin Pokeys EN', 'TR Twin Pokeys SW'], ['TR Twin Pokeys NW'], 0x24, [3, 4, 5, 6]),
'TR Tongue Pull': # Kill zols for money 'TR Tongue Pull': # Kill zols for money
(['TR Tongue Pull NE'], 0x04, [9, 13, 14]), (['TR Tongue Pull NE'], ['TR Tongue Pull WS'], 0x04, [9, 13, 14]),
'GT Petting Zoo': # Don't make anyone do this room with bombs. 'GT Petting Zoo': # Don't make anyone do this room with bombs.
(['GT Petting Zoo SE'], 0x7d, [4, 5, 6, 7, 8, 10]), (['GT Petting Zoo SE'], [], 0x7d, [4, 5, 6, 7, 8, 10]),
'GT DMs Room': # Four red stalfos 'GT DMs Room': # Four red stalfos
(['GT DMs Room SW'], 0x7b, [2, 3, 4, 5, 8, 9, 10]), (['GT DMs Room SW'], [], 0x7b, [2, 3, 4, 5, 8, 9, 10]),
'GT Gauntlet 1': # Stalfos/zazaks 'GT Gauntlet 1': # Stalfos/zazaks
(['GT Gauntlet 1 WN'], 0x5d, [3, 4, 5, 6]), (['GT Gauntlet 1 WN'], [], 0x5d, [3, 4, 5, 6]),
'GT Gauntlet 2': # Red stalfos 'GT Gauntlet 2': # Red stalfos
(['GT Gauntlet 2 EN', 'GT Gauntlet 2 SW'], 0x5d, [0, 1, 2, 7]), (['GT Gauntlet 2 EN', 'GT Gauntlet 2 SW'], [], 0x5d, [0, 1, 2, 7]),
'GT Gauntlet 3': # Blue zazaks 'GT Gauntlet 3': # Blue zazaks
(['GT Gauntlet 3 NW', 'GT Gauntlet 3 SW'], 0x5d, [8, 9, 10, 11, 12]), (['GT Gauntlet 3 NW', 'GT Gauntlet 3 SW'], [], 0x5d, [8, 9, 10, 11, 12]),
'GT Gauntlet 4': # Red zazaks 'GT Gauntlet 4': # Red zazaks
(['GT Gauntlet 4 NW', 'GT Gauntlet 4 SW'], 0x6d, [0, 1, 2, 3]), (['GT Gauntlet 4 NW', 'GT Gauntlet 4 SW'], [], 0x6d, [0, 1, 2, 3]),
'GT Gauntlet 5': # Stalfos and zazak 'GT Gauntlet 5': # Stalfos and zazak
(['GT Gauntlet 5 NW', 'GT Gauntlet 5 WS'], 0x6d, [4, 5, 6, 7, 8]), (['GT Gauntlet 5 NW', 'GT Gauntlet 5 WS'], [], 0x6d, [4, 5, 6, 7, 8]),
'GT Wizzrobes 1': # Wizzrobes. Bombs don't work 'GT Wizzrobes 1': # Wizzrobes. Bombs don't work
(['GT Wizzrobes 1 SW'], 0xa5, [2, 3, 7]), (['GT Wizzrobes 1 SW'], [], 0xa5, [2, 3, 7]),
'GT Wizzrobes 2': # Wizzrobes. Bombs don't work 'GT Wizzrobes 2': # Wizzrobes. Bombs don't work
(['GT Wizzrobes 2 SE', 'GT Wizzrobes 2 NE'], 0xa5, [0, 1, 4, 5, 6]), (['GT Wizzrobes 2 SE', 'GT Wizzrobes 2 NE'], [], 0xa5, [0, 1, 4, 5, 6]),
'Spiral Cave (Top)': # for traversal in enemizer at low health 'Spiral Cave (Top)': # for traversal in enemizer at low health
(['Spiral Cave (top to bottom)'], 0xEE, [0, 1, 2, 3, 4]), (['Spiral Cave (top to bottom)'], [], 0xEE, [0, 1, 2, 3, 4]),
} # all trap rooms? (Desert Trap Room, Thieves Trap Room currently subtile only) } # all trap rooms? (Desert Trap Room, Thieves Trap Room currently subtile only)
kill_chests = { kill_chests = {
@@ -1296,15 +1339,21 @@ def standard_rules(world, player):
if region.name in std_kill_rooms: if region.name in std_kill_rooms:
for ent in std_kill_rooms[region.name][0]: for ent in std_kill_rooms[region.name][0]:
add_rule(world.get_entrance(ent, player), lambda state: standard_escape_rule(state)) add_rule(world.get_entrance(ent, player), lambda state: standard_escape_rule(state))
for ent in std_kill_rooms[region.name][1]:
entrance = world.get_entrance(ent, player)
if entrance.door.trapped:
add_rule(entrance, lambda state: standard_escape_rule(state))
set_rule(world.get_location('Zelda Pickup', player), lambda state: state.has('Big Key (Escape)', player)) set_rule(world.get_location('Zelda Pickup', player), lambda state: state.has('Big Key (Escape)', player))
set_rule(world.get_entrance('Hyrule Castle Throne Room Tapestry', player), lambda state: state.has('Zelda Herself', player))
set_rule(world.get_entrance('Hyrule Castle Tapestry Backwards', player), lambda state: state.has('Zelda Herself', player)) set_rule(world.get_entrance('Hyrule Castle Tapestry Backwards', player), lambda state: state.has('Zelda Herself', player))
def check_rule_list(state, r_list): def check_rule_list(state, r_list):
return True if len(r_list) <= 0 else r_list[0](state) and check_rule_list(state, r_list[1:]) return True if len(r_list) <= 0 else r_list[0](state) and check_rule_list(state, r_list[1:])
rule_list, debug_path = find_rules_for_zelda_delivery(world, player) rule_list, debug_path = find_rules_for_zelda_delivery(world, player)
set_rule(world.get_location('Zelda Drop Off', player), lambda state: state.has('Zelda Herself', player) and check_rule_list(state, rule_list)) set_rule(world.get_entrance('Hyrule Castle Throne Room Tapestry', player),
lambda state: state.has('Zelda Herself', player) and check_rule_list(state, rule_list))
set_rule(world.get_location('Zelda Drop Off', player),
lambda state: state.has('Zelda Herself', player) and check_rule_list(state, rule_list))
for location in ['Mushroom', 'Bottle Merchant', 'Flute Spot', 'Sunken Treasure', 'Purple Chest']: for location in ['Mushroom', 'Bottle Merchant', 'Flute Spot', 'Sunken Treasure', 'Purple Chest']:
add_rule(world.get_location(location, player), lambda state: state.has('Zelda Delivered', player)) add_rule(world.get_location(location, player), lambda state: state.has('Zelda Delivered', player))
@@ -1921,6 +1970,11 @@ def set_bunny_rules(world, player, inverted):
if is_bunny(bunny_exit.parent_region): if is_bunny(bunny_exit.parent_region):
add_rule(bunny_exit, get_rule_to_add(bunny_exit.parent_region)) add_rule(bunny_exit, get_rule_to_add(bunny_exit.parent_region))
for ent_name in bunny_impassible_if_trapped:
bunny_exit = world.get_entrance(ent_name, player)
if bunny_exit.door.trapped and is_bunny(bunny_exit.parent_region):
add_rule(bunny_exit, get_rule_to_add(bunny_exit.parent_region))
doors_to_check = [x for x in world.doors if x.player == player and x not in bunny_impassible_doors] doors_to_check = [x for x in world.doors if x.player == player and x not in bunny_impassible_doors]
doors_to_check = [x for x in doors_to_check if x.type in [DoorType.Normal, DoorType.Interior] and not x.blocked] doors_to_check = [x for x in doors_to_check if x.type in [DoorType.Normal, DoorType.Interior] and not x.blocked]
for door in doors_to_check: for door in doors_to_check:
@@ -2053,6 +2107,21 @@ bunny_impassible_doors = {
'GT Validation Block Path' 'GT Validation Block Path'
} }
# todo: for trap_door_exceptions: 'Ice Tall Hint SE' (not excepted)
# todo: 'Eastern Courtyard Ledge S', 'PoD Lobby N', 'Ice Tall Hint SE', 'TR Tongue Pull WS'
# these should generally match trap_door_exceptions unless the switch is in the open/push block
bunny_impassible_if_trapped = {
'Hyrule Dungeon Armory Interior Key Door N', 'Eastern Pot Switch WN', 'Eastern Lobby NW',
'Eastern Lobby NE', 'Desert Compass Key Door WN', 'Tower Circle of Pots ES', 'PoD Mimics 2 SW',
'PoD Middle Cage S', 'Swamp Push Statue S', 'Skull 2 East Lobby WS', 'Skull Torch Room WS',
'Thieves Conveyor Maze WN', 'Thieves Conveyor Maze SW', 'Thieves Blocked Entry SW', 'Ice Bomb Jump NW',
'Ice Tall Hint EN', 'Ice Switch Room ES', 'Ice Switch Room NE', 'Mire Cross SW',
'Mire Tile Room SW', 'Mire Tile Room ES', 'TR Twin Pokeys NW', 'TR Torches WN', 'GT Hope Room WN',
'GT Speed Torch NE', 'GT Speed Torch WS', 'GT Torch Cross WN', 'GT Hidden Spikes SE', 'GT Conveyor Cross EN',
'GT Speed Torch WN', 'Ice Lobby SE'
}
def add_key_logic_rules(world, player): def add_key_logic_rules(world, player):
key_logic = world.key_logic[player] key_logic = world.key_logic[player]

View File

@@ -3,6 +3,7 @@ meta:
players: 1 players: 1
seed: 41 # note to self: seed 42 had an interesting Swamp Palace problem seed: 41 # note to self: seed 42 had an interesting Swamp Palace problem
names: Lonk names: Lonk
notes: "Some notes specified by the user"
settings: settings:
1: 1:
door_shuffle: basic door_shuffle: basic

View File

@@ -31,6 +31,9 @@
partial: 0 partial: 0
strict: 0 strict: 0
decoupledoors: off decoupledoors: off
door_self_loops:
on: 1
off: 1
dropshuffle: dropshuffle:
none: 4 none: 4
keys: 1 keys: 1

View File

@@ -31,6 +31,9 @@ key_logic_algorithm:
decoupledoors: decoupledoors:
off: 9 # more strict off: 9 # more strict
on: 1 on: 1
door_self_loops:
on: 1
off: 1
dropshuffle: dropshuffle:
none: 10 # fewer locations none: 10 # fewer locations
keys: 1 keys: 1

View File

@@ -215,6 +215,10 @@
"action": "store_true", "action": "store_true",
"type": "bool" "type": "bool"
}, },
"door_self_loops": {
"action": "store_true",
"type": "bool"
},
"experimental": { "experimental": {
"action": "store_true", "action": "store_true",
"type": "bool" "type": "bool"
@@ -518,5 +522,6 @@
] ]
}, },
"outputname": {}, "outputname": {},
"notes": {},
"code": {} "code": {}
} }

View File

@@ -220,6 +220,7 @@
"door_shuffle": [ "door_shuffle": [
"Select Door Shuffling Algorithm. (default: %(default)s)", "Select Door Shuffling Algorithm. (default: %(default)s)",
"Basic: Doors are mixed within a single dungeon.", "Basic: Doors are mixed within a single dungeon.",
"Paired Dungeon are paired (with one trio) and only mixed in those groups",
"Partitioned Doors are mixed in 3 partitions: L1-3+HC+AT, D1-4, D5-8", "Partitioned Doors are mixed in 3 partitions: L1-3+HC+AT, D1-4, D5-8",
"Crossed: Doors are mixed between all dungeons.", "Crossed: Doors are mixed between all dungeons.",
"Vanilla: All doors are connected the same way they were in the", "Vanilla: All doors are connected the same way they were in the",
@@ -253,6 +254,7 @@
"strict: Ensure small keys are available" "strict: Ensure small keys are available"
], ],
"decoupledoors" : [ "Door entrances and exits are decoupled" ], "decoupledoors" : [ "Door entrances and exits are decoupled" ],
"door_self_loops" : [ "Spiral stairs are allowed to self-loop" ],
"experimental": [ "Enable experimental features. (default: %(default)s)" ], "experimental": [ "Enable experimental features. (default: %(default)s)" ],
"dungeon_counters": [ "Enable dungeon chest counters. (default: %(default)s)" ], "dungeon_counters": [ "Enable dungeon chest counters. (default: %(default)s)" ],
"crystals_ganon": [ "crystals_ganon": [

View File

@@ -58,10 +58,12 @@
"randomizer.dungeon.smallkeyshuffle.universal": "Universal", "randomizer.dungeon.smallkeyshuffle.universal": "Universal",
"randomizer.dungeon.bigkeyshuffle": "Big Keys", "randomizer.dungeon.bigkeyshuffle": "Big Keys",
"randomizer.dungeon.decoupledoors": "Decouple Doors", "randomizer.dungeon.decoupledoors": "Decouple Doors",
"randomizer.dungeon.door_self_loops": "Allow Self-Looping Spiral Stairs",
"randomizer.dungeon.dungeondoorshuffle": "Dungeon Door Shuffle", "randomizer.dungeon.dungeondoorshuffle": "Dungeon Door Shuffle",
"randomizer.dungeon.dungeondoorshuffle.vanilla": "Vanilla", "randomizer.dungeon.dungeondoorshuffle.vanilla": "Vanilla",
"randomizer.dungeon.dungeondoorshuffle.basic": "Basic", "randomizer.dungeon.dungeondoorshuffle.basic": "Basic",
"randomizer.dungeon.dungeondoorshuffle.paired": "Paired",
"randomizer.dungeon.dungeondoorshuffle.partitioned": "Partitioned", "randomizer.dungeon.dungeondoorshuffle.partitioned": "Partitioned",
"randomizer.dungeon.dungeondoorshuffle.crossed": "Crossed", "randomizer.dungeon.dungeondoorshuffle.crossed": "Crossed",

View File

@@ -66,6 +66,12 @@
} }
}, },
"decoupledoors": { "decoupledoors": {
"type": "checkbox",
"config": {
"padx": [20,0]
}
},
"door_self_loops": {
"type": "checkbox", "type": "checkbox",
"config": { "config": {
"padx": [20,0], "padx": [20,0],

View File

@@ -63,6 +63,7 @@ class CustomSettings(object):
args.suppress_rom = get_setting(meta['suppress_rom'], args.suppress_rom) args.suppress_rom = get_setting(meta['suppress_rom'], args.suppress_rom)
args.names = get_setting(meta['names'], args.names) args.names = get_setting(meta['names'], args.names)
args.race = get_setting(meta['race'], args.race) args.race = get_setting(meta['race'], args.race)
args.notes = get_setting(meta['user_notes'], args.notes)
self.player_range = range(1, args.multi + 1) self.player_range = range(1, args.multi + 1)
if 'settings' in self.file_source: if 'settings' in self.file_source:
for p in self.player_range: for p in self.player_range:
@@ -114,6 +115,7 @@ class CustomSettings(object):
args.trap_door_mode[p] = get_setting(settings['trap_door_mode'], args.trap_door_mode[p]) args.trap_door_mode[p] = get_setting(settings['trap_door_mode'], args.trap_door_mode[p])
args.key_logic_algorithm[p] = get_setting(settings['key_logic_algorithm'], args.key_logic_algorithm[p]) args.key_logic_algorithm[p] = get_setting(settings['key_logic_algorithm'], args.key_logic_algorithm[p])
args.decoupledoors[p] = get_setting(settings['decoupledoors'], args.decoupledoors[p]) args.decoupledoors[p] = get_setting(settings['decoupledoors'], args.decoupledoors[p])
args.door_self_loops[p] = get_setting(settings['door_self_loops'], args.door_self_loops[p])
args.dungeon_counters[p] = get_setting(settings['dungeon_counters'], args.dungeon_counters[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_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.crystals_ganon[p] = get_setting(settings['crystals_ganon'], args.crystals_ganon[p])
@@ -221,14 +223,15 @@ class CustomSettings(object):
return self.file_source['enemies'] return self.file_source['enemies']
return None return None
def create_from_world(self, world, race): def create_from_world(self, world, settings):
self.player_range = range(1, world.players + 1) self.player_range = range(1, world.players + 1)
settings_dict, meta_dict = {}, {} settings_dict, meta_dict = {}, {}
self.world_rep['meta'] = meta_dict self.world_rep['meta'] = meta_dict
meta_dict['players'] = world.players meta_dict['players'] = world.players
meta_dict['algorithm'] = world.algorithm meta_dict['algorithm'] = world.algorithm
meta_dict['seed'] = world.seed meta_dict['seed'] = world.seed
meta_dict['race'] = race meta_dict['race'] = settings.race
meta_dict['user_notes'] = settings.notes
self.world_rep['settings'] = settings_dict self.world_rep['settings'] = settings_dict
for p in self.player_range: for p in self.player_range:
settings_dict[p] = {} settings_dict[p] = {}
@@ -239,6 +242,7 @@ class CustomSettings(object):
settings_dict[p]['trap_door_mode'] = world.trap_door_mode[p] settings_dict[p]['trap_door_mode'] = world.trap_door_mode[p]
settings_dict[p]['key_logic_algorithm'] = world.key_logic_algorithm[p] settings_dict[p]['key_logic_algorithm'] = world.key_logic_algorithm[p]
settings_dict[p]['decoupledoors'] = world.decoupledoors[p] settings_dict[p]['decoupledoors'] = world.decoupledoors[p]
settings_dict[p]['door_self_loops'] = world.door_self_loops[p]
settings_dict[p]['logic'] = world.logic[p] settings_dict[p]['logic'] = world.logic[p]
settings_dict[p]['mode'] = world.mode[p] settings_dict[p]['mode'] = world.mode[p]
settings_dict[p]['swords'] = world.swords[p] settings_dict[p]['swords'] = world.swords[p]

View File

@@ -106,6 +106,7 @@ SETTINGSTOPROCESS = {
"door_type_mode": "door_type_mode", "door_type_mode": "door_type_mode",
"trap_door_mode": "trap_door_mode", "trap_door_mode": "trap_door_mode",
"decoupledoors": "decoupledoors", "decoupledoors": "decoupledoors",
"door_self_loops": "door_self_loops",
"experimental": "experimental", "experimental": "experimental",
"dungeon_counters": "dungeon_counters", "dungeon_counters": "dungeon_counters",
"mixed_travel": "mixed_travel", "mixed_travel": "mixed_travel",

View File

@@ -22,7 +22,7 @@ def generate_dungeon(builder, entrance_region_names, split_dungeon, world, playe
queue = collections.deque(proposed_map.items()) queue = collections.deque(proposed_map.items())
while len(queue) > 0: while len(queue) > 0:
a, b = queue.popleft() a, b = queue.popleft()
if world.decoupledoors[player]: if a == b or world.decoupledoors[player]:
connect_doors_one_way(a, b) connect_doors_one_way(a, b)
else: else:
connect_doors(a, b) connect_doors(a, b)
@@ -72,11 +72,13 @@ def generate_dungeon_find_proposal(builder, entrance_region_names, split_dungeon
entrance_regions = [x for x in entrance_regions if x not in excluded.keys()] entrance_regions = [x for x in entrance_regions if x not in excluded.keys()]
doors_to_connect, idx = {}, 0 doors_to_connect, idx = {}, 0
all_regions = set() all_regions = set()
bk_special = False
for sector in builder.sectors: for sector in builder.sectors:
for door in sector.outstanding_doors: for door in sector.outstanding_doors:
doors_to_connect[door.name] = door, idx doors_to_connect[door.name] = door, idx
idx += 1 idx += 1
all_regions.update(sector.regions) all_regions.update(sector.regions)
bk_special |= check_for_special(sector.regions)
finished = False finished = False
# flag if standard and this is hyrule castle # flag if standard and this is hyrule castle
paths = determine_paths_for_dungeon(world, player, all_regions, name) paths = determine_paths_for_dungeon(world, player, all_regions, name)
@@ -95,9 +97,9 @@ def generate_dungeon_find_proposal(builder, entrance_region_names, split_dungeon
if hash_code not in hash_code_set: if hash_code not in hash_code_set:
hash_code_set.add(hash_code) hash_code_set.add(hash_code)
explored_state = explore_proposal(name, entrance_regions, all_regions, proposed_map, doors_to_connect, explored_state = explore_proposal(name, entrance_regions, all_regions, proposed_map, doors_to_connect,
world, player) bk_special, world, player)
if check_valid(name, explored_state, proposed_map, doors_to_connect, all_regions, if check_valid(name, explored_state, proposed_map, doors_to_connect, all_regions,
paths, entrance_regions, world, player): paths, entrance_regions, bk_special, world, player):
finished = True finished = True
else: else:
proposed_map, hash_code = modify_proposal(proposed_map, explored_state, doors_to_connect, proposed_map, hash_code = modify_proposal(proposed_map, explored_state, doors_to_connect,
@@ -128,14 +130,14 @@ def create_random_proposal(doors_to_connect, world, player):
next_hook = random.choice(hooks_left) next_hook = random.choice(hooks_left)
primary_door = random.choice(primary_bucket[next_hook]) primary_door = random.choice(primary_bucket[next_hook])
opp_hook, secondary_door = type_map[next_hook], None opp_hook, secondary_door = type_map[next_hook], None
while (secondary_door is None or secondary_door == primary_door while (secondary_door is None or (secondary_door == primary_door and not world.door_self_loops[player])
or decouple_check(primary_bucket[next_hook], secondary_bucket[opp_hook], or decouple_check(primary_bucket[next_hook], secondary_bucket[opp_hook],
primary_door, secondary_door, world, player)): primary_door, secondary_door, world, player)):
secondary_door = random.choice(secondary_bucket[opp_hook]) secondary_door = random.choice(secondary_bucket[opp_hook])
proposal[primary_door] = secondary_door proposal[primary_door] = secondary_door
primary_bucket[next_hook].remove(primary_door) primary_bucket[next_hook].remove(primary_door)
secondary_bucket[opp_hook].remove(secondary_door) secondary_bucket[opp_hook].remove(secondary_door)
if not world.decoupledoors[player]: if primary_door != secondary_door and not world.decoupledoors[player]:
proposal[secondary_door] = primary_door proposal[secondary_door] = primary_door
primary_bucket[opp_hook].remove(secondary_door) primary_bucket[opp_hook].remove(secondary_door)
secondary_bucket[next_hook].remove(primary_door) secondary_bucket[next_hook].remove(primary_door)
@@ -200,11 +202,19 @@ def modify_proposal(proposed_map, explored_state, doors_to_connect, hash_code_se
unvisted_bucket[opp_hook].sort(key=lambda d: d.name) unvisted_bucket[opp_hook].sort(key=lambda d: d.name)
new_door = random.choice(unvisted_bucket[opp_hook]) new_door = random.choice(unvisted_bucket[opp_hook])
old_target = proposed_map[attempt] old_target = proposed_map[attempt]
proposed_map[attempt] = new_door
if not world.decoupledoors[player]: if not world.decoupledoors[player]:
old_attempt = proposed_map[new_door] old_attempt = proposed_map[new_door]
else: else:
old_attempt = next(x for x in proposed_map if proposed_map[x] == new_door) old_attempt = next(x for x in proposed_map if proposed_map[x] == new_door)
# ensure nothing gets messed up when something loops with itself
if attempt == old_target and old_attempt == new_door:
old_attempt = new_door
old_target = attempt
elif attempt == old_target:
old_target = old_attempt
elif old_attempt == new_door:
old_attempt = old_target
proposed_map[attempt] = new_door
proposed_map[old_attempt] = old_target proposed_map[old_attempt] = old_target
if not world.decoupledoors[player]: if not world.decoupledoors[player]:
proposed_map[old_target] = old_attempt proposed_map[old_target] = old_attempt
@@ -221,21 +231,24 @@ def modify_proposal(proposed_map, explored_state, doors_to_connect, hash_code_se
return proposed_map, hash_code return proposed_map, hash_code
def explore_proposal(name, entrance_regions, all_regions, proposed_map, valid_doors, world, player): def explore_proposal(name, entrance_regions, all_regions, proposed_map, valid_doors, bk_special, world, player):
start = ExplorationState(dungeon=name) start = ExplorationState(dungeon=name)
bk_relevant = (world.door_type_mode[player] == 'original' and not world.bigkeyshuffle[player]) or bk_special
start.big_key_special = bk_special
original_state = extend_reachable_state_lenient(entrance_regions, start, proposed_map, original_state = extend_reachable_state_lenient(entrance_regions, start, proposed_map,
all_regions, valid_doors, world, player) all_regions, valid_doors, bk_relevant, world, player)
return original_state return original_state
def check_valid(name, exploration_state, proposed_map, doors_to_connect, all_regions, def check_valid(name, exploration_state, proposed_map, doors_to_connect, all_regions,
paths, entrance_regions, world, player): paths, entrance_regions, bk_special, world, player):
all_visited = set() all_visited = set()
all_visited.update(exploration_state.visited_blue) all_visited.update(exploration_state.visited_blue)
all_visited.update(exploration_state.visited_orange) all_visited.update(exploration_state.visited_orange)
if len(all_regions.difference(all_visited)) > 0: if len(all_regions.difference(all_visited)) > 0:
return False return False
if not valid_paths(name, paths, entrance_regions, doors_to_connect, all_regions, proposed_map, world, player): if not valid_paths(name, paths, entrance_regions, doors_to_connect, all_regions, proposed_map,
bk_special, world, player):
return False return False
return True return True
@@ -258,7 +271,7 @@ def check_for_special(regions):
return False return False
def valid_paths(name, paths, entrance_regions, valid_doors, all_regions, proposed_map, world, player): def valid_paths(name, paths, entrance_regions, valid_doors, all_regions, proposed_map, bk_special, world, player):
for path in paths: for path in paths:
if type(path) is tuple: if type(path) is tuple:
target = path[1] target = path[1]
@@ -270,12 +283,13 @@ def valid_paths(name, paths, entrance_regions, valid_doors, all_regions, propose
else: else:
target = path target = path
start_regions = entrance_regions start_regions = entrance_regions
if not valid_path(name, start_regions, target, valid_doors, proposed_map, all_regions, world, player): if not valid_path(name, start_regions, target, valid_doors, proposed_map, all_regions,
bk_special, world, player):
return False return False
return True return True
def valid_path(name, starting_regions, target, valid_doors, proposed_map, all_regions, world, player): def valid_path(name, starting_regions, target, valid_doors, proposed_map, all_regions, bk_special, world, player):
target_regions = set() target_regions = set()
if type(target) is not list: if type(target) is not list:
for region in all_regions: for region in all_regions:
@@ -288,8 +302,10 @@ def valid_path(name, starting_regions, target, valid_doors, proposed_map, all_re
target_regions.add(region) target_regions.add(region)
start = ExplorationState(dungeon=name) start = ExplorationState(dungeon=name)
bk_relevant = (world.door_type_mode[player] == 'original' and not world.bigkeyshuffle[player]) or bk_special
start.big_key_special = bk_special
original_state = extend_reachable_state_lenient(starting_regions, start, proposed_map, all_regions, original_state = extend_reachable_state_lenient(starting_regions, start, proposed_map, all_regions,
valid_doors, world, player) valid_doors, bk_relevant, world, player)
for exp_door in original_state.unattached_doors: for exp_door in original_state.unattached_doors:
if not exp_door.door.blocked or exp_door.door.trapFlag != 0: if not exp_door.door.blocked or exp_door.door.trapFlag != 0:
@@ -523,7 +539,7 @@ class ExplorationState(object):
self.crystal = exp_door.crystal self.crystal = exp_door.crystal
return exp_door return exp_door
def visit_region(self, region, key_region=None, key_checks=False, bk_flag=False): def visit_region(self, region, key_region=None, key_checks=False, bk_relevant=False):
if region.type != RegionType.Dungeon: if region.type != RegionType.Dungeon:
self.crystal = CrystalBarrier.Orange self.crystal = CrystalBarrier.Orange
if self.crystal == CrystalBarrier.Either: if self.crystal == CrystalBarrier.Either:
@@ -544,8 +560,14 @@ class ExplorationState(object):
self.ttl_locations += 1 self.ttl_locations += 1
if location not in self.found_locations: if location not in self.found_locations:
self.found_locations.append(location) self.found_locations.append(location)
if not bk_flag: if bk_relevant:
self.bk_found.add(location) if self.big_key_special:
if special_big_key_found(self):
self.bk_found.add(location)
self.re_add_big_key_doors()
else:
self.bk_found.add(location)
self.re_add_big_key_doors()
if location.name in dungeon_events and location.name not in self.events: if location.name in dungeon_events and location.name not in self.events:
if self.flooded_key_check(location): if self.flooded_key_check(location):
self.perform_event(location.name, key_region) self.perform_event(location.name, key_region)
@@ -566,6 +588,14 @@ class ExplorationState(object):
return True return True
return False return False
def re_add_big_key_doors(self):
self.big_key_opened = True
queue = collections.deque(self.big_doors)
while len(queue) > 0:
exp_door = queue.popleft()
self.avail_doors.append(exp_door)
self.big_doors.remove(exp_door)
def perform_event(self, location_name, key_region): def perform_event(self, location_name, key_region):
self.events.add(location_name) self.events.add(location_name)
queue = collections.deque(self.event_doors) queue = collections.deque(self.event_doors)
@@ -632,7 +662,7 @@ class ExplorationState(object):
self.append_door_to_list(door, self.avail_doors, flag) self.append_door_to_list(door, self.avail_doors, flag)
# same as above but traps are ignored, and flag is not used # same as above but traps are ignored, and flag is not used
def add_all_doors_check_proposed_2(self, region, proposed_map, valid_doors, world, player): def add_all_doors_check_proposed_2(self, region, proposed_map, valid_doors, bk_relevant, world, player):
for door in get_doors(world, region, player): for door in get_doors(world, region, player):
if door in proposed_map and door.name in valid_doors: if door in proposed_map and door.name in valid_doors:
self.visited_doors.add(door) self.visited_doors.add(door)
@@ -646,14 +676,18 @@ class ExplorationState(object):
other = self.find_door_in_list(door, self.unattached_doors) other = self.find_door_in_list(door, self.unattached_doors)
if self.crystal != other.crystal: if self.crystal != other.crystal:
other.crystal = CrystalBarrier.Either 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, elif (door.req_event is not None and door.req_event not in self.events
self.event_doors): and not self.in_door_list(door, self.event_doors)):
self.append_door_to_list(door, self.event_doors) self.append_door_to_list(door, self.event_doors)
elif (bk_relevant and (door.bigKey or door.name in get_special_big_key_doors(world, player))
and not self.big_key_opened):
if not self.in_door_list(door, self.big_doors):
self.append_door_to_list(door, self.big_doors)
elif not self.in_door_list(door, self.avail_doors): elif not self.in_door_list(door, self.avail_doors):
self.append_door_to_list(door, self.avail_doors) self.append_door_to_list(door, self.avail_doors)
# same as above but traps are checked for # same as above but traps are checked for
def add_all_doors_check_proposed_3(self, region, proposed_map, valid_doors, world, player): def add_all_doors_check_proposed_3(self, region, proposed_map, valid_doors, bk_relevant, world, player):
for door in get_doors(world, region, player): for door in get_doors(world, region, player):
if door in proposed_map and door.name in valid_doors: if door in proposed_map and door.name in valid_doors:
self.visited_doors.add(door) self.visited_doors.add(door)
@@ -667,9 +701,13 @@ class ExplorationState(object):
other = self.find_door_in_list(door, self.unattached_doors) other = self.find_door_in_list(door, self.unattached_doors)
if self.crystal != other.crystal: if self.crystal != other.crystal:
other.crystal = CrystalBarrier.Either 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, elif (door.req_event is not None and door.req_event not in self.events
self.event_doors): and not self.in_door_list(door, self.event_doors)):
self.append_door_to_list(door, self.event_doors) self.append_door_to_list(door, self.event_doors)
elif (bk_relevant and (door.bigKey or door.name in get_special_big_key_doors(world, player))
and not self.big_key_opened):
if not self.in_door_list(door, self.big_doors):
self.append_door_to_list(door, self.big_doors)
elif not self.in_door_list(door, self.avail_doors): elif not self.in_door_list(door, self.avail_doors):
self.append_door_to_list(door, self.avail_doors) self.append_door_to_list(door, self.avail_doors)
@@ -855,16 +893,22 @@ def extend_reachable_state_improved(search_regions, state, proposed_map, all_reg
return local_state return local_state
def extend_reachable_state_lenient(search_regions, state, proposed_map, all_regions, valid_doors, world, player): # bk_relevant means the big key doors need to be checks
def extend_reachable_state_lenient(search_regions, state, proposed_map, all_regions, valid_doors, bk_relevant,
world, player):
local_state = state.copy() local_state = state.copy()
for region in search_regions: for region in search_regions:
local_state.visit_region(region) local_state.visit_region(region, bk_relevant=bk_relevant)
if world.trap_door_mode[player] == 'vanilla': if world.trap_door_mode[player] == 'vanilla':
local_state.add_all_doors_check_proposed_3(region, proposed_map, valid_doors, world, player) local_state.add_all_doors_check_proposed_3(region, proposed_map, valid_doors, bk_relevant, world, player)
else: else:
local_state.add_all_doors_check_proposed_2(region, proposed_map, valid_doors, world, player) local_state.add_all_doors_check_proposed_2(region, proposed_map, valid_doors, bk_relevant, world, player)
while len(local_state.avail_doors) > 0: while len(local_state.avail_doors) > 0:
explorable_door = local_state.next_avail_door() explorable_door = local_state.next_avail_door()
if explorable_door.door.bigKey:
if bk_relevant and (not special_big_key_found(local_state) if local_state.big_key_special
else local_state.count_locations_exclude_specials(world, player) == 0):
continue
if explorable_door.door in proposed_map: if explorable_door.door in proposed_map:
connect_region = world.get_entrance(proposed_map[explorable_door.door].name, player).parent_region connect_region = world.get_entrance(proposed_map[explorable_door.door].name, player).parent_region
else: else:
@@ -872,11 +916,13 @@ def extend_reachable_state_lenient(search_regions, state, proposed_map, all_regi
if connect_region is not None: if connect_region is not None:
if (valid_region_to_explore_in_regions(connect_region, all_regions, world, player) if (valid_region_to_explore_in_regions(connect_region, all_regions, world, player)
and not local_state.visited(connect_region)): and not local_state.visited(connect_region)):
local_state.visit_region(connect_region) local_state.visit_region(connect_region, bk_relevant=bk_relevant)
if world.trap_door_mode[player] == 'vanilla': if world.trap_door_mode[player] == 'vanilla':
local_state.add_all_doors_check_proposed_3(connect_region, proposed_map, valid_doors, world, player) local_state.add_all_doors_check_proposed_3(connect_region, proposed_map, valid_doors,
bk_relevant, world, player)
else: else:
local_state.add_all_doors_check_proposed_2(connect_region, proposed_map, valid_doors, world, player) local_state.add_all_doors_check_proposed_2(connect_region, proposed_map, valid_doors,
bk_relevant, world, player)
return local_state return local_state

View File

@@ -357,7 +357,7 @@ def determine_major_items(world, player):
major_item_set.add('Single Arrow') major_item_set.add('Single Arrow')
if world.keyshuffle[player] == 'universal': if world.keyshuffle[player] == 'universal':
major_item_set.add('Small Key (Universal)') major_item_set.add('Small Key (Universal)')
if world.goal == 'triforcehunt': if world.goal[player] in {'triforcehunt', 'ganonhunt'}:
major_item_set.add('Triforce Piece') major_item_set.add('Triforce Piece')
if world.bombbag[player]: if world.bombbag[player]:
major_item_set.add('Bomb Upgrade (+10)') major_item_set.add('Bomb Upgrade (+10)')

View File

@@ -66,6 +66,10 @@ def link_entrances_new(world, player):
default_map['Old Man Cave (East)'] = 'Death Mountain Return Cave Exit (West)' default_map['Old Man Cave (East)'] = 'Death Mountain Return Cave Exit (West)'
one_way_map['Bumper Cave (Top)'] = 'Dark Death Mountain Healer Fairy' one_way_map['Bumper Cave (Top)'] = 'Dark Death Mountain Healer Fairy'
del default_map['Bumper Cave (Top)'] del default_map['Bumper Cave (Top)']
del one_way_map['Big Bomb Shop']
one_way_map['Links House'] = 'Big Bomb Shop'
del default_map['Links House']
default_map['Big Bomb Shop'] = 'Links House Exit'
avail_pool.default_map = default_map avail_pool.default_map = default_map
avail_pool.one_way_map = one_way_map avail_pool.one_way_map = one_way_map

View File

@@ -87,6 +87,7 @@ def roll_settings(weights):
ret.trap_door_mode = get_choice('trap_door_mode') ret.trap_door_mode = get_choice('trap_door_mode')
ret.key_logic_algorithm = get_choice('key_logic_algorithm') ret.key_logic_algorithm = get_choice('key_logic_algorithm')
ret.decoupledoors = get_choice('decoupledoors') ret.decoupledoors = get_choice('decoupledoors')
ret.door_self_loops = get_choice('door_self_loops')
ret.experimental = get_choice('experimental') ret.experimental = get_choice('experimental')
ret.collection_rate = get_choice('collection_rate') ret.collection_rate = get_choice('collection_rate')

View File

@@ -0,0 +1,14 @@
meta:
players: 1
settings:
1:
door_shuffle: crossed
intensity: 3
mode: standard
pottery: keys
dropshuffle: 'on'
doors:
1:
doors:
Hyrule Dungeon Cellblock Up Stairs:
dest: Ice Hammer Block Down Stairs

View File

@@ -0,0 +1,125 @@
meta:
players: 1
settings:
1:
door_shuffle: basic
intensity: 3
door_type_mode: all
doors:
1:
doors:
PoD Mimics 2 SW:
type: Trap Door
# TR Twin Pokeys NW: # not possible due to trap flags
# type: Trap Door
Thieves Blocked Entry SW:
type: Trap Door
Hyrule Dungeon Armory Interior Key Door N:
type: Trap Door
Desert Compass Key Door WN:
type: Trap Door
TR Tile Room SE:
type: Trap Door
# Mire Cross SW: # not possible due to trap flags
# type: Trap Door
Tower Circle of Pots ES:
type: Trap Door
Eastern Single Eyegore ES:
type: Trap Door
Eastern Duo Eyegores SE:
type: Trap Door
Swamp Push Statue S:
type: Trap Door
# Skull 2 East Lobby WS: # currently not possible due to trap flags
# type: Trap Door
GT Hope Room WN :
type: Trap Door
# Eastern Courtyard Ledge S: # currently not possible due to trap flags
# type: Trap Door
Ice Switch Room ES :
type: Trap Door
Ice Switch Room NE :
type: Trap Door
Skull Torch Room WS :
type: Trap Door
GT Speed Torch NE :
type: Trap Door
GT Speed Torch WS :
type: Trap Door
GT Torch Cross WN :
type: Trap Door
Mire Tile Room SW :
type: Trap Door
Mire Tile Room ES :
type: Trap Door
TR Torches WN :
type: Trap Door
PoD Lobby N:
type: Trap Door
PoD Middle Cage S:
type: Trap Door
Ice Bomb Jump NW:
type: Trap Door
GT Hidden Spikes SE:
type: Trap Door
Ice Tall Hint EN:
type: Trap Door
GT Conveyor Cross EN:
type: Trap Door
Eastern Pot Switch WN:
type: Trap Door
Thieves Conveyor Maze WN:
type: Trap Door
# Thieves Conveyor Maze SW: #not possible due to 4 door limit
# type: Trap Door
Eastern Dark Square Key Door WN:
type: Trap Door
Eastern Lobby NW:
type: Trap Door
Eastern Lobby NE:
type: Trap Door
# Ice Cross Bottom SE: # not possible due to trap flags
# type: Trap Door
Desert Back Lobby S:
type: Trap Door
# Desert West S: need enough lobbies for basic, should otherwise work
# type: Trap Door
Desert West Lobby ES:
type: Trap Door
# Mire Hidden Shooters SE: # not possible due to trap flags
# type: Trap Door
# Mire Hidden Shooters ES: # not possible due to trap flags
# type: Trap Door
Mire Hidden Shooters WS:
type: Trap Door
Tower Dark Pits EN:
type: Trap Door
Tower Dark Maze ES:
type: Trap Door
TR Tongue Pull WS:
type: Trap Door
# Lower layer: not valid
# Sewers Pull Switch N:
# type: Trap Door
# PoD Sexy Statue W: # not possible due to trap flags and low layer too, so likely not an exception
# type: Trap Door
# Not valid due to disappearing somaria block
# Mire Dark Shooters SE:
# type: Trap Door
# These triggers don't open doors
# Ice Compass Room NE:
# type: Trap Door
# Hera Torches NE:
# type: Trap Door
# Mire Spikes WS:
# type: Trap Door
# Mire Spikes SW:
# type: Trap Door
# Mire Spikes NW:
# type: Trap Door
# Tower Room 03 WN:
# type: Trap Door