diff --git a/Bosses.py b/Bosses.py index 53393d5f..206b797c 100644 --- a/Bosses.py +++ b/Bosses.py @@ -202,12 +202,15 @@ def place_bosses(world, player): place_boss(boss, level, loc, loc_text, world, player) elif world.boss_shuffle[player] == 'unique': bosses = list(placeable_bosses) + gt_bosses = [] for [loc, level] in boss_locations: loc_text = loc + (' ('+level+')' if level else '') try: if level: - boss = random.choice([b for b in placeable_bosses if can_place_boss(world, player, b, loc, level)]) + boss = random.choice([b for b in placeable_bosses if can_place_boss(world, player, b, loc, level) + and b not in gt_bosses]) + gt_bosses.append(boss) else: boss = random.choice([b for b in bosses if can_place_boss(world, player, b, loc, level)]) bosses.remove(boss) diff --git a/Fill.py b/Fill.py index 74274543..bb6e70cf 100644 --- a/Fill.py +++ b/Fill.py @@ -3,6 +3,7 @@ import collections import itertools import logging import math +from contextlib import suppress from BaseClasses import CollectionState, FillError, LocationType from Items import ItemFactory @@ -35,17 +36,6 @@ def dungeon_tracking(world): def fill_dungeons_restrictive(world, shuffled_locations): dungeon_tracking(world) - 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(): @@ -55,17 +45,28 @@ def fill_dungeons_restrictive(world, shuffled_locations): item.priority = True dungeon_items = [item for item in get_dungeon_item_pool(world) if item.is_inside_dungeon_item(world)] + bigs, smalls, others = [], [], [] + for i in dungeon_items: + (bigs if i.bigkey else smalls if i.smallkey else others).append(i) - # 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)) + def fill(base_state, items, key_pool): + fill_restrictive(world, base_state, shuffled_locations, items, key_pool, True) - 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) + all_state_base = world.get_all_state() + big_state_base = all_state_base.copy() + for x in smalls + others: + big_state_base.collect(x, True) + fill(big_state_base, bigs, smalls) + random.shuffle(shuffled_locations) + small_state_base = all_state_base.copy() + for x in others: + small_state_base.collect(x, True) + fill(small_state_base, smalls, smalls) + random.shuffle(shuffled_locations) + fill(all_state_base, others, None) -def fill_restrictive(world, base_state, locations, itempool, keys_in_itempool=None, single_player_placement=False, +def fill_restrictive(world, base_state, locations, itempool, key_pool=None, single_player_placement=False, vanilla=False): def sweep_from_pool(): new_state = base_state.copy() @@ -101,8 +102,7 @@ def fill_restrictive(world, base_state, locations, itempool, keys_in_itempool=No 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) + single_player_placement, perform_access_check, key_pool, world) if spot_to_fill: break if spot_to_fill is None: @@ -111,7 +111,7 @@ def fill_restrictive(world, base_state, locations, itempool, keys_in_itempool=No continue 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) + 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) @@ -123,6 +123,10 @@ def fill_restrictive(world, base_state, locations, itempool, keys_in_itempool=No raise FillError('No more spots to place %s' % item_to_place) world.push_item(spot_to_fill, item_to_place, False) + # todo: remove key item from key_pool + 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) @@ -132,7 +136,7 @@ def fill_restrictive(world, base_state, locations, itempool, keys_in_itempool=No def verify_spot_to_fill(location, item_to_place, max_exp_state, single_player_placement, perform_access_check, - itempool, keys_in_itempool, world): + 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 test_state = max_exp_state.copy() @@ -141,8 +145,7 @@ def verify_spot_to_fill(location, item_to_place, max_exp_state, single_player_pl 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 valid_key_placement(item_to_place, location, key_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: @@ -150,7 +153,7 @@ def verify_spot_to_fill(location, item_to_place, max_exp_state, single_player_pl return None -def valid_key_placement(item, location, itempool, world): +def valid_key_placement(item, location, key_pool, world): if not valid_reserved_placement(item, location, world): return False if ((not item.smallkey and not item.bigkey) or item.player != location.player @@ -161,7 +164,7 @@ def valid_key_placement(item, location, itempool, world): if dungeon.name not in item.name and (dungeon.name != 'Hyrule Castle' or 'Escape' not in item.name): return True key_logic = world.key_logic[item.player][dungeon.name] - unplaced_keys = len([x for x in itempool if x.name == key_logic.small_key_name and x.player == item.player]) + 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: prize_loc = world.get_location(key_logic.prize_location, location.player) @@ -216,16 +219,16 @@ def is_dungeon_item(item, world): def recovery_placement(item_to_place, locations, world, state, base_state, itempool, perform_access_check, attempted, - keys_in_itempool=None, single_player_placement=False): + 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, keys_in_itempool, + 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.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) + key_pool, single_player_placement) else: i, config = 0, world.item_pool_config tried = set(attempted) @@ -235,7 +238,7 @@ def recovery_placement(item_to_place, locations, world, state, base_state, itemp 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) + perform_access_check, key_pool, world) if spot_to_fill: return spot_to_fill i += 1 @@ -244,14 +247,14 @@ def recovery_placement(item_to_place, locations, world, state, base_state, itemp 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) + 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, itempool, keys_in_itempool, world) + perform_access_check, key_pool, world) if spot_to_fill: return spot_to_fill return None @@ -259,14 +262,14 @@ def recovery_placement(item_to_place, locations, world, state, base_state, itemp 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) + 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, - keys_in_itempool=None, single_player_placement=False): + key_pool=None, single_player_placement=False): def location_preference(loc): if not loc.item.advancement: return 1 @@ -284,21 +287,21 @@ def last_ditch_placement(item_to_place, locations, world, state, base_state, ite 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) + key_pool, 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): + 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, - keys_in_itempool, single_player_placement) + 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, - keys_in_itempool, single_player_placement) + 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) @@ -414,13 +417,13 @@ 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)} + 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, keys_in_pool, vanilla=True) - fill_restrictive(world, world.state, fill_locations, progitempool, keys_in_pool) + 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 == 'balanced': fast_fill(world, prioitempool, fill_locations) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index ce3e948c..56ecca32 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -184,7 +184,11 @@ Same as above but both small keys and bigs keys of the dungeon are not allowed o #### Unstable * 1.0.1.1 - * Fixed the pots in Mire Storyteller/ Dark Desert Hint to be colorized when they should be + * Fixed the pots in Mire Storyteller/ Dark Desert Hint to be colorized when they should be + * Certain pot items no longer reload when reloading the supertile (matches original pot behavior better) + * Changed the key distribution that made small keys placement more random when keys are in their own dungeon + * Unique boss shuffle no longer allows repeat bosses in GT (e.g. only one Trinexx in GT, so exactly 3 bosses are repeated in the seed. This is a difference process than full which does affect the probability distribution.) + * Removed text color in hints due to vanilla bug * 1.0.1.0 * Large features * New pottery modes - see notes above @@ -208,7 +212,6 @@ Same as above but both small keys and bigs keys of the dungeon are not allowed o * Refactored spoiler to generate in stages for better error collection. A meta file will be generated additionally for mystery seeds. Some random settings moved later in the spoiler to have the meta section at the top not spoil certain things. (GT/Ganon requirements.) Thanks to codemann and OWR for most of this work. * Updated tourney winners (included Doors Async League winners) * Some textual changes for hints (capitalization standardization) - * Item will be highlighted in red if experimental is on. This will likely be removed. * Reworked GT Trash Fill. Base rate is 0-75% of locations fill with 7 crystals entrance requirements. Triforce hunt is 75%-100% of locations. The 75% number will decrease based on the crystal entrance requirement. Dungeon_only algorithm caps it based on how many items need to be placed in dungeons. Cross dungeon shuffle will now work with the trash fill. * Expanded Mystery logic options (e.g. owglitches) * Updated indicators on keysanity menu for overworld map option diff --git a/Rom.py b/Rom.py index 3a4661da..c3312b9a 100644 --- a/Rom.py +++ b/Rom.py @@ -37,7 +37,7 @@ from source.dungeon.RoomList import Room0127 JAP10HASH = '03a63945398191337e896e5771f77173' -RANDOMIZERBASEHASH = '9008f4335101689f01184e58295fdbc5' +RANDOMIZERBASEHASH = '0f96237c73cccaf7a250343fe3e8c887' class JsonRom(object): @@ -1979,8 +1979,6 @@ def write_strings(rom, world, player, team): else: if isinstance(dest, Region) and dest.type == RegionType.Dungeon and dest.dungeon: hint = dest.dungeon.name - elif isinstance(dest, Item) and world.experimental[player]: - hint = f'{{C:RED}}{dest.hint_text}{{C:WHITE}}' if dest.hint_text else 'something' else: hint = dest.hint_text if dest.hint_text else "something" if dest.player != player: @@ -2149,8 +2147,7 @@ def write_strings(rom, world, player, team): if this_location: item_name = this_location[0].item.hint_text item_name = item_name[0].upper() + item_name[1:] - item_format = f'{{C:RED}}{item_name}{{C:WHITE}}' if world.experimental[player] else item_name - this_hint = f'{item_format} can be found {hint_text(this_location[0])}.' + this_hint = f'{item_name} can be found {hint_text(this_location[0])}.' tt[hint_locations.pop(0)] = this_hint hint_count -= 1 @@ -2204,8 +2201,7 @@ def write_strings(rom, world, player, team): elif hint_type == 'path': if item_count == 1: the_item = text_for_item(next(iter(choice_set)), world, player, team) - item_format = f'{{C:RED}}{the_item}{{C:WHITE}}' if world.experimental[player] else the_item - hint_candidates.append((hint_type, f'{name} conceals only {item_format}')) + hint_candidates.append((hint_type, f'{name} conceals only {the_item}')) else: hint_candidates.append((hint_type, f'{name} conceals {item_count} {item_type} items')) district_hints = min(len(hint_candidates), len(hint_locations)) diff --git a/data/base2current.bps b/data/base2current.bps index 464ceaf6..55525ee5 100644 Binary files a/data/base2current.bps and b/data/base2current.bps differ