From 46903470b5f9daefa058b9ab217dfe7848f36dd1 Mon Sep 17 00:00:00 2001 From: aerinon Date: Tue, 3 Jan 2023 16:14:42 -0700 Subject: [PATCH 01/15] Fix typo for door "Sewers Key Rat E" --- Doors.py | 2 +- Main.py | 2 +- RELEASENOTES.md | 2 ++ 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Doors.py b/Doors.py index a7c1d9f3..c137983f 100644 --- a/Doors.py +++ b/Doors.py @@ -104,7 +104,7 @@ def create_doors(world, player): create_door(player, 'Sewers Dark Cross Key Door N', Nrml).dir(No, 0x32, Mid, High).small_key().pos(0), create_door(player, 'Sewers Water S', Nrml).dir(So, 0x22, Mid, High).small_key().pos(0).portal(Z, 0x22), create_door(player, 'Sewers Water W', Nrml).dir(We, 0x22, Bot, High).pos(1), - create_door(player, 'Sewers Key Rat E', Nrml).dir(Ea, 0x21, Bot, High).pos(1), + create_door(player, 'Sewers Key Rat E', Nrml).dir(Ea, 0x21, Bot, High).pos(2), create_door(player, 'Sewers Key Rat Key Door N', Nrml).dir(No, 0x21, Right, High).small_key().pos(0), create_door(player, 'Sewers Secret Room Key Door S', Nrml).dir(So, 0x11, Right, High).small_key().pos(2).portal(X, 0x02), create_door(player, 'Sewers Rat Path WS', Intr).dir(We, 0x11, Bot, High).pos(1), diff --git a/Main.py b/Main.py index 506fe985..e1a86313 100644 --- a/Main.py +++ b/Main.py @@ -31,7 +31,7 @@ from Utils import output_path, parse_player_names from source.item.FillUtil import create_item_pool_config, massage_item_pool, district_item_pool_config from source.tools.BPS import create_bps_from_data -__version__ = '1.1.2-dev' +__version__ = '1.1.3-dev' from source.classes.BabelFish import BabelFish diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 831368e8..924fdaa5 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -181,6 +181,8 @@ Same as above but both small keys and bigs keys of the dungeon are not allowed o # Bug Fixes and Notes +* 1.1.3 + * Fixed a typo on a door near the key rat * 1.1.2 * Fixed a logic bug with GT Refill room not requiring boots to access the pots in there. * 1.1.1 From beb7f8074b8f9d7f1cb9656c3797eb242800d4c3 Mon Sep 17 00:00:00 2001 From: Thomas Prescott Date: Sat, 7 Jan 2023 14:58:24 -0600 Subject: [PATCH 02/15] remove hard requirement of tkinter when using CLI --- source/meta/check_requirements.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/source/meta/check_requirements.py b/source/meta/check_requirements.py index 680dfe8f..6976f707 100644 --- a/source/meta/check_requirements.py +++ b/source/meta/check_requirements.py @@ -1,6 +1,4 @@ import importlib.util -import webbrowser -from tkinter import Tk, Label, Button, Frame def check_requirements(console=False): @@ -26,6 +24,9 @@ def check_requirements(console=False): logger.error('See the step about "Installing Platform-specific dependencies":') logger.error('https://github.com/aerinon/ALttPDoorRandomizer/blob/DoorDev/docs/BUILDING.md') else: + import webbrowser + from tkinter import Tk, Label, Button, Frame + master = Tk() master.title('Error') frame = Frame(master) From 9bf73159045601238015de013235f6392db55e2b Mon Sep 17 00:00:00 2001 From: Thomas Prescott Date: Thu, 9 Feb 2023 13:08:36 -0600 Subject: [PATCH 03/15] Remove a bad triforce text It was brought up on the ALTTPR Discord that this Triforce text can feel really bad to see at the end of a long race. I think it's best we give it a permanent vacation. --- Text.py | 1 - 1 file changed, 1 deletion(-) diff --git a/Text.py b/Text.py index 4da86b4c..27d27f1d 100644 --- a/Text.py +++ b/Text.py @@ -91,7 +91,6 @@ Triforce_texts = [ 'Who stole the fourth triangle?', 'Trifource?\nMore Like Tritrice, am I right?' '\n Well Done!', - 'You just wasted 2 hours of your life.', 'This was meant to be a trapezoid', # these ones are from web randomizer "\n G G", From 142b82a2619a65b9f8ee8ee136f72f8b0e75cc18 Mon Sep 17 00:00:00 2001 From: aerinon Date: Fri, 10 Feb 2023 13:47:58 -0700 Subject: [PATCH 04/15] BK door fix --- DoorShuffle.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/DoorShuffle.py b/DoorShuffle.py index e4d4a7be..a4cf7397 100644 --- a/DoorShuffle.py +++ b/DoorShuffle.py @@ -2493,6 +2493,8 @@ def reassign_big_key_doors(bk_map, world, player): if d1.type is DoorType.Interior: change_door_to_big_key(d1, world, player) d2.bigKey = True # ensure flag is set + if d2.smallKey: + d2.smallKey = False else: world.paired_doors[player].append(PairedDoor(d1.name, d2.name)) change_door_to_big_key(d1, world, player) @@ -2521,6 +2523,8 @@ def reassign_big_key_doors(bk_map, world, player): def change_door_to_big_key(d, world, player): d.bigKey = True + if d.smallKey: + d.smallKey = False room = world.get_room(d.roomIndex, player) if room.doorList[d.doorListPos][1] != DoorKind.BigKey: verify_door_list_pos(d, room, world, player) From a304fd31acb0819a1eb1194b1a9497983ebfb40a Mon Sep 17 00:00:00 2001 From: aerinon Date: Tue, 14 Feb 2023 15:58:52 -0700 Subject: [PATCH 05/15] Standard keysanity distribution and generation fixes Includes new S&Q safety --- BaseClasses.py | 6 ++-- DungeonGenerator.py | 13 ++++++-- Fill.py | 36 +++++++++++++++++----- KeyDoorShuffle.py | 48 +++++++++++++++++++----------- Rom.py | 2 +- Rules.py | 2 +- data/base2current.bps | Bin 93487 -> 93523 bytes source/dungeon/DungeonStitcher.py | 12 +++++++- 8 files changed, 86 insertions(+), 33 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index ec04d679..c2da8e91 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -1423,10 +1423,10 @@ class Region(object): or (item.bigkey and not self.world.bigkeyshuffle[item.player]) or (item.map and not self.world.mapshuffle[item.player]) or (item.compass and not self.world.compassshuffle[item.player])) - sewer_hack = self.world.mode[item.player] == 'standard' and item.name == 'Small Key (Escape)' - if sewer_hack or inside_dungeon_item: + # not all small keys to escape must be in escape + # sewer_hack = self.world.mode[item.player] == 'standard' and item.name == 'Small Key (Escape)' + if inside_dungeon_item: return self.dungeon and self.dungeon.is_dungeon_item(item) and item.player == self.player - return True def __str__(self): diff --git a/DungeonGenerator.py b/DungeonGenerator.py index 2140520e..8231833b 100644 --- a/DungeonGenerator.py +++ b/DungeonGenerator.py @@ -772,6 +772,13 @@ def connect_simple_door(exit_door, region): special_big_key_doors = ['Hyrule Dungeon Cellblock Door', "Thieves Blind's Cell Door"] +std_special_big_key_doors = ['Hyrule Castle Throne Room Tapestry'] + special_big_key_doors + + +def get_special_big_key_doors(world, player): + if world.mode[player] == 'standard': + return std_special_big_key_doors + return special_big_key_doors class ExplorationState(object): @@ -1002,7 +1009,8 @@ class ExplorationState(object): if self.can_traverse(door): if door.controller: door = door.controller - if (door in big_key_door_proposal or door.name in special_big_key_doors) and not self.big_key_opened: + if (door in big_key_door_proposal + or door.name in get_special_big_key_doors(world, player)) and not self.big_key_opened: if not self.in_door_list(door, self.big_doors): self.append_door_to_list(door, self.big_doors) elif door.req_event is not None and door.req_event not in self.events: @@ -3554,7 +3562,8 @@ def check_for_valid_layout(builder, sector_list, builder_info): split_list['Sewers'].remove(temp_builder.throne_door.entrance.parent_region.name) builder.exception_list = list(sector_list) return True, {}, package - except (GenerationException, NeutralizingException, OtherGenException): + except (GenerationException, NeutralizingException, OtherGenException) as e: + logging.getLogger('').info(f'Bailing on this layout for', e) builder.split_dungeon_map = None builder.valid_proposal = None if temp_builder.name == 'Hyrule Castle' and temp_builder.throne_door: diff --git a/Fill.py b/Fill.py index 321e1a8e..239d8e8e 100644 --- a/Fill.py +++ b/Fill.py @@ -143,21 +143,24 @@ def verify_spot_to_fill(location, item_to_place, max_exp_state, single_player_pl key_pool, world): if item_to_place.smallkey or item_to_place.bigkey: # a better test to see if a key can go there location.item = item_to_place + location.event = True test_state = max_exp_state.copy() test_state.stale[item_to_place.player] = True else: test_state = max_exp_state if not single_player_placement or location.player == item_to_place.player: + test_state.sweep_for_events() if location.can_fill(test_state, item_to_place, perform_access_check): - if valid_key_placement(item_to_place, location, key_pool, world): + if valid_key_placement(item_to_place, location, key_pool, test_state, world): if item_to_place.crystal or valid_dungeon_placement(item_to_place, location, world): return location if item_to_place.smallkey or item_to_place.bigkey: location.item = None + location.event = False return None -def valid_key_placement(item, location, key_pool, world): +def valid_key_placement(item, location, key_pool, collection_state, world): if not valid_reserved_placement(item, location, world): return False if ((not item.smallkey and not item.bigkey) or item.player != location.player @@ -174,7 +177,15 @@ def valid_key_placement(item, location, key_pool, world): prize_loc = world.get_location(key_logic.prize_location, location.player) cr_count = world.crystals_needed_for_gt[location.player] wild_keys = world.keyshuffle[item.player] != 'none' - return key_logic.check_placement(unplaced_keys, wild_keys, location if item.bigkey else None, prize_loc, cr_count) + if wild_keys: + reached_keys = {x for x in collection_state.locations_checked + if x.item and x.item.name == key_logic.small_key_name and x.item.player == item.player} + else: + reached_keys = set() # will be calculated using key logic in a moment + self_locking_keys = sum(1 for d, rule in key_logic.door_rules.items() if rule.allow_small + and rule.small_location.item and rule.small_location.item.name == key_logic.small_key_name) + return key_logic.check_placement(unplaced_keys, wild_keys, reached_keys, self_locking_keys, + location if item.bigkey else None, prize_loc, cr_count) else: return not item.is_inside_dungeon_item(world) @@ -205,6 +216,7 @@ def track_outside_keys(item, location, world): if loc_dungeon and loc_dungeon.name == item_dungeon: return # this is an inside key world.key_logic[item.player][item_dungeon].outside_keys += 1 + world.key_logic[item.player][item_dungeon].outside_keys_locations.add(location) def track_dungeon_items(item, location, world): @@ -345,7 +357,9 @@ def find_spot_for_item(item_to_place, locations, world, base_state, pool, test_state = maximum_exploration_state if (not single_player_placement or location.player == item_to_place.player) \ and location.can_fill(test_state, item_to_place, perform_access_check) \ - and valid_key_placement(item_to_place, location, pool if (keys_in_itempool and keys_in_itempool[item_to_place.player]) else world.itempool, world): + and valid_key_placement(item_to_place, location, + pool if (keys_in_itempool and keys_in_itempool[item_to_place.player]) else world.itempool, + test_state, world): return location if item_to_place.smallkey or item_to_place.bigkey: location.item = old_item @@ -424,10 +438,16 @@ def distribute_items_restrictive(world, gftower_trash=False, fill_locations=None random.shuffle(fill_locations) fill_locations.reverse() - # Make sure the escape small key is placed first in standard with key shuffle to prevent running out of spots - # todo: crossed - progitempool.sort(key=lambda item: 1 if item.name == 'Small Key (Escape)' - and world.keyshuffle[item.player] != 'none' and world.mode[item.player] == 'standard' else 0) + # Make sure the escape keys ire placed first in standard to prevent running out of spots + def std_item_sort(item): + if world.mode[item.player] == 'standard': + if item.name == 'Small Key (Escape)': + return 1 + if item.name == 'Big Key (Escape)': + return 2 + return 0 + + progitempool.sort(key=std_item_sort) key_pool = [x for x in progitempool if x.smallkey] # sort maps and compasses to the back -- this may not be viable in equitable & ambrosia diff --git a/KeyDoorShuffle.py b/KeyDoorShuffle.py index 7b5e011f..2a39f38c 100644 --- a/KeyDoorShuffle.py +++ b/KeyDoorShuffle.py @@ -5,7 +5,7 @@ from collections import defaultdict, deque from BaseClasses import DoorType, dungeon_keys, KeyRuleType, RegionType from Regions import dungeon_events from Dungeons import dungeon_keys, dungeon_bigs, dungeon_table -from DungeonGenerator import ExplorationState, special_big_key_doors, count_locations_exclude_big_chest, prize_or_event +from DungeonGenerator import ExplorationState, get_special_big_key_doors, count_locations_exclude_big_chest, prize_or_event from DungeonGenerator import reserved_location, blind_boss_unavail @@ -59,13 +59,16 @@ class KeyLogic(object): self.placement_rules = [] self.location_rules = {} self.outside_keys = 0 + self.outside_keys_locations = set() self.dungeon = dungeon_name self.sm_doors = {} self.prize_location = None - def check_placement(self, unplaced_keys, wild_keys, big_key_loc=None, prize_loc=None, cr_count=7): + def check_placement(self, unplaced_keys, wild_keys, reached_keys, self_locking_keys, + big_key_loc=None, prize_loc=None, cr_count=7): for rule in self.placement_rules: - if not rule.is_satisfiable(self.outside_keys, wild_keys, unplaced_keys, big_key_loc, prize_loc, cr_count): + if not rule.is_satisfiable(self.outside_keys_locations, wild_keys, reached_keys, self_locking_keys, + unplaced_keys, big_key_loc, prize_loc, cr_count): return False if big_key_loc: for rule_a, rule_b in itertools.combinations(self.placement_rules, 2): @@ -159,7 +162,8 @@ class PlacementRule(object): left -= rule_needed return False - def is_satisfiable(self, outside_keys, wild_keys, unplaced_keys, big_key_loc, prize_location, cr_count): + def is_satisfiable(self, outside_keys_locations, wild_keys, reached_keys, self_locking_keys, unplaced_keys, + big_key_loc, prize_location, cr_count): if self.prize_relevance and prize_location: if self.prize_relevance == 'BigBomb': if prize_location.item.name not in ['Crystal 5', 'Crystal 6']: @@ -186,10 +190,11 @@ class PlacementRule(object): check_locations = self.check_locations_wo_bk if bk_blocked else self.check_locations_w_bk if not bk_blocked and check_locations is None: return True - available_keys = outside_keys + available_keys = len(outside_keys_locations) # todo: sometimes we need an extra empty chest to accomodate the big key too # dungeon bias seed 563518200 for example threshold = self.needed_keys_wo_bk if bk_blocked else self.needed_keys_w_bk + threshold -= self_locking_keys if not wild_keys: empty_chests = 0 for loc in check_locations: @@ -200,7 +205,8 @@ class PlacementRule(object): place_able_keys = min(empty_chests, unplaced_keys) available_keys += place_able_keys else: - available_keys += unplaced_keys + available_keys += len(reached_keys.difference(outside_keys_locations)) # already placed small keys + available_keys += unplaced_keys # small keys not yet placed return available_keys >= threshold @@ -1002,7 +1008,7 @@ def find_worst_counter_wo_bk(small_key_num, accessible_set, door, odd_ctr, key_c def open_a_door(door, child_state, flat_proposal, world, player): - if door.bigKey or door.name in special_big_key_doors: + if door.bigKey or door.name in get_special_big_key_doors(world, player): child_state.big_key_opened = True child_state.avail_doors.extend(child_state.big_doors) child_state.opened_doors.extend(set([d.door for d in child_state.big_doors])) @@ -1485,7 +1491,8 @@ def validate_key_layout_sub_loop(key_layout, state, checked_states, flat_proposa else: ttl_locations = count_locations_exclude_big_chest(state.found_locations, world, player) ttl_small_key_only = count_small_key_only_locations(state) - available_small_locations = cnt_avail_small_locations(ttl_locations, ttl_small_key_only, state, world, player) + available_small_locations = cnt_avail_small_locations(ttl_locations, ttl_small_key_only, state, + key_layout, world, player) available_big_locations = cnt_avail_big_locations(ttl_locations, state, world, player) if invalid_self_locking_key(key_layout, state, prev_state, prev_avail, world, player): return False @@ -1615,18 +1622,24 @@ def determine_prize_lock(key_layout, world, player): key_layout.prize_can_lock = True -def cnt_avail_small_locations(free_locations, key_only, state, world, player): - if world.keyshuffle[player] == 'none': +def cnt_avail_small_locations(free_locations, key_only, state, key_layout, world, player): + std_flag = world.mode[player] == 'standard' and key_layout.sector.name == 'Hyrule Castle' + if world.keyshuffle[player] == 'none' or std_flag: bk_adj = 1 if state.big_key_opened and not state.big_key_special else 0 - avail_chest_keys = min(free_locations - bk_adj, state.key_locations - key_only) + # this is the secret passage, could expand to Uncle/Links House with appropriate logic + std_adj = 1 if std_flag and world.keyshuffle[player] != 'none' else 0 + avail_chest_keys = min(free_locations + std_adj - bk_adj, state.key_locations - key_only) return max(0, avail_chest_keys + key_only - state.used_smalls) return state.key_locations - state.used_smalls def cnt_avail_small_locations_by_ctr(free_locations, counter, layout, world, player): - if world.keyshuffle[player] == 'none': + std_flag = world.mode[player] == 'standard' and layout.sector.name == 'Hyrule Castle' + if world.keyshuffle[player] == 'none' or std_flag: bk_adj = 1 if counter.big_key_opened and not layout.big_key_special else 0 - avail_chest_keys = min(free_locations - bk_adj, layout.max_chests) + # this is the secret passage, could expand to Uncle/Links House with appropriate logic + std_adj = 1 if std_flag and world.keyshuffle[player] != 'none' else 0 + avail_chest_keys = min(free_locations + std_adj - bk_adj, layout.max_chests) return max(0, avail_chest_keys + len(counter.key_only_locations) - counter.used_keys) return layout.max_chests + len(counter.key_only_locations) - counter.used_keys @@ -1683,10 +1696,10 @@ def create_key_counters(key_layout, world, player): if door.dest in flat_proposal and door.type != DoorType.SpiralStairs: key_layout.found_doors.add(door.dest) child_state = parent_state.copy() - if door.bigKey or door.name in special_big_key_doors: + if door.bigKey or door.name in get_special_big_key_doors(world, player): key_layout.key_logic.bk_doors.add(door) # open the door, if possible - if can_open_door(door, child_state, world, player): + if can_open_door(door, child_state, key_layout, world, player): open_a_door(door, child_state, flat_proposal, world, player) expand_key_state(child_state, flat_proposal, world, player) code = state_id(child_state, key_layout.flat_prop) @@ -1707,14 +1720,15 @@ def find_outside_connection(region): return None, None -def can_open_door(door, state, world, player): +def can_open_door(door, state, key_layout, world, player): if state.big_key_opened: ttl_locations = count_free_locations(state, world, player) else: ttl_locations = count_locations_exclude_big_chest(state.found_locations, world, player) if door.smallKey: ttl_small_key_only = count_small_key_only_locations(state) - available_small_locations = cnt_avail_small_locations(ttl_locations, ttl_small_key_only, state, world, player) + available_small_locations = cnt_avail_small_locations(ttl_locations, ttl_small_key_only, state, + key_layout, world, player) return available_small_locations > 0 elif door.bigKey: available_big_locations = cnt_avail_big_locations(ttl_locations, state, world, player) diff --git a/Rom.py b/Rom.py index 54d801bd..3e353b3a 100644 --- a/Rom.py +++ b/Rom.py @@ -37,7 +37,7 @@ from source.dungeon.RoomList import Room0127 JAP10HASH = '03a63945398191337e896e5771f77173' -RANDOMIZERBASEHASH = '4eeafc915838a7c9c5eff7a1b53d4709' +RANDOMIZERBASEHASH = '6f64fcea052e37b39d6b4bb24ae2f548' class JsonRom(object): diff --git a/Rules.py b/Rules.py index 4bf9cd83..a550a9d4 100644 --- a/Rules.py +++ b/Rules.py @@ -1288,7 +1288,7 @@ def standard_rules(world, player): # zelda should be saved before agahnim is in play add_rule(world.get_location('Agahnim 1', player), lambda state: state.has('Zelda Delivered', player)) - # too restrictive for crossed? + # uncle can't have keys generally because unplaced items aren't used here def uncle_item_rule(item): copy_state = CollectionState(world) copy_state.collect(item) diff --git a/data/base2current.bps b/data/base2current.bps index 3afa29d0716c0797214cc3ce5204a82e3c8f5ff5..93e4cfd280c90a77261c2e40c3ac5c95ef7ece69 100644 GIT binary patch delta 1001 zcmW+zUrbwd6z=b~x2?3L1-iD#pZ$7a&?d5U%mJ%ovN;lk8A-N;7!uPEH*rPwpb0W2 z{BFxdIz!sqOZi>6!M3oQ(l)cojcJ%6OqLCJAhIkxSlz>>B%6sz6X5}z;>D-)o$q|- zJNa@lIr4Fi{PqK&dh&##xNcAfcqs(zO5*60B&qHbz%TV{3;<*#Mr%@mYuu#%hPEd# zPQ4RM@KUj%Sp)u*I?Nk{x;jXkF8nJ&>&v$YJug*ueb-BvC!|NKC*J_yU{VQcYf~&D zMF6*1SmsAc*ytC5v*m60D$|oEV`|v@W%`#&6wl#HN?<4Jv5)S{4k$sJMqke|RB{LD zclm4zAa3S6haraoHNj`s>d^YE|X z7$IMDaO($o+w?#eyLay(Pr@%)nOtO{D%9k$dH$z%4ym5-^7%>&qRHU=_&FB3xoAXG zGO-c$eQI%5Iq%(*pUsM z@p-rx>6*o@a7a|bPe(m49ugr;hny`6xQl-_g35;BMVb^kuY|W#OZf-R1I4_kfS+*; zA!_7CMk$dDs{j&gc5a4%JjWd=xa z4;egB0>UNDm^1lAjbc-Iu^A_e9GNqP__P@uh2KXKw&6#S#Qd@+pP0H}SzAl)R?Btn zR8u~#g&zF5862~G=rAMUT|aFUNaHk{S3}IF%S*6i25$}zP70?{Qa0A|nO5X->aoJl zB<OE2C(DCzBjiF`9unK@G`vB#=Yj_$jiI9_Xwy3Zai@W zoGq%Um=0JRp5Qu5@egn3a6kU-aN3P^7EovSL@h8YF5N`SH5yy9trjp^Ku{fc1A=qL p_lL*W9=A6Z%bYkF5-w*e?D9K_#{TSa3hcFMZI5&B{WI`_`47GVkP`p^ delta 906 zcmW-eZA@Er6vlgQU&mT*-)RTKe+wIJwq+yBgw5eVG{z3H=pw_pXhU%7N{nW67_z{> zotIFSuD!jMQ@BoA*iBcqg)P^tpdsL11QM4Jr-{~RvN@whKh*dVr$)T^={(PKemT#F zbEl}CE^4p50Giy-oi?Ehwn49)0FRpaH!aJW8w_;Gy?PVCS(!7KHSm-$V(eq=!?MfN zq=!%B4*P9J{dzf-{T2VTGC!eQ_x#l!=2t;3F7A8;Vpy(*IzyDFS_#NaY*Irt%;FI> zc(fLbgxd=dH3alV3(tA;ztQyy)L!_a3@SBjp2?d-*U7RMR*d77{aDFD)4+$x3`5mK zFD$Q?Tw_nO$p;IoB`rC^D`A=^-^Lq*uHgUCowex6skzcE|ElVOq#_3e zj*;|=QPh;AO=^IO9Yi?CkY0gPNk-g_ZD!cC@x%vVLp*X>D`|*@|6y+Q7PnrQh#7-^ zR(#frXU*W%ZS_`At{tx_8lw2688*YsYZhRkUJ|H%K^xypqn8AlG~B%$P^#(I!3CA7 zLEbg_zQA>9^K7nno(T~nA z_RsAok#3Z2khE2yV2((7)rz{U3iB4F}|cDC({=XTk%sn?Avm9MBGhd%Hi*EC~U-@ z2$%O1UXGl3kH7DX)mBVqYiKg2BY)uhC2VnkgSnT+P6uq<(22gjze2_NRm@G1?P*00 z2ZXH=q4glf9pD2u&N$#vOLx^+)aeVxmw9jc Date: Wed, 15 Feb 2023 08:41:08 -0700 Subject: [PATCH 06/15] Fix for Desert Tiles 1 --- RoomData.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RoomData.py b/RoomData.py index 85c3b1fd..d8d3a82a 100644 --- a/RoomData.py +++ b/RoomData.py @@ -103,7 +103,7 @@ def create_rooms(world, player): Room(player, 0x60, 0x51309).door(Position.NorthE2, DoorKind.NormalLow2).door(Position.East2, DoorKind.NormalLow2).door(Position.East2, DoorKind.ToggleFlag).door(Position.EastN, DoorKind.Normal).door(Position.SouthE, DoorKind.Normal).door(Position.SouthE, DoorKind.IncognitoEntrance), Room(player, 0x61, 0x51454).door(Position.West2, DoorKind.NormalLow).door(Position.West2, DoorKind.ToggleFlag).door(Position.East2, DoorKind.NormalLow).door(Position.East2, DoorKind.ToggleFlag).door(Position.South2, DoorKind.NormalLow).door(Position.South2, DoorKind.IncognitoEntrance).door(Position.WestN, DoorKind.Normal), Room(player, 0x62, 0x51577).door(Position.West2, DoorKind.NormalLow2).door(Position.West2, DoorKind.ToggleFlag).door(Position.NorthW2, DoorKind.NormalLow2).door(Position.North, DoorKind.Normal).door(Position.SouthW, DoorKind.Normal).door(Position.SouthW, DoorKind.IncognitoEntrance), - Room(player, 0x63, 0xf88ed).door(Position.NorthE, DoorKind.StairKey).door(Position.InteriorW, DoorKind.TrapTriggerable).door(Position.SouthW, DoorKind.DungeonEntrance), # looked like a huge typo - I had to guess on StairKey + Room(player, 0x63, 0xf88ed).door(Position.NorthW, DoorKind.StairKey).door(Position.InteriorW, DoorKind.TrapTriggerable).door(Position.SouthW, DoorKind.DungeonEntrance), # looked like a huge typo - I had to guess on StairKey Room(player, 0x64, 0xfda53).door(Position.InteriorS, DoorKind.Trap2), Room(player, 0x65, 0xfdac5).door(Position.InteriorS, DoorKind.Normal), Room(player, 0x66, 0xfa01b).door(Position.InteriorE2, DoorKind.Waterfall).door(Position.SouthW2, DoorKind.NormalLow2).door(Position.SouthW2, DoorKind.ToggleFlag).door(Position.InteriorW2, DoorKind.NormalLow2), From 67bd5e870c53945a9d26cc4c91b7fd02e773ee22 Mon Sep 17 00:00:00 2001 From: aerinon Date: Wed, 15 Feb 2023 14:13:10 -0700 Subject: [PATCH 07/15] Fix for decoupled door in standard --- DungeonGenerator.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/DungeonGenerator.py b/DungeonGenerator.py index 8231833b..7881df0a 100644 --- a/DungeonGenerator.py +++ b/DungeonGenerator.py @@ -3517,6 +3517,8 @@ def identify_branching_issues(dungeon_map, builder_info): unconnected_builders[name] = builder for hook, door_list in unreached_doors.items(): builder.unfulfilled[hook] += len(door_list) + elif package: + builder.throne_door, builder.throne_sector, builder.chosen_lobby = package return unconnected_builders From fe0e0805ad94b34f1fb979e3a8fc564d98a7c5f6 Mon Sep 17 00:00:00 2001 From: aerinon Date: Wed, 15 Feb 2023 08:41:08 -0700 Subject: [PATCH 08/15] Fix for Desert Tiles 1 --- RoomData.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RoomData.py b/RoomData.py index f3c82576..eae844ef 100644 --- a/RoomData.py +++ b/RoomData.py @@ -103,7 +103,7 @@ def create_rooms(world, player): Room(player, 0x60, 0x51309).door(Position.NorthE2, DoorKind.NormalLow2).door(Position.East2, DoorKind.NormalLow2).door(Position.East2, DoorKind.ToggleFlag).door(Position.EastN, DoorKind.Normal).door(Position.SouthE, DoorKind.Normal).door(Position.SouthE, DoorKind.IncognitoEntrance), Room(player, 0x61, 0x51454).door(Position.West2, DoorKind.NormalLow).door(Position.West2, DoorKind.ToggleFlag).door(Position.East2, DoorKind.NormalLow).door(Position.East2, DoorKind.ToggleFlag).door(Position.South2, DoorKind.NormalLow).door(Position.South2, DoorKind.IncognitoEntrance).door(Position.WestN, DoorKind.Normal), Room(player, 0x62, 0x51577).door(Position.West2, DoorKind.NormalLow2).door(Position.West2, DoorKind.ToggleFlag).door(Position.NorthW2, DoorKind.NormalLow2).door(Position.North, DoorKind.Normal).door(Position.SouthW, DoorKind.Normal).door(Position.SouthW, DoorKind.IncognitoEntrance), - Room(player, 0x63, 0xf88ed).door(Position.NorthE, DoorKind.StairKey).door(Position.InteriorW, DoorKind.TrapTriggerable).door(Position.SouthW, DoorKind.DungeonEntrance), # looked like a huge typo - I had to guess on StairKey + Room(player, 0x63, 0xf88ed).door(Position.NorthW, DoorKind.StairKey).door(Position.InteriorW, DoorKind.TrapTriggerable).door(Position.SouthW, DoorKind.DungeonEntrance), # looked like a huge typo - I had to guess on StairKey Room(player, 0x64, 0xfda53).door(Position.InteriorS, DoorKind.Trap2), Room(player, 0x65, 0xfdac5).door(Position.InteriorS, DoorKind.Normal), Room(player, 0x66, 0xfa01b).door(Position.InteriorE2, DoorKind.Waterfall).door(Position.SouthW2, DoorKind.NormalLow2).door(Position.SouthW2, DoorKind.ToggleFlag).door(Position.InteriorW2, DoorKind.NormalLow2), From afac655de2bfa82fb72e23f50e3f0b352bc91cbd Mon Sep 17 00:00:00 2001 From: aerinon Date: Thu, 29 Dec 2022 19:39:13 -0700 Subject: [PATCH 09/15] Fixed door position typo --- Doors.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doors.py b/Doors.py index c137983f..f89696c6 100644 --- a/Doors.py +++ b/Doors.py @@ -773,7 +773,7 @@ def create_doors(world, player): create_door(player, 'Ice Freezors Hole', Hole), create_door(player, 'Ice Freezors Bomb Hole', Hole), # combine these two? -- they have to lead to the same spot create_door(player, 'Ice Freezors Ledge Hole', Hole), - create_door(player, 'Ice Freezors Ledge ES', Intr).dir(Ea, 0x7e, Bot, High).pos(2), + create_door(player, 'Ice Freezors Ledge ES', Intr).dir(Ea, 0x7e, Bot, High).pos(1), create_door(player, 'Ice Tall Hint WS', Intr).dir(We, 0x7e, Bot, High).pos(1), create_door(player, 'Ice Tall Hint EN', Nrml).dir(Ea, 0x7e, Top, High).pos(2), create_door(player, 'Ice Tall Hint SE', Nrml).dir(So, 0x7e, Right, High).small_key().pos(0).portal(X, 0x02), From 5f8be831ec96184dfd031f16cb8e5d7e8180707a Mon Sep 17 00:00:00 2001 From: aerinon Date: Wed, 15 Feb 2023 14:25:00 -0700 Subject: [PATCH 10/15] Version bump and release notes --- Main.py | 2 +- RELEASENOTES.md | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/Main.py b/Main.py index e1a86313..11138c65 100644 --- a/Main.py +++ b/Main.py @@ -31,7 +31,7 @@ from Utils import output_path, parse_player_names from source.item.FillUtil import create_item_pool_config, massage_item_pool, district_item_pool_config from source.tools.BPS import create_bps_from_data -__version__ = '1.1.3-dev' +__version__ = '1.1.4-dev' from source.classes.BabelFish import BabelFish diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 924fdaa5..fd66faff 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -181,6 +181,10 @@ Same as above but both small keys and bigs keys of the dungeon are not allowed o # Bug Fixes and Notes +* 1.1.4 + * Removed a Triforce text + * Fix for Desert Tiles 1 key door + * Fix for Ice Freezors Ledge door position * 1.1.3 * Fixed a typo on a door near the key rat * 1.1.2 From f0101c98547f0bcec7840b007ce86c2a7ac11d83 Mon Sep 17 00:00:00 2001 From: aerinon Date: Thu, 16 Feb 2023 11:49:29 -0700 Subject: [PATCH 11/15] Trap door mode initial work --- BaseClasses.py | 28 +++- CLI.py | 5 +- DoorShuffle.py | 124 ++++++++++-------- Main.py | 2 + README.md | 21 +++ mystery_example.yml | 9 ++ resources/app/cli/args.json | 15 +++ resources/app/cli/lang/en.json | 13 ++ resources/app/gui/lang/en.json | 11 ++ .../app/gui/randomize/dungeon/widgets.json | 21 ++- source/classes/CustomSettings.py | 4 + source/classes/constants.py | 2 + source/tools/MysteryUtils.py | 2 + 13 files changed, 195 insertions(+), 62 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index c2da8e91..d9952095 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -29,6 +29,8 @@ class World(object): self.doorShuffle = doorShuffle.copy() self.intensity = {} self.door_type_mode = {} + self.trap_door_mode = {} + self.key_logic_algorithm = {} self.logic = logic.copy() self.mode = mode.copy() self.swords = swords.copy() @@ -144,6 +146,8 @@ class World(object): set_player_attr('pot_pool', {}) set_player_attr('decoupledoors', False) set_player_attr('door_type_mode', 'original') + set_player_attr('trap_door_mode', 'vanilla') + set_player_attr('key_logic_algorithm', 'default') set_player_attr('shopsanity', False) set_player_attr('mixed_travel', 'prevent') @@ -2434,6 +2438,8 @@ class Spoiler(object): 'door_shuffle': self.world.doorShuffle, 'intensity': self.world.intensity, 'door_type_mode': self.world.door_type_mode, + 'trap_door_mode': self.world.trap_door_mode, + 'key_logic': self.world.key_logic_algorithm, 'decoupledoors': self.world.decoupledoors, 'dungeon_counters': self.world.dungeon_counters, 'item_pool': self.world.difficulty, @@ -2640,6 +2646,8 @@ class Spoiler(object): if self.metadata['door_shuffle'][player] != 'vanilla': outfile.write(f"Intensity: {self.metadata['intensity'][player]}\n") outfile.write(f"Door Type Mode: {self.metadata['door_type_mode'][player]}\n") + outfile.write(f"Trap Door Mode: {self.metadata['trap_door_mode'][player]}\n") + outfile.write(f"Key Logic Algorithm: {self.metadata['key_logic'][player]}\n") outfile.write(f"Decouple Doors: {yn(self.metadata['decoupledoors'][player])}\n") outfile.write(f"Experimental: {yn(self.metadata['experimental'][player])}\n") outfile.write(f"Dungeon Counters: {self.metadata['dungeon_counters'][player]}\n") @@ -2931,15 +2939,19 @@ boss_mode = {"none": 0, "simple": 1, "full": 2, "chaos": 3, 'random': 3, 'unique # byte 10: settings_version -# byte 11: FBBB, TTSS (flute_mode, bow_mode, take_any, small_key_mode) +# byte 11: FBBB TTSS (flute_mode, bow_mode, take_any, small_key_mode) flute_mode = {'normal': 0, 'active': 1} keyshuffle_mode = {'none': 0, 'wild': 1, 'universal': 2} # reserved 8 modes? take_any_mode = {'none': 0, 'random': 1, 'fixed': 2} bow_mode = {'progressive': 0, 'silvers': 1, 'retro': 2, 'retro_silvers': 3} # additions -# psuedoboots does not effect code -# sfx_shuffle and other adjust items does not effect settings code +# byte 12: POOT TKKK (pseudoboots, overworld_map, trap_door_mode, key_logic_algo) +overworld_map_mode = {'default': 0, 'compass': 1, 'map': 2} +trap_door_mode = {'vanilla': 0, 'boss': 1, 'oneway': 2} +key_logic_algo = {'loose': 0, 'default': 1, 'partial': 2, 'strict': 4} + +# sfx_shuffle and other adjust items does not affect settings code # Bump this when making changes that are not backwards compatible (nearly all of them) settings_version = 1 @@ -2983,7 +2995,10 @@ class Settings(object): settings_version, (flute_mode[w.flute_mode[p]] << 7 | bow_mode[w.bow_mode[p]] << 4 - | take_any_mode[w.take_any[p]] << 2 | keyshuffle_mode[w.keyshuffle[p]]) + | take_any_mode[w.take_any[p]] << 2 | keyshuffle_mode[w.keyshuffle[p]]), + + ((0x80 if w.pseudoboots[p] else 0) | overworld_map_mode[w.overworld_map[p]] << 6 + | trap_door_mode[w.trap_door_mode[p]] << 4 | key_logic_algo[w.key_logic_algorithm[p]]), ]) return base64.b64encode(code, "+-".encode()).decode() @@ -3051,6 +3066,11 @@ class Settings(object): args.bow_mode[p] = r(bow_mode)[(settings[11] & 0x70) >> 4] args.take_any[p] = r(take_any_mode)[(settings[11] & 0xC) >> 2] args.keyshuffle[p] = r(keyshuffle_mode)[settings[11] & 0x3] + if len(settings) > 12: + args.pseudoboots[p] = True if settings[12] & 0x80 else False + args.overworld_map[p] = r(overworld_map_mode)[(settings[12] & 0x60) >> 6] + args.trap_door_mode[p] = r(trap_door_mode)[(settings[12] & 0x14) >> 4] + args.key_logic_algorithm[p] = r(key_logic_algo)[settings[12] & 0x07] class KeyRuleType(FastEnum): diff --git a/CLI.py b/CLI.py index 0bbf1181..0df0d03a 100644 --- a/CLI.py +++ b/CLI.py @@ -140,7 +140,8 @@ def parse_cli(argv, no_defaults=False): 'ow_palettes', 'uw_palettes', 'sprite', 'disablemusic', 'quickswap', 'fastmenu', 'heartcolor', 'heartbeep', 'remote_items', 'shopsanity', 'dropshuffle', 'pottery', 'keydropshuffle', 'mixed_travel', 'standardize_palettes', 'code', 'reduce_flashing', 'shuffle_sfx', - 'msu_resume', 'collection_rate', 'colorizepots', 'decoupledoors', 'door_type_mode']: + 'msu_resume', 'collection_rate', 'colorizepots', 'decoupledoors', 'door_type_mode', + 'trap_door_mode', 'key_logic_algorithm']: value = getattr(defaults, name) if getattr(playerargs, name) is None else getattr(playerargs, name) if player == 1: setattr(ret, name, {1: value}) @@ -214,6 +215,8 @@ def parse_settings(): 'door_shuffle': 'vanilla', 'intensity': 2, 'door_type_mode': 'original', + 'trap_door_mode': 'vanilla', + 'key_logic_algorithm': 'default', 'decoupledoors': False, 'experimental': False, 'dungeon_counters': 'default', diff --git a/DoorShuffle.py b/DoorShuffle.py index a4cf7397..f53fe867 100644 --- a/DoorShuffle.py +++ b/DoorShuffle.py @@ -1781,58 +1781,61 @@ def shuffle_door_types(door_type_pools, paths, world, player): def shuffle_trap_doors(door_type_pools, paths, start_regions_map, world, player): used_doors = set() for pool, door_type_pool in door_type_pools: - ttl = 0 - suggestion_map, trap_map, flex_map = {}, {}, {} - remaining = door_type_pool.traps - if player in world.custom_door_types: - custom_trap_doors = world.custom_door_types[player]['Trap Door'] - else: - custom_trap_doors = defaultdict(list) + if world.trap_door_mode[player] != 'oneway': + ttl = 0 + suggestion_map, trap_map, flex_map = {}, {}, {} + remaining = door_type_pool.traps + if player in world.custom_door_types: + custom_trap_doors = world.custom_door_types[player]['Trap Door'] + else: + custom_trap_doors = defaultdict(list) - for dungeon in pool: - builder = world.dungeon_layouts[player][dungeon] - find_trappable_candidates(builder, world, player) - if custom_trap_doors[dungeon]: - builder.candidates.trap = filter_key_door_pool(builder.candidates.trap, custom_trap_doors[dungeon]) - remaining -= len(custom_trap_doors[dungeon]) - ttl += len(builder.candidates.trap) - if ttl == 0: - continue - for dungeon in pool: - builder = world.dungeon_layouts[player][dungeon] - proportion = len(builder.candidates.trap) - calc = int(round(proportion * door_type_pool.traps/ttl)) - suggested = min(proportion, calc) - remaining -= suggested - suggestion_map[dungeon] = suggested - flex_map[dungeon] = (proportion - suggested) if suggested < proportion else 0 - for dungeon in pool: - builder = world.dungeon_layouts[player][dungeon] - valid_traps, trap_number = find_valid_trap_combination(builder, suggestion_map[dungeon], - start_regions_map[dungeon], paths, world, player, - drop=True) - trap_map[dungeon] = valid_traps - if trap_number < suggestion_map[dungeon]: - flex_map[dungeon] = 0 - remaining += suggestion_map[dungeon] - trap_number - suggestion_map[dungeon] = trap_number - builder_order = [x for x in pool if flex_map[x] > 0] - random.shuffle(builder_order) - queue = deque(builder_order) - while len(queue) > 0 and remaining > 0: - dungeon = queue.popleft() - builder = world.dungeon_layouts[player][dungeon] - increased = suggestion_map[dungeon] + 1 - valid_traps, trap_number = find_valid_trap_combination(builder, increased, start_regions_map[dungeon], - paths, world, player) - if valid_traps: + for dungeon in pool: + builder = world.dungeon_layouts[player][dungeon] + find_trappable_candidates(builder, world, player) # todo: + if custom_trap_doors[dungeon]: + builder.candidates.trap = filter_key_door_pool(builder.candidates.trap, custom_trap_doors[dungeon]) + remaining -= len(custom_trap_doors[dungeon]) + ttl += len(builder.candidates.trap) + if ttl == 0: + continue + for dungeon in pool: + builder = world.dungeon_layouts[player][dungeon] + proportion = len(builder.candidates.trap) + calc = int(round(proportion * door_type_pool.traps/ttl)) + suggested = min(proportion, calc) + remaining -= suggested + suggestion_map[dungeon] = suggested + flex_map[dungeon] = (proportion - suggested) if suggested < proportion else 0 + for dungeon in pool: + builder = world.dungeon_layouts[player][dungeon] + valid_traps, trap_number = find_valid_trap_combination(builder, suggestion_map[dungeon], + start_regions_map[dungeon], paths, world, player, + drop=True) trap_map[dungeon] = valid_traps - remaining -= 1 - suggestion_map[dungeon] = increased - flex_map[dungeon] -= 1 - if flex_map[dungeon] > 0: - queue.append(dungeon) - # time to re-assign + if trap_number < suggestion_map[dungeon]: + flex_map[dungeon] = 0 + remaining += suggestion_map[dungeon] - trap_number + suggestion_map[dungeon] = trap_number + builder_order = [x for x in pool if flex_map[x] > 0] + random.shuffle(builder_order) + queue = deque(builder_order) + while len(queue) > 0 and remaining > 0: + dungeon = queue.popleft() + builder = world.dungeon_layouts[player][dungeon] + increased = suggestion_map[dungeon] + 1 + valid_traps, trap_number = find_valid_trap_combination(builder, increased, start_regions_map[dungeon], + paths, world, player) + if valid_traps: + trap_map[dungeon] = valid_traps + remaining -= 1 + suggestion_map[dungeon] = increased + flex_map[dungeon] -= 1 + if flex_map[dungeon] > 0: + queue.append(dungeon) + # time to re-assign + else: + trap_map = {dungeon: [] for dungeon in pool} reassign_trap_doors(trap_map, world, player) for name, traps in trap_map.items(): used_doors.update(traps) @@ -2138,7 +2141,7 @@ def find_trappable_candidates(builder, world, player): for ext in world.get_region(r, player).exits: if ext.door: d = ext.door - if d.blocked and d.trapFlag != 0 and 'Boss' not in d.name and 'Agahnim' not in d.name: + if d.blocked and d.trapFlag != 0 and exclude_boss_traps(d): builder.candidates.trap.append(d) @@ -2281,7 +2284,7 @@ def reassign_trap_doors(trap_map, world, player): logger = logging.getLogger('') for name, traps in trap_map.items(): builder = world.dungeon_layouts[player][name] - queue = deque(find_current_trap_doors(builder)) + queue = deque(find_current_trap_doors(builder, world, player)) while len(queue) > 0: d = queue.pop() if d.type is DoorType.Interior and d not in traps: @@ -2304,12 +2307,21 @@ def reassign_trap_doors(trap_map, world, player): logger.debug('Trap Door: %s', d.name) -def find_current_trap_doors(builder): +def exclude_boss_traps(d): + return ' Boss ' not in d.name and ' Agahnim ' not in d.name and d.name not in ['Skull Spike Corner SW', + 'Mire Warping Pool ES'] + +def exclude_logic_traps(d): + return d.name != 'Mire Warping Pool ES' + + +def find_current_trap_doors(builder, world, player): + checker = exclude_boss_traps if world.trap_door_mode[player] == 'vanilla' else exclude_logic_traps current_doors = [] for region in builder.master_sector.regions: for ext in region.exits: d = ext.door - if d and d.blocked and d.trapFlag != 0: # could exclude removing boss doors here + if d and d.blocked and d.trapFlag != 0 and checker(d): current_doors.append(d) return current_doors @@ -4550,8 +4562,8 @@ door_type_counts = { 'Agahnims Tower': (4, 0, 1, 0, 0, 1, 0), 'Swamp Palace': (6, 0, 0, 2, 0, 0, 0), 'Palace of Darkness': (6, 1, 1, 3, 2, 0, 0), - 'Misery Mire': (6, 3, 5, 2, 0, 0, 0), - 'Skull Woods': (5, 0, 2, 2, 0, 1, 0), + 'Misery Mire': (6, 3, 4, 2, 0, 0, 0), + 'Skull Woods': (5, 0, 1, 2, 0, 1, 0), 'Ice Palace': (6, 1, 3, 0, 0, 0, 0), 'Tower of Hera': (1, 1, 0, 0, 0, 0, 0), 'Thieves Town': (3, 1, 2, 1, 1, 0, 0), diff --git a/Main.py b/Main.py index 02b7d032..83e14f47 100644 --- a/Main.py +++ b/Main.py @@ -110,6 +110,8 @@ def main(args, seed=None, fish=None): world.beemizer = args.beemizer.copy() world.intensity = {player: random.randint(1, 3) if args.intensity[player] == 'random' else int(args.intensity[player]) for player in range(1, world.players + 1)} world.door_type_mode = args.door_type_mode.copy() + world.trap_door_mode = args.trap_door_mode.copy() + world.key_logic_algorithm = args.key_logic_algorithm.copy() world.decoupledoors = args.decoupledoors.copy() world.experimental = args.experimental.copy() world.dungeon_counters = args.dungeon_counters.copy() diff --git a/README.md b/README.md index bbb9b9bf..2cd2b881 100644 --- a/README.md +++ b/README.md @@ -144,6 +144,27 @@ Four options here, and all of them only take effect if Dungeon Door Shuffle is n CLI: `--door_type_mode [original|big|all|chaos]` +### Trap Door Removal + +Three options here for making dungeon traversal nicer. Only applies if door shuffle is not vanilla. + +* Normal: This does not remove any trap doors. Note that boss trap doors are never shuffled in this mode. +* Remove Boss Traps: Boss traps are removed this includes the one near Mothula. +* Remove All Annoying Traps: This removes all trap doors that are annoying, including boss traps. Note, that the trap door near the mire cutscene chest is left alone because it enforces the use of fire to get to the chest. + +CLI: `--trap_door_mode [vanilla|boss|oneway]` + +### Key Logic Algorithm + +Determines how small key door logic works. + +* Loose: Skips placement rules checks. Currently, experimental to see what kinds of problems can arise. +* Default: Current key logic. Assumes worse case usage, placement checks, but assumes you can't get to a chest until you have sufficient keys. (May assume items are unreachable) +* Partial Protection: Assumes you always have full inventory and worse case usage. This should account for dark room and bunny revival glitches. +* Strict: For those would like to glitch and be protected from yourselves. Small keys door require all small keys to be available to be in logic. + +CLI: `--key_logic [loose|default|partial|strict]` + ### Decouple Doors This is similar to insanity mode in ER where door entrances and exits are not paired anymore. Tends to remove more logic from dungeons as many rooms will not be required to traverse to explore. Hope you like transitions. diff --git a/mystery_example.yml b/mystery_example.yml index 275359fc..dc1a21a7 100644 --- a/mystery_example.yml +++ b/mystery_example.yml @@ -22,6 +22,15 @@ big: 2 all: 1 chaos: 1 + trap_door_mode: + vanilla: 1 + boss: 0 + oneway: 0 + key_logic_algorithm: + loose: 0 + default: 1 + partial: 0 + strict: 0 decoupledoors: off dropshuffle: on: 1 diff --git a/resources/app/cli/args.json b/resources/app/cli/args.json index a09b1722..0926a8c9 100644 --- a/resources/app/cli/args.json +++ b/resources/app/cli/args.json @@ -193,6 +193,21 @@ "chaos" ] }, + "trap_door_mode": { + "choices": [ + "vanilla", + "boss", + "oneway" + ] + }, + "key_logic_algorithm": { + "choices": [ + "loose", + "default", + "partial", + "strict" + ] + }, "decoupledoors": { "action": "store_true", "type": "bool" diff --git a/resources/app/cli/lang/en.json b/resources/app/cli/lang/en.json index 0d3d4cdd..8e574ee0 100644 --- a/resources/app/cli/lang/en.json +++ b/resources/app/cli/lang/en.json @@ -239,6 +239,19 @@ "all: Adds traps doors (and any future supported door types)", "chaos: Increases the number of door types in all dungeon pools" ], + "trap_door_mode" : [ + "Trap Door Removal (default: %(default)s)", + "vanilla: No trap door removal", + "boss: Remove boss traps", + "oneway: Remove annoying trap doors" + ], + "key_logic_algorithm": [ + "Key Logic Algorithm (default: %(default)s)", + "loose: Allow more randomization", + "default: Balance between safety and randomization", + "partial: Partial protection when using certain minor glitches", + "strict: Ensure small keys are available" + ], "decoupledoors" : [ "Door entrances and exits are decoupled" ], "experimental": [ "Enable experimental features. (default: %(default)s)" ], "dungeon_counters": [ "Enable dungeon chest counters. (default: %(default)s)" ], diff --git a/resources/app/gui/lang/en.json b/resources/app/gui/lang/en.json index 418d623b..3135572e 100644 --- a/resources/app/gui/lang/en.json +++ b/resources/app/gui/lang/en.json @@ -91,6 +91,17 @@ "randomizer.dungeon.door_type_mode.all": "Adds Trap Doors", "randomizer.dungeon.door_type_mode.chaos": "Increases all door types", + "randomizer.dungeon.trap_door_mode": "Trap Door Removal", + "randomizer.dungeon.trap_door_mode.vanilla": "No Removal", + "randomizer.dungeon.trap_door_mode.boss": "Remove Boss Traps", + "randomizer.dungeon.trap_door_mode.oneway": "Remove Annoying Traps", + + "randomizer.dungeon.key_logic_algorithm": "Key Logic Algorithm", + "randomizer.dungeon.key_logic_algorithm.loose": "Loose", + "randomizer.dungeon.key_logic_algorithm.default": "Default", + "randomizer.dungeon.key_logic_algorithm.partial": "Partial Protection", + "randomizer.dungeon.key_logic_algorithm.strict": "Strict", + "randomizer.dungeon.experimental": "Enable Experimental Features", "randomizer.dungeon.dungeon_counters": "Dungeon Chest Counters", diff --git a/resources/app/gui/randomize/dungeon/widgets.json b/resources/app/gui/randomize/dungeon/widgets.json index bdedbfba..cecfeeff 100644 --- a/resources/app/gui/randomize/dungeon/widgets.json +++ b/resources/app/gui/randomize/dungeon/widgets.json @@ -33,7 +33,7 @@ }, "door_type_mode": { "type": "selectbox", - "default": "basic", + "default": "original", "options": [ "original", "big", @@ -44,6 +44,25 @@ "width": 45 } }, + "trap_door_mode": { + "type": "selectbox", + "default": "vanilla", + "options": [ + "vanilla", + "boss", + "oneway" + ] + }, + "key_logic_algorithm": { + "type": "selectbox", + "default": "default", + "options": [ + "loose", + "default", + "partial", + "strict" + ] + }, "decoupledoors": { "type": "checkbox" }, "keydropshuffle": { "type": "checkbox" }, "pottery": { diff --git a/source/classes/CustomSettings.py b/source/classes/CustomSettings.py index af6e47ff..fd15253f 100644 --- a/source/classes/CustomSettings.py +++ b/source/classes/CustomSettings.py @@ -109,6 +109,8 @@ class CustomSettings(object): args.standardize_palettes[p]) args.intensity[p] = get_setting(settings['intensity'], args.intensity[p]) args.door_type_mode[p] = get_setting(settings['door_type_mode'], args.door_type_mode[p]) + args.trap_door_mode[p] = get_setting(settings['trap_door_mode'], args.trap_door_mode[p]) + args.key_logic_algorithm[p] = get_setting(settings['key_logic_algorithm'], args.key_logic_algorithm[p]) args.decoupledoors[p] = get_setting(settings['decoupledoors'], args.decoupledoors[p]) args.dungeon_counters[p] = get_setting(settings['dungeon_counters'], args.dungeon_counters[p]) args.crystals_gt[p] = get_setting(settings['crystals_gt'], args.crystals_gt[p]) @@ -219,6 +221,8 @@ class CustomSettings(object): settings_dict[p]['door_shuffle'] = world.doorShuffle[p] settings_dict[p]['intensity'] = world.intensity[p] settings_dict[p]['door_type_mode'] = world.door_type_mode[p] + settings_dict[p]['trap_door_mode'] = world.trap_door_mode[p] + settings_dict[p]['key_logic_algorithm'] = world.key_logic_algorithm[p] settings_dict[p]['decoupledoors'] = world.decoupledoors[p] settings_dict[p]['logic'] = world.logic[p] settings_dict[p]['mode'] = world.mode[p] diff --git a/source/classes/constants.py b/source/classes/constants.py index e5ecd68c..d6624d8f 100644 --- a/source/classes/constants.py +++ b/source/classes/constants.py @@ -98,6 +98,8 @@ SETTINGSTOPROCESS = { "dungeondoorshuffle": "door_shuffle", "dungeonintensity": "intensity", "door_type_mode": "door_type_mode", + "trap_door_mode": "trap_door_mode", + "key_logic_algorithm": "key_logic_algorithm", "decoupledoors": "decoupledoors", "keydropshuffle": "keydropshuffle", "dropshuffle": "dropshuffle", diff --git a/source/tools/MysteryUtils.py b/source/tools/MysteryUtils.py index b5fef26d..322ed447 100644 --- a/source/tools/MysteryUtils.py +++ b/source/tools/MysteryUtils.py @@ -84,6 +84,8 @@ def roll_settings(weights): ret.door_shuffle = door_shuffle if door_shuffle != 'none' else 'vanilla' ret.intensity = get_choice('intensity') ret.door_type_mode = get_choice('door_type_mode') + ret.trap_door_mode = get_choice('trap_door_mode') + ret.key_logic_algorithm = get_choice('key_logic_algorithm') ret.decoupledoors = get_choice('decoupledoors') == 'on' ret.experimental = get_choice('experimental') == 'on' ret.collection_rate = get_choice('collection_rate') == 'on' From 77c4babca1f5e47079e8941428ff90eb10a25910 Mon Sep 17 00:00:00 2001 From: aerinon Date: Thu, 16 Feb 2023 15:40:13 -0700 Subject: [PATCH 12/15] Potential fix for vanilla door + ER key logic --- DoorShuffle.py | 44 ++++++++++++++++++++++++++++++-------------- 1 file changed, 30 insertions(+), 14 deletions(-) diff --git a/DoorShuffle.py b/DoorShuffle.py index f53fe867..f1457b36 100644 --- a/DoorShuffle.py +++ b/DoorShuffle.py @@ -225,21 +225,37 @@ def vanilla_key_logic(world, player): add_inaccessible_doors(world, player) entrances_map, potentials, connections = determine_entrance_list(world, player) - for builder in builders: + enabled_entrances = world.enabled_entrances[player] = {} + builder_queue = deque(builders) + last_key, loops = None, 0 + while len(builder_queue) > 0: + builder = builder_queue.popleft() origin_list = entrances_map[builder.name] - start_regions = convert_regions(origin_list, world, player) - doors = convert_key_doors(default_small_key_doors[builder.name], world, player) - key_layout = build_key_layout(builder, start_regions, doors, {}, world, player) - valid = validate_key_layout(key_layout, world, player) - if not valid: - logging.getLogger('').info('Vanilla key layout not valid %s', builder.name) - builder.key_door_proposal = doors - if player not in world.key_logic.keys(): - world.key_logic[player] = {} - analyze_dungeon(key_layout, world, player) - world.key_logic[player][builder.name] = key_layout.key_logic - world.key_layout[player][builder.name] = key_layout - log_key_logic(builder.name, key_layout.key_logic) + find_enabled_origins(builder.sectors, enabled_entrances, origin_list, entrances_map, builder.name) + if len(origin_list) <= 0: + if last_key == builder.name or loops > 1000: + origin_name = (world.get_region(origin_list[0], player).entrances[0].parent_region.name + if len(origin_list) > 0 else 'no origin') + raise GenerationException(f'Infinite loop detected for "{builder.name}" located at {origin_name}') + builder_queue.append(builder) + last_key = builder.name + loops += 1 + else: + find_new_entrances(builder.master_sector, entrances_map, connections, potentials, + enabled_entrances, world, player) + start_regions = convert_regions(origin_list, world, player) + doors = convert_key_doors(default_small_key_doors[builder.name], world, player) + key_layout = build_key_layout(builder, start_regions, doors, {}, world, player) + valid = validate_key_layout(key_layout, world, player) + if not valid: + logging.getLogger('').info('Vanilla key layout not valid %s', builder.name) + builder.key_door_proposal = doors + if player not in world.key_logic.keys(): + world.key_logic[player] = {} + analyze_dungeon(key_layout, world, player) + world.key_logic[player][builder.name] = key_layout.key_logic + world.key_layout[player][builder.name] = key_layout + log_key_logic(builder.name, key_layout.key_logic) # if world.shuffle[player] == 'vanilla' and world.accessibility[player] == 'items' and not world.retro[player] and not world.keydropshuffle[player]: # validate_vanilla_key_logic(world, player) From 2b8b9156d96a7cc997ca96613d10f5e2a3a3ec78 Mon Sep 17 00:00:00 2001 From: aerinon Date: Fri, 17 Feb 2023 10:03:37 -0700 Subject: [PATCH 13/15] Minor randomization consistency --- source/overworld/EntranceShuffle2.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/source/overworld/EntranceShuffle2.py b/source/overworld/EntranceShuffle2.py index a364530c..a89e6085 100644 --- a/source/overworld/EntranceShuffle2.py +++ b/source/overworld/EntranceShuffle2.py @@ -304,7 +304,8 @@ def do_main_shuffle(entrances, exits, avail, mode_def): unused_entrances = set() if not cross_world: lw_entrances, dw_entrances = [], [] - for x in rem_entrances: + left = sorted(rem_entrances) + for x in left: if bonk_fairy_exception(x): lw_entrances.append(x) if x in LW_Entrances else dw_entrances.append(x) do_same_world_connectors(lw_entrances, dw_entrances, multi_exit_caves, avail) From d7c15ae22c7833c5746dc9dcbadbf1aee73e90ad Mon Sep 17 00:00:00 2001 From: aerinon Date: Fri, 17 Feb 2023 10:07:43 -0700 Subject: [PATCH 14/15] Strict and Partial key logic implementations with new test suite utility --- BaseClasses.py | 28 ++- Fill.py | 64 ++++++- ItemList.py | 7 + README.md | 3 +- Rules.py | 21 ++- mystery_example.yml | 1 - resources/app/cli/args.json | 1 - resources/app/cli/lang/en.json | 1 - resources/app/gui/lang/en.json | 1 - .../app/gui/randomize/dungeon/widgets.json | 1 - source/item/FillUtil.py | 6 + test/NewTestSuite.py | 126 ++++++++++++++ test/suite/default_key_logic.yaml | 44 +++++ test/suite/partial_key_logic.yaml | 24 +++ test/suite/partial_key_logic_2.yaml | 26 +++ test/suite/partial_key_logic_3.yaml | 160 ++++++++++++++++++ test/suite/strict_key_logic.yaml | 22 +++ 17 files changed, 512 insertions(+), 24 deletions(-) create mode 100644 test/NewTestSuite.py create mode 100644 test/suite/default_key_logic.yaml create mode 100644 test/suite/partial_key_logic.yaml create mode 100644 test/suite/partial_key_logic_2.yaml create mode 100644 test/suite/partial_key_logic_3.yaml create mode 100644 test/suite/strict_key_logic.yaml diff --git a/BaseClasses.py b/BaseClasses.py index d9952095..ca44b0a3 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -510,6 +510,7 @@ class CollectionState(object): self.world = parent if not skip_init: self.prog_items = Counter() + self.forced_keys = Counter() self.reachable_regions = {player: dict() for player in range(1, parent.players + 1)} self.blocked_connections = {player: dict() for player in range(1, parent.players + 1)} self.events = [] @@ -542,12 +543,13 @@ class CollectionState(object): queue = deque(self.blocked_connections[player].items()) self.traverse_world(queue, rrp, bc, player) - unresolved_events = [x for y in self.reachable_regions[player] for x in y.locations - if x.event and x.item and (x.item.smallkey or x.item.bigkey or x.item.advancement) - and x not in self.locations_checked and x.can_reach(self)] - unresolved_events = self._do_not_flood_the_keys(unresolved_events) - if len(unresolved_events) == 0: - self.check_key_doors_in_dungeons(rrp, player) + if self.world.key_logic_algorithm[player] == 'default': + unresolved_events = [x for y in self.reachable_regions[player] for x in y.locations + if x.event and x.item and (x.item.smallkey or x.item.bigkey or x.item.advancement) + and x not in self.locations_checked and x.can_reach(self)] + unresolved_events = self._do_not_flood_the_keys(unresolved_events) + if len(unresolved_events) == 0: + self.check_key_doors_in_dungeons(rrp, player) def traverse_world(self, queue, rrp, bc, player): # run BFS on all connections, and keep track of those blocked by missing items @@ -639,6 +641,7 @@ class CollectionState(object): def check_key_doors_in_dungeons(self, rrp, player): for dungeon_name, checklist in self.dungeons_to_check[player].items(): + # todo: optimization idea - abort exploration if there are unresolved events now if self.apply_dungeon_exploration(rrp, player, dungeon_name, checklist): continue init_door_candidates = self.should_explore_child_state(self, dungeon_name, player) @@ -838,6 +841,7 @@ class CollectionState(object): def copy(self): ret = CollectionState(self.world, skip_init=True) ret.prog_items = self.prog_items.copy() + ret.forced_keys = self.forced_keys.copy() ret.reachable_regions = {player: copy.copy(self.reachable_regions[player]) for player in range(1, self.world.players + 1)} ret.blocked_connections = {player: copy.copy(self.blocked_connections[player]) for player in range(1, self.world.players + 1)} ret.events = copy.copy(self.events) @@ -1045,6 +1049,14 @@ class CollectionState(object): return (item, player) in self.prog_items return self.prog_items[item, player] >= count + def has_sm_key_strict(self, item, player, count=1): + if self.world.keyshuffle[player] == 'universal': + if self.world.mode[player] == 'standard' and self.world.doorShuffle[player] == 'vanilla' and item == 'Small Key (Escape)': + return True # Cannot access the shop until escape is finished. This is safe because the key is manually placed in make_custom_item_pool + return self.can_buy_unlimited('Small Key (Universal)', player) + obtained = self.prog_items[item, player] - self.forced_keys[item, player] + return obtained >= count + def can_buy_unlimited(self, item, player): for shop in self.world.shops[player]: if shop.region.player == player and shop.has_unlimited(item) and shop.region.can_reach(self): @@ -1241,6 +1253,8 @@ class CollectionState(object): def collect(self, item, event=False, location=None): if location: self.locations_checked.add(location) + if item and item.smallkey and location.forced_item is not None: + self.forced_keys[item.name, item.player] += 1 if not item: return changed = False @@ -2949,7 +2963,7 @@ bow_mode = {'progressive': 0, 'silvers': 1, 'retro': 2, 'retro_silvers': 3} # byte 12: POOT TKKK (pseudoboots, overworld_map, trap_door_mode, key_logic_algo) overworld_map_mode = {'default': 0, 'compass': 1, 'map': 2} trap_door_mode = {'vanilla': 0, 'boss': 1, 'oneway': 2} -key_logic_algo = {'loose': 0, 'default': 1, 'partial': 2, 'strict': 4} +key_logic_algo = {'default': 0, 'partial': 1, 'strict': 2} # sfx_shuffle and other adjust items does not affect settings code diff --git a/Fill.py b/Fill.py index 239d8e8e..4859ebc3 100644 --- a/Fill.py +++ b/Fill.py @@ -105,6 +105,8 @@ def fill_restrictive(world, base_state, locations, itempool, key_pool=None, sing spot_to_fill = None item_locations = filter_locations(item_to_place, locations, world, vanilla) + verify(item_to_place, item_locations, maximum_exploration_state, single_player_placement, + perform_access_check, key_pool, world) for location in item_locations: spot_to_fill = verify_spot_to_fill(location, item_to_place, maximum_exploration_state, single_player_placement, perform_access_check, key_pool, world) @@ -128,9 +130,6 @@ def fill_restrictive(world, base_state, locations, itempool, key_pool=None, sing raise FillError('No more spots to place %s' % item_to_place) world.push_item(spot_to_fill, item_to_place, False) - if item_to_place.smallkey: - with suppress(ValueError): - key_pool.remove(item_to_place) track_outside_keys(item_to_place, spot_to_fill, world) track_dungeon_items(item_to_place, spot_to_fill, world) locations.remove(spot_to_fill) @@ -144,6 +143,9 @@ def verify_spot_to_fill(location, item_to_place, max_exp_state, single_player_pl if item_to_place.smallkey or item_to_place.bigkey: # a better test to see if a key can go there location.item = item_to_place location.event = True + if item_to_place.smallkey: + with suppress(ValueError): + key_pool.remove(item_to_place) test_state = max_exp_state.copy() test_state.stale[item_to_place.player] = True else: @@ -157,6 +159,8 @@ def verify_spot_to_fill(location, item_to_place, max_exp_state, single_player_pl if item_to_place.smallkey or item_to_place.bigkey: location.item = None location.event = False + if item_to_place.smallkey: + key_pool.append(item_to_place) return None @@ -394,11 +398,7 @@ def distribute_items_restrictive(world, gftower_trash=False, fill_locations=None random.shuffle(fill_locations) random.shuffle(world.itempool) - if world.item_pool_config.preferred: - pref = list(world.item_pool_config.preferred.keys()) - pref_len = len(pref) - world.itempool.sort(key=lambda i: pref_len - pref.index((i.name, i.player)) - if (i.name, i.player) in world.item_pool_config.preferred else 0) + config_sort(world) progitempool = [item for item in world.itempool if item.advancement] prioitempool = [item for item in world.itempool if not item.advancement and item.priority] restitempool = [item for item in world.itempool if not item.advancement and not item.priority] @@ -496,6 +496,20 @@ def distribute_items_restrictive(world, gftower_trash=False, fill_locations=None ensure_good_pots(world) +def config_sort(world): + if world.item_pool_config.verify: + config_sort_helper(world, world.item_pool_config.verify) + elif world.item_pool_config.preferred: + config_sort_helper(world, world.item_pool_config.preferred) + + +def config_sort_helper(world, sort_dict): + pref = list(sort_dict.keys()) + pref_len = len(pref) + world.itempool.sort(key=lambda i: pref_len - pref.index((i.name, i.player)) + if (i.name, i.player) in sort_dict else 0) + + def calc_trash_locations(world, player): total_count, gt_count = 0, 0 for loc in world.get_locations(): @@ -667,6 +681,40 @@ def sell_keys(world, player): world.itempool.remove(universal_key) +def verify(item_to_place, item_locations, state, spp, pac, key_pool, world): + if world.item_pool_config.verify: + logger = logging.getLogger('') + item_name = 'Bottle' if item_to_place.name.startswith('Bottle') else item_to_place.name + item_player = item_to_place.player + config = world.item_pool_config + if (item_name, item_player) in config.verify: + tests = config.verify[(item_name, item_player)] + issues = [] + for location in item_locations: + if location.name in tests: + expected = tests[location.name] + spot = verify_spot_to_fill(location, item_to_place, state, spp, pac, key_pool, world) + if spot and (item_to_place.smallkey or item_to_place.bigkey): + location.item = None + location.event = False + if item_to_place.smallkey: + key_pool.append(item_to_place) + if (expected and spot) or (not expected and spot is None): + logger.debug(f'Placing {item_name} ({item_player}) at {location.name} was {expected}') + config.verify_count += 1 + if config.verify_count >= config.verify_target: + exit() + else: + issues.append((item_name, item_player, location.name, expected)) + if len(issues) > 0: + for name, player, loc, expected in issues: + if expected: + logger.error(f'Could not place {name} ({player}) at {loc}') + else: + logger.error(f'{name} ({player}) should not be allowed at {loc}') + raise Exception(f'Test failed placing {name}') + + def balance_multiworld_progression(world): state = CollectionState(world) checked_locations = set() diff --git a/ItemList.py b/ItemList.py index cac01f3f..b708b284 100644 --- a/ItemList.py +++ b/ItemList.py @@ -1431,6 +1431,13 @@ def fill_specific_items(world): item_player = player if len(item_parts) < 2 else int(item_parts[1]) item_name = item_parts[0] world.item_pool_config.preferred[(item_name, item_player)] = placement['locations'] + elif placement['type'] == 'Verification': + item = placement['item'] + item_parts = item.split('#') + item_player = player if len(item_parts) < 2 else int(item_parts[1]) + item_name = item_parts[0] + world.item_pool_config.verify[(item_name, item_player)] = placement['locations'] + world.item_pool_config.verify_target += len(placement['locations']) def get_item_and_event_flag(item, world, player, dungeon_pool, prize_set, prize_pool): diff --git a/README.md b/README.md index 2cd2b881..c7fd99bd 100644 --- a/README.md +++ b/README.md @@ -158,12 +158,11 @@ CLI: `--trap_door_mode [vanilla|boss|oneway]` Determines how small key door logic works. -* Loose: Skips placement rules checks. Currently, experimental to see what kinds of problems can arise. * Default: Current key logic. Assumes worse case usage, placement checks, but assumes you can't get to a chest until you have sufficient keys. (May assume items are unreachable) * Partial Protection: Assumes you always have full inventory and worse case usage. This should account for dark room and bunny revival glitches. * Strict: For those would like to glitch and be protected from yourselves. Small keys door require all small keys to be available to be in logic. -CLI: `--key_logic [loose|default|partial|strict]` +CLI: `--key_logic [default|partial|strict]` ### Decouple Doors diff --git a/Rules.py b/Rules.py index a550a9d4..ed6367dd 100644 --- a/Rules.py +++ b/Rules.py @@ -2076,13 +2076,16 @@ bunny_impassible_doors = { def add_key_logic_rules(world, player): key_logic = world.key_logic[player] + eval_func = eval_small_key_door + if world.key_logic_algorithm[player] == 'strict' and world.keyshuffle[player] == 'wild': + eval_func = eval_small_key_door_strict for d_name, d_logic in key_logic.items(): for door_name, rule in d_logic.door_rules.items(): door_entrance = world.get_entrance(door_name, player) - add_rule(door_entrance, eval_small_key_door(door_name, d_name, player)) + add_rule(door_entrance, eval_func(door_name, d_name, player)) if door_entrance.door.dependents: for dep in door_entrance.door.dependents: - add_rule(dep.entrance, eval_small_key_door(door_name, d_name, player)) + add_rule(dep.entrance, eval_func(door_name, d_name, player)) for location in d_logic.bk_restricted: if not location.forced_item: forbid_item(location, d_logic.bk_name, player) @@ -2129,10 +2132,24 @@ def eval_small_key_door_main(state, door_name, dungeon, player): return door_openable +def eval_small_key_door_strict_main(state, door_name, dungeon, player): + if state.is_door_open(door_name, player): + return True + key_layout = state.world.key_layout[player][dungeon] + number = key_layout.max_chests + if number <= 0: + return True + return state.has_sm_key_strict(key_layout.key_logic.small_key_name, player, number) + + def eval_small_key_door(door_name, dungeon, player): return lambda state: eval_small_key_door_main(state, door_name, dungeon, player) +def eval_small_key_door_strict(door_name, dungeon, player): + return lambda state: eval_small_key_door_strict_main(state, door_name, dungeon, player) + + def allow_big_key_in_big_chest(bk_name, player): return lambda state, item: item.name == bk_name and item.player == player diff --git a/mystery_example.yml b/mystery_example.yml index dc1a21a7..47d44e00 100644 --- a/mystery_example.yml +++ b/mystery_example.yml @@ -27,7 +27,6 @@ boss: 0 oneway: 0 key_logic_algorithm: - loose: 0 default: 1 partial: 0 strict: 0 diff --git a/resources/app/cli/args.json b/resources/app/cli/args.json index 0926a8c9..53eb2f45 100644 --- a/resources/app/cli/args.json +++ b/resources/app/cli/args.json @@ -202,7 +202,6 @@ }, "key_logic_algorithm": { "choices": [ - "loose", "default", "partial", "strict" diff --git a/resources/app/cli/lang/en.json b/resources/app/cli/lang/en.json index 8e574ee0..d27e69ab 100644 --- a/resources/app/cli/lang/en.json +++ b/resources/app/cli/lang/en.json @@ -247,7 +247,6 @@ ], "key_logic_algorithm": [ "Key Logic Algorithm (default: %(default)s)", - "loose: Allow more randomization", "default: Balance between safety and randomization", "partial: Partial protection when using certain minor glitches", "strict: Ensure small keys are available" diff --git a/resources/app/gui/lang/en.json b/resources/app/gui/lang/en.json index 3135572e..54cd4965 100644 --- a/resources/app/gui/lang/en.json +++ b/resources/app/gui/lang/en.json @@ -97,7 +97,6 @@ "randomizer.dungeon.trap_door_mode.oneway": "Remove Annoying Traps", "randomizer.dungeon.key_logic_algorithm": "Key Logic Algorithm", - "randomizer.dungeon.key_logic_algorithm.loose": "Loose", "randomizer.dungeon.key_logic_algorithm.default": "Default", "randomizer.dungeon.key_logic_algorithm.partial": "Partial Protection", "randomizer.dungeon.key_logic_algorithm.strict": "Strict", diff --git a/resources/app/gui/randomize/dungeon/widgets.json b/resources/app/gui/randomize/dungeon/widgets.json index cecfeeff..d442ae21 100644 --- a/resources/app/gui/randomize/dungeon/widgets.json +++ b/resources/app/gui/randomize/dungeon/widgets.json @@ -57,7 +57,6 @@ "type": "selectbox", "default": "default", "options": [ - "loose", "default", "partial", "strict" diff --git a/source/item/FillUtil.py b/source/item/FillUtil.py index 07f36ac8..8c85bfdd 100644 --- a/source/item/FillUtil.py +++ b/source/item/FillUtil.py @@ -20,6 +20,9 @@ class ItemPoolConfig(object): self.reserved_locations = defaultdict(set) self.restricted = {} self.preferred = {} + self.verify = {} + self.verify_count = 0 + self.verify_target = 0 self.recorded_choices = [] @@ -435,6 +438,9 @@ def filter_locations(item_to_place, locations, world, vanilla_skip=False, potion if (item_name, item_to_place.player) in config.preferred: locs = config.preferred[(item_name, item_to_place.player)] return sorted(locations, key=lambda l: 0 if l.name in locs else 1) + if (item_name, item_to_place.player) in config.verify: + locs = config.verify[(item_name, item_to_place.player)].keys() + return sorted(locations, key=lambda l: 0 if l.name in locs else 1) return locations diff --git a/test/NewTestSuite.py b/test/NewTestSuite.py new file mode 100644 index 00000000..aca4e796 --- /dev/null +++ b/test/NewTestSuite.py @@ -0,0 +1,126 @@ +import fnmatch +import os +import subprocess +import sys +import multiprocessing +import concurrent.futures +import argparse +from collections import OrderedDict + +cpu_threads = multiprocessing.cpu_count() +py_version = f"{sys.version_info.major}.{sys.version_info.minor}" + + +def main(args=None): + successes = [] + errors = [] + task_mapping = [] + tests = OrderedDict() + + successes.append(f"Testing DR (NewTestSuite)") + print(successes[0]) + + # max_attempts = args.count + pool = concurrent.futures.ThreadPoolExecutor(max_workers=cpu_threads) + dead_or_alive = 0 + alive = 0 + + def test(test_name: str, command: str, test_file: str): + tests[test_name] = [command] + + base_command = f"python3.8 DungeonRandomizer.py --suppress_rom --suppress_spoiler" + + def gen_seed(): + task_command = base_command + " " + command + return subprocess.run(task_command, capture_output=True, shell=True, text=True) + + task = pool.submit(gen_seed) + task.success = False + task.name = test_name + task.test_file = test_file + task.cmd = base_command + " " + command + task_mapping.append(task) + + for test_suite, test_files in args.test_suite.items(): + for test_file in test_files: + test(test_suite, f'--customizer {os.path.join(test_suite, test_file)}', test_file) + + from tqdm import tqdm + with tqdm(concurrent.futures.as_completed(task_mapping), + total=len(task_mapping), unit="seed(s)", + desc=f"Success rate: 0.00%") as progressbar: + for task in progressbar: + dead_or_alive += 1 + try: + result = task.result() + if result.returncode: + errors.append([task.name + ' ' + task.test_file, task.cmd, result.stderr]) + else: + alive += 1 + task.success = True + except Exception as e: + raise e + + progressbar.set_description(f"Success rate: {(alive/dead_or_alive)*100:.2f}% - {task.name}") + + def get_results(testname: str): + result = "" + dead_or_alive = [task.success for task in task_mapping if task.name == testname] + alive = [x for x in dead_or_alive if x] + success = f"{testname} Rate: {(len(alive) / len(dead_or_alive)) * 100:.2f}%" + successes.append(success) + print(success) + result += f"{(len(alive)/len(dead_or_alive))*100:.2f}%\t" + return result.strip() + + results = [] + for t in tests.keys(): + results.append(get_results(t)) + + for result in results: + print(result) + successes.append(result) + + return successes, errors + + +if __name__ == "__main__": + successes = [] + + parser = argparse.ArgumentParser(add_help=False) + # parser.add_argument('--count', default=0, type=lambda value: max(int(value), 0)) + parser.add_argument('--cpu_threads', default=cpu_threads, type=lambda value: max(int(value), 1)) + parser.add_argument('--help', default=False, action='store_true') + + args = parser.parse_args() + + if args.help: + parser.print_help() + exit(0) + + cpu_threads = args.cpu_threads + + test_suites = {} + # not sure if it supports subdirectories properly yet + for root, dirnames, filenames in os.walk('test/suite'): + test_suites[root] = fnmatch.filter(filenames, '*.yaml') + + args = argparse.Namespace() + args.test_suite = test_suites + s, errors = main(args=args) + if successes: + successes += [""] * 2 + successes += s + print() + + if errors: + with open(f"new-test-suite-errors.txt", 'w') as stream: + for error in errors: + stream.write(error[0] + "\n") + stream.write(error[1] + "\n") + stream.write(error[2] + "\n\n") + + with open("new-test-suite-success.txt", "w") as stream: + stream.write(str.join("\n", successes)) + + input("Press enter to continue") diff --git a/test/suite/default_key_logic.yaml b/test/suite/default_key_logic.yaml new file mode 100644 index 00000000..e9f3e94b --- /dev/null +++ b/test/suite/default_key_logic.yaml @@ -0,0 +1,44 @@ +# Possible improvements: account for items that are possibly in logic +# Example: Mire Big Key in harmless means all 6 mire smalls required for fire-locked side, +# if you have access to harmless via: +# 2 pod smalls + bow, hammer or 3 pod small +meta: + players: 1 +settings: + 1: + key_logic_algorithm: default + keysanity: True + crystals_needed_for_gt: 0 # to skip trash fill +placements: + 1: + Hobo: Big Key (Misery Mire) + Waterfall Fairy - Left: Small Key (Misery Mire) + Waterfall Fairy - Right: Small Key (Misery Mire) + Palace of Darkness - Big Chest: Hammer +advanced_placements: + 1: + # Contrast with partial_2 + - type: Verification + item: Flippers + locations: + Misery Mire - Map Chest: True + Misery Mire - Main Lobby: True + Misery Mire - Bridge Chest: True + Misery Mire - Spike Chest: True + Misery Mire - Compass Chest: False + Misery Mire - Big Key Chest: False + Misery Mire - Boss: False + # Contrast with partial_3 + - type: Verification + item: Big Key (Ganons Tower) + locations: + Ganons Tower - Big Key Chest: True + Ganons Tower - Big Key Room - Left: True + Ganons Tower - Big Key Room - Right: True + Ganons Tower - Bob's Chest: True + # Normal logic doesn't allow this placement + # unless hammer is placed before it - no algorithm does this in non-keysanity, but possible in keysanity + - type: Verification + item: Small Key (Palace of Darkness) + locations: + Palace of Darkness - Dark Maze - Bottom: True \ No newline at end of file diff --git a/test/suite/partial_key_logic.yaml b/test/suite/partial_key_logic.yaml new file mode 100644 index 00000000..b5e951ed --- /dev/null +++ b/test/suite/partial_key_logic.yaml @@ -0,0 +1,24 @@ +# Even though Lamp is Flipper-locked, this logic considers that a key could be wasted in the dark in mire +# Only fire locked mire is off limits +meta: + players: 1 +settings: + 1: + key_logic_algorithm: partial + keysanity: true +placements: + 1: + Hobo: Lamp + Waterfall Fairy - Left: Small Key (Misery Mire) +advanced_placements: + 1: + - type: Verification + item: Flippers + locations: + Misery Mire - Map Chest: True + Misery Mire - Main Lobby: True + Misery Mire - Bridge Chest: True + Misery Mire - Spike Chest: True + Misery Mire - Compass Chest: False + Misery Mire - Big Key Chest: False + Misery Mire - Boss: False \ No newline at end of file diff --git a/test/suite/partial_key_logic_2.yaml b/test/suite/partial_key_logic_2.yaml new file mode 100644 index 00000000..a6f676e6 --- /dev/null +++ b/test/suite/partial_key_logic_2.yaml @@ -0,0 +1,26 @@ +# For contrast with default logic +# This logic is not yet smart enough to allow the crystal blocked chests with two keys (Spike Pot and one other) +meta: + players: 1 +settings: + 1: + key_logic_algorithm: partial + keysanity: True +placements: + 1: + Hobo: Lamp + Waterfall Fairy - Left: Small Key (Misery Mire) + Waterfall Fairy - Right: Small Key (Misery Mire) + Swamp Palace - Entrance: Big Key (Misery Mire) +advanced_placements: + 1: + - type: Verification + item: Flippers + locations: + Misery Mire - Map Chest: False + Misery Mire - Main Lobby: False + Misery Mire - Bridge Chest: True + Misery Mire - Spike Chest: True + Misery Mire - Compass Chest: False + Misery Mire - Big Key Chest: False + Misery Mire - Boss: False \ No newline at end of file diff --git a/test/suite/partial_key_logic_3.yaml b/test/suite/partial_key_logic_3.yaml new file mode 100644 index 00000000..774e4830 --- /dev/null +++ b/test/suite/partial_key_logic_3.yaml @@ -0,0 +1,160 @@ +# For contrast with default logic +# Examples of valid big key placement that doesn't work with pure worst case scenarios +# Basically chests that are obtainable two ways+ of spending keys +# (Possible fix: access to the extra door grants access to the mini helma key too) +meta: + players: 1 +settings: + 1: + key_logic_algorithm: partial + crystals_needed_for_gt: 0 # to skip trash fill +advanced_placements: + 1: + - type: Verification + item: Big Key (Desert Palace) + locations: + Desert Palace - Big Chest: False + Desert Palace - Big Key Chest: True + Desert Palace - Boss: False + Desert Palace - Compass Chest: True + Desert Palace - Map Chest: True + Desert Palace - Torch: True + - type: Verification + item: Big Key (Eastern Palace) + locations: + Eastern Palace - Big Chest: False + Eastern Palace - Big Key Chest: True + Eastern Palace - Boss: False + Eastern Palace - Cannonball Chest: True + Eastern Palace - Compass Chest: True + Eastern Palace - Map Chest: True + - type: Verification + item: Big Key (Ganons Tower) + locations: + # These four require not wasting keys upstairs because the big key is down here + Ganons Tower - Big Key Chest: False + Ganons Tower - Big Key Room - Left: False + Ganons Tower - Big Key Room - Right: False + Ganons Tower - Bob's Chest: False + # These are normal + Ganons Tower - Big Chest: False + Ganons Tower - Bob's Torch: True + Ganons Tower - Compass Room - Bottom Left: True + Ganons Tower - Compass Room - Bottom Right: True + Ganons Tower - Compass Room - Top Left: True + Ganons Tower - Compass Room - Top Right: True + Ganons Tower - DMs Room - Bottom Left: True + Ganons Tower - DMs Room - Bottom Right: True + Ganons Tower - DMs Room - Top Left: True + Ganons Tower - DMs Room - Top Right: True + Ganons Tower - Firesnake Room: True + Ganons Tower - Hope Room - Left: True + Ganons Tower - Hope Room - Right: True + Ganons Tower - Map Chest: True + Ganons Tower - Mini Helmasaur Room - Left: False + Ganons Tower - Mini Helmasaur Room - Right: False + Ganons Tower - Pre-Moldorm Chest: False + Ganons Tower - Randomizer Room - Bottom Left: True + Ganons Tower - Randomizer Room - Bottom Right: True + Ganons Tower - Randomizer Room - Top Left: True + Ganons Tower - Randomizer Room - Top Right: True + Ganons Tower - Tile Room: True + Ganons Tower - Validation Chest: False + - type: Verification + item: Big Key (Ice Palace) + locations: + Ice Palace - Big Chest: False + Ice Palace - Big Key Chest: True + Ice Palace - Boss: False + Ice Palace - Compass Chest: True + Ice Palace - Freezor Chest: True + Ice Palace - Iced T Room: True + Ice Palace - Map Chest: True + Ice Palace - Spike Room: True + - type: Verification + item: Big Key (Misery Mire) + locations: + Misery Mire - Big Chest: False + Misery Mire - Big Key Chest: True + Misery Mire - Boss: False + Misery Mire - Bridge Chest: True + Misery Mire - Compass Chest: True + Misery Mire - Main Lobby: True + Misery Mire - Map Chest: True + Misery Mire - Spike Chest: True + - type: Verification + item: Big Key (Palace of Darkness) + locations: + Palace of Darkness - Big Chest: False + Palace of Darkness - Big Key Chest: True + Palace of Darkness - Boss: False + Palace of Darkness - Compass Chest: True + Palace of Darkness - Dark Basement - Left: True + Palace of Darkness - Dark Basement - Right: True + Palace of Darkness - Dark Maze - Bottom: True + Palace of Darkness - Dark Maze - Top: True + Palace of Darkness - Harmless Hellway: True + Palace of Darkness - Map Chest: True + Palace of Darkness - Shooter Room: True + Palace of Darkness - Stalfos Basement: True + Palace of Darkness - The Arena - Bridge: True + Palace of Darkness - The Arena - Ledge: True + - type: Verification + item: Big Key (Skull Woods) + locations: + Skull Woods - Big Chest: True + Skull Woods - Big Key Chest: True + Skull Woods - Boss: True + Skull Woods - Bridge Room: True + Skull Woods - Compass Chest: True + Skull Woods - Map Chest: True + Skull Woods - Pinball Room: True + Skull Woods - Pot Prison: True + - type: Verification + item: Big Key (Swamp Palace) + locations: + Swamp Palace - Big Chest: True + Swamp Palace - Big Key Chest: True + Swamp Palace - Boss: True + Swamp Palace - Compass Chest: True + Swamp Palace - Entrance: False + Swamp Palace - Flooded Room - Left: True + Swamp Palace - Flooded Room - Right: True + Swamp Palace - Map Chest: True + Swamp Palace - Waterfall Room: True + Swamp Palace - West Chest: True + - type: Verification + item: Big Key (Thieves Town) + locations: + Thieves' Town - Ambush Chest: True + Thieves' Town - Attic: False + Thieves' Town - Big Chest: False + Thieves' Town - Big Key Chest: True + Thieves' Town - Blind's Cell: False + Thieves' Town - Boss: False + Thieves' Town - Compass Chest: True + Thieves' Town - Map Chest: True + - type: Verification + item: Big Key (Tower of Hera) + locations: + Tower of Hera - Basement Cage: True + Tower of Hera - Big Chest: False + Tower of Hera - Big Key Chest: True + Tower of Hera - Boss: False + Tower of Hera - Compass Chest: False + Tower of Hera - Map Chest: True + - type: Verification + item: Big Key (Turtle Rock) + locations: + Turtle Rock - Big Chest: False + Turtle Rock - Big Key Chest: True + Turtle Rock - Boss: False + Turtle Rock - Chain Chomps: True + Turtle Rock - Compass Chest: True + Turtle Rock - Crystaroller Room: False + Turtle Rock - Eye Bridge - Bottom Left: False + Turtle Rock - Eye Bridge - Bottom Right: False + Turtle Rock - Eye Bridge - Top Left: False + Turtle Rock - Eye Bridge - Top Right: False + Turtle Rock - Roller Room - Left: True + Turtle Rock - Roller Room - Right: True \ No newline at end of file diff --git a/test/suite/strict_key_logic.yaml b/test/suite/strict_key_logic.yaml new file mode 100644 index 00000000..89eb11c0 --- /dev/null +++ b/test/suite/strict_key_logic.yaml @@ -0,0 +1,22 @@ +meta: + players: 1 +settings: + 1: + key_logic_algorithm: strict + keysanity: true +placements: + 1: + Hobo: Big Key (Misery Mire) + Waterfall Fairy - Left: Small Key (Misery Mire) +advanced_placements: + 1: + - type: Verification + item: Flippers + locations: + Misery Mire - Map Chest: False + Misery Mire - Main Lobby: False + Misery Mire - Bridge Chest: True + Misery Mire - Spike Chest: True + Misery Mire - Compass Chest: False + Misery Mire - Big Key Chest: False + Misery Mire - Boss: False \ No newline at end of file From 9a71e56546ca0110370fa61c38ea998893faa616 Mon Sep 17 00:00:00 2001 From: aerinon Date: Fri, 17 Feb 2023 16:55:35 -0700 Subject: [PATCH 15/15] Trap door refinement with "optional" value versus "vanilla" Slight balance of chaos mode Warping Pool trap no longer shuffled --- BaseClasses.py | 7 +- CLI.py | 2 +- DoorShuffle.py | 65 ++++++++++--------- DungeonGenerator.py | 2 +- Main.py | 2 +- README.md | 17 +++-- RELEASENOTES.md | 10 ++- mystery_testsuite.yml | 20 +++++- resources/app/cli/args.json | 1 + resources/app/cli/lang/en.json | 5 +- resources/app/gui/lang/en.json | 5 +- .../app/gui/randomize/dungeon/widgets.json | 8 ++- source/dungeon/DungeonStitcher.py | 32 ++++++++- 13 files changed, 123 insertions(+), 53 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index ca44b0a3..dd39cfc9 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -146,7 +146,7 @@ class World(object): set_player_attr('pot_pool', {}) set_player_attr('decoupledoors', False) set_player_attr('door_type_mode', 'original') - set_player_attr('trap_door_mode', 'vanilla') + set_player_attr('trap_door_mode', 'optional') set_player_attr('key_logic_algorithm', 'default') set_player_attr('shopsanity', False) @@ -1905,6 +1905,9 @@ class Door(object): return world.get_room(self.roomIndex, self.player).kind(self) return None + def dungeon_name(self): + return self.entrance.parent_region.dungeon.name if self.entrance.parent_region.dungeon else 'Cave' + def __eq__(self, other): return isinstance(other, self.__class__) and self.name == other.name @@ -2962,7 +2965,7 @@ bow_mode = {'progressive': 0, 'silvers': 1, 'retro': 2, 'retro_silvers': 3} # additions # byte 12: POOT TKKK (pseudoboots, overworld_map, trap_door_mode, key_logic_algo) overworld_map_mode = {'default': 0, 'compass': 1, 'map': 2} -trap_door_mode = {'vanilla': 0, 'boss': 1, 'oneway': 2} +trap_door_mode = {'vanilla': 0, 'optional': 1, 'boss': 2, 'oneway': 3} key_logic_algo = {'default': 0, 'partial': 1, 'strict': 2} # sfx_shuffle and other adjust items does not affect settings code diff --git a/CLI.py b/CLI.py index 0df0d03a..536543f2 100644 --- a/CLI.py +++ b/CLI.py @@ -215,7 +215,7 @@ def parse_settings(): 'door_shuffle': 'vanilla', 'intensity': 2, 'door_type_mode': 'original', - 'trap_door_mode': 'vanilla', + 'trap_door_mode': 'optional', 'key_logic_algorithm': 'default', 'decoupledoors': False, 'experimental': False, diff --git a/DoorShuffle.py b/DoorShuffle.py index f1457b36..308a0731 100644 --- a/DoorShuffle.py +++ b/DoorShuffle.py @@ -1760,12 +1760,12 @@ class DoorTypePool: self.tricky += counts[6] def chaos_shuffle(self, counts): - weights = [1, 2, 4, 3, 2, 1] + weights = [1, 2, 4, 3, 2] return [random.choices(self.get_choices(counts[i]), weights=weights)[0] for i, c in enumerate(counts)] @staticmethod def get_choices(number): - return [max(number+i, 0) for i in range(-1, 5)] + return [max(number+i, 0) for i in range(-1, 4)] class BuilderDoorCandidates: @@ -1801,14 +1801,17 @@ def shuffle_trap_doors(door_type_pools, paths, start_regions_map, world, player) ttl = 0 suggestion_map, trap_map, flex_map = {}, {}, {} remaining = door_type_pool.traps - if player in world.custom_door_types: + if player in world.custom_door_types and 'Trap Door' in world.custom_door_types[player]: custom_trap_doors = world.custom_door_types[player]['Trap Door'] else: custom_trap_doors = defaultdict(list) for dungeon in pool: builder = world.dungeon_layouts[player][dungeon] - find_trappable_candidates(builder, world, player) # todo: + if 'Mire Warping Pool' in builder.master_sector.region_set(): + custom_trap_doors[dungeon].append(world.get_door('Mire Warping Pool ES', player)) + world.custom_door_types[player]['Trap Door'] = custom_trap_doors + find_trappable_candidates(builder, world, player) if custom_trap_doors[dungeon]: builder.candidates.trap = filter_key_door_pool(builder.candidates.trap, custom_trap_doors[dungeon]) remaining -= len(custom_trap_doors[dungeon]) @@ -1852,6 +1855,10 @@ def shuffle_trap_doors(door_type_pools, paths, start_regions_map, world, player) # time to re-assign else: trap_map = {dungeon: [] for dungeon in pool} + for dungeon in pool: + builder = world.dungeon_layouts[player][dungeon] + if 'Mire Warping Pool' in builder.master_sector.region_set(): + trap_map[dungeon].append(world.get_door('Mire Warping Pool ES', player)) reassign_trap_doors(trap_map, world, player) for name, traps in trap_map.items(): used_doors.update(traps) @@ -1863,7 +1870,7 @@ def shuffle_big_key_doors(door_type_pools, used_doors, start_regions_map, world, ttl = 0 suggestion_map, bk_map, flex_map = {}, {}, {} remaining = door_type_pool.bigs - if player in world.custom_door_types: + if player in world.custom_door_types and 'Big Key Door' in world.custom_door_types[player]: custom_bk_doors = world.custom_door_types[player]['Big Key Door'] else: custom_bk_doors = defaultdict(list) @@ -1925,7 +1932,7 @@ def shuffle_small_key_doors(door_type_pools, used_doors, start_regions_map, worl suggestion_map, small_map, flex_map = {}, {}, {} remaining = door_type_pool.smalls total_keys = remaining - if player in world.custom_door_types: + if player in world.custom_door_types and 'Key Door' in world.custom_door_types[player]: custom_key_doors = world.custom_door_types[player]['Key Door'] else: custom_key_doors = defaultdict(list) @@ -2025,7 +2032,7 @@ def shuffle_bomb_dash_doors(door_type_pools, used_doors, start_regions_map, worl remaining_bomb = door_type_pool.bombable remaining_dash = door_type_pool.dashable - if player in world.custom_door_types: + if player in world.custom_door_types and 'Bomb Door' in world.custom_door_types[player]: custom_bomb_doors = world.custom_door_types[player]['Bomb Door'] custom_dash_doors = world.custom_door_types[player]['Dash Door'] else: @@ -2164,7 +2171,7 @@ def find_trappable_candidates(builder, world, player): def find_valid_trap_combination(builder, suggested, start_regions, paths, world, player, drop=True): trap_door_pool = builder.candidates.trap trap_doors_needed = suggested - if player in world.custom_door_types: + if player in world.custom_door_types and 'Trap Door' in world.custom_door_types[player]: custom_trap_doors = world.custom_door_types[player]['Trap Door'][builder.name] else: custom_trap_doors = [] @@ -2319,20 +2326,16 @@ def reassign_trap_doors(trap_map, world, player): d.blocked = False for d in traps: change_door_to_trap(d, world, player) - world.spoiler.set_door_type(d.name, 'Trap Door', player) - logger.debug('Trap Door: %s', d.name) + world.spoiler.set_door_type(f'{d.name} ({d.dungeon_name()})', 'Trap Door', player) + logger.debug(f'Trap Door: {d.name} ({d.dungeon_name()})') def exclude_boss_traps(d): - return ' Boss ' not in d.name and ' Agahnim ' not in d.name and d.name not in ['Skull Spike Corner SW', - 'Mire Warping Pool ES'] - -def exclude_logic_traps(d): - return d.name != 'Mire Warping Pool ES' + return ' Boss ' not in d.name and ' Agahnim ' not in d.name and d.name not in ['Skull Spike Corner SW'] def find_current_trap_doors(builder, world, player): - checker = exclude_boss_traps if world.trap_door_mode[player] == 'vanilla' else exclude_logic_traps + checker = exclude_boss_traps if world.trap_door_mode[player] in ['vanilla', 'optional'] else (lambda x: True) current_doors = [] for region in builder.master_sector.regions: for ext in region.exits: @@ -2452,7 +2455,7 @@ def find_big_key_door_candidates(region, checked, used, world, player): def find_valid_bk_combination(builder, suggested, start_regions, world, player, drop=True): bk_door_pool = builder.candidates.big bk_doors_needed = suggested - if player in world.custom_door_types: + if player in world.custom_door_types and 'Big Key Door' in world.custom_door_types[player]: custom_bk_doors = world.custom_door_types[player]['Big Key Door'][builder.name] else: custom_bk_doors = [] @@ -2527,8 +2530,8 @@ def reassign_big_key_doors(bk_map, world, player): world.paired_doors[player].append(PairedDoor(d1.name, d2.name)) change_door_to_big_key(d1, world, player) change_door_to_big_key(d2, world, player) - world.spoiler.set_door_type(d1.name+' <-> '+d2.name, 'Big Key Door', player) - logger.debug(f'Big Key Door: {d1.name} <-> {d2.name}') + world.spoiler.set_door_type(f'{d1.name} <-> {d2.name} ({d1.dungeon_name()})', 'Big Key Door', player) + logger.debug(f'Big Key Door: {d1.name} <-> {d2.name} ({d1.dungeon_name()})') else: d = obj if d.type is DoorType.Interior: @@ -2545,8 +2548,8 @@ def reassign_big_key_doors(bk_map, world, player): if stateful_door(d.dest, dest_room.kind(d.dest)): change_door_to_big_key(d.dest, world, player) add_pair(d, d.dest, world, player) - world.spoiler.set_door_type(d.name, 'Big Key Door', player) - logger.debug(f'Big Key Door: {d.name}') + world.spoiler.set_door_type(f'{d.name} ({d.dungeon_name()})', 'Big Key Door', player) + logger.debug(f'Big Key Door: {d.name} ({d.dungeon_name()})') def change_door_to_big_key(d, world, player): @@ -2596,7 +2599,7 @@ def find_valid_combination(builder, target, start_regions, world, player, drop_k logger = logging.getLogger('') key_door_pool = list(builder.candidates.small) key_doors_needed = target - if player in world.custom_door_types: + if player in world.custom_door_types and 'Key Door' in world.custom_door_types[player]: custom_key_doors = world.custom_door_types[player]['Key Door'][builder.name] else: custom_key_doors = [] @@ -2724,7 +2727,7 @@ def find_valid_bd_combination(builder, suggested, world, player): bd_door_pool = builder.candidates.bomb_dash bomb_doors_needed, dash_doors_needed = suggested ttl_needed = bomb_doors_needed + dash_doors_needed - if player in world.custom_door_types: + if player in world.custom_door_types and 'Bomb Door' in world.custom_door_types[player]: custom_bomb_doors = world.custom_door_types[player]['Bomb Door'][builder.name] custom_dash_doors = world.custom_door_types[player]['Dash Door'][builder.name] else: @@ -2800,7 +2803,7 @@ def do_bombable_dashable(proposal, kind, world, player): change_door_to_kind(d1, kind, world, player) change_door_to_kind(d2, kind, world, player) spoiler_type = 'Bomb Door' if kind == DoorKind.Bombable else 'Dash Door' - world.spoiler.set_door_type(d1.name+' <-> '+d2.name, spoiler_type, player) + world.spoiler.set_door_type(f'{d1.name} <-> {d2.name} ({d1.dungeon_name()})', spoiler_type, player) else: d = obj if d.type is DoorType.Interior: @@ -2814,7 +2817,7 @@ def do_bombable_dashable(proposal, kind, world, player): change_door_to_kind(d.dest, kind, world, player) add_pair(d, d.dest, world, player) spoiler_type = 'Bomb Door' if kind == DoorKind.Bombable else 'Dash Door' - world.spoiler.set_door_type(d.name, spoiler_type, player) + world.spoiler.set_door_type(f'{d.name} ({d.dungeon_name()})', spoiler_type, player) def find_current_bd_doors(builder, world): @@ -3017,8 +3020,8 @@ def reassign_key_doors(small_map, world, player): world.paired_doors[player].append(PairedDoor(d1.name, d2.name)) change_door_to_small_key(d1, world, player) change_door_to_small_key(d2, world, player) - world.spoiler.set_door_type(d1.name+' <-> '+d2.name, 'Key Door', player) - logger.debug('Key Door: %s', d1.name+' <-> '+d2.name) + world.spoiler.set_door_type(f'{d1.name} <-> {d2.name} ({d1.dungeon_name()})', 'Key Door', player) + logger.debug(f'Key Door: {d1.name} <-> {d2.name} ({d1.dungeon_name()})') else: d = obj if d.type is DoorType.Interior: @@ -3034,8 +3037,8 @@ def reassign_key_doors(small_map, world, player): if stateful_door(d.dest, dest_room.kind(d.dest)): change_door_to_small_key(d.dest, world, player) add_pair(d, d.dest, world, player) - world.spoiler.set_door_type(d.name, 'Key Door', player) - logger.debug('Key Door: %s', d.name) + world.spoiler.set_door_type(f'{d.name} ({d.dungeon_name()})', 'Key Door', player) + logger.debug(f'Key Door: {d.name} ({d.dungeon_name()})') def change_door_to_small_key(d, world, player): @@ -3225,7 +3228,7 @@ def change_pair_type(door, new_type, world, player): room_b.change(door.dest.doorListPos, new_type) add_pair(door, door.dest, world, player) spoiler_type = 'Bomb Door' if new_type == DoorKind.Bombable else 'Dash Door' - world.spoiler.set_door_type(door.name + ' <-> ' + door.dest.name, spoiler_type, player) + world.spoiler.set_door_type(f'{door.name} <-> {door.dest.name} ({door.dungeon_name()})', spoiler_type, player) def remove_pair_type_if_present(door, world, player): @@ -4578,7 +4581,7 @@ door_type_counts = { 'Agahnims Tower': (4, 0, 1, 0, 0, 1, 0), 'Swamp Palace': (6, 0, 0, 2, 0, 0, 0), 'Palace of Darkness': (6, 1, 1, 3, 2, 0, 0), - 'Misery Mire': (6, 3, 4, 2, 0, 0, 0), + 'Misery Mire': (6, 3, 5, 2, 0, 0, 0), 'Skull Woods': (5, 0, 1, 2, 0, 1, 0), 'Ice Palace': (6, 1, 3, 0, 0, 0, 0), 'Tower of Hera': (1, 1, 0, 0, 0, 0, 0), diff --git a/DungeonGenerator.py b/DungeonGenerator.py index 7881df0a..9be244f8 100644 --- a/DungeonGenerator.py +++ b/DungeonGenerator.py @@ -3565,7 +3565,7 @@ def check_for_valid_layout(builder, sector_list, builder_info): builder.exception_list = list(sector_list) return True, {}, package except (GenerationException, NeutralizingException, OtherGenException) as e: - logging.getLogger('').info(f'Bailing on this layout for', e) + logging.getLogger('').info(f'Bailing on this layout for {builder.name}', exc_info=1) builder.split_dungeon_map = None builder.valid_proposal = None if temp_builder.name == 'Hyrule Castle' and temp_builder.throne_door: diff --git a/Main.py b/Main.py index 83e14f47..a39fbe57 100644 --- a/Main.py +++ b/Main.py @@ -34,7 +34,7 @@ from source.overworld.EntranceShuffle2 import link_entrances_new from source.tools.BPS import create_bps_from_data from source.classes.CustomSettings import CustomSettings -__version__ = '1.2.0.7-u' +__version__ = '1.2.0.8-u' from source.classes.BabelFish import BabelFish diff --git a/README.md b/README.md index c7fd99bd..6af52f44 100644 --- a/README.md +++ b/README.md @@ -139,20 +139,25 @@ Four options here, and all of them only take effect if Dungeon Door Shuffle is n * Small Key Doors, Bomb Doors, Dash Doors: This is what was normally shuffled previously * Adds Big Keys Doors: Big key doors are now shuffled in addition to those above, and Big Key doors are enabled to be on in both vertical directions thanks to a graphic that ended up on the cutting room floor. This does change -* Adds Trap Doors: All trap doors that are permanently shut in vanilla are shuffled. +* Adds Trap Doors: All trap doors that are permanently shut in vanilla are shuffled, excluding those by bosses. * Increases all Door Types: This is a chaos mode where each door type per dungeon is randomized between 1 less and 4 more. CLI: `--door_type_mode [original|big|all|chaos]` ### Trap Door Removal -Three options here for making dungeon traversal nicer. Only applies if door shuffle is not vanilla. +Options here for making dungeon traversal nicer. Only applies if door shuffle is not vanilla. -* Normal: This does not remove any trap doors. Note that boss trap doors are never shuffled in this mode. -* Remove Boss Traps: Boss traps are removed this includes the one near Mothula. -* Remove All Annoying Traps: This removes all trap doors that are annoying, including boss traps. Note, that the trap door near the mire cutscene chest is left alone because it enforces the use of fire to get to the chest. +* No Removal: This does not remove any trap doors. +* Removed If Blocking Path: Dungeon generation is relaxed to allow annoying trap doors to be removed if necessary. Note that boss trap doors are never shuffled in this mode. +* Remove Boss Traps: Boss traps are removed, this includes the one near Mothula. +* Remove All Annoying Traps: This removes all trap doors that are annoying, including boss traps. -CLI: `--trap_door_mode [vanilla|boss|oneway]` +If trap doors are shuffled the first two option behave the same. The last option overrides the shuffle because there is nothing left to shuffle. Boss traps are never shuffled. + +In all cases, that the trap door near the mire cutscene chest (Mire Warping Pool ES) is left alone because it enforces the use of fire to get to the chest. + +CLI: `--trap_door_mode [vanilla|optional|boss|oneway]` ### Key Logic Algorithm diff --git a/RELEASENOTES.md b/RELEASENOTES.md index efa12d9f..0328487c 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -109,8 +109,14 @@ These are now independent of retro mode and have three options: None, Random, an # Bug Fixes and Notes * 1.2.0.8-u - * Removed a Triforce text - * Fix for Desert Tiles 1 key door + * New Features: trap_door_mode and key_logic_algorithm + * Change S&Q in door shuffle + standard during escape to spawn as Uncle + * Fix for vanilla doors + certain ER modes + * Fix for unintentional decoupled door in standard + * Fix a problem with BK doors being one-sided + * Change to how wilds keys are placed in standard, better randomization + * Removed a Triforce text + * Fix for Desert Tiles 1 key door * 1.2.0.7-u * Fix for some misery mire key logic * Minor standard generation fix diff --git a/mystery_testsuite.yml b/mystery_testsuite.yml index aca535c3..f919b7cc 100644 --- a/mystery_testsuite.yml +++ b/mystery_testsuite.yml @@ -7,12 +7,30 @@ algorithm: district: 1 door_shuffle: vanilla: 1 - basic: 2 + basic: 1 + partitioned: 1 crossed: 3 # crossed yield more errors so is preferred intensity: 1: 1 2: 1 3: 2 # intensity 3 usually yield more errors +door_type_mode: + original: 2 + big: 2 + all: 1 + chaos: 1 +trap_door_mode: + vanilla: 3 # more errors + optional: 1 + boss: 1 + oneway: 1 +key_logic_algorithm: + default: 1 + partial: 0 + strict: 0 +decoupledoors: + off: 9 # more strict + on: 1 dropshuffle: on: 1 off: 1 diff --git a/resources/app/cli/args.json b/resources/app/cli/args.json index 53eb2f45..f85e51cd 100644 --- a/resources/app/cli/args.json +++ b/resources/app/cli/args.json @@ -196,6 +196,7 @@ "trap_door_mode": { "choices": [ "vanilla", + "optional", "boss", "oneway" ] diff --git a/resources/app/cli/lang/en.json b/resources/app/cli/lang/en.json index d27e69ab..4d5fb151 100644 --- a/resources/app/cli/lang/en.json +++ b/resources/app/cli/lang/en.json @@ -242,8 +242,9 @@ "trap_door_mode" : [ "Trap Door Removal (default: %(default)s)", "vanilla: No trap door removal", - "boss: Remove boss traps", - "oneway: Remove annoying trap doors" + "optional: Trap doors removed if blocking", + "boss: Also remove boss traps", + "oneway: Remove all annoying trap doors" ], "key_logic_algorithm": [ "Key Logic Algorithm (default: %(default)s)", diff --git a/resources/app/gui/lang/en.json b/resources/app/gui/lang/en.json index 54cd4965..b8166baa 100644 --- a/resources/app/gui/lang/en.json +++ b/resources/app/gui/lang/en.json @@ -93,8 +93,9 @@ "randomizer.dungeon.trap_door_mode": "Trap Door Removal", "randomizer.dungeon.trap_door_mode.vanilla": "No Removal", - "randomizer.dungeon.trap_door_mode.boss": "Remove Boss Traps", - "randomizer.dungeon.trap_door_mode.oneway": "Remove Annoying Traps", + "randomizer.dungeon.trap_door_mode.optional": "Removed If Blocking Path", + "randomizer.dungeon.trap_door_mode.boss": "Also Remove Boss Traps", + "randomizer.dungeon.trap_door_mode.oneway": "Remove All Annoying Traps", "randomizer.dungeon.key_logic_algorithm": "Key Logic Algorithm", "randomizer.dungeon.key_logic_algorithm.default": "Default", diff --git a/resources/app/gui/randomize/dungeon/widgets.json b/resources/app/gui/randomize/dungeon/widgets.json index d442ae21..9749486e 100644 --- a/resources/app/gui/randomize/dungeon/widgets.json +++ b/resources/app/gui/randomize/dungeon/widgets.json @@ -46,12 +46,16 @@ }, "trap_door_mode": { "type": "selectbox", - "default": "vanilla", + "default": "optional", "options": [ "vanilla", + "optional", "boss", "oneway" - ] + ], + "config": { + "width": 30 + } }, "key_logic_algorithm": { "type": "selectbox", diff --git a/source/dungeon/DungeonStitcher.py b/source/dungeon/DungeonStitcher.py index 8052b728..1eac3fb1 100644 --- a/source/dungeon/DungeonStitcher.py +++ b/source/dungeon/DungeonStitcher.py @@ -631,6 +631,7 @@ class ExplorationState(object): elif not self.in_door_list(door, self.avail_doors): self.append_door_to_list(door, self.avail_doors, flag) + # same as above but traps are ignored, and flag is not used def add_all_doors_check_proposed_2(self, region, proposed_map, valid_doors, world, player): for door in get_doors(world, region, player): if door in proposed_map and door.name in valid_doors: @@ -651,6 +652,27 @@ class ExplorationState(object): elif not self.in_door_list(door, self.avail_doors): self.append_door_to_list(door, self.avail_doors) + # same as above but traps are checked for + def add_all_doors_check_proposed_3(self, region, proposed_map, valid_doors, world, player): + for door in get_doors(world, region, player): + if door in proposed_map and door.name in valid_doors: + self.visited_doors.add(door) + if self.can_traverse(door): + if door.controller is not None: + door = door.controller + if door.dest is None and door not in proposed_map.keys() and door.name in valid_doors: + if not self.in_door_list_ic(door, self.unattached_doors): + self.append_door_to_list(door, self.unattached_doors) + else: + other = self.find_door_in_list(door, self.unattached_doors) + if self.crystal != other.crystal: + other.crystal = CrystalBarrier.Either + elif door.req_event is not None and door.req_event not in self.events and not self.in_door_list(door, + self.event_doors): + self.append_door_to_list(door, self.event_doors) + elif not self.in_door_list(door, self.avail_doors): + self.append_door_to_list(door, self.avail_doors) + def add_all_doors_check_proposed_traps(self, region, proposed_traps, world, player): for door in get_doors(world, region, player): if self.can_traverse_ignore_traps(door) and door not in proposed_traps: @@ -837,7 +859,10 @@ def extend_reachable_state_lenient(search_regions, state, proposed_map, all_regi local_state = state.copy() for region in search_regions: local_state.visit_region(region) - local_state.add_all_doors_check_proposed_2(region, proposed_map, valid_doors, world, player) + if world.trap_door_mode[player] == 'vanilla': + local_state.add_all_doors_check_proposed_3(region, proposed_map, valid_doors, world, player) + else: + local_state.add_all_doors_check_proposed_2(region, proposed_map, valid_doors, world, player) while len(local_state.avail_doors) > 0: explorable_door = local_state.next_avail_door() if explorable_door.door in proposed_map: @@ -848,7 +873,10 @@ def extend_reachable_state_lenient(search_regions, state, proposed_map, all_regi if (valid_region_to_explore_in_regions(connect_region, all_regions, world, player) and not local_state.visited(connect_region)): local_state.visit_region(connect_region) - local_state.add_all_doors_check_proposed_2(connect_region, proposed_map, valid_doors, world, player) + if world.trap_door_mode[player] == 'vanilla': + local_state.add_all_doors_check_proposed_3(connect_region, proposed_map, valid_doors, world, player) + else: + local_state.add_all_doors_check_proposed_2(connect_region, proposed_map, valid_doors, world, player) return local_state