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.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))

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
# 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)])

View File

@@ -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

View File

@@ -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

View File

@@ -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
@@ -832,8 +833,6 @@ def connect_mandatory_exits(world, entrances, caves, must_be_exits, player, must
random.shuffle(entrances)
random.shuffle(caves)
from DoorShuffle import find_inaccessible_regions
used_caves = []
required_entrances = 0 # Number of entrances reserved for used_caves
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])
# 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)

99
Fill.py
View File

@@ -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

View File

@@ -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])

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.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,7 +543,6 @@ 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)
@@ -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()

View File

@@ -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):

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 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)
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,14 +635,20 @@ 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
@@ -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]:

View File

@@ -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])),

View File

@@ -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.

View File

@@ -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

58
Rom.py
View File

@@ -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,14 +1499,23 @@ 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 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:
portal_idx = 0
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
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))

View File

@@ -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))

View File

@@ -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")

Binary file not shown.

View File

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

View File

@@ -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",

View File

@@ -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",

View File

@@ -20,7 +20,6 @@
"default": "vanilla",
"options": [
"none",
"allowed",
"polar",
"grouped",
"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} ",
upx_string,
"-y ",
"--onefile ",
f"--distpath {DEST_DIRECTORY} ",
]),
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} ",
upx_string,
"-y ",
"--onefile ",
f"--distpath {DEST_DIRECTORY} ",
]),
shell=True)