Skip to content

Commit

Permalink
LTTP: Key Drop Shuffle (ArchipelagoMW#282)
Browse files Browse the repository at this point in the history
Co-authored-by: espeon65536 <[email protected]>
Co-authored-by: black-sliver <[email protected]>
Co-authored-by: Bondo <[email protected]>
Co-authored-by: Fabian Dill <[email protected]>
  • Loading branch information
5 people authored and FlySniper committed Nov 14, 2023
1 parent 42c31fa commit 2f65d7a
Show file tree
Hide file tree
Showing 25 changed files with 809 additions and 707 deletions.
6 changes: 0 additions & 6 deletions Fill.py
Original file line number Diff line number Diff line change
Expand Up @@ -753,8 +753,6 @@ def failed(warning: str, force: typing.Union[bool, str]) -> None:
else: # not reachable with swept state
non_early_locations[loc.player].append(loc.name)

# TODO: remove. Preferably by implementing key drop
from worlds.alttp.Regions import key_drop_data
world_name_lookup = world.world_name_lookup

block_value = typing.Union[typing.List[str], typing.Dict[str, typing.Any], str]
Expand Down Expand Up @@ -897,10 +895,6 @@ def failed(warning: str, force: typing.Union[bool, str]) -> None:
for item_name in items:
item = world.worlds[player].create_item(item_name)
for location in reversed(candidates):
if location in key_drop_data:
warn(
f"Can't place '{item_name}' at '{placement.location}', as key drop shuffle locations are not supported yet.")
continue
if not location.item:
if location.item_rule(item):
if location.can_fill(world.state, item, False):
Expand Down
5 changes: 4 additions & 1 deletion playerSettings.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ name: YourName{number} # Your name in-game. Spaces will be replaced with undersc
game: # Pick a game to play
A Link to the Past: 1
requires:
version: 0.3.3 # Version of Archipelago required for this yaml to work as expected.
version: 0.4.3 # Version of Archipelago required for this yaml to work as expected.
A Link to the Past:
progression_balancing:
# A system that can move progression earlier, to try and prevent the player from getting stuck and bored early.
Expand Down Expand Up @@ -114,6 +114,9 @@ A Link to the Past:
different_world: 0
universal: 0
start_with: 0
key_drop_shuffle: # Shuffle keys found in pots or dropped from killed enemies
off: 50
on: 0
compass_shuffle: # Compass Placement
original_dungeon: 50
own_dungeons: 0
Expand Down
14 changes: 10 additions & 4 deletions test/TestBase.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,9 @@ def get_state(self, items):
state = CollectionState(self.multiworld)
for item in items:
item.classification = ItemClassification.progression
state.collect(item)
state.collect(item, event=True)
state.sweep_for_events()
state.update_reachable_regions(1)
self._state_cache[self.multiworld, tuple(items)] = state
return state

Expand All @@ -53,15 +54,19 @@ def run_location_tests(self, access_pool):
with self.subTest(msg="Reach Location", location=location, access=access, items=items,
all_except=all_except, path=path, entry=i):

self.assertEqual(self.multiworld.get_location(location, 1).can_reach(state), access)
self.assertEqual(self.multiworld.get_location(location, 1).can_reach(state), access,
f"failed {self.multiworld.get_location(location, 1)} with: {item_pool}")

# check for partial solution
if not all_except and access: # we are not supposed to be able to reach location with partial inventory
for missing_item in item_pool[0]:
with self.subTest(msg="Location reachable without required item", location=location,
items=item_pool[0], missing_item=missing_item, entry=i):
state = self._get_items_partial(item_pool, missing_item)
self.assertEqual(self.multiworld.get_location(location, 1).can_reach(state), False)

self.assertEqual(self.multiworld.get_location(location, 1).can_reach(state), False,
f"failed {self.multiworld.get_location(location, 1)}: succeeded with "
f"{missing_item} removed from: {item_pool}")

def run_entrance_tests(self, access_pool):
for i, (entrance, access, *item_pool) in enumerate(access_pool):
Expand All @@ -80,7 +85,8 @@ def run_entrance_tests(self, access_pool):
with self.subTest(msg="Entrance reachable without required item", entrance=entrance,
items=item_pool[0], missing_item=missing_item, entry=i):
state = self._get_items_partial(item_pool, missing_item)
self.assertEqual(self.multiworld.get_entrance(entrance, 1).can_reach(state), False)
self.assertEqual(self.multiworld.get_entrance(entrance, 1).can_reach(state), False,
f"failed {self.multiworld.get_entrance(entrance, 1)} with: {item_pool}")

def _get_items(self, item_pool, all_except):
if all_except and len(all_except) > 0:
Expand Down
2 changes: 1 addition & 1 deletion worlds/alttp/Client.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@
"Hyrule Castle - Zelda's Chest": (0x80, 0x10),
'Hyrule Castle - Big Key Drop': (0x80, 0x400),
'Sewers - Dark Cross': (0x32, 0x10),
'Hyrule Castle - Key Rat Key Drop': (0x21, 0x400),
'Sewers - Key Rat Key Drop': (0x21, 0x400),
'Sewers - Secret Room - Left': (0x11, 0x10),
'Sewers - Secret Room - Middle': (0x11, 0x20),
'Sewers - Secret Room - Right': (0x11, 0x40),
Expand Down
59 changes: 37 additions & 22 deletions worlds/alttp/Dungeons.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

from .Bosses import BossFactory, Boss
from .Items import ItemFactory
from .Regions import lookup_boss_drops
from .Regions import lookup_boss_drops, key_drop_data
from .Options import smallkey_shuffle

if typing.TYPE_CHECKING:
Expand Down Expand Up @@ -81,15 +81,17 @@ def make_dungeon(name, default_boss, dungeon_regions, big_key, small_keys, dunge
return dungeon

ES = make_dungeon('Hyrule Castle', None, ['Hyrule Castle', 'Sewers', 'Sewer Drop', 'Sewers (Dark)', 'Sanctuary'],
None, [ItemFactory('Small Key (Hyrule Castle)', player)],
ItemFactory('Big Key (Hyrule Castle)', player),
ItemFactory(['Small Key (Hyrule Castle)'] * 4, player),
[ItemFactory('Map (Hyrule Castle)', player)])
EP = make_dungeon('Eastern Palace', 'Armos Knights', ['Eastern Palace'],
ItemFactory('Big Key (Eastern Palace)', player), [],
ItemFactory('Big Key (Eastern Palace)', player),
ItemFactory(['Small Key (Eastern Palace)'] * 2, player),
ItemFactory(['Map (Eastern Palace)', 'Compass (Eastern Palace)'], player))
DP = make_dungeon('Desert Palace', 'Lanmolas',
['Desert Palace North', 'Desert Palace Main (Inner)', 'Desert Palace Main (Outer)',
'Desert Palace East'], ItemFactory('Big Key (Desert Palace)', player),
[ItemFactory('Small Key (Desert Palace)', player)],
ItemFactory(['Small Key (Desert Palace)'] * 4, player),
ItemFactory(['Map (Desert Palace)', 'Compass (Desert Palace)'], player))
ToH = make_dungeon('Tower of Hera', 'Moldorm',
['Tower of Hera (Bottom)', 'Tower of Hera (Basement)', 'Tower of Hera (Top)'],
Expand All @@ -105,60 +107,63 @@ def make_dungeon(name, default_boss, dungeon_regions, big_key, small_keys, dunge
ItemFactory(['Small Key (Palace of Darkness)'] * 6, player),
ItemFactory(['Map (Palace of Darkness)', 'Compass (Palace of Darkness)'], player))
TT = make_dungeon('Thieves Town', 'Blind', ['Thieves Town (Entrance)', 'Thieves Town (Deep)', 'Blind Fight'],
ItemFactory('Big Key (Thieves Town)', player), [ItemFactory('Small Key (Thieves Town)', player)],
ItemFactory('Big Key (Thieves Town)', player),
ItemFactory(['Small Key (Thieves Town)'] * 3, player),
ItemFactory(['Map (Thieves Town)', 'Compass (Thieves Town)'], player))
SW = make_dungeon('Skull Woods', 'Mothula', ['Skull Woods Final Section (Entrance)', 'Skull Woods First Section',
'Skull Woods Second Section', 'Skull Woods Second Section (Drop)',
'Skull Woods Final Section (Mothula)',
'Skull Woods First Section (Right)',
'Skull Woods First Section (Left)', 'Skull Woods First Section (Top)'],
ItemFactory('Big Key (Skull Woods)', player),
ItemFactory(['Small Key (Skull Woods)'] * 3, player),
ItemFactory(['Small Key (Skull Woods)'] * 5, player),
ItemFactory(['Map (Skull Woods)', 'Compass (Skull Woods)'], player))
SP = make_dungeon('Swamp Palace', 'Arrghus',
['Swamp Palace (Entrance)', 'Swamp Palace (First Room)', 'Swamp Palace (Starting Area)',
'Swamp Palace (Center)', 'Swamp Palace (North)'], ItemFactory('Big Key (Swamp Palace)', player),
[ItemFactory('Small Key (Swamp Palace)', player)],
'Swamp Palace (West)', 'Swamp Palace (Center)', 'Swamp Palace (North)'],
ItemFactory('Big Key (Swamp Palace)', player),
ItemFactory(['Small Key (Swamp Palace)'] * 6, player),
ItemFactory(['Map (Swamp Palace)', 'Compass (Swamp Palace)'], player))
IP = make_dungeon('Ice Palace', 'Kholdstare',
['Ice Palace (Entrance)', 'Ice Palace (Main)', 'Ice Palace (East)', 'Ice Palace (East Top)',
'Ice Palace (Kholdstare)'], ItemFactory('Big Key (Ice Palace)', player),
ItemFactory(['Small Key (Ice Palace)'] * 2, player),
['Ice Palace (Entrance)', 'Ice Palace (Second Section)', 'Ice Palace (Main)', 'Ice Palace (East)',
'Ice Palace (East Top)', 'Ice Palace (Kholdstare)'], ItemFactory('Big Key (Ice Palace)', player),
ItemFactory(['Small Key (Ice Palace)'] * 6, player),
ItemFactory(['Map (Ice Palace)', 'Compass (Ice Palace)'], player))
MM = make_dungeon('Misery Mire', 'Vitreous',
['Misery Mire (Entrance)', 'Misery Mire (Main)', 'Misery Mire (West)', 'Misery Mire (Final Area)',
'Misery Mire (Vitreous)'], ItemFactory('Big Key (Misery Mire)', player),
ItemFactory(['Small Key (Misery Mire)'] * 3, player),
ItemFactory(['Small Key (Misery Mire)'] * 6, player),
ItemFactory(['Map (Misery Mire)', 'Compass (Misery Mire)'], player))
TR = make_dungeon('Turtle Rock', 'Trinexx',
['Turtle Rock (Entrance)', 'Turtle Rock (First Section)', 'Turtle Rock (Chain Chomp Room)',
'Turtle Rock (Pokey Room)',
'Turtle Rock (Second Section)', 'Turtle Rock (Big Chest)', 'Turtle Rock (Crystaroller Room)',
'Turtle Rock (Dark Room)', 'Turtle Rock (Eye Bridge)', 'Turtle Rock (Trinexx)'],
ItemFactory('Big Key (Turtle Rock)', player),
ItemFactory(['Small Key (Turtle Rock)'] * 4, player),
ItemFactory(['Small Key (Turtle Rock)'] * 6, player),
ItemFactory(['Map (Turtle Rock)', 'Compass (Turtle Rock)'], player))

if multiworld.mode[player] != 'inverted':
AT = make_dungeon('Agahnims Tower', 'Agahnim', ['Agahnims Tower', 'Agahnim 1'], None,
ItemFactory(['Small Key (Agahnims Tower)'] * 2, player), [])
ItemFactory(['Small Key (Agahnims Tower)'] * 4, player), [])
GT = make_dungeon('Ganons Tower', 'Agahnim2',
['Ganons Tower (Entrance)', 'Ganons Tower (Tile Room)', 'Ganons Tower (Compass Room)',
'Ganons Tower (Hookshot Room)', 'Ganons Tower (Map Room)', 'Ganons Tower (Firesnake Room)',
'Ganons Tower (Teleport Room)', 'Ganons Tower (Bottom)', 'Ganons Tower (Top)',
'Ganons Tower (Before Moldorm)', 'Ganons Tower (Moldorm)', 'Agahnim 2'],
ItemFactory('Big Key (Ganons Tower)', player),
ItemFactory(['Small Key (Ganons Tower)'] * 4, player),
ItemFactory(['Small Key (Ganons Tower)'] * 8, player),
ItemFactory(['Map (Ganons Tower)', 'Compass (Ganons Tower)'], player))
else:
AT = make_dungeon('Inverted Agahnims Tower', 'Agahnim', ['Inverted Agahnims Tower', 'Agahnim 1'], None,
ItemFactory(['Small Key (Agahnims Tower)'] * 2, player), [])
ItemFactory(['Small Key (Agahnims Tower)'] * 4, player), [])
GT = make_dungeon('Inverted Ganons Tower', 'Agahnim2',
['Inverted Ganons Tower (Entrance)', 'Ganons Tower (Tile Room)',
'Ganons Tower (Compass Room)', 'Ganons Tower (Hookshot Room)', 'Ganons Tower (Map Room)',
'Ganons Tower (Firesnake Room)', 'Ganons Tower (Teleport Room)', 'Ganons Tower (Bottom)',
'Ganons Tower (Top)', 'Ganons Tower (Before Moldorm)', 'Ganons Tower (Moldorm)',
'Agahnim 2'], ItemFactory('Big Key (Ganons Tower)', player),
ItemFactory(['Small Key (Ganons Tower)'] * 4, player),
ItemFactory(['Small Key (Ganons Tower)'] * 8, player),
ItemFactory(['Map (Ganons Tower)', 'Compass (Ganons Tower)'], player))

GT.bosses['bottom'] = BossFactory('Armos Knights', player)
Expand Down Expand Up @@ -195,10 +200,11 @@ def fill_dungeons_restrictive(multiworld: MultiWorld):
dungeon_specific: set = set()
for subworld in multiworld.get_game_worlds("A Link to the Past"):
player = subworld.player
localized |= {(player, item_name) for item_name in
subworld.dungeon_local_item_names}
dungeon_specific |= {(player, item_name) for item_name in
subworld.dungeon_specific_item_names}
if player not in multiworld.groups:
localized |= {(player, item_name) for item_name in
subworld.dungeon_local_item_names}
dungeon_specific |= {(player, item_name) for item_name in
subworld.dungeon_specific_item_names}

if localized:
in_dungeon_items = [item for item in get_dungeon_item_pool(multiworld) if (item.player, item.name) in localized]
Expand Down Expand Up @@ -249,7 +255,16 @@ def fill_dungeons_restrictive(multiworld: MultiWorld):
if all_state_base.has("Triforce", player):
all_state_base.remove(multiworld.worlds[player].create_item("Triforce"))

fill_restrictive(multiworld, all_state_base, locations, in_dungeon_items, True, True, allow_excluded=True)
for (player, key_drop_shuffle) in enumerate(multiworld.key_drop_shuffle.values(), start=1):
if not key_drop_shuffle and player not in multiworld.groups:
for key_loc in key_drop_data:
key_data = key_drop_data[key_loc]
all_state_base.remove(ItemFactory(key_data[3], player))
loc = multiworld.get_location(key_loc, player)

if loc in all_state_base.events:
all_state_base.events.remove(loc)
fill_restrictive(multiworld, all_state_base, locations, in_dungeon_items, True, True)


dungeon_music_addresses = {'Eastern Palace - Prize': [0x1559A],
Expand Down
Loading

0 comments on commit 2f65d7a

Please sign in to comment.