Merge branch 'OverworldShuffleDev' into OverworldShuffle

This commit is contained in:
codemann8
2022-09-04 22:23:58 -05:00
23 changed files with 403 additions and 239 deletions

View File

@@ -62,6 +62,8 @@ class World(object):
self.aga_randomness = True self.aga_randomness = True
self.lock_aga_door_in_escape = False self.lock_aga_door_in_escape = False
self.save_and_quit_from_boss = True self.save_and_quit_from_boss = True
self.override_bomb_check = False
self.is_copied_world = False
self.accessibility = accessibility.copy() self.accessibility = accessibility.copy()
self.fix_skullwoods_exit = {} self.fix_skullwoods_exit = {}
self.fix_palaceofdarkness_exit = {} self.fix_palaceofdarkness_exit = {}
@@ -1104,7 +1106,7 @@ class CollectionState(object):
region = self.world.get_region(regionname, player) 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)) 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): if can_reach_non_bunny(region):
return True 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)) 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 # 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): if can_reach_non_bunny(region):
return True return True
@@ -1306,7 +1308,7 @@ class CollectionState(object):
# In the future, this can be used to check if the player starts without bombs # In the future, this can be used to check if the player starts without bombs
def can_use_bombs(self, player): 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): def can_hit_crystal(self, player):
return (self.can_use_bombs(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) and (self.can_buy_unlimited('Single Arrow', player) or self.has('Single Arrow', player))
return self.has('Bow', player) return self.has('Bow', player)
def can_get_good_bee(self, player): # def can_get_good_bee(self, player):
cave = self.world.get_region('Good Bee Cave', player) # cave = self.world.get_region('Good Bee Cave', player)
return ( # return (
self.can_use_bombs(player) and # self.can_use_bombs(player) and
self.has_bottle(player) and # self.has_bottle(player) and
self.has('Bug Catching Net', player) and # self.has('Bug Catching Net', player) and
(self.has_Boots(player) or (self.has_sword(player) and self.has('Quake', player))) and # (self.has_Boots(player) or (self.has_sword(player) and self.has('Quake', player))) and
cave.can_reach(self) and # cave.can_reach(self) and
self.is_not_bunny(cave, player) # self.is_not_bunny(cave, player)
) # )
def has_beaten_aga(self, 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)) return self.has('Beat Agahnim 1', player) and (self.world.mode[player] != 'standard' or self.has('Zelda Delivered', player))

View File

@@ -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 # 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: # 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 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.has('Cane of Byrna', player) and state.can_extend_magic(player, 16))
state.can_get_good_bee(player)
) )
def BlindDefeatRule(state, player): def BlindDefeatRule(state, player):
@@ -202,13 +201,14 @@ def place_bosses(world, player):
place_boss(boss, level, loc, loc_text, world, player) place_boss(boss, level, loc, loc_text, world, player)
elif world.boss_shuffle[player] == 'unique': elif world.boss_shuffle[player] == 'unique':
bosses = list(placeable_bosses) bosses = list(placeable_bosses)
gt_bosses = list() gt_bosses = []
for [loc, level] in boss_locations: for [loc, level] in boss_locations:
loc_text = loc + (' ('+level+')' if level else '') loc_text = loc + (' ('+level+')' if level else '')
try: try:
if level: 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) gt_bosses.append(boss)
else: else:
boss = random.choice([b for b in bosses if can_place_boss(world, player, b, loc, level)]) boss = random.choice([b for b in bosses if can_place_boss(world, player, b, loc, level)])

View File

@@ -1,5 +1,19 @@
# Changelog # 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 ### 0.2.9.1
- Lite/Lean ER now includes Cave Pot locations with various Pottery options - Lite/Lean ER now includes Cave Pot locations with various Pottery options
- Changed Unique Boss Shuffle so that GT Bosses are unique amongst themselves - Changed Unique Boss Shuffle so that GT Bosses are unique amongst themselves

View File

@@ -215,7 +215,7 @@ def vanilla_key_logic(world, player):
key_layout = build_key_layout(builder, start_regions, doors, world, player) key_layout = build_key_layout(builder, start_regions, doors, world, player)
valid = validate_key_layout(key_layout, world, player) valid = validate_key_layout(key_layout, world, player)
if not valid: 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 builder.key_door_proposal = doors
if player not in world.key_logic.keys(): if player not in world.key_logic.keys():
world.key_logic[player] = {} world.key_logic[player] = {}
@@ -381,7 +381,7 @@ def choose_portals(world, player):
if world.doorShuffle[player] in ['basic', 'crossed']: if world.doorShuffle[player] in ['basic', 'crossed']:
cross_flag = world.doorShuffle[player] == 'crossed' cross_flag = world.doorShuffle[player] == 'crossed'
# key drops allow the big key in the right place in Desert Tiles 2 # 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' std_flag = world.mode[player] == 'standard'
# roast incognito doors # roast incognito doors
world.get_room(0x60, player).delete(5) world.get_room(0x60, player).delete(5)
@@ -989,11 +989,16 @@ def cross_dungeon(world, player):
paths = determine_required_paths(world, player) paths = determine_required_paths(world, player)
check_required_paths(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 = 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 = world.get_dungeon('Agahnims Tower', player)
at.dungeon_items.append(ItemFactory('Compass (Agahnims Tower)', player)) at.dungeon_items.append(at_compass)
at.dungeon_items.append(ItemFactory('Map (Agahnims Tower)', player)) at.dungeon_items.append(at_map)
assign_cross_keys(dungeon_builders, world, player) 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)) 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'): 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') world.inaccessible_regions[player].append('Hyrule Castle Ledge')
logger = logging.getLogger('') logger = logging.getLogger('')
logger.debug('Inaccessible Regions:') #logger.debug('Inaccessible Regions:')
for r in world.inaccessible_regions[player]: #for r in world.inaccessible_regions[player]:
logger.debug('%s', r) # logger.debug('%s', r)
def find_accessible_entrances(world, player, builder): 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 = [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]) entrances.extend(drop_entrances[builder.name])
hc_std = False
if world.mode[player] == 'standard' and builder.name == 'Hyrule Castle': if world.mode[player] == 'standard' and builder.name == 'Hyrule Castle':
hc_std = True
start_regions = ['Hyrule Castle Courtyard'] start_regions = ['Hyrule Castle Courtyard']
else: 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'] 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: if connect not in queue and connect not in visited_regions:
queue.append(connect) queue.append(connect)
for ext in next_region.exits: 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 connect = ext.connected_region
if connect is None or ext.door and ext.door.blocked: if connect is None or ext.door and ext.door.blocked:
continue continue

View File

@@ -3,6 +3,7 @@ from collections import defaultdict, OrderedDict
import RaceRandom as random import RaceRandom as random
from BaseClasses import CollectionState, RegionType from BaseClasses import CollectionState, RegionType
from OverworldShuffle import build_accessible_region_list from OverworldShuffle import build_accessible_region_list
from DoorShuffle import find_inaccessible_regions
from OWEdges import OWTileRegions from OWEdges import OWTileRegions
from Utils import stack_size3a from Utils import stack_size3a
@@ -31,7 +32,7 @@ def link_entrances(world, player):
Cave_Three_Exits = Cave_Three_Exits_Base.copy() Cave_Three_Exits = Cave_Three_Exits_Base.copy()
from OverworldShuffle import build_sectors 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) world.owsectors[player] = build_sectors(world, player)
# modifications to lists # modifications to lists
@@ -832,8 +833,6 @@ def connect_mandatory_exits(world, entrances, caves, must_be_exits, player, must
random.shuffle(entrances) random.shuffle(entrances)
random.shuffle(caves) random.shuffle(caves)
from DoorShuffle import find_inaccessible_regions
used_caves = [] used_caves = []
required_entrances = 0 # Number of entrances reserved for used_caves required_entrances = 0 # Number of entrances reserved for used_caves
skip_remaining = False skip_remaining = False
@@ -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]) dw_entrances.extend([e for e in dungeon_owid_map[owid][0] if e in entrance_pool])
# determine must-exit entrances # determine must-exit entrances
from DoorShuffle import find_inaccessible_regions
find_inaccessible_regions(world, player) find_inaccessible_regions(world, player)
lw_must_exit = list() lw_must_exit = list()
@@ -1442,13 +1440,12 @@ def place_old_man(world, pool, player, ignore_list=[]):
def junk_fill_inaccessible(world, player): def junk_fill_inaccessible(world, player):
from Main import copy_world from Main import copy_world_limited
from DoorShuffle import find_inaccessible_regions
find_inaccessible_regions(world, player) find_inaccessible_regions(world, player)
for p in range(1, world.players + 1): for p in range(1, world.players + 1):
world.key_logic[p] = {} world.key_logic[p] = {}
base_world = copy_world(world, True) base_world = copy_world_limited(world)
base_world.override_bomb_check = True base_world.override_bomb_check = True
# remove regions that have a dungeon entrance # 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(lw_entrances)
random.shuffle(dw_entrances) random.shuffle(dw_entrances)
from DoorShuffle import find_inaccessible_regions
find_inaccessible_regions(world, player) find_inaccessible_regions(world, player)
# remove regions that have a dungeon entrance # 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): 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 from Items import ItemFactory
for p in range(1, world.players + 1): for p in range(1, world.players + 1):
world.key_logic[p] = {} world.key_logic[p] = {}
base_world = copy_world(world, True) base_world = copy_world_limited(world)
base_world.override_bomb_check = True base_world.override_bomb_check = True
connect_simple(base_world, 'Links House S&Q', start_region, player) 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): 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 Items import ItemFactory
from DoorShuffle import find_inaccessible_regions
for p in range(1, world.players + 1): for p in range(1, world.players + 1):
world.key_logic[p] = {} world.key_logic[p] = {}
base_world = copy_world(world, True) base_world = copy_world_limited(world)
base_world.override_bomb_check = True base_world.override_bomb_check = True
entrance = world.get_entrance(entrance_name, player) entrance = world.get_entrance(entrance_name, player)

99
Fill.py
View File

@@ -3,6 +3,7 @@ import collections
import itertools import itertools
import logging import logging
import math import math
from contextlib import suppress
from BaseClasses import CollectionState, FillError, LocationType from BaseClasses import CollectionState, FillError, LocationType
from Items import ItemFactory from Items import ItemFactory
@@ -35,17 +36,6 @@ def dungeon_tracking(world):
def fill_dungeons_restrictive(world, shuffled_locations): def fill_dungeons_restrictive(world, shuffled_locations):
dungeon_tracking(world) 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 # with shuffled dungeon items they are distributed as part of the normal item pool
for item in world.get_items(): for item in world.get_items():
@@ -55,17 +45,32 @@ def fill_dungeons_restrictive(world, shuffled_locations):
item.priority = True item.priority = True
dungeon_items = [item for item in get_dungeon_item_pool(world) if item.is_inside_dungeon_item(world)] 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 def fill(base_state, items, key_pool):
sort_order = {"BigKey": 3, "SmallKey": 2} fill_restrictive(world, base_state, shuffled_locations, items, key_pool, True)
dungeon_items.sort(key=lambda item: sort_order.get(item.type, 1))
fill_restrictive(world, all_state_base, shuffled_locations, dungeon_items, all_state_base = world.get_all_state()
keys_in_itempool={player: not world.keyshuffle[player] for player in range(1, world.players+1)}, big_state_base = all_state_base.copy()
single_player_placement=True) 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): vanilla=False):
def sweep_from_pool(): def sweep_from_pool():
new_state = base_state.copy() 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) item_locations = filter_locations(item_to_place, locations, world, vanilla)
for location in item_locations: for location in item_locations:
spot_to_fill = verify_spot_to_fill(location, item_to_place, maximum_exploration_state, spot_to_fill = verify_spot_to_fill(location, item_to_place, maximum_exploration_state,
single_player_placement, perform_access_check, itempool, single_player_placement, perform_access_check, key_pool, world)
keys_in_itempool, world)
if spot_to_fill: if spot_to_fill:
break break
if spot_to_fill is None: if spot_to_fill is None:
@@ -111,7 +115,7 @@ def fill_restrictive(world, base_state, locations, itempool, keys_in_itempool=No
continue continue
spot_to_fill = recovery_placement(item_to_place, locations, world, maximum_exploration_state, spot_to_fill = recovery_placement(item_to_place, locations, world, maximum_exploration_state,
base_state, itempool, perform_access_check, item_locations, 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: if spot_to_fill is None:
# we filled all reachable spots. Maybe the game can be beaten anyway? # we filled all reachable spots. Maybe the game can be beaten anyway?
unplaced_items.insert(0, item_to_place) 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) raise FillError('No more spots to place %s' % item_to_place)
world.push_item(spot_to_fill, item_to_place, False) 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_outside_keys(item_to_place, spot_to_fill, world)
track_dungeon_items(item_to_place, spot_to_fill, world) track_dungeon_items(item_to_place, spot_to_fill, world)
locations.remove(spot_to_fill) 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, 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 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.item = item_to_place
test_state = max_exp_state.copy() 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 test_state = max_exp_state
if not single_player_placement or location.player == item_to_place.player: if not single_player_placement or location.player == item_to_place.player:
if location.can_fill(test_state, item_to_place, perform_access_check): 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, key_pool, world):
if valid_key_placement(item_to_place, location, test_pool, world):
if item_to_place.crystal or valid_dungeon_placement(item_to_place, location, world): if item_to_place.crystal or valid_dungeon_placement(item_to_place, location, world):
return location return location
if item_to_place.smallkey or item_to_place.bigkey: 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 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): if not valid_reserved_placement(item, location, world):
return False return False
if ((not item.smallkey and not item.bigkey) or item.player != location.player 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): if dungeon.name not in item.name and (dungeon.name != 'Hyrule Castle' or 'Escape' not in item.name):
return True return True
key_logic = world.key_logic[item.player][dungeon.name] 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 prize_loc = None
if key_logic.prize_location: if key_logic.prize_location:
prize_loc = world.get_location(key_logic.prize_location, location.player) 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, 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') logging.getLogger('').debug(f'Could not place {item_to_place} attempting recovery')
if world.algorithm in ['balanced', 'equitable']: 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) single_player_placement)
elif world.algorithm == 'vanilla_fill': elif world.algorithm == 'vanilla_fill':
if item_to_place.type == 'Crystal': if item_to_place.type == 'Crystal':
possible_swaps = [x for x in state.locations_checked if x.item.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, 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: else:
i, config = 0, world.item_pool_config i, config = 0, world.item_pool_config
tried = set(attempted) 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] other_locs = [x for x in locations if x.name in fallback_locations]
for location in other_locs: for location in other_locs:
spot_to_fill = verify_spot_to_fill(location, item_to_place, state, single_player_placement, 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: if spot_to_fill:
return spot_to_fill return spot_to_fill
i += 1 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) other_locations = vanilla_fallback(item_to_place, locations, world)
for location in other_locations: for location in other_locations:
spot_to_fill = verify_spot_to_fill(location, item_to_place, state, single_player_placement, 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: if spot_to_fill:
return spot_to_fill return spot_to_fill
tried.update(other_locations) tried.update(other_locations)
other_locations = [x for x in locations if x not in tried] other_locations = [x for x in locations if x not in tried]
for location in other_locations: for location in other_locations:
spot_to_fill = verify_spot_to_fill(location, item_to_place, state, single_player_placement, 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: if spot_to_fill:
return spot_to_fill return spot_to_fill
return None 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] other_locations = [x for x in locations if x not in attempted]
for location in other_locations: for location in other_locations:
spot_to_fill = verify_spot_to_fill(location, item_to_place, state, single_player_placement, 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: if spot_to_fill:
return spot_to_fill return spot_to_fill
return None return None
def last_ditch_placement(item_to_place, locations, world, state, base_state, itempool, 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): def location_preference(loc):
if not loc.item.advancement: if not loc.item.advancement:
return 1 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] if x.item.type not in ['Event', 'Crystal'] and not x.forced_item]
swap_locations = sorted(possible_swaps, key=location_preference) swap_locations = sorted(possible_swaps, key=location_preference)
return try_possible_swaps(swap_locations, item_to_place, locations, world, base_state, itempool, 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, 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: for location in swap_locations:
old_item = location.item old_item = location.item
new_pool = list(itempool) + [old_item] new_pool = list(itempool) + [old_item]
new_spot = find_spot_for_item(item_to_place, [location], world, base_state, new_pool, 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: if new_spot:
restore_item = new_spot.item restore_item = new_spot.item
new_spot.item = item_to_place new_spot.item = item_to_place
swap_spot = find_spot_for_item(old_item, locations, world, base_state, itempool, 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: if swap_spot:
logging.getLogger('').debug(f'Swapping {old_item} for {item_to_place}') logging.getLogger('').debug(f'Swapping {old_item} for {item_to_place}')
world.push_item(swap_spot, old_item, False) 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 # Make sure the escape small key is placed first in standard with key shuffle to prevent running out of spots
# todo: crossed # 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) 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 # 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) progitempool.sort(key=lambda item: 0 if item.map or item.compass else 1)
if world.algorithm == 'vanilla_fill': 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, key_pool, vanilla=True)
fill_restrictive(world, world.state, fill_locations, progitempool, keys_in_pool) fill_restrictive(world, world.state, fill_locations, progitempool, key_pool)
random.shuffle(fill_locations) random.shuffle(fill_locations)
if world.algorithm == 'balanced': if world.algorithm == 'balanced':
fast_fill(world, prioitempool, fill_locations) 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.') 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.parent_region.name in shop_to_location_table:
if l.name in shop_to_location_table[l.parent_region.name]: if l.name in shop_to_location_table[l.parent_region.name]:
idx = shop_to_location_table[l.parent_region.name].index(l.name) idx = shop_to_location_table[l.parent_region.name].index(l.name)
inv_slot = l.parent_region.shop.inventory[idx] inv_slot = l.parent_region.shop.inventory[idx]
inv_slot['item'] = l.item.name inv_slot['item'] = l.item.name
if make_item_free:
inv_slot['price'] = 0
elif l.parent_region in retro_shops: elif l.parent_region in retro_shops:
idx = retro_shops[l.parent_region.name].index(l.name) idx = retro_shops[l.parent_region.name].index(l.name)
inv_slot = l.parent_region.shop.inventory[idx] inv_slot = l.parent_region.shop.inventory[idx]
@@ -921,12 +929,13 @@ def balance_money_progression(world):
if len(increase_targets) == 0: if len(increase_targets) == 0:
raise Exception('No early sphere swaps for rupees - money grind would be required - bailing for now') 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) 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: if best_swap is None:
logger.debug(f'Upgrading {best_target.item.name} @ {best_target.name} for 300 Rupees') 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 = ItemFactory('Rupees (300)', best_target.item.player)
best_target.item.location = best_target best_target.item.location = best_target
check_shop_swap(best_target.item.location) check_shop_swap(best_target.item.location, make_item_free)
else: else:
old_item = best_target.item old_item = best_target.item
logger.debug(f'Swapping {best_target.item.name} @ {best_target.name} for {best_swap.item.name} @ {best_swap.name}') 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_target.item.location = best_target
best_swap.item = old_item best_swap.item = old_item
best_swap.item.location = best_swap 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) check_shop_swap(best_swap.item.location)
increase = best_value - old_value increase = best_value - old_value
difference -= increase difference -= increase

View File

@@ -1155,27 +1155,3 @@ def test():
if __name__ == '__main__': if __name__ == '__main__':
test() 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])

135
Main.py
View File

@@ -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.item.FillUtil import create_item_pool_config, massage_item_pool, district_item_pool_config
from source.tools.BPS import create_bps_from_data 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 from source.classes.BabelFish import BabelFish
@@ -396,7 +396,7 @@ def main(args, seed=None, fish=None):
return world return world
def copy_world(world, partial_copy=False): def copy_world(world):
# ToDo: Not good yet # ToDo: Not good yet
ret = World(world.players, world.owShuffle, world.owCrossed, world.owMixed, world.shuffle, world.doorShuffle, world.logic, world.mode, world.swords, 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.difficulty, world.difficulty_adjustments, world.timer, world.progressive, world.goal, world.algorithm,
@@ -543,7 +543,6 @@ def copy_world(world, partial_copy=False):
ret.dungeon_layouts = world.dungeon_layouts ret.dungeon_layouts = world.dungeon_layouts
ret.key_logic = world.key_logic ret.key_logic = world.key_logic
ret.dungeon_portals = world.dungeon_portals ret.dungeon_portals = world.dungeon_portals
if not partial_copy:
for player, portals in world.dungeon_portals.items(): for player, portals in world.dungeon_portals.items():
for portal in portals: for portal in portals:
connect_portal(portal, ret, player) connect_portal(portal, ret, player)
@@ -554,9 +553,127 @@ def copy_world(world, partial_copy=False):
categorize_world_regions(ret, player) categorize_world_regions(ret, player)
set_rules(ret, player) set_rules(ret, player)
if partial_copy: return ret
# undo some of the things that unintentionally affect the original world object
world.key_logic = {}
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 return ret
@@ -578,11 +695,7 @@ def copy_dynamic_regions_and_locations(world, ret):
for location in world.dynamic_locations: for location in world.dynamic_locations:
new_reg = ret.get_region(location.parent_region.name, location.parent_region.player) 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) 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 new_loc.type = location.type
# 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_reg.locations.append(new_loc) new_reg.locations.append(new_loc)
ret.clear_location_cache() ret.clear_location_cache()

View File

@@ -1,5 +1,7 @@
import argparse import argparse
import logging import logging
from pathlib import Path
import os
import RaceRandom as random import RaceRandom as random
import urllib.request import urllib.request
import urllib.parse import urllib.parse
@@ -106,13 +108,11 @@ def main():
DRMain(erargs, seed, BabelFish()) DRMain(erargs, seed, BabelFish())
def get_weights(path): def get_weights(path):
try: if os.path.exists(Path(path)):
if urllib.parse.urlparse(path).scheme: with open(path, "r", encoding="utf-8") as f:
return yaml.load(urllib.request.urlopen(path), Loader=yaml.FullLoader)
with open(path, 'r', encoding='utf-8') as f:
return yaml.load(f, Loader=yaml.SafeLoader) return yaml.load(f, Loader=yaml.SafeLoader)
except Exception as e: elif urllib.parse.urlparse(path).scheme in ['http', 'https']:
raise Exception(f'Failed to read weights file: {e}') return yaml.load(urllib.request.urlopen(path), Loader=yaml.FullLoader)
def roll_settings(weights): def roll_settings(weights):
def get_choice(option, root=None): def get_choice(option, root=None):

View File

@@ -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 OWEdges import OWTileRegions, OWEdgeGroups, OWExitTypes, OpenStd, parallel_links, IsParallel
from Utils import bidict from Utils import bidict
version_number = '0.2.9.1' version_number = '0.2.10.0'
# branch indicator is intentionally different across branches # branch indicator is intentionally different across branches
version_branch = '' version_branch = ''
@@ -289,6 +289,8 @@ def link_overworld(world, player):
for whirlpools in whirlpool_candidates: for whirlpools in whirlpool_candidates:
random.shuffle(whirlpools) random.shuffle(whirlpools)
while len(whirlpools): while len(whirlpools):
if len(whirlpools) % 2 == 1:
x=0
from_owid, from_whirlpool, from_region = whirlpools.pop() from_owid, from_whirlpool, from_region = whirlpools.pop()
to_owid, to_whirlpool, to_region = whirlpools.pop() to_owid, to_whirlpool, to_region = whirlpools.pop()
connect_simple(world, from_whirlpool, to_region, player) connect_simple(world, from_whirlpool, to_region, player)
@@ -329,7 +331,7 @@ def link_overworld(world, player):
# layout shuffle # layout shuffle
groups = adjust_edge_groups(world, trimmed_groups, edges_to_swap, player) groups = adjust_edge_groups(world, trimmed_groups, edges_to_swap, player)
tries = 20 tries = 100
valid_layout = False valid_layout = False
connected_edge_cache = connected_edges.copy() connected_edge_cache = connected_edges.copy()
while not valid_layout and tries > 0: 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): if not ignore_proximity and random.randint(0, 31) != 0 and new_ignored.intersection(ignored_regions):
return False return False
ignored_regions.update(new_ignored) ignored_regions.update(new_ignored)
if owid in flute_pool:
flute_pool.remove(owid) flute_pool.remove(owid)
if ignore_proximity: if ignore_proximity:
logging.getLogger('').warning(f'Warning: Adding flute spot within proximity: {hex(owid)}') 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) 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 return True
# determine sectors (isolated groups of regions) to place flute spots # determine sectors (isolated groups of regions) to place flute spots
@@ -442,6 +449,7 @@ def link_overworld(world, player):
sector_total -= 1 sector_total -= 1
spots_to_place = min(flute_spots - sector_total, max(1, round((sector[0] * (flute_spots - sector_total) / region_total) + 0.5))) 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 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]: if 'Desert Palace Teleporter Ledge' in sector[1] or 'Misery Mire Teleporter Ledge' in sector[1]:
addSpot(0x38, False) # guarantee desert/mire access addSpot(0x38, False) # guarantee desert/mire access
@@ -553,8 +561,8 @@ def shuffle_tiles(world, groups, result_list, do_grouped, player):
group_parity = {} group_parity = {}
for group_data in groups: for group_data in groups:
group = group_data[0] group = group_data[0]
parity = [0, 0, 0, 0, 0] parity = [0, 0, 0, 0, 0, 0]
# vertical land # 0: vertical
if 0x00 in group: if 0x00 in group:
parity[0] += 1 parity[0] += 1
if 0x0f in group: if 0x0f in group:
@@ -563,40 +571,46 @@ def shuffle_tiles(world, groups, result_list, do_grouped, player):
parity[0] -= 1 parity[0] -= 1
if 0x81 in group: if 0x81 in group:
parity[0] -= 1 parity[0] -= 1
# horizontal land # 1: horizontal land single
if 0x1a in group: if 0x1a in group:
parity[1] -= 1 parity[1] -= 1
if 0x1b in group: if 0x1b in group:
parity[1] += 1 parity[1] += 1
if 0x28 in group: if 0x28 in group:
parity[1] += 1
if 0x29 in group:
parity[1] -= 1 parity[1] -= 1
if 0x30 in group: if 0x29 in group:
parity[1] -= 2 parity[1] += 1
if 0x3a in group: # 2: horizontal land double
parity[1] += 2 if 0x28 in group:
# horizontal water
if 0x2d in group:
parity[2] += 1 parity[2] += 1
if 0x80 in group: if 0x29 in group:
parity[2] -= 1 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: if 0x0f in group:
parity[3] += 1 parity[4] += 1
if 0x12 in group: if 0x12 in group:
parity[3] += 1 parity[4] += 1
if 0x33 in group: if 0x33 in group:
parity[3] += 1 parity[4] += 1
if 0x35 in group: 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 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': if 0x1b in group and world.mode[player] != 'standard':
parity[4] += 1 parity[5] += 1
if 0x1b in group and world.shuffle_ganon: if 0x1b in group and world.shuffle_ganon:
parity[4] -= 1 parity[5] -= 1
group_parity[group[0]] = parity group_parity[group[0]] = parity
attempts = 1000 attempts = 1000
@@ -607,7 +621,7 @@ def shuffle_tiles(world, groups, result_list, do_grouped, player):
# tile shuffle happens here # tile shuffle happens here
removed = list() removed = list()
for group in groups: 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): if random.randint(0, 1):
removed.append(group) removed.append(group)
@@ -621,14 +635,20 @@ def shuffle_tiles(world, groups, result_list, do_grouped, player):
exist_lw_regions.extend(lw_regions) exist_lw_regions.extend(lw_regions)
exist_dw_regions.extend(dw_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 = [sum(group_parity[group[0][0]][i] for group in groups if group not in removed) for i in range(6)]
parity[3] %= 2 # actual parity if not world.owKeepSimilar[player]:
if (world.owCrossed[player] == 'none' or do_grouped) and parity[:4] != [0, 0, 0, 0]: 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 attempts -= 1
continue continue
# ensure sanc can be placed in LW in certain modes # 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'): 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) free_drops = 6 + (1 if world.mode[player] != 'standard' else 0) + (1 if world.shuffle_ganon else 0)
if free_dw_drops == free_drops: if free_dw_drops == free_drops:
attempts -= 1 attempts -= 1
@@ -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 # 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'] \ 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)) \ 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 False
return True 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): 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]]) 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]]) merge_groups([[0x28, 0x29]])
if not world.owWhirlpoolShuffle[player] and (world.owCrossed[player] == 'none' or do_grouped): 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 return found
def build_sectors(world, player): def build_sectors(world, player):
from Main import copy_world from Main import copy_world_limited
from OWEdges import OWTileRegions from OWEdges import OWTileRegions
# perform accessibility check on duplicate world # perform accessibility check on duplicate world
for p in range(1, world.players + 1): for p in range(1, world.players + 1):
world.key_logic[p] = {} 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) # build lists of contiguous regions accessible with full inventory (excl portals/mirror/flute/entrances)
regions = list(OWTileRegions.copy().keys()) regions = list(OWTileRegions.copy().keys())
@@ -928,11 +948,23 @@ def build_sectors(world, player):
sectors2.append(explored_regions) sectors2.append(explored_regions)
sectors[s] = sectors2 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 return sectors
def build_accessible_region_list(world, start_region, player, build_copy_world=False, cross_world=False, region_rules=True, ignore_ledges = False): 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 BaseClasses import CollectionState
from Main import copy_world_limited
from Items import ItemFactory from Items import ItemFactory
from Utils import stack_size3a 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: if build_copy_world:
for p in range(1, world.players + 1): for p in range(1, world.players + 1):
world.key_logic[p] = {} world.key_logic[p] = {}
base_world = copy_world(world, True) base_world = copy_world_limited(world)
base_world.override_bomb_check = True base_world.override_bomb_check = True
else: else:
base_world = world base_world = world
@@ -1015,7 +1047,7 @@ def validate_layout(world, player):
entrance_connectors['Bumper Cave Entrance'] = ['West Dark Death Mountain (Bottom)'] entrance_connectors['Bumper Cave Entrance'] = ['West Dark Death Mountain (Bottom)']
entrance_connectors['Mountain Entry Entrance'] = ['Mountain Entry Ledge'] 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 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 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): for p in range(1, world.players + 1):
world.key_logic[p] = {} world.key_logic[p] = {}
base_world = copy_world(world, True) base_world = copy_world_limited(world)
explored_regions = list() explored_regions = list()
if world.shuffle[player] in ['vanilla', 'dungeonssimple', 'dungeonsfull'] or not world.shufflelinks[player]: if world.shuffle[player] in ['vanilla', 'dungeonssimple', 'dungeonsfull'] or not world.shufflelinks[player]:

View File

@@ -787,12 +787,13 @@ vanilla_pots = {
Pot(230, 27, PotItem.Bomb, 'Light World Bomb Hut', obj=RoomObject(0x03EF5E, [0xCF, 0xDF, 0xFA]))], 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]))], 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]))], 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])), # note: these addresses got moved thanks to waterfall fairy edit
Pot(96, 4, PotItem.Heart, 'Dark Desert Hint', obj=RoomObject(0x03F7A3, [0xC3, 0x23, 0xFA])), 0x114: [Pot(92, 4, PotItem.Heart, 'Dark Desert Hint', obj=RoomObject(0x03F79A, [0xBB, 0x23, 0xFA])),
Pot(92, 5, PotItem.Bomb, 'Dark Desert Hint', obj=RoomObject(0x03F7A6, [0xBB, 0x2B, 0xFA])), Pot(96, 4, PotItem.Heart, 'Dark Desert Hint', obj=RoomObject(0x03F79D, [0xC3, 0x23, 0xFA])),
Pot(96, 5, PotItem.Bomb, 'Dark Desert Hint', obj=RoomObject(0x03F7A9, [0xC3, 0x2B, 0xFA])), Pot(92, 5, PotItem.Bomb, 'Dark Desert Hint', obj=RoomObject(0x03F7A0, [0xBB, 0x2B, 0xFA])),
Pot(92, 10, PotItem.FiveArrows, 'Dark Desert Hint', obj=RoomObject(0x03F7AC, [0xBB, 0x53, 0xFA])), Pot(96, 5, PotItem.Bomb, 'Dark Desert Hint', obj=RoomObject(0x03F7A3, [0xC3, 0x2B, 0xFA])),
Pot(96, 10, PotItem.Heart, 'Dark Desert Hint', obj=RoomObject(0x03F7AF, [0xC3, 0x53, 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 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(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])), Pot(166, 3, PotItem.Heart, 'Spike Cave', obj=RoomObject(0x03FCC1, [0x4F, 0x1F, 0xFA])),

View File

@@ -68,8 +68,6 @@ OW Transitions are shuffled within each world separately.
This allows OW connections to be shuffled cross-world. 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 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. 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.

View File

@@ -183,6 +183,21 @@ Same as above but both small keys and bigs keys of the dungeon are not allowed o
#### Unstable #### 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 * 1.0.1.0
* Large features * Large features
* New pottery modes - see notes above * 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. * 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) * Updated tourney winners (included Doors Async League winners)
* Some textual changes for hints (capitalization standardization) * 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. * 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) * Expanded Mystery logic options (e.g. owglitches)
* Updated indicators on keysanity menu for overworld map option * Updated indicators on keysanity menu for overworld map option

58
Rom.py
View File

@@ -38,7 +38,7 @@ from source.dungeon.RoomList import Room0127
JAP10HASH = '03a63945398191337e896e5771f77173' JAP10HASH = '03a63945398191337e896e5771f77173'
RANDOMIZERBASEHASH = '92a390672efafb652774c1514ac66c4b' RANDOMIZERBASEHASH = '831beb6f60c3c99467552493b3ce6f19'
class JsonRom(object): class JsonRom(object):
@@ -672,18 +672,6 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False):
if world.mapshuffle[player]: if world.mapshuffle[player]:
rom.write_byte(0x155C9, random.choice([0x11, 0x16])) # Randomize GT music too with map shuffle 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 # fix for swamp drains if necessary
swamp1location = world.get_location('Swamp Palace - Trench 1 Pot Key', player) swamp1location = world.get_location('Swamp Palace - Trench 1 Pot Key', player)
if not swamp1location.pot.indicator: 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] 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']): or world.dungeon_counters[player] == 'pickup' or world.pottery[player] not in ['none', 'cave']):
compass_mode = 0x01 # show on pickup compass_mode = 0x01 # show on pickup
if (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]:
or (world.owMixed[player] and not (world.shuffle[player] != 'vanilla' and world.overworld_map[player] == 'default')):
compass_mode |= 0x80 # turn on locating dungeons compass_mode |= 0x80 # turn on locating dungeons
if world.overworld_map[player] == 'compass': if world.overworld_map[player] == 'compass':
compass_mode |= 0x20 # show icon if compass is collected, 0x00 for maps compass_mode |= 0x20 # show icon if compass is collected, 0x00 for maps
@@ -1512,14 +1499,23 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False):
for idx, x_map in enumerate(x_map_position_generic): for idx, x_map in enumerate(x_map_position_generic):
rom.write_bytes(0x53df6+idx*2, int16_as_bytes(x_map)) rom.write_bytes(0x53df6+idx*2, int16_as_bytes(x_map))
rom.write_bytes(0x53e16+idx*2, int16_as_bytes(0xFC0)) 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 # disable HC/AT/GT icons
# rom.write_bytes(0x53E8A, int16_as_bytes(0xFF00)) # GT if not world.owMixed[player]:
# rom.write_bytes(0x53E8C, int16_as_bytes(0xFF00)) # AT 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 rom.write_bytes(0x53E8E, int16_as_bytes(0xFF00)) # HC
for dungeon, portal_list in dungeon_portals.items(): for dungeon, portal_list in dungeon_portals.items():
ow_map_index = dungeon_table[dungeon].map_index ow_map_index = dungeon_table[dungeon].map_index
if world.shuffle[player] != 'vanilla' and world.overworld_map[player] != 'default': 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 world.shuffle[player] != 'vanilla':
if len(portal_list) == 1: if len(portal_list) == 1:
portal_idx = 0 portal_idx = 0
else: else:
@@ -1541,10 +1537,11 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False):
world_indicator = 0x01 if entrance.parent_region.type == RegionType.DarkWorld else 0x00 world_indicator = 0x01 if entrance.parent_region.type == RegionType.DarkWorld else 0x00
coords = ow_prize_table[entrance.name] coords = ow_prize_table[entrance.name]
# figure out compass entrances and what world (light/dark) # 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(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_bytes(0x53E56+ow_map_index*2, int16_as_bytes(coords[1]))
rom.write_byte(0x53EA6+ow_map_index, world_indicator) rom.write_byte(0x53EA6+ow_map_index, world_indicator)
# in crossed doors - flip the compass exists flags # in crossed doors - flip the compass exists flags
if world.doorShuffle[player] == 'crossed': if world.doorShuffle[player] == 'crossed':
for dungeon, portal_list in dungeon_portals.items(): 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: if room.player == player and room.modified:
rom.write_bytes(room.address(), room.rom_data()) 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_strings(rom, world, player, team)
# write initial sram # write initial sram
@@ -2137,8 +2147,6 @@ def write_strings(rom, world, player, team):
else: else:
if isinstance(dest, Region) and dest.type == RegionType.Dungeon and dest.dungeon: if isinstance(dest, Region) and dest.type == RegionType.Dungeon and dest.dungeon:
hint = dest.dungeon.name 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: else:
hint = dest.hint_text if dest.hint_text else "something" hint = dest.hint_text if dest.hint_text else "something"
if dest.player != player: if dest.player != player:
@@ -2325,8 +2333,7 @@ def write_strings(rom, world, player, team):
if this_location: if this_location:
item_name = this_location[0].item.hint_text item_name = this_location[0].item.hint_text
item_name = item_name[0].upper() + item_name[1:] 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_name} can be found {hint_text(this_location[0])}.'
this_hint = f'{item_format} can be found {hint_text(this_location[0])}.'
tt[hint_locations.pop(0)] = this_hint tt[hint_locations.pop(0)] = this_hint
hint_count -= 1 hint_count -= 1
@@ -2380,8 +2387,7 @@ def write_strings(rom, world, player, team):
elif hint_type == 'path': elif hint_type == 'path':
if item_count == 1: if item_count == 1:
the_item = text_for_item(next(iter(choice_set)), world, player, team) 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 {the_item}'))
hint_candidates.append((hint_type, f'{name} conceals only {item_format}'))
else: else:
hint_candidates.append((hint_type, f'{name} conceals {item_count} {item_type} items')) hint_candidates.append((hint_type, f'{name} conceals {item_count} {item_type} items'))
district_hints = min(len(hint_candidates), len(hint_locations)) district_hints = min(len(hint_candidates), len(hint_locations))

View File

@@ -21,12 +21,12 @@ def set_rules(world, player):
global_rules(world, player) global_rules(world, player)
default_rules(world, player) default_rules(world, player)
ow_rules(world, player) ow_inverted_rules(world, player)
ow_bunny_rules(world, player) ow_bunny_rules(world, player)
if world.mode[player] == 'standard': 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) standard_rules(world, player)
elif world.mode[player] == 'open' or world.mode[player] == 'inverted': elif world.mode[player] == 'open' or world.mode[player] == 'inverted':
open_rules(world, player) open_rules(world, player)
@@ -356,8 +356,10 @@ def global_rules(world, player):
# byrna could work with sufficient magic # 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)) 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) 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 if loc.pot:
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.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 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 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)) 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)) add_rule(l, lambda state: state.can_hit_crystal(player))
def default_rules(world, 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)) 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 # Bonk Item Access
if world.shuffle_bonk_drops[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:
from Regions import bonk_prize_table from Regions import bonk_prize_table
for location_name, (_, _, aga_required, _, _, _) in bonk_prize_table.items(): for location_name, (_, _, aga_required, _, _, _) in bonk_prize_table.items():
loc = world.get_location(location_name, player) 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('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 (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('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 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('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)) 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) swordless_rules(world, player)
def ow_rules(world, player): def ow_inverted_rules(world, player):
if world.is_atgt_swapped(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)) set_rule(world.get_entrance('Agahnims Tower', player), lambda state: state.has_crystals(world.crystals_needed_for_gt[player], player))
else: 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 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 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('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', 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)) set_rule(world.get_entrance('Top of Pyramid (Inner)', player), lambda state: state.has_beaten_aga(player))
else: else:
@@ -1481,7 +1482,7 @@ def no_glitches_rules(world, player):
# add_rule(world.get_location(location, player), lambda state: state.has('Hookshot', 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 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) 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) 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)) add_rule(world.get_entrance('Bonk Fairy (Light)', player), lambda state: state.has('Zelda Delivered', player))
if world.shuffle_bonk_drops[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('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)) add_rule(world.get_location('Central Bonk Rocks Tree', player), lambda state: state.has('Zelda Delivered', player))

View File

@@ -45,7 +45,7 @@ def main(args=None):
test("Vanilla ", "--shuffle vanilla") test("Vanilla ", "--shuffle vanilla")
test("Retro ", "--retro --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("Shopsanity", "--shuffle vanilla --shopsanity")
test("Simple ", "--shuffle simple") test("Simple ", "--shuffle simple")
test("Full ", "--shuffle full") test("Full ", "--shuffle full")

Binary file not shown.

View File

@@ -148,7 +148,6 @@
"ow_crossed": { "ow_crossed": {
"choices": [ "choices": [
"none", "none",
"allowed",
"polar", "polar",
"grouped", "grouped",
"limited", "limited",

View File

@@ -217,7 +217,6 @@
"ow_crossed": [ "ow_crossed": [
"This allows cross-world connections to occur on the overworld.", "This allows cross-world connections to occur on the overworld.",
"None: No transitions are cross-world connections.", "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", "Polar: Only used when Mixed is enabled. This retains original",
" connections even when overworld tiles are swapped.", " connections even when overworld tiles are swapped.",
"Limited: Exactly nine transitions are randomly chosen as", "Limited: Exactly nine transitions are randomly chosen as",

View File

@@ -133,7 +133,6 @@
"randomizer.overworld.crossed": "Crossed", "randomizer.overworld.crossed": "Crossed",
"randomizer.overworld.crossed.none": "None", "randomizer.overworld.crossed.none": "None",
"randomizer.overworld.crossed.allowed": "None (Allowed)",
"randomizer.overworld.crossed.polar": "Polar", "randomizer.overworld.crossed.polar": "Polar",
"randomizer.overworld.crossed.grouped": "Grouped", "randomizer.overworld.crossed.grouped": "Grouped",
"randomizer.overworld.crossed.limited": "Limited", "randomizer.overworld.crossed.limited": "Limited",

View File

@@ -20,7 +20,6 @@
"default": "vanilla", "default": "vanilla",
"options": [ "options": [
"none", "none",
"allowed",
"polar", "polar",
"grouped", "grouped",
"limited", "limited",

View File

@@ -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} ", subprocess.run(" ".join([f"pyinstaller {SPEC_FILE} ",
upx_string, upx_string,
"-y ", "-y ",
"--onefile ",
f"--distpath {DEST_DIRECTORY} ", f"--distpath {DEST_DIRECTORY} ",
]), ]),
shell=True) shell=True)

View File

@@ -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} ", subprocess.run(" ".join([f"pyinstaller {SPEC_FILE} ",
upx_string, upx_string,
"-y ", "-y ",
"--onefile ",
f"--distpath {DEST_DIRECTORY} ", f"--distpath {DEST_DIRECTORY} ",
]), ]),
shell=True) shell=True)