Files
alttpr-python/Fill.py
2025-12-07 00:00:14 -06:00

1290 lines
66 KiB
Python

import RaceRandom as random
import collections
import itertools
import logging
import math
from collections import Counter
from contextlib import suppress
from BaseClasses import CollectionState, FillError, LocationType
from Items import ItemFactory
from Regions import shop_to_location_table, retro_shops
from source.item.FillUtil import filter_locations, classify_major_items, replace_trash_item, vanilla_fallback
from source.item.FillUtil import filter_special_locations, valid_pot_items
def get_dungeon_item_pool(world):
dungeon_items = [item for dungeon in world.dungeons for item in dungeon.all_items if item.location is None]
for player in range(1, world.players+1):
if world.prizeshuffle[player] != 'none':
dungeon_items.extend(ItemFactory(['Red Pendant', 'Blue Pendant', 'Green Pendant', 'Crystal 1', 'Crystal 2', 'Crystal 3', 'Crystal 4', 'Crystal 7', 'Crystal 5', 'Crystal 6'], player))
return dungeon_items
def promote_dungeon_items(world):
world.itempool += get_dungeon_item_pool(world)
for item in world.get_items():
if item.smallkey or item.bigkey or item.prize:
item.advancement = True
elif item.map or item.compass:
item.priority = True
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)])
if world.prizeshuffle[dungeon.player] in ['dungeon', 'nearby'] and not dungeon.prize:
from Dungeons import dungeon_table
if dungeon_table[dungeon.name].prize:
layout.dungeon_items += 1
layout.free_items = layout.location_cnt - layout.dungeon_items
def fill_dungeons_restrictive(world, shuffled_locations):
# with shuffled dungeon items they are distributed as part of the normal item pool
for item in world.get_items():
if ((item.prize and world.prizeshuffle[item.player] != 'none')
or (item.smallkey and world.keyshuffle[item.player] != 'none')
or (item.bigkey and world.bigkeyshuffle[item.player] != 'none')):
item.advancement = True
elif (item.map and world.mapshuffle[item.player] not in ['none', 'nearby']) or (item.compass and world.compassshuffle[item.player] not in ['none', 'nearby']):
item.priority = True
dungeon_items = [item for item in get_dungeon_item_pool(world) if item.is_inside_dungeon_item(world) or item.is_near_dungeon_item(world)]
bigs, smalls, prizes, others = [], [], [], []
for i in dungeon_items:
(bigs if i.bigkey else smalls if i.smallkey else prizes if i.prize else others).append(i)
unplaced_smalls = list(smalls)
for i in world.itempool:
if i.smallkey and world.keyshuffle[i.player] not in ['none', 'nearby']:
unplaced_smalls.append(i)
def fill(base_state, items, locations, key_pool=None):
fill_restrictive(world, base_state, locations, items, key_pool, True)
all_state_base = world.get_all_state()
for player in range(1, world.players + 1):
if world.logic[player] == 'hybridglitches' and world.keyshuffle[player] in ['none', 'nearby'] \
and world.pottery[player] not in ['none', 'cave']:
# remove 2 keys from main pool
count_to_remove = 2
to_remove = []
for wix, wi in enumerate(smalls):
if wi.name == 'Small Key (Swamp Palace)' and wi.player == player:
to_remove.append(wix)
if count_to_remove == len(to_remove):
break
for wix in reversed(to_remove):
del smalls[wix]
# remove 2 swamp locations from pool
hybrid_locations = []
to_remove = []
for i, loc in enumerate(shuffled_locations):
if loc.name in ['Swamp Palace - Trench 1 Pot Key', 'Swamp Palace - Pot Row Pot Key'] and loc.player == player:
to_remove.append(i)
hybrid_locations.append(loc)
if count_to_remove == len(to_remove):
break
for i in reversed(to_remove):
shuffled_locations.pop(i)
# place 2 HMG keys
hybrid_state_base = all_state_base.copy()
for x in bigs + smalls + prizes + others:
hybrid_state_base.collect(x, True)
hybrid_smalls = [ItemFactory('Small Key (Swamp Palace)', player)] * 2
fill(hybrid_state_base, hybrid_smalls, hybrid_locations, unplaced_smalls)
big_state_base = all_state_base.copy()
for x in smalls + prizes + others:
big_state_base.collect(x, True)
fill(big_state_base, bigs, shuffled_locations, unplaced_smalls)
random.shuffle(shuffled_locations)
small_state_base = all_state_base.copy()
for x in prizes + others:
small_state_base.collect(x, True)
fill(small_state_base, smalls, shuffled_locations, unplaced_smalls)
prizes_copy = prizes.copy()
for attempt in range(15):
try:
for player in range(1, world.players + 1):
if world.prizeshuffle[player] == 'nearby':
dungeon_pool = []
for dungeon in world.dungeons:
from Dungeons import dungeon_table
if dungeon.player == player and dungeon_table[dungeon.name].prize:
dungeon_pool.append(dungeon)
random.shuffle(dungeon_pool)
for item in prizes:
if item.player == player:
dungeon = dungeon_pool.pop()
dungeon.prize = item
item.dungeon_object = dungeon
random.shuffle(prizes)
random.shuffle(shuffled_locations)
prize_state_base = all_state_base.copy()
for x in others:
prize_state_base.collect(x, True)
fill(prize_state_base, prizes, shuffled_locations)
except FillError as e:
logging.getLogger('').info("Failed to place dungeon prizes (%s). Will retry %s more times", e, 14 - attempt)
prizes = prizes_copy.copy()
for dungeon in world.dungeons:
dungeon.prize = None
for prize in prizes:
if prize.location:
prize.location.item = None
prize.location = None
continue
break
else:
raise FillError(f'Unable to place dungeon prizes: {", ".join(list(map(lambda d: d.name, prizes)))}')
random.shuffle(shuffled_locations)
fill(all_state_base, others, shuffled_locations)
def fill_restrictive(world, base_state, locations, itempool, key_pool=None, single_player_placement=False,
vanilla=False):
def sweep_from_pool(placing_items=None):
new_state = base_state.copy()
for item in itempool:
new_state.collect(item, True)
new_state.placing_items = placing_items
new_state.sweep_for_events()
new_state.placing_items = None
return new_state
unplaced_items = []
no_access_checks = {}
reachable_items = {}
for item in itempool:
if world.accessibility[item.player] == 'none':
no_access_checks.setdefault(item.player, []).append(item)
else:
reachable_items.setdefault(item.player, []).append(item)
for player_items in [no_access_checks, reachable_items]:
while any(player_items.values()) and locations:
items_to_place = [[itempool.remove(items[-1]), items.pop()][-1] for items in player_items.values() if items]
maximum_exploration_state = sweep_from_pool(placing_items=items_to_place)
has_beaten_game = world.has_beaten_game(maximum_exploration_state)
for item_to_place in items_to_place:
perform_access_check = True
if world.accessibility[item_to_place.player] == 'none':
perform_access_check = not world.has_beaten_game(maximum_exploration_state, item_to_place.player) if single_player_placement else not has_beaten_game
spot_to_fill = None
item_locations = filter_locations(item_to_place, locations, world, vanilla)
if is_dungeon_item(item_to_place, world) and not (item_to_place.prize and world.prizeshuffle[item_to_place.player] == 'none'):
item_locations = [l for l in item_locations if valid_dungeon_placement(item_to_place, l, world)]
verify(item_to_place, item_locations, maximum_exploration_state, single_player_placement,
perform_access_check, key_pool, world)
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, key_pool, world)
if spot_to_fill:
break
if spot_to_fill is None:
if vanilla:
unplaced_items.insert(0, item_to_place)
continue
spot_to_fill = recovery_placement(item_to_place, locations, world, maximum_exploration_state,
base_state, itempool, perform_access_check, item_locations,
key_pool, 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,
key_pool, world):
if item_to_place.smallkey or item_to_place.bigkey or item_to_place.prize: # a better test to see if a key can go there
location.item = item_to_place
location.event = True
if item_to_place.smallkey:
with suppress(ValueError):
key_pool.remove(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:
test_state.sweep_for_events()
if location.can_fill(test_state, item_to_place, perform_access_check):
if valid_key_placement(item_to_place, location, key_pool, test_state, world):
if (item_to_place.prize and world.prizeshuffle[item_to_place.player] == 'none') \
or valid_dungeon_placement(item_to_place, location, world):
return location
if item_to_place.smallkey or item_to_place.bigkey or item_to_place.prize:
location.item = None
location.event = False
if item_to_place.smallkey:
key_pool.append(item_to_place)
return None
def valid_key_placement(item, location, key_pool, collection_state, world):
if not valid_reserved_placement(item, location, world):
return False
if ((not item.smallkey and not item.bigkey) or item.player != location.player
or world.keyshuffle[item.player] == 'universal' or world.logic[item.player] == 'nologic'):
return True
dungeon = location.parent_region.dungeon
if not dungeon and item.is_near_dungeon_item(world):
check_dungeon = world.get_dungeon(item.dungeon, item.player) if item.dungeon else item.dungeon_object
if len([d for d in location.parent_region.districts if d in check_dungeon.districts]):
dungeon = check_dungeon
if dungeon:
if dungeon.name not in item.name and (dungeon.name != 'Hyrule Castle' or 'Escape' not in item.name):
return True
# Small key and big key in Swamp and Hera are placed without logic
if world.logic[item.player] == 'hybridglitches' and dungeon.name in ['Tower of Hera', 'Swamp Palace'] and dungeon.name in item.name:
return True
key_logic = world.key_logic[item.player][dungeon.name]
unplaced_keys = len([x for x in key_pool if x.name == key_logic.small_key_name and x.player == item.player])
prize_loc = None
if key_logic.prize_location and dungeon.prize and dungeon.prize.location and dungeon.prize.location.player == item.player:
prize_loc = dungeon.prize.location
cr_count = world.crystals_needed_for_gt[location.player]
wild_keys = world.keyshuffle[item.player] != 'none'
if wild_keys:
reached_keys = {x for x in collection_state.locations_checked
if x.item and x.item.name == key_logic.small_key_name and x.item.player == item.player}
else:
reached_keys = set() # will be calculated using key logic in a moment
self_locking_keys = sum(1 for d, rule in key_logic.door_rules.items() if rule.allow_small
and rule.small_location.item and rule.small_location.item.name == key_logic.small_key_name)
return key_logic.check_placement(unplaced_keys, wild_keys, reached_keys, self_locking_keys,
location if item.bigkey else None, prize_loc, cr_count)
else:
return not item.is_inside_dungeon_item(world)
def valid_reserved_placement(item, location, world):
if item.player == location.player and item.is_inside_dungeon_item(world):
return location.name not in world.item_pool_config.reserved_locations[location.player]
return True
def valid_dungeon_placement(item, location, world):
dungeon = location.parent_region.dungeon
if not dungeon and item.is_near_dungeon_item(world):
check_dungeon = world.get_dungeon(item.dungeon, item.player) if item.dungeon else item.dungeon_object
if len([d for d in location.parent_region.districts if d in check_dungeon.districts]):
dungeon = check_dungeon
if dungeon:
layout = world.dungeon_layouts[location.player][dungeon.name]
if not is_dungeon_item(item, world) or item.player != location.player:
if item.prize and item.is_near_dungeon_item(world):
return item.dungeon_object == dungeon and layout.free_items > 0
return layout.free_items > 0
elif item.prize:
return not dungeon.prize and layout.dungeon_items > 0
else:
# the second half probably doesn't matter much - should always return true
return item.dungeon == 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
item_dungeon = item.dungeon
if location.player == item.player:
loc_dungeon = location.parent_region.dungeon
if loc_dungeon and loc_dungeon.name == item_dungeon:
return # this is an inside key
world.key_logic[item.player][item_dungeon].outside_keys += 1
world.key_logic[item.player][item_dungeon].outside_keys_locations.add(location)
def track_dungeon_items(item, location, world):
if location.parent_region.dungeon and (not item.prize or world.prizeshuffle[item.player] == 'dungeon'):
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
if item.prize:
location.parent_region.dungeon.prize = item
item.dungeon_object = location.parent_region.dungeon
def is_dungeon_item(item, world):
return ((item.prize and world.prizeshuffle[item.player] in ['none', 'dungeon'])
or (item.smallkey and world.keyshuffle[item.player] == 'none')
or (item.bigkey and world.bigkeyshuffle[item.player] == 'none')
or (item.compass and world.compassshuffle[item.player] == 'none')
or (item.map and world.mapshuffle[item.player] == 'none'))
def recovery_placement(item_to_place, locations, world, state, base_state, itempool, perform_access_check, attempted,
key_pool=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, key_pool,
single_player_placement)
elif world.algorithm == 'vanilla_fill':
if item_to_place.prize:
possible_swaps = [x for x in state.locations_checked if x.item.prize]
return try_possible_swaps(possible_swaps, item_to_place, locations, world, base_state, itempool,
key_pool, 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, key_pool, 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, key_pool, 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, key_pool, world)
if spot_to_fill:
return spot_to_fill
return None
# explicitly fail these cases
elif world.algorithm in ['dungeon_only', 'major_only', 'district']:
raise FillError(f'Rare placement for {world.algorithm} detected. {item_to_place} unable to be placed.'
f' Try a different seed')
# I don't think any algorithm uses fallback placement anymore, vanilla is special. Others simply fail.
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, key_pool, world)
if spot_to_fill:
return spot_to_fill
return None
def last_ditch_placement(item_to_place, locations, world, state, base_state, itempool,
key_pool=None, single_player_placement=False):
def location_preference(loc):
if not loc.item.advancement:
return 1
if loc.item.type and loc.item.type != 'Sword':
if loc.item.type in ['Map', 'Compass']:
return 2
else:
return 3
return 4
# TODO: Verify correctness in using item player in multiworld situations
if item_to_place.prize and world.prizeshuffle[item_to_place.player] == 'none':
possible_swaps = [x for x in state.locations_checked if x.item.prize]
else:
ignored_types = ['Event']
if world.prizeshuffle[item_to_place.player] == 'none':
ignored_types.append('Prize')
possible_swaps = [x for x in state.locations_checked
if x.item.type not in ignored_types and not x.forced_item and not x.locked]
swap_locations = sorted(possible_swaps, key=location_preference)
return try_possible_swaps(swap_locations, item_to_place, locations, world, base_state, itempool,
key_pool, single_player_placement)
def try_possible_swaps(swap_locations, item_to_place, locations, world, base_state, itempool,
key_pool=None, single_player_placement=False):
for location in swap_locations:
old_item = location.item
new_pool = list(itempool) + [old_item]
new_spot = find_spot_for_item(item_to_place, [location], world, base_state, new_pool,
key_pool, single_player_placement)
if new_spot:
restore_item = new_spot.item
new_spot.item = item_to_place
swap_spot = find_spot_for_item(old_item, locations, world, base_state, itempool,
key_pool, single_player_placement)
if swap_spot:
logging.getLogger('').debug(f'Swapping {old_item} for {item_to_place}')
world.push_item(swap_spot, old_item, False)
swap_spot.event = True
locations.remove(swap_spot)
locations.append(new_spot)
return new_spot
else:
new_spot.item = restore_item
else:
location.item = old_item
return None
def find_spot_for_item(item_to_place, locations, world, base_state, pool,
keys_in_itempool=None, single_player_placement=False):
def sweep_from_pool():
new_state = base_state.copy()
for item in pool:
new_state.collect(item, True)
new_state.sweep_for_events()
return new_state
for location in locations:
maximum_exploration_state = sweep_from_pool()
perform_access_check = True
old_item = None
if world.accessibility[item_to_place.player] == 'none':
perform_access_check = not world.has_beaten_game(maximum_exploration_state, item_to_place.player) if single_player_placement else not world.has_beaten_game(maximum_exploration_state)
if item_to_place.smallkey or item_to_place.bigkey: # a better test to see if a key can go there
old_item = location.item
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,
pool if (keys_in_itempool and keys_in_itempool[item_to_place.player]) else world.itempool,
test_state, world):
return location
if item_to_place.smallkey or item_to_place.bigkey:
location.item = old_item
return None
def distribute_items_restrictive(world, gftower_trash=False, fill_locations=None):
# If not passed in, then get a shuffled list of locations to fill in
if not fill_locations:
fill_locations = world.get_unfilled_locations()
random.shuffle(fill_locations)
# get items to distribute
classify_major_items(world)
# handle special case fast fill
locations_used = False
location_item_pool = collections.defaultdict(list)
# guarantee one big magic in a bonk location
for player in range(1, world.players + 1):
if world.shuffle_bonk_drops[player]:
for item in world.itempool:
if item.name in ['Big Magic'] and item.player == player:
location_item_pool[player].append(item)
break
from Regions import bonk_prize_table
for player, magic_pool in location_item_pool.items():
if len(magic_pool) > 0:
world.itempool.remove(magic_pool[0])
bonk_locations = [location for location in fill_locations if location.player == player
and location.name in [n for n, (_, _, aga, _, _, _) in bonk_prize_table.items() if not aga]]
bonk_locations = filter_special_locations(bonk_locations, world, lambda l: l.name == 'Kakariko Portal Tree')
fast_fill_helper(world, magic_pool, bonk_locations)
locations_used = True
if locations_used:
fill_locations = world.get_unfilled_locations()
random.shuffle(fill_locations)
random.shuffle(world.itempool)
config_sort(world)
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.logic[player] in ['owglitches', 'hybridglitches', 'nologic']):
continue
gt_count, total_count = calc_trash_locations(world, player)
scale_factor = .75 * (world.crystals_needed_for_gt[player] / 7)
if world.algorithm == 'dungeon_only':
reserved_space = sum(1 for i in progitempool+prioitempool if i.player == player)
max_trash = max(0, min(gt_count, total_count - reserved_space))
else:
max_trash = gt_count
scaled_trash = math.floor(max_trash * scale_factor)
if world.goal[player] in ['triforcehunt', 'trinity', 'ganonhunt'] or world.algorithm == 'dungeon_only':
gftower_trash_count = random.randint(scaled_trash, max_trash)
else:
gftower_trash_count = random.randint(0, scaled_trash)
gtower_locations = [location for location in fill_locations if location.parent_region.dungeon
and location.parent_region.dungeon.name == 'Ganons Tower' and location.player == player]
random.shuffle(gtower_locations)
trashcnt = 0
while gtower_locations and restitempool and trashcnt < gftower_trash_count:
spot_to_fill = gtower_locations.pop()
item_to_place = restitempool.pop()
world.push_item(spot_to_fill, item_to_place, False)
fill_locations.remove(spot_to_fill)
trashcnt += 1
random.shuffle(fill_locations)
fill_locations.reverse()
# Make sure the escape keys ire placed first in standard to prevent running out of spots
def std_item_sort(item):
if world.mode[item.player] == 'standard':
if item.name == 'Small Key (Escape)':
return 1
if item.name == 'Big Key (Escape)':
return 2
return 0
progitempool.sort(key=std_item_sort)
key_pool = [x for x in progitempool if x.smallkey]
# 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, key_pool, vanilla=True)
fill_restrictive(world, world.state, fill_locations, progitempool, key_pool)
random.shuffle(fill_locations)
if world.algorithm == 'vanilla_fill':
fast_vanilla_fill(world, prioitempool, fill_locations)
elif world.algorithm in ['balanced', '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)
if world.players > 1:
fast_fill_pot_for_multiworld(world, restitempool, fill_locations)
# todo: fast fill drops?
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)
ensure_good_items(world)
def config_sort(world):
if world.item_pool_config.verify:
config_sort_helper(world, world.item_pool_config.verify)
elif world.item_pool_config.preferred:
config_sort_helper_random(world, world.item_pool_config.preferred)
def config_sort_helper(world, sort_dict):
pref = list(sort_dict.keys())
pref_len = len(pref)
world.itempool.sort(key=lambda i: pref_len - pref.index((i.name, i.player))
if (i.name, i.player) in sort_dict else 0)
def config_sort_helper_random(world, sort_dict):
world.itempool.sort(key=lambda i: 1 if (i.name, i.player) in sort_dict else 0)
def calc_trash_locations(world, player):
total_count, gt_count = 0, 0
for loc in world.get_locations():
if (loc.player == player and loc.item is None
and (loc.type not in {LocationType.Bonk, LocationType.Pot, LocationType.Drop, LocationType.Normal} or not loc.forced_item)
and (loc.type != LocationType.Shop or world.shopsanity[player])
and loc.parent_region.dungeon):
total_count += 1
if loc.parent_region.dungeon.name == 'Ganons Tower':
gt_count += 1
return gt_count, total_count
def ensure_good_items(world, write_skips=False):
for loc in world.get_locations():
if loc.item is None:
loc.item = ItemFactory('Nothing', loc.player)
# convert Arrows 5 and Nothing when necessary
if (loc.item.name in {'Arrows (5)', 'Nothing'}
and (loc.type != LocationType.Pot or loc.item.player != loc.player)):
loc.item = ItemFactory(invalid_location_replacement[loc.item.name], loc.item.player)
# do the arrow retro check
if world.bow_mode[loc.item.player].startswith('retro') and loc.item.name in {'Arrows (5)', 'Arrows (10)'}:
loc.item = ItemFactory('Rupees (5)', loc.item.player)
# don't write out all pots to spoiler
# todo: skip uninteresting enemy drops
if write_skips:
if loc.type in [LocationType.Pot, LocationType.Bonk] and loc.item.name in valid_pot_items:
loc.skip = True
dungeon_pool = collections.defaultdict(list)
prize_pool = collections.defaultdict(list)
from Dungeons import dungeon_table
from Items import prize_item_table
for dungeon in world.dungeons:
if dungeon_table[dungeon.name].prize:
dungeon_pool[dungeon.player].append(dungeon)
prize_set = set(prize_item_table.keys())
for p in range(1, world.players + 1):
prize_pool[p] = prize_set.copy()
for player in dungeon_pool:
dungeons = list(dungeon_pool[player])
random.shuffle(dungeons)
dungeon_pool[player] = dungeons
for dungeon in world.dungeons:
if dungeon.prize:
dungeon_pool[dungeon.player].remove(dungeon)
prize_pool[dungeon.prize.player].remove(dungeon.prize.name)
for p in range(1, world.players + 1):
for dungeon in dungeon_pool[p]:
dungeon.prize = ItemFactory(prize_pool[p].pop(), p)
invalid_location_replacement = {'Arrows (5)': 'Arrows (10)', 'Nothing': 'Rupees (5)',
'Chicken': 'Rupees (5)', 'Big Magic': 'Small Magic', 'Fairy': 'Small Heart'}
def fast_fill_helper(world, item_pool, fill_locations):
if world.algorithm == 'vanilla_fill':
fast_vanilla_fill(world, item_pool, fill_locations)
else:
fast_fill(world, item_pool, fill_locations)
# todo: other fast fill methods?
def fast_fill(world, item_pool, fill_locations):
config = world.item_pool_config
fast_pool = [x for x in item_pool if (x.name, x.player) not in config.restricted]
filtered_pool = [x for x in item_pool if (x.name, x.player) in config.restricted]
filtered_fill(world, filtered_pool, fill_locations)
while fast_pool and fill_locations:
spot_to_fill = fill_locations.pop()
item_to_place = fast_pool.pop()
world.push_item(spot_to_fill, item_to_place, False)
item_pool.clear()
item_pool.extend(filtered_pool)
item_pool.extend(fast_pool)
def fast_fill_pot_for_multiworld(world, item_pool, fill_locations):
pot_item_pool = collections.defaultdict(list)
pot_fill_locations = collections.defaultdict(list)
for item in item_pool:
if item.name in valid_pot_items:
pot_item_pool[item.player].append(item)
for loc in fill_locations:
if loc.type == LocationType.Pot:
pot_fill_locations[loc.player].append(loc)
for player in range(1, world.players+1):
flex = 256 - world.data_tables[player].pot_secret_table.multiworld_count
fill_count = len(pot_fill_locations[player]) - flex
if fill_count > 0:
fill_spots = random.sample(pot_fill_locations[player], fill_count)
fill_items = random.sample(pot_item_pool[player], fill_count)
for x in fill_items:
item_pool.remove(x)
for x in fill_spots:
fill_locations.remove(x)
fast_fill(world, fill_items, fill_spots)
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()
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)
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)
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).locked = True
def sell_potions(world, player):
loc_choices = []
for shop in world.shops[player]:
# potions are excluded from the cap fairy due to visual problem
if shop.region.name in shop_to_location_table and shop.region.name != 'Capacity Upgrade':
loc_choices += [world.get_location(loc, player) for loc in shop_to_location_table[shop.region.name]]
locations = [loc for loc in loc_choices if not loc.item]
for potion in ['Green Potion', 'Blue Potion', 'Red Potion']:
location = random.choice(filter_locations(ItemFactory(potion, player), locations, world, potion=True))
locations.remove(location)
p_item = next((item for item in world.itempool if item.name == potion and item.player == player), None)
if p_item:
world.push_item(location, p_item, collect=False)
world.itempool.remove(p_item)
def sell_keys(world, player):
# exclude the old man or take any caves because free keys are too good
shop_names = {shop.region.name: shop for shop in world.shops[player] if shop.region.name in shop_to_location_table}
choices = [(world.get_location(loc, player), shop) for shop in shop_names for loc in shop_to_location_table[shop]]
locations = [l for l, shop in choices]
locations = filter_locations(ItemFactory('Small Key (Universal)', player), locations, world)
locations = [(loc, shop) for loc, shop in choices if not loc.item and loc in locations]
location, shop = random.choice(locations)
universal_key = next(i for i in world.itempool if i.name == 'Small Key (Universal)' and i.player == player)
world.push_item(location, universal_key, collect=False)
idx = shop_to_location_table[shop_names[shop].region.name].index(location.name)
shop_names[shop].add_inventory(idx, 'Small Key (Universal)', 100)
world.itempool.remove(universal_key)
def verify(item_to_place, item_locations, state, spp, pac, key_pool, world):
if world.item_pool_config.verify:
logger = logging.getLogger('')
item_name = 'Bottle' if item_to_place.name.startswith('Bottle') else item_to_place.name
item_player = item_to_place.player
config = world.item_pool_config
if (item_name, item_player) in config.verify:
tests = config.verify[(item_name, item_player)]
issues = []
for location in item_locations:
if location.name in tests:
expected = tests[location.name]
spot = verify_spot_to_fill(location, item_to_place, state, spp, pac, key_pool, world)
if spot and (item_to_place.smallkey or item_to_place.bigkey):
location.item = None
location.event = False
if item_to_place.smallkey:
key_pool.append(item_to_place)
if (expected and spot) or (not expected and spot is None):
logger.debug(f'Placing {item_name} ({item_player}) at {location.name} was {expected}')
config.verify_count += 1
if config.verify_count >= config.verify_target:
exit()
else:
issues.append((item_name, item_player, location.name, expected))
if len(issues) > 0:
for name, player, loc, expected in issues:
if expected:
logger.error(f'Could not place {name} ({player}) at {loc}')
else:
logger.error(f'{name} ({player}) should not be allowed at {loc}')
raise Exception(f'Test failed placing {name}')
def balance_multiworld_progression(world):
state = CollectionState(world)
checked_locations = set()
unchecked_locations = set(world.get_locations())
total_locations_count = Counter(location.player for location in world.get_locations() if not location.locked and not location.forced_item)
reachable_locations_count = {}
for player in range(1, world.players + 1):
reachable_locations_count[player] = 0
sphere_num = 1
moved_item_count = 0
def get_sphere_locations(sphere_state, locations):
sphere_state.sweep_for_events(key_only=True, locations=locations)
return {loc for loc in locations if sphere_state.can_reach(loc) and sphere_state.not_flooding_a_key(sphere_state.world, loc)}
def item_percentage(player, num):
return num / total_locations_count[player]
while True:
sphere_locations = get_sphere_locations(state, unchecked_locations)
for location in sphere_locations:
unchecked_locations.remove(location)
if not location.locked and not location.forced_item:
reachable_locations_count[location.player] += 1
logging.debug(f'Sphere {sphere_num}')
logging.debug(f'Reachable locations: {reachable_locations_count}')
debug_percentages = {
player: round(item_percentage(player, num), 2)
for player, num in reachable_locations_count.items()
}
logging.debug(f'Reachable percentages: {debug_percentages}\n')
sphere_num += 1
if checked_locations:
max_percentage = max(map(lambda p: item_percentage(p, reachable_locations_count[p]), reachable_locations_count))
threshold_percentages = {player: max_percentage * .8 for player in range(1, world.players + 1)}
logging.debug(f'Thresholds: {threshold_percentages}')
balancing_players = {player for player, reachables in reachable_locations_count.items()
if item_percentage(player, reachables) < threshold_percentages[player]}
if balancing_players:
balancing_state = state.copy()
balancing_unchecked_locations = unchecked_locations.copy()
balancing_reachables = reachable_locations_count.copy()
balancing_sphere = sphere_locations.copy()
candidate_items = collections.defaultdict(set)
while True:
for location in balancing_sphere:
if location.event and (world.keyshuffle[location.item.player] != 'none' or not location.item.smallkey) \
and (world.bigkeyshuffle[location.item.player] != 'none' or not location.item.bigkey):
balancing_state.collect(location.item, True, location)
player = location.item.player
if player in balancing_players and not location.locked and location.player != player:
candidate_items[player].add(location)
balancing_sphere = get_sphere_locations(balancing_state, balancing_unchecked_locations)
for location in balancing_sphere:
balancing_unchecked_locations.remove(location)
balancing_reachables[location.player] += 1
if world.has_beaten_game(balancing_state) or all(item_percentage(player, reachables) >= threshold_percentages[player]
for player, reachables in balancing_reachables.items()):
break
elif not balancing_sphere:
raise RuntimeError('Not all required items reachable. Something went terribly wrong here.')
unlocked_locations = collections.defaultdict(set)
for l in unchecked_locations:
if l not in balancing_unchecked_locations:
unlocked_locations[l.player].add(l)
items_to_replace = []
for player in balancing_players:
locations_to_test = unlocked_locations[player]
items_to_test = candidate_items[player]
while items_to_test:
testing = items_to_test.pop()
reducing_state = state.copy()
for location in itertools.chain((l for l in items_to_replace if l.item.player == player),
items_to_test):
reducing_state.collect(location.item, True, location)
reducing_state.sweep_for_events(locations=locations_to_test)
if world.has_beaten_game(balancing_state):
if not world.has_beaten_game(reducing_state):
items_to_replace.append(testing)
else:
reduced_sphere = get_sphere_locations(reducing_state, locations_to_test)
p = item_percentage(player, reachable_locations_count[player] + len(reduced_sphere))
if p < threshold_percentages[player]:
items_to_replace.append(testing)
replaced_items = False
# sort then shuffle to maintain deterministic behaviour,
# while allowing use of set for better algorithm growth behaviour elsewhere
replacement_locations = sorted((l for l in checked_locations if not l.event and not l.locked),
key=lambda loc: (loc.name, loc.player))
random.shuffle(replacement_locations)
items_to_replace.sort(key=lambda item: (item.name, item.player))
random.shuffle(items_to_replace)
while replacement_locations and items_to_replace:
old_location = items_to_replace.pop()
for new_location in replacement_locations:
if (new_location.can_fill(state, old_location.item, False) and
old_location.can_fill(state, new_location.item, False)):
replacement_locations.remove(new_location)
new_location.item, old_location.item = old_location.item, new_location.item
if world.shopsanity[new_location.player]:
check_shop_swap(new_location)
if world.shopsanity[old_location.player]:
check_shop_swap(old_location)
new_location.event, old_location.event = True, False
logging.debug(f"Progression balancing moved {new_location.item} to {new_location}, "
f"displacing {old_location.item} into {old_location}")
moved_item_count += 1
state.collect(new_location.item, True, new_location)
replaced_items = True
break
else:
logging.warning(f"Could not Progression Balance {old_location.item}")
if replaced_items:
logging.debug(f'Moved {moved_item_count} items so far\n')
unlocked = {fresh for player in balancing_players for fresh in unlocked_locations[player]}
for location in get_sphere_locations(state, unlocked):
unchecked_locations.remove(location)
reachable_locations_count[location.player] += 1
sphere_locations.add(location)
for location in sphere_locations:
if location.event and (world.keyshuffle[location.item.player] != 'none' or not location.item.smallkey) \
and (world.bigkeyshuffle[location.item.player] != 'none' or not location.item.bigkey):
state.collect(location.item, True, location)
checked_locations |= sphere_locations
if world.has_beaten_game(state):
break
elif not sphere_locations:
logging.warning('Progression Balancing ran out of paths.')
break
def check_shop_swap(l, make_item_free=False):
if l.parent_region.name in shop_to_location_table:
if l.name in shop_to_location_table[l.parent_region.name]:
idx = shop_to_location_table[l.parent_region.name].index(l.name)
inv_slot = l.parent_region.shop.inventory[idx]
inv_slot['item'] = l.item.name
if make_item_free:
inv_slot['price'] = 0
elif l.parent_region in retro_shops:
idx = retro_shops[l.parent_region.name].index(l.name)
inv_slot = l.parent_region.shop.inventory[idx]
inv_slot['item'] = l.item.name
def balance_money_progression(world):
logger = logging.getLogger('')
state = CollectionState(world)
unchecked_locations = world.get_locations().copy()
wallet = {player: 0 for player in range(1, world.players+1)}
kiki_check = {player: False for player in range(1, world.players+1)}
kiki_paid = {player: False for player in range(1, world.players+1)}
rooms_visited = {player: set() for player in range(1, world.players+1)}
balance_locations = {player: set() for player in range(1, world.players+1)}
pay_for_locations = {'Bottle Merchant': 100, 'Chest Game': 30, 'Digging Game': 80,
'King Zora': 500, 'Blacksmith': 10}
rupee_chart = {'Rupee (1)': 1, 'Rupees (5)': 5, 'Rupees (20)': 20, 'Rupees (50)': 50,
'Rupees (100)': 100, 'Rupees (300)': 300}
rupee_rooms = {'Eastern Rupees': 90, 'Mire Key Rupees': 45, 'Mire Shooter Rupees': 90,
'TR Rupees': 270, 'PoD Dark Basement': 270}
acceptable_balancers = ['Bombs (3)', 'Arrows (10)', 'Bombs (10)']
base_value = sum(rupee_rooms.values())
available_money = {player: base_value for player in range(1, world.players+1)}
for loc in world.get_locations():
if loc.item and loc.item.name in rupee_chart:
available_money[loc.item.player] += rupee_chart[loc.item.name]
total_price = {player: 0 for player in range(1, world.players+1)}
for player in range(1, world.players+1):
for shop, loc_list in shop_to_location_table.items():
for loc in loc_list:
loc = world.get_location(loc, player)
slot = shop_to_location_table[loc.parent_region.name].index(loc.name)
shop = loc.parent_region.shop
shop_item = shop.inventory[slot]
if shop_item:
total_price[player] += shop_item['price']
total_price[player] += 110 + sum(pay_for_locations.values())
# base needed: 830
# base available: 765
for player in range(1, world.players+1):
logger.debug(f'Money balance for P{player}: Needed: {total_price[player]} Available: {available_money[player]}')
def get_sphere_locations(sphere_state, locations):
sphere_state.sweep_for_events(key_only=True, locations=locations)
return [loc for loc in locations if sphere_state.can_reach(loc) and sphere_state.not_flooding_a_key(sphere_state.world, loc)]
def interesting_item(location, item, world, player):
if item.advancement:
return True
if item.type is not None or item.name.startswith('Rupee'):
return True
if item.name in ['Progressive Armor', 'Blue Mail', 'Red Mail']:
return True
if world.keyshuffle[player] == 'universal' and item.name == 'Small Key (Universal)':
return True
if world.bow_mode[player].startswith('retro') and item.name == 'Single Arrow':
return True
if location.name in pay_for_locations:
return True
return False
def kiki_required(state, location):
path = state.path[location.parent_region]
if path:
while path[1]:
if path[0] == 'Palace of Darkness':
return True
path = path[1]
return False
done = False
attempts = world.players * 20 + 20
while not done:
attempts -= 1
if attempts < 0:
from DungeonGenerator import GenerationException
raise GenerationException(f'Infinite loop detected at "balance_money_progression"')
sphere_costs = {player: 0 for player in range(1, world.players+1)}
locked_by_money = {player: set() for player in range(1, world.players+1)}
sphere_locations = get_sphere_locations(state, unchecked_locations)
checked_locations = []
for player in range(1, world.players+1):
kiki_payable = state.prog_items[('Moon Pearl', player)] > 0 or world.is_tile_swapped(0x1e, player)
if kiki_payable and world.get_region('Palace of Darkness Area', player) in state.reachable_regions[player]:
if not kiki_paid[player]:
kiki_check[player] = True
sphere_costs[player] += 110
locked_by_money[player].add('Kiki')
for location in sphere_locations:
location_free, loc_player = True, location.player
if location.parent_region.name in shop_to_location_table and location.name != 'Potion Shop':
slot = shop_to_location_table[location.parent_region.name].index(location.name)
shop = location.parent_region.shop
shop_item = shop.inventory[slot]
if location.item and interesting_item(location, location.item, world, location.item.player):
if location.item.name.startswith('Rupee') and loc_player == location.item.player:
if shop_item['price'] < rupee_chart[location.item.name]:
wallet[loc_player] -= shop_item['price'] # will get picked up in the location_free block
else:
location_free = False
else:
location_free = False
sphere_costs[loc_player] += shop_item['price']
locked_by_money[loc_player].add(location)
elif location.name in pay_for_locations:
sphere_costs[loc_player] += pay_for_locations[location.name]
location_free = False
locked_by_money[loc_player].add(location)
if kiki_check[loc_player] and not kiki_paid[loc_player] and kiki_required(state, location):
locked_by_money[loc_player].add(location)
location_free = False
if location_free and location.item:
state.collect(location.item, True, location)
unchecked_locations.remove(location)
if location.item:
if location.item.name.startswith('Rupee'):
wallet[location.item.player] += rupee_chart[location.item.name]
if location.item.name != 'Rupees (300)':
balance_locations[location.item.player].add(location)
if interesting_item(location, location.item, world, location.item.player):
checked_locations.append(location)
elif location.item.name in acceptable_balancers:
balance_locations[location.item.player].add(location)
for room, income in rupee_rooms.items():
for player in range(1, world.players+1):
if room not in rooms_visited[player] and world.get_region(room, player) in state.reachable_regions[player]:
wallet[player] += income
rooms_visited[player].add(room)
if checked_locations or len(unchecked_locations) == 0:
if world.has_beaten_game(state):
done = True
continue
# else go to next sphere
else:
# check for solvent players
solvent = set()
insolvent = set()
for player in range(1, world.players+1):
modifier = world.money_balance[player]/100
if wallet[player] >= sphere_costs[player] * modifier >= 0:
solvent.add(player)
if sphere_costs[player] > 0 and sphere_costs[player] * modifier > wallet[player]:
insolvent.add(player)
if len([p for p in solvent if len(locked_by_money[p]) > 0]) == 0:
if len(insolvent) > 0:
target_player = min(insolvent, key=lambda p: sphere_costs[p]-wallet[p])
target_modifier = world.money_balance[target_player]/100
difference = sphere_costs[target_player] * target_modifier - wallet[target_player]
logger.debug(f'Money balancing needed: Player {target_player} short {difference}')
else:
difference = 0
target_player = next(p for p in solvent)
while difference > 0:
swap_targets = [x for x in unchecked_locations if x not in sphere_locations and x.item.name.startswith('Rupees') and x.item.player == target_player]
if len(swap_targets) == 0:
best_swap, best_value = None, 300
else:
best_swap = max(swap_targets, key=lambda t: rupee_chart[t.item.name])
best_value = rupee_chart[best_swap.item.name]
increase_targets = [x for x in balance_locations[target_player] if x.item.name in rupee_chart and rupee_chart[x.item.name] < best_value]
if len(increase_targets) == 0:
increase_targets = [x for x in balance_locations[target_player] if (rupee_chart[x.item.name] if x.item.name in rupee_chart else 0) < best_value]
if len(increase_targets) == 0:
raise Exception('No early sphere swaps for rupees - money grind would be required - bailing for now')
best_target = min(increase_targets, key=lambda t: rupee_chart[t.item.name] if t.item.name in rupee_chart else 0)
make_item_free = wallet[target_player] < 20
old_value = 0 if make_item_free else (rupee_chart[best_target.item.name] if best_target.item.name in rupee_chart else 0)
if best_swap is None:
logger.debug(f'Upgrading {best_target.item.name} @ {best_target.name} for 300 Rupees')
best_target.item = ItemFactory('Rupees (300)', best_target.item.player)
best_target.item.location = best_target
check_shop_swap(best_target.item.location, make_item_free)
else:
old_item = best_target.item
logger.debug(f'Swapping {best_target.item.name} @ {best_target.name} for {best_swap.item.name} @ {best_swap.name}')
best_target.item = best_swap.item
best_target.item.location = best_target
best_swap.item = old_item
best_swap.item.location = best_swap
check_shop_swap(best_target.item.location, make_item_free)
check_shop_swap(best_swap.item.location)
increase = best_value - old_value
difference -= increase
wallet[target_player] += increase
solvent.add(target_player)
# apply solvency
for player in solvent:
modifier = world.money_balance[player]/100
wallet[player] -= sphere_costs[player] * modifier
for location in locked_by_money[player]:
if isinstance(location, str) and location == 'Kiki':
kiki_paid[player] = True
else:
state.collect(location.item, True, location)
unchecked_locations.remove(location)
if location.item and location.item.name.startswith('Rupee'):
wallet[location.item.player] += rupee_chart[location.item.name]
def set_prize_drops(world, player):
prizes = [0xD8, 0xD8, 0xD8, 0xD8, 0xD9, 0xD8, 0xD8, 0xD9,
0xDA, 0xD9, 0xDA, 0xDB, 0xDA, 0xD9, 0xDA, 0xDA,
0xE0, 0xDF, 0xDF, 0xDA, 0xE0, 0xDF, 0xD8, 0xDF,
0xDC, 0xDC, 0xDC, 0xDD, 0xDC, 0xDC, 0xDE, 0xDC,
0xE1, 0xD8, 0xE1, 0xE2, 0xE1, 0xD8, 0xE1, 0xE2,
0xDF, 0xD9, 0xD8, 0xE1, 0xDF, 0xDC, 0xD9, 0xD8,
0xD8, 0xE3, 0xE0, 0xDB, 0xDE, 0xD8, 0xDB, 0xE2,
0xD9, 0xDA, 0xDB, 0xD9, 0xDB, 0xD9, 0xDB]
dig_prizes = [0xB2, 0xD8, 0xD8, 0xD8, 0xD8, 0xD8, 0xD8, 0xD8, 0xD8,
0xD9, 0xD9, 0xD9, 0xD9, 0xD9, 0xDA, 0xDA, 0xDA, 0xDA, 0xDA,
0xDB, 0xDB, 0xDB, 0xDB, 0xDB, 0xDC, 0xDC, 0xDC, 0xDC, 0xDC,
0xDD, 0xDD, 0xDD, 0xDD, 0xDD, 0xDE, 0xDE, 0xDE, 0xDE, 0xDE,
0xDF, 0xDF, 0xDF, 0xDF, 0xDF, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0,
0xE1, 0xE1, 0xE1, 0xE1, 0xE1, 0xE2, 0xE2, 0xE2, 0xE2, 0xE2,
0xE3, 0xE3, 0xE3, 0xE3, 0xE3]
def chunk(l,n):
return [l[i:i+n] for i in range(0, len(l), n)]
possible_prizes = {
'Small Heart': 0xD8, 'Fairy': 0xE3,
'Rupee (1)': 0xD9, 'Rupees (5)': 0xDA, 'Rupees (20)': 0xDB,
'Big Magic': 0xE0, 'Small Magic': 0xDF,
'Single Bomb': 0xDC, 'Bombs (4)': 0xDD,
'Bombs (8)': 0xDE, 'Arrows (5)': 0xE1, 'Arrows (10)': 0xE2
} #weights, if desired 13, 1, 9, 7, 6, 3, 6, 7, 1, 2, 5, 3
uniform_prizes = list(possible_prizes.values())
prizes[-7:] = random.sample(prizes, 7)
#shuffle order of 7 main packs
packs = chunk(prizes[:56], 8)
random.shuffle(packs)
prizes[:56] = [drop for pack in packs for drop in pack]
if world.customizer:
drops = world.customizer.get_drops()
if drops:
for player, drop_config in drops.items():
for pack_num in range(1, 8):
if f'Pack {pack_num}' in drop_config:
for idx, prize in enumerate(drop_config[f'Pack {pack_num}']):
chosen = random.choice(uniform_prizes) if prize == 'Random' else possible_prizes[prize]
prizes[(pack_num-1)*8 + idx] = chosen
for tree_pull_tier in range(1, 4):
if f'Tree Pull Tier {tree_pull_tier}' in drop_config:
prize = drop_config[f'Tree Pull Tier {tree_pull_tier}']
chosen = random.choice(uniform_prizes) if prize == 'Random' else possible_prizes[prize]
prizes[63-tree_pull_tier] = chosen # (62 through 60 in reverse)
for key, pos in {'Crab Normal': 59, 'Crab Special': 58, 'Stun Prize': 57, 'Fish': 56}.items():
if key in drop_config:
prize = drop_config[key]
chosen = random.choice(uniform_prizes) if prize == 'Random' else possible_prizes[prize]
prizes[pos] = chosen
if world.difficulty_adjustments[player] in ['hard', 'expert']:
prize_replacements = {0xE0: 0xDF, # Fairy -> heart
0xE3: 0xD8} # Big magic -> small magic
prizes = [prize_replacements.get(prize, prize) for prize in prizes]
dig_prizes = [prize_replacements.get(prize, prize) for prize in dig_prizes]
if world.bow_mode[player].startswith('retro'):
prize_replacements = {0xE1: 0xDA, #5 Arrows -> Blue Rupee
0xE2: 0xDB} #10 Arrows -> Red Rupee
prizes = [prize_replacements.get(prize, prize) for prize in prizes]
dig_prizes = [prize_replacements.get(prize, prize) for prize in dig_prizes]
# write tree pull prizes
world.prizes[player]['dig'] = dig_prizes
# write tree pull prizes
world.prizes[player]['pull'] = [ prizes.pop(), prizes.pop(), prizes.pop() ]
# rupee crab prizes
world.prizes[player]['crab'] = [ prizes.pop(), prizes.pop() ]
# stunned enemy prize
world.prizes[player]['stun'] = prizes.pop()
# saved fish prize
world.prizes[player]['fish'] = prizes.pop()
world.prizes[player]['enemies'] = prizes