Initial fill changes

Merge branch 'Bias' into DoorDevVolatile

# Conflicts:
#	Rom.py
This commit is contained in:
aerinon
2021-11-02 16:03:21 -06:00
19 changed files with 1626 additions and 438 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 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

View File

@@ -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:

View File

@@ -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,

View File

@@ -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

View File

@@ -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
View File

@@ -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):

View File

@@ -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)')

View File

@@ -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
View File

@@ -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

View File

@@ -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']:

View File

@@ -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

View File

@@ -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
View File

@@ -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)

View File

@@ -101,12 +101,11 @@
"algorithm": {
"choices": [
"balanced",
"freshness",
"flood",
"vt21",
"vt22",
"vt25",
"vt26"
"equitable",
"vanilla_fill",
"major_only",
"dungeon_only",
"district"
]
},
"shuffle": {

View File

@@ -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)",

View File

@@ -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",

View File

@@ -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
View 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