Main structure for various biased fills

Lots of help around correctly reserving locations
This commit is contained in:
aerinon
2021-08-26 15:27:05 -06:00
parent 4d776e0fee
commit 746a739339
17 changed files with 1239 additions and 445 deletions

View File

@@ -17,6 +17,7 @@ from Utils import int16_as_bytes
from Tables import normal_offset_table, spiral_offset_table, multiply_lookup, divisor_lookup from Tables import normal_offset_table, spiral_offset_table, multiply_lookup, divisor_lookup
from RoomData import Room from RoomData import Room
class World(object): class World(object):
def __init__(self, players, shuffle, doorShuffle, logic, mode, swords, difficulty, difficulty_adjustments, def __init__(self, players, shuffle, doorShuffle, logic, mode, swords, difficulty, difficulty_adjustments,
@@ -213,6 +214,11 @@ class World(object):
return r_location return r_location
raise RuntimeError('No such location %s for player %d' % (location, player)) raise RuntimeError('No such location %s for player %d' % (location, player))
def get_location_unsafe(self, location, player):
if (location, player) in self._location_cache:
return self._location_cache[(location, player)]
return None
def get_dungeon(self, dungeonname, player): def get_dungeon(self, dungeonname, player):
if isinstance(dungeonname, Dungeon): if isinstance(dungeonname, Dungeon):
return dungeonname return dungeonname
@@ -1452,6 +1458,10 @@ class Dungeon(object):
return self.world.get_name_string_for_object(self) if self.world else f'{self.name} (Player {self.player})' return self.world.get_name_string_for_object(self) if self.world else f'{self.name} (Player {self.player})'
class FillError(RuntimeError):
pass
@unique @unique
class DoorType(Enum): class DoorType(Enum):
Normal = 1 Normal = 1
@@ -1842,6 +1852,8 @@ class Sector(object):
self.destination_entrance = False self.destination_entrance = False
self.equations = None self.equations = None
self.item_logic = set() self.item_logic = set()
self.chest_location_set = set()
def region_set(self): def region_set(self):
if self.r_name_set is None: if self.r_name_set is None:
@@ -2084,6 +2096,7 @@ class Location(object):
self.recursion_count = 0 self.recursion_count = 0
self.staleness_count = 0 self.staleness_count = 0
self.locked = False self.locked = False
self.real = not crystal
self.always_allow = lambda item, state: False self.always_allow = lambda item, state: False
self.access_rule = lambda state: True self.access_rule = lambda state: True
self.item_rule = lambda item: True self.item_rule = lambda item: True

View File

@@ -1,8 +1,8 @@
import logging import logging
import RaceRandom as random import RaceRandom as random
from BaseClasses import Boss from BaseClasses import Boss, FillError
from Fill import FillError
def BossFactory(boss, player): def BossFactory(boss, player):
if boss is None: if boss is None:

View File

@@ -46,8 +46,7 @@ def link_doors(world, player):
world.swamp_patch_required[player] = orig_swamp_patch world.swamp_patch_required[player] = orig_swamp_patch
def link_doors_main(world, player): def link_doors_prep(world, player):
# Drop-down connections & push blocks # Drop-down connections & push blocks
for exitName, regionName in logical_connections: for exitName, regionName in logical_connections:
connect_simple_door(world, exitName, regionName, player) connect_simple_door(world, exitName, regionName, player)
@@ -99,6 +98,7 @@ def link_doors_main(world, player):
analyze_portals(world, player) analyze_portals(world, player)
for portal in world.dungeon_portals[player]: for portal in world.dungeon_portals[player]:
connect_portal(portal, world, player) connect_portal(portal, world, player)
if not world.doorShuffle[player] == 'vanilla': if not world.doorShuffle[player] == 'vanilla':
fix_big_key_doors_with_ugly_smalls(world, player) fix_big_key_doors_with_ugly_smalls(world, player)
else: else:
@@ -119,11 +119,14 @@ def link_doors_main(world, player):
for ent, ext in default_one_way_connections: for ent, ext in default_one_way_connections:
connect_one_way(world, ent, ext, player) connect_one_way(world, ent, ext, player)
vanilla_key_logic(world, player) vanilla_key_logic(world, player)
elif world.doorShuffle[player] == 'basic':
def link_doors_main(world, player):
if world.doorShuffle[player] == 'basic':
within_dungeon(world, player) within_dungeon(world, player)
elif world.doorShuffle[player] == 'crossed': elif world.doorShuffle[player] == 'crossed':
cross_dungeon(world, player) cross_dungeon(world, player)
else: elif world.doorShuffle[player] != 'vanilla':
logging.getLogger('').error('Invalid door shuffle setting: %s' % world.doorShuffle[player]) logging.getLogger('').error('Invalid door shuffle setting: %s' % world.doorShuffle[player])
raise Exception('Invalid door shuffle setting: %s' % world.doorShuffle[player]) raise Exception('Invalid door shuffle setting: %s' % world.doorShuffle[player])
@@ -214,11 +217,33 @@ def vanilla_key_logic(world, player):
world.key_logic[player] = {} world.key_logic[player] = {}
analyze_dungeon(key_layout, world, player) analyze_dungeon(key_layout, world, player)
world.key_logic[player][builder.name] = key_layout.key_logic 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) 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]: # 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) # validate_vanilla_key_logic(world, player)
def validate_vanilla_reservation(dungeon, world, player):
return validate_key_layout(world.key_layout[player][dungeon.name], world, player)
# if not hasattr(world, 'builder_cache'):
# world.builder_cache = {}
# if (dungeon.name, player) not in world.builder_cache:
# sector = Sector()
# sector.name = dungeon.name
# sector.regions.extend(convert_regions(dungeon.regions, world, player))
# builder = simple_dungeon_builder(sector.name, [sector])
# builder.master_sector = sector
#
# origin_list = find_accessible_entrances(world, player, builder)
# 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)
# world.builder_cache[(dungeon.name, player)] = key_layout
# else:
# key_layout = world.builder_cache[(dungeon.name, player)]
# return validate_key_layout(key_layout, world, player)
# some useful functions # some useful functions
oppositemap = { oppositemap = {
Direction.South: Direction.North, Direction.South: Direction.North,

View File

@@ -1198,6 +1198,8 @@ class DungeonBuilder(object):
self.sectors = [] self.sectors = []
self.location_cnt = 0 self.location_cnt = 0
self.key_drop_cnt = 0 self.key_drop_cnt = 0
self.dungeon_items = None # during fill how many dungeon items are left
self.free_items = None # during fill how many dungeon items are left
self.bk_required = False self.bk_required = False
self.bk_provided = False self.bk_provided = False
self.c_switch_required = False self.c_switch_required = False
@@ -1359,7 +1361,7 @@ def create_dungeon_builders(all_sectors, connections_tuple, world, player,
polarized_sectors[sector] = None polarized_sectors[sector] = None
if bow_sectors: if bow_sectors:
assign_bow_sectors(dungeon_map, bow_sectors, global_pole) assign_bow_sectors(dungeon_map, bow_sectors, global_pole)
assign_location_sectors(dungeon_map, free_location_sectors, global_pole) assign_location_sectors(dungeon_map, free_location_sectors, global_pole, world, player)
leftover = assign_crystal_switch_sectors(dungeon_map, crystal_switches, crystal_barriers, global_pole) leftover = assign_crystal_switch_sectors(dungeon_map, crystal_switches, crystal_barriers, global_pole)
ensure_crystal_switches_reachable(dungeon_map, leftover, polarized_sectors, crystal_barriers, global_pole) ensure_crystal_switches_reachable(dungeon_map, leftover, polarized_sectors, crystal_barriers, global_pole)
for sector in leftover: for sector in leftover:
@@ -1510,6 +1512,7 @@ def define_sector_features(sectors):
sector.bk_provided = True sector.bk_provided = True
elif loc.name not in dungeon_events and not loc.forced_item: elif loc.name not in dungeon_events and not loc.forced_item:
sector.chest_locations += 1 sector.chest_locations += 1
sector.chest_location_set.add(loc.name)
if '- Big Chest' in loc.name or loc.name in ["Hyrule Castle - Zelda's Chest", if '- Big Chest' in loc.name or loc.name in ["Hyrule Castle - Zelda's Chest",
"Thieves' Town - Blind's Cell"]: "Thieves' Town - Blind's Cell"]:
sector.bk_required = True sector.bk_required = True
@@ -1590,19 +1593,26 @@ def assign_bow_sectors(dungeon_map, bow_sectors, global_pole):
assign_sector(sector_list[i], builder, bow_sectors, global_pole) assign_sector(sector_list[i], builder, bow_sectors, global_pole)
def assign_location_sectors(dungeon_map, free_location_sectors, global_pole): def assign_location_sectors(dungeon_map, free_location_sectors, global_pole, world, player):
valid = False valid = False
choices = None choices = None
sector_list = list(free_location_sectors) sector_list = list(free_location_sectors)
random.shuffle(sector_list) random.shuffle(sector_list)
orig_location_set = build_orig_location_set(dungeon_map)
num_dungeon_items = requested_dungeon_items(world, player)
while not valid: while not valid:
choices, d_idx, totals = weighted_random_locations(dungeon_map, sector_list) choices, d_idx, totals = weighted_random_locations(dungeon_map, sector_list)
location_set = {x: set(y) for x, y in orig_location_set.items()}
for i, sector in enumerate(sector_list): for i, sector in enumerate(sector_list):
choice = d_idx[choices[i].name] d_name = choices[i].name
choice = d_idx[d_name]
totals[choice] += sector.chest_locations totals[choice] += sector.chest_locations
location_set[d_name].update(sector.chest_location_set)
valid = True valid = True
for d_name, idx in d_idx.items(): for d_name, idx in d_idx.items():
if totals[idx] < 5: # min locations for dungeons is 5 (bk exception) free_items = count_reserved_locations(world, player, location_set[d_name])
target = max(free_items, 2) + num_dungeon_items
if totals[idx] < target:
valid = False valid = False
break break
for i, choice in enumerate(choices): for i, choice in enumerate(choices):
@@ -1633,6 +1643,30 @@ def weighted_random_locations(dungeon_map, free_location_sectors):
return choices, d_idx, totals return choices, d_idx, totals
def build_orig_location_set(dungeon_map):
orig_locations = {}
for name, builder in dungeon_map.items():
orig_locations[name] = set().union(*(s.chest_location_set for s in builder.sectors))
return orig_locations
def requested_dungeon_items(world, player):
num = 0
if not world.bigkeyshuffle[player]:
num += 1
if not world.compassshuffle[player]:
num += 1
if not world.mapshuffle[player]:
num += 1
return num
def count_reserved_locations(world, player, proposed_set):
if world.item_pool_config:
return len([x for x in proposed_set if x in world.item_pool_config.reserved_locations[player]])
return 2
def assign_crystal_switch_sectors(dungeon_map, crystal_switches, crystal_barriers, global_pole, assign_one=False): def assign_crystal_switch_sectors(dungeon_map, crystal_switches, crystal_barriers, global_pole, assign_one=False):
population = [] population = []
some_c_switches_present = False some_c_switches_present = False

View File

@@ -1,8 +1,5 @@
import RaceRandom as random
from BaseClasses import Dungeon from BaseClasses import Dungeon
from Bosses import BossFactory from Bosses import BossFactory
from Fill import fill_restrictive
from Items import ItemFactory from Items import ItemFactory
@@ -36,117 +33,6 @@ def create_dungeons(world, player):
world.dungeons += [ES, EP, DP, ToH, AT, PoD, TT, SW, SP, IP, MM, TR, GT] world.dungeons += [ES, EP, DP, ToH, AT, PoD, TT, SW, SP, IP, MM, TR, GT]
def fill_dungeons(world):
freebes = ['Ganons Tower - Map Chest', 'Palace of Darkness - Harmless Hellway', 'Palace of Darkness - Big Key Chest', 'Turtle Rock - Big Key Chest']
all_state_base = world.get_all_state()
for player in range(1, world.players + 1):
pinball_room = world.get_location('Skull Woods - Pinball Room', player)
if world.retro[player]:
world.push_item(pinball_room, ItemFactory('Small Key (Universal)', player), False)
else:
world.push_item(pinball_room, ItemFactory('Small Key (Skull Woods)', player), False)
pinball_room.event = True
pinball_room.locked = True
dungeons = [(list(dungeon.regions), dungeon.big_key, list(dungeon.small_keys), list(dungeon.dungeon_items)) for dungeon in world.dungeons]
loopcnt = 0
while dungeons:
loopcnt += 1
dungeon_regions, big_key, small_keys, dungeon_items = dungeons.pop(0)
# this is what we need to fill
dungeon_locations = [location for location in world.get_unfilled_locations() if location.parent_region.name in dungeon_regions]
random.shuffle(dungeon_locations)
all_state = all_state_base.copy()
# first place big key
if big_key is not None:
bk_location = None
for location in dungeon_locations:
if location.item_rule(big_key):
bk_location = location
break
if bk_location is None:
raise RuntimeError('No suitable location for %s' % big_key)
world.push_item(bk_location, big_key, False)
bk_location.event = True
bk_location.locked = True
dungeon_locations.remove(bk_location)
big_key = None
# next place small keys
while small_keys:
small_key = small_keys.pop()
all_state.sweep_for_events()
sk_location = None
for location in dungeon_locations:
if location.name in freebes or (location.can_reach(all_state) and location.item_rule(small_key)):
sk_location = location
break
if sk_location is None:
# need to retry this later
small_keys.append(small_key)
dungeons.append((dungeon_regions, big_key, small_keys, dungeon_items))
# infinite regression protection
if loopcnt < (30 * world.players):
break
else:
raise RuntimeError('No suitable location for %s' % small_key)
world.push_item(sk_location, small_key, False)
sk_location.event = True
sk_location.locked = True
dungeon_locations.remove(sk_location)
if small_keys:
# key placement not finished, loop again
continue
# next place dungeon items
for dungeon_item in dungeon_items:
di_location = dungeon_locations.pop()
world.push_item(di_location, dungeon_item, False)
def get_dungeon_item_pool(world):
return [item for dungeon in world.dungeons for item in dungeon.all_items]
def fill_dungeons_restrictive(world, shuffled_locations):
all_state_base = world.get_all_state()
# for player in range(1, world.players + 1):
# pinball_room = world.get_location('Skull Woods - Pinball Room', player)
# if world.retro[player]:
# world.push_item(pinball_room, ItemFactory('Small Key (Universal)', player), False)
# else:
# world.push_item(pinball_room, ItemFactory('Small Key (Skull Woods)', player), False)
# pinball_room.event = True
# pinball_room.locked = True
# shuffled_locations.remove(pinball_room)
# with shuffled dungeon items they are distributed as part of the normal item pool
for item in world.get_items():
if (item.smallkey and world.keyshuffle[item.player]) or (item.bigkey and world.bigkeyshuffle[item.player]):
item.advancement = True
elif (item.map and world.mapshuffle[item.player]) or (item.compass and world.compassshuffle[item.player]):
item.priority = True
dungeon_items = [item for item in get_dungeon_item_pool(world) if item.is_inside_dungeon_item(world)]
# sort in the order Big Key, Small Key, Other before placing dungeon items
sort_order = {"BigKey": 3, "SmallKey": 2}
dungeon_items.sort(key=lambda item: sort_order.get(item.type, 1))
fill_restrictive(world, all_state_base, shuffled_locations, dungeon_items,
keys_in_itempool={player: not world.keyshuffle[player] for player in range(1, world.players+1)}, single_player_placement=True)
dungeon_music_addresses = {'Eastern Palace - Prize': [0x1559A], dungeon_music_addresses = {'Eastern Palace - Prize': [0x1559A],
'Desert Palace - Prize': [0x1559B, 0x1559C, 0x1559D, 0x1559E], 'Desert Palace - Prize': [0x1559B, 0x1559C, 0x1559D, 0x1559E],

403
Fill.py
View File

@@ -3,176 +3,74 @@ import collections
import itertools import itertools
import logging import logging
from BaseClasses import CollectionState from BaseClasses import CollectionState, FillError
from Items import ItemFactory from Items import ItemFactory
from Regions import shop_to_location_table, retro_shops from Regions import shop_to_location_table, retro_shops
from source.item.BiasedFill import filter_locations, classify_major_items, split_pool
class FillError(RuntimeError): def get_dungeon_item_pool(world):
pass return [item for dungeon in world.dungeons for item in dungeon.all_items]
def distribute_items_cutoff(world, cutoffrate=0.33):
# get list of locations to fill in
fill_locations = world.get_unfilled_locations()
random.shuffle(fill_locations)
# get items to distribute
random.shuffle(world.itempool)
itempool = world.itempool
total_advancement_items = len([item for item in itempool if item.advancement])
placed_advancement_items = 0
progress_done = False
advancement_placed = False
# sweep once to pick up preplaced items
world.state.sweep_for_events()
while itempool and fill_locations:
candidate_item_to_place = None
item_to_place = None
for item in itempool:
if advancement_placed or (progress_done and (item.advancement or item.priority)):
item_to_place = item
break
if item.advancement:
candidate_item_to_place = item
if world.unlocks_new_location(item):
item_to_place = item
placed_advancement_items += 1
break
if item_to_place is None:
# check if we can reach all locations and that is why we find no new locations to place
if not progress_done and len(world.get_reachable_locations()) == len(world.get_locations()):
progress_done = True
continue
# check if we have now placed all advancement items
if progress_done:
advancement_placed = True
continue
# we might be in a situation where all new locations require multiple items to reach. If that is the case, just place any advancement item we've found and continue trying
if candidate_item_to_place is not None:
item_to_place = candidate_item_to_place
placed_advancement_items += 1
else:
# we placed all available progress items. Maybe the game can be beaten anyway?
if world.can_beat_game():
logging.getLogger('').warning('Not all locations reachable. Game beatable anyway.')
progress_done = True
continue
raise FillError('No more progress items left to place.')
spot_to_fill = None
for location in fill_locations if placed_advancement_items / total_advancement_items < cutoffrate else reversed(fill_locations):
if location.can_fill(world.state, item_to_place):
spot_to_fill = location
break
if spot_to_fill is None:
# we filled all reachable spots. Maybe the game can be beaten anyway?
if world.can_beat_game():
logging.getLogger('').warning('Not all items placed. Game beatable anyway.')
break
raise FillError('No more spots to place %s' % item_to_place)
world.push_item(spot_to_fill, item_to_place, True)
itempool.remove(item_to_place)
fill_locations.remove(spot_to_fill)
unplaced = [item.name for item in itempool]
unfilled = [location.name for location in fill_locations]
if unplaced or unfilled:
logging.warning('Unplaced items: %s - Unfilled Locations: %s', unplaced, unfilled)
def distribute_items_staleness(world): def promote_dungeon_items(world):
# get list of locations to fill in world.itempool += get_dungeon_item_pool(world)
fill_locations = world.get_unfilled_locations()
random.shuffle(fill_locations)
# get items to distribute for item in world.get_items():
random.shuffle(world.itempool) if item.smallkey or item.bigkey:
itempool = world.itempool item.advancement = True
elif item.map or item.compass:
item.priority = True
dungeon_tracking(world)
progress_done = False
advancement_placed = False
# sweep once to pick up preplaced items def dungeon_tracking(world):
world.state.sweep_for_events() for dungeon in world.dungeons:
layout = world.dungeon_layouts[dungeon.player][dungeon.name]
layout.dungeon_items = len(dungeon.all_items)
layout.free_items = layout.location_cnt - layout.dungeon_items
while itempool and fill_locations:
candidate_item_to_place = None
item_to_place = None
for item in itempool:
if advancement_placed or (progress_done and (item.advancement or item.priority)):
item_to_place = item
break
if item.advancement:
candidate_item_to_place = item
if world.unlocks_new_location(item):
item_to_place = item
break
if item_to_place is None: def fill_dungeons_restrictive(world, shuffled_locations):
# check if we can reach all locations and that is why we find no new locations to place dungeon_tracking(world)
if not progress_done and len(world.get_reachable_locations()) == len(world.get_locations()): all_state_base = world.get_all_state()
progress_done = True
continue
# check if we have now placed all advancement items
if progress_done:
advancement_placed = True
continue
# we might be in a situation where all new locations require multiple items to reach. If that is the case, just place any advancement item we've found and continue trying
if candidate_item_to_place is not None:
item_to_place = candidate_item_to_place
else:
# we placed all available progress items. Maybe the game can be beaten anyway?
if world.can_beat_game():
logging.getLogger('').warning('Not all locations reachable. Game beatable anyway.')
progress_done = True
continue
raise FillError('No more progress items left to place.')
spot_to_fill = None # for player in range(1, world.players + 1):
for location in fill_locations: # pinball_room = world.get_location('Skull Woods - Pinball Room', player)
# increase likelyhood of skipping a location if it has been found stale # if world.retro[player]:
if not progress_done and random.randint(0, location.staleness_count) > 2: # world.push_item(pinball_room, ItemFactory('Small Key (Universal)', player), False)
continue # else:
# world.push_item(pinball_room, ItemFactory('Small Key (Skull Woods)', player), False)
# pinball_room.event = True
# pinball_room.locked = True
# shuffled_locations.remove(pinball_room)
if location.can_fill(world.state, item_to_place): # with shuffled dungeon items they are distributed as part of the normal item pool
spot_to_fill = location for item in world.get_items():
break if (item.smallkey and world.keyshuffle[item.player]) or (item.bigkey and world.bigkeyshuffle[item.player]):
else: item.advancement = True
location.staleness_count += 1 elif (item.map and world.mapshuffle[item.player]) or (item.compass and world.compassshuffle[item.player]):
item.priority = True
# might have skipped too many locations due to potential staleness. Do not check for staleness now to find a candidate dungeon_items = [item for item in get_dungeon_item_pool(world) if item.is_inside_dungeon_item(world)]
if spot_to_fill is None:
for location in fill_locations:
if location.can_fill(world.state, item_to_place):
spot_to_fill = location
break
if spot_to_fill is None: # sort in the order Big Key, Small Key, Other before placing dungeon items
# we filled all reachable spots. Maybe the game can be beaten anyway? sort_order = {"BigKey": 3, "SmallKey": 2}
if world.can_beat_game(): dungeon_items.sort(key=lambda item: sort_order.get(item.type, 1))
logging.getLogger('').warning('Not all items placed. Game beatable anyway.')
break
raise FillError('No more spots to place %s' % item_to_place)
world.push_item(spot_to_fill, item_to_place, True) fill_restrictive(world, all_state_base, shuffled_locations, dungeon_items,
itempool.remove(item_to_place) keys_in_itempool={player: not world.keyshuffle[player] for player in range(1, world.players+1)},
fill_locations.remove(spot_to_fill) single_player_placement=True)
unplaced = [item.name for item in itempool]
unfilled = [location.name for location in fill_locations]
if unplaced or unfilled:
logging.warning('Unplaced items: %s - Unfilled Locations: %s', unplaced, unfilled)
def fill_restrictive(world, base_state, locations, itempool, keys_in_itempool = None, single_player_placement = False): def fill_restrictive(world, base_state, locations, itempool, keys_in_itempool=None, single_player_placement=False,
reserved_items=None):
if not reserved_items:
reserved_items = []
def sweep_from_pool(): def sweep_from_pool():
new_state = base_state.copy() new_state = base_state.copy()
for item in itempool: for item in itempool + reserved_items:
new_state.collect(item, True) new_state.collect(item, True)
new_state.sweep_for_events() new_state.sweep_for_events()
return new_state return new_state
@@ -201,41 +99,56 @@ def fill_restrictive(world, base_state, locations, itempool, keys_in_itempool =
spot_to_fill = None spot_to_fill = None
for location in locations: item_locations = filter_locations(item_to_place, locations, world)
if item_to_place.smallkey or item_to_place.bigkey: # a better test to see if a key can go there for location in item_locations:
location.item = item_to_place spot_to_fill = verify_spot_to_fill(location, item_to_place, maximum_exploration_state,
test_state = maximum_exploration_state.copy() single_player_placement, perform_access_check, itempool,
test_state.stale[item_to_place.player] = True keys_in_itempool, world)
else: if spot_to_fill:
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, itempool if (keys_in_itempool and keys_in_itempool[item_to_place.player]) else world.itempool, world):
spot_to_fill = location
break break
if item_to_place.smallkey or item_to_place.bigkey:
location.item = None
if spot_to_fill is None: if spot_to_fill is None:
# we filled all reachable spots. Maybe the game can be beaten anyway? # we filled all reachable spots. Maybe the game can be beaten anyway?
unplaced_items.insert(0, item_to_place) unplaced_items.insert(0, item_to_place)
if world.can_beat_game(): if world.can_beat_game():
if world.accessibility[item_to_place.player] != 'none': if world.accessibility[item_to_place.player] != 'none':
logging.getLogger('').warning('Not all items placed. Game beatable anyway. (Could not place %s)' % item_to_place) logging.getLogger('').warning('Not all items placed. Game beatable anyway.'
f' (Could not place {item_to_place})')
continue continue
spot_to_fill = last_ditch_placement(item_to_place, locations, world, maximum_exploration_state, if world.algorithm in ['balanced', 'equitable']:
base_state, itempool, keys_in_itempool, single_player_placement) spot_to_fill = last_ditch_placement(item_to_place, locations, world, maximum_exploration_state,
base_state, itempool, keys_in_itempool,
single_player_placement)
if spot_to_fill is None: if spot_to_fill is None:
raise FillError('No more spots to place %s' % item_to_place) raise FillError('No more spots to place %s' % item_to_place)
world.push_item(spot_to_fill, item_to_place, False) world.push_item(spot_to_fill, item_to_place, False)
track_outside_keys(item_to_place, spot_to_fill, world) 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) locations.remove(spot_to_fill)
spot_to_fill.event = True spot_to_fill.event = True
itempool.extend(unplaced_items) itempool.extend(unplaced_items)
def verify_spot_to_fill(location, item_to_place, max_exp_state, single_player_placement, perform_access_check,
itempool, keys_in_itempool, 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
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:
if location.can_fill(test_state, item_to_place, perform_access_check):
test_pool = itempool if (keys_in_itempool and keys_in_itempool[item_to_place.player]) else world.itempool
if valid_key_placement(item_to_place, location, test_pool, 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
return None
def valid_key_placement(item, location, itempool, world): def valid_key_placement(item, location, itempool, world):
if not valid_reserved_placement(item, location, world): if not valid_reserved_placement(item, location, world):
return False return False
@@ -259,6 +172,17 @@ def valid_reserved_placement(item, location, world):
return True return True
def valid_dungeon_placement(item, location, world):
if location.parent_region.dungeon:
layout = world.dungeon_layouts[location.player][location.parent_region.dungeon.name]
if not is_dungeon_item(item, world) or item.player != location.player:
return layout.free_items > 0
else:
# the second half probably doesn't matter much - should always return true
return item.dungeon == location.parent_region.dungeon.name and layout.dungeon_items > 0
return not is_dungeon_item(item, world)
def track_outside_keys(item, location, world): def track_outside_keys(item, location, world):
if not item.smallkey: if not item.smallkey:
return return
@@ -270,6 +194,22 @@ def track_outside_keys(item, location, world):
world.key_logic[item.player][item_dungeon].outside_keys += 1 world.key_logic[item.player][item_dungeon].outside_keys += 1
def track_dungeon_items(item, location, world):
if location.parent_region.dungeon and not item.crystal:
layout = world.dungeon_layouts[location.player][location.parent_region.dungeon.name]
if is_dungeon_item(item, world) and item.player == location.player:
layout.dungeon_items -= 1
else:
layout.free_items -= 1
def is_dungeon_item(item, world):
return ((item.smallkey and not world.keyshuffle[item.player])
or (item.bigkey and not world.bigkeyshuffle[item.player])
or (item.compass and not world.compassshuffle[item.player])
or (item.map and not world.mapshuffle[item.player]))
def last_ditch_placement(item_to_place, locations, world, state, base_state, itempool, def last_ditch_placement(item_to_place, locations, world, state, base_state, itempool,
keys_in_itempool=None, single_player_placement=False): keys_in_itempool=None, single_player_placement=False):
def location_preference(loc): def location_preference(loc):
@@ -351,6 +291,7 @@ def distribute_items_restrictive(world, gftower_trash=False, fill_locations=None
random.shuffle(fill_locations) random.shuffle(fill_locations)
# get items to distribute # get items to distribute
classify_major_items(world)
random.shuffle(world.itempool) random.shuffle(world.itempool)
progitempool = [item for item in world.itempool if item.advancement] progitempool = [item for item in world.itempool if item.advancement]
prioitempool = [item for item in world.itempool if not item.advancement and item.priority] prioitempool = [item for item in world.itempool if not item.advancement and item.priority]
@@ -379,21 +320,53 @@ def distribute_items_restrictive(world, gftower_trash=False, fill_locations=None
# Make sure the escape small key is placed first in standard with key shuffle to prevent running out of spots # Make sure the escape small key is placed first in standard with key shuffle to prevent running out of spots
# todo: crossed # todo: crossed
progitempool.sort(key=lambda item: 1 if item.name == 'Small Key (Escape)' and world.keyshuffle[item.player] and world.mode[item.player] == 'standard' else 0) progitempool.sort(key=lambda item: 1 if item.name == 'Small Key (Escape)' and world.keyshuffle[item.player] and world.mode[item.player] == 'standard' else 0)
keys_in_pool = {player: world.keyshuffle[player] or world.algorithm != 'balanced' for player in range(1, world.players + 1)}
if world.algorithm in ['balanced', 'equitable', 'vanilla_bias', 'dungeon_bias', 'entangled']:
fill_restrictive(world, world.state, fill_locations, progitempool, keys_in_pool)
random.shuffle(fill_locations)
if world.algorithm == 'balanced':
fast_fill(world, prioitempool, fill_locations)
elif world.algorithm == 'vanilla_bias':
fast_vanilla_fill(world, prioitempool, fill_locations)
elif world.algorithm in ['dungeon_bias', 'entangled']:
filtered_fill(world, prioitempool, fill_locations)
else: # just need to ensure dungeon items still get placed in dungeons
fast_equitable_fill(world, prioitempool, fill_locations)
# placeholder work
if world.algorithm == 'entangled' and world.players > 1:
random.shuffle(fill_locations)
placeholder_locations = filter_locations('Placeholder', fill_locations, world)
placeholder_items = [item for item in world.itempool if item.name == 'Rupee (1)']
for i in placeholder_items:
restitempool.remove(i)
for l in placeholder_locations:
fill_locations.remove(l)
filtered_fill(world, placeholder_items, placeholder_locations)
else:
primary, secondary = split_pool(progitempool, world)
fill_restrictive(world, world.state, fill_locations, primary, keys_in_pool, False, secondary)
random.shuffle(fill_locations)
tertiary, quaternary = split_pool(prioitempool, world)
prioitempool = []
filtered_equitable_fill(world, tertiary, fill_locations)
prioitempool += tertiary
random.shuffle(fill_locations)
fill_restrictive(world, world.state, fill_locations, secondary, keys_in_pool)
random.shuffle(fill_locations)
fast_equitable_fill(world, quaternary, fill_locations)
prioitempool += quaternary
fill_restrictive(world, world.state, fill_locations, progitempool, if world.algorithm == 'vanilla_bias':
keys_in_itempool={player: world.keyshuffle[player] for player in range(1, world.players + 1)}) fast_vanilla_fill(world, restitempool, fill_locations)
else:
random.shuffle(fill_locations) fast_fill(world, restitempool, fill_locations)
fast_fill(world, prioitempool, fill_locations)
fast_fill(world, restitempool, fill_locations)
unplaced = [item.name for item in prioitempool + restitempool] unplaced = [item.name for item in prioitempool + restitempool]
unfilled = [location.name for location in fill_locations] unfilled = [location.name for location in fill_locations]
if unplaced or unfilled: if unplaced or unfilled:
logging.warning('Unplaced items: %s - Unfilled Locations: %s', unplaced, unfilled) logging.warning('Unplaced items: %s - Unfilled Locations: %s', unplaced, unfilled)
def fast_fill(world, item_pool, fill_locations): def fast_fill(world, item_pool, fill_locations):
while item_pool and fill_locations: while item_pool and fill_locations:
spot_to_fill = fill_locations.pop() spot_to_fill = fill_locations.pop()
@@ -401,70 +374,48 @@ def fast_fill(world, item_pool, fill_locations):
world.push_item(spot_to_fill, item_to_place, False) world.push_item(spot_to_fill, item_to_place, False)
def flood_items(world): def filtered_fill(world, item_pool, fill_locations):
# get items to distribute while item_pool and fill_locations:
random.shuffle(world.itempool) item_to_place = item_pool.pop()
itempool = world.itempool item_locations = filter_locations(item_to_place, fill_locations, world)
progress_done = False spot_to_fill = next(iter(item_locations))
fill_locations.remove(spot_to_fill)
world.push_item(spot_to_fill, item_to_place, False)
# sweep once to pick up preplaced items # sweep once to pick up preplaced items
world.state.sweep_for_events() world.state.sweep_for_events()
# fill world from top of itempool while we can def fast_vanilla_fill(world, item_pool, fill_locations):
while not progress_done: while item_pool and fill_locations:
location_list = world.get_unfilled_locations() item_to_place = item_pool.pop()
random.shuffle(location_list) spot_to_fill = next(iter(filter_locations(item_to_place, fill_locations, world)))
spot_to_fill = None fill_locations.remove(spot_to_fill)
for location in location_list: world.push_item(spot_to_fill, item_to_place, False)
if location.can_fill(world.state, itempool[0]):
spot_to_fill = location
break
if spot_to_fill:
item = itempool.pop(0)
world.push_item(spot_to_fill, item, True)
continue
# ran out of spots, check if we need to step in and correct things def filtered_equitable_fill(world, item_pool, fill_locations):
if len(world.get_reachable_locations()) == len(world.get_locations()): while item_pool and fill_locations:
progress_done = True item_to_place = item_pool.pop()
continue item_locations = filter_locations(item_to_place, fill_locations, world)
spot_to_fill = next(l for l in item_locations if valid_dungeon_placement(item_to_place, l, world))
fill_locations.remove(spot_to_fill)
world.push_item(spot_to_fill, item_to_place, False)
track_dungeon_items(item_to_place, spot_to_fill, world)
# need to place a progress item instead of an already placed item, find candidate
item_to_place = None
candidate_item_to_place = None
for item in itempool:
if item.advancement:
candidate_item_to_place = item
if world.unlocks_new_location(item):
item_to_place = item
break
# we might be in a situation where all new locations require multiple items to reach. If that is the case, just place any advancement item we've found and continue trying def fast_equitable_fill(world, item_pool, fill_locations):
if item_to_place is None: while item_pool and fill_locations:
if candidate_item_to_place is not None: item_to_place = item_pool.pop()
item_to_place = candidate_item_to_place spot_to_fill = next(l for l in fill_locations if valid_dungeon_placement(item_to_place, l, world))
else: fill_locations.remove(spot_to_fill)
raise FillError('No more progress items left to place.') world.push_item(spot_to_fill, item_to_place, False)
track_dungeon_items(item_to_place, spot_to_fill, world)
# find item to replace with progress item
location_list = world.get_reachable_locations()
random.shuffle(location_list)
for location in location_list:
if location.item is not None and not location.item.advancement and not location.item.priority and not location.item.smallkey and not location.item.bigkey:
# safe to replace
replace_item = location.item
replace_item.location = None
itempool.append(replace_item)
world.push_item(location, item_to_place, True)
itempool.remove(item_to_place)
break
def lock_shop_locations(world, player): def lock_shop_locations(world, player):
for shop, loc_names in shop_to_location_table.items(): for shop, loc_names in shop_to_location_table.items():
for loc in loc_names: for loc in loc_names:
world.get_location(loc, player).event = True # world.get_location(loc, player).event = True
world.get_location(loc, player).locked = True world.get_location(loc, player).locked = True
# I don't believe these locations exist in non-shopsanity # I don't believe these locations exist in non-shopsanity
# if world.retro[player]: # if world.retro[player]:

View File

@@ -5,12 +5,13 @@ import RaceRandom as random
from BaseClasses import Region, RegionType, Shop, ShopType, Location, CollectionState from BaseClasses import Region, RegionType, Shop, ShopType, Location, CollectionState
from Bosses import place_bosses from Bosses import place_bosses
from Dungeons import get_dungeon_item_pool
from EntranceShuffle import connect_entrance from EntranceShuffle import connect_entrance
from Regions import shop_to_location_table, retro_shops, shop_table_by_location from Regions import shop_to_location_table, retro_shops, shop_table_by_location
from Fill import FillError, fill_restrictive, fast_fill from Fill import FillError, fill_restrictive, fast_fill, get_dungeon_item_pool
from Items import ItemFactory from Items import ItemFactory
from source.item.BiasedFill import trash_items
import source.classes.constants as CONST import source.classes.constants as CONST
@@ -262,8 +263,12 @@ def generate_itempool(world, player):
if player in world.pool_adjustment.keys(): if player in world.pool_adjustment.keys():
amt = world.pool_adjustment[player] amt = world.pool_adjustment[player]
if amt < 0: if amt < 0:
for _ in range(amt, 0): trash_options = [x for x in pool if x in trash_items]
pool.remove(next(iter([x for x in pool if x in ['Rupees (20)', 'Rupees (5)', 'Rupee (1)']]))) random.shuffle(trash_options)
trash_options = sorted(trash_options, key=lambda x: trash_items[x], reverse=True)
while amt > 0 and len(trash_options) > 0:
pool.remove(trash_options.pop())
amt -= 1
elif amt > 0: elif amt > 0:
for _ in range(0, amt): for _ in range(0, amt):
pool.append('Rupees (20)') pool.append('Rupees (20)')

View File

@@ -177,6 +177,8 @@ class PlacementRule(object):
return True return True
available_keys = outside_keys available_keys = outside_keys
empty_chests = 0 empty_chests = 0
# 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.needed_keys_wo_bk if bk_blocked else self.needed_keys_w_bk
for loc in check_locations: for loc in check_locations:
if not loc.item: if not loc.item:

64
Main.py
View File

@@ -19,16 +19,17 @@ from InvertedRegions import create_inverted_regions, mark_dark_world_regions
from EntranceShuffle import link_entrances, link_inverted_entrances from EntranceShuffle import link_entrances, link_inverted_entrances
from Rom import patch_rom, patch_race_rom, patch_enemizer, apply_rom_settings, LocalRom, JsonRom, get_hash_string from Rom import patch_rom, patch_race_rom, patch_enemizer, apply_rom_settings, LocalRom, JsonRom, get_hash_string
from Doors import create_doors from Doors import create_doors
from DoorShuffle import link_doors, connect_portal from DoorShuffle import link_doors, connect_portal, link_doors_prep
from RoomData import create_rooms from RoomData import create_rooms
from Rules import set_rules from Rules import set_rules
from Dungeons import create_dungeons, fill_dungeons, fill_dungeons_restrictive from Dungeons import create_dungeons
from Fill import distribute_items_cutoff, distribute_items_staleness, distribute_items_restrictive, flood_items from Fill import distribute_items_restrictive, promote_dungeon_items, fill_dungeons_restrictive
from Fill import sell_potions, sell_keys, balance_multiworld_progression, balance_money_progression, lock_shop_locations from Fill import sell_potions, sell_keys, balance_multiworld_progression, balance_money_progression, lock_shop_locations
from ItemList import generate_itempool, difficulties, fill_prizes, customize_shops from ItemList import generate_itempool, difficulties, fill_prizes, customize_shops
from Utils import output_path, parse_player_names from Utils import output_path, parse_player_names
from source.item.FillUtil import create_item_pool_config from source.item.BiasedFill import create_item_pool_config, massage_item_pool
__version__ = '0.5.1.0-u' __version__ = '0.5.1.0-u'
@@ -149,7 +150,6 @@ def main(args, seed=None, fish=None):
create_rooms(world, player) create_rooms(world, player)
create_dungeons(world, player) create_dungeons(world, player)
adjust_locations(world, player) adjust_locations(world, player)
create_item_pool_config(world)
if any(world.potshuffle.values()): if any(world.potshuffle.values()):
logger.info(world.fish.translate("cli", "cli", "shuffling.pots")) logger.info(world.fish.translate("cli", "cli", "shuffling.pots"))
@@ -165,7 +165,13 @@ def main(args, seed=None, fish=None):
else: else:
link_inverted_entrances(world, player) link_inverted_entrances(world, player)
logger.info(world.fish.translate("cli","cli","shuffling.dungeons")) logger.info(world.fish.translate("cli", "cli", "shuffling.prep"))
for player in range(1, world.players + 1):
link_doors_prep(world, player)
create_item_pool_config(world)
logger.info(world.fish.translate("cli", "cli", "shuffling.dungeons"))
for player in range(1, world.players + 1): for player in range(1, world.players + 1):
link_doors(world, player) link_doors(world, player)
@@ -173,8 +179,7 @@ def main(args, seed=None, fish=None):
mark_light_world_regions(world, player) mark_light_world_regions(world, player)
else: else:
mark_dark_world_regions(world, player) mark_dark_world_regions(world, player)
logger.info(world.fish.translate("cli","cli","generating.itempool")) logger.info(world.fish.translate("cli", "cli", "generating.itempool"))
logger.info(world.fish.translate("cli","cli","generating.itempool"))
for player in range(1, world.players + 1): for player in range(1, world.players + 1):
generate_itempool(world, player) generate_itempool(world, player)
@@ -192,8 +197,8 @@ def main(args, seed=None, fish=None):
else: else:
lock_shop_locations(world, player) lock_shop_locations(world, player)
massage_item_pool(world)
logger.info(world.fish.translate("cli","cli","placing.dungeon.prizes")) logger.info(world.fish.translate("cli", "cli", "placing.dungeon.prizes"))
fill_prizes(world) fill_prizes(world)
@@ -202,14 +207,14 @@ def main(args, seed=None, fish=None):
logger.info(world.fish.translate("cli","cli","placing.dungeon.items")) logger.info(world.fish.translate("cli","cli","placing.dungeon.items"))
shuffled_locations = None if args.algorithm in ['balanced', 'dungeon_bias', 'entangled']:
if args.algorithm in ['balanced', 'vt26'] or any(list(args.mapshuffle.values()) + list(args.compassshuffle.values()) +
list(args.keyshuffle.values()) + list(args.bigkeyshuffle.values())):
shuffled_locations = world.get_unfilled_locations() shuffled_locations = world.get_unfilled_locations()
random.shuffle(shuffled_locations) random.shuffle(shuffled_locations)
fill_dungeons_restrictive(world, shuffled_locations) fill_dungeons_restrictive(world, shuffled_locations)
elif args.algorithm == 'equitable':
promote_dungeon_items(world)
else: else:
fill_dungeons(world) promote_dungeon_items(world)
for player in range(1, world.players+1): for player in range(1, world.players+1):
if world.logic[player] != 'nologic': if world.logic[player] != 'nologic':
@@ -227,34 +232,22 @@ def main(args, seed=None, fish=None):
logger.info(world.fish.translate("cli","cli","fill.world")) logger.info(world.fish.translate("cli","cli","fill.world"))
if args.algorithm == 'flood': distribute_items_restrictive(world, True)
flood_items(world) # different algo, biased towards early game progress items
elif args.algorithm == 'vt21':
distribute_items_cutoff(world, 1)
elif args.algorithm == 'vt22':
distribute_items_cutoff(world, 0.66)
elif args.algorithm == 'freshness':
distribute_items_staleness(world)
elif args.algorithm == 'vt25':
distribute_items_restrictive(world, False)
elif args.algorithm == 'vt26':
distribute_items_restrictive(world, True, shuffled_locations)
elif args.algorithm == 'balanced':
distribute_items_restrictive(world, True)
if world.players > 1: if world.players > 1:
logger.info(world.fish.translate("cli","cli","balance.multiworld")) logger.info(world.fish.translate("cli", "cli", "balance.multiworld"))
balance_multiworld_progression(world) if args.algorithm in ['balanced', 'equitable']:
balance_multiworld_progression(world)
# if we only check for beatable, we can do this sanity check first before creating the rom # if we only check for beatable, we can do this sanity check first before creating the rom
if not world.can_beat_game(log_error=True): if not world.can_beat_game(log_error=True):
raise RuntimeError(world.fish.translate("cli","cli","cannot.beat.game")) raise RuntimeError(world.fish.translate("cli", "cli", "cannot.beat.game"))
for player in range(1, world.players+1): for player in range(1, world.players+1):
if world.shopsanity[player]: if world.shopsanity[player]:
customize_shops(world, player) customize_shops(world, player)
balance_money_progression(world) if args.algorithm in ['balanced', 'equitable']:
balance_money_progression(world)
outfilebase = f'DR_{args.outputname if args.outputname else world.seed}' outfilebase = f'DR_{args.outputname if args.outputname else world.seed}'
@@ -405,6 +398,7 @@ def copy_world(world):
ret.keydropshuffle = world.keydropshuffle.copy() ret.keydropshuffle = world.keydropshuffle.copy()
ret.mixed_travel = world.mixed_travel.copy() ret.mixed_travel = world.mixed_travel.copy()
ret.standardize_palettes = world.standardize_palettes.copy() ret.standardize_palettes = world.standardize_palettes.copy()
ret.restrict_boss_items = world.restrict_boss_items.copy()
ret.exp_cache = world.exp_cache.copy() ret.exp_cache = world.exp_cache.copy()
@@ -579,11 +573,11 @@ def create_playthrough(world):
# todo: this is not very efficient, but I'm not sure how else to do it for this backwards logic # todo: this is not very efficient, but I'm not sure how else to do it for this backwards logic
# world.clear_exp_cache() # world.clear_exp_cache()
if world.can_beat_game(state_cache[num]): if world.can_beat_game(state_cache[num]):
# logging.getLogger('').debug(f'{old_item.name} (Player {old_item.player}) is not required') logging.getLogger('').debug(f'{old_item.name} (Player {old_item.player}) is not required')
to_delete.add(location) to_delete.add(location)
else: else:
# still required, got to keep it around # still required, got to keep it around
# logging.getLogger('').debug(f'{old_item.name} (Player {old_item.player}) is required') logging.getLogger('').debug(f'{old_item.name} (Player {old_item.player}) is required')
location.item = old_item location.item = old_item
# cull entries in spheres for spoiler walkthrough at end # cull entries in spheres for spoiler walkthrough at end

View File

@@ -71,6 +71,8 @@ def main():
if args.enemizercli: if args.enemizercli:
erargs.enemizercli = args.enemizercli erargs.enemizercli = args.enemizercli
mw_settings = {'algorithm': False}
settings_cache = {k: (roll_settings(v) if args.samesettings else None) for k, v in weights_cache.items()} settings_cache = {k: (roll_settings(v) if args.samesettings else None) for k, v in weights_cache.items()}
for player in range(1, args.multi + 1): for player in range(1, args.multi + 1):
@@ -79,7 +81,12 @@ def main():
settings = settings_cache[path] if settings_cache[path] else roll_settings(weights_cache[path]) settings = settings_cache[path] if settings_cache[path] else roll_settings(weights_cache[path])
for k, v in vars(settings).items(): for k, v in vars(settings).items():
if v is not None: if v is not None:
getattr(erargs, k)[player] = v if k == 'algorithm': # multiworld wide parameters
if not mw_settings[k]: # only use the first roll
setattr(erargs, k, v)
mw_settings[k] = True
else:
getattr(erargs, k)[player] = v
else: else:
raise RuntimeError(f'No weights specified for player {player}') raise RuntimeError(f'No weights specified for player {player}')
@@ -116,6 +123,8 @@ def roll_settings(weights):
ret = argparse.Namespace() ret = argparse.Namespace()
ret.algorithm = get_choice('algorithm')
glitches_required = get_choice('glitches_required') glitches_required = get_choice('glitches_required')
if glitches_required not in ['none', 'no_logic']: if glitches_required not in ['none', 'no_logic']:
print("Only NMG and No Logic supported") print("Only NMG and No Logic supported")

View File

@@ -999,6 +999,14 @@ def adjust_locations(world, player):
world.get_location(location, player).address = 0x400000 + index world.get_location(location, player).address = 0x400000 + index
# player address? it is in the shop table # player address? it is in the shop table
index += 1 index += 1
# unreal events:
for l in ['Ganon', 'Agahnim 1', 'Agahnim 2', 'Dark Blacksmith Ruins', 'Frog', 'Missing Smith', 'Floodgate',
'Trench 1 Switch', 'Trench 2 Switch', 'Swamp Drain', 'Attic Cracked Floor', 'Suspicious Maiden',
'Revealing Light', 'Ice Block Drop', 'Zelda Pickup', 'Zelda Drop Off']:
location = world.get_location_unsafe(l, player)
if location:
location.real = False
# (type, room_id, shopkeeper, custom, locked, [items]) # (type, room_id, shopkeeper, custom, locked, [items])

View File

@@ -101,12 +101,12 @@
"algorithm": { "algorithm": {
"choices": [ "choices": [
"balanced", "balanced",
"freshness", "equitable",
"flood", "vanilla_bias",
"vt21", "major_bias",
"vt22", "dungeon_bias",
"vt25", "cluster_bias",
"vt26" "entangled"
] ]
}, },
"shuffle": { "shuffle": {

View File

@@ -7,6 +7,7 @@
"seed": "Seed", "seed": "Seed",
"player": "Player", "player": "Player",
"shuffling.world": "Shuffling the World about", "shuffling.world": "Shuffling the World about",
"shuffling.prep": "Dungeon and Item prep",
"shuffling.dungeons": "Shuffling dungeons", "shuffling.dungeons": "Shuffling dungeons",
"shuffling.pots": "Shuffling pots", "shuffling.pots": "Shuffling pots",
"basic.traversal": "--Basic Traversal", "basic.traversal": "--Basic Traversal",
@@ -153,22 +154,29 @@
"balanced: vt26 derivative that aims to strike a balance between", "balanced: vt26 derivative that aims to strike a balance between",
" the overworld heavy vt25 and the dungeon heavy vt26", " the overworld heavy vt25 and the dungeon heavy vt26",
" algorithm.", " algorithm.",
"vt26: Shuffle items and place them in a random location", "equitable: does not place dungeon items first allowing new potential",
" that it is not impossible to be in. This includes", " but mixed with the normal advancement pool",
" dungeon keys and items.", "biased placements: these consider all major items to be special and attempts",
"vt25: Shuffle items and place them in a random location", "to place items from fixed to semi-random locations. For purposes of these shuffles, all",
" that it is not impossible to be in.", "Y items, A items, swords (unless vanilla swords), mails, shields, heart containers and",
"vt21: Unbiased in its selection, but has tendency to put", "1/2 magic are considered to be part of a major items pool. Big Keys are added to the pool",
" Ice Rod in Turtle Rock.", "if shuffled. Same for small keys, compasses, maps, keydrops (if small keys are also shuffled),",
"vt22: Drops off stale locations after 1/3 of progress", "1 of each capacity upgrade for shopsanity, the quiver item for retro+shopsanity, and",
" items were placed to try to circumvent vt21\\'s", "triforce pieces for Triforce Hunt. Future modes will add to these as appropriate.",
" shortcomings.", "vanilla_bias Same as above, but attempts to place items in their vanilla",
"Freshness: Keep track of stale locations (ones that cannot be", " location first. Major items that cannot be placed that way",
" reached yet) and decrease likeliness of selecting", " will attempt to be placed in other failed locations first.",
" them the more often they were found unreachable.", " Also attempts to place junk items in vanilla locations",
"Flood: Push out items starting from Link\\'s House and", "major_bias same as above, but uses the major items' location preferentially",
" slightly biased to placing progression items with", " major item location are defined as the group of location where",
" less restrictions." " the items are found in the vanilla game. Backup locations for items",
" not in the vanilla game will be in the documentation",
"dungeon_bias same as above, but major items are preferentially placed",
" in dungeons locations first",
"cluster_bias same as above, but groups of locations are chosen randomly",
" from a pool of fixed locations designed to be interesting",
" and give major clues about the location of other",
" advancement items. These fixed groups will be documented"
], ],
"shuffle": [ "shuffle": [
"Select Entrance Shuffling Algorithm. (default: %(default)s)", "Select Entrance Shuffling Algorithm. (default: %(default)s)",

View File

@@ -279,13 +279,12 @@
"randomizer.item.accessibility.none": "Beatable", "randomizer.item.accessibility.none": "Beatable",
"randomizer.item.sortingalgo": "Item Sorting", "randomizer.item.sortingalgo": "Item Sorting",
"randomizer.item.sortingalgo.freshness": "Freshness",
"randomizer.item.sortingalgo.flood": "Flood",
"randomizer.item.sortingalgo.vt21": "VT8.21",
"randomizer.item.sortingalgo.vt22": "VT8.22",
"randomizer.item.sortingalgo.vt25": "VT8.25",
"randomizer.item.sortingalgo.vt26": "VT8.26",
"randomizer.item.sortingalgo.balanced": "Balanced", "randomizer.item.sortingalgo.balanced": "Balanced",
"randomizer.item.sortingalgo.equitable": "Equitable",
"randomizer.item.sortingalgo.vanilla_bias": "Biased: Vanilla",
"randomizer.item.sortingalgo.major_bias": "Biased: Major Items",
"randomizer.item.sortingalgo.dungeon_bias": "Biased: Dungeons",
"randomizer.item.sortingalgo.cluster_bias": "Biased: Clustered",
"randomizer.item.restrict_boss_items": "Forbidden Boss Items", "randomizer.item.restrict_boss_items": "Forbidden Boss Items",
"randomizer.item.restrict_boss_items.none": "None", "randomizer.item.restrict_boss_items.none": "None",

View File

@@ -116,13 +116,12 @@
"type": "selectbox", "type": "selectbox",
"default": "balanced", "default": "balanced",
"options": [ "options": [
"freshness", "balanced",
"flood", "equitable",
"vt21", "vanilla_bias",
"vt22", "major_bias",
"vt25", "dungeon_bias",
"vt26", "cluster_bias"
"balanced"
] ]
}, },
"restrict_boss_items": { "restrict_boss_items": {

881
source/item/BiasedFill.py Normal file
View File

@@ -0,0 +1,881 @@
import RaceRandom as random
import logging
from collections import defaultdict
from DoorShuffle import validate_vanilla_reservation
from Dungeons import dungeon_table
from Items import item_table, ItemFactory
class ItemPoolConfig(object):
def __init__(self):
self.location_groups = None
self.static_placement = None
self.item_pool = None
self.placeholders = None
self.reserved_locations = defaultdict(set)
class LocationGroup(object):
def __init__(self, name):
self.name = name
self.locations = []
# flags
self.keyshuffle = False
self.keydropshuffle = False
self.shopsanity = False
self.retro = False
def locs(self, locs):
self.locations = locs
return self
def flags(self, k, d=False, s=False, r=False):
self.keyshuffle = k
self.keydropshuffle = d
self.shopsanity = s
self.retro = r
return self
def create_item_pool_config(world):
world.item_pool_config = config = ItemPoolConfig()
player_set = set()
for player in range(1, world.players+1):
if world.restrict_boss_items[player] != 'none':
player_set.add(player)
if world.restrict_boss_items[player] == 'dungeon':
for dungeon, info in dungeon_table.items():
if info.prize:
d_name = "Thieves' Town" if dungeon.startswith('Thieves') else dungeon
config.reserved_locations[player].add(f'{d_name} - Boss')
for dungeon in world.dungeons:
for item in dungeon.all_items:
if item.map or item.compass:
item.advancement = True
if world.algorithm == 'vanilla_bias':
config.static_placement = {}
config.location_groups = {}
for player in range(1, world.players + 1):
config.static_placement[player] = vanilla_mapping.copy()
if world.keydropshuffle[player]:
for item, locs in keydrop_vanilla_mapping.items():
if item in config.static_placement[player]:
config.static_placement[player][item].extend(locs)
else:
config.static_placement[player][item] = list(locs)
# todo: shopsanity...
# todo: retro (universal keys...)
# retro + shops
config.location_groups[player] = [
LocationGroup('bkhp').locs(mode_grouping['Heart Pieces']),
LocationGroup('bktrash').locs(mode_grouping['Overworld Trash'] + mode_grouping['Dungeon Trash']),
LocationGroup('bkgt').locs(mode_grouping['GT Trash'])]
elif world.algorithm == 'major_bias':
config.location_groups = [
LocationGroup('MajorItems'),
LocationGroup('Backup')
]
config.item_pool = {}
init_set = mode_grouping['Overworld Major'] + mode_grouping['Big Chests'] + mode_grouping['Heart Containers']
for player in range(1, world.players + 1):
groups = LocationGroup('Major').locs(init_set)
if world.bigkeyshuffle[player]:
groups.locations.extend(mode_grouping['Big Keys'])
if world.keydropshuffle[player]:
groups.locations.append(mode_grouping['Big Key Drops'])
if world.keyshuffle[player]:
groups.locations.extend(mode_grouping['Small Keys'])
if world.keydropshuffle[player]:
groups.locations.extend(mode_grouping['Key Drops'])
if world.compassshuffle[player]:
groups.locations.extend(mode_grouping['Compasses'])
if world.mapshuffle[player]:
groups.locations.extend(mode_grouping['Maps'])
if world.shopsanity[player]:
groups.locations.append('Capacity Upgrade - Left')
groups.locations.append('Capacity Upgrade - Right')
if world.retro[player]:
if world.shopsanity[player]:
pass # todo: 5 locations for single arrow representation?
config.item_pool[player] = determine_major_items(world, player)
config.location_groups[0].locations = set(groups.locations)
backup = (mode_grouping['Heart Pieces'] + mode_grouping['Dungeon Trash'] + mode_grouping['Shops']
+ mode_grouping['Overworld Trash'] + mode_grouping['GT Trash'] + mode_grouping['RetroShops'])
config.location_groups[1].locations = set(backup)
elif world.algorithm == 'dungeon_bias':
config.location_groups = [
LocationGroup('Dungeons'),
LocationGroup('Backup')
]
config.item_pool = {}
dungeon_set = (mode_grouping['Big Chests'] + mode_grouping['Dungeon Trash'] + mode_grouping['Big Keys'] +
mode_grouping['Heart Containers'] + mode_grouping['GT Trash'] + mode_grouping['Small Keys'] +
mode_grouping['Compasses'] + mode_grouping['Maps'] + mode_grouping['Key Drops'] +
mode_grouping['Big Key Drops'])
for player in range(1, world.players + 1):
config.item_pool[player] = determine_major_items(world, player)
config.location_groups[0].locations = set(dungeon_set)
backup = (mode_grouping['Heart Pieces'] + mode_grouping['Overworld Major']
+ mode_grouping['Overworld Trash'] + mode_grouping['Shops'] + mode_grouping['RetroShops'])
config.location_groups[1].locations = set(backup)
elif world.algorithm == 'entangled' and world.players > 1:
config.location_groups = [
LocationGroup('Entangled'),
]
item_cnt = 0
config.item_pool = {}
limits = {}
for player in range(1, world.players + 1):
config.item_pool[player] = determine_major_items(world, player)
item_cnt += count_major_items(world, player)
limits[player] = calc_dungeon_limits(world, player)
c_set = {}
for location in world.get_locations():
if location.real and not location.forced_item:
c_set[location.name] = None
# todo: retroshop locations are created later, so count them here?
ttl_locations, candidates = 0, list(c_set.keys())
chosen_locations = defaultdict(set)
random.shuffle(candidates)
while ttl_locations < item_cnt:
choice = candidates.pop()
dungeon = world.get_location(choice, 1).parent_region.dungeon
if dungeon:
for player in range(1, world.players + 1):
location = world.get_location(choice, player)
if location.real and not location.forced_item:
if isinstance(limits[player], int):
if limits[player] > 0:
config.reserved_locations[player].add(choice)
limits[player] -= 1
chosen_locations[choice].add(player)
else:
previous = previously_reserved(location, world, player)
if limits[player][dungeon.name] > 0 or previous:
if validate_reservation(location, dungeon, world, player):
if not previous:
limits[player][dungeon.name] -= 1
chosen_locations[choice].add(player)
else: # not dungeon restricted
for player in range(1, world.players + 1):
location = world.get_location(choice, player)
if location.real and not location.forced_item:
chosen_locations[choice].add(player)
ttl_locations += len(chosen_locations[choice])
config.placeholders = ttl_locations - item_cnt
config.location_groups[0].locations = chosen_locations
def previously_reserved(location, world, player):
if '- Boss' in location.name:
if world.restrict_boss_items[player] == 'mapcompass' and (not world.compassshuffle[player]
or not world.mapshuffle[player]):
return True
if world.restrict_boss_items[player] == 'dungeon' and (not world.compassshuffle[player]
or not world.mapshuffle[player]
or not world.bigkeyshuffle[player]
or not (world.keyshuffle[player] or world.retro[player])):
return True
return False
def massage_item_pool(world):
player_pool = defaultdict(list)
for item in world.itempool:
player_pool[item.player].append(item)
for dungeon in world.dungeons:
for item in dungeon.all_items:
if item not in player_pool[item.player]: # filters out maps, compasses, etc
player_pool[item.player].append(item)
player_locations = defaultdict(list)
for player in player_pool:
player_locations[player] = [x for x in world.get_unfilled_locations(player) if '- Prize' not in x.name]
discrepancy = len(player_pool[player]) - len(player_locations[player])
if discrepancy:
trash_options = [x for x in player_pool[player] if x.name in trash_items]
random.shuffle(trash_options)
trash_options = sorted(trash_options, key=lambda x: trash_items[x.name], reverse=True)
while discrepancy > 0 and len(trash_options) > 0:
deleted = trash_options.pop()
world.itempool.remove(deleted)
discrepancy -= 1
if discrepancy > 0:
logging.getLogger('').warning(f'Too many good items in pool, something will be removed at random')
if world.item_pool_config.placeholders is not None:
removed = 0
single_rupees = [item for item in world.itempool if item.name == 'Rupee (1)']
removed += len(single_rupees)
for x in single_rupees:
world.itempool.remove(x)
if removed < world.item_pool_config.placeholders:
trash_options = [x for x in world.itempool if x.name in trash_items]
random.shuffle(trash_options)
trash_options = sorted(trash_options, key=lambda x: trash_items[x.name], reverse=True)
while removed < world.item_pool_config.placeholders:
if len(trash_options) == 0:
logging.getLogger('').warning(f'Too many good items in pool, not enough room for placeholders')
deleted = trash_options.pop()
world.itempool.remove(deleted)
removed += 1
placeholders = random.sample(single_rupees, world.item_pool_config.placeholders)
world.itempool += placeholders
removed -= len(placeholders)
for _ in range(removed):
world.itempool.append(ItemFactory('Rupees (5)', random.randint(1, world.players)))
def validate_reservation(location, dungeon, world, player):
world.item_pool_config.reserved_locations[player].add(location.name)
if world.doorShuffle[player] != 'vanilla':
return True # we can generate the dungeon somehow most likely
if validate_vanilla_reservation(dungeon, world, player):
return True
world.item_pool_config.reserved_locations[player].remove(location.name)
return False
def count_major_items(world, player):
major_item_set = 52
if world.bigkeyshuffle[player]:
major_item_set += 11
if world.keydropshuffle[player]:
major_item_set += 1
if world.doorShuffle[player] == 'crossed':
major_item_set += 1
if world.keyshuffle[player]:
major_item_set += 29
if world.keydropshuffle[player]:
major_item_set += 32
if world.compassshuffle[player]:
major_item_set += 11
if world.doorShuffle[player] == 'crossed':
major_item_set += 2
if world.mapshuffle[player]:
major_item_set += 12
if world.doorShuffle[player] == 'crossed':
major_item_set += 1
if world.shopsanity[player]:
major_item_set += 2
if world.retro[player]:
major_item_set += 5 # the single arrow quiver
if world.goal == 'triforcehunt':
major_item_set += world.triforce_pool[player]
if world.bombbag[player]:
major_item_set += world.triforce_pool[player]
# todo: vanilla, assured, swordless?
# if world.swords[player] != "random":
# if world.swords[player] == 'assured':
# major_item_set -= 1
# if world.swords[player] in ['vanilla', 'swordless']:
# major_item_set -= 4
# todo: starting equipment?
return major_item_set
def calc_dungeon_limits(world, player):
b, s, c, m, k, r, bi = (world.bigkeyshuffle[player], world.keyshuffle[player], world.compassshuffle[player],
world.mapshuffle[player], world.keydropshuffle[player], world.retro[player],
world.restrict_boss_items[player])
if world.doorShuffle[player] in ['vanilla', 'basic']:
limits = {}
for dungeon, info in dungeon_table.items():
val = info.free_items
if bi != 'none' and info.prize:
if bi == 'mapcompass' and (not c or not m):
val -= 1
if bi == 'dungeon' and (not c or not m or not (s or r) or not b):
val -= 1
if b:
val += 1 if info.bk_present else 0
if k:
val += 1 if info.bk_drops else 0
if s or r:
val += info.key_num
if k:
val += info.key_drops
if c:
val += 1 if info.compass_present else 0
if m:
val += 1 if info.map_present else 0
limits[dungeon] = val
else:
limits = 60
if world.bigkeyshuffle[player]:
limits += 11
if world.keydropshuffle[player]:
limits += 1
if world.keyshuffle[player] or world.retro[player]:
limits += 29
if world.keydropshuffle[player]:
limits += 32
if world.compassshuffle[player]:
limits += 11
if world.mapshuffle[player]:
limits += 12
return limits
def determine_major_items(world, player):
major_item_set = set(major_items)
if world.bigkeyshuffle[player]:
major_item_set.update({x for x, y in item_table.items() if y[2] == 'BigKey'})
if world.keyshuffle[player]:
major_item_set.update({x for x, y in item_table.items() if y[2] == 'SmallKey'})
if world.compassshuffle[player]:
major_item_set.update({x for x, y in item_table.items() if y[2] == 'Compass'})
if world.mapshuffle[player]:
major_item_set.update({x for x, y in item_table.items() if y[2] == 'Map'})
if world.shopsanity[player]:
major_item_set.add('Bomb Upgrade (+5)')
major_item_set.add('Arrow Upgrade (+5)')
if world.retro[player]:
major_item_set.add('Single Arrow')
major_item_set.add('Small Key (Universal)')
if world.goal == 'triforcehunt':
major_item_set.add('Triforce Piece')
if world.bombbag[player]:
major_item_set.add('Bomb Upgrade (+10)')
return major_item_set
def classify_major_items(world):
if world.algorithm in ['major_bias', 'dungeon_bias', 'cluster_bias'] or (world.algorithm == 'entangled'
and world.players > 1):
config = world.item_pool_config
for item in world.itempool:
if item.name in config.item_pool[item.player]:
if not item.advancement or not item.priority:
if item.smallkey or item.bigkey:
item.advancement = True
else:
item.priority = True
def split_pool(pool, world):
# bias or entangled
config = world.item_pool_config
priority, secondary = [], []
for item in pool:
if item.name in config.item_pool[item.player]:
priority.append(item)
else:
secondary.append(item)
return priority, secondary
def filter_locations(item_to_place, locations, world):
if world.algorithm == 'vanilla_bias':
config, filtered = world.item_pool_config, []
item_name = 'Bottle' if item_to_place.name.startswith('Bottle') else item_to_place.name
if item_name in config.static_placement[item_to_place.player]:
restricted = config.static_placement[item_to_place.player][item_name]
filtered = [l for l in locations if l.player == item_to_place.player and l.name in restricted]
i = 0
while len(filtered) <= 0:
if i >= len(config.location_groups[item_to_place.player]):
return locations
restricted = config.location_groups[item_to_place.player][i].locations
filtered = [l for l in locations if l.player == item_to_place.player and l.name in restricted]
i += 1
return filtered
if world.algorithm in ['major_bias', 'dungeon_bias']:
config = world.item_pool_config
if item_to_place.name in config.item_pool[item_to_place.player]:
restricted = config.location_groups[0].locations
filtered = [l for l in locations if l.name in restricted]
if len(filtered) == 0:
restricted = config.location_groups[1].locations
filtered = [l for l in locations if l.name in restricted]
# bias toward certain location in overflow? (thinking about this for major_bias)
return filtered if len(filtered) > 0 else locations
if world.algorithm == 'entangled' and world.players > 1:
config = world.item_pool_config
if item_to_place == 'Placeholder' or item_to_place.name in config.item_pool[item_to_place.player]:
restricted = config.location_groups[0].locations
filtered = [l for l in locations if l.name in restricted and l.player in restricted[l.name]]
return filtered if len(filtered) > 0 else locations
return locations
vanilla_mapping = {
'Green Pendant': ['Eastern Palace - Prize'],
'Red Pendant': ['Desert Palace - Prize', 'Tower of Hera - Prize'],
'Blue Pendant': ['Desert Palace - Prize', 'Tower of Hera - Prize'],
'Crystal 1': ['Palace of Darkness - Prize', 'Swamp Palace - Prize', 'Thieves\' Town - Prize',
'Skull Woods - Prize', 'Turtle Rock - Prize'],
'Crystal 2': ['Palace of Darkness - Prize', 'Swamp Palace - Prize', 'Thieves\' Town - Prize',
'Skull Woods - Prize', 'Turtle Rock - Prize'],
'Crystal 3': ['Palace of Darkness - Prize', 'Swamp Palace - Prize', 'Thieves\' Town - Prize',
'Skull Woods - Prize', 'Turtle Rock - Prize'],
'Crystal 4': ['Palace of Darkness - Prize', 'Swamp Palace - Prize', 'Thieves\' Town - Prize',
'Skull Woods - Prize', 'Turtle Rock - Prize'],
'Crystal 7': ['Palace of Darkness - Prize', 'Swamp Palace - Prize', 'Thieves\' Town - Prize',
'Skull Woods - Prize', 'Turtle Rock - Prize'],
'Crystal 5': ['Ice Palace - Prize', 'Misery Mire - Prize'],
'Crystal 6': ['Ice Palace - Prize', 'Misery Mire - Prize'],
'Bow': ['Eastern Palace - Big Chest'],
'Progressive Bow': ['Eastern Palace - Big Chest', 'Pyramid Fairy - Left'],
'Book of Mudora': ['Library'],
'Hammer': ['Palace of Darkness - Big Chest'],
'Hookshot': ['Swamp Palace - Big Chest'],
'Magic Mirror': ['Old Man'],
'Ocarina': ['Flute Spot'],
'Pegasus Boots': ['Sahasrahla'],
'Power Glove': ['Desert Palace - Big Chest'],
'Cape': ["King's Tomb"],
'Mushroom': ['Mushroom'],
'Shovel': ['Stumpy'],
'Lamp': ["Link's House"],
'Magic Powder': ['Potion Shop'],
'Moon Pearl': ['Tower of Hera - Big Chest'],
'Cane of Somaria': ['Misery Mire - Big Chest'],
'Fire Rod': ['Skull Woods - Big Chest'],
'Flippers': ['King Zora'],
'Ice Rod': ['Ice Rod Cave'],
'Titans Mitts': ["Thieves' Town - Big Chest"],
'Bombos': ['Bombos Tablet'],
'Ether': ['Ether Tablet'],
'Quake': ['Catfish'],
'Bottle': ['Bottle Merchant', 'Kakariko Tavern', 'Purple Chest', 'Hobo'],
'Master Sword': ['Master Sword Pedestal'],
'Tempered Sword': ['Blacksmith'],
'Fighter Sword': ["Link's Uncle"],
'Golden Sword': ['Pyramid Fairy - Right'],
'Progressive Sword': ["Link's Uncle", 'Blacksmith', 'Master Sword Pedestal', 'Pyramid Fairy - Right'],
'Progressive Glove': ['Desert Palace - Big Chest', "Thieves' Town - Big Chest"],
'Silver Arrows': ['Pyramid Fairy - Left'],
'Single Arrow': ['Palace of Darkness - Dark Basement - Left'],
'Arrows (10)': ['Chicken House', 'Mini Moldorm Cave - Far Right', 'Sewers - Secret Room - Right',
'Paradox Cave Upper - Right', 'Mire Shed - Right', 'Ganons Tower - Hope Room - Left',
'Ganons Tower - Compass Room - Bottom Right', 'Ganons Tower - DMs Room - Top Right',
'Ganons Tower - Randomizer Room - Top Left', 'Ganons Tower - Randomizer Room - Top Right',
"Ganons Tower - Bob's Chest", 'Ganons Tower - Big Key Room - Left'],
'Bombs (3)': ['Floodgate Chest', "Sahasrahla's Hut - Middle", 'Kakariko Well - Bottom', 'Superbunny Cave - Top',
'Mini Moldorm Cave - Far Left', 'Sewers - Secret Room - Left', 'Paradox Cave Upper - Left',
"Thieves' Town - Attic", 'Ice Palace - Freezor Chest', 'Palace of Darkness - Dark Maze - Top',
'Ganons Tower - Hope Room - Right', 'Ganons Tower - DMs Room - Top Left',
'Ganons Tower - Randomizer Room - Bottom Left', 'Ganons Tower - Randomizer Room - Bottom Right',
'Ganons Tower - Big Key Room - Right', 'Ganons Tower - Mini Helmasaur Room - Left',
'Ganons Tower - Mini Helmasaur Room - Right'],
'Blue Mail': ['Ice Palace - Big Chest'],
'Red Mail': ['Ganons Tower - Big Chest'],
'Progressive Armor': ['Ice Palace - Big Chest', 'Ganons Tower - Big Chest'],
'Blue Boomerang': ['Hyrule Castle - Boomerang Chest'],
'Red Boomerang': ['Waterfall Fairy - Left'],
'Blue Shield': ['Secret Passage'],
'Red Shield': ['Waterfall Fairy - Right'],
'Mirror Shield': ['Turtle Rock - Big Chest'],
'Progressive Shield': ['Secret Passage', 'Waterfall Fairy - Right', 'Turtle Rock - Big Chest'],
'Bug Catching Net': ['Sick Kid'],
'Cane of Byrna': ['Spike Cave'],
'Boss Heart Container': ['Desert Palace - Boss', 'Eastern Palace - Boss', 'Tower of Hera - Boss',
'Swamp Palace - Boss', "Thieves' Town - Boss", 'Skull Woods - Boss', 'Ice Palace - Boss',
'Misery Mire - Boss', 'Turtle Rock - Boss', 'Palace of Darkness - Boss'],
'Sanctuary Heart Container': ['Sanctuary'],
'Piece of Heart': ['Sunken Treasure', "Blind's Hideout - Top", "Zora's Ledge", "Aginah's Cave", 'Maze Race',
'Kakariko Well - Top', 'Lost Woods Hideout', 'Lumberjack Tree', 'Cave 45', 'Graveyard Cave',
'Checkerboard Cave', 'Bonk Rock Cave', 'Lake Hylia Island', 'Desert Ledge', 'Spectacle Rock',
'Spectacle Rock Cave', 'Pyramid', 'Digging Game', 'Peg Cave', 'Chest Game', 'Bumper Cave Ledge',
'Mire Shed - Left', 'Floating Island', 'Mimic Cave'],
'Rupee (1)': ['Turtle Rock - Eye Bridge - Top Right', 'Ganons Tower - Compass Room - Top Right'],
'Rupees (5)': ["Hyrule Castle - Zelda's Chest", 'Turtle Rock - Eye Bridge - Top Left',
# 'Palace of Darkness - Harmless Hellway',
'Palace of Darkness - Dark Maze - Bottom',
'Ganons Tower - Validation Chest'],
'Rupees (20)': ["Blind's Hideout - Left", "Blind's Hideout - Right", "Blind's Hideout - Far Left",
"Blind's Hideout - Far Right", 'Kakariko Well - Left', 'Kakariko Well - Middle',
'Kakariko Well - Right', 'Mini Moldorm Cave - Left', 'Mini Moldorm Cave - Right',
'Paradox Cave Lower - Far Left', 'Paradox Cave Lower - Left', 'Paradox Cave Lower - Right',
'Paradox Cave Lower - Far Right', 'Paradox Cave Lower - Middle', 'Hype Cave - Top',
'Hype Cave - Middle Right', 'Hype Cave - Middle Left', 'Hype Cave - Bottom',
'Swamp Palace - West Chest', 'Swamp Palace - Flooded Room - Left', 'Swamp Palace - Waterfall Room',
'Swamp Palace - Flooded Room - Right', "Thieves' Town - Ambush Chest",
'Turtle Rock - Eye Bridge - Bottom Right', 'Ganons Tower - Compass Room - Bottom Left',
'Swamp Palace - Flooded Room - Right', "Thieves' Town - Ambush Chest",
'Ganons Tower - DMs Room - Bottom Left', 'Ganons Tower - DMs Room - Bottom Right'],
'Rupees (50)': ["Sahasrahla's Hut - Left", "Sahasrahla's Hut - Right", 'Spiral Cave', 'Superbunny Cave - Bottom',
'Hookshot Cave - Top Right', 'Hookshot Cave - Top Left', 'Hookshot Cave - Bottom Right',
'Hookshot Cave - Bottom Left'],
'Rupees (100)': ['Eastern Palace - Cannonball Chest'],
'Rupees (300)': ['Mini Moldorm Cave - Generous Guy', 'Sewers - Secret Room - Middle', 'Hype Cave - Generous Guy',
'Brewery', 'C-Shaped House'],
'Magic Upgrade (1/2)': ['Magic Bat'],
'Big Key (Eastern Palace)': ['Eastern Palace - Big Key Chest'],
'Compass (Eastern Palace)': ['Eastern Palace - Compass Chest'],
'Map (Eastern Palace)': ['Eastern Palace - Map Chest'],
'Small Key (Desert Palace)': ['Desert Palace - Torch'],
'Big Key (Desert Palace)': ['Desert Palace - Big Key Chest'],
'Compass (Desert Palace)': ['Desert Palace - Compass Chest'],
'Map (Desert Palace)': ['Desert Palace - Map Chest'],
'Small Key (Tower of Hera)': ['Tower of Hera - Basement Cage'],
'Big Key (Tower of Hera)': ['Tower of Hera - Big Key Chest'],
'Compass (Tower of Hera)': ['Tower of Hera - Compass Chest'],
'Map (Tower of Hera)': ['Tower of Hera - Map Chest'],
'Small Key (Escape)': ['Sewers - Dark Cross'],
'Map (Escape)': ['Hyrule Castle - Map Chest'],
'Small Key (Agahnims Tower)': ['Castle Tower - Room 03', 'Castle Tower - Dark Maze'],
'Small Key (Palace of Darkness)': ['Palace of Darkness - Shooter Room', 'Palace of Darkness - The Arena - Bridge',
'Palace of Darkness - Stalfos Basement',
'Palace of Darkness - The Arena - Ledge',
'Palace of Darkness - Dark Basement - Right',
'Palace of Darkness - Harmless Hellway'],
# 'Palace of Darkness - Dark Maze - Bottom'],
'Big Key (Palace of Darkness)': ['Palace of Darkness - Big Key Chest'],
'Compass (Palace of Darkness)': ['Palace of Darkness - Compass Chest'],
'Map (Palace of Darkness)': ['Palace of Darkness - Map Chest'],
'Small Key (Thieves Town)': ["Thieves' Town - Blind's Cell"],
'Big Key (Thieves Town)': ["Thieves' Town - Big Key Chest"],
'Compass (Thieves Town)': ["Thieves' Town - Compass Chest"],
'Map (Thieves Town)': ["Thieves' Town - Map Chest"],
'Small Key (Skull Woods)': ['Skull Woods - Pot Prison', 'Skull Woods - Pinball Room', 'Skull Woods - Bridge Room'],
'Big Key (Skull Woods)': ['Skull Woods - Big Key Chest'],
'Compass (Skull Woods)': ['Skull Woods - Compass Chest'],
'Map (Skull Woods)': ['Skull Woods - Map Chest'],
'Small Key (Swamp Palace)': ['Swamp Palace - Entrance'],
'Big Key (Swamp Palace)': ['Swamp Palace - Big Key Chest'],
'Compass (Swamp Palace)': ['Swamp Palace - Compass Chest'],
'Map (Swamp Palace)': ['Swamp Palace - Map Chest'],
'Small Key (Ice Palace)': ['Ice Palace - Iced T Room', 'Ice Palace - Spike Room'],
'Big Key (Ice Palace)': ['Ice Palace - Big Key Chest'],
'Compass (Ice Palace)': ['Ice Palace - Compass Chest'],
'Map (Ice Palace)': ['Ice Palace - Map Chest'],
'Small Key (Misery Mire)': ['Misery Mire - Main Lobby', 'Misery Mire - Bridge Chest', 'Misery Mire - Spike Chest'],
'Big Key (Misery Mire)': ['Misery Mire - Big Key Chest'],
'Compass (Misery Mire)': ['Misery Mire - Compass Chest'],
'Map (Misery Mire)': ['Misery Mire - Map Chest'],
'Small Key (Turtle Rock)': ['Turtle Rock - Roller Room - Right', 'Turtle Rock - Chain Chomps',
'Turtle Rock - Crystaroller Room', 'Turtle Rock - Eye Bridge - Bottom Left'],
'Big Key (Turtle Rock)': ['Turtle Rock - Big Key Chest'],
'Compass (Turtle Rock)': ['Turtle Rock - Compass Chest'],
'Map (Turtle Rock)': ['Turtle Rock - Roller Room - Left'],
'Small Key (Ganons Tower)': ["Ganons Tower - Bob's Torch", 'Ganons Tower - Tile Room',
'Ganons Tower - Firesnake Room', 'Ganons Tower - Pre-Moldorm Chest'],
'Big Key (Ganons Tower)': ['Ganons Tower - Big Key Chest'],
'Compass (Ganons Tower)': ['Ganons Tower - Compass Room - Top Left'],
'Map (Ganons Tower)': ['Ganons Tower - Map Chest']
}
keydrop_vanilla_mapping = {
'Small Key (Desert Palace)': ['Desert Palace - Desert Tiles 1 Pot Key',
'Desert Palace - Beamos Hall Pot Key', 'Desert Palace - Desert Tiles 2 Pot Key'],
'Small Key (Eastern Palace)': ['Eastern Palace - Dark Square Pot Key', 'Eastern Palace - Dark Eyegore Key Drop'],
'Small Key (Escape)': ['Hyrule Castle - Map Guard Key Drop', 'Hyrule Castle - Boomerang Guard Key Drop',
'Hyrule Castle - Key Rat Key Drop'],
'Big Key (Escape)': ['Hyrule Castle - Big Key Drop'],
'Small Key (Agahnims Tower)': ['Castle Tower - Dark Archer Key Drop', 'Castle Tower - Circle of Pots Key Drop'],
'Small Key (Thieves Town)': ["Thieves' Town - Hallway Pot Key", "Thieves' Town - Spike Switch Pot Key"],
'Small Key (Skull Woods)': ['Skull Woods - West Lobby Pot Key', 'Skull Woods - Spike Corner Key Drop'],
'Small Key (Swamp Palace)': ['Swamp Palace - Pot Row Pot Key', 'Swamp Palace - Trench 1 Pot Key',
'Swamp Palace - Hookshot Pot Key', 'Swamp Palace - Trench 2 Pot Key',
'Swamp Palace - Waterway Pot Key'],
'Small Key (Ice Palace)': ['Ice Palace - Jelly Key Drop', 'Ice Palace - Conveyor Key Drop',
'Ice Palace - Hammer Block Key Drop', 'Ice Palace - Many Pots Pot Key'],
'Small Key (Misery Mire)': ['Misery Mire - Spikes Pot Key',
'Misery Mire - Fishbone Pot Key', 'Misery Mire - Conveyor Crystal Key Drop'],
'Small Key (Turtle Rock)': ['Turtle Rock - Pokey 1 Key Drop', 'Turtle Rock - Pokey 2 Key Drop'],
'Small Key (Ganons Tower)': ['Ganons Tower - Conveyor Cross Pot Key', 'Ganons Tower - Double Switch Pot Key',
'Ganons Tower - Conveyor Star Pits Pot Key', 'Ganons Tower - Mini Helmasuar Key Drop'],
}
mode_grouping = {
'Overworld Major': [
"Link's Uncle", 'King Zora', "Link's House", 'Sahasrahla', 'Ice Rod Cave', 'Library',
'Master Sword Pedestal', 'Old Man', 'Ether Tablet', 'Catfish', 'Stumpy', 'Bombos Tablet', 'Mushroom',
'Bottle Merchant', 'Kakariko Tavern', 'Secret Passage', 'Flute Spot', 'Purple Chest',
'Waterfall Fairy - Left', 'Waterfall Fairy - Right', 'Blacksmith', 'Magic Bat', 'Sick Kid', 'Hobo',
'Potion Shop', 'Spike Cave', 'Pyramid Fairy - Left', 'Pyramid Fairy - Right', "King's Tomb",
],
'Big Chests': ['Eastern Palace - Big Chest','Desert Palace - Big Chest', 'Tower of Hera - Big Chest',
'Palace of Darkness - Big Chest', 'Swamp Palace - Big Chest', 'Skull Woods - Big Chest',
"Thieves' Town - Big Chest", 'Misery Mire - Big Chest', 'Hyrule Castle - Boomerang Chest',
'Ice Palace - Big Chest', 'Turtle Rock - Big Chest', 'Ganons Tower - Big Chest'],
'Heart Containers': ['Sanctuary', 'Eastern Palace - Boss','Desert Palace - Boss', 'Tower of Hera - Boss',
'Palace of Darkness - Boss', 'Swamp Palace - Boss', 'Skull Woods - Boss',
"Thieves' Town - Boss", 'Ice Palace - Boss', 'Misery Mire - Boss', 'Turtle Rock - Boss'],
'Heart Pieces': [
'Bumper Cave Ledge', 'Desert Ledge', 'Lake Hylia Island', 'Floating Island',
'Maze Race', 'Spectacle Rock', 'Pyramid', "Zora's Ledge", 'Lumberjack Tree',
'Sunken Treasure', 'Spectacle Rock Cave', 'Lost Woods Hideout', 'Checkerboard Cave', 'Peg Cave', 'Cave 45',
'Graveyard Cave', 'Kakariko Well - Top', "Blind's Hideout - Top", 'Bonk Rock Cave', "Aginah's Cave",
'Chest Game', 'Digging Game', 'Mire Shed - Right', 'Mimic Cave'
],
'Big Keys': [
'Eastern Palace - Big Key Chest', 'Ganons Tower - Big Key Chest',
'Desert Palace - Big Key Chest', 'Tower of Hera - Big Key Chest', 'Palace of Darkness - Big Key Chest',
'Swamp Palace - Big Key Chest', "Thieves' Town - Big Key Chest", 'Skull Woods - Big Key Chest',
'Ice Palace - Big Key Chest', 'Misery Mire - Big Key Chest', 'Turtle Rock - Big Key Chest',
],
'Compasses': [
'Eastern Palace - Compass Chest', 'Desert Palace - Compass Chest', 'Tower of Hera - Compass Chest',
'Palace of Darkness - Compass Chest', 'Swamp Palace - Compass Chest', 'Skull Woods - Compass Chest',
"Thieves' Town - Compass Chest", 'Ice Palace - Compass Chest', 'Misery Mire - Compass Chest',
'Turtle Rock - Compass Chest', 'Ganons Tower - Compass Room - Top Left'
],
'Maps': [
'Hyrule Castle - Map Chest', 'Eastern Palace - Map Chest', 'Desert Palace - Map Chest',
'Tower of Hera - Map Chest', 'Palace of Darkness - Map Chest', 'Swamp Palace - Map Chest',
'Skull Woods - Map Chest', "Thieves' Town - Map Chest", 'Ice Palace - Map Chest', 'Misery Mire - Map Chest',
'Turtle Rock - Roller Room - Left', 'Ganons Tower - Map Chest'
],
'Small Keys': [
'Sewers - Dark Cross', 'Desert Palace - Torch', 'Tower of Hera - Basement Cage',
'Castle Tower - Room 03', 'Castle Tower - Dark Maze',
'Palace of Darkness - Stalfos Basement', 'Palace of Darkness - Dark Basement - Right',
'Palace of Darkness - Dark Maze - Bottom', 'Palace of Darkness - Shooter Room',
'Palace of Darkness - The Arena - Bridge', 'Palace of Darkness - The Arena - Ledge',
"Thieves' Town - Blind's Cell", 'Skull Woods - Bridge Room', 'Ice Palace - Spike Room',
'Skull Woods - Pot Prison', 'Skull Woods - Pinball Room', 'Misery Mire - Spike Chest',
'Ice Palace - Iced T Room', 'Misery Mire - Main Lobby', 'Misery Mire - Bridge Chest', 'Swamp Palace - Entrance',
'Turtle Rock - Chain Chomps', 'Turtle Rock - Crystaroller Room', 'Turtle Rock - Roller Room - Right',
'Turtle Rock - Eye Bridge - Bottom Left', "Ganons Tower - Bob's Torch", 'Ganons Tower - Tile Room',
'Ganons Tower - Firesnake Room', 'Ganons Tower - Pre-Moldorm Chest'
],
'Dungeon Trash': [
'Sewers - Secret Room - Right', 'Sewers - Secret Room - Left', 'Sewers - Secret Room - Middle',
"Hyrule Castle - Zelda's Chest", 'Eastern Palace - Cannonball Chest', "Thieves' Town - Ambush Chest",
"Thieves' Town - Attic", 'Ice Palace - Freezor Chest', 'Palace of Darkness - Dark Basement - Left',
'Palace of Darkness - Dark Maze - Bottom', 'Palace of Darkness - Dark Maze - Top',
'Swamp Palace - Flooded Room - Left', 'Swamp Palace - Flooded Room - Right', 'Swamp Palace - Waterfall Room',
'Turtle Rock - Eye Bridge - Bottom Right', 'Turtle Rock - Eye Bridge - Top Left',
'Turtle Rock - Eye Bridge - Top Right', 'Swamp Palace - West Chest',
],
'Overworld Trash': [
"Blind's Hideout - Left", "Blind's Hideout - Right", "Blind's Hideout - Far Left",
"Blind's Hideout - Far Right", 'Kakariko Well - Left', 'Kakariko Well - Middle', 'Kakariko Well - Right',
'Kakariko Well - Bottom', 'Chicken House', 'Floodgate Chest', 'Mini Moldorm Cave - Left',
'Mini Moldorm Cave - Right', 'Mini Moldorm Cave - Generous Guy', 'Mini Moldorm Cave - Far Left',
'Mini Moldorm Cave - Far Right', "Sahasrahla's Hut - Left", "Sahasrahla's Hut - Right",
"Sahasrahla's Hut - Middle", 'Paradox Cave Lower - Far Left', 'Paradox Cave Lower - Left',
'Paradox Cave Lower - Right', 'Paradox Cave Lower - Far Right', 'Paradox Cave Lower - Middle',
'Paradox Cave Upper - Left', 'Paradox Cave Upper - Right', 'Spiral Cave', 'Brewery', 'C-Shaped House',
'Hype Cave - Top', 'Hype Cave - Middle Right', 'Hype Cave - Middle Left', 'Hype Cave - Bottom',
'Hype Cave - Generous Guy', 'Superbunny Cave - Bottom', 'Superbunny Cave - Top', 'Hookshot Cave - Top Right',
'Hookshot Cave - Top Left', 'Hookshot Cave - Bottom Right', 'Hookshot Cave - Bottom Left', 'Mire Shed - Left'
],
'GT Trash': [
'Ganons Tower - DMs Room - Top Right', 'Ganons Tower - DMs Room - Top Left',
'Ganons Tower - DMs Room - Bottom Left', 'Ganons Tower - DMs Room - Bottom Right',
'Ganons Tower - Compass Room - Top Right', 'Ganons Tower - Compass Room - Bottom Right',
'Ganons Tower - Compass Room - Bottom Left', 'Ganons Tower - Hope Room - Left',
'Ganons Tower - Hope Room - Right', 'Ganons Tower - Randomizer Room - Top Left',
'Ganons Tower - Randomizer Room - Top Right', 'Ganons Tower - Randomizer Room - Bottom Right',
'Ganons Tower - Randomizer Room - Bottom Left', "Ganons Tower - Bob's Chest",
'Ganons Tower - Big Key Room - Left', 'Ganons Tower - Big Key Room - Right',
'Ganons Tower - Mini Helmasaur Room - Left', 'Ganons Tower - Mini Helmasaur Room - Right',
'Ganons Tower - Validation Chest',
],
'Key Drops': [
'Hyrule Castle - Map Guard Key Drop', 'Hyrule Castle - Boomerang Guard Key Drop',
'Hyrule Castle - Key Rat Key Drop', 'Eastern Palace - Dark Square Pot Key',
'Eastern Palace - Dark Eyegore Key Drop', 'Desert Palace - Desert Tiles 1 Pot Key',
'Desert Palace - Beamos Hall Pot Key', 'Desert Palace - Desert Tiles 2 Pot Key',
'Castle Tower - Dark Archer Key Drop', 'Castle Tower - Circle of Pots Key Drop',
'Swamp Palace - Pot Row Pot Key', 'Swamp Palace - Trench 1 Pot Key', 'Swamp Palace - Hookshot Pot Key',
'Swamp Palace - Trench 2 Pot Key', 'Swamp Palace - Waterway Pot Key', 'Skull Woods - West Lobby Pot Key',
'Skull Woods - Spike Corner Key Drop', "Thieves' Town - Hallway Pot Key",
"Thieves' Town - Spike Switch Pot Key", 'Ice Palace - Jelly Key Drop', 'Ice Palace - Conveyor Key Drop',
'Ice Palace - Hammer Block Key Drop', 'Ice Palace - Many Pots Pot Key', 'Misery Mire - Spikes Pot Key',
'Misery Mire - Fishbone Pot Key', 'Misery Mire - Conveyor Crystal Key Drop', 'Turtle Rock - Pokey 1 Key Drop',
'Turtle Rock - Pokey 2 Key Drop', 'Ganons Tower - Conveyor Cross Pot Key',
'Ganons Tower - Double Switch Pot Key', 'Ganons Tower - Conveyor Star Pits Pot Key',
'Ganons Tower - Mini Helmasuar Key Drop',
],
'Big Key Drops': ['Hyrule Castle - Big Key Drop'],
'Shops': [
'Dark Death Mountain Shop - Left', 'Dark Death Mountain Shop - Middle', 'Dark Death Mountain Shop - Right',
'Red Shield Shop - Left', 'Red Shield Shop - Middle', 'Red Shield Shop - Right', 'Dark Lake Hylia Shop - Left',
'Dark Lake Hylia Shop - Middle', 'Dark Lake Hylia Shop - Right', 'Dark Lumberjack Shop - Left',
'Dark Lumberjack Shop - Middle', 'Dark Lumberjack Shop - Right', 'Village of Outcasts Shop - Left',
'Village of Outcasts Shop - Middle', 'Village of Outcasts Shop - Right', 'Dark Potion Shop - Left',
'Dark Potion Shop - Middle', 'Dark Potion Shop - Right', 'Paradox Shop - Left', 'Paradox Shop - Middle',
'Paradox Shop - Right', 'Kakariko Shop - Left', 'Kakariko Shop - Middle', 'Kakariko Shop - Right',
'Lake Hylia Shop - Left', 'Lake Hylia Shop - Middle', 'Lake Hylia Shop - Right', 'Capacity Upgrade - Left',
'Capacity Upgrade - Right'
],
'RetroShops': [
'Old Man Sword Cave Item 1', 'Take-Any #1 Item 1', 'Take-Any #1 Item 2', 'Take-Any #2 Item 1',
'Take-Any #2 Item 2', 'Take-Any #3 Item 1', 'Take-Any #3 Item 2','Take-Any #4 Item 1', 'Take-Any #4 Item 2'
]
}
major_items = {'Bombos', 'Book of Mudora', 'Cane of Somaria', 'Ether', 'Fire Rod', 'Flippers', 'Ocarina', 'Hammer',
'Hookshot', 'Ice Rod', 'Lamp', 'Cape', 'Magic Powder', 'Mushroom', 'Pegasus Boots', 'Quake', 'Shovel',
'Bug Catching Net', 'Cane of Byrna', 'Blue Boomerang', 'Red Boomerang', 'Progressive Glove',
'Power Glove', 'Titans Mitts', 'Bottle', 'Bottle (Red Potion)', 'Bottle (Green Potion)', 'Magic Mirror',
'Bottle (Blue Potion)', 'Bottle (Fairy)', 'Bottle (Bee)', 'Bottle (Good Bee)', 'Magic Upgrade (1/2)',
'Sanctuary Heart Container', 'Boss Heart Container', 'Progressive Shield', 'Blue Shield', 'Red Shield',
'Mirror Shield', 'Progressive Armor', 'Blue Mail', 'Red Mail', 'Progressive Sword', 'Fighter Sword',
'Master Sword', 'Tempered Sword', 'Golden Sword', 'Bow', 'Silver Arrows', 'Triforce Piece', 'Moon Pearl',
'Progressive Bow', 'Progressive Bow (Alt)'}
# todo: re-enter these
clustered_groups = [
LocationGroup("MajorRoute1").locs([
'Library', 'Master Sword Pedestal', 'Old Man', 'Flute Spot',
'Ether Tablet', 'Stumpy', 'Bombos Tablet', 'Mushroom', 'Bottle Merchant', 'Kakariko Tavern',
'Sick Kid', 'Pyramid Fairy - Left', 'Pyramid Fairy - Right'
]),
LocationGroup("MajorRoute2").locs([
'King Zora', 'Sahasrahla', 'Ice Rod Cave', 'Catfish',
'Purple Chest', 'Waterfall Fairy - Left', 'Waterfall Fairy - Right', 'Blacksmith',
'Magic Bat', 'Hobo', 'Potion Shop', 'Spike Cave', "King's Tomb"
]),
LocationGroup("BigChest").locs([
'Sanctuary', 'Eastern Palace - Big Chest',
'Desert Palace - Big Chest', 'Tower of Hera - Big Chest', 'Palace of Darkness - Big Chest',
'Swamp Palace - Big Chest', 'Skull Woods - Big Chest', "Thieves' Town - Big Chest",
'Misery Mire - Big Chest', 'Hyrule Castle - Boomerang Chest', 'Ice Palace - Big Chest',
'Turtle Rock - Big Chest', 'Ganons Tower - Big Chest'
]),
LocationGroup("BossUncle").locs([
"Link's Uncle", "Link's House", 'Secret Passage', 'Eastern Palace - Boss',
'Desert Palace - Boss', 'Tower of Hera - Boss', 'Palace of Darkness - Boss', 'Swamp Palace - Boss',
'Skull Woods - Boss', "Thieves' Town - Boss", 'Ice Palace - Boss', 'Misery Mire - Boss',
'Turtle Rock - Boss']),
LocationGroup("HeartPieces LW").locs([
'Lost Woods Hideout', 'Kakariko Well - Top', "Blind's Hideout - Top", 'Maze Race', 'Sunken Treasure',
'Bonk Rock Cave', 'Desert Ledge', "Aginah's Cave", 'Spectacle Rock Cave', 'Spectacle Rock', 'Pyramid',
'Lumberjack Tree', "Zora's Ledge"]),
LocationGroup("HeartPieces DW").locs([
'Lake Hylia Island', 'Chest Game', 'Digging Game', 'Graveyard Cave', 'Mimic Cave',
'Cave 45', 'Peg Cave', 'Bumper Cave Ledge', 'Checkerboard Cave', 'Mire Shed - Right', 'Floating Island',
'Ganons Tower - Mini Helmasaur Room - Left', 'Ganons Tower - Mini Helmasaur Room - Right']),
LocationGroup("Minor Trash").locs([
'Ice Palace - Freezor Chest', 'Skull Woods - Pot Prison', 'Misery Mire - Bridge Chest',
'Palace of Darkness - Dark Basement - Left', 'Palace of Darkness - Dark Maze - Top',
'Palace of Darkness - Shooter Room', 'Palace of Darkness - The Arena - Bridge',
'Swamp Palace - Flooded Room - Left', 'Swamp Palace - Flooded Room - Right',
'Swamp Palace - Waterfall Room', 'Turtle Rock - Eye Bridge - Bottom Right',
'Turtle Rock - Eye Bridge - Top Left', 'Turtle Rock - Eye Bridge - Top Right']),
LocationGroup("CompassTT").locs([
"Thieves' Town - Ambush Chest", "Thieves' Town - Attic",
'Eastern Palace - Compass Chest', 'Desert Palace - Compass Chest', 'Tower of Hera - Compass Chest',
'Palace of Darkness - Compass Chest', 'Swamp Palace - Compass Chest', 'Skull Woods - Compass Chest',
"Thieves' Town - Compass Chest", 'Ice Palace - Compass Chest', 'Misery Mire - Compass Chest',
'Turtle Rock - Compass Chest', 'Ganons Tower - Compass Room - Top Left']),
LocationGroup("Early SKs").locs([
'Sewers - Dark Cross', 'Desert Palace - Torch', 'Tower of Hera - Basement Cage',
'Palace of Darkness - Stalfos Basement', 'Palace of Darkness - Dark Basement - Right',
'Palace of Darkness - Dark Maze - Bottom', 'Palace of Darkness - Harmless Hellway',
"Thieves' Town - Blind's Cell", 'Eastern Palace - Cannonball Chest',
'Sewers - Secret Room - Right', 'Sewers - Secret Room - Left',
'Sewers - Secret Room - Middle', 'Floodgate Chest'
]),
LocationGroup("Late SKs").locs([
'Skull Woods - Bridge Room', 'Ice Palace - Spike Room', "Hyrule Castle - Zelda's Chest",
'Ice Palace - Iced T Room', 'Misery Mire - Main Lobby', 'Swamp Palace - West Chest',
'Turtle Rock - Chain Chomps', 'Turtle Rock - Crystaroller Room',
'Turtle Rock - Eye Bridge - Bottom Left', "Ganons Tower - Bob's Torch", 'Ganons Tower - Tile Room',
'Ganons Tower - Firesnake Room', 'Ganons Tower - Pre-Moldorm Chest',
]),
LocationGroup("Kak-LDM").locs([
"Blind's Hideout - Left", "Blind's Hideout - Right", "Blind's Hideout - Far Left",
"Blind's Hideout - Far Right", 'Chicken House', 'Paradox Cave Lower - Far Left',
'Paradox Cave Lower - Left', 'Paradox Cave Lower - Right', 'Paradox Cave Lower - Far Right',
'Paradox Cave Lower - Middle', 'Paradox Cave Upper - Left', 'Paradox Cave Upper - Right', 'Spiral Cave',
]),
LocationGroup("BK-Bunny").locs([
'Eastern Palace - Big Key Chest', 'Ganons Tower - Big Key Chest',
'Desert Palace - Big Key Chest', 'Tower of Hera - Big Key Chest', 'Palace of Darkness - Big Key Chest',
'Swamp Palace - Big Key Chest', "Thieves' Town - Big Key Chest", 'Skull Woods - Big Key Chest',
'Ice Palace - Big Key Chest', 'Misery Mire - Big Key Chest', 'Turtle Rock - Big Key Chest',
'Superbunny Cave - Top', 'Superbunny Cave - Bottom',
]),
LocationGroup("Early Drops").flags(True, True).locs([
'Hyrule Castle - Map Guard Key Drop', 'Hyrule Castle - Boomerang Guard Key Drop',
'Hyrule Castle - Key Rat Key Drop', 'Eastern Palace - Dark Square Pot Key',
'Eastern Palace - Dark Eyegore Key Drop', 'Desert Palace - Desert Tiles 1 Pot Key',
'Desert Palace - Beamos Hall Pot Key', 'Desert Palace - Desert Tiles 2 Pot Key',
'Castle Tower - Dark Archer Key Drop', 'Castle Tower - Circle of Pots Key Drop',
'Thieves\' Town - Hallway Pot Key', 'Thieves\' Town - Spike Switch Pot Key', 'Hyrule Castle - Big Key Drop',
]),
LocationGroup("Late Drops").flags(True, True).locs([
'Swamp Palace - Pot Row Pot Key', 'Swamp Palace - Trench 1 Pot Key', 'Swamp Palace - Hookshot Pot Key',
'Swamp Palace - Trench 2 Pot Key', 'Swamp Palace - Waterway Pot Key', 'Skull Woods - West Lobby Pot Key',
'Skull Woods - Spike Corner Key Drop', 'Ice Palace - Jelly Key Drop', 'Ice Palace - Conveyor Key Drop',
'Ice Palace - Hammer Block Key Drop', 'Ice Palace - Many Pots Pot Key', 'Ganons Tower - Conveyor Cross Pot Key',
'Ganons Tower - Double Switch Pot Key']),
LocationGroup("SS-Hype-Voo").locs([
'Mini Moldorm Cave - Left',
'Mini Moldorm Cave - Right', 'Mini Moldorm Cave - Generous Guy', 'Mini Moldorm Cave - Far Left',
'Mini Moldorm Cave - Far Right', 'Hype Cave - Top', 'Hype Cave - Middle Right',
'Hype Cave - Middle Left', 'Hype Cave - Bottom', 'Hype Cave - Generous Guy', 'Brewery',
'C-Shaped House', 'Palace of Darkness - The Arena - Ledge',
]),
LocationGroup("DDM Hard").flags(True, True).locs([
'Hookshot Cave - Top Right', 'Hookshot Cave - Top Left',
'Hookshot Cave - Bottom Right', 'Hookshot Cave - Bottom Left',
'Misery Mire - Spike Chest', 'Misery Mire - Spikes Pot Key', 'Misery Mire - Fishbone Pot Key',
'Misery Mire - Conveyor Crystal Key Drop', 'Turtle Rock - Pokey 1 Key Drop',
'Turtle Rock - Pokey 2 Key Drop', 'Turtle Rock - Roller Room - Right',
'Ganons Tower - Conveyor Star Pits Pot Key', 'Ganons Tower - Mini Helmasaur Key Drop'
]),
LocationGroup("Kak Shop").flags(False, False, True).locs([
'Dark Lake Hylia Shop - Left', 'Dark Lake Hylia Shop - Middle', 'Dark Lake Hylia Shop - Right',
'Dark Lumberjack Shop - Left', 'Dark Lumberjack Shop - Middle', 'Dark Lumberjack Shop - Right',
'Paradox Shop - Left', 'Paradox Shop - Middle', 'Paradox Shop - Right',
'Kakariko Shop - Left', 'Kakariko Shop - Middle', 'Kakariko Shop - Right',
'Capacity Upgrade - Left']),
LocationGroup("Hylia Shop").flags(False, False, True).locs([
'Red Shield Shop - Left', 'Red Shield Shop - Middle', 'Red Shield Shop - Right',
'Village of Outcasts Shop - Left', 'Village of Outcasts Shop - Middle', 'Village of Outcasts Shop - Right',
'Dark Potion Shop - Left', 'Dark Potion Shop - Middle', 'Dark Potion Shop - Right',
'Lake Hylia Shop - Left', 'Lake Hylia Shop - Middle', 'Lake Hylia Shop - Right',
'Capacity Upgrade - Right']),
LocationGroup("Map Validation").locs([
'Hyrule Castle - Map Chest',
'Eastern Palace - Map Chest', 'Desert Palace - Map Chest', 'Tower of Hera - Map Chest',
'Palace of Darkness - Map Chest', 'Swamp Palace - Map Chest', 'Skull Woods - Map Chest',
"Thieves' Town - Map Chest", 'Ice Palace - Map Chest', 'Misery Mire - Map Chest',
'Turtle Rock - Roller Room - Left', 'Ganons Tower - Map Chest', 'Ganons Tower - Validation Chest']),
LocationGroup("SahasWell+MireHopeDDMShop").flags(False, False, True).locs([
'Dark Death Mountain Shop - Left', 'Dark Death Mountain Shop - Middle', 'Dark Death Mountain Shop - Right',
'Kakariko Well - Bottom', 'Kakariko Well - Left', 'Kakariko Well - Middle', 'Kakariko Well - Right',
"Sahasrahla's Hut - Left", "Sahasrahla's Hut - Right", "Sahasrahla's Hut - Middle",
'Mire Shed - Left', 'Ganons Tower - Hope Room - Left', 'Ganons Tower - Hope Room - Right']),
LocationGroup("Tower Pain").flags(True).locs([
'Castle Tower - Room 03', 'Castle Tower - Dark Maze',
'Ganons Tower - DMs Room - Bottom Left', 'Ganons Tower - DMs Room - Bottom Right',
'Ganons Tower - DMs Room - Top Left', 'Ganons Tower - DMs Room - Top Right',
'Ganons Tower - Randomizer Room - Bottom Left', 'Ganons Tower - Randomizer Room - Bottom Right',
'Ganons Tower - Randomizer Room - Top Left', 'Ganons Tower - Randomizer Room - Top Right',
"Ganons Tower - Bob's Chest", 'Ganons Tower - Big Key Room - Right', 'Ganons Tower - Big Key Room - Left']),
LocationGroup("Retro Shops").flags(False, False, True, True).locs([
'Old Man Sword Cave Item 1', 'Take-Any #1 Item 1', 'Take-Any #1 Item 2',
'Take-Any #2 Item 1', 'Take-Any #2 Item 2', 'Take-Any #3 Item 1', 'Take-Any #3 Item 2',
'Take-Any #4 Item 1', 'Take-Any #4 Item 2', 'Swamp Palace - Entrance',
'Ganons Tower - Compass Room - Bottom Left', 'Ganons Tower - Compass Room - Top Right',
'Ganons Tower - Compass Room - Bottom Right',
])
]
trash_items = {
'Nothing': -1,
'Bee Trap': 0,
'Rupee (1)': 1,
'Rupees (5)': 1,
'Rupees (20)': 1,
'Small Heart': 2,
'Bee': 2,
'Bombs (3)': 3,
'Arrows (10)': 3,
'Bombs (10)': 3,
'Red Potion': 4,
'Blue Shield': 4,
'Rupees (50)': 4,
'Rupees (100)': 4,
'Rupees (300)': 5,
'Piece of Heart': 17
}

View File

@@ -1,20 +0,0 @@
from collections import defaultdict
from Dungeons import dungeon_prize
class ItemPoolConfig(object):
def __init__(self):
self.reserved_locations = defaultdict(set)
def create_item_pool_config(world):
config = ItemPoolConfig()
if world.algorithm in ['balanced']:
for player in range(1, world.players+1):
if world.restrict_boss_items[player]:
for dungeon in dungeon_prize:
if dungeon.startswith('Thieves'):
dungeon = "Thieves' Town"
config.reserved_locations[player].add(f'{dungeon} - Boss')
world.item_pool_config = config