diff --git a/BaseClasses.py b/BaseClasses.py index a62ca101..6d7430f4 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -146,6 +146,7 @@ class World(object): set_player_attr('colorizepots', True) set_player_attr('pot_pool', {}) set_player_attr('decoupledoors', False) + set_player_attr('door_self_loops', False) set_player_attr('door_type_mode', 'original') set_player_attr('trap_door_mode', 'optional') set_player_attr('key_logic_algorithm', 'default') @@ -2474,6 +2475,7 @@ class Spoiler(object): 'trap_door_mode': self.world.trap_door_mode, 'key_logic': self.world.key_logic_algorithm, 'decoupledoors': self.world.decoupledoors, + 'door_self_loops': self.world.door_self_loops, 'dungeon_counters': self.world.dungeon_counters, 'item_pool': self.world.difficulty, 'item_functionality': self.world.difficulty_adjustments, @@ -2688,6 +2690,7 @@ class Spoiler(object): outfile.write(f"Trap Door Mode: {self.metadata['trap_door_mode'][player]}\n") outfile.write(f"Key Logic Algorithm: {self.metadata['key_logic'][player]}\n") outfile.write(f"Decouple Doors: {yn(self.metadata['decoupledoors'][player])}\n") + outfile.write(f"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"Dungeon Counters: {self.metadata['dungeon_counters'][player]}\n") outfile.write(f"Drop Shuffle: {yn(self.metadata['dropshuffle'][player])}\n") @@ -2953,10 +2956,10 @@ mixed_travel_mode = {"prevent": 0, "allow": 1, "force": 2} pottery_mode = {'none': 0, 'keys': 2, 'lottery': 3, 'dungeon': 4, 'cave': 5, 'cavekeys': 6, 'reduced': 7, '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} -# byte 6: CCCC CPAA (crystals ganon, pyramid, access +# byte 6: ?CCC CPAA (crystals ganon, pyramid, access access_mode = {"items": 0, "locations": 1, "none": 2} # byte 7: B?MC DDEE (big, ?, maps, compass, door_type, enemies) @@ -3014,7 +3017,8 @@ class Settings(object): (0x80 if w.shuffletavern[p] else 0) | (0x10 if w.dropshuffle[p] else 0) | (pottery_mode[w.pottery[p]]), - ((8 if w.crystals_gt_orig[p] == "random" else int(w.crystals_gt_orig[p])) << 3) + (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), ((8 if w.crystals_ganon_orig[p] == "random" else int(w.crystals_ganon_orig[p])) << 3) @@ -3034,8 +3038,8 @@ class Settings(object): (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]]), - ((0x80 if w.pseudoboots[p] else 0) | overworld_map_mode[w.overworld_map[p]] << 6 - | trap_door_mode[w.trap_door_mode[p]] << 4 | key_logic_algo[w.key_logic_algorithm[p]]), + ((0x80 if w.pseudoboots[p] else 0) | overworld_map_mode[w.overworld_map[p]] << 5 + | trap_door_mode[w.trap_door_mode[p]] << 3 | key_logic_algo[w.key_logic_algorithm[p]]), ]) return base64.b64encode(code, "+-".encode()).decode() @@ -3073,12 +3077,13 @@ class Settings(object): args.dropshuffle[p] = True if settings[4] & 0x10 else False 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] - cgt = (settings[5] & 0xf8) >> 3 + cgt = (settings[5] & 0x78) >> 3 args.crystals_gt[p] = "random" if cgt == 8 else cgt 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.openpyramid[p] = True if settings[6] & 0x4 else False @@ -3105,8 +3110,8 @@ class Settings(object): args.keyshuffle[p] = r(keyshuffle_mode)[settings[11] & 0x3] if len(settings) > 12: args.pseudoboots[p] = True if settings[12] & 0x80 else False - args.overworld_map[p] = r(overworld_map_mode)[(settings[12] & 0x60) >> 6] - args.trap_door_mode[p] = r(trap_door_mode)[(settings[12] & 0x14) >> 4] + args.overworld_map[p] = r(overworld_map_mode)[(settings[12] & 0x60) >> 5] + 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] diff --git a/CLI.py b/CLI.py index 8f790ed8..71023217 100644 --- a/CLI.py +++ b/CLI.py @@ -141,7 +141,7 @@ def parse_cli(argv, no_defaults=False): 'heartbeep', 'remote_items', 'shopsanity', 'dropshuffle', 'pottery', 'keydropshuffle', 'mixed_travel', 'standardize_palettes', 'code', 'reduce_flashing', 'shuffle_sfx', 'msu_resume', 'collection_rate', 'colorizepots', 'decoupledoors', 'door_type_mode', - 'trap_door_mode', 'key_logic_algorithm']: + 'trap_door_mode', 'key_logic_algorithm', 'door_self_loops']: value = getattr(defaults, name) if getattr(playerargs, name) is None else getattr(playerargs, name) if player == 1: setattr(ret, name, {1: value}) @@ -218,6 +218,7 @@ def parse_settings(): 'trap_door_mode': 'optional', 'key_logic_algorithm': 'default', 'decoupledoors': False, + 'door_self_loops': False, 'experimental': False, 'dungeon_counters': 'default', 'mixed_travel': 'prevent', diff --git a/Main.py b/Main.py index be6266e2..259d7a4b 100644 --- a/Main.py +++ b/Main.py @@ -115,6 +115,7 @@ def main(args, seed=None, fish=None): world.trap_door_mode = args.trap_door_mode.copy() world.key_logic_algorithm = args.key_logic_algorithm.copy() world.decoupledoors = args.decoupledoors.copy() + world.door_self_loops = args.door_self_loops.copy() world.experimental = args.experimental.copy() world.dungeon_counters = args.dungeon_counters.copy() world.fish = fish @@ -487,6 +488,7 @@ def copy_world(world): ret.beemizer = world.beemizer.copy() ret.intensity = world.intensity.copy() ret.decoupledoors = world.decoupledoors.copy() + ret.door_self_loops = world.door_self_loops.copy() ret.experimental = world.experimental.copy() ret.shopsanity = world.shopsanity.copy() ret.dropshuffle = world.dropshuffle.copy() diff --git a/README.md b/README.md index df21ad17..24e79807 100644 --- a/README.md +++ b/README.md @@ -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. -CLI `--decoupledoors` +CLI: `--decoupledoors` + +### Self-Looping Spiral Stairs + +If enabled, spiral stairs are allowed to lead to themselves. + +CLI: `--door_self_loops` ### Pottery diff --git a/mystery_example.yml b/mystery_example.yml index 92abd726..bd8cbbe2 100644 --- a/mystery_example.yml +++ b/mystery_example.yml @@ -31,6 +31,9 @@ partial: 0 strict: 0 decoupledoors: off + door_self_loops: + on: 1 + off: 1 dropshuffle: on: 1 off: 1 diff --git a/mystery_testsuite.yml b/mystery_testsuite.yml index d6e46832..c7250d2b 100644 --- a/mystery_testsuite.yml +++ b/mystery_testsuite.yml @@ -31,6 +31,9 @@ key_logic_algorithm: decoupledoors: off: 9 # more strict on: 1 +door_self_loops: + on: 1 + off: 1 dropshuffle: on: 1 off: 1 diff --git a/resources/app/cli/args.json b/resources/app/cli/args.json index bdc6b4db..0d6db740 100644 --- a/resources/app/cli/args.json +++ b/resources/app/cli/args.json @@ -212,6 +212,10 @@ "action": "store_true", "type": "bool" }, + "door_self_loops": { + "action": "store_true", + "type": "bool" + }, "experimental": { "action": "store_true", "type": "bool" diff --git a/resources/app/cli/lang/en.json b/resources/app/cli/lang/en.json index 49bd891a..10790d99 100644 --- a/resources/app/cli/lang/en.json +++ b/resources/app/cli/lang/en.json @@ -254,6 +254,7 @@ "strict: Ensure small keys are available" ], "decoupledoors" : [ "Door entrances and exits are decoupled" ], + "door_self_loops" : [ "Spiral stairs are allowed to self-loop" ], "experimental": [ "Enable experimental features. (default: %(default)s)" ], "dungeon_counters": [ "Enable dungeon chest counters. (default: %(default)s)" ], "crystals_ganon": [ diff --git a/resources/app/gui/lang/en.json b/resources/app/gui/lang/en.json index fb81c391..6a30113d 100644 --- a/resources/app/gui/lang/en.json +++ b/resources/app/gui/lang/en.json @@ -58,6 +58,7 @@ "randomizer.dungeon.smallkeyshuffle.universal": "Universal", "randomizer.dungeon.bigkeyshuffle": "Big Keys", "randomizer.dungeon.decoupledoors": "Decouple Doors", + "randomizer.dungeon.door_self_loops": "Allow Self-Looping Spiral Stairs", "randomizer.dungeon.dungeondoorshuffle": "Dungeon Door Shuffle", "randomizer.dungeon.dungeondoorshuffle.vanilla": "Vanilla", diff --git a/resources/app/gui/randomize/dungeon/widgets.json b/resources/app/gui/randomize/dungeon/widgets.json index bcf7232a..4be6a385 100644 --- a/resources/app/gui/randomize/dungeon/widgets.json +++ b/resources/app/gui/randomize/dungeon/widgets.json @@ -66,6 +66,12 @@ } }, "decoupledoors": { + "type": "checkbox", + "config": { + "padx": [20,0] + } + }, + "door_self_loops": { "type": "checkbox", "config": { "padx": [20,0], diff --git a/source/classes/CustomSettings.py b/source/classes/CustomSettings.py index c0e82eed..07bbbbf7 100644 --- a/source/classes/CustomSettings.py +++ b/source/classes/CustomSettings.py @@ -114,6 +114,7 @@ class CustomSettings(object): args.trap_door_mode[p] = get_setting(settings['trap_door_mode'], args.trap_door_mode[p]) args.key_logic_algorithm[p] = get_setting(settings['key_logic_algorithm'], args.key_logic_algorithm[p]) args.decoupledoors[p] = get_setting(settings['decoupledoors'], args.decoupledoors[p]) + args.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.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]) @@ -234,6 +235,7 @@ class CustomSettings(object): settings_dict[p]['trap_door_mode'] = world.trap_door_mode[p] settings_dict[p]['key_logic_algorithm'] = world.key_logic_algorithm[p] settings_dict[p]['decoupledoors'] = world.decoupledoors[p] + settings_dict[p]['door_self_loops'] = world.door_self_loops[p] settings_dict[p]['logic'] = world.logic[p] settings_dict[p]['mode'] = world.mode[p] settings_dict[p]['swords'] = world.swords[p] diff --git a/source/classes/constants.py b/source/classes/constants.py index 812042b8..1ca3c08e 100644 --- a/source/classes/constants.py +++ b/source/classes/constants.py @@ -106,6 +106,7 @@ SETTINGSTOPROCESS = { "door_type_mode": "door_type_mode", "trap_door_mode": "trap_door_mode", "decoupledoors": "decoupledoors", + "door_self_loops": "door_self_loops", "experimental": "experimental", "dungeon_counters": "dungeon_counters", "mixed_travel": "mixed_travel", diff --git a/source/dungeon/DungeonStitcher.py b/source/dungeon/DungeonStitcher.py index 1eac3fb1..504fc03b 100644 --- a/source/dungeon/DungeonStitcher.py +++ b/source/dungeon/DungeonStitcher.py @@ -22,7 +22,7 @@ def generate_dungeon(builder, entrance_region_names, split_dungeon, world, playe queue = collections.deque(proposed_map.items()) while len(queue) > 0: a, b = queue.popleft() - if world.decoupledoors[player]: + if a == b or world.decoupledoors[player]: connect_doors_one_way(a, b) else: connect_doors(a, b) @@ -128,14 +128,14 @@ def create_random_proposal(doors_to_connect, world, player): next_hook = random.choice(hooks_left) primary_door = random.choice(primary_bucket[next_hook]) opp_hook, secondary_door = type_map[next_hook], None - while (secondary_door is None or secondary_door == primary_door + 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], primary_door, secondary_door, world, player)): secondary_door = random.choice(secondary_bucket[opp_hook]) proposal[primary_door] = secondary_door primary_bucket[next_hook].remove(primary_door) secondary_bucket[opp_hook].remove(secondary_door) - if not world.decoupledoors[player]: + if primary_door != secondary_door and not world.decoupledoors[player]: proposal[secondary_door] = primary_door primary_bucket[opp_hook].remove(secondary_door) secondary_bucket[next_hook].remove(primary_door) @@ -200,11 +200,19 @@ def modify_proposal(proposed_map, explored_state, doors_to_connect, hash_code_se unvisted_bucket[opp_hook].sort(key=lambda d: d.name) new_door = random.choice(unvisted_bucket[opp_hook]) old_target = proposed_map[attempt] - proposed_map[attempt] = new_door if not world.decoupledoors[player]: old_attempt = proposed_map[new_door] else: old_attempt = next(x for x in proposed_map if proposed_map[x] == new_door) + # 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 if not world.decoupledoors[player]: proposed_map[old_target] = old_attempt diff --git a/source/tools/MysteryUtils.py b/source/tools/MysteryUtils.py index 0405efff..5000f670 100644 --- a/source/tools/MysteryUtils.py +++ b/source/tools/MysteryUtils.py @@ -87,6 +87,7 @@ def roll_settings(weights): ret.trap_door_mode = get_choice('trap_door_mode') ret.key_logic_algorithm = get_choice('key_logic_algorithm') ret.decoupledoors = get_choice('decoupledoors') == 'on' + ret.door_self_loops = get_choice('door_self_loops') == 'on' ret.experimental = get_choice('experimental') == 'on' ret.collection_rate = get_choice('collection_rate') == 'on'