Merge remote-tracking branch 'origin/OverworldShuffle' into OverworldShuffle

This commit is contained in:
2023-02-27 10:21:25 -06:00
92 changed files with 13130 additions and 3047 deletions

238
Fill.py
View File

@@ -13,7 +13,7 @@ from source.item.FillUtil import filter_pot_locations, valid_pot_items
def get_dungeon_item_pool(world):
return [item for dungeon in world.dungeons for item in dungeon.all_items]
return [item for dungeon in world.dungeons for item in dungeon.all_items if item.location is None]
def promote_dungeon_items(world):
@@ -24,7 +24,6 @@ def promote_dungeon_items(world):
item.advancement = True
elif item.map or item.compass:
item.priority = True
dungeon_tracking(world)
def dungeon_tracking(world):
@@ -35,11 +34,11 @@ def dungeon_tracking(world):
def fill_dungeons_restrictive(world, shuffled_locations):
dungeon_tracking(world)
# 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]):
if ((item.smallkey and world.keyshuffle[item.player] != 'none')
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
@@ -50,7 +49,7 @@ def fill_dungeons_restrictive(world, shuffled_locations):
(bigs if i.bigkey else smalls if i.smallkey else others).append(i)
unplaced_smalls = list(smalls)
for i in world.itempool:
if i.smallkey and world.keyshuffle[i.player]:
if i.smallkey and world.keyshuffle[i.player] != 'none':
unplaced_smalls.append(i)
def fill(base_state, items, key_pool):
@@ -72,11 +71,13 @@ def fill_dungeons_restrictive(world, shuffled_locations):
def fill_restrictive(world, base_state, locations, itempool, key_pool=None, single_player_placement=False,
vanilla=False):
def sweep_from_pool():
def sweep_from_pool(placing_item=None):
new_state = base_state.copy()
for item in itempool:
new_state.collect(item, True)
new_state.placing_item = placing_item
new_state.sweep_for_events()
new_state.placing_item = None
return new_state
unplaced_items = []
@@ -93,7 +94,7 @@ def fill_restrictive(world, base_state, locations, itempool, key_pool=None, sing
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()
maximum_exploration_state = sweep_from_pool(placing_item=items_to_place[0])
has_beaten_game = world.has_beaten_game(maximum_exploration_state)
for item_to_place in items_to_place:
@@ -104,6 +105,8 @@ def fill_restrictive(world, base_state, locations, itempool, key_pool=None, sing
spot_to_fill = None
item_locations = filter_locations(item_to_place, locations, world, vanilla)
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)
@@ -127,9 +130,6 @@ def fill_restrictive(world, base_state, locations, itempool, key_pool=None, sing
raise FillError('No more spots to place %s' % item_to_place)
world.push_item(spot_to_fill, item_to_place, False)
if item_to_place.smallkey:
with suppress(ValueError):
key_pool.remove(item_to_place)
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)
@@ -142,25 +142,33 @@ def verify_spot_to_fill(location, item_to_place, max_exp_state, single_player_pl
key_pool, 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
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, world):
if valid_key_placement(item_to_place, location, key_pool, test_state, 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
location.event = False
if item_to_place.smallkey:
key_pool.append(item_to_place)
return None
def valid_key_placement(item, location, key_pool, world):
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.universal_keys[item.player] or world.logic[item.player] == 'nologic'):
or world.keyshuffle[item.player] == 'universal' or world.logic[item.player] == 'nologic'):
return True
dungeon = location.parent_region.dungeon
if dungeon:
@@ -172,8 +180,16 @@ def valid_key_placement(item, location, key_pool, world):
if key_logic.prize_location:
prize_loc = world.get_location(key_logic.prize_location, location.player)
cr_count = world.crystals_needed_for_gt[location.player]
wild_keys = world.keyshuffle[item.player]
return key_logic.check_placement(unplaced_keys, wild_keys, location if item.bigkey else None, prize_loc, cr_count)
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)
@@ -204,6 +220,7 @@ def track_outside_keys(item, location, world):
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):
@@ -216,7 +233,7 @@ def track_dungeon_items(item, location, world):
def is_dungeon_item(item, world):
return ((item.smallkey and not world.keyshuffle[item.player])
return ((item.smallkey and world.keyshuffle[item.player] == 'none')
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]))
@@ -288,7 +305,7 @@ 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 == 'Crystal']
else:
possible_swaps = [x for x in state.locations_checked
if x.item.type not in ['Event', 'Crystal'] and not x.forced_item]
if x.item.type not in ['Event', 'Crystal'] 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)
@@ -344,7 +361,9 @@ def find_spot_for_item(item_to_place, locations, world, base_state, pool,
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, world):
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
@@ -385,6 +404,7 @@ def distribute_items_restrictive(world, gftower_trash=False, fill_locations=None
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]
@@ -405,7 +425,7 @@ def distribute_items_restrictive(world, gftower_trash=False, fill_locations=None
else:
max_trash = gt_count
scaled_trash = math.floor(max_trash * scale_factor)
if world.goal[player] in ['triforcehunt', 'trinity', 'z1']:
if world.goal[player] in ['triforcehunt', 'trinity', 'z1', 'ganonhunt']:
gftower_trash_count = random.randint(scaled_trash, max_trash)
else:
gftower_trash_count = random.randint(0, scaled_trash)
@@ -424,9 +444,16 @@ def distribute_items_restrictive(world, gftower_trash=False, fill_locations=None
random.shuffle(fill_locations)
fill_locations.reverse()
# 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)
# 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
@@ -475,6 +502,20 @@ def distribute_items_restrictive(world, gftower_trash=False, fill_locations=None
ensure_good_pots(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(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 calc_trash_locations(world, player):
total_count, gt_count = 0, 0
for loc in world.get_locations():
@@ -490,14 +531,15 @@ def calc_trash_locations(world, player):
def ensure_good_pots(world, write_skips=False):
for loc in world.get_locations():
# # convert Arrows 5 when necessary
# if (loc.item.name in {'Arrows (5)'}
# and loc.type not in [LocationType.Pot, LocationType.Bonk]):
# loc.item = ItemFactory(invalid_location_replacement[loc.item.name], loc.item.player)
# convert Nothing when necessary
if loc.item is None:
loc.item = ItemFactory('Nothing', loc.player)
# convert Arrows 5 and Nothing when necessary
if (loc.item.name in {'Nothing'}
and (loc.type != LocationType.Pot or loc.item.player != loc.player)):
loc.item = ItemFactory(invalid_location_replacement[loc.item.name], loc.item.player)
if (loc.item.name in {'Arrows (5)'}
and (loc.type not in [LocationType.Pot, LocationType.Bonk] or loc.item.player != loc.player)):
loc.item = ItemFactory(invalid_location_replacement[loc.item.name], loc.item.player)
# # can be placed here by multiworld balancing or shop balancing
# # change it to something normal for the player it got swapped to
# elif (loc.item.name in {'Chicken', 'Big Magic'}
@@ -507,7 +549,7 @@ def ensure_good_pots(world, write_skips=False):
# else:
# loc.item = ItemFactory(invalid_location_replacement[loc.item.name], loc.player)
# do the arrow retro check
if world.rupee_bow[loc.item.player] and loc.item.name in {'Arrows (5)', 'Arrows (10)'}:
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
if write_skips:
@@ -528,10 +570,17 @@ def fast_fill_helper(world, item_pool, fill_locations):
def fast_fill(world, item_pool, fill_locations):
while item_pool and 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 = item_pool.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):
@@ -641,6 +690,40 @@ def sell_keys(world, player):
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()
@@ -672,7 +755,7 @@ def balance_multiworld_progression(world):
candidate_items = collections.defaultdict(set)
while True:
for location in balancing_sphere:
if location.event and (world.keyshuffle[location.item.player] or not location.item.smallkey) and (world.bigkeyshuffle[location.item.player] or not location.item.bigkey):
if location.event and (world.keyshuffle[location.item.player] != 'none' or not location.item.smallkey) and (world.bigkeyshuffle[location.item.player] 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:
@@ -747,7 +830,7 @@ def balance_multiworld_progression(world):
sphere_locations.add(location)
for location in sphere_locations:
if location.event and (world.keyshuffle[location.item.player] or not location.item.smallkey) and (world.bigkeyshuffle[location.item.player] or not location.item.bigkey):
if location.event and (world.keyshuffle[location.item.player] != 'none' or not location.item.smallkey) and (world.bigkeyshuffle[location.item.player] or not location.item.bigkey):
state.collect(location.item, True, location)
checked_locations |= sphere_locations
@@ -823,9 +906,9 @@ def balance_money_progression(world):
return True
if item.name in ['Progressive Armor', 'Blue Mail', 'Red Mail']:
return True
if world.universal_keys and item.name == 'Small Key (Universal)':
if world.keyshuffle[player] == 'universal' and item.name == 'Small Key (Universal)':
return True
if world.rupee_bow and item.name == 'Single Arrow':
if world.bow_mode[player].startswith('retro') and item.name == 'Single Arrow':
return True
if location.name in pay_for_locations:
return True
@@ -965,31 +1048,92 @@ def balance_money_progression(world):
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]
# randomize last 7 slots
new_prizes = random.sample(prizes, 7)
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 prize, idx 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
new_prizes = [prize_replacements.get(prize, prize) for prize in new_prizes]
prizes = [prize_replacements.get(prize, prize) for prize in prizes]
dig_prizes = [prize_replacements.get(prize, prize) for prize in dig_prizes]
if world.rupee_bow[player]:
if world.bow_mode[player].startswith('retro'):
prize_replacements = {0xE1: 0xDA, #5 Arrows -> Blue Rupee
0xE2: 0xDB} #10 Arrows -> Red Rupee
new_prizes = [prize_replacements.get(prize, prize) for prize in new_prizes]
prizes = [prize_replacements.get(prize, prize) for prize in prizes]
dig_prizes = [prize_replacements.get(prize, prize) for prize in dig_prizes]
if world.swords[player] == "bombs":
prize_replacements = {0xDC: 0xD9, # 1 Bomb -> Green Rupee
0xDD: 0xDA, # 3 Bombs -> Blue Rupee
0xDE: 0xDB} # 10 Bombs -> 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]['pull'] = [ new_prizes.pop(), new_prizes.pop(), new_prizes.pop() ]
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'] = [ new_prizes.pop(), new_prizes.pop() ]
world.prizes[player]['crab'] = [ prizes.pop(), prizes.pop() ]
# stunned enemy prize
world.prizes[player]['stun'] = new_prizes.pop()
world.prizes[player]['stun'] = prizes.pop()
# saved fish prize
world.prizes[player]['fish'] = new_prizes.pop()
world.prizes[player]['fish'] = prizes.pop()
world.prizes[player]['enemies'] = prizes