Initial fill changes
Merge branch 'Bias' into DoorDevVolatile # Conflicts: # Rom.py
This commit is contained in:
@@ -17,6 +17,7 @@ from Utils import int16_as_bytes
|
||||
from Tables import normal_offset_table, spiral_offset_table, multiply_lookup, divisor_lookup
|
||||
from RoomData import Room
|
||||
|
||||
|
||||
class World(object):
|
||||
|
||||
def __init__(self, players, shuffle, doorShuffle, logic, mode, swords, difficulty, difficulty_adjustments,
|
||||
@@ -216,6 +217,11 @@ class World(object):
|
||||
return r_location
|
||||
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):
|
||||
if isinstance(dungeonname, Dungeon):
|
||||
return dungeonname
|
||||
@@ -1462,6 +1468,10 @@ class Dungeon(object):
|
||||
return self.world.get_name_string_for_object(self) if self.world else f'{self.name} (Player {self.player})'
|
||||
|
||||
|
||||
class FillError(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
@unique
|
||||
class DoorType(Enum):
|
||||
Normal = 1
|
||||
@@ -1853,6 +1863,8 @@ class Sector(object):
|
||||
self.destination_entrance = False
|
||||
self.equations = None
|
||||
self.item_logic = set()
|
||||
self.chest_location_set = set()
|
||||
|
||||
|
||||
def region_set(self):
|
||||
if self.r_name_set is None:
|
||||
@@ -2101,6 +2113,7 @@ class Location(object):
|
||||
self.recursion_count = 0
|
||||
self.staleness_count = 0
|
||||
self.locked = False
|
||||
self.real = not crystal
|
||||
self.always_allow = lambda item, state: False
|
||||
self.access_rule = lambda state: True
|
||||
self.item_rule = lambda item: True
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import logging
|
||||
import RaceRandom as random
|
||||
|
||||
from BaseClasses import Boss
|
||||
from Fill import FillError
|
||||
from BaseClasses import Boss, FillError
|
||||
|
||||
|
||||
def BossFactory(boss, player):
|
||||
if boss is None:
|
||||
|
||||
@@ -44,10 +44,10 @@ def link_doors(world, player):
|
||||
reset_rooms(world, player)
|
||||
world.get_door("Skull Pinball WS", player).no_exit()
|
||||
world.swamp_patch_required[player] = orig_swamp_patch
|
||||
link_doors_prep(world, player)
|
||||
|
||||
|
||||
def link_doors_main(world, player):
|
||||
|
||||
def link_doors_prep(world, player):
|
||||
# Drop-down connections & push blocks
|
||||
for exitName, regionName in logical_connections:
|
||||
connect_simple_door(world, exitName, regionName, player)
|
||||
@@ -99,6 +99,7 @@ def link_doors_main(world, player):
|
||||
analyze_portals(world, player)
|
||||
for portal in world.dungeon_portals[player]:
|
||||
connect_portal(portal, world, player)
|
||||
|
||||
if not world.doorShuffle[player] == 'vanilla':
|
||||
fix_big_key_doors_with_ugly_smalls(world, player)
|
||||
else:
|
||||
@@ -119,11 +120,14 @@ def link_doors_main(world, player):
|
||||
for ent, ext in default_one_way_connections:
|
||||
connect_one_way(world, ent, ext, 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)
|
||||
elif world.doorShuffle[player] == 'crossed':
|
||||
cross_dungeon(world, player)
|
||||
else:
|
||||
elif world.doorShuffle[player] != 'vanilla':
|
||||
logging.getLogger('').error('Invalid door shuffle setting: %s' % world.doorShuffle[player])
|
||||
raise Exception('Invalid door shuffle setting: %s' % world.doorShuffle[player])
|
||||
|
||||
@@ -214,11 +218,16 @@ def vanilla_key_logic(world, player):
|
||||
world.key_logic[player] = {}
|
||||
analyze_dungeon(key_layout, world, player)
|
||||
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)
|
||||
# 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)
|
||||
|
||||
|
||||
def validate_vanilla_reservation(dungeon, world, player):
|
||||
return validate_key_layout(world.key_layout[player][dungeon.name], world, player)
|
||||
|
||||
|
||||
# some useful functions
|
||||
oppositemap = {
|
||||
Direction.South: Direction.North,
|
||||
|
||||
@@ -1162,6 +1162,8 @@ class DungeonBuilder(object):
|
||||
self.sectors = []
|
||||
self.location_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_provided = False
|
||||
self.c_switch_required = False
|
||||
@@ -1324,7 +1326,7 @@ def create_dungeon_builders(all_sectors, connections_tuple, world, player,
|
||||
polarized_sectors[sector] = None
|
||||
if bow_sectors:
|
||||
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)
|
||||
ensure_crystal_switches_reachable(dungeon_map, leftover, polarized_sectors, crystal_barriers, global_pole)
|
||||
for sector in leftover:
|
||||
@@ -1475,6 +1477,7 @@ def define_sector_features(sectors):
|
||||
sector.bk_provided = True
|
||||
elif loc.name not in dungeon_events and not loc.forced_item:
|
||||
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",
|
||||
"Thieves' Town - Blind's Cell"]:
|
||||
sector.bk_required = True
|
||||
@@ -1555,19 +1558,26 @@ def assign_bow_sectors(dungeon_map, 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
|
||||
choices = None
|
||||
sector_list = list(free_location_sectors)
|
||||
random.shuffle(sector_list)
|
||||
orig_location_set = build_orig_location_set(dungeon_map)
|
||||
num_dungeon_items = requested_dungeon_items(world, player)
|
||||
while not valid:
|
||||
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):
|
||||
choice = d_idx[choices[i].name]
|
||||
d_name = choices[i].name
|
||||
choice = d_idx[d_name]
|
||||
totals[choice] += sector.chest_locations
|
||||
location_set[d_name].update(sector.chest_location_set)
|
||||
valid = True
|
||||
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
|
||||
break
|
||||
for i, choice in enumerate(choices):
|
||||
@@ -1598,6 +1608,30 @@ def weighted_random_locations(dungeon_map, free_location_sectors):
|
||||
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):
|
||||
population = []
|
||||
some_c_switches_present = False
|
||||
|
||||
114
Dungeons.py
114
Dungeons.py
@@ -1,8 +1,5 @@
|
||||
import RaceRandom as random
|
||||
|
||||
from BaseClasses import Dungeon
|
||||
from Bosses import BossFactory
|
||||
from Fill import fill_restrictive
|
||||
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]
|
||||
|
||||
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],
|
||||
'Desert Palace - Prize': [0x1559B, 0x1559C, 0x1559D, 0x1559E],
|
||||
|
||||
476
Fill.py
476
Fill.py
@@ -3,173 +3,68 @@ import collections
|
||||
import itertools
|
||||
import logging
|
||||
|
||||
from BaseClasses import CollectionState
|
||||
from BaseClasses import CollectionState, FillError
|
||||
from Items import ItemFactory
|
||||
from Regions import shop_to_location_table, retro_shops
|
||||
from source.item.BiasedFill import filter_locations, classify_major_items, replace_trash_item, vanilla_fallback
|
||||
|
||||
|
||||
class FillError(RuntimeError):
|
||||
pass
|
||||
|
||||
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 get_dungeon_item_pool(world):
|
||||
return [item for dungeon in world.dungeons for item in dungeon.all_items]
|
||||
|
||||
|
||||
def distribute_items_staleness(world):
|
||||
# get list of locations to fill in
|
||||
fill_locations = world.get_unfilled_locations()
|
||||
random.shuffle(fill_locations)
|
||||
def promote_dungeon_items(world):
|
||||
world.itempool += get_dungeon_item_pool(world)
|
||||
|
||||
# get items to distribute
|
||||
random.shuffle(world.itempool)
|
||||
itempool = world.itempool
|
||||
for item in world.get_items():
|
||||
if item.smallkey or item.bigkey:
|
||||
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
|
||||
world.state.sweep_for_events()
|
||||
def dungeon_tracking(world):
|
||||
for dungeon in world.dungeons:
|
||||
layout = world.dungeon_layouts[dungeon.player][dungeon.name]
|
||||
layout.dungeon_items = len([i for i in dungeon.all_items if i.is_inside_dungeon_item(world)])
|
||||
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:
|
||||
# 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
|
||||
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.')
|
||||
def fill_dungeons_restrictive(world, shuffled_locations):
|
||||
dungeon_tracking(world)
|
||||
all_state_base = world.get_all_state()
|
||||
|
||||
spot_to_fill = None
|
||||
for location in fill_locations:
|
||||
# increase likelyhood of skipping a location if it has been found stale
|
||||
if not progress_done and random.randint(0, location.staleness_count) > 2:
|
||||
continue
|
||||
# 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)
|
||||
|
||||
if location.can_fill(world.state, item_to_place):
|
||||
spot_to_fill = location
|
||||
break
|
||||
else:
|
||||
location.staleness_count += 1
|
||||
# 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
|
||||
|
||||
# might have skipped too many locations due to potential staleness. Do not check for staleness now to find a candidate
|
||||
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
|
||||
dungeon_items = [item for item in get_dungeon_item_pool(world) if item.is_inside_dungeon_item(world)]
|
||||
|
||||
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)
|
||||
# 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))
|
||||
|
||||
world.push_item(spot_to_fill, item_to_place, True)
|
||||
itempool.remove(item_to_place)
|
||||
fill_locations.remove(spot_to_fill)
|
||||
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)
|
||||
|
||||
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,
|
||||
vanilla=False):
|
||||
def sweep_from_pool():
|
||||
new_state = base_state.copy()
|
||||
for item in itempool:
|
||||
@@ -201,41 +96,58 @@ def fill_restrictive(world, base_state, locations, itempool, keys_in_itempool =
|
||||
|
||||
spot_to_fill = None
|
||||
|
||||
for location in locations:
|
||||
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 = maximum_exploration_state.copy()
|
||||
test_state.stale[item_to_place.player] = True
|
||||
else:
|
||||
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
|
||||
item_locations = filter_locations(item_to_place, locations, world, vanilla)
|
||||
for location in item_locations:
|
||||
spot_to_fill = verify_spot_to_fill(location, item_to_place, maximum_exploration_state,
|
||||
single_player_placement, perform_access_check, itempool,
|
||||
keys_in_itempool, world)
|
||||
if spot_to_fill:
|
||||
break
|
||||
if item_to_place.smallkey or item_to_place.bigkey:
|
||||
location.item = None
|
||||
|
||||
if spot_to_fill is None:
|
||||
# we filled all reachable spots. Maybe the game can be beaten anyway?
|
||||
unplaced_items.insert(0, item_to_place)
|
||||
if world.can_beat_game():
|
||||
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)
|
||||
if vanilla:
|
||||
unplaced_items.insert(0, item_to_place)
|
||||
continue
|
||||
spot_to_fill = last_ditch_placement(item_to_place, locations, world, maximum_exploration_state,
|
||||
base_state, itempool, keys_in_itempool, single_player_placement)
|
||||
spot_to_fill = recovery_placement(item_to_place, locations, world, maximum_exploration_state,
|
||||
base_state, itempool, perform_access_check, item_locations,
|
||||
keys_in_itempool, single_player_placement)
|
||||
if spot_to_fill is None:
|
||||
# we filled all reachable spots. Maybe the game can be beaten anyway?
|
||||
unplaced_items.insert(0, item_to_place)
|
||||
if world.can_beat_game():
|
||||
if world.accessibility[item_to_place.player] != 'none':
|
||||
logging.getLogger('').warning('Not all items placed. Game beatable anyway.'
|
||||
f' (Could not place {item_to_place})')
|
||||
continue
|
||||
raise FillError('No more spots to place %s' % item_to_place)
|
||||
|
||||
world.push_item(spot_to_fill, item_to_place, False)
|
||||
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)
|
||||
spot_to_fill.event = True
|
||||
|
||||
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):
|
||||
if not valid_reserved_placement(item, location, world):
|
||||
return False
|
||||
@@ -254,7 +166,7 @@ def valid_key_placement(item, location, itempool, world):
|
||||
cr_count = world.crystals_needed_for_gt[location.player]
|
||||
return key_logic.check_placement(unplaced_keys, location if item.bigkey else None, prize_loc, cr_count)
|
||||
else:
|
||||
return not item.is_inside_dungeon_item(world) # todo: big deal for ambrosia to fix this
|
||||
return not item.is_inside_dungeon_item(world)
|
||||
|
||||
|
||||
def valid_reserved_placement(item, location, world):
|
||||
@@ -263,6 +175,17 @@ def valid_reserved_placement(item, location, world):
|
||||
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):
|
||||
if not item.smallkey:
|
||||
return
|
||||
@@ -274,6 +197,72 @@ def track_outside_keys(item, location, world):
|
||||
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 recovery_placement(item_to_place, locations, world, state, base_state, itempool, perform_access_check, attempted,
|
||||
keys_in_itempool=None, single_player_placement=False):
|
||||
logging.getLogger('').debug(f'Could not place {item_to_place} attempting recovery')
|
||||
if world.algorithm in ['balanced', 'equitable']:
|
||||
return last_ditch_placement(item_to_place, locations, world, state, base_state, itempool, keys_in_itempool,
|
||||
single_player_placement)
|
||||
elif world.algorithm == 'vanilla_fill':
|
||||
if item_to_place.type == 'Crystal':
|
||||
possible_swaps = [x for x in state.locations_checked if x.item.type == 'Crystal']
|
||||
return try_possible_swaps(possible_swaps, item_to_place, locations, world, base_state, itempool,
|
||||
keys_in_itempool, single_player_placement)
|
||||
else:
|
||||
i, config = 0, world.item_pool_config
|
||||
tried = set(attempted)
|
||||
if not item_to_place.is_inside_dungeon_item(world):
|
||||
while i < len(config.location_groups[item_to_place.player]):
|
||||
fallback_locations = config.location_groups[item_to_place.player][i].locations
|
||||
other_locs = [x for x in locations if x.name in fallback_locations]
|
||||
for location in other_locs:
|
||||
spot_to_fill = verify_spot_to_fill(location, item_to_place, state, single_player_placement,
|
||||
perform_access_check, itempool, keys_in_itempool, world)
|
||||
if spot_to_fill:
|
||||
return spot_to_fill
|
||||
i += 1
|
||||
tried.update(other_locs)
|
||||
else:
|
||||
other_locations = vanilla_fallback(item_to_place, locations, world)
|
||||
for location in other_locations:
|
||||
spot_to_fill = verify_spot_to_fill(location, item_to_place, state, single_player_placement,
|
||||
perform_access_check, itempool, keys_in_itempool, world)
|
||||
if spot_to_fill:
|
||||
return spot_to_fill
|
||||
tried.update(other_locations)
|
||||
other_locations = [x for x in locations if x not in tried]
|
||||
for location in other_locations:
|
||||
spot_to_fill = verify_spot_to_fill(location, item_to_place, state, single_player_placement,
|
||||
perform_access_check, itempool, keys_in_itempool, world)
|
||||
if spot_to_fill:
|
||||
return spot_to_fill
|
||||
return None
|
||||
else:
|
||||
other_locations = [x for x in locations if x not in attempted]
|
||||
for location in other_locations:
|
||||
spot_to_fill = verify_spot_to_fill(location, item_to_place, state, single_player_placement,
|
||||
perform_access_check, itempool, keys_in_itempool, world)
|
||||
if spot_to_fill:
|
||||
return spot_to_fill
|
||||
return None
|
||||
|
||||
|
||||
def last_ditch_placement(item_to_place, locations, world, state, base_state, itempool,
|
||||
keys_in_itempool=None, single_player_placement=False):
|
||||
def location_preference(loc):
|
||||
@@ -292,7 +281,12 @@ def last_ditch_placement(item_to_place, locations, world, state, base_state, ite
|
||||
possible_swaps = [x for x in state.locations_checked
|
||||
if x.item.type not in ['Event', 'Crystal'] and not x.forced_item]
|
||||
swap_locations = sorted(possible_swaps, key=location_preference)
|
||||
return try_possible_swaps(swap_locations, item_to_place, locations, world, base_state, itempool,
|
||||
keys_in_itempool, single_player_placement)
|
||||
|
||||
|
||||
def try_possible_swaps(swap_locations, item_to_place, locations, world, base_state, itempool,
|
||||
keys_in_itempool=None, single_player_placement=False):
|
||||
for location in swap_locations:
|
||||
old_item = location.item
|
||||
new_pool = list(itempool) + [old_item]
|
||||
@@ -355,11 +349,15 @@ def distribute_items_restrictive(world, gftower_trash=False, fill_locations=None
|
||||
random.shuffle(fill_locations)
|
||||
|
||||
# get items to distribute
|
||||
classify_major_items(world)
|
||||
random.shuffle(world.itempool)
|
||||
progitempool = [item for item in world.itempool if item.advancement]
|
||||
prioitempool = [item for item in world.itempool if not item.advancement and item.priority]
|
||||
restitempool = [item for item in world.itempool if not item.advancement and not item.priority]
|
||||
|
||||
gftower_trash &= world.algorithm in ['balanced', 'equitable', 'dungeon_only']
|
||||
# dungeon only may fill up the dungeon... and push items out into the overworld
|
||||
|
||||
# fill in gtower locations with trash first
|
||||
for player in range(1, world.players + 1):
|
||||
if not gftower_trash or not world.ganonstower_vanilla[player] or world.doorShuffle[player] == 'crossed' or world.logic[player] in ['owglitches', 'nologic']:
|
||||
@@ -383,21 +381,51 @@ 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
|
||||
# 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)
|
||||
keys_in_pool = {player: world.keyshuffle[player] or world.algorithm != 'balanced' for player in range(1, world.players + 1)}
|
||||
|
||||
fill_restrictive(world, world.state, fill_locations, progitempool,
|
||||
keys_in_itempool={player: world.keyshuffle[player] for player in range(1, world.players + 1)})
|
||||
|
||||
# sort maps and compasses to the back -- this may not be viable in equitable & ambrosia
|
||||
progitempool.sort(key=lambda item: 0 if item.map or item.compass else 1)
|
||||
if world.algorithm == 'vanilla_fill':
|
||||
fill_restrictive(world, world.state, fill_locations, progitempool, keys_in_pool, vanilla=True)
|
||||
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_fill':
|
||||
fast_vanilla_fill(world, prioitempool, fill_locations)
|
||||
elif world.algorithm in ['major_only', 'dungeon_only', 'district']:
|
||||
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 == 'district':
|
||||
random.shuffle(fill_locations)
|
||||
placeholder_items = [item for item in world.itempool if item.name == 'Rupee (1)']
|
||||
num_ph_items = len(placeholder_items)
|
||||
if num_ph_items > 0:
|
||||
placeholder_locations = filter_locations('Placeholder', fill_locations, world)
|
||||
num_ph_locations = len(placeholder_locations)
|
||||
if num_ph_items < num_ph_locations < len(fill_locations):
|
||||
for _ in range(num_ph_locations - num_ph_items):
|
||||
placeholder_items.append(replace_trash_item(restitempool, 'Rupee (1)'))
|
||||
assert len(placeholder_items) == len(placeholder_locations)
|
||||
for i in placeholder_items:
|
||||
restitempool.remove(i)
|
||||
for l in placeholder_locations:
|
||||
fill_locations.remove(l)
|
||||
filtered_fill(world, placeholder_items, placeholder_locations)
|
||||
|
||||
fast_fill(world, prioitempool, fill_locations)
|
||||
|
||||
fast_fill(world, restitempool, fill_locations)
|
||||
if world.algorithm == 'vanilla_fill':
|
||||
fast_vanilla_fill(world, restitempool, fill_locations)
|
||||
else:
|
||||
fast_fill(world, restitempool, fill_locations)
|
||||
|
||||
unplaced = [item.name for item in prioitempool + restitempool]
|
||||
unfilled = [location.name for location in fill_locations]
|
||||
if unplaced or unfilled:
|
||||
logging.warning('Unplaced items: %s - Unfilled Locations: %s', unplaced, unfilled)
|
||||
|
||||
|
||||
def fast_fill(world, item_pool, fill_locations):
|
||||
while item_pool and fill_locations:
|
||||
spot_to_fill = fill_locations.pop()
|
||||
@@ -405,77 +433,59 @@ def fast_fill(world, item_pool, fill_locations):
|
||||
world.push_item(spot_to_fill, item_to_place, False)
|
||||
|
||||
|
||||
def flood_items(world):
|
||||
# get items to distribute
|
||||
random.shuffle(world.itempool)
|
||||
itempool = world.itempool
|
||||
progress_done = False
|
||||
def filtered_fill(world, item_pool, fill_locations):
|
||||
while item_pool and fill_locations:
|
||||
item_to_place = item_pool.pop()
|
||||
item_locations = filter_locations(item_to_place, fill_locations, world)
|
||||
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
|
||||
world.state.sweep_for_events()
|
||||
|
||||
# fill world from top of itempool while we can
|
||||
while not progress_done:
|
||||
location_list = world.get_unfilled_locations()
|
||||
random.shuffle(location_list)
|
||||
spot_to_fill = None
|
||||
for location in location_list:
|
||||
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
|
||||
def fast_vanilla_fill(world, item_pool, fill_locations):
|
||||
next_item_pool = []
|
||||
while item_pool and fill_locations:
|
||||
item_to_place = item_pool.pop()
|
||||
locations = filter_locations(item_to_place, fill_locations, world, vanilla_skip=True)
|
||||
if len(locations):
|
||||
spot_to_fill = locations.pop()
|
||||
fill_locations.remove(spot_to_fill)
|
||||
world.push_item(spot_to_fill, item_to_place, False)
|
||||
else:
|
||||
next_item_pool.append(item_to_place)
|
||||
while next_item_pool and fill_locations:
|
||||
item_to_place = next_item_pool.pop()
|
||||
spot_to_fill = next(iter(filter_locations(item_to_place, fill_locations, world)))
|
||||
fill_locations.remove(spot_to_fill)
|
||||
world.push_item(spot_to_fill, item_to_place, False)
|
||||
|
||||
# ran out of spots, check if we need to step in and correct things
|
||||
if len(world.get_reachable_locations()) == len(world.get_locations()):
|
||||
progress_done = True
|
||||
continue
|
||||
|
||||
# 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
|
||||
def filtered_equitable_fill(world, item_pool, fill_locations):
|
||||
while item_pool and fill_locations:
|
||||
item_to_place = item_pool.pop()
|
||||
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)
|
||||
|
||||
# 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 item_to_place is None:
|
||||
if candidate_item_to_place is not None:
|
||||
item_to_place = candidate_item_to_place
|
||||
else:
|
||||
raise FillError('No more progress items left to place.')
|
||||
|
||||
# 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 fast_equitable_fill(world, item_pool, fill_locations):
|
||||
while item_pool and fill_locations:
|
||||
item_to_place = item_pool.pop()
|
||||
spot_to_fill = next(l for l in fill_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)
|
||||
|
||||
|
||||
def lock_shop_locations(world, player):
|
||||
for shop, loc_names in shop_to_location_table.items():
|
||||
for loc in loc_names:
|
||||
world.get_location(loc, player).event = True
|
||||
world.get_location(loc, player).locked = True
|
||||
# I don't believe these locations exist in non-shopsanity
|
||||
# if world.retro[player]:
|
||||
# for shop, loc_names in retro_shops.items():
|
||||
# for loc in loc_names:
|
||||
# world.get_location(loc, player).event = True
|
||||
# world.get_location(loc, player).locked = True
|
||||
|
||||
|
||||
def sell_potions(world, player):
|
||||
|
||||
13
ItemList.py
13
ItemList.py
@@ -4,12 +4,13 @@ import math
|
||||
import RaceRandom as random
|
||||
|
||||
from BaseClasses import Region, RegionType, Shop, ShopType, Location, CollectionState
|
||||
from Dungeons import get_dungeon_item_pool
|
||||
from EntranceShuffle import connect_entrance
|
||||
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 source.item.BiasedFill import trash_items
|
||||
|
||||
import source.classes.constants as CONST
|
||||
|
||||
|
||||
@@ -261,8 +262,12 @@ def generate_itempool(world, player):
|
||||
if player in world.pool_adjustment.keys():
|
||||
amt = world.pool_adjustment[player]
|
||||
if amt < 0:
|
||||
for _ in range(amt, 0):
|
||||
pool.remove(next(iter([x for x in pool if x in ['Rupees (20)', 'Rupees (5)', 'Rupee (1)']])))
|
||||
trash_options = [x for x in pool if x in trash_items]
|
||||
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:
|
||||
for _ in range(0, amt):
|
||||
pool.append('Rupees (20)')
|
||||
|
||||
@@ -187,6 +187,8 @@ class PlacementRule(object):
|
||||
return True
|
||||
available_keys = outside_keys
|
||||
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
|
||||
for loc in check_locations:
|
||||
if not loc.item:
|
||||
|
||||
63
Main.py
63
Main.py
@@ -20,16 +20,17 @@ from InvertedRegions import create_inverted_regions, mark_dark_world_regions
|
||||
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 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 Rules import set_rules
|
||||
from Dungeons import create_dungeons, fill_dungeons, fill_dungeons_restrictive
|
||||
from Fill import distribute_items_cutoff, distribute_items_staleness, distribute_items_restrictive, flood_items
|
||||
from Dungeons import create_dungeons
|
||||
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 ItemList import generate_itempool, difficulties, fill_prizes, customize_shops
|
||||
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, district_item_pool_config
|
||||
|
||||
|
||||
__version__ = '1.0.2.0-v'
|
||||
|
||||
@@ -152,7 +153,6 @@ def main(args, seed=None, fish=None):
|
||||
create_dungeons(world, player)
|
||||
adjust_locations(world, player)
|
||||
place_bosses(world, player)
|
||||
create_item_pool_config(world)
|
||||
|
||||
if any(world.potshuffle.values()):
|
||||
logger.info(world.fish.translate("cli", "cli", "shuffling.pots"))
|
||||
@@ -168,7 +168,13 @@ def main(args, seed=None, fish=None):
|
||||
else:
|
||||
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):
|
||||
link_doors(world, player)
|
||||
@@ -176,8 +182,7 @@ def main(args, seed=None, fish=None):
|
||||
mark_light_world_regions(world, player)
|
||||
else:
|
||||
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):
|
||||
generate_itempool(world, player)
|
||||
@@ -195,8 +200,9 @@ def main(args, seed=None, fish=None):
|
||||
else:
|
||||
lock_shop_locations(world, player)
|
||||
|
||||
|
||||
logger.info(world.fish.translate("cli","cli","placing.dungeon.prizes"))
|
||||
district_item_pool_config(world)
|
||||
massage_item_pool(world)
|
||||
logger.info(world.fish.translate("cli", "cli", "placing.dungeon.prizes"))
|
||||
|
||||
fill_prizes(world)
|
||||
|
||||
@@ -205,14 +211,12 @@ def main(args, seed=None, fish=None):
|
||||
|
||||
logger.info(world.fish.translate("cli","cli","placing.dungeon.items"))
|
||||
|
||||
shuffled_locations = None
|
||||
if args.algorithm in ['balanced', 'vt26'] or any(list(args.mapshuffle.values()) + list(args.compassshuffle.values()) +
|
||||
list(args.keyshuffle.values()) + list(args.bigkeyshuffle.values())):
|
||||
if args.algorithm != 'equitable':
|
||||
shuffled_locations = world.get_unfilled_locations()
|
||||
random.shuffle(shuffled_locations)
|
||||
fill_dungeons_restrictive(world, shuffled_locations)
|
||||
else:
|
||||
fill_dungeons(world)
|
||||
promote_dungeon_items(world)
|
||||
|
||||
for player in range(1, world.players+1):
|
||||
if world.logic[player] != 'nologic':
|
||||
@@ -230,34 +234,22 @@ def main(args, seed=None, fish=None):
|
||||
|
||||
logger.info(world.fish.translate("cli","cli","fill.world"))
|
||||
|
||||
if args.algorithm == 'flood':
|
||||
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)
|
||||
distribute_items_restrictive(world, True)
|
||||
|
||||
if world.players > 1:
|
||||
logger.info(world.fish.translate("cli","cli","balance.multiworld"))
|
||||
balance_multiworld_progression(world)
|
||||
logger.info(world.fish.translate("cli", "cli", "balance.multiworld"))
|
||||
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 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):
|
||||
if world.shopsanity[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}'
|
||||
|
||||
@@ -409,6 +401,7 @@ def copy_world(world):
|
||||
ret.keydropshuffle = world.keydropshuffle.copy()
|
||||
ret.mixed_travel = world.mixed_travel.copy()
|
||||
ret.standardize_palettes = world.standardize_palettes.copy()
|
||||
ret.restrict_boss_items = world.restrict_boss_items.copy()
|
||||
|
||||
ret.exp_cache = world.exp_cache.copy()
|
||||
|
||||
@@ -583,11 +576,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
|
||||
# world.clear_exp_cache()
|
||||
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)
|
||||
else:
|
||||
# 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
|
||||
|
||||
# cull entries in spheres for spoiler walkthrough at end
|
||||
|
||||
11
Mystery.py
11
Mystery.py
@@ -73,6 +73,8 @@ def main():
|
||||
if 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()}
|
||||
|
||||
for player in range(1, args.multi + 1):
|
||||
@@ -81,7 +83,12 @@ def main():
|
||||
settings = settings_cache[path] if settings_cache[path] else roll_settings(weights_cache[path])
|
||||
for k, v in vars(settings).items():
|
||||
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:
|
||||
raise RuntimeError(f'No weights specified for player {player}')
|
||||
|
||||
@@ -129,6 +136,8 @@ def roll_settings(weights):
|
||||
|
||||
ret = argparse.Namespace()
|
||||
|
||||
ret.algorithm = get_choice('algorithm')
|
||||
|
||||
glitches_required = get_choice('glitches_required')
|
||||
if glitches_required is not None:
|
||||
if glitches_required not in ['none', 'no_logic']:
|
||||
|
||||
@@ -22,6 +22,7 @@ def _wrap(name):
|
||||
|
||||
# These are for intellisense purposes only, and will be overwritten below
|
||||
choice = _prng_inst.choice
|
||||
choices = _prng_inst.choices
|
||||
gauss = _prng_inst.gauss
|
||||
getrandbits = _prng_inst.getrandbits
|
||||
randint = _prng_inst.randint
|
||||
|
||||
@@ -999,6 +999,14 @@ def adjust_locations(world, player):
|
||||
world.get_location(location, player).address = 0x400000 + index
|
||||
# player address? it is in the shop table
|
||||
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])
|
||||
|
||||
24
Rom.py
24
Rom.py
@@ -2088,6 +2088,7 @@ def write_strings(rom, world, player, team):
|
||||
else:
|
||||
entrances_to_hint.update({'Pyramid Ledge': 'The pyramid ledge'})
|
||||
hint_count = 4 if world.shuffle[player] not in ['vanilla', 'dungeonssimple', 'dungeonsfull'] else 0
|
||||
hint_count -= 2 if world.algorithm == 'district' and world.shuffle[player] not in ['simple', 'restricted'] else 0
|
||||
for entrance in all_entrances:
|
||||
if entrance.name in entrances_to_hint:
|
||||
if hint_count > 0:
|
||||
@@ -2179,11 +2180,22 @@ def write_strings(rom, world, player, team):
|
||||
else:
|
||||
tt[hint_locations.pop(0)] = this_hint
|
||||
|
||||
# All remaining hint slots are filled with junk hints. It is done this way to ensure the same junk hint isn't selected twice.
|
||||
junk_hints = junk_texts.copy()
|
||||
random.shuffle(junk_hints)
|
||||
for location in hint_locations:
|
||||
tt[location] = junk_hints.pop(0)
|
||||
if world.algorithm == 'district':
|
||||
hint_candidates = []
|
||||
for name, district in world.districts[player].items():
|
||||
if name not in world.item_pool_config.recorded_choices and not district.sphere_one:
|
||||
hint_candidates.append(f'{name} is a foolish choice')
|
||||
random.shuffle(hint_candidates)
|
||||
foolish_choice_hints = min(len(hint_candidates), len(hint_locations))
|
||||
for i in range(0, foolish_choice_hints):
|
||||
tt[hint_locations.pop(0)] = hint_candidates.pop(0)
|
||||
if len(hint_locations) > 0:
|
||||
# All remaining hint slots are filled with junk hints. It is done this way to ensure the same junk hint
|
||||
# isn't selected twice.
|
||||
junk_hints = junk_texts.copy()
|
||||
random.shuffle(junk_hints)
|
||||
for location in hint_locations:
|
||||
tt[location] = junk_hints.pop(0)
|
||||
|
||||
# We still need the older hints of course. Those are done here.
|
||||
|
||||
@@ -2341,7 +2353,7 @@ def set_inverted_mode(world, player, rom):
|
||||
write_int16(rom, snes_to_pc(0x02E8D5), 0x07C8)
|
||||
write_int16(rom, snes_to_pc(0x02E8F7), 0x01F8)
|
||||
rom.write_byte(snes_to_pc(0x08D40C), 0xD0) # morph proof
|
||||
rom.write_byte(snes_to_pc(0x1BC428), 0x00) # remove diggable light world portals
|
||||
rom.write_byte(snes_to_pc(0x1BC428), 0x00) # remove diggable light world portals
|
||||
rom.write_byte(snes_to_pc(0x1BC43A), 0x00)
|
||||
rom.write_byte(snes_to_pc(0x1BC590), 0x00)
|
||||
rom.write_byte(snes_to_pc(0x1BC5A1), 0x00)
|
||||
|
||||
@@ -101,12 +101,11 @@
|
||||
"algorithm": {
|
||||
"choices": [
|
||||
"balanced",
|
||||
"freshness",
|
||||
"flood",
|
||||
"vt21",
|
||||
"vt22",
|
||||
"vt25",
|
||||
"vt26"
|
||||
"equitable",
|
||||
"vanilla_fill",
|
||||
"major_only",
|
||||
"dungeon_only",
|
||||
"district"
|
||||
]
|
||||
},
|
||||
"shuffle": {
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
"seed": "Seed",
|
||||
"player": "Player",
|
||||
"shuffling.world": "Shuffling the World about",
|
||||
"shuffling.prep": "Dungeon and Item prep",
|
||||
"shuffling.dungeons": "Shuffling dungeons",
|
||||
"shuffling.pots": "Shuffling pots",
|
||||
"basic.traversal": "--Basic Traversal",
|
||||
@@ -153,22 +154,28 @@
|
||||
"balanced: vt26 derivative that aims to strike a balance between",
|
||||
" the overworld heavy vt25 and the dungeon heavy vt26",
|
||||
" algorithm.",
|
||||
"vt26: Shuffle items and place them in a random location",
|
||||
" that it is not impossible to be in. This includes",
|
||||
" dungeon keys and items.",
|
||||
"vt25: Shuffle items and place them in a random location",
|
||||
" that it is not impossible to be in.",
|
||||
"vt21: Unbiased in its selection, but has tendency to put",
|
||||
" Ice Rod in Turtle Rock.",
|
||||
"vt22: Drops off stale locations after 1/3 of progress",
|
||||
" items were placed to try to circumvent vt21\\'s",
|
||||
" shortcomings.",
|
||||
"Freshness: Keep track of stale locations (ones that cannot be",
|
||||
" reached yet) and decrease likeliness of selecting",
|
||||
" them the more often they were found unreachable.",
|
||||
"Flood: Push out items starting from Link\\'s House and",
|
||||
" slightly biased to placing progression items with",
|
||||
" less restrictions."
|
||||
"equitable: does not place dungeon items first allowing new potential",
|
||||
" but mixed with the normal advancement pool",
|
||||
"restricted placements: these consider all major items to be special and attempts",
|
||||
"to place items from fixed to semi-random locations. For purposes of these shuffles, all",
|
||||
"Y items, A items, swords (unless vanilla swords), mails, shields, heart containers and",
|
||||
"1/2 magic are considered to be part of a major items pool. Big Keys are added to the pool",
|
||||
"if shuffled. Same for small keys, compasses, maps, keydrops (if small keys are also shuffled),",
|
||||
"1 of each capacity upgrade for shopsanity, the quiver item for retro+shopsanity, and",
|
||||
"triforce pieces for Triforce Hunt. Future modes will add to these as appropriate.",
|
||||
"vanilla_fill As above, but attempts to place items in their vanilla",
|
||||
" location first. Major items that cannot be placed that way",
|
||||
" will attempt to be placed in other failed locations first.",
|
||||
" Also attempts to place all items in vanilla locations",
|
||||
"major_only As above, but uses the major items' location preferentially",
|
||||
" major item location are defined as the group of location where",
|
||||
" the items are found in the vanilla game.",
|
||||
"dungeon_only As above, but major items are preferentially placed",
|
||||
" in dungeons locations first",
|
||||
"district 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": [
|
||||
"Select Entrance Shuffling Algorithm. (default: %(default)s)",
|
||||
|
||||
@@ -283,13 +283,12 @@
|
||||
"randomizer.item.accessibility.none": "Beatable",
|
||||
|
||||
"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.equitable": "Equitable",
|
||||
"randomizer.item.sortingalgo.vanilla_fill": "Vanilla Fill",
|
||||
"randomizer.item.sortingalgo.major_only": "Major Location Restriction",
|
||||
"randomizer.item.sortingalgo.dungeon_only": "Dungeon Restriction",
|
||||
"randomizer.item.sortingalgo.district": "District Restriction",
|
||||
|
||||
"randomizer.item.restrict_boss_items": "Forbidden Boss Items",
|
||||
"randomizer.item.restrict_boss_items.none": "None",
|
||||
|
||||
@@ -116,13 +116,12 @@
|
||||
"type": "selectbox",
|
||||
"default": "balanced",
|
||||
"options": [
|
||||
"freshness",
|
||||
"flood",
|
||||
"vt21",
|
||||
"vt22",
|
||||
"vt25",
|
||||
"vt26",
|
||||
"balanced"
|
||||
"balanced",
|
||||
"equitable",
|
||||
"vanilla_fill",
|
||||
"major_only",
|
||||
"dungeon_only",
|
||||
"district"
|
||||
]
|
||||
},
|
||||
"restrict_boss_items": {
|
||||
|
||||
169
source/item/District.py
Normal file
169
source/item/District.py
Normal file
@@ -0,0 +1,169 @@
|
||||
from collections import deque
|
||||
|
||||
from BaseClasses import CollectionState, RegionType
|
||||
from Dungeons import dungeon_table
|
||||
|
||||
|
||||
class District(object):
|
||||
|
||||
def __init__(self, name, locations, entrances=None, dungeon=None):
|
||||
self.name = name
|
||||
self.dungeon = dungeon
|
||||
self.locations = locations
|
||||
self.entrances = entrances if entrances else []
|
||||
self.sphere_one = False
|
||||
|
||||
|
||||
def create_districts(world):
|
||||
world.districts = {}
|
||||
for p in range(1, world.players + 1):
|
||||
create_district_helper(world, p)
|
||||
|
||||
|
||||
def create_district_helper(world, player):
|
||||
inverted = world.mode[player] == 'inverted'
|
||||
districts = {}
|
||||
kak_locations = {'Bottle Merchant', 'Kakariko Tavern', 'Maze Race'}
|
||||
nw_lw_locations = {'Mushroom', 'Master Sword Pedestal'}
|
||||
central_lw_locations = {'Sunken Treasure', 'Flute Spot'}
|
||||
desert_locations = {'Purple Chest', 'Desert Ledge'}
|
||||
lake_locations = {'Hobo'}
|
||||
east_lw_locations = {"Zora's Ledge", 'King Zora'}
|
||||
lw_dm_locations = {'Old Man', 'Spectacle Rock', 'Ether Tablet'}
|
||||
east_dw_locations = {'Pyramid', 'Catfish'}
|
||||
south_dw_locations = {'Stumpy', 'Digging Game', 'Bombos Tablet', 'Lake Hylia Island'}
|
||||
voo_north_locations = {'Bumper Cave Ledge'}
|
||||
ddm_locations = {'Floating Island'}
|
||||
|
||||
kak_entrances = ['Kakariko Well Cave', 'Bat Cave Cave', 'Elder House (East)', 'Elder House (West)',
|
||||
'Two Brothers House (East)', 'Two Brothers House (West)', 'Blinds Hideout', 'Chicken House',
|
||||
'Blacksmiths Hut', 'Sick Kids House', 'Snitch Lady (East)', 'Snitch Lady (West)',
|
||||
'Bush Covered House', 'Tavern (Front)', 'Light World Bomb Hut', 'Kakariko Shop', 'Library',
|
||||
'Kakariko Gamble Game', 'Kakariko Well Drop', 'Bat Cave Drop']
|
||||
nw_lw_entrances = ['North Fairy Cave', 'Lost Woods Hideout Stump', 'Lumberjack Tree Cave', 'Sanctuary',
|
||||
'Old Man Cave (West)', 'Death Mountain Return Cave (West)', 'Kings Grave', 'Lost Woods Gamble',
|
||||
'Fortune Teller (Light)', 'Bonk Rock Cave', 'Lumberjack House', 'North Fairy Cave Drop',
|
||||
'Lost Woods Hideout Drop', 'Lumberjack Tree Tree', 'Sanctuary Grave']
|
||||
central_lw_entrances = ['Links House', 'Hyrule Castle Entrance (South)', 'Hyrule Castle Entrance (West)',
|
||||
'Hyrule Castle Entrance (East)', 'Agahnims Tower', 'Hyrule Castle Secret Entrance Stairs',
|
||||
'Dam', 'Bonk Fairy (Light)', 'Light Hype Fairy', 'Cave Shop (Lake Hylia)',
|
||||
'Lake Hylia Fortune Teller', 'Hyrule Castle Secret Entrance Drop']
|
||||
desert_entrances = ['Desert Palace Entrance (South)', 'Desert Palace Entrance (West)',
|
||||
'Desert Palace Entrance (North)', 'Desert Palace Entrance (East)', 'Desert Fairy',
|
||||
'Aginahs Cave', '50 Rupee Cave']
|
||||
lake_entrances = ['Capacity Upgrade', 'Mini Moldorm Cave', 'Good Bee Cave', '20 Rupee Cave', 'Ice Rod Cave']
|
||||
east_lw_entrances = ['Eastern Palace', 'Waterfall of Wishing', 'Lake Hylia Fairy', 'Sahasrahlas Hut',
|
||||
'Long Fairy Cave', 'Potion Shop']
|
||||
lw_dm_entrances = ['Tower of Hera', 'Old Man Cave (East)', 'Old Man House (Bottom)', 'Old Man House (Top)',
|
||||
'Death Mountain Return Cave (East)', 'Spectacle Rock Cave Peak', 'Spectacle Rock Cave',
|
||||
'Spectacle Rock Cave (Bottom)', 'Paradox Cave (Bottom)', 'Paradox Cave (Middle)',
|
||||
'Paradox Cave (Top)', 'Fairy Ascension Cave (Bottom)', 'Fairy Ascension Cave (Top)',
|
||||
'Spiral Cave', 'Spiral Cave (Bottom)', 'Hookshot Fairy']
|
||||
east_dw_entrances = ['Palace of Darkness', 'Pyramid Entrance', 'Pyramid Fairy', 'East Dark World Hint',
|
||||
'Palace of Darkness Hint', 'Dark Lake Hylia Fairy', 'Dark World Potion Shop', 'Pyramid Hole']
|
||||
south_dw_entrances = ['Ice Palace', 'Swamp Palace', 'Dark Lake Hylia Ledge Fairy',
|
||||
'Dark Lake Hylia Ledge Spike Cave', 'Dark Lake Hylia Ledge Hint', 'Hype Cave',
|
||||
'Bonk Fairy (Dark)', 'Archery Game', 'Big Bomb Shop', 'Dark Lake Hylia Shop', 'Cave 45']
|
||||
voo_north_entrances = ['Thieves Town', 'Skull Woods First Section Door', 'Skull Woods Second Section Door (East)',
|
||||
'Skull Woods Second Section Door (West)', 'Skull Woods Final Section',
|
||||
'Bumper Cave (Bottom)', 'Bumper Cave (Top)', 'Brewery', 'C-Shaped House', 'Chest Game',
|
||||
'Dark World Hammer Peg Cave', 'Red Shield Shop', 'Dark Sanctuary Hint',
|
||||
'Fortune Teller (Dark)', 'Dark World Shop', 'Dark World Lumberjack Shop', 'Graveyard Cave',
|
||||
'Skull Woods First Section Hole (West)', 'Skull Woods First Section Hole (East)',
|
||||
'Skull Woods First Section Hole (North)', 'Skull Woods Second Section Hole']
|
||||
mire_entrances = ['Misery Mire', 'Mire Shed', 'Dark Desert Hint', 'Dark Desert Fairy', 'Checkerboard Cave']
|
||||
ddm_entrances = ['Turtle Rock', 'Dark Death Mountain Ledge (West)', 'Dark Death Mountain Ledge (East)',
|
||||
'Turtle Rock Isolated Ledge Entrance', 'Superbunny Cave (Top)', 'Superbunny Cave (Bottom)',
|
||||
'Hookshot Cave', 'Hookshot Cave Back Entrance', 'Ganons Tower', 'Spike Cave',
|
||||
'Cave Shop (Dark Death Mountain)', 'Dark Death Mountain Fairy', 'Mimic Cave']
|
||||
|
||||
if inverted:
|
||||
south_dw_locations.remove('Bombos Tablet')
|
||||
south_dw_locations.remove('Lake Hylia Island')
|
||||
voo_north_locations.remove('Bumper Cave Ledge')
|
||||
ddm_locations.remove('Floating Island')
|
||||
desert_locations.add('Bombos Tablet')
|
||||
lake_locations.add('Lake Hylia Island')
|
||||
nw_lw_locations.add('Bumper Cave Ledge')
|
||||
lw_dm_locations.add('Floating Island')
|
||||
|
||||
south_dw_entrances.remove('Cave 45')
|
||||
central_lw_entrances.append('Cave 45')
|
||||
voo_north_entrances.remove('Graveyard Cave')
|
||||
nw_lw_entrances.append('Graveyard Cave')
|
||||
mire_entrances.remove('Checkerboard Cave')
|
||||
desert_entrances.append('Checkerboard Cave')
|
||||
ddm_entrances.remove('Mimic Cave')
|
||||
lw_dm_entrances.append('Mimic Cave')
|
||||
|
||||
south_dw_entrances.remove('Big Bomb Shop')
|
||||
central_lw_entrances.append('Inverted Big Bomb Shop')
|
||||
central_lw_entrances.remove('Links House')
|
||||
south_dw_entrances.append('Inverted Links House')
|
||||
voo_north_entrances.remove('Dark Sanctuary')
|
||||
voo_north_entrances.append('Inverted Dark Sanctuary')
|
||||
ddm_entrances.remove('Ganons Tower')
|
||||
central_lw_entrances.append('Inverted Ganons Tower')
|
||||
central_lw_entrances.remove('Agahnims Tower')
|
||||
ddm_entrances.append('Inverted Agahnims Tower')
|
||||
east_dw_entrances.remove('Pyramid Entrance')
|
||||
central_lw_entrances.append('Inverted Pyramid Entrance')
|
||||
east_dw_entrances.remove('Pyramid Hole')
|
||||
central_lw_entrances.append('Inverted Pyramid Hole')
|
||||
|
||||
districts['Kakariko'] = District('Kakariko', kak_locations, entrances=kak_entrances)
|
||||
districts['Northwest Hyrule'] = District('Northwest Hyrule', nw_lw_locations, entrances=nw_lw_entrances)
|
||||
districts['Central Hyrule'] = District('Central Hyrule', central_lw_locations, entrances=central_lw_entrances)
|
||||
districts['Desert'] = District('Desert', desert_locations, entrances=desert_entrances)
|
||||
districts['Lake Hylia'] = District('Lake Hylia', lake_locations, entrances=lake_entrances)
|
||||
districts['Eastern Hyrule'] = District('Eastern Hyrule', east_lw_locations, entrances=east_lw_entrances)
|
||||
districts['Death Mountain'] = District('Death Mountain', lw_dm_locations, entrances=lw_dm_entrances)
|
||||
districts['East Dark World'] = District('East Dark World', east_dw_locations, entrances=east_dw_entrances)
|
||||
districts['South Dark World'] = District('South Dark World', south_dw_locations, entrances=south_dw_entrances)
|
||||
districts['Northwest Dark World'] = District('Northwest Dark World', voo_north_locations,
|
||||
entrances=voo_north_entrances)
|
||||
districts['The Mire'] = District('The Mire', set(), entrances=mire_entrances)
|
||||
districts['Dark Death Mountain'] = District('Dark Death Mountain', ddm_locations, entrances=ddm_entrances)
|
||||
districts.update({x: District(x, set(), dungeon=x) for x in dungeon_table.keys()})
|
||||
|
||||
world.districts[player] = districts
|
||||
|
||||
|
||||
def resolve_districts(world):
|
||||
create_districts(world)
|
||||
state = CollectionState(world)
|
||||
state.sweep_for_events()
|
||||
for player in range(1, world.players + 1):
|
||||
check_set = find_reachable_locations(state, player)
|
||||
used_locations = {l for d in world.districts[player].values() for l in d.locations}
|
||||
for name, district in world.districts[player].items():
|
||||
if district.dungeon:
|
||||
layout = world.dungeon_layouts[player][district.dungeon]
|
||||
district.locations.update([l.name for r in layout.master_sector.regions
|
||||
for l in r.locations if not l.item and l.real])
|
||||
else:
|
||||
for entrance in district.entrances:
|
||||
ent = world.get_entrance(entrance, player)
|
||||
queue = deque([ent.connected_region])
|
||||
visited = set()
|
||||
while len(queue) > 0:
|
||||
region = queue.pop()
|
||||
visited.add(region)
|
||||
if region.type == RegionType.Cave:
|
||||
for location in region.locations:
|
||||
if location.name not in used_locations and not location.item and location.real:
|
||||
district.locations.add(location.name)
|
||||
used_locations.add(location.name)
|
||||
for ext in region.exits:
|
||||
if ext.connected_region not in visited:
|
||||
queue.appendleft(ext.connected_region)
|
||||
district.sphere_one = len(check_set.intersection(district.locations)) > 0
|
||||
|
||||
|
||||
def find_reachable_locations(state, player):
|
||||
check_set = set()
|
||||
for region in state.reachable_regions[player]:
|
||||
for location in region.locations:
|
||||
if location.can_reach(state) and not location.forced_item and location.real:
|
||||
check_set.add(location.name)
|
||||
return check_set
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user