diff --git a/BaseClasses.py b/BaseClasses.py index 83d04e74..c0248d11 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -1499,6 +1499,7 @@ class Portal(object): self.boss_exit_idx = boss_exit_idx self.default = True self.destination = False + self.dependent = None self.deadEnd = False self.light_world = False diff --git a/CLI.py b/CLI.py index 087bc739..35204b65 100644 --- a/CLI.py +++ b/CLI.py @@ -2,8 +2,6 @@ import argparse import copy import json import os -import logging -import random import textwrap import shlex import sys diff --git a/DoorShuffle.py b/DoorShuffle.py index 2c6ef833..59e412ef 100644 --- a/DoorShuffle.py +++ b/DoorShuffle.py @@ -462,7 +462,12 @@ def analyze_portals(world, player): if len(possible_portals) == 1: world.get_portal(possible_portals[0], player).destination = True elif len(possible_portals) > 1: - world.get_portal(random.choice(possible_portals), player).destination = True + dest_portal = random.choice(possible_portals) + access_portal = world.get_portal(dest_portal, player) + access_portal.destination = True + for other_portal in possible_portals: + if other_portal != dest_portal: + world.get_portal(dest_portal, player).dependent = access_portal def connect_portal(portal, world, player): @@ -560,7 +565,7 @@ def create_dungeon_entrances(world, player): for key, portal_list in dungeon_portals.items(): if key in dungeon_drops.keys(): entrance_map[key].extend(dungeon_drops[key]) - if key in split_portals.keys() and world.intensity[player] >= 3: + if key in split_portals.keys(): dead_ends = [] destinations = [] the_rest = [] @@ -586,10 +591,12 @@ def create_dungeon_entrances(world, player): p_entrance = portal.door.entrance r_name = p_entrance.parent_region.name split_map[key][choice].append(r_name) - originating[key][choice][p_entrance.connected_region.name] = None + entrance_region = find_entrance_region(portal) + originating[key][choice][entrance_region.name] = None dest_choices = [x for x in choices if len(split_map[key][x]) > 0] for portal in destinations: - restricted = portal.door.entrance.connected_region.name in world.inaccessible_regions[player] + entrance_region = find_entrance_region(portal) + restricted = entrance_region.name in world.inaccessible_regions[player] if restricted: filtered_choices = [x for x in choices if any(y not in world.inaccessible_regions[player] for y in originating[key][x].keys())] else: @@ -604,15 +611,16 @@ def create_dungeon_entrances(world, player): portal = world.get_portal(portal_name, player) r_name = portal.door.entrance.parent_region.name entrance_map[key].append(r_name) - if key in split_portals.keys(): - for split_key in split_portals[key]: - if split_key not in split_map[key]: - split_map[key][split_key] = [] - if world.intensity[player] < 3: - split_map[key][split_portal_defaults[key][r_name]].append(r_name) return entrance_map, split_map +def find_entrance_region(portal): + for entrance in portal.door.entrance.connected_region.entrances: + if entrance.parent_region.type != RegionType.Dungeon: + return entrance.parent_region + return None + + # def unpair_all_doors(world, player): # for paired_door in world.paired_doors[player]: # paired_door.pair = False diff --git a/DungeonGenerator.py b/DungeonGenerator.py index 5887a882..1ce3c637 100644 --- a/DungeonGenerator.py +++ b/DungeonGenerator.py @@ -2750,14 +2750,14 @@ def split_dungeon_builder(builder, split_list, builder_info): builder.split_dungeon_map[name].valid_proposal = proposal return builder.split_dungeon_map # we made this earlier in gen, just use it - attempts, comb_w_replace, merge_attempt = 0, None, False + attempts, comb_w_replace, merge_attempt, merge_limit = 0, None, 0, len(split_list) - 1 while attempts < 5: # does not solve coin flips 3% of the time try: candidate_sectors = dict.fromkeys(builder.sectors) global_pole = GlobalPolarity(candidate_sectors) dungeon_map, sub_builder, merge_keys = {}, None, [] - if merge_attempt: + if merge_attempt > 0: candidates = [] for name, split_entrances in split_list.items(): if len(split_entrances) > 1: @@ -2770,12 +2770,13 @@ def split_dungeon_builder(builder, split_list, builder_info): p = next(x for x in world.dungeon_portals[player] if x.door.entrance.parent_region.name == r_name) if not p.deadEnd: candidates.append(name) - merge_keys = random.sample(candidates, 2) if len(candidates) >= 2 else [] + merge_keys = random.sample(candidates, merge_attempt+1) if len(candidates) >= merge_attempt+1 else [] for name, split_entrances in split_list.items(): key = builder.name + ' ' + name if merge_keys and name in merge_keys: - other_key = builder.name + ' ' + [x for x in merge_keys if x != name][0] - if other_key in dungeon_map: + other_keys = [builder.name + ' ' + x for x in merge_keys if x != name] + other_key = next((x for x in other_keys if x in dungeon_map), None) + if other_key: key = other_key sub_builder = dungeon_map[other_key] sub_builder.all_entrances.extend(split_entrances) @@ -2791,8 +2792,8 @@ def split_dungeon_builder(builder, split_list, builder_info): attempts += 5 # all the combinations were tried already, no use repeating else: attempts += 1 - if attempts >= 5 and not merge_attempt: - merge_attempt, attempts = True, 0 + if attempts >= 5 and merge_attempt < merge_limit: + merge_attempt, attempts = merge_attempt + 1, 0 raise GenerationException('Unable to resolve in 5 attempts') diff --git a/InvertedRegions.py b/InvertedRegions.py index 338bb409..71c26fc9 100644 --- a/InvertedRegions.py +++ b/InvertedRegions.py @@ -1,12 +1,12 @@ import collections from BaseClasses import RegionType -from Regions import create_lw_region, create_dw_region, create_cave_region, create_dungeon_region +from Regions import create_lw_region, create_dw_region, create_cave_region, create_dungeon_region, create_menu_region def create_inverted_regions(world, player): world.regions += [ - create_dw_region(player, 'Menu', None, ['Links House S&Q', 'Dark Sanctuary S&Q', 'Old Man S&Q', 'Castle Ledge S&Q']), + create_menu_region(player, 'Menu', None, ['Links House S&Q', 'Dark Sanctuary S&Q', 'Old Man S&Q', 'Castle Ledge S&Q']), create_lw_region(player, 'Light World', ['Mushroom', 'Bottle Merchant', 'Flute Spot', 'Sunken Treasure', 'Purple Chest', 'Bombos Tablet'], ["Blinds Hideout", "Hyrule Castle Secret Entrance Drop", 'Kings Grave Outer Rocks', 'Dam', 'Inverted Big Bomb Shop', 'Tavern North', 'Chicken House', 'Aginahs Cave', 'Sahasrahlas Hut', 'Kakariko Well Drop', 'Kakariko Well Cave', diff --git a/Main.py b/Main.py index 1d549fca..06c8228b 100644 --- a/Main.py +++ b/Main.py @@ -25,7 +25,7 @@ from Fill import distribute_items_cutoff, distribute_items_staleness, distribute from ItemList import generate_itempool, difficulties, fill_prizes, fill_specific_items from Utils import output_path, parse_player_names -__version__ = '0.2.0.16u' +__version__ = '0.2.0.17u' class EnemizerError(RuntimeError): pass @@ -354,6 +354,7 @@ def main(args, seed=None, fish=None): return world + def copy_world(world): # ToDo: Not good yet ret = World(world.players, world.shuffle, world.doorShuffle, world.logic, world.mode, world.swords, @@ -443,15 +444,19 @@ def copy_world(world): # fill locations for location in world.get_locations(): + new_location = ret.get_location(location.name, location.player) if location.item is not None: item = Item(location.item.name, location.item.advancement, location.item.priority, location.item.type, player = location.item.player) - ret.get_location(location.name, location.player).item = item - item.location = ret.get_location(location.name, location.player) + new_location.item = item + item.location = new_location item.world = ret if location.event: - ret.get_location(location.name, location.player).event = True + new_location.event = True if location.locked: - ret.get_location(location.name, location.player).locked = True + new_location.locked = True + # these need to be modified properly by set_rules + new_location.access_rule = lambda state: True + new_location.item_rule = lambda state: True # copy remaining itempool. No item in itempool should have an assigned location for item in world.itempool: diff --git a/RELEASENOTES.md b/RELEASENOTES.md index eb30ad5e..50f191b4 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -92,6 +92,8 @@ testing to verify logic is all good. # Bug Fixes +* 2.0.17u + * Generation improvements * 2.0.16u * Prevent HUD from showing key counter when in the overworld. (Aga 2 doesn't always clear the dungeon indicator) * Fixed key logic regarding certain isolated "important" locations diff --git a/Rules.py b/Rules.py index b3ed526e..9c11552a 100644 --- a/Rules.py +++ b/Rules.py @@ -564,7 +564,7 @@ def inverted_rules(world, player): set_rule(world.get_entrance('Bomb Hut Outer Bushes', player), lambda state: state.has_Pearl(player)) set_rule(world.get_entrance('North Fairy Cave Drop', player), lambda state: state.has_Pearl(player)) set_rule(world.get_entrance('Lost Woods Hideout Drop', player), lambda state: state.has_Pearl(player)) - set_rule(world.get_location('Potion Shop', player), lambda state: state.has('Mushroom', player) and (state.can_reach('Potion Shop Area', 'Region', player))) # new inverted region, need pearl for bushes or access to potion shop door/waterfall fairy + set_rule(world.get_location('Potion Shop', player), lambda state: state.has('Mushroom', player) and (state.can_reach('Potion Shop Area', 'Region', player))) # new inverted region, need pearl for bushes or access to potion shop door/waterfall fairy set_rule(world.get_entrance('Desert Palace Entrance (North) Rocks', player), lambda state: state.can_lift_rocks(player) and state.has_Pearl(player)) set_rule(world.get_entrance('Desert Ledge Return Rocks', player), lambda state: state.can_lift_rocks(player) and state.has_Pearl(player)) # should we decide to place something that is not a dungeon end up there at some point set_rule(world.get_entrance('Checkerboard Cave', player), lambda state: state.can_lift_rocks(player) and state.has_Pearl(player)) @@ -1348,7 +1348,7 @@ def set_bunny_rules(world, player): # Note spiral cave may be technically passible, but it would be too absurd to require since OHKO mode is a thing. bunny_impassable_caves = ['Bumper Cave', 'Two Brothers House', 'Hookshot Cave', 'Pyramid', 'Spiral Cave (Top)', 'Fairy Ascension Cave (Drop)'] - bunny_accessible_locations = ['Link\'s Uncle', 'Sahasrahla', 'Sick Kid', 'Lost Woods Hideout', 'Lumberjack Tree', + bunny_accessible_locations = ['Link\'s House', 'Link\'s Uncle', 'Sahasrahla', 'Sick Kid', 'Lost Woods Hideout', 'Lumberjack Tree', 'Checkerboard Cave', 'Potion Shop', 'Spectacle Rock Cave', 'Pyramid', 'Hype Cave - Generous Guy', 'Peg Cave', 'Bumper Cave Ledge', 'Dark Blacksmith Ruins'] @@ -1432,7 +1432,7 @@ def set_inverted_bunny_rules(world, player): # Note spiral cave may be technically passible, but it would be too absurd to require since OHKO mode is a thing. bunny_impassable_caves = ['Bumper Cave', 'Two Brothers House', 'Hookshot Cave', 'Pyramid', 'Spiral Cave (Top)', 'Fairy Ascension Cave (Drop)', 'The Sky'] - bunny_accessible_locations = ['Link\'s Uncle', 'Sahasrahla', 'Sick Kid', 'Lost Woods Hideout', 'Lumberjack Tree', + bunny_accessible_locations = ['Link\'s House', 'Link\'s Uncle', 'Sahasrahla', 'Sick Kid', 'Lost Woods Hideout', 'Lumberjack Tree', 'Checkerboard Cave', 'Potion Shop', 'Spectacle Rock Cave', 'Pyramid', 'Hype Cave - Generous Guy', 'Peg Cave', 'Bumper Cave Ledge', 'Dark Blacksmith Ruins', 'Bombos Tablet', 'Ether Tablet', 'Purple Chest'] diff --git a/TestSuite.py b/TestSuite.py index c1fa790c..000f7de4 100644 --- a/TestSuite.py +++ b/TestSuite.py @@ -1,7 +1,5 @@ import subprocess import sys -import traceback -import io import multiprocessing import concurrent.futures import argparse @@ -42,6 +40,7 @@ def main(args=None): task.success = False task.name = testname task.mode = mode[0] + task.cmd = basecommand + " " + command + mode[1] task_mapping.append(task) test("Vanilla ", "--shuffle vanilla") @@ -61,14 +60,12 @@ def main(args=None): try: result = task.result() if result.returncode: - raise Exception(result.stderr) - except: - error = io.StringIO() - traceback.print_exc(file=error) - errors.append([task.name + task.mode, error.getvalue()]) - else: - alive += 1 - task.success = True + errors.append([task.name + task.mode, 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}{task.mode}") @@ -129,7 +126,8 @@ if __name__ == "__main__": with open(f"{dr[0]}{(f'-{tense}' if dr[0] in ['basic', 'crossed'] else '')}-errors.txt", 'w') as stream: for error in errors: stream.write(error[0] + "\n") - stream.write(error[1] + "\n\n") + stream.write(error[1] + "\n") + stream.write(error[2] + "\n\n") with open("success.txt", "w") as stream: stream.write(str.join("\n", successes))