Skip to content

Commit

Permalink
Initial PoC for non-recursive copy.
Browse files Browse the repository at this point in the history
  • Loading branch information
Cuphat committed Aug 7, 2023
1 parent 9bbf665 commit 16260df
Show file tree
Hide file tree
Showing 7 changed files with 129 additions and 76 deletions.
21 changes: 8 additions & 13 deletions Dungeon.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
26 changes: 10 additions & 16 deletions Entrance.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
15 changes: 4 additions & 11 deletions Item.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
15 changes: 4 additions & 11 deletions Location.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
18 changes: 6 additions & 12 deletions Region.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand Down
95 changes: 92 additions & 3 deletions Spoiler.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
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
from LocationList import location_sort_order
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

Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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())
15 changes: 5 additions & 10 deletions World.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Expand Down

0 comments on commit 16260df

Please sign in to comment.