diff --git a/BaseClasses.py b/BaseClasses.py index e37b8d47..58cfed79 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -62,6 +62,8 @@ class World(object): self.aga_randomness = True self.lock_aga_door_in_escape = False self.save_and_quit_from_boss = True + self.override_bomb_check = False + self.is_copied_world = False self.accessibility = accessibility.copy() self.fix_skullwoods_exit = {} self.fix_palaceofdarkness_exit = {} @@ -1104,7 +1106,7 @@ class CollectionState(object): region = self.world.get_region(regionname, player) return region.can_reach(self) and ((self.world.mode[player] != 'inverted' and region.is_light_world) or (self.world.mode[player] == 'inverted' and region.is_dark_world) or self.has('Pearl', player)) - for region in rupee_farms: + for region in rupee_farms if self.world.pottery[player] in ['none', 'keys', 'dungeon'] else ['Archery Game']: if can_reach_non_bunny(region): return True @@ -1186,7 +1188,7 @@ class CollectionState(object): return region.can_reach(self) and ((self.world.mode[player] != 'inverted' and region.is_light_world) or (self.world.mode[player] == 'inverted' and region.is_dark_world) or self.has('Pearl', player)) # bomb pickups - for region in bush_bombs + bomb_caves: + for region in bush_bombs + (bomb_caves if self.world.pottery[player] in ['none', 'keys', 'dungeon'] else []): if can_reach_non_bunny(region): return True @@ -1306,7 +1308,7 @@ class CollectionState(object): # In the future, this can be used to check if the player starts without bombs def can_use_bombs(self, player): - return (not self.world.bombbag[player] or self.has('Bomb Upgrade (+10)', player) or self.has('Bomb Upgrade (+5)', player, 2)) and ((hasattr(self.world,"override_bomb_check") and self.world.override_bomb_check) or self.can_farm_bombs(player)) + return (not self.world.bombbag[player] or self.has('Bomb Upgrade (+10)', player) or self.has('Bomb Upgrade (+5)', player, 2)) and (self.world.override_bomb_check or self.can_farm_bombs(player)) def can_hit_crystal(self, player): return (self.can_use_bombs(player) @@ -1335,16 +1337,16 @@ class CollectionState(object): return self.has('Bow', player) and (self.can_buy_unlimited('Single Arrow', player) or self.has('Single Arrow', player)) return self.has('Bow', player) - def can_get_good_bee(self, player): - cave = self.world.get_region('Good Bee Cave', player) - return ( - self.can_use_bombs(player) and - self.has_bottle(player) and - self.has('Bug Catching Net', player) and - (self.has_Boots(player) or (self.has_sword(player) and self.has('Quake', player))) and - cave.can_reach(self) and - self.is_not_bunny(cave, player) - ) + # def can_get_good_bee(self, player): + # cave = self.world.get_region('Good Bee Cave', player) + # return ( + # self.can_use_bombs(player) and + # self.has_bottle(player) and + # self.has('Bug Catching Net', player) and + # (self.has_Boots(player) or (self.has_sword(player) and self.has('Quake', player))) and + # cave.can_reach(self) and + # self.is_not_bunny(cave, player) + # ) def has_beaten_aga(self, player): return self.has('Beat Agahnim 1', player) and (self.world.mode[player] != 'standard' or self.has('Zelda Delivered', player)) diff --git a/Bosses.py b/Bosses.py index a42b5176..d84df921 100644 --- a/Bosses.py +++ b/Bosses.py @@ -61,8 +61,7 @@ def MothulaDefeatRule(state, player): # TODO: Not sure how much (if any) extend magic is needed for these two, since they only apply # to non-vanilla locations, so are harder to test, so sticking with what VT has for now: (state.has('Cane of Somaria', player) and state.can_extend_magic(player, 16)) or - (state.has('Cane of Byrna', player) and state.can_extend_magic(player, 16)) or - state.can_get_good_bee(player) + (state.has('Cane of Byrna', player) and state.can_extend_magic(player, 16)) ) def BlindDefeatRule(state, player): @@ -202,13 +201,14 @@ 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 = list() + 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) and b not in gt_bosses]) + 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)]) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1755913e..aac5994d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,19 @@ # Changelog +## 0.2.10.0 +- Merged DR v1.0.1.1-1.0.1.2 + - Removed text color from hint tiles + - Removed Good Bee requirement from Mothula + - Some keylogic/generation fixes + - Fixed a Pottery logic issue in the playthru +- Fixed a generation error in Mixed OWR, resulting in more possible Mixed scenarios (thanks Catobat) +- Added more scenarios where OW Map Checks in Mixed OWR show dungeon prizes in their respective worlds +- Fixed rupee logic to consider Pottery option and lack of early rupees +- Changed Lean ER + Inverted Dark Chapel start is guaranteed to be in DW +- Fixed graphical issue with Hammerpeg Cave +- Fixed logic rule with HC Main Gate to not require mirror if screen is swapped +- Removed Crossed OWR option: "None (Allowed)" + ### 0.2.9.1 - Lite/Lean ER now includes Cave Pot locations with various Pottery options - Changed Unique Boss Shuffle so that GT Bosses are unique amongst themselves diff --git a/DoorShuffle.py b/DoorShuffle.py index 09886e1f..145ff35d 100644 --- a/DoorShuffle.py +++ b/DoorShuffle.py @@ -215,7 +215,7 @@ def vanilla_key_logic(world, player): key_layout = build_key_layout(builder, start_regions, doors, world, player) valid = validate_key_layout(key_layout, world, player) if not valid: - logging.getLogger('').warning('Vanilla key layout not valid %s', builder.name) + logging.getLogger('').info('Vanilla key layout not valid %s', builder.name) builder.key_door_proposal = doors if player not in world.key_logic.keys(): world.key_logic[player] = {} @@ -381,7 +381,7 @@ def choose_portals(world, player): if world.doorShuffle[player] in ['basic', 'crossed']: cross_flag = world.doorShuffle[player] == 'crossed' # key drops allow the big key in the right place in Desert Tiles 2 - bk_shuffle = world.bigkeyshuffle[player] or world.dropshuffle[player] + bk_shuffle = world.bigkeyshuffle[player] or world.pottery[player] not in ['none', 'cave'] std_flag = world.mode[player] == 'standard' # roast incognito doors world.get_room(0x60, player).delete(5) @@ -989,11 +989,16 @@ def cross_dungeon(world, player): paths = determine_required_paths(world, player) check_required_paths(paths, world, player) + hc_compass = ItemFactory('Compass (Escape)', player) + at_compass = ItemFactory('Compass (Agahnims Tower)', player) + at_map = ItemFactory('Map (Agahnims Tower)', player) + if world.restrict_boss_items[player] != 'none': + hc_compass.advancement = at_compass.advancement = at_map.advancement = True hc = world.get_dungeon('Hyrule Castle', player) - hc.dungeon_items.append(ItemFactory('Compass (Escape)', player)) + hc.dungeon_items.append(hc_compass) at = world.get_dungeon('Agahnims Tower', player) - at.dungeon_items.append(ItemFactory('Compass (Agahnims Tower)', player)) - at.dungeon_items.append(ItemFactory('Map (Agahnims Tower)', player)) + at.dungeon_items.append(at_compass) + at.dungeon_items.append(at_map) assign_cross_keys(dungeon_builders, world, player) all_dungeon_items_cnt = len(list(y for x in world.dungeons if x.player == player for y in x.all_items)) @@ -1896,16 +1901,18 @@ def find_inaccessible_regions(world, player): if any(x for x in ledge.exits if x.connected_region and x.connected_region.name == 'Agahnims Tower Portal'): world.inaccessible_regions[player].append('Hyrule Castle Ledge') logger = logging.getLogger('') - logger.debug('Inaccessible Regions:') - for r in world.inaccessible_regions[player]: - logger.debug('%s', r) + #logger.debug('Inaccessible Regions:') + #for r in world.inaccessible_regions[player]: + # logger.debug('%s', r) def find_accessible_entrances(world, player, builder): entrances = [region.name for region in (portal.door.entrance.parent_region for portal in world.dungeon_portals[player]) if region.dungeon.name == builder.name] entrances.extend(drop_entrances[builder.name]) + hc_std = False if world.mode[player] == 'standard' and builder.name == 'Hyrule Castle': + hc_std = True start_regions = ['Hyrule Castle Courtyard'] else: start_regions = ['Links House' if not world.is_bombshop_start(player) else 'Big Bomb Shop', 'Sanctuary' if world.mode[player] != 'inverted' else 'Dark Sanctuary Hint'] @@ -1930,6 +1937,8 @@ def find_accessible_entrances(world, player, builder): if connect not in queue and connect not in visited_regions: queue.append(connect) for ext in next_region.exits: + if hc_std and ext.name == 'Hyrule Castle Main Gate (North)': # just skip it + continue connect = ext.connected_region if connect is None or ext.door and ext.door.blocked: continue diff --git a/EntranceShuffle.py b/EntranceShuffle.py index b15a7438..d6c207f2 100644 --- a/EntranceShuffle.py +++ b/EntranceShuffle.py @@ -3,6 +3,7 @@ from collections import defaultdict, OrderedDict import RaceRandom as random from BaseClasses import CollectionState, RegionType from OverworldShuffle import build_accessible_region_list +from DoorShuffle import find_inaccessible_regions from OWEdges import OWTileRegions from Utils import stack_size3a @@ -31,7 +32,7 @@ def link_entrances(world, player): Cave_Three_Exits = Cave_Three_Exits_Base.copy() from OverworldShuffle import build_sectors - if not world.owsectors[player]: + if not world.owsectors[player] and world.shuffle[player] != 'vanilla': world.owsectors[player] = build_sectors(world, player) # modifications to lists @@ -831,8 +832,6 @@ def connect_mandatory_exits(world, entrances, caves, must_be_exits, player, must """This works inplace""" random.shuffle(entrances) random.shuffle(caves) - - from DoorShuffle import find_inaccessible_regions used_caves = [] required_entrances = 0 # Number of entrances reserved for used_caves @@ -1274,7 +1273,6 @@ def full_shuffle_dungeons(world, Dungeon_Exits, player): dw_entrances.extend([e for e in dungeon_owid_map[owid][0] if e in entrance_pool]) # determine must-exit entrances - from DoorShuffle import find_inaccessible_regions find_inaccessible_regions(world, player) lw_must_exit = list() @@ -1442,13 +1440,12 @@ def place_old_man(world, pool, player, ignore_list=[]): def junk_fill_inaccessible(world, player): - from Main import copy_world - from DoorShuffle import find_inaccessible_regions + from Main import copy_world_limited find_inaccessible_regions(world, player) for p in range(1, world.players + 1): world.key_logic[p] = {} - base_world = copy_world(world, True) + base_world = copy_world_limited(world) base_world.override_bomb_check = True # remove regions that have a dungeon entrance @@ -1488,7 +1485,6 @@ def connect_inaccessible_regions(world, lw_entrances, dw_entrances, caves, playe random.shuffle(lw_entrances) random.shuffle(dw_entrances) - from DoorShuffle import find_inaccessible_regions find_inaccessible_regions(world, player) # remove regions that have a dungeon entrance @@ -1611,12 +1607,12 @@ def unbias_dungeons(Dungeon_Exits): def build_accessible_entrance_list(world, start_region, player, assumed_inventory=[], cross_world=False, region_rules=True, exit_rules=True, include_one_ways=False): - from Main import copy_world + from Main import copy_world_limited from Items import ItemFactory for p in range(1, world.players + 1): world.key_logic[p] = {} - base_world = copy_world(world, True) + base_world = copy_world_limited(world) base_world.override_bomb_check = True connect_simple(base_world, 'Links House S&Q', start_region, player) @@ -1719,13 +1715,12 @@ def get_distant_entrances(world, start_entrance, player): def can_reach(world, entrance_name, region_name, player): - from Main import copy_world + from Main import copy_world_limited from Items import ItemFactory - from DoorShuffle import find_inaccessible_regions for p in range(1, world.players + 1): world.key_logic[p] = {} - base_world = copy_world(world, True) + base_world = copy_world_limited(world) base_world.override_bomb_check = True entrance = world.get_entrance(entrance_name, player) diff --git a/Fill.py b/Fill.py index daa6f6bd..1b6d5b75 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,32 @@ 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) + unplaced_smalls = list(smalls) + for i in world.itempool: + if i.smallkey and world.keyshuffle[i.player]: + unplaced_smalls.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, unplaced_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, unplaced_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 +106,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 +115,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 +127,9 @@ 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) + 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 +139,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 +148,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 +156,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 +167,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 +222,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 +241,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 +250,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 +265,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 +290,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) @@ -420,13 +426,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) @@ -750,12 +756,14 @@ def balance_multiworld_progression(world): raise RuntimeError('Not all required items reachable. Something went terribly wrong here.') -def check_shop_swap(l): +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] @@ -921,12 +929,13 @@ def balance_money_progression(world): 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) - old_value = rupee_chart[best_target.item.name] if best_target.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) + 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}') @@ -934,7 +943,7 @@ def balance_money_progression(world): best_target.item.location = best_target best_swap.item = old_item best_swap.item.location = best_swap - check_shop_swap(best_target.item.location) + check_shop_swap(best_target.item.location, make_item_free) check_shop_swap(best_swap.item.location) increase = best_value - old_value difference -= increase diff --git a/ItemList.py b/ItemList.py index fdcf5412..5c4aa40d 100644 --- a/ItemList.py +++ b/ItemList.py @@ -1155,27 +1155,3 @@ def test(): if __name__ == '__main__': test() - - -def fill_specific_items(world): - keypool = [item for item in world.itempool if item.smallkey] - cage = world.get_location('Tower of Hera - Basement Cage', 1) - c_dungeon = cage.parent_region.dungeon - key_item = next(x for x in keypool if c_dungeon.name in x.name or (c_dungeon.name == 'Hyrule Castle' and 'Escape' in x.name)) - world.itempool.remove(key_item) - all_state = world.get_all_state(True) - fill_restrictive(world, all_state, [cage], [key_item]) - - location = world.get_location('Tower of Hera - Map Chest', 1) - key_item = next(x for x in world.itempool if 'Byrna' in x.name) - world.itempool.remove(key_item) - fast_fill(world, [key_item], [location]) - - - # somaria = next(item for item in world.itempool if item.name == 'Cane of Somaria') - # shooter = world.get_location('Palace of Darkness - Shooter Room', 1) - # world.itempool.remove(somaria) - # all_state = world.get_all_state(True) - # fill_restrictive(world, all_state, [shooter], [somaria]) - - diff --git a/Main.py b/Main.py index 6241a830..2483bb62 100644 --- a/Main.py +++ b/Main.py @@ -32,7 +32,7 @@ from Utils import output_path, parse_player_names from source.item.FillUtil import create_item_pool_config, massage_item_pool, district_item_pool_config from source.tools.BPS import create_bps_from_data -__version__ = '1.0.1.0-u' +__version__ = '1.0.1.2-u' from source.classes.BabelFish import BabelFish @@ -396,7 +396,7 @@ def main(args, seed=None, fish=None): return world -def copy_world(world, partial_copy=False): +def copy_world(world): # ToDo: Not good yet ret = World(world.players, world.owShuffle, world.owCrossed, world.owMixed, world.shuffle, world.doorShuffle, world.logic, world.mode, world.swords, world.difficulty, world.difficulty_adjustments, world.timer, world.progressive, world.goal, world.algorithm, @@ -543,10 +543,9 @@ def copy_world(world, partial_copy=False): ret.dungeon_layouts = world.dungeon_layouts ret.key_logic = world.key_logic ret.dungeon_portals = world.dungeon_portals - if not partial_copy: - for player, portals in world.dungeon_portals.items(): - for portal in portals: - connect_portal(portal, ret, player) + for player, portals in world.dungeon_portals.items(): + for portal in portals: + connect_portal(portal, ret, player) ret.sanc_portal = world.sanc_portal from OverworldShuffle import categorize_world_regions @@ -554,9 +553,127 @@ def copy_world(world, partial_copy=False): categorize_world_regions(ret, player) set_rules(ret, player) - if partial_copy: - # undo some of the things that unintentionally affect the original world object - world.key_logic = {} + return ret + + +def copy_world_limited(world): + # ToDo: Not good yet + ret = World(world.players, world.owShuffle, world.owCrossed, world.owMixed, world.shuffle, world.doorShuffle, world.logic, world.mode, world.swords, + world.difficulty, world.difficulty_adjustments, world.timer, world.progressive, world.goal, world.algorithm, + world.accessibility, world.shuffle_ganon, world.retro, world.custom, world.customitemarray, world.hints) + ret.teams = world.teams + ret.player_names = copy.deepcopy(world.player_names) + ret.remote_items = world.remote_items.copy() + ret.required_medallions = world.required_medallions.copy() + ret.bottle_refills = world.bottle_refills.copy() + ret.swamp_patch_required = world.swamp_patch_required.copy() + ret.ganon_at_pyramid = world.ganon_at_pyramid.copy() + ret.powder_patch_required = world.powder_patch_required.copy() + ret.ganonstower_vanilla = world.ganonstower_vanilla.copy() + ret.treasure_hunt_count = world.treasure_hunt_count.copy() + ret.treasure_hunt_icon = world.treasure_hunt_icon.copy() + ret.sewer_light_cone = world.sewer_light_cone.copy() + ret.light_world_light_cone = world.light_world_light_cone + ret.dark_world_light_cone = world.dark_world_light_cone + ret.seed = world.seed + ret.can_access_trock_eyebridge = world.can_access_trock_eyebridge.copy() + ret.can_access_trock_front = world.can_access_trock_front.copy() + ret.can_access_trock_big_chest = world.can_access_trock_big_chest.copy() + ret.can_access_trock_middle = world.can_access_trock_middle.copy() + ret.can_take_damage = world.can_take_damage + ret.difficulty_requirements = world.difficulty_requirements.copy() + ret.fix_fake_world = world.fix_fake_world.copy() + ret.lamps_needed_for_dark_rooms = world.lamps_needed_for_dark_rooms + ret.mapshuffle = world.mapshuffle.copy() + ret.compassshuffle = world.compassshuffle.copy() + ret.keyshuffle = world.keyshuffle.copy() + ret.bigkeyshuffle = world.bigkeyshuffle.copy() + ret.bombbag = world.bombbag.copy() + ret.crystals_needed_for_ganon = world.crystals_needed_for_ganon.copy() + ret.crystals_needed_for_gt = world.crystals_needed_for_gt.copy() + ret.crystals_ganon_orig = world.crystals_ganon_orig.copy() + ret.crystals_gt_orig = world.crystals_gt_orig.copy() + ret.owKeepSimilar = world.owKeepSimilar.copy() + ret.owWhirlpoolShuffle = world.owWhirlpoolShuffle.copy() + ret.owFluteShuffle = world.owFluteShuffle.copy() + ret.shuffle_bonk_drops = world.shuffle_bonk_drops.copy() + ret.open_pyramid = world.open_pyramid.copy() + ret.boss_shuffle = world.boss_shuffle.copy() + ret.enemy_shuffle = world.enemy_shuffle.copy() + ret.enemy_health = world.enemy_health.copy() + ret.enemy_damage = world.enemy_damage.copy() + ret.beemizer = world.beemizer.copy() + ret.intensity = world.intensity.copy() + ret.experimental = world.experimental.copy() + ret.shopsanity = world.shopsanity.copy() + ret.dropshuffle = world.dropshuffle.copy() + ret.pottery = world.pottery.copy() + ret.potshuffle = world.potshuffle.copy() + ret.mixed_travel = world.mixed_travel.copy() + ret.standardize_palettes = world.standardize_palettes.copy() + ret.owswaps = world.owswaps.copy() + ret.owflutespots = world.owflutespots.copy() + ret.prizes = world.prizes.copy() + ret.restrict_boss_items = world.restrict_boss_items.copy() + + ret.is_copied_world = True + + for player in range(1, world.players + 1): + create_regions(ret, player) + update_world_regions(ret, player) + if world.logic[player] in ('owglitches', 'nologic'): + create_owg_connections(ret, player) + create_flute_exits(ret, player) + create_dungeon_regions(ret, player) + create_owedges(ret, player) + create_shops(ret, player) + create_doors(ret, player) + create_rooms(ret, player) + create_dungeons(ret, player) + + for player in range(1, world.players + 1): + if world.mode[player] == 'standard': + parent = ret.get_region('Menu', player) + target = ret.get_region('Hyrule Castle Secret Entrance', player) + connection = Entrance(player, 'Uncle S&Q', parent) + parent.exits.append(connection) + connection.connect(target) + + # connect copied world + copied_locations = {(loc.name, loc.player): loc for loc in ret.get_locations()} # caches all locations + for region in world.regions: + copied_region = ret.get_region(region.name, region.player) + copied_region.is_light_world = region.is_light_world + copied_region.is_dark_world = region.is_dark_world + copied_region.dungeon = region.dungeon + copied_region.locations = [copied_locations[(location.name, location.player)] for location in region.locations if (location.name, location.player) in copied_locations] + for location in copied_region.locations: + location.parent_region = copied_region + for entrance in region.entrances: + ret.get_entrance(entrance.name, entrance.player).connect(copied_region) + + for item in world.precollected_items: + ret.push_precollected(ItemFactory(item.name, item.player)) + + for edge in world.owedges: + copiededge = ret.check_for_owedge(edge.name, edge.player) + if copiededge is not None: + copiededge.dest = ret.check_for_owedge(edge.dest.name, edge.dest.player) + + for door in world.doors: + entrance = ret.check_for_entrance(door.name, door.player) + if entrance is not None: + destdoor = ret.check_for_door(entrance.door.name, entrance.door.player) + entrance.door = destdoor + if destdoor is not None: + destdoor.entrance = entrance + + ret.key_logic = world.key_logic.copy() + + from OverworldShuffle import categorize_world_regions + for player in range(1, world.players + 1): + categorize_world_regions(ret, player) + set_rules(ret, player) return ret @@ -578,11 +695,7 @@ def copy_dynamic_regions_and_locations(world, ret): for location in world.dynamic_locations: new_reg = ret.get_region(location.parent_region.name, location.parent_region.player) new_loc = Location(location.player, location.name, location.address, location.crystal, location.hint_text, new_reg) - # todo: this is potentially dangerous. later refactor so we - # can apply dynamic region rules on top of copied world like other rules - new_loc.access_rule = location.access_rule - new_loc.always_allow = location.always_allow - new_loc.item_rule = location.item_rule + new_loc.type = location.type new_reg.locations.append(new_loc) ret.clear_location_cache() diff --git a/Mystery.py b/Mystery.py index e1be0eb7..9bd1235a 100644 --- a/Mystery.py +++ b/Mystery.py @@ -1,5 +1,7 @@ import argparse import logging +from pathlib import Path +import os import RaceRandom as random import urllib.request import urllib.parse @@ -106,13 +108,11 @@ def main(): DRMain(erargs, seed, BabelFish()) def get_weights(path): - try: - if urllib.parse.urlparse(path).scheme: - return yaml.load(urllib.request.urlopen(path), Loader=yaml.FullLoader) - with open(path, 'r', encoding='utf-8') as f: + if os.path.exists(Path(path)): + with open(path, "r", encoding="utf-8") as f: return yaml.load(f, Loader=yaml.SafeLoader) - except Exception as e: - raise Exception(f'Failed to read weights file: {e}') + elif urllib.parse.urlparse(path).scheme in ['http', 'https']: + return yaml.load(urllib.request.urlopen(path), Loader=yaml.FullLoader) def roll_settings(weights): def get_choice(option, root=None): diff --git a/OverworldShuffle.py b/OverworldShuffle.py index 994fec9a..deb428a3 100644 --- a/OverworldShuffle.py +++ b/OverworldShuffle.py @@ -6,7 +6,7 @@ from Regions import mark_dark_world_regions, mark_light_world_regions from OWEdges import OWTileRegions, OWEdgeGroups, OWExitTypes, OpenStd, parallel_links, IsParallel from Utils import bidict -version_number = '0.2.9.1' +version_number = '0.2.10.0' # branch indicator is intentionally different across branches version_branch = '' @@ -289,6 +289,8 @@ def link_overworld(world, player): for whirlpools in whirlpool_candidates: random.shuffle(whirlpools) while len(whirlpools): + if len(whirlpools) % 2 == 1: + x=0 from_owid, from_whirlpool, from_region = whirlpools.pop() to_owid, to_whirlpool, to_region = whirlpools.pop() connect_simple(world, from_whirlpool, to_region, player) @@ -329,7 +331,7 @@ def link_overworld(world, player): # layout shuffle groups = adjust_edge_groups(world, trimmed_groups, edges_to_swap, player) - tries = 20 + tries = 100 valid_layout = False connected_edge_cache = connected_edges.copy() while not valid_layout and tries > 0: @@ -423,10 +425,15 @@ def link_overworld(world, player): if not ignore_proximity and random.randint(0, 31) != 0 and new_ignored.intersection(ignored_regions): return False ignored_regions.update(new_ignored) - flute_pool.remove(owid) - if ignore_proximity: - logging.getLogger('').warning(f'Warning: Adding flute spot within proximity: {hex(owid)}') - new_spots.append(owid) + if owid in flute_pool: + flute_pool.remove(owid) + if ignore_proximity: + logging.getLogger('').warning(f'Warning: Adding flute spot within proximity: {hex(owid)}') + logging.getLogger('').debug(f'Placing flute at: {hex(owid)}') + new_spots.append(owid) + else: + # TODO: Inspect later, seems to happen only with 'random' flute shuffle + logging.getLogger('').warning(f'Warning: Attempted to place flute spot not in pool: {hex(owid)}') return True # determine sectors (isolated groups of regions) to place flute spots @@ -442,6 +449,7 @@ def link_overworld(world, player): sector_total -= 1 spots_to_place = min(flute_spots - sector_total, max(1, round((sector[0] * (flute_spots - sector_total) / region_total) + 0.5))) target_spots = len(new_spots) + spots_to_place + logging.getLogger('').debug(f'Sector of {sector[0]} regions gets {spots_to_place} spot(s)') if 'Desert Palace Teleporter Ledge' in sector[1] or 'Misery Mire Teleporter Ledge' in sector[1]: addSpot(0x38, False) # guarantee desert/mire access @@ -553,8 +561,8 @@ def shuffle_tiles(world, groups, result_list, do_grouped, player): group_parity = {} for group_data in groups: group = group_data[0] - parity = [0, 0, 0, 0, 0] - # vertical land + parity = [0, 0, 0, 0, 0, 0] + # 0: vertical if 0x00 in group: parity[0] += 1 if 0x0f in group: @@ -563,40 +571,46 @@ def shuffle_tiles(world, groups, result_list, do_grouped, player): parity[0] -= 1 if 0x81 in group: parity[0] -= 1 - # horizontal land + # 1: horizontal land single if 0x1a in group: parity[1] -= 1 if 0x1b in group: parity[1] += 1 if 0x28 in group: - parity[1] += 1 - if 0x29 in group: parity[1] -= 1 - if 0x30 in group: - parity[1] -= 2 - if 0x3a in group: - parity[1] += 2 - # horizontal water - if 0x2d in group: + if 0x29 in group: + parity[1] += 1 + # 2: horizontal land double + if 0x28 in group: parity[2] += 1 - if 0x80 in group: + if 0x29 in group: parity[2] -= 1 - # whirlpool + if 0x30 in group: + parity[2] -= 1 + if 0x3a in group: + parity[2] += 1 + # 3: horizontal water + if 0x2d in group: + parity[3] += 1 + if 0x80 in group: + parity[3] -= 1 + # 4: whirlpool if 0x0f in group: - parity[3] += 1 + parity[4] += 1 if 0x12 in group: - parity[3] += 1 + parity[4] += 1 if 0x33 in group: - parity[3] += 1 + parity[4] += 1 if 0x35 in group: - parity[3] += 1 - # dropdown exit - if 0x00 in group or 0x02 in group or 0x13 in group or 0x15 in group or 0x18 in group or 0x22 in group: parity[4] += 1 + # 5: dropdown exit + for id in [0x00, 0x02, 0x13, 0x15, 0x18, 0x22]: + if id in group: + parity[5] += 1 if 0x1b in group and world.mode[player] != 'standard': - parity[4] += 1 + parity[5] += 1 if 0x1b in group and world.shuffle_ganon: - parity[4] -= 1 + parity[5] -= 1 group_parity[group[0]] = parity attempts = 1000 @@ -607,7 +621,7 @@ def shuffle_tiles(world, groups, result_list, do_grouped, player): # tile shuffle happens here removed = list() for group in groups: - # if 0x1b in group[0] or (0x1a in group[0] and world.owCrossed[player] == 'none'): # TODO: Standard + Inverted + #if 0x1b in group[0] or 0x13 in group[0] or (0x1a in group[0] and world.owCrossed[player] == 'none'): # TODO: Standard + Inverted if random.randint(0, 1): removed.append(group) @@ -621,18 +635,24 @@ def shuffle_tiles(world, groups, result_list, do_grouped, player): exist_lw_regions.extend(lw_regions) exist_dw_regions.extend(dw_regions) - parity = [sum(group_parity[group[0][0]][i] for group in groups if group not in removed) for i in range(5)] - parity[3] %= 2 # actual parity - if (world.owCrossed[player] == 'none' or do_grouped) and parity[:4] != [0, 0, 0, 0]: + parity = [sum(group_parity[group[0][0]][i] for group in groups if group not in removed) for i in range(6)] + if not world.owKeepSimilar[player]: + parity[1] += 2*parity[2] + parity[2] = 0 + # if crossed terrain: + # parity[1] += parity[3] + # parity[3] = 0 + parity[4] %= 2 # actual parity + if (world.owCrossed[player] == 'none' or do_grouped) and parity[:5] != [0, 0, 0, 0, 0]: attempts -= 1 continue # ensure sanc can be placed in LW in certain modes if not do_grouped and world.shuffle[player] not in ['vanilla', 'dungeonssimple', 'dungeonsfull', 'lean', 'crossed', 'insanity'] and world.mode[player] != 'inverted' and (world.doorShuffle[player] != 'crossed' or world.intensity[player] < 3 or world.mode[player] == 'standard'): - free_dw_drops = parity[4] + (1 if world.shuffle_ganon else 0) + free_dw_drops = parity[5] + (1 if world.shuffle_ganon else 0) free_drops = 6 + (1 if world.mode[player] != 'standard' else 0) + (1 if world.shuffle_ganon else 0) if free_dw_drops == free_drops: - attempts -= 1 - continue + attempts -= 1 + continue break (exist_owids, exist_lw_regions, exist_dw_regions) = result_list @@ -681,7 +701,7 @@ def define_tile_groups(world, player, do_grouped): # sanctuary/chapel should not be swapped if S+Q guaranteed to output on that screen if 0x13 in group and ((world.shuffle[player] in ['vanilla', 'dungeonssimple', 'dungeonsfull'] \ and (world.mode[player] in ['standard', 'inverted'] or world.doorShuffle[player] != 'crossed' or world.intensity[player] < 3)) \ - or (world.shuffle[player] == 'lite' and world.mode[player] == 'inverted')): + or (world.shuffle[player] in ['lite', 'lean'] and world.mode[player] == 'inverted')): return False return True @@ -708,7 +728,7 @@ def define_tile_groups(world, player, do_grouped): if world.owShuffle[player] == 'vanilla' and (world.owCrossed[player] == 'none' or do_grouped): merge_groups([[0x00, 0x2d, 0x80], [0x0f, 0x81], [0x1a, 0x1b], [0x28, 0x29], [0x30, 0x3a]]) - if world.owShuffle[player] == 'parallel' and world.owKeepSimilar[player] and world.owCrossed[player] == 'none': + if world.owShuffle[player] == 'parallel' and world.owKeepSimilar[player] and (world.owCrossed[player] == 'none' or do_grouped): merge_groups([[0x28, 0x29]]) if not world.owWhirlpoolShuffle[player] and (world.owCrossed[player] == 'none' or do_grouped): @@ -878,13 +898,13 @@ def can_reach_smith(world, player): return found def build_sectors(world, player): - from Main import copy_world + from Main import copy_world_limited from OWEdges import OWTileRegions # perform accessibility check on duplicate world for p in range(1, world.players + 1): world.key_logic[p] = {} - base_world = copy_world(world, True) + base_world = copy_world_limited(world) # build lists of contiguous regions accessible with full inventory (excl portals/mirror/flute/entrances) regions = list(OWTileRegions.copy().keys()) @@ -928,11 +948,23 @@ def build_sectors(world, player): sectors2.append(explored_regions) sectors[s] = sectors2 + #TODO: Keep largest LW sector for Links House consideration, keep sector containing WDM for Old Man consideration + # sector_entrances = list() + # for sector in sectors: + # entrances = list() + # for s2 in sector: + # for region_name in s2: + # region = world.get_region(region_name, player) + # for exit in region.exits: + # if exit.spot_type == 'Entrance' and exit.name in entrance_pool: + # entrances.append(exit.name) + # sector_entrances.append(entrances) + return sectors def build_accessible_region_list(world, start_region, player, build_copy_world=False, cross_world=False, region_rules=True, ignore_ledges = False): - from Main import copy_world from BaseClasses import CollectionState + from Main import copy_world_limited from Items import ItemFactory from Utils import stack_size3a @@ -959,7 +991,7 @@ def build_accessible_region_list(world, start_region, player, build_copy_world=F if build_copy_world: for p in range(1, world.players + 1): world.key_logic[p] = {} - base_world = copy_world(world, True) + base_world = copy_world_limited(world) base_world.override_bomb_check = True else: base_world = world @@ -1015,7 +1047,7 @@ def validate_layout(world, player): entrance_connectors['Bumper Cave Entrance'] = ['West Dark Death Mountain (Bottom)'] entrance_connectors['Mountain Entry Entrance'] = ['Mountain Entry Ledge'] - from Main import copy_world + from Main import copy_world_limited from Utils import stack_size3a from EntranceShuffle import default_dungeon_connections, default_connector_connections, default_item_connections, default_shop_connections, default_drop_connections, default_dropexit_connections @@ -1048,7 +1080,7 @@ def validate_layout(world, player): for p in range(1, world.players + 1): world.key_logic[p] = {} - base_world = copy_world(world, True) + base_world = copy_world_limited(world) explored_regions = list() if world.shuffle[player] in ['vanilla', 'dungeonssimple', 'dungeonsfull'] or not world.shufflelinks[player]: diff --git a/PotShuffle.py b/PotShuffle.py index a1c96783..7806f495 100644 --- a/PotShuffle.py +++ b/PotShuffle.py @@ -787,12 +787,13 @@ vanilla_pots = { Pot(230, 27, PotItem.Bomb, 'Light World Bomb Hut', obj=RoomObject(0x03EF5E, [0xCF, 0xDF, 0xFA]))], 0x108: [Pot(166, 19, PotItem.Chicken, 'Chicken House', obj=RoomObject(0x03EFA9, [0x4F, 0x9F, 0xFA]))], 0x10C: [Pot(88, 14, PotItem.Heart, 'Hookshot Fairy', obj=RoomObject(0x03F329, [0xB3, 0x73, 0xFA]))], - 0x114: [Pot(92, 4, PotItem.Heart, 'Dark Desert Hint', obj=RoomObject(0x03F7A0, [0xBB, 0x23, 0xFA])), - Pot(96, 4, PotItem.Heart, 'Dark Desert Hint', obj=RoomObject(0x03F7A3, [0xC3, 0x23, 0xFA])), - Pot(92, 5, PotItem.Bomb, 'Dark Desert Hint', obj=RoomObject(0x03F7A6, [0xBB, 0x2B, 0xFA])), - Pot(96, 5, PotItem.Bomb, 'Dark Desert Hint', obj=RoomObject(0x03F7A9, [0xC3, 0x2B, 0xFA])), - Pot(92, 10, PotItem.FiveArrows, 'Dark Desert Hint', obj=RoomObject(0x03F7AC, [0xBB, 0x53, 0xFA])), - Pot(96, 10, PotItem.Heart, 'Dark Desert Hint', obj=RoomObject(0x03F7AF, [0xC3, 0x53, 0xFA]))], + # note: these addresses got moved thanks to waterfall fairy edit + 0x114: [Pot(92, 4, PotItem.Heart, 'Dark Desert Hint', obj=RoomObject(0x03F79A, [0xBB, 0x23, 0xFA])), + Pot(96, 4, PotItem.Heart, 'Dark Desert Hint', obj=RoomObject(0x03F79D, [0xC3, 0x23, 0xFA])), + Pot(92, 5, PotItem.Bomb, 'Dark Desert Hint', obj=RoomObject(0x03F7A0, [0xBB, 0x2B, 0xFA])), + Pot(96, 5, PotItem.Bomb, 'Dark Desert Hint', obj=RoomObject(0x03F7A3, [0xC3, 0x2B, 0xFA])), + Pot(92, 10, PotItem.FiveArrows, 'Dark Desert Hint', obj=RoomObject(0x03F7A6, [0xBB, 0x53, 0xFA])), + Pot(96, 10, PotItem.Heart, 'Dark Desert Hint', obj=RoomObject(0x03F7A9, [0xC3, 0x53, 0xFA]))], 0x117: [Pot(138, 3, PotItem.Heart, 'Spike Cave', obj=RoomObject(0x03FCB2, [0x17, 0x1F, 0xFA])), # 0x38A -> 38A Pot(142, 3, PotItem.Heart, 'Spike Cave', obj=RoomObject(0x03FCB8, [0x1F, 0x1F, 0xFA])), Pot(166, 3, PotItem.Heart, 'Spike Cave', obj=RoomObject(0x03FCC1, [0x4F, 0x1F, 0xFA])), diff --git a/README.md b/README.md index c6e5187c..be396995 100644 --- a/README.md +++ b/README.md @@ -68,8 +68,6 @@ OW Transitions are shuffled within each world separately. This allows OW connections to be shuffled cross-world. -'None (Allowed)' allows entrance connectors and whirlpools to result in cross-world behavior, but edge transitions will not. This isn't a recommended option. - Polar and Grouped both are guaranteed to result in two separated planes of tiles. To navigate to the other plane, you have the following methods: 1) Normal portals 2) Mirroring on DW tiles 3) Fluting to a LW tile that was previously unreachable Limited and Chaos are not bound to follow a two-plane framework. This means that it could be possible to travel on foot to every tile without entering a normal portal. diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 65293af1..1e059997 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -183,6 +183,21 @@ Same as above but both small keys and bigs keys of the dungeon are not allowed o #### Unstable +* 1.0.1.2 + * Removed "good bee" as an in-logic way of killing Mothula + * Fixed an issue with Mystery generation and Windows path + * Fixed an issue with small key bias rework + * Fixed an issue where trinity goal would open pyramid unexpectedly. (No longer does so if ER mdoe is shuffling holes). Crystals goal updated to match that behavior. + * Fixed a playthrough issue that was not respecting pot rules + * Fixed an issue that was conflicting with downstream OWR project + * Fixed an issue with inverted and certain pottery settings + * Fixed an issue with small keys being shuffled and big keys not (key distribution) +* 1.0.1.1 + * 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 @@ -206,7 +221,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 @@ -235,7 +249,7 @@ Same as above but both small keys and bigs keys of the dungeon are not allowed o * Fixed a bug with shopsanity + district algorithm where pre-placed potions messed up the placeholder count * Fixed usestartinventory flag (can be use on a per player basis) * Sprite selector fix for systems with SSL issues - * Fix for Standard ER where locations in rain state could be in logic + * Fix for Standard ER where locations in rain state could be in logic * 1.0.0.3 * overworld_map=map mode fixed. Location of dungeons with maps are not shown until map is retrieved. (Dungeon that do not have map like Castle Tower are simply never shown) * Aga2 completion on overworld_map now tied to boss defeat flag instead of pyramid hole being opened (fast ganon fix) diff --git a/Rom.py b/Rom.py index 63b5577c..598df466 100644 --- a/Rom.py +++ b/Rom.py @@ -38,7 +38,7 @@ from source.dungeon.RoomList import Room0127 JAP10HASH = '03a63945398191337e896e5771f77173' -RANDOMIZERBASEHASH = '92a390672efafb652774c1514ac66c4b' +RANDOMIZERBASEHASH = '831beb6f60c3c99467552493b3ce6f19' class JsonRom(object): @@ -672,18 +672,6 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False): if world.mapshuffle[player]: rom.write_byte(0x155C9, random.choice([0x11, 0x16])) # Randomize GT music too with map shuffle - if world.pottery[player] not in ['none']: - rom.write_bytes(snes_to_pc(0x1F8375), int32_as_bytes(0x2A8000)) - # make hammer pegs use different tiles - Room0127.write_to_rom(snes_to_pc(0x2A8000), rom) - - if world.pot_contents[player]: - colorize_pots = is_mystery or (world.pottery[player] not in ['vanilla', 'lottery'] - and (world.colorizepots[player] - or world.pottery[player] in ['reduced', 'clustered'])) - if world.pot_contents[player].size() > 0x2800: - raise Exception('Pot table is too big for current area') - world.pot_contents[player].write_pot_data_to_rom(rom, colorize_pots) # fix for swamp drains if necessary swamp1location = world.get_location('Swamp Palace - Trench 1 Pot Key', player) if not swamp1location.pot.indicator: @@ -1496,8 +1484,7 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False): elif (world.compassshuffle[player] or world.doorShuffle[player] != 'vanilla' or world.dropshuffle[player] or world.dungeon_counters[player] == 'pickup' or world.pottery[player] not in ['none', 'cave']): compass_mode = 0x01 # show on pickup - if (world.shuffle[player] != 'vanilla' and world.overworld_map[player] != 'default') \ - or (world.owMixed[player] and not (world.shuffle[player] != 'vanilla' and world.overworld_map[player] == 'default')): + if (world.shuffle[player] != 'vanilla' and world.overworld_map[player] != 'default') or world.owMixed[player]: compass_mode |= 0x80 # turn on locating dungeons if world.overworld_map[player] == 'compass': compass_mode |= 0x20 # show icon if compass is collected, 0x00 for maps @@ -1512,39 +1499,49 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False): for idx, x_map in enumerate(x_map_position_generic): rom.write_bytes(0x53df6+idx*2, int16_as_bytes(x_map)) rom.write_bytes(0x53e16+idx*2, int16_as_bytes(0xFC0)) - elif world.shuffle[player] == 'vanilla': + elif world.overworld_map[player] == 'default': # disable HC/AT/GT icons - # rom.write_bytes(0x53E8A, int16_as_bytes(0xFF00)) # GT - # rom.write_bytes(0x53E8C, int16_as_bytes(0xFF00)) # AT + if not world.owMixed[player]: + rom.write_bytes(0x53E8A, int16_as_bytes(0xFF00)) # GT + rom.write_bytes(0x53E8C, int16_as_bytes(0xFF00)) # AT rom.write_bytes(0x53E8E, int16_as_bytes(0xFF00)) # HC for dungeon, portal_list in dungeon_portals.items(): ow_map_index = dungeon_table[dungeon].map_index - if world.shuffle[player] != 'vanilla' and world.overworld_map[player] != 'default': - if len(portal_list) == 1: - portal_idx = 0 - else: - if world.doorShuffle[player] == 'crossed': - # the random choice excludes sanctuary - portal_idx = next((i for i, elem in enumerate(portal_list) - if world.get_portal(elem, player).chosen), random.choice([1, 2, 3])) - else: - portal_idx = {'Hyrule Castle': 0, 'Desert Palace': 0, 'Skull Woods': 3, 'Turtle Rock': 3}[dungeon] + if world.shuffle[player] != 'vanilla' and world.overworld_map[player] == 'default': + vanilla_entrances = { 'Hyrule Castle': 'Hyrule Castle Entrance (South)', + 'Desert Palace': 'Desert Palace Entrance (North)', + 'Skull Woods': 'Skull Woods Final Section' + } + entrance_name = vanilla_entrances[dungeon] if dungeon in vanilla_entrances else dungeon + entrance = world.get_entrance(entrance_name, player) else: - if dungeon in ['Hyrule Castle', 'Agahnims Tower', 'Ganons Tower']: - portal_idx = -1 - elif len(portal_list) == 1: - portal_idx = 0 + if world.shuffle[player] != 'vanilla': + if len(portal_list) == 1: + portal_idx = 0 + else: + if world.doorShuffle[player] == 'crossed': + # the random choice excludes sanctuary + portal_idx = next((i for i, elem in enumerate(portal_list) + if world.get_portal(elem, player).chosen), random.choice([1, 2, 3])) + else: + portal_idx = {'Hyrule Castle': 0, 'Desert Palace': 0, 'Skull Woods': 3, 'Turtle Rock': 3}[dungeon] else: - portal_idx = {'Desert Palace': 1, 'Skull Woods': 3, 'Turtle Rock': 0}[dungeon] - portal = world.get_portal(portal_list[0 if portal_idx == -1 else portal_idx], player) - entrance = portal.find_portal_entrance() + if dungeon in ['Hyrule Castle', 'Agahnims Tower', 'Ganons Tower']: + portal_idx = -1 + elif len(portal_list) == 1: + portal_idx = 0 + else: + portal_idx = {'Desert Palace': 1, 'Skull Woods': 3, 'Turtle Rock': 0}[dungeon] + portal = world.get_portal(portal_list[0 if portal_idx == -1 else portal_idx], player) + entrance = portal.find_portal_entrance() world_indicator = 0x01 if entrance.parent_region.type == RegionType.DarkWorld else 0x00 coords = ow_prize_table[entrance.name] # figure out compass entrances and what world (light/dark) - if world.shuffle[player] == 'vanilla' or world.overworld_map[player] != 'default': + if world.overworld_map[player] != 'default' or world.owMixed[player]: rom.write_bytes(0x53E36+ow_map_index*2, int16_as_bytes(coords[0])) rom.write_bytes(0x53E56+ow_map_index*2, int16_as_bytes(coords[1])) rom.write_byte(0x53EA6+ow_map_index, world_indicator) + # in crossed doors - flip the compass exists flags if world.doorShuffle[player] == 'crossed': for dungeon, portal_list in dungeon_portals.items(): @@ -1706,6 +1703,19 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False): if room.player == player and room.modified: rom.write_bytes(room.address(), room.rom_data()) + if world.pottery[player] not in ['none']: + rom.write_bytes(snes_to_pc(0x1F8375), int32_as_bytes(0x2B8000)) + # make hammer pegs use different tiles + Room0127.write_to_rom(snes_to_pc(0x2B8000), rom) + + if world.pot_contents[player]: + colorize_pots = is_mystery or (world.pottery[player] not in ['vanilla', 'lottery'] + and (world.colorizepots[player] + or world.pottery[player] in ['reduced', 'clustered'])) + if world.pot_contents[player].size() > 0x2800: + raise Exception('Pot table is too big for current area') + world.pot_contents[player].write_pot_data_to_rom(rom, colorize_pots) + write_strings(rom, world, player, team) # write initial sram @@ -2137,8 +2147,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: @@ -2325,8 +2333,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 @@ -2380,8 +2387,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/Rules.py b/Rules.py index 7e020507..6b8812fd 100644 --- a/Rules.py +++ b/Rules.py @@ -21,12 +21,12 @@ def set_rules(world, player): global_rules(world, player) default_rules(world, player) - ow_rules(world, player) + ow_inverted_rules(world, player) ow_bunny_rules(world, player) if world.mode[player] == 'standard': - if world.get_region('Big Bomb Shop', player).entrances: # just some location that is placed late in the ER algorithm, prevent standard rules from applying when trying to search reachability in the overworld + if not world.is_copied_world: standard_rules(world, player) elif world.mode[player] == 'open' or world.mode[player] == 'inverted': open_rules(world, player) @@ -356,8 +356,10 @@ def global_rules(world, player): # byrna could work with sufficient magic set_rule(world.get_location('Misery Mire - Spike Chest', player), lambda state: (state.world.can_take_damage and state.has_hearts(player, 4)) or state.has('Cane of Byrna', player) or state.has('Cape', player)) loc = world.get_location('Misery Mire - Spikes Pot Key', player) - if loc.pot is not None and loc.pot.x == 48 and loc.pot.y == 28: # pot shuffled to spike area - set_rule(loc, lambda state: (state.world.can_take_damage and state.has_hearts(player, 4)) or state.has('Cane of Byrna', player) or state.has('Cape', player)) + if loc.pot: + if loc.pot.x == 48 and loc.pot.y == 28: # pot shuffled to spike area + set_rule(loc, lambda state: (state.world.can_take_damage and state.has_hearts(player, 4)) + or state.has('Cane of Byrna', player) or state.has('Cape', player)) set_rule(world.get_entrance('Mire Left Bridge Hook Path', player), lambda state: state.has('Hookshot', player)) set_rule(world.get_entrance('Mire Tile Room NW', player), lambda state: state.has_fire_source(player)) set_rule(world.get_entrance('Mire Attic Hint Hole', player), lambda state: state.has_fire_source(player)) @@ -805,7 +807,6 @@ def pot_rules(world, player): add_rule(l, lambda state: state.can_hit_crystal(player)) - def default_rules(world, player): set_rule(world.get_entrance('Other World S&Q', player), lambda state: state.has_Mirror(player) and state.has_beaten_aga(player)) @@ -825,7 +826,7 @@ def default_rules(world, player): # Bonk Item Access if world.shuffle_bonk_drops[player]: - if world.get_region('Big Bomb Shop', player).entrances: # just some location that is placed late in the ER algorithm, prevent standard rules from applying when trying to search reachability in the overworld + if not world.is_copied_world: from Regions import bonk_prize_table for location_name, (_, _, aga_required, _, _, _) in bonk_prize_table.items(): loc = world.get_location(location_name, player) @@ -873,8 +874,6 @@ def default_rules(world, player): set_rule(world.get_entrance('Potion Shop Rock (North)', player), lambda state: state.can_lift_rocks(player)) set_rule(world.get_entrance('Zora Approach Rocks (West)', player), lambda state: state.can_lift_heavy_rocks(player) or state.has_Boots(player)) set_rule(world.get_entrance('Zora Approach Rocks (East)', player), lambda state: state.can_lift_heavy_rocks(player) or state.has_Boots(player)) - set_rule(world.get_entrance('Hyrule Castle Main Gate (South)', player), lambda state: state.has_Mirror(player)) - set_rule(world.get_entrance('Hyrule Castle Main Gate (North)', player), lambda state: state.has_Mirror(player)) set_rule(world.get_entrance('Hyrule Castle Inner East Rock', player), lambda state: state.can_lift_rocks(player)) set_rule(world.get_entrance('Hyrule Castle Outer East Rock', player), lambda state: state.can_lift_rocks(player)) set_rule(world.get_entrance('Bat Cave Ledge Peg', player), lambda state: state.has('Hammer', player)) @@ -958,7 +957,7 @@ def default_rules(world, player): swordless_rules(world, player) -def ow_rules(world, player): +def ow_inverted_rules(world, player): if world.is_atgt_swapped(player): set_rule(world.get_entrance('Agahnims Tower', player), lambda state: state.has_crystals(world.crystals_needed_for_gt[player], player)) else: @@ -1129,6 +1128,8 @@ def ow_rules(world, player): set_rule(world.get_entrance('HC East Entry Mirror Spot', player), lambda state: state.has_Mirror(player)) set_rule(world.get_entrance('HC Courtyard Left Mirror Spot', player), lambda state: state.has_Mirror(player)) set_rule(world.get_entrance('HC Area South Mirror Spot', player), lambda state: state.has_Mirror(player)) + set_rule(world.get_entrance('Hyrule Castle Main Gate (South)', player), lambda state: state.has_Mirror(player)) + set_rule(world.get_entrance('Hyrule Castle Main Gate (North)', player), lambda state: state.has_Mirror(player)) set_rule(world.get_entrance('Top of Pyramid', player), lambda state: state.has_beaten_aga(player)) set_rule(world.get_entrance('Top of Pyramid (Inner)', player), lambda state: state.has_beaten_aga(player)) else: @@ -1481,7 +1482,7 @@ def no_glitches_rules(world, player): # add_rule(world.get_location(location, player), lambda state: state.has('Hookshot', player)) set_rule(world.get_entrance('Paradox Cave Push Block Reverse', player), lambda state: False) # no glitches does not require block override forbid_bomb_jump_requirements(world, player) - if world.get_region('Big Bomb Shop', player).entrances: # just some location that is placed late in the ER algorithm, prevent underworld rules from applying when trying to search reachability in the overworld + if not world.is_copied_world: add_conditional_lamps(world, player) @@ -1744,7 +1745,7 @@ def standard_rules(world, player): add_rule(world.get_entrance('Bonk Fairy (Light)', player), lambda state: state.has('Zelda Delivered', player)) if world.shuffle_bonk_drops[player]: - if world.get_region('Big Bomb Shop', player).entrances: # just some location that is placed late in the ER algorithm, prevent standard rules from applying when trying to search reachability in the overworld + if not world.is_copied_world: add_rule(world.get_location('Hyrule Castle Tree', player), lambda state: state.has('Zelda Delivered', player)) add_rule(world.get_location('Central Bonk Rocks Tree', player), lambda state: state.has('Zelda Delivered', player)) diff --git a/TestSuite.py b/TestSuite.py index aac277af..24601514 100644 --- a/TestSuite.py +++ b/TestSuite.py @@ -45,7 +45,7 @@ def main(args=None): test("Vanilla ", "--shuffle vanilla") test("Retro ", "--retro --shuffle vanilla") - test("Keysanity ", "--shuffle vanilla --keydropshuffle drops_only --keysanity") + test("Keysanity ", "--shuffle vanilla --dropshuffle --keysanity") test("Shopsanity", "--shuffle vanilla --shopsanity") test("Simple ", "--shuffle simple") test("Full ", "--shuffle full") diff --git a/data/base2current.bps b/data/base2current.bps index c6d73f65..b7f58d18 100644 Binary files a/data/base2current.bps and b/data/base2current.bps differ diff --git a/resources/app/cli/args.json b/resources/app/cli/args.json index ebeedf53..d8166c48 100644 --- a/resources/app/cli/args.json +++ b/resources/app/cli/args.json @@ -148,7 +148,6 @@ "ow_crossed": { "choices": [ "none", - "allowed", "polar", "grouped", "limited", diff --git a/resources/app/cli/lang/en.json b/resources/app/cli/lang/en.json index 5d753a66..aabea6c0 100644 --- a/resources/app/cli/lang/en.json +++ b/resources/app/cli/lang/en.json @@ -217,7 +217,6 @@ "ow_crossed": [ "This allows cross-world connections to occur on the overworld.", "None: No transitions are cross-world connections.", - "Allowed: Only entrances/whirlpools can end up cross-world.", "Polar: Only used when Mixed is enabled. This retains original", " connections even when overworld tiles are swapped.", "Limited: Exactly nine transitions are randomly chosen as", diff --git a/resources/app/gui/lang/en.json b/resources/app/gui/lang/en.json index b25f4233..e1b5f497 100644 --- a/resources/app/gui/lang/en.json +++ b/resources/app/gui/lang/en.json @@ -133,7 +133,6 @@ "randomizer.overworld.crossed": "Crossed", "randomizer.overworld.crossed.none": "None", - "randomizer.overworld.crossed.allowed": "None (Allowed)", "randomizer.overworld.crossed.polar": "Polar", "randomizer.overworld.crossed.grouped": "Grouped", "randomizer.overworld.crossed.limited": "Limited", diff --git a/resources/app/gui/randomize/overworld/widgets.json b/resources/app/gui/randomize/overworld/widgets.json index 9595ea8e..dc0363c6 100644 --- a/resources/app/gui/randomize/overworld/widgets.json +++ b/resources/app/gui/randomize/overworld/widgets.json @@ -20,7 +20,6 @@ "default": "vanilla", "options": [ "none", - "allowed", "polar", "grouped", "limited", diff --git a/source/meta/build-dr.py b/source/meta/build-dr.py index 6f26adb9..a83c9d56 100644 --- a/source/meta/build-dr.py +++ b/source/meta/build-dr.py @@ -22,7 +22,6 @@ if os.path.isdir("build") and not sys.platform.find("mac") and not sys.platform. subprocess.run(" ".join([f"pyinstaller {SPEC_FILE} ", upx_string, "-y ", - "--onefile ", f"--distpath {DEST_DIRECTORY} ", ]), shell=True) diff --git a/source/meta/build-gui.py b/source/meta/build-gui.py index ae284261..4986df67 100644 --- a/source/meta/build-gui.py +++ b/source/meta/build-gui.py @@ -22,7 +22,6 @@ if os.path.isdir("build") and not sys.platform.find("mac") and not sys.platform. subprocess.run(" ".join([f"pyinstaller {SPEC_FILE} ", upx_string, "-y ", - "--onefile ", f"--distpath {DEST_DIRECTORY} ", ]), shell=True)