diff --git a/Dungeon.py b/Dungeon.py index 7f9ea55fd..d8deb0a12 100644 --- a/Dungeon.py +++ b/Dungeon.py @@ -27,19 +27,14 @@ def __init__(self, world: World, name: str, hint: HintArea, regions: Optional[li region.dungeon = self self.regions.append(region) - def copy(self, *, copy_dict: Optional[dict[int, Any]] = None) -> Dungeon: - copy_dict = {} if copy_dict is None else copy_dict - if (new_dungeon := copy_dict.get(id(self), None)) and isinstance(new_dungeon, Dungeon): - return new_dungeon - - new_dungeon = Dungeon(world=self.world.copy(copy_dict=copy_dict), name=self.name, hint=self.hint, regions=[]) - copy_dict[id(self)] = new_dungeon - - new_dungeon.regions = [region.copy(copy_dict=copy_dict) for region in self.regions] - new_dungeon.boss_key = [item.copy(copy_dict=copy_dict) for item in self.boss_key] - new_dungeon.small_keys = [item.copy(copy_dict=copy_dict) for item in self.small_keys] - new_dungeon.dungeon_items = [item.copy(copy_dict=copy_dict) for item in self.dungeon_items] - new_dungeon.silver_rupees = [item.copy(copy_dict=copy_dict) for item in self.silver_rupees] + def copy(self) -> Dungeon: + new_dungeon = Dungeon(world=self.world, name=self.name, hint=self.hint, regions=[]) + + new_dungeon.regions = [region for region in self.regions] + new_dungeon.boss_key = [item for item in self.boss_key] + new_dungeon.small_keys = [item for item in self.small_keys] + new_dungeon.dungeon_items = [item for item in self.dungeon_items] + new_dungeon.silver_rupees = [item for item in self.silver_rupees] return new_dungeon diff --git a/Entrance.py b/Entrance.py index 2769123f5..c27077bac 100644 --- a/Entrance.py +++ b/Entrance.py @@ -26,24 +26,15 @@ def __init__(self, name: str = '', parent: Optional[Region] = None) -> None: self.never: bool = False self.rule_string: Optional[str] = None - def copy(self, *, copy_dict: Optional[dict[int, Any]] = None) -> Entrance: - copy_dict = {} if copy_dict is None else copy_dict - if (new_entrance := copy_dict.get(id(self), None)) and isinstance(new_entrance, Entrance): - return new_entrance + def copy(self) -> Entrance: + new_entrance = Entrance(self.name, self.parent_region) - new_entrance = Entrance(self.name, self.parent_region.copy(copy_dict=copy_dict) if self.parent_region else None) - copy_dict[id(self)] = new_entrance - - if self.connected_region is not None: - new_entrance.connected_region = self.connected_region.copy(copy_dict=copy_dict) + new_entrance.connected_region = self.connected_region new_entrance.access_rule = self.access_rule new_entrance.access_rules = list(self.access_rules) - if self.reverse: - new_entrance.reverse = self.reverse.copy(copy_dict=copy_dict) - if self.replaces: - new_entrance.replaces = self.replaces.copy(copy_dict=copy_dict) - if self.assumed: - new_entrance.assumed = self.assumed.copy(copy_dict=copy_dict) + new_entrance.reverse = self.reverse + new_entrance.replaces = self.replaces + new_entrance.assumed = self.assumed new_entrance.type = self.type new_entrance.shuffled = self.shuffled new_entrance.data = self.data @@ -74,7 +65,10 @@ def connect(self, region: Region) -> None: def disconnect(self) -> Optional[Region]: if self.connected_region is None: raise Exception(f"`disconnect()` called without a valid `connected_region` for entrance {self.name}.") - self.connected_region.entrances.remove(self) + try: + self.connected_region.entrances.remove(self) + except ValueError as e: + raise e previously_connected = self.connected_region self.connected_region = None return previously_connected diff --git a/Item.py b/Item.py index c639030ab..3568b4833 100644 --- a/Item.py +++ b/Item.py @@ -99,19 +99,12 @@ def __init__(self, name: str = '', world: Optional[World] = None, event: bool = # Do not alias to junk--it has no solver id! self.alias_id: Optional[int] = ItemInfo.solver_ids[escape_name(self.alias[0])] if self.alias else None - def copy(self, *, copy_dict: Optional[dict[int, Any]] = None) -> Item: - copy_dict = {} if copy_dict is None else copy_dict - if (new_item := copy_dict.get(id(self), None)) and isinstance(new_item, Item): - return new_item + def copy(self) -> Item: + new_item = Item(name=self.name, world=self.world, event=self.event) - new_item = Item(name=self.name, world=self.world.copy(copy_dict=copy_dict), event=self.event) - copy_dict[id(self)] = new_item - - if self.location: - new_item.location = self.location.copy(copy_dict=copy_dict) + new_item.location = self.location new_item.price = self.price - if self.looks_like_item: - new_item.looks_like_item = self.looks_like_item.copy(copy_dict=copy_dict) + new_item.looks_like_item = self.looks_like_item return new_item diff --git a/Location.py b/Location.py index 162e54a67..fdf8cbfb5 100644 --- a/Location.py +++ b/Location.py @@ -49,20 +49,13 @@ def __init__(self, name: str = '', address: LocationAddress = None, address2: Lo self.filter_tags: Optional[tuple[str, ...]] = (filter_tags,) if isinstance(filter_tags, str) else filter_tags self.rule_string: Optional[str] = None - def copy(self, *, copy_dict: Optional[dict[int, Any]] = None) -> Location: - copy_dict = {} if copy_dict is None else copy_dict - if (new_location := copy_dict.get(id(self), None)) and isinstance(new_location, Location): - return new_location - + def copy(self) -> Location: new_location = Location(name=self.name, address=self.address, address2=self.address2, default=self.default, - location_type=self.type, scene=self.scene, parent=self.parent_region.copy(copy_dict=copy_dict) if self.parent_region else None, + location_type=self.type, scene=self.scene, parent=self.parent_region, filter_tags=self.filter_tags, internal=self.internal, vanilla_item=self.vanilla_item) - copy_dict[id(self)] = new_location - new_location.world = self.world.copy(copy_dict=copy_dict) - if self.item: - new_location.item = self.item.copy(copy_dict=copy_dict) - new_location.item.location = new_location + new_location.world = self.world + new_location.item = self.item new_location.access_rule = self.access_rule new_location.access_rules = list(self.access_rules) new_location.item_rule = self.item_rule diff --git a/Region.py b/Region.py index b8b2df130..1e03f748c 100644 --- a/Region.py +++ b/Region.py @@ -51,22 +51,16 @@ def __init__(self, world: World, name: str, region_type: RegionType = RegionType self.is_boss_room: bool = False self.savewarp: Optional[Entrance] = None - def copy(self, *, copy_dict: Optional[dict[int, Any]] = None) -> Region: - copy_dict = {} if copy_dict is None else copy_dict - if (new_region := copy_dict.get(id(self), None)) and isinstance(new_region, Region): - return new_region + def copy(self) -> Region: + new_region = Region(world=self.world, name=self.name, region_type=self.type) - new_region = Region(world=self.world.copy(copy_dict=copy_dict), name=self.name, region_type=self.type) - copy_dict[id(self)] = new_region - - new_region.exits = [entrance.copy(copy_dict=copy_dict) for entrance in self.exits] - new_region.locations = [location.copy(copy_dict=copy_dict) for location in self.locations] + new_region.exits = [entrance for entrance in self.exits] + new_region.locations = [location for location in self.locations] # Why does this not work properly? # new_region.entrances = [entrance.copy(copy_dict=copy_dict) for entrance in self.entrances] - if self.dungeon: - new_region.dungeon = self.dungeon.copy(copy_dict=copy_dict) + new_region.dungeon = self.dungeon new_region.dungeon_name = self.dungeon_name new_region.hint_name = self.hint_name new_region.alt_hint_name = self.alt_hint_name @@ -75,7 +69,7 @@ def copy(self, *, copy_dict: Optional[dict[int, Any]] = None) -> Region: new_region.provides_time = self.provides_time new_region.scene = self.scene new_region.is_boss_room = self.is_boss_room - new_region.savewarp = None if self.savewarp is None else self.savewarp.copy(copy_dict=copy_dict) + new_region.savewarp = self.savewarp return new_region diff --git a/Spoiler.py b/Spoiler.py index ce6e936d6..90764b00d 100644 --- a/Spoiler.py +++ b/Spoiler.py @@ -1,7 +1,8 @@ from __future__ import annotations -from collections import OrderedDict import logging import random +from collections import OrderedDict +from itertools import chain from typing import TYPE_CHECKING, Any from Item import Item @@ -9,10 +10,12 @@ from Search import Search, RewindableSearch if TYPE_CHECKING: + from Dungeon import Dungeon from Entrance import Entrance from Goals import GoalCategory from Hints import GossipText from Location import Location + from Region import Region from Settings import Settings from World import World @@ -118,8 +121,8 @@ def parse_data(self) -> None: self.entrances[world.id] = spoiler_entrances def copy_worlds(self) -> list[World]: - copy_dict: dict[int, Any] = {} - worlds = [world.copy(copy_dict=copy_dict) for world in self.worlds] + copier = Copier(self) + worlds = copier.copy() return worlds def find_misc_hint_items(self) -> None: @@ -285,3 +288,89 @@ def create_playthrough(self) -> None: if worlds[0].entrance_shuffle: self.entrance_playthrough = OrderedDict((str(i + 1), list(sphere)) for i, sphere in enumerate(entrance_spheres)) + + +class Copier: + def __init__(self, spoiler: Spoiler) -> None: + self.spoiler: Spoiler = spoiler + self.worlds: dict[int, World] = {} + self.dungeons: dict[int, Dungeon] = {} + self.regions: dict[int, Region] = {} + self.entrances: dict[int, Entrance] = {} + self.locations: dict[int, Location] = {} + self.items: dict[int, Item] = {} + + def copy(self) -> list[World]: + if self.worlds: + return list(self.worlds.values()) + + # Make copies. + for world in self.spoiler.worlds: + self.worlds[id(world)] = world.copy() + for dungeon in world.dungeons: + self.dungeons[id(dungeon)] = dungeon.copy() + for item in chain(dungeon.boss_key, dungeon.small_keys, dungeon.dungeon_items, dungeon.silver_rupees): + if id(item) in self.items: + continue + self.items[id(item)] = item.copy() + for region in world.regions: + self.regions[id(region)] = region.copy() + for entrance in chain(region.entrances, region.exits, [region.savewarp] if region.savewarp else []): + if id(entrance) in self.entrances: + continue + self.entrances[id(entrance)] = entrance.copy() + for location in region.locations: + if id(location) in self.locations: + continue + self.locations[id(location)] = location.copy() + if location.item and id(location.item) not in self.items: + self.items[id(location.item)] = location.item.copy() + for item in world.itempool: + if id(item) in self.items: + continue + self.items[id(item)] = item.copy() + + # Update references. + for world in self.worlds.values(): + world.dungeons = [self.dungeons.get(id(dungeon), dungeon) for dungeon in world.dungeons] + world.regions = [self.regions.get(id(region), region) for region in world.regions] + world.itempool = [self.items.get(id(item), item) for item in world.itempool] + + for dungeon in self.dungeons.values(): + dungeon.world = self.worlds.get(id(dungeon.world), dungeon.world) + dungeon.regions = [self.regions.get(id(region), region) for region in dungeon.regions] + dungeon.boss_key = [self.items.get(id(item), item) for item in dungeon.boss_key] + dungeon.small_keys = [self.items.get(id(item), item) for item in dungeon.small_keys] + dungeon.dungeon_items = [self.items.get(id(item), item) for item in dungeon.dungeon_items] + dungeon.silver_rupees = [self.items.get(id(item), item) for item in dungeon.silver_rupees] + + for region in self.regions.values(): + region.world = self.worlds.get(id(region.world), region.world) + region.entrances = [self.entrances.get(id(entrance), entrance) for entrance in region.entrances] + region.exits = [self.entrances.get(id(entrance), entrance) for entrance in region.exits] + region.locations = [self.locations.get(id(location), location) for location in region.locations] + region.dungeon = self.dungeons.get(id(region.dungeon), region.dungeon) + region.savewarp = self.entrances.get(id(region.savewarp), region.savewarp) + + for entrance in self.entrances.values(): + entrance.world = self.worlds.get(id(entrance.world), entrance.world) + entrance.parent_region = self.regions.get(id(entrance.parent_region), entrance.parent_region) + entrance.connected_region = self.regions.get(id(entrance.connected_region), entrance.connected_region) + entrance.reverse = self.entrances.get(id(entrance.reverse), entrance.reverse) + entrance.replaces = self.entrances.get(id(entrance.replaces), entrance.replaces) + entrance.assumed = self.entrances.get(id(entrance.assumed), entrance.assumed) + + for location in self.locations.values(): + location.world = self.worlds.get(id(location.world), location.world) + location.parent_region = self.regions.get(id(location.parent_region), location.parent_region) + location.item = self.items.get(id(location.item), location.item) + + for item in self.items.values(): + item.world = self.worlds.get(id(item.world), item.world) + item.location = self.locations.get(id(item.location), item.location) + item.looks_like_item = self.items.get(id(item.looks_like_item), item.looks_like_item) + + for world in self.worlds.values(): + world.initialize_entrances() + + return list(self.worlds.values()) diff --git a/World.py b/World.py index 6e76a8160..c06c20746 100644 --- a/World.py +++ b/World.py @@ -340,13 +340,8 @@ def __missing__(self, dungeon_name: str) -> EmptyDungeonInfo: self.locked_goal_categories: dict[str, GoalCategory] = {name: category for (name, category) in self.goal_categories.items() if category.lock_entrances} self.unlocked_goal_categories: dict[str, GoalCategory] = {name: category for (name, category) in self.goal_categories.items() if not category.lock_entrances} - def copy(self, *, copy_dict: Optional[dict[int, Any]] = None) -> World: - copy_dict = {} if copy_dict is None else copy_dict - if (new_world := copy_dict.get(id(self), None)) and isinstance(new_world, World): - return new_world - + def copy(self) -> World: new_world = World(self.id, self.settings, False) - copy_dict[id(self)] = new_world new_world.skipped_trials = copy.copy(self.skipped_trials) new_world.dungeon_mq = copy.copy(self.dungeon_mq) @@ -358,13 +353,13 @@ def copy(self, *, copy_dict: Optional[dict[int, Any]] = None) -> World: new_world.maximum_wallets = self.maximum_wallets new_world.distribution = self.distribution - new_world.dungeons = [dungeon.copy(copy_dict=copy_dict) for dungeon in self.dungeons] - new_world.regions = [region.copy(copy_dict=copy_dict) for region in self.regions] - new_world.itempool = [item.copy(copy_dict=copy_dict) for item in self.itempool] + new_world.dungeons = [dungeon for dungeon in self.dungeons] + new_world.regions = [region for region in self.regions] + new_world.itempool = [item for item in self.itempool] new_world.state = self.state.copy(new_world) # TODO: Why is this necessary over copying region.entrances on region copy? - new_world.initialize_entrances() + # new_world.initialize_entrances() # copy any randomized settings to match the original copy new_world.randomized_list = list(self.randomized_list)