Customizer main work

This commit is contained in:
aerinon
2022-03-22 16:13:31 -06:00
parent 97377c9749
commit dfb9ebfbdb
35 changed files with 4599 additions and 688 deletions

View File

@@ -113,7 +113,7 @@ class World(object):
set_player_attr('can_access_trock_front', None)
set_player_attr('can_access_trock_big_chest', None)
set_player_attr('can_access_trock_middle', None)
set_player_attr('fix_fake_world', logic[player] not in ['owglitches', 'nologic'] or shuffle[player] in ['crossed', 'insanity', 'madness_legacy'])
set_player_attr('fix_fake_world', logic[player] not in ['owglitches', 'nologic'] or shuffle[player] in ['crossed', 'insanity'])
set_player_attr('mapshuffle', False)
set_player_attr('compassshuffle', False)
set_player_attr('keyshuffle', False)
@@ -2733,8 +2733,8 @@ class Pot(object):
# byte 0: DDDE EEEE (DR, ER)
dr_mode = {"basic": 1, "crossed": 2, "vanilla": 0}
er_mode = {"vanilla": 0, "simple": 1, "restricted": 2, "full": 3, "crossed": 4, "insanity": 5, "restricted_legacy": 8,
"full_legacy": 9, "madness_legacy": 10, "insanity_legacy": 11, "dungeonsfull": 7, "dungeonssimple": 6}
er_mode = {"vanilla": 0, "simple": 1, "restricted": 2, "full": 3, "crossed": 4, "insanity": 5, 'lite': 8,
'lean': 9, "dungeonsfull": 7, "dungeonssimple": 6}
# byte 1: LLLW WSSR (logic, mode, sword, retro)
logic_mode = {"noglitches": 0, "minorglitches": 1, "nologic": 2, "owglitches": 3, "majorglitches": 4}
@@ -2753,7 +2753,7 @@ mixed_travel_mode = {"prevent": 0, "allow": 1, "force": 2}
# new byte 4: ?DDD PPPP (unused, drop, pottery)
# dropshuffle reserves 2 bits, pottery needs 2 but reserves 2 for future modes)
pottery_mode = {"none": 0, "shuffle": 1, "keys": 2, "lottery": 3, 'dungeon': 4, 'cave': 5}
pottery_mode = {"none": 0, "shuffle": 1, "keys": 2, 'lottery': 3, 'dungeon': 4, 'cave': 5}
# byte 5: CCCC CTTX (crystals gt, ctr2, experimental)
counter_mode = {"default": 0, "off": 1, "on": 2, "pickup": 3}

View File

@@ -165,6 +165,22 @@ def place_bosses(world, player):
all_bosses = sorted(boss_table.keys()) #s orted to be deterministic on older pythons
placeable_bosses = [boss for boss in all_bosses if boss not in ['Agahnim', 'Agahnim2', 'Ganon']]
used_bosses = []
if world.customizer and world.customizer.get_bosses():
custom_bosses = world.customizer.get_bosses()
if player in custom_bosses:
for location, boss in custom_bosses[player].items():
level = None
if '(' in location:
i = location.find('(')
level = location[i+1:location.find(')')]
location = location[:i-1]
if can_place_boss(world, player, boss, location, level):
loc_text = location + (' ('+level+')' if level else '')
place_boss(boss, level, location, loc_text, world, player)
boss_locations.remove([location, level])
used_bosses.append((boss, level))
# temporary hack for swordless kholdstare:
if world.boss_shuffle[player] in ["simple", "full", "unique"]:
@@ -179,6 +195,8 @@ def place_bosses(world, player):
bosses = placeable_bosses + ['Armos Knights', 'Lanmolas', 'Moldorm']
else: # all bosses present, the three duplicates chosen at random
bosses = placeable_bosses + random.sample(placeable_bosses, 3)
for u, level in used_bosses:
placeable_bosses.remove(u)
logging.getLogger('').debug('Bosses chosen %s', bosses)
@@ -202,6 +220,9 @@ def place_bosses(world, player):
place_boss(boss, level, loc, loc_text, world, player)
elif world.boss_shuffle[player] == 'unique':
bosses = list(placeable_bosses)
for u, level in used_bosses:
if not level:
bosses.remove(u)
for [loc, level] in boss_locations:
loc_text = loc + (' ('+level+')' if level else '')

25
CLI.py
View File

@@ -9,6 +9,7 @@ import sys
from source.classes.BabelFish import BabelFish
from Utils import update_deprecated_args
from source.classes.CustomSettings import CustomSettings
class ArgumentDefaultsHelpFormatter(argparse.RawTextHelpFormatter):
@@ -32,11 +33,25 @@ def parse_cli(argv, no_defaults=False):
parser = argparse.ArgumentParser(add_help=False)
parser.add_argument('--settingsfile', help="input json file of settings", type=str)
parser.add_argument('--multi', default=defval(settings["multi"]), type=lambda value: min(max(int(value), 1), 255))
parser.add_argument('--customizer', help='input yaml file for customizations', type=str)
parser.add_argument('--print_custom_yaml', help='print example yaml for current settings',
default=False, action="store_true")
parser.add_argument('--mystery', dest="mystery", default=False, action="store_true")
multiargs, _ = parser.parse_known_args(argv)
if multiargs.settingsfile:
settings = apply_settings_file(settings, multiargs.settingsfile)
player_num = multiargs.multi
if multiargs.customizer:
custom = CustomSettings()
custom.load_yaml(multiargs.customizer)
cp = custom.determine_players()
if cp:
player_num = cp
parser = argparse.ArgumentParser(formatter_class=ArgumentDefaultsHelpFormatter)
# get args
@@ -78,9 +93,11 @@ def parse_cli(argv, no_defaults=False):
parser.add_argument('--securerandom', default=defval(settings["securerandom"]), action='store_true')
parser.add_argument('--teams', default=defval(1), type=lambda value: max(int(value), 1))
parser.add_argument('--settingsfile', dest="filename", help="input json file of settings", type=str)
parser.add_argument('--customizer', dest="customizer", help='input yaml file for customizations', type=str)
parser.add_argument('--print_custom_yaml', dest="print_custom_yaml", default=False, action="store_true")
if multiargs.multi:
for player in range(1, multiargs.multi + 1):
if player_num:
for player in range(1, player_num + 1):
parser.add_argument(f'--p{player}', default=defval(''), help=argparse.SUPPRESS)
ret = parser.parse_args(argv)
@@ -92,9 +109,9 @@ def parse_cli(argv, no_defaults=False):
ret.dropshuffle = True
ret.pottery = 'keys' if ret.pottery == 'none' else ret.pottery
if multiargs.multi:
if player_num:
defaults = copy.deepcopy(ret)
for player in range(1, multiargs.multi + 1):
for player in range(1, player_num + 1):
playerargs = parse_cli(shlex.split(getattr(ret, f"p{player}")), True)
for name in ['logic', 'mode', 'swords', 'goal', 'difficulty', 'item_functionality',

View File

@@ -256,9 +256,23 @@ def convert_key_doors(k_doors, world, player):
def connect_custom(world, player):
if hasattr(world, 'custom_doors') and world.custom_doors[player]:
for entrance, ext in world.custom_doors[player]:
connect_two_way(world, entrance, ext, player)
if world.customizer and world.customizer.get_doors():
custom_doors = world.customizer.get_doors()
if player not in custom_doors:
return
custom_doors = custom_doors[player]
if 'doors' not in custom_doors:
return
for door, dest in custom_doors['doors'].items():
d = world.get_door(door, player)
if d.type not in [DoorType.Interior, DoorType.Logical]:
if isinstance(dest, str):
connect_two_way(world, door, dest, player)
elif 'dest' in dest:
if 'one-way' in dest and dest['one-way']:
connect_one_way(world, door, dest['dest'], player)
else:
connect_two_way(world, door, dest['dest'], player)
def connect_simple_door(world, exit_name, region_name, player):
@@ -422,6 +436,9 @@ def choose_portals(world, player):
master_door_list = [x for x in world.doors if x.player == player and x.portalAble]
portal_assignment = defaultdict(list)
shuffled_info = list(info_map.items())
custom = customizer_portals(master_door_list, world, player)
if cross_flag:
random.shuffle(shuffled_info)
for dungeon, info in shuffled_info:
@@ -436,17 +453,17 @@ def choose_portals(world, player):
info.required_passage[target_region] = [x for x in possible_portals if x != sanc.name]
info.required_passage = {x: y for x, y in info.required_passage.items() if len(y) > 0}
for target_region, possible_portals in info.required_passage.items():
candidates = find_portal_candidates(master_door_list, dungeon, need_passage=True, crossed=cross_flag,
bk_shuffle=bk_shuffle, rupee_bow=rupee_bow_flag)
choice, portal = assign_portal(candidates, possible_portals, world, player)
candidates = find_portal_candidates(master_door_list, dungeon, custom, need_passage=True,
crossed=cross_flag, bk_shuffle=bk_shuffle, rupee_bow=rupee_bow_flag)
choice, portal = assign_portal(candidates, possible_portals, custom, world, player)
portal.destination = True
clean_up_portal_assignment(portal_assignment, dungeon, portal, master_door_list, outstanding_portals)
dead_end_choices = info.total - 1 - len(portal_assignment[dungeon])
for i in range(0, dead_end_choices):
candidates = find_portal_candidates(master_door_list, dungeon, dead_end_allowed=True,
candidates = find_portal_candidates(master_door_list, dungeon, custom, dead_end_allowed=True,
crossed=cross_flag, bk_shuffle=bk_shuffle, rupee_bow=rupee_bow_flag)
possible_portals = outstanding_portals if not info.sole_entrance else [x for x in outstanding_portals if x != info.sole_entrance]
choice, portal = assign_portal(candidates, possible_portals, world, player)
choice, portal = assign_portal(candidates, possible_portals, custom, world, player)
if choice.deadEnd:
if choice.passage:
portal.destination = True
@@ -455,9 +472,9 @@ def choose_portals(world, player):
clean_up_portal_assignment(portal_assignment, dungeon, portal, master_door_list, outstanding_portals)
the_rest = info.total - len(portal_assignment[dungeon])
for i in range(0, the_rest):
candidates = find_portal_candidates(master_door_list, dungeon, crossed=cross_flag,
candidates = find_portal_candidates(master_door_list, dungeon, custom, crossed=cross_flag,
bk_shuffle=bk_shuffle, standard=hc_flag, rupee_bow=rupee_bow_flag)
choice, portal = assign_portal(candidates, outstanding_portals, world, player)
choice, portal = assign_portal(candidates, outstanding_portals, custom, world, player)
clean_up_portal_assignment(portal_assignment, dungeon, portal, master_door_list, outstanding_portals)
for portal in world.dungeon_portals[player]:
@@ -495,6 +512,29 @@ def choose_portals(world, player):
world.swamp_patch_required[player] = True
def customizer_portals(master_door_list, world, player):
custom_portals = {}
assigned_doors = set()
if world.customizer and world.customizer.get_doors():
custom_doors = world.customizer.get_doors()[player]
if custom_doors and 'lobbies' in custom_doors:
for portal, assigned_door in custom_doors['lobbies'].items():
door = next(x for x in master_door_list if x.name == assigned_door)
custom_portals[portal] = door
assigned_doors.add(door)
if custom_doors and 'doors' in custom_doors:
for src_door, dest in custom_doors['doors'].items():
door = world.get_door(src_door, player)
assigned_doors.add(door)
if isinstance(dest, str):
door = world.get_door(dest, player)
assigned_doors.add(door)
else:
door = world.get_door(dest['dest'], player)
assigned_doors.add(door)
return custom_portals, assigned_doors
def analyze_portals(world, player):
info_map = {}
for dungeon, portal_list in dungeon_portals.items():
@@ -576,9 +616,14 @@ def disconnect_portal(portal, world, player):
chosen_door.entranceFlag = False
def find_portal_candidates(door_list, dungeon, need_passage=False, dead_end_allowed=False, crossed=False,
def find_portal_candidates(door_list, dungeon, custom, need_passage=False, dead_end_allowed=False, crossed=False,
bk_shuffle=False, standard=False, rupee_bow=False):
ret = [x for x in door_list if bk_shuffle or not x.bk_shuffle_req]
custom_portals, assigned_doors = custom
if assigned_doors:
ret = [x for x in door_list if x not in assigned_doors]
else:
ret = door_list
ret = [x for x in ret if bk_shuffle or not x.bk_shuffle_req]
if crossed:
ret = [x for x in ret if not x.dungeonLink or x.dungeonLink == dungeon or x.dungeonLink.startswith('link')]
else:
@@ -594,9 +639,13 @@ def find_portal_candidates(door_list, dungeon, need_passage=False, dead_end_allo
return ret
def assign_portal(candidates, possible_portals, world, player):
candidate = random.choice(candidates)
def assign_portal(candidates, possible_portals, custom, world, player):
custom_portals, assigned_doors = custom
portal_choice = random.choice(possible_portals)
if portal_choice in custom_portals:
candidate = custom_portals[portal_choice]
else:
candidate = random.choice(candidates)
portal = world.get_portal(portal_choice, player)
while candidate.lw_restricted and not portal.light_world:
candidates.remove(candidate)
@@ -723,6 +772,7 @@ def within_dungeon(world, player):
paths = determine_required_paths(world, player)
check_required_paths(paths, world, player)
setup_custom_door_types(world, player)
# shuffle_key_doors for dungeons
logging.getLogger('').info(world.fish.translate("cli", "cli", "shuffling.keydoors"))
start = time.process_time()
@@ -994,6 +1044,7 @@ def cross_dungeon(world, player):
at.dungeon_items.append(ItemFactory('Compass (Agahnims Tower)', player))
at.dungeon_items.append(ItemFactory('Map (Agahnims Tower)', player))
setup_custom_door_types(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))
target_items = 34
@@ -1058,6 +1109,29 @@ def cross_dungeon(world, player):
refine_boss_exits(world, player)
def filter_key_door_pool(pool, selected_custom):
new_pool = []
for cand in pool:
found = False
for custom in selected_custom:
if isinstance(cand, Door):
if isinstance(custom, Door):
found = cand.name == custom.name
else:
found = cand.name == custom[0].name or cand.name == custom[1].name
else:
if isinstance(custom, Door):
found = cand[0].name == custom.name or cand[1].name == custom.name
else:
found = (cand[0].name == custom[0].name or cand[0].name == custom[1].name
or cand[1].name == custom[0].name or cand[1].name == custom[1].name)
if found:
break
if not found:
new_pool.append(cand)
return new_pool
def assign_cross_keys(dungeon_builders, world, player):
logging.getLogger('').info(world.fish.translate("cli", "cli", "shuffling.keydoors"))
start = time.process_time()
@@ -1069,9 +1143,13 @@ def assign_cross_keys(dungeon_builders, world, player):
remaining += 19
else:
remaining = len(list(x for dgn in world.dungeons if dgn.player == player for x in dgn.small_keys))
total_keys = remaining
total_candidates = 0
start_regions_map = {}
if player in world.custom_door_types:
custom_key_doors = world.custom_door_types[player]['Key Door']
else:
custom_key_doors = defaultdict(list)
key_door_pool, key_doors_assigned = {}, {}
# Step 1: Find Small Key Door Candidates
for name, builder in dungeon_builders.items():
dungeon = world.get_dungeon(name, player)
@@ -1081,22 +1159,27 @@ def assign_cross_keys(dungeon_builders, world, player):
dungeon.big_key = ItemFactory(dungeon_bigs[name], player)
start_regions = convert_regions(builder.path_entrances, world, player)
find_small_key_door_candidates(builder, start_regions, world, player)
builder.key_doors_num = max(0, len(builder.candidates) - builder.key_drop_cnt)
key_door_pool[name] = list(builder.candidates)
if custom_key_doors[name]:
key_door_pool[name] = filter_key_door_pool(key_door_pool[name], custom_key_doors[name])
remaining -= len(custom_key_doors[name])
builder.key_doors_num = max(0, len(key_door_pool[name]) - builder.key_drop_cnt)
total_candidates += builder.key_doors_num
start_regions_map[name] = start_regions
total_keys = remaining
# Step 2: Initial Key Number Assignment & Calculate Flexibility
for name, builder in dungeon_builders.items():
calculated = int(round(builder.key_doors_num*total_keys/total_candidates))
max_keys = builder.location_cnt - calc_used_dungeon_items(builder)
cand_len = max(0, len(builder.candidates) - builder.key_drop_cnt)
max_keys = max(0, builder.location_cnt - calc_used_dungeon_items(builder, world, player))
cand_len = max(0, len(key_door_pool[name]) - builder.key_drop_cnt)
limit = min(max_keys, cand_len)
suggested = min(calculated, limit)
combo_size = ncr(len(builder.candidates), suggested + builder.key_drop_cnt)
combo_size = ncr(len(key_door_pool[name]), suggested + builder.key_drop_cnt)
while combo_size > 500000 and suggested > 0:
suggested -= 1
combo_size = ncr(len(builder.candidates), suggested + builder.key_drop_cnt)
builder.key_doors_num = suggested + builder.key_drop_cnt
combo_size = ncr(len(key_door_pool[name]), suggested + builder.key_drop_cnt)
builder.key_doors_num = suggested + builder.key_drop_cnt + len(custom_key_doors[name])
remaining -= suggested
builder.combo_size = combo_size
if suggested < limit:
@@ -1104,7 +1187,7 @@ def assign_cross_keys(dungeon_builders, world, player):
# Step 3: Initial valid combination find - reduce flex if needed
for name, builder in dungeon_builders.items():
suggested = builder.key_doors_num - builder.key_drop_cnt
suggested = builder.key_doors_num - builder.key_drop_cnt - len(custom_key_doors[name])
builder.total_keys = builder.key_doors_num
find_valid_combination(builder, start_regions_map[name], world, player)
actual_chest_keys = builder.key_doors_num - builder.key_drop_cnt
@@ -1161,7 +1244,7 @@ def reassign_boss(boss_region, boss_key, builder, gt, world, player):
def check_entrance_fixes(world, player):
# I believe these modes will be fine
if world.shuffle[player] not in ['insanity', 'insanity_legacy', 'madness_legacy']:
if world.shuffle[player] not in ['insanity']:
checks = {
'Palace of Darkness': 'pod',
'Skull Woods Final Section': 'sw',
@@ -1402,6 +1485,39 @@ def valid_region_to_explore(region, world, player):
or (region.name == 'Hyrule Castle Ledge' and world.mode[player] == 'standard'))
def setup_custom_door_types(world, player):
if not hasattr(world, 'custom_door_types'):
world.custom_door_types = defaultdict(dict)
if world.customizer and world.customizer.get_doors():
# type_conv = {'Bomb Door': DoorKind.Bombable , 'Dash Door', DoorKind.Dashable, 'Key Door', DoorKind.SmallKey}
custom_doors = world.customizer.get_doors()
if player not in custom_doors:
return
custom_doors = custom_doors[player]
if 'doors' not in custom_doors:
return
world.custom_door_types[player] = type_map = {'Key Door': defaultdict(list), 'Dash Door': [], 'Bomb Door': []}
for door, dest in custom_doors['doors'].items():
if isinstance(dest, dict):
if 'type' in dest:
door_kind = dest['type']
d = world.get_door(door, player)
dungeon = d.entrance.parent_region.dungeon
if d.type == DoorType.SpiralStairs:
type_map[door_kind][dungeon.name].append(d)
elif door_kind == 'Key Door':
# check if the
if d.dest.type in [DoorType.Interior, DoorType.Normal]:
type_map[door_kind][dungeon.name].append((d, d.dest))
else:
type_map[door_kind][dungeon.name].append(d)
else:
if d.dest.type in [DoorType.Interior, DoorType.Normal]:
type_map[door_kind].append((d, d.dest))
else:
type_map[door_kind].append(d)
def shuffle_key_doors(builder, world, player):
start_regions = convert_regions(builder.path_entrances, world, player)
# count number of key doors - this could be a table?
@@ -1458,34 +1574,44 @@ def find_small_key_door_candidates(builder, start_regions, world, player):
builder.candidates = paired_candidates
def calc_used_dungeon_items(builder):
base = 4
def calc_used_dungeon_items(builder, world, player):
base = 2
if not world.bigkeyshuffle[player]:
if builder.bk_required and not builder.bk_provided:
base += 1
# if builder.name == 'Hyrule Castle':
# base -= 1 # Missing compass/map
# if builder.name == 'Agahnims Tower':
# base -= 2 # Missing both compass/map
# gt can lose map once compasses work
if not world.compassshuffle[player]:
base += 1
if not world.mapshuffle[player]:
base += 1
return base
def find_valid_combination(builder, start_regions, world, player, drop_keys=True):
logger = logging.getLogger('')
key_door_pool = list(builder.candidates) # can these be a set?
key_doors_needed = builder.key_doors_num
if player in world.custom_door_types:
custom_key_doors = world.custom_door_types[player]['Key Door'][builder.name]
else:
custom_key_doors = []
if custom_key_doors: # could validate that each custom item is in the candidates
key_door_pool = filter_key_door_pool(key_door_pool, custom_key_doors)
key_doors_needed -= len(custom_key_doors)
# find valid combination of candidates
if len(builder.candidates) < builder.key_doors_num:
if len(key_door_pool) < key_doors_needed:
if not drop_keys:
logger.info('No valid layouts for %s with %s doors', builder.name, builder.key_doors_num)
return False
builder.key_doors_num = len(builder.candidates) # reduce number of key doors
logger.info('%s: %s', world.fish.translate("cli","cli","lowering.keys.candidates"), builder.name)
combinations = ncr(len(builder.candidates), builder.key_doors_num)
builder.key_doors_num -= key_doors_needed - len(key_door_pool) # reduce number of key doors
logger.info('%s: %s', world.fish.translate("cli", "cli", "lowering.keys.candidates"), builder.name)
combinations = ncr(len(key_door_pool), key_doors_needed)
itr = 0
start = time.process_time()
sample_list = list(range(0, int(combinations)))
random.shuffle(sample_list)
proposal = kth_combination(sample_list[itr], builder.candidates, builder.key_doors_num)
proposal = kth_combination(sample_list[itr], key_door_pool, key_doors_needed)
proposal.extend(custom_key_doors)
# eliminate start region if portal marked as destination
excluded = {}
for region in start_regions:
@@ -1509,14 +1635,16 @@ def find_valid_combination(builder, start_regions, world, player, drop_keys=True
return False
logger.info('%s: %s', world.fish.translate("cli","cli","lowering.keys.layouts"), builder.name)
builder.key_doors_num -= 1
key_doors_needed -= 1
if builder.key_doors_num < 0:
raise Exception('Bad dungeon %s - 0 key doors not valid' % builder.name)
combinations = ncr(len(builder.candidates), builder.key_doors_num)
raise Exception('Bad dungeon %s - less than 0 key doors not valid' % builder.name)
combinations = ncr(len(key_door_pool), max(0, key_doors_needed))
sample_list = list(range(0, int(combinations)))
random.shuffle(sample_list)
itr = 0
start = time.process_time() # reset time since itr reset
proposal = kth_combination(sample_list[itr], builder.candidates, builder.key_doors_num)
proposal = kth_combination(sample_list[itr], key_door_pool, key_doors_needed)
proposal.extend(custom_key_doors)
key_layout.reset(proposal, builder, world, player)
if (itr+1) % 1000 == 0:
mark = time.process_time()-start
@@ -1710,15 +1838,31 @@ def change_door_to_small_key(d, world, player):
d.smallKey = True
room = world.get_room(d.roomIndex, player)
if room.doorList[d.doorListPos][1] != DoorKind.SmallKey:
verify_door_list_pos(d, room, world, player)
room.change(d.doorListPos, DoorKind.SmallKey)
def verify_door_list_pos(d, room, world, player):
if d.doorListPos >= 4:
new_index = room.next_free()
if new_index is not None:
room.swap(new_index, d.doorListPos)
other = next(x for x in world.doors if x.player == player and x.roomIndex == d.roomIndex
and x.doorListPos == new_index)
other.doorListPos = d.doorListPos
d.doorListPos = new_index
else:
raise Exception(f'Invalid stateful door: {d.name}. Only 4 stateful doors per supertile')
def smooth_door_pairs(world, player):
all_doors = [x for x in world.doors if x.player == player]
skip = set()
bd_candidates = defaultdict(list)
for door in all_doors:
if door.type in [DoorType.Normal, DoorType.Interior] and door not in skip and not door.entranceFlag:
if not door.dest:
continue
partner = door.dest
skip.add(partner)
room_a = world.get_room(door.roomIndex, player)
@@ -1791,15 +1935,33 @@ def stateful_door(door, kind):
return False
def custom_door_kind(custom_key, kind, bd_candidates, counts, world, player):
if custom_key in world.custom_door_types[player]:
for door_a, door_b in world.custom_door_types[player][custom_key]:
change_pair_type(door_a, kind, world, player)
d_name = door_a.entrance.parent_region.dungeon.name
bd_list = next(bd_list for dungeon, bd_list in bd_candidates.items() if dungeon.name == d_name)
if door_a in bd_list:
bd_list.remove(door_a)
if door_b in bd_list:
bd_list.remove(door_b)
counts[d_name] += 1
def shuffle_bombable_dashable(bd_candidates, world, player):
dash_counts = defaultdict(int)
bomb_counts = defaultdict(int)
if world.custom_door_types[player]:
custom_door_kind('Dash Door', DoorKind.Dashable, bd_candidates, dash_counts, world, player)
custom_door_kind('Bomb Door', DoorKind.Bombable, bd_candidates, bomb_counts, world, player)
if world.doorShuffle[player] == 'basic':
for dungeon, candidates in bd_candidates.items():
diff = bomb_dash_counts[dungeon.name][1]
diff = bomb_dash_counts[dungeon.name][1] - dash_counts[dungeon.name]
if diff > 0:
for chosen in random.sample(candidates, min(diff, len(candidates))):
change_pair_type(chosen, DoorKind.Dashable, world, player)
candidates.remove(chosen)
diff = bomb_dash_counts[dungeon.name][0]
diff = bomb_dash_counts[dungeon.name][0] - bomb_counts[dungeon.name]
if diff > 0:
for chosen in random.sample(candidates, min(diff, len(candidates))):
change_pair_type(chosen, DoorKind.Bombable, world, player)
@@ -1808,10 +1970,14 @@ def shuffle_bombable_dashable(bd_candidates, world, player):
remove_pair_type_if_present(excluded, world, player)
elif world.doorShuffle[player] == 'crossed':
all_candidates = sum(bd_candidates.values(), [])
for chosen in random.sample(all_candidates, min(8, len(all_candidates))):
desired_dashables = 8 - sum(dash_counts.values(), 0)
desired_bombables = 12 - sum(bomb_counts.values(), 0)
if desired_dashables > 0:
for chosen in random.sample(all_candidates, min(desired_dashables, len(all_candidates))):
change_pair_type(chosen, DoorKind.Dashable, world, player)
all_candidates.remove(chosen)
for chosen in random.sample(all_candidates, min(12, len(all_candidates))):
if desired_bombables > 0:
for chosen in random.sample(all_candidates, min(desired_bombables, len(all_candidates))):
change_pair_type(chosen, DoorKind.Bombable, world, player)
all_candidates.remove(chosen)
for excluded in all_candidates:
@@ -1820,9 +1986,11 @@ def shuffle_bombable_dashable(bd_candidates, world, player):
def change_pair_type(door, new_type, world, player):
room_a = world.get_room(door.roomIndex, player)
verify_door_list_pos(door, room_a, world, player)
room_a.change(door.doorListPos, new_type)
if door.type != DoorType.Interior:
room_b = world.get_room(door.dest.roomIndex, player)
verify_door_list_pos(door.dest, room_b, world, player)
room_b.change(door.dest.doorListPos, new_type)
add_pair(door, door.dest, world, player)
spoiler_type = 'Bomb Door' if new_type == DoorKind.Bombable else 'Dash Door'

View File

@@ -1286,6 +1286,10 @@ def create_dungeon_builders(all_sectors, connections_tuple, world, player,
complete_dungeons = {x: y for x, y in dungeon_map.items() if sum(len(sector.outstanding_doors) for sector in y.sectors) <= 0}
[dungeon_map.pop(key) for key in complete_dungeons.keys()]
if not dungeon_map:
dungeon_map.update(complete_dungeons)
return dungeon_map
# categorize sectors
identify_destination_sectors(accessible_sectors, reverse_d_map, dungeon_map, connections,
dungeon_entrances, split_dungeon_entrances)
@@ -1398,6 +1402,8 @@ def identify_destination_sectors(accessible_sectors, reverse_d_map, dungeon_map,
explored = False
else:
d_name = reverse_d_map[sector]
if d_name not in dungeon_map:
return
if d_name not in split_dungeon_entrances:
for r_name in dungeon_entrances[d_name]:
ent_sector = find_sector(r_name, dungeon_map[d_name].sectors)
@@ -2953,6 +2959,8 @@ def split_dungeon_builder(builder, split_list, builder_info):
sub_builder.all_entrances = list(split_entrances)
for r_name in split_entrances:
assign_sector(find_sector(r_name, candidate_sectors), sub_builder, candidate_sectors, global_pole)
if len(candidate_sectors) == 0:
return dungeon_map
comb_w_replace = len(dungeon_map) ** len(candidate_sectors)
return balance_split(candidate_sectors, dungeon_map, global_pole, builder_info)
except (GenerationException, NeutralizingException):

View File

@@ -245,61 +245,6 @@ def link_entrances(world, player):
# place remaining doors
connect_doors(world, doors, door_targets, player)
elif world.shuffle[player] == 'restricted_legacy':
simple_shuffle_dungeons(world, player)
lw_entrances = list(LW_Entrances)
dw_entrances = list(DW_Entrances)
dw_must_exits = list(DW_Entrances_Must_Exit)
old_man_entrances = list(Old_Man_Entrances)
caves = list(Cave_Exits)
three_exit_caves = list(Cave_Three_Exits)
single_doors = list(Single_Cave_Doors)
bomb_shop_doors = list(Bomb_Shop_Single_Cave_Doors)
blacksmith_doors = list(Blacksmith_Single_Cave_Doors)
door_targets = list(Single_Cave_Targets)
# only use two exit caves to do mandatory dw connections
connect_mandatory_exits(world, dw_entrances, caves, dw_must_exits, player)
# add three exit doors to pool for remainder
caves.extend(three_exit_caves)
# place old man, has limited options
# exit has to come from specific set of doors, the entrance is free to move about
random.shuffle(old_man_entrances)
old_man_exit = old_man_entrances.pop()
lw_entrances.extend(old_man_entrances)
random.shuffle(lw_entrances)
old_man_entrance = lw_entrances.pop()
connect_two_way(world, old_man_entrance, 'Old Man Cave Exit (West)', player)
connect_two_way(world, old_man_exit, 'Old Man Cave Exit (East)', player)
# place Old Man House in Light World
connect_caves(world, lw_entrances, [], Old_Man_House, player)
# connect rest. There's 2 dw entrances remaining, so we will not run into parity issue placing caves
connect_caves(world, lw_entrances, dw_entrances, caves, player)
# scramble holes
scramble_holes(world, player)
# place blacksmith, has limited options
random.shuffle(blacksmith_doors)
blacksmith_hut = blacksmith_doors.pop()
connect_entrance(world, blacksmith_hut, 'Blacksmiths Hut', player)
bomb_shop_doors.extend(blacksmith_doors)
# place dam and pyramid fairy, have limited options
random.shuffle(bomb_shop_doors)
bomb_shop = bomb_shop_doors.pop()
connect_entrance(world, bomb_shop, 'Big Bomb Shop', player)
single_doors.extend(bomb_shop_doors)
# tavern back door cannot be shuffled yet
connect_doors(world, ['Tavern North'], ['Tavern'], player)
# place remaining doors
connect_doors(world, single_doors, door_targets, player)
elif world.shuffle[player] == 'full':
skull_woods_shuffle(world, player)
@@ -532,324 +477,6 @@ def link_entrances(world, player):
# place remaining doors
connect_doors(world, entrances, door_targets, player)
elif world.shuffle[player] == 'full_legacy':
skull_woods_shuffle(world, player)
lw_entrances = list(LW_Entrances + LW_Dungeon_Entrances + Old_Man_Entrances)
dw_entrances = list(DW_Entrances + DW_Dungeon_Entrances)
dw_must_exits = list(DW_Entrances_Must_Exit + DW_Dungeon_Entrances_Must_Exit)
lw_must_exits = list(LW_Dungeon_Entrances_Must_Exit)
old_man_entrances = list(Old_Man_Entrances + ['Tower of Hera'])
caves = list(Cave_Exits + Dungeon_Exits + Cave_Three_Exits) # don't need to consider three exit caves, have one exit caves to avoid parity issues
single_doors = list(Single_Cave_Doors)
bomb_shop_doors = list(Bomb_Shop_Single_Cave_Doors)
blacksmith_doors = list(Blacksmith_Single_Cave_Doors)
door_targets = list(Single_Cave_Targets)
if world.mode[player] == 'standard':
# must connect front of hyrule castle to do escape
connect_two_way(world, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player)
else:
caves.append(tuple(random.sample(['Hyrule Castle Exit (South)', 'Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)'],3)))
lw_entrances.append('Hyrule Castle Entrance (South)')
if not world.shuffle_ganon:
connect_two_way(world, 'Ganons Tower', 'Ganons Tower Exit', player)
else:
dw_entrances.append('Ganons Tower')
caves.append('Ganons Tower Exit')
# we randomize which world requirements we fulfill first so we get better dungeon distribution
if random.randint(0, 1) == 0:
connect_mandatory_exits(world, lw_entrances, caves, lw_must_exits, player)
connect_mandatory_exits(world, dw_entrances, caves, dw_must_exits, player)
else:
connect_mandatory_exits(world, dw_entrances, caves, dw_must_exits, player)
connect_mandatory_exits(world, lw_entrances, caves, lw_must_exits, player)
if world.mode[player] == 'standard':
# rest of hyrule castle must be in light world
connect_caves(world, lw_entrances, [], [('Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)')], player)
# place old man, has limited options
# exit has to come from specific set of doors, the entrance is free to move about
old_man_entrances = [door for door in old_man_entrances if door in lw_entrances]
random.shuffle(old_man_entrances)
old_man_exit = old_man_entrances.pop()
lw_entrances.remove(old_man_exit)
random.shuffle(lw_entrances)
old_man_entrance = lw_entrances.pop()
connect_two_way(world, old_man_entrance, 'Old Man Cave Exit (West)', player)
connect_two_way(world, old_man_exit, 'Old Man Cave Exit (East)', player)
# place Old Man House in Light World
connect_caves(world, lw_entrances, [], list(Old_Man_House), player) #need this to avoid badness with multiple seeds
# now scramble the rest
connect_caves(world, lw_entrances, dw_entrances, caves, player)
# scramble holes
scramble_holes(world, player)
# place blacksmith, has limited options
random.shuffle(blacksmith_doors)
blacksmith_hut = blacksmith_doors.pop()
connect_entrance(world, blacksmith_hut, 'Blacksmiths Hut', player)
bomb_shop_doors.extend(blacksmith_doors)
# place bomb shop, has limited options
random.shuffle(bomb_shop_doors)
bomb_shop = bomb_shop_doors.pop()
connect_entrance(world, bomb_shop, 'Big Bomb Shop', player)
single_doors.extend(bomb_shop_doors)
# tavern back door cannot be shuffled yet
connect_doors(world, ['Tavern North'], ['Tavern'], player)
# place remaining doors
connect_doors(world, single_doors, door_targets, player)
elif world.shuffle[player] == 'madness_legacy':
# here lie dragons, connections are no longer two way
lw_entrances = list(LW_Entrances + LW_Dungeon_Entrances + Old_Man_Entrances)
dw_entrances = list(DW_Entrances + DW_Dungeon_Entrances)
dw_entrances_must_exits = list(DW_Entrances_Must_Exit + DW_Dungeon_Entrances_Must_Exit)
lw_doors = list(LW_Entrances + LW_Dungeon_Entrances + LW_Dungeon_Entrances_Must_Exit) + ['Kakariko Well Cave', 'Bat Cave Cave', 'North Fairy Cave', 'Sanctuary', 'Lost Woods Hideout Stump',
'Lumberjack Tree Cave'] + list(Old_Man_Entrances)
dw_doors = list(DW_Entrances + DW_Dungeon_Entrances + DW_Entrances_Must_Exit + DW_Dungeon_Entrances_Must_Exit) + ['Skull Woods First Section Door', 'Skull Woods Second Section Door (East)', 'Skull Woods Second Section Door (West)']
random.shuffle(lw_doors)
random.shuffle(dw_doors)
dw_entrances_must_exits.append('Skull Woods Second Section Door (West)')
dw_entrances.append('Skull Woods Second Section Door (East)')
dw_entrances.append('Skull Woods First Section Door')
lw_entrances.extend(['Kakariko Well Cave', 'Bat Cave Cave', 'North Fairy Cave', 'Sanctuary', 'Lost Woods Hideout Stump', 'Lumberjack Tree Cave'])
lw_entrances_must_exits = list(LW_Dungeon_Entrances_Must_Exit)
old_man_entrances = list(Old_Man_Entrances) + ['Tower of Hera']
mandatory_light_world = ['Old Man House Exit (Bottom)', 'Old Man House Exit (Top)']
mandatory_dark_world = []
caves = list(Cave_Exits + Dungeon_Exits + Cave_Three_Exits)
# shuffle up holes
lw_hole_entrances = ['Kakariko Well Drop', 'Bat Cave Drop', 'North Fairy Cave Drop', 'Lost Woods Hideout Drop', 'Lumberjack Tree Tree', 'Sanctuary Grave']
dw_hole_entrances = ['Skull Woods First Section Hole (East)', 'Skull Woods First Section Hole (West)', 'Skull Woods First Section Hole (North)', 'Skull Woods Second Section Hole']
hole_targets = [('Kakariko Well Exit', 'Kakariko Well (top)'),
('Bat Cave Exit', 'Bat Cave (right)'),
('North Fairy Cave Exit', 'North Fairy Cave'),
('Lost Woods Hideout Exit', 'Lost Woods Hideout (top)'),
('Lumberjack Tree Exit', 'Lumberjack Tree (top)'),
(('Skull Woods Second Section Exit (East)', 'Skull Woods Second Section Exit (West)'), 'Skull Back Drop')]
if world.mode[player] == 'standard':
# cannot move uncle cave
connect_entrance(world, 'Hyrule Castle Secret Entrance Drop', 'Hyrule Castle Secret Entrance', player)
connect_exit(world, 'Hyrule Castle Secret Entrance Exit', 'Hyrule Castle Secret Entrance Stairs', player)
connect_entrance(world, 'Hyrule Castle Secret Entrance Stairs', 'Hyrule Castle Secret Entrance Exit', player)
else:
lw_hole_entrances.append('Hyrule Castle Secret Entrance Drop')
hole_targets.append(('Hyrule Castle Secret Entrance Exit', 'Hyrule Castle Secret Entrance'))
lw_doors.append('Hyrule Castle Secret Entrance Stairs')
lw_entrances.append('Hyrule Castle Secret Entrance Stairs')
if not world.shuffle_ganon:
connect_two_way(world, 'Ganons Tower', 'Ganons Tower Exit', player)
connect_two_way(world, 'Pyramid Entrance', 'Pyramid Exit', player)
connect_entrance(world, 'Pyramid Hole', 'Pyramid', player)
else:
dw_entrances.append('Ganons Tower')
caves.append('Ganons Tower Exit')
dw_hole_entrances.append('Pyramid Hole')
hole_targets.append(('Pyramid Exit', 'Pyramid'))
dw_entrances_must_exits.append('Pyramid Entrance')
dw_doors.extend(['Ganons Tower', 'Pyramid Entrance'])
random.shuffle(lw_hole_entrances)
random.shuffle(dw_hole_entrances)
random.shuffle(hole_targets)
# decide if skull woods first section should be in light or dark world
sw_light = random.randint(0, 1) == 0
if sw_light:
sw_hole_pool = lw_hole_entrances
mandatory_light_world.append('Skull Woods First Section Exit')
else:
sw_hole_pool = dw_hole_entrances
mandatory_dark_world.append('Skull Woods First Section Exit')
for target in ['Skull Left Drop', 'Skull Pinball', 'Skull Pot Circle']:
connect_entrance(world, sw_hole_pool.pop(), target, player)
# sanctuary has to be in light world
connect_entrance(world, lw_hole_entrances.pop(), 'Sewer Drop', player)
mandatory_light_world.append('Sanctuary Exit')
# fill up remaining holes
for hole in dw_hole_entrances:
exits, target = hole_targets.pop()
mandatory_dark_world.append(exits)
connect_entrance(world, hole, target, player)
for hole in lw_hole_entrances:
exits, target = hole_targets.pop()
mandatory_light_world.append(exits)
connect_entrance(world, hole, target, player)
# hyrule castle handling
if world.mode[player] == 'standard':
# must connect front of hyrule castle to do escape
connect_entrance(world, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player)
connect_exit(world, 'Hyrule Castle Exit (South)', 'Hyrule Castle Entrance (South)', player)
mandatory_light_world.append(('Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)'))
else:
lw_doors.append('Hyrule Castle Entrance (South)')
lw_entrances.append('Hyrule Castle Entrance (South)')
caves.append(('Hyrule Castle Exit (South)', 'Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)'))
# now let's deal with mandatory reachable stuff
def extract_reachable_exit(cavelist):
random.shuffle(cavelist)
candidate = None
for cave in cavelist:
if isinstance(cave, tuple) and len(cave) > 1:
# special handling: TRock and Spectracle Rock cave have two entries that we should consider entrance only
# ToDo this should be handled in a more sensible manner
if cave[0] in ['Turtle Rock Exit (Front)', 'Spectacle Rock Cave Exit (Peak)'] and len(cave) == 2:
continue
candidate = cave
break
if candidate is None:
raise RuntimeError('No suitable cave.')
cavelist.remove(candidate)
return candidate
def connect_reachable_exit(entrance, general, worldspecific, worldoors):
# select which one is the primary option
if random.randint(0, 1) == 0:
primary = general
secondary = worldspecific
else:
primary = worldspecific
secondary = general
try:
cave = extract_reachable_exit(primary)
except RuntimeError:
cave = extract_reachable_exit(secondary)
exit = cave[-1]
cave = cave[:-1]
connect_exit(world, exit, entrance, player)
connect_entrance(world, worldoors.pop(), exit, player)
# rest of cave now is forced to be in this world
worldspecific.append(cave)
# we randomize which world requirements we fulfill first so we get better dungeon distribution
if random.randint(0, 1) == 0:
for entrance in lw_entrances_must_exits:
connect_reachable_exit(entrance, caves, mandatory_light_world, lw_doors)
for entrance in dw_entrances_must_exits:
connect_reachable_exit(entrance, caves, mandatory_dark_world, dw_doors)
else:
for entrance in dw_entrances_must_exits:
connect_reachable_exit(entrance, caves, mandatory_dark_world, dw_doors)
for entrance in lw_entrances_must_exits:
connect_reachable_exit(entrance, caves, mandatory_light_world, lw_doors)
# place old man, has limited options
# exit has to come from specific set of doors, the entrance is free to move about
old_man_entrances = [entrance for entrance in old_man_entrances if entrance in lw_entrances]
random.shuffle(old_man_entrances)
old_man_exit = old_man_entrances.pop()
lw_entrances.remove(old_man_exit)
connect_exit(world, 'Old Man Cave Exit (East)', old_man_exit, player)
connect_entrance(world, lw_doors.pop(), 'Old Man Cave Exit (East)', player)
mandatory_light_world.append('Old Man Cave Exit (West)')
# we connect up the mandatory associations we have found
for mandatory in mandatory_light_world:
if not isinstance(mandatory, tuple):
mandatory = (mandatory,)
for exit in mandatory:
# point out somewhere
connect_exit(world, exit, lw_entrances.pop(), player)
# point in from somewhere
connect_entrance(world, lw_doors.pop(), exit, player)
for mandatory in mandatory_dark_world:
if not isinstance(mandatory, tuple):
mandatory = (mandatory,)
for exit in mandatory:
# point out somewhere
connect_exit(world, exit, dw_entrances.pop(), player)
# point in from somewhere
connect_entrance(world, dw_doors.pop(), exit, player)
# handle remaining caves
while caves:
# connect highest exit count caves first, prevent issue where we have 2 or 3 exits across worlds left to fill
cave_candidate = (None, 0)
for i, cave in enumerate(caves):
if isinstance(cave, str):
cave = (cave,)
if len(cave) > cave_candidate[1]:
cave_candidate = (i, len(cave))
cave = caves.pop(cave_candidate[0])
place_lightworld = random.randint(0, 1) == 0
if place_lightworld:
target_doors = lw_doors
target_entrances = lw_entrances
else:
target_doors = dw_doors
target_entrances = dw_entrances
if isinstance(cave, str):
cave = (cave,)
# check if we can still fit the cave into our target group
if len(target_doors) < len(cave):
if not place_lightworld:
target_doors = lw_doors
target_entrances = lw_entrances
else:
target_doors = dw_doors
target_entrances = dw_entrances
for exit in cave:
connect_exit(world, exit, target_entrances.pop(), player)
connect_entrance(world, target_doors.pop(), exit, player)
# handle simple doors
single_doors = list(Single_Cave_Doors)
bomb_shop_doors = list(Bomb_Shop_Single_Cave_Doors)
blacksmith_doors = list(Blacksmith_Single_Cave_Doors)
door_targets = list(Single_Cave_Targets)
# place blacksmith, has limited options
random.shuffle(blacksmith_doors)
blacksmith_hut = blacksmith_doors.pop()
connect_entrance(world, blacksmith_hut, 'Blacksmiths Hut', player)
bomb_shop_doors.extend(blacksmith_doors)
# place dam and pyramid fairy, have limited options
random.shuffle(bomb_shop_doors)
bomb_shop = bomb_shop_doors.pop()
connect_entrance(world, bomb_shop, 'Big Bomb Shop', player)
single_doors.extend(bomb_shop_doors)
# tavern back door cannot be shuffled yet
connect_doors(world, ['Tavern North'], ['Tavern'], player)
# place remaining doors
connect_doors(world, single_doors, door_targets, player)
elif world.shuffle[player] == 'insanity':
# beware ye who enter here
@@ -1023,149 +650,6 @@ def link_entrances(world, player):
# place remaining doors
connect_doors(world, doors, door_targets, player)
elif world.shuffle[player] == 'insanity_legacy':
world.fix_fake_world[player] = False
# beware ye who enter here
entrances = LW_Entrances + LW_Dungeon_Entrances + DW_Entrances + DW_Dungeon_Entrances + Old_Man_Entrances + ['Skull Woods Second Section Door (East)', 'Skull Woods First Section Door', 'Kakariko Well Cave', 'Bat Cave Cave', 'North Fairy Cave', 'Sanctuary', 'Lost Woods Hideout Stump', 'Lumberjack Tree Cave']
entrances_must_exits = DW_Entrances_Must_Exit + DW_Dungeon_Entrances_Must_Exit + LW_Dungeon_Entrances_Must_Exit + ['Skull Woods Second Section Door (West)']
doors = LW_Entrances + LW_Dungeon_Entrances + LW_Dungeon_Entrances_Must_Exit + ['Kakariko Well Cave', 'Bat Cave Cave', 'North Fairy Cave', 'Sanctuary', 'Lost Woods Hideout Stump', 'Lumberjack Tree Cave'] + Old_Man_Entrances +\
DW_Entrances + DW_Dungeon_Entrances + DW_Entrances_Must_Exit + DW_Dungeon_Entrances_Must_Exit + ['Skull Woods First Section Door', 'Skull Woods Second Section Door (East)', 'Skull Woods Second Section Door (West)']
random.shuffle(doors)
old_man_entrances = list(Old_Man_Entrances) + ['Tower of Hera']
caves = Cave_Exits + Dungeon_Exits + Cave_Three_Exits + ['Old Man House Exit (Bottom)', 'Old Man House Exit (Top)', 'Skull Woods First Section Exit', 'Skull Woods Second Section Exit (East)', 'Skull Woods Second Section Exit (West)',
'Kakariko Well Exit', 'Bat Cave Exit', 'North Fairy Cave Exit', 'Lost Woods Hideout Exit', 'Lumberjack Tree Exit', 'Sanctuary Exit']
# shuffle up holes
hole_entrances = ['Kakariko Well Drop', 'Bat Cave Drop', 'North Fairy Cave Drop', 'Lost Woods Hideout Drop', 'Lumberjack Tree Tree', 'Sanctuary Grave',
'Skull Woods First Section Hole (East)', 'Skull Woods First Section Hole (West)', 'Skull Woods First Section Hole (North)', 'Skull Woods Second Section Hole']
hole_targets = ['Kakariko Well (top)', 'Bat Cave (right)', 'North Fairy Cave', 'Lost Woods Hideout (top)', 'Lumberjack Tree (top)', 'Sewer Drop', 'Skull Back Drop',
'Skull Left Drop', 'Skull Pinball', 'Skull Pot Circle']
if world.mode[player] == 'standard':
# cannot move uncle cave
connect_entrance(world, 'Hyrule Castle Secret Entrance Drop', 'Hyrule Castle Secret Entrance', player)
connect_exit(world, 'Hyrule Castle Secret Entrance Exit', 'Hyrule Castle Secret Entrance Stairs', player)
connect_entrance(world, 'Hyrule Castle Secret Entrance Stairs', 'Hyrule Castle Secret Entrance Exit', player)
else:
hole_entrances.append('Hyrule Castle Secret Entrance Drop')
hole_targets.append('Hyrule Castle Secret Entrance')
doors.append('Hyrule Castle Secret Entrance Stairs')
entrances.append('Hyrule Castle Secret Entrance Stairs')
caves.append('Hyrule Castle Secret Entrance Exit')
if not world.shuffle_ganon:
connect_two_way(world, 'Ganons Tower', 'Ganons Tower Exit', player)
connect_two_way(world, 'Pyramid Entrance', 'Pyramid Exit', player)
connect_entrance(world, 'Pyramid Hole', 'Pyramid', player)
else:
entrances.append('Ganons Tower')
caves.extend(['Ganons Tower Exit', 'Pyramid Exit'])
hole_entrances.append('Pyramid Hole')
hole_targets.append('Pyramid')
entrances_must_exits.append('Pyramid Entrance')
doors.extend(['Ganons Tower', 'Pyramid Entrance'])
random.shuffle(hole_entrances)
random.shuffle(hole_targets)
random.shuffle(entrances)
# fill up holes
for hole in hole_entrances:
connect_entrance(world, hole, hole_targets.pop(), player)
# hyrule castle handling
if world.mode[player] == 'standard':
# must connect front of hyrule castle to do escape
connect_entrance(world, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player)
connect_exit(world, 'Hyrule Castle Exit (South)', 'Hyrule Castle Entrance (South)', player)
caves.append(('Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)'))
else:
doors.append('Hyrule Castle Entrance (South)')
entrances.append('Hyrule Castle Entrance (South)')
caves.append(('Hyrule Castle Exit (South)', 'Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)'))
# now let's deal with mandatory reachable stuff
def extract_reachable_exit(cavelist):
random.shuffle(cavelist)
candidate = None
for cave in cavelist:
if isinstance(cave, tuple) and len(cave) > 1:
# special handling: TRock has two entries that we should consider entrance only
# ToDo this should be handled in a more sensible manner
if cave[0] in ['Turtle Rock Exit (Front)', 'Spectacle Rock Cave Exit (Peak)'] and len(cave) == 2:
continue
candidate = cave
break
if candidate is None:
raise RuntimeError('No suitable cave.')
cavelist.remove(candidate)
return candidate
def connect_reachable_exit(entrance, caves, doors):
cave = extract_reachable_exit(caves)
exit = cave[-1]
cave = cave[:-1]
connect_exit(world, exit, entrance, player)
connect_entrance(world, doors.pop(), exit, player)
# rest of cave now is forced to be in this world
caves.append(cave)
# connect mandatory exits
for entrance in entrances_must_exits:
connect_reachable_exit(entrance, caves, doors)
# place old man, has limited options
# exit has to come from specific set of doors, the entrance is free to move about
old_man_entrances = [entrance for entrance in old_man_entrances if entrance in entrances]
random.shuffle(old_man_entrances)
old_man_exit = old_man_entrances.pop()
entrances.remove(old_man_exit)
connect_exit(world, 'Old Man Cave Exit (East)', old_man_exit, player)
connect_entrance(world, doors.pop(), 'Old Man Cave Exit (East)', player)
caves.append('Old Man Cave Exit (West)')
# handle remaining caves
for cave in caves:
if isinstance(cave, str):
cave = (cave,)
for exit in cave:
connect_exit(world, exit, entrances.pop(), player)
connect_entrance(world, doors.pop(), exit, player)
# handle simple doors
single_doors = list(Single_Cave_Doors)
bomb_shop_doors = list(Bomb_Shop_Single_Cave_Doors)
blacksmith_doors = list(Blacksmith_Single_Cave_Doors)
door_targets = list(Single_Cave_Targets)
# place blacksmith, has limited options
random.shuffle(blacksmith_doors)
blacksmith_hut = blacksmith_doors.pop()
connect_entrance(world, blacksmith_hut, 'Blacksmiths Hut', player)
bomb_shop_doors.extend(blacksmith_doors)
# place dam and pyramid fairy, have limited options
random.shuffle(bomb_shop_doors)
bomb_shop = bomb_shop_doors.pop()
connect_entrance(world, bomb_shop, 'Big Bomb Shop', player)
single_doors.extend(bomb_shop_doors)
# tavern back door cannot be shuffled yet
connect_doors(world, ['Tavern North'], ['Tavern'], player)
# place remaining doors
connect_doors(world, single_doors, door_targets, player)
else:
raise NotImplementedError('Shuffling not supported yet')
@@ -3059,7 +2543,8 @@ Inverted_Must_Exit_Invalid_Connections = defaultdict(set, {
})
# these are connections that cannot be shuffled and always exist. They link together separate parts of the world we need to divide into regions
# these are connections that cannot be shuffled and always exist.
# They link together separate parts of the world we need to divide into regions
mandatory_connections = [('Links House S&Q', 'Links House'),
('Sanctuary S&Q', 'Sanctuary'),
('Old Man S&Q', 'Old Man House'),

View File

@@ -11,7 +11,7 @@ from source.item.FillUtil import filter_pot_locations, valid_pot_items
def get_dungeon_item_pool(world):
return [item for dungeon in world.dungeons for item in dungeon.all_items]
return [item for dungeon in world.dungeons for item in dungeon.all_items if item.location is None]
def promote_dungeon_items(world):

2
Gui.py
View File

@@ -179,7 +179,7 @@ def guiMain(args=None):
self.pages["startinventory"].content.pack(side=TOP, fill=BOTH, expand=True)
# Custom Controls
self.pages["custom"].content = custom_page(self,self.pages["custom"])
self.pages["custom"].content = custom_page(self, self.pages["custom"])
self.pages["custom"].content.pack(side=TOP, fill=BOTH, expand=True)
def validation(P):

View File

@@ -6,7 +6,7 @@ import RaceRandom as random
from BaseClasses import Region, RegionType, Shop, ShopType, Location, CollectionState, PotItem
from EntranceShuffle import connect_entrance
from Regions import shop_to_location_table, retro_shops, shop_table_by_location, valid_pot_location
from Fill import FillError, fill_restrictive, fast_fill, get_dungeon_item_pool
from Fill import FillError, fill_restrictive, get_dungeon_item_pool, is_dungeon_item
from PotShuffle import vanilla_pots
from Items import ItemFactory
@@ -254,7 +254,9 @@ def generate_itempool(world, player):
world.get_location('Zelda Drop Off', player).locked = True
# set up item pool
if world.custom:
if world.customizer and world.customizer.get_item_pool():
(pool, placed_items, precollected_items, clock_mode, lamps_needed_for_dark_rooms) = make_customizer_pool(world, player)
elif world.custom:
(pool, placed_items, precollected_items, clock_mode, treasure_hunt_count, treasure_hunt_icon, lamps_needed_for_dark_rooms) = make_custom_item_pool(world.progressive, world.shuffle[player], world.difficulty[player], world.timer, world.goal[player], world.mode[player], world.swords[player], world.retro[player], world.bombbag[player], world.customitemarray)
world.rupoor_cost = min(world.customitemarray[player]["rupoorcost"], 9999)
else:
@@ -352,7 +354,9 @@ def generate_itempool(world, player):
# rather than making all hearts/heart pieces progression items (which slows down generation considerably)
# We mark one random heart container as an advancement item (or 4 heart pieces in expert mode)
if world.difficulty[player] in ['normal', 'hard'] and not (world.custom and world.customitemarray[player]["heartcontainer"] == 0):
next(item for item in items if item.name == 'Boss Heart Container').advancement = True
container = next((item for item in items if item.name == 'Boss Heart Container'), None)
if container:
container.advancement = True
elif world.difficulty[player] in ['expert'] and not (world.custom and world.customitemarray[player]["heartpiece"] < 4):
adv_heart_pieces = (item for item in items if item.name == 'Piece of Heart')
for i in range(4):
@@ -372,7 +376,18 @@ def generate_itempool(world, player):
world.itempool += [beemizer(item) for item in items]
# shuffle medallions
mm_medallion, tr_medallion = None, None
if world.customizer and world.customizer.get_medallions() and player in world.customizer.get_medallions():
medal_map = world.customizer.get_medallions()
if player in medal_map:
custom_medallions = medal_map[player]
if 'Misery Mire' in custom_medallions:
mm_medallion = custom_medallions['Misery Mire']
if 'Turtle Rock' in custom_medallions:
tr_medallion = custom_medallions['Turtle Rock']
if not mm_medallion:
mm_medallion = ['Ether', 'Quake', 'Bombos'][random.randint(0, 2)]
if not tr_medallion:
tr_medallion = ['Ether', 'Quake', 'Bombos'][random.randint(0, 2)]
world.required_medallions[player] = (mm_medallion, tr_medallion)
@@ -797,11 +812,7 @@ def get_pool_core(progressive, shuffle, difficulty, treasure_hunt_total, timer,
lamps_needed_for_dark_rooms = 1
# insanity shuffle doesn't have fake LW/DW logic so for now guaranteed Mirror and Moon Pearl at the start
if shuffle == 'insanity_legacy':
place_item('Link\'s House', 'Magic Mirror')
place_item('Sanctuary', 'Moon Pearl')
else:
# old insanity shuffle didn't have fake LW/DW logic so this used to be conditional
pool.extend(['Magic Mirror', 'Moon Pearl'])
if timer == 'display':
@@ -993,13 +1004,6 @@ def make_custom_item_pool(progressive, shuffle, difficulty, timer, goal, mode, s
pool.extend(['Fighter Sword'] * customitemarray["sword1"])
pool.extend(['Progressive Sword'] * customitemarray["progressivesword"])
if shuffle == 'insanity_legacy':
place_item('Link\'s House', 'Magic Mirror')
place_item('Sanctuary', 'Moon Pearl')
pool.extend(['Magic Mirror'] * max((customitemarray["mirror"] -1 ), 0))
pool.extend(['Moon Pearl'] * max((customitemarray["pearl"] - 1), 0))
else:
pool.extend(['Magic Mirror'] * customitemarray["mirror"])
pool.extend(['Moon Pearl'] * customitemarray["pearl"])
@@ -1012,6 +1016,39 @@ def make_custom_item_pool(progressive, shuffle, difficulty, timer, goal, mode, s
return (pool, placed_items, precollected_items, clock_mode, treasure_hunt_count, treasure_hunt_icon, lamps_needed_for_dark_rooms)
def make_customizer_pool(world, player):
pool = []
placed_items = {}
precollected_items = []
clock_mode = None
def place_item(loc, item):
assert loc not in placed_items
placed_items[loc] = item
diff = difficulties[world.difficulty[player]]
for item_name, amount in world.customizer.get_item_pool()[player].items():
if isinstance(amount, int):
if item_name == 'Bottle (Random)':
for _ in range(amount):
pool.append(random.choice(diff.bottles))
else:
pool.extend([item_name] * amount)
timer = world.timer[player]
if timer in ['display', 'timed', 'timed-countdown']:
clock_mode = 'countdown' if timer == 'timed-countdown' else 'stopwatch'
elif timer == 'timed-ohko':
clock_mode = 'countdown-ohko'
elif timer == 'ohko':
clock_mode = 'ohko'
if world.goal[player] == 'pedestal':
place_item('Master Sword Pedestal', 'Triforce')
return pool, placed_items, precollected_items, clock_mode, 1
# A quick test to ensure all combinations generate the correct amount of items.
def test():
for difficulty in ['normal', 'hard', 'expert']:
@@ -1020,7 +1057,7 @@ def test():
for mode in ['open', 'standard', 'inverted', 'retro']:
for swords in ['random', 'assured', 'swordless', 'vanilla']:
for progressive in ['on', 'off']:
for shuffle in ['full', 'insanity_legacy']:
for shuffle in ['full']:
for logic in ['noglitches', 'minorglitches', 'owglitches', 'nologic']:
for retro in [True, False]:
for bombbag in [True, False]:
@@ -1044,24 +1081,48 @@ if __name__ == '__main__':
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])
if world.customizer:
placements = world.customizer.get_placements()
dungeon_pool = get_dungeon_item_pool(world)
prize_pool = []
prize_set = {'Red Pendant', 'Blue Pendant', 'Green Pendant', 'Crystal 1', 'Crystal 2',
'Crystal 3', 'Crystal 4', 'Crystal 7', 'Crystal 5', 'Crystal 6'}
for p in range(1, world.players + 1):
prize_pool.extend(prize_set)
if placements:
for player, placement_list in placements.items():
for location, item in placement_list.items():
loc = world.get_location(location, player)
item_parts = item.split('#')
item_player = player if len(item_parts) < 2 else int(item_parts[1])
item_name = item_parts[0]
event_flag = False
if is_dungeon_item(item_name, world, item_player):
item_to_place = next(x for x in dungeon_pool
if x.name == item_name and x.player == item_player)
dungeon_pool.remove(item_to_place)
event_flag = True
elif item_name in prize_set:
item_player = player # prizes must be for that player
item_to_place = ItemFactory(item_name, item_player)
prize_pool.remove(item_name)
event_flag = True
else:
item_to_place = next((x for x in world.itempool
if x.name == item_name and x.player == item_player), None)
if item_to_place is None:
item_to_place = ItemFactory(item_name, player)
else:
world.itempool.remove(item_to_place)
world.push_item(loc, item_to_place, False)
# track_outside_keys(item_to_place, spot_to_fill, world)
# track_dungeon_items(item_to_place, spot_to_fill, world)
loc.event = event_flag or item_to_place.advancement
# 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])
def is_dungeon_item(item, world, player):
return ((item.startswith('Small Key') and not world.keyshuffle[player])
or (item.startswith('Big Key') and not world.bigkeyshuffle[player])
or (item.startswith('Compass') and not world.compassshuffle[player])
or (item.startswith('Map') and not world.mapshuffle[player]))

43
Main.py
View File

@@ -25,11 +25,13 @@ from Rules import set_rules
from Dungeons import create_dungeons
from Fill import distribute_items_restrictive, promote_dungeon_items, fill_dungeons_restrictive, ensure_good_pots
from Fill import sell_potions, sell_keys, balance_multiworld_progression, balance_money_progression, lock_shop_locations
from ItemList import generate_itempool, difficulties, fill_prizes, customize_shops
from ItemList import generate_itempool, difficulties, fill_prizes, customize_shops, fill_specific_items
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.overworld.EntranceShuffle2 import link_entrances_new
from source.tools.BPS import create_bps_from_data
from source.classes.CustomSettings import CustomSettings
__version__ = '1.0.1.11v'
@@ -57,21 +59,31 @@ def main(args, seed=None, fish=None):
if args.securerandom:
random.use_secure()
seeded = False
# initialize the world
if args.code:
for player, code in args.code.items():
if code:
Settings.adjust_args_from_code(code, player, args)
customized = None
if args.customizer:
customized = CustomSettings()
customized.load_yaml(args.customizer)
seed = customized.determine_seed()
if seed:
seeded = True
customized.adjust_args(args)
world = World(args.multi, args.shuffle, args.door_shuffle, args.logic, args.mode, args.swords,
args.difficulty, args.item_functionality, args.timer, args.progressive, args.goal, args.algorithm,
args.accessibility, args.shuffleganon, args.retro, args.custom, args.customitemarray, args.hints)
world.customizer = customized if customized else None
logger = logging.getLogger('')
if seed is None:
random.seed(None)
world.seed = random.randint(0, 999999999)
else:
world.seed = int(seed)
if not seeded:
random.seed(world.seed)
if args.securerandom:
@@ -128,6 +140,8 @@ def main(args, seed=None, fish=None):
for player, name in enumerate(team, 1):
world.player_names[player].append(name)
logger.info('')
world.settings = CustomSettings()
world.settings.create_from_world(world)
for player in range(1, world.players + 1):
world.difficulty_requirements[player] = difficulties[world.difficulty[player]]
@@ -156,6 +170,15 @@ def main(args, seed=None, fish=None):
adjust_locations(world, player)
place_bosses(world, player)
if world.customizer and world.customizer.get_start_inventory():
for p, inv_list in world.customizer.get_start_inventory().items():
for inv_item in inv_list:
item = ItemFactory(inv_item.strip(), p)
if item:
world.push_precollected(item)
if args.print_custom_yaml:
world.settings.record_info(world)
if any(world.potshuffle.values()):
logger.info(world.fish.translate("cli", "cli", "shuffling.pots"))
for player in range(1, world.players + 1):
@@ -168,6 +191,9 @@ def main(args, seed=None, fish=None):
logger.info(world.fish.translate("cli","cli","shuffling.world"))
for player in range(1, world.players + 1):
if world.experimental[player] or (world.customizer and world.customizer.get_entrances()):
link_entrances_new(world, player)
else:
if world.mode[player] != 'inverted':
link_entrances(world, player)
else:
@@ -177,6 +203,8 @@ def main(args, seed=None, fish=None):
for player in range(1, world.players + 1):
link_doors_prep(world, player)
if args.print_custom_yaml:
world.settings.record_entrances(world)
create_item_pool_config(world)
logger.info(world.fish.translate("cli", "cli", "shuffling.dungeons"))
@@ -187,6 +215,8 @@ def main(args, seed=None, fish=None):
mark_light_world_regions(world, player)
else:
mark_dark_world_regions(world, player)
if args.print_custom_yaml:
world.settings.record_doors(world)
logger.info(world.fish.translate("cli", "cli", "generating.itempool"))
for player in range(1, world.players + 1):
@@ -207,13 +237,13 @@ def main(args, seed=None, fish=None):
lock_shop_locations(world, player)
massage_item_pool(world)
if args.print_custom_yaml:
world.settings.record_item_pool(world)
fill_specific_items(world)
logger.info(world.fish.translate("cli", "cli", "placing.dungeon.prizes"))
fill_prizes(world)
# used for debugging
# fill_specific_items(world)
logger.info(world.fish.translate("cli","cli","placing.dungeon.items"))
if args.algorithm != 'equitable':
@@ -258,6 +288,9 @@ def main(args, seed=None, fish=None):
ensure_good_pots(world, True)
outfilebase = f'DR_{args.outputname if args.outputname else world.seed}'
if args.print_custom_yaml:
world.settings.record_item_placements(world)
world.settings.write_to_file(output_path(f'{outfilebase}_custom.yaml'))
rom_names = []
jsonout = {}

View File

@@ -1,15 +1,15 @@
import argparse
import logging
import RaceRandom as random
import urllib.request
import urllib.parse
import yaml
from DungeonRandomizer import parse_cli
from Main import main as DRMain
from source.classes.BabelFish import BabelFish
from yaml.constructor import SafeConstructor
from source.tools.MysteryUtils import roll_settings, get_weights
def add_bool(self, node):
return self.construct_scalar(node)

View File

@@ -35,6 +35,15 @@ The old "Pot Shuffle" option is still available under "Pot Shuffle (Legacy)" or
The sram locations for pots and sprite drops are not yet final, please reach out for assistance or investigate the rom changes.
## Customizer
Please refer to [the documentation](docs/Customizer.md) and examples of customizer [here](docs/customizer_example.yaml) and [here](docs/multi_mystery_example.yaml)
note that entrance customization is only available with experimental features turned on.
## Experimental Entrance Shuffle
To support customizer and future entrance shuffle modes (perhaps even customizable ones), the entrance shuffle algorithm has been re-written. It is currently in an unstable state, and will use the old method unless you turn experimental features on. I'm currently in the process of evaluating most modes with different combinations of settings and checking the distribution of entrances. Entrance customization is only supported with this new experimental entrance shuffle. The experimental entrance shuffle includes prototypes of Lean and Lite entrance shuffles from the OWR branch.
## Restricted Item Placement Algorithm

92
Rom.py
View File

@@ -681,7 +681,7 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False):
# Thanks to Zarby89 for originally finding these values
# todo fix screen scrolling
if world.shuffle[player] not in ['insanity', 'insanity_legacy', 'madness_legacy'] and \
if world.shuffle[player] not in ['insanity'] and \
exit.name in ['Eastern Palace Exit', 'Tower of Hera Exit', 'Thieves Town Exit', 'Ice Palace Exit', 'Misery Mire Exit',
'Palace of Darkness Exit', 'Swamp Palace Exit', 'Ganons Tower Exit', 'Desert Palace Exit (North)', 'Agahnims Tower Exit', 'Spiral Cave Exit (Top)',
'Superbunny Cave Exit (Bottom)', 'Turtle Rock Ledge Exit (East)']:
@@ -790,8 +790,8 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False):
rom.write_byte(0x13f030+offset, layout.max_chests)
builder = world.dungeon_layouts[player][name]
valid_cnt = len(valid_loc_by_dungeon[name])
if valid_cnt > 99:
logging.getLogger('').warning(f'{name} exceeds 99 in locations ({valid_cnt})')
if valid_cnt > 256:
logging.getLogger('').warning(f'{name} exceeds 256 in locations ({valid_cnt})')
rom.write_byte(0x13f080+offset, valid_cnt % 10)
rom.write_byte(0x13f090+offset, valid_cnt // 10)
rom.write_byte(0x13f0a0+offset, valid_cnt)
@@ -1644,7 +1644,7 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False):
rom.write_bytes(0x02F539, [0xEA, 0xEA, 0xEA, 0xEA, 0xEA] if world.powder_patch_required[player] else [0xAD, 0xBF, 0x0A, 0xF0, 0x4F])
# allow smith into multi-entrance caves in appropriate shuffles
if world.shuffle[player] in ['restricted', 'full', 'crossed', 'insanity'] or (world.shuffle[player] == 'simple' and world.mode[player] == 'inverted'):
if world.shuffle[player] in ['restricted', 'full', 'lite', 'lean', 'crossed', 'insanity'] or (world.shuffle[player] == 'simple' and world.mode[player] == 'inverted'):
rom.write_byte(0x18004C, 0x01)
# set correct flag for hera basement item
@@ -2133,7 +2133,7 @@ def write_strings(rom, world, player, team):
entrances_to_hint.update({'Inverted Ganons Tower': 'The sealed castle door'})
else:
entrances_to_hint.update({'Ganons Tower': 'Ganon\'s Tower'})
if world.shuffle[player] in ['simple', 'restricted', 'restricted_legacy']:
if world.shuffle[player] in ['simple', 'restricted']:
for entrance in all_entrances:
if entrance.name in entrances_to_hint:
this_hint = entrances_to_hint[entrance.name] + ' leads to ' + hint_text(entrance.connected_region) + '.'
@@ -2144,13 +2144,13 @@ def write_strings(rom, world, player, team):
entrances_to_hint.update(InconvenientOtherEntrances)
if world.shuffle[player] in ['vanilla', 'dungeonssimple', 'dungeonsfull']:
hint_count = 0
elif world.shuffle[player] in ['simple', 'restricted', 'restricted_legacy']:
elif world.shuffle[player] in ['simple', 'restricted']:
hint_count = 2
else:
hint_count = 4
for entrance in all_entrances:
if entrance.name in entrances_to_hint:
if hint_count > 0:
if entrance.name in entrances_to_hint:
this_hint = entrances_to_hint[entrance.name] + ' leads to ' + hint_text(entrance.connected_region) + '.'
tt[hint_locations.pop(0)] = this_hint
entrances_to_hint.pop(entrance.name)
@@ -2159,7 +2159,7 @@ def write_strings(rom, world, player, team):
break
#Next we handle hints for randomly selected other entrances, curating the selection intelligently based on shuffle.
if world.shuffle[player] not in ['simple', 'restricted', 'restricted_legacy']:
if world.shuffle[player] not in ['simple', 'restricted']:
entrances_to_hint.update(ConnectorEntrances)
entrances_to_hint.update(DungeonEntrances)
if world.mode[player] == 'inverted':
@@ -2168,7 +2168,17 @@ def write_strings(rom, world, player, team):
entrances_to_hint.update({'Agahnims Tower': 'The sealed castle door'})
elif world.shuffle[player] == 'restricted':
entrances_to_hint.update(ConnectorEntrances)
entrances_to_hint.update(ItemEntrances)
if world.shuffle[player] not in ['lite', 'lean']:
entrances_to_hint.update(ShopEntrances)
entrances_to_hint.update(OtherEntrances)
elif world.shopsanity[player]:
entrances_to_hint.update(ShopEntrances)
if world.shufflelinks[player] and world.shuffle[player] not in ['vanilla', 'dungeonssimple', 'dungeonsfull']:
if world.mode[player] == 'inverted':
entrances_to_hint.update({'Inverted Links House': 'The hero\'s old residence'})
else:
entrances_to_hint.update({'Links House': 'The hero\'s old residence'})
if world.mode[player] == 'inverted':
entrances_to_hint.update({'Inverted Dark Sanctuary': 'The dark sanctuary cave'})
entrances_to_hint.update({'Inverted Big Bomb Shop': 'The old hero\'s dark home'})
@@ -2176,7 +2186,7 @@ def write_strings(rom, world, player, team):
else:
entrances_to_hint.update({'Dark Sanctuary Hint': 'The dark sanctuary cave'})
entrances_to_hint.update({'Big Bomb Shop': 'The old bomb shop'})
if world.shuffle[player] in ['insanity', 'madness_legacy', 'insanity_legacy']:
if world.shuffle[player] in ['insanity']:
entrances_to_hint.update(InsanityEntrances)
if world.shuffle_ganon:
if world.mode[player] == 'inverted':
@@ -2802,16 +2812,45 @@ DungeonEntrances = {'Eastern Palace': 'Eastern Palace',
'Desert Palace Entrance (North)': 'The northmost cave in the desert'
}
OtherEntrances = {'Blinds Hideout': 'Blind\'s old house',
'Lake Hylia Fairy': 'A cave NE of Lake Hylia',
'Light Hype Fairy': 'The cave south of your house',
'Desert Fairy': 'The cave near the desert',
ItemEntrances = {'Blinds Hideout': 'Blind\'s old house',
'Chicken House': 'The chicken lady\'s house',
'Aginahs Cave': 'The open desert cave',
'Sahasrahlas Hut': 'The house near armos',
'Cave Shop (Lake Hylia)': 'The cave NW Lake Hylia',
'Blacksmiths Hut': 'The old smithery',
'Sick Kids House': 'The central house in Kakariko',
'Mini Moldorm Cave': 'The cave south of Lake Hylia',
'Ice Rod Cave': 'The sealed cave SE Lake Hylia',
'Library': 'The old library',
'Potion Shop': 'The witch\'s building',
'Dam': 'The old dam',
'Waterfall of Wishing': 'Going behind the waterfall',
'Bonk Rock Cave': 'The rock pile near Sanctuary',
'Graveyard Cave': 'The graveyard ledge',
'Checkerboard Cave': 'The NE desert ledge',
'Cave 45': 'The ledge south of haunted grove',
'Kings Grave': 'The northeastmost grave',
'C-Shaped House': 'The NE house in Village of Outcasts',
'Mire Shed': 'The western hut in the mire',
'Spike Cave': 'The ledge cave on west dark DM',
'Hype Cave': 'The cave south of the old bomb shop',
'Brewery': 'The Village of Outcasts building with no door',
'Chest Game': 'The westmost building in the Village of Outcasts',
}
ShopEntrances = {'Cave Shop (Lake Hylia)': 'The cave NW Lake Hylia',
'Kakariko Shop': 'The old Kakariko shop',
'Capacity Upgrade': 'The cave on the island',
'Dark Lake Hylia Shop': 'The building NW dark Lake Hylia',
'Dark World Shop': 'The hammer sealed building',
'Red Shield Shop': 'The fenced in building',
'Cave Shop (Dark Death Mountain)': 'The base of east dark DM',
'Dark World Potion Shop': 'The building near the catfish',
'Dark World Lumberjack Shop': 'The northmost Dark World building'
}
OtherEntrances = {'Lake Hylia Fairy': 'A cave NE of Lake Hylia',
'Light Hype Fairy': 'The cave south of your house',
'Desert Fairy': 'The cave near the desert',
'Lost Woods Gamble': 'A tree trunk door',
'Fortune Teller (Light)': 'A building NE of Kakariko',
'Snitch Lady (East)': 'A house guarded by a snitch',
@@ -2819,49 +2858,24 @@ OtherEntrances = {'Blinds Hideout': 'Blind\'s old house',
'Bush Covered House': 'A house with an uncut lawn',
'Tavern (Front)': 'A building with a backdoor',
'Light World Bomb Hut': 'A Kakariko building with no door',
'Kakariko Shop': 'The old Kakariko shop',
'Mini Moldorm Cave': 'The cave south of Lake Hylia',
'Long Fairy Cave': 'The eastmost portal cave',
'Good Bee Cave': 'The open cave SE Lake Hylia',
'20 Rupee Cave': 'The rock SE Lake Hylia',
'50 Rupee Cave': 'The rock near the desert',
'Ice Rod Cave': 'The sealed cave SE Lake Hylia',
'Library': 'The old library',
'Potion Shop': 'The witch\'s building',
'Dam': 'The old dam',
'Lumberjack House': 'The lumberjack house',
'Lake Hylia Fortune Teller': 'The building NW Lake Hylia',
'Kakariko Gamble Game': 'The old Kakariko gambling den',
'Waterfall of Wishing': 'Going behind the waterfall',
'Capacity Upgrade': 'The cave on the island',
'Bonk Rock Cave': 'The rock pile near Sanctuary',
'Graveyard Cave': 'The graveyard ledge',
'Checkerboard Cave': 'The NE desert ledge',
'Cave 45': 'The ledge south of haunted grove',
'Kings Grave': 'The northeastmost grave',
'Bonk Fairy (Light)': 'The rock pile near your home',
'Hookshot Fairy': 'The left paired cave on east DM',
'Bonk Fairy (Dark)': 'The rock pile near the old bomb shop',
'Dark Lake Hylia Fairy': 'The cave NE dark Lake Hylia',
'C-Shaped House': 'The NE house in Village of Outcasts',
'Dark Death Mountain Fairy': 'The SW cave on dark DM',
'Dark Lake Hylia Shop': 'The building NW dark Lake Hylia',
'Dark World Shop': 'The hammer sealed building',
'Red Shield Shop': 'The fenced in building',
'Mire Shed': 'The western hut in the mire',
'East Dark World Hint': 'The dark cave near the eastmost portal',
'Dark Desert Hint': 'The cave east of the mire',
'Spike Cave': 'The ledge cave on west dark DM',
'Palace of Darkness Hint': 'The building south of Kiki',
'Dark Lake Hylia Ledge Spike Cave': 'The rock SE dark Lake Hylia',
'Cave Shop (Dark Death Mountain)': 'The base of east dark DM',
'Dark World Potion Shop': 'The building near the catfish',
'Archery Game': 'The old archery game',
'Dark World Lumberjack Shop': 'The northmost Dark World building',
'Hype Cave': 'The cave south of the old bomb shop',
'Brewery': 'The Village of Outcasts building with no door',
'Dark Lake Hylia Ledge Hint': 'The open cave SE dark Lake Hylia',
'Chest Game': 'The westmost building in the Village of Outcasts',
'Dark Desert Fairy': 'The eastern hut in the mire',
'Dark Lake Hylia Ledge Fairy': 'The sealed cave SE dark Lake Hylia',
'Fortune Teller (Dark)': 'The building NE the Village of Outcasts'

View File

@@ -316,6 +316,18 @@ class Room(object):
byte_array.append(kind.value)
return byte_array
def next_free(self):
for i, door in enumerate(self.doorList):
if i >= 4:
return None
pos, kind = door
if kind not in [DoorKind.SmallKey, DoorKind.Dashable, DoorKind.Bombable, DoorKind.TrapTriggerable,
DoorKind.Trap, DoorKind.Trap2, DoorKind.TrapTriggerableLow, DoorKind.TrapLowE3,
DoorKind.BigKey, DoorKind.StairKey, DoorKind.StairKey2, DoorKind.StairKeyLow,
DoorKind.BlastWall, DoorKind.BombableEntrance]:
return i
return None
def __str__(self):
return str(self.__unicode__())

View File

@@ -63,7 +63,7 @@ def set_rules(world, player):
if world.mode[player] != 'inverted':
set_big_bomb_rules(world, player)
if world.logic[player] == 'owglitches' and world.shuffle[player] not in ('insanity', 'insanity_legacy'):
if world.logic[player] == 'owglitches' and world.shuffle[player] != 'insanity':
path_to_courtyard = mirrorless_path_to_castle_courtyard(world, player)
add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.world.get_entrance('Dark Death Mountain Offset Mirror', player).can_reach(state) and all(rule(state) for rule in path_to_courtyard), 'or')
else:

182
docs/Customizer.md Normal file
View File

@@ -0,0 +1,182 @@
## Customizer Files
A customizer file is [yaml file](https://yaml.org/) with different settings. This documentation is intended to be read with an [example customizer file](customizer_example.yaml).
This can also be used to roll a mystery or mutli-mystery seed via the GUI. [Example](multi_mystery_example.yaml)
The cli includes a couple arguments to help:
`--print_custom_yaml` will create a yaml file based on the seed rolled. Treat it like a spoiler.
`--customizer` takes a file as an argument.
Present on the GUI as `Print Customizer File` and `Customizer File` on the Generation Setup tab.
### meta
Supported values:
* `players`: number of players
* `seed`: you can set the seed number for a set experience on things that are randomized instead of specified
* `algorithm`: fill algorithm
* `names`: for naming mutliworld players
###### Not Yet Implemented
* `mystery`: indicates the seed should be treated as a mystery (hides total collection_rate) (This may end up player specific)
### settings
This must be defined by player. Each player number should be listed with the appropriate settings.
in each player section you can set up default settings for that player or you can specify a mystery yaml file.
```
1: player1.yaml
2:
shuffle: crossed
door_shuffle: basic
```
Player 1's settings will be determined by rolling the mystery weights and player 2's setting will be default except for those two specified in his section. Each settings should be consistent with the CLI arguments.
Start inventory is not supported here. It has a separate section.
###### Not Yet Implemented
Rom/Adjust flags like sprite, quickswap are not outputing with the print_custom_yaml settings
### item_pool
This must be defined by player. Each player number should be listed with the appropriate pool.
Then each player can have the entire item pool defined. The name of item should be followed by the number of that item in the pool. All key items need to be listed here for now.
`Bottle (Random)` is supported to randomize bottle contents according to those allowed by difficulty. Pendants and crystals are supported here.
##### Known Issues
1. Dungeon items amount can be increased but not eliminated (as the amount of each dungeon item is either pre-determined or calculated by door rando) and these extra items may not be confined to the dungeon
2. Door rando removes Red Rupees from the pool to make room for extra dungeon items as needed.
3. Shopsanity appends extra shop items to the pool.
4. Beemizer runs after pool creation changing junk items into bees
5. Retro + Shopsanity adds more items to the pool
6. Retro + either of dropshuffle or pottery adds keys to the pool
7. Most pottery settings add a large amount of junk items to the pool
### placements
This must be defined by player. Each player number should be listed with the appropriate placement list.
You may list each location for a player and the item you wish to place there. A location name requires to be enclosed by single quotes if the location name contains a `#` (Most pot locations have the `#`). (Currently no location names have both a `'` and a `#` so you don't need to worry about escaping the `'`)
For multiworld you can specify which player the item is for using this syntax:
`<item name>#<player number>`
Example:
`Pegasus Boots#3` means the boots for player 3.
### entrances
This must be defined by player. Each player number should be listed with the appropriate sections. This section has three primary subsections: `entrances`, `exits`, and `two-way`.
#### two-way
`two-way` should be used for connectors, dungeons that you wish to couple. (as opposite to decoupled in the insanity shuffle). Links house should be placed using this method as is can be decoupled logically. (Haven't tested to see if it works in game). The overworld entrance is listed first, followed by the interior exit that it leads to. (The exit will then be linked to exit at that entrance).
`50 Rupee Cave: Desert Palace Exit (North)` The 50 Rupee entrance leads to Desert North entrance, and leaving there will spit you out at the same place.
#### exits
`exits` is used for the Chris Houlihan Room Exit and connectors and dungeons that you wish to be decoupled from their entrances. Perhaps counter-intuitively, the exit is listed after the entrance from which it emerges.
`Light Hype Fairy: Chris Houlihan Room Exit` leaving Chris Houlihan Room will spit you out at the Light Hype Fairy.
(I can easily switch this syntax around if people would like me too)
#### entrances
`entrances` is used for single entrances caves, houses, shops, etc. and drops. Single entrance caves always exit to where you enter, they cannot be decoupled. Dungeons and connectors which are decoupled can also be listed here.
`Chicken House: Kakariko Shop` if you walk into Chicken House door, you will in the Kakariko Shop.
##### Known Issues
Chris Houlihan and Links House should be specified together or not at all.
### doors
This must be defined by player. Each player number should be listed with the appropriate sections. This section has three primary subsections: `lobbies` and `doors`.
`lobbies` lists the doors by which each dungeon is entered
`<lobby name>: <door name>` Ex. `Turtle Rock Chest: TR Lava Escape SE`
`doors` lists pairs of doors. The first door name is listed is the key. The value of this object may be the paired door name or optionally it can have two properties: `dest` and `type`. If you want a type, you must use the second option.
The destination door is listed under `dest`
Supported `type`s are `Key Door`, `Bomb Door`, and `Dash Door`
Here are the two examples of the syntax:
```Hyrule Dungeon Guardroom Abyss Edge: Hyrule Castle Lobby W```
```
Sewers Rat Path WS:
dest: Sewers Secret Room ES
two-way: true
type: Key Door
```
You'll note that sub-tile door do not need to be listed, but if you want them to be key doors you will have to list them.
###### Not Yet Implemented
`one-way` to indicate decoupled doors
##### Known Issue
If you specify a door type and those doors cannot be a stateful door due to the nature of the supertile (or you've placed too many on the supertile) an exception is thrown.
### medallions
This must be defined by player. Each player number should be listed with the appropriate info.
Example:
```
Misery Mire: Ether
Turtle Rock: Quake
```
Leave blank or omit if you wish it to be random.
### bosses
This must be defined by player. Each player number should be listed with the appropriate boss list.
This is done as `<dungeon>: <boss>`
E.g. `Skull Woods: Helmasaur King` for helmacopter. Be sure to turn on at least one enemizer setting for the bosses to actually be randomized.
### startinventory
This must be defined by player. Each player number should be listed with a list of items to start with.
This is a yaml list (note the hyphens):
```
startinventory:
1:
- Pegasus Boots
- Progressive Sword
```
To start with multiple copies of progressive items, list them more than once.
##### Known Issue
This conflicts with the mystery yaml, if specified. These start inventory items will be added after those are added.

View File

@@ -0,0 +1,108 @@
meta:
algorithm: balanced
players: 1
seed: 42
names: Lonk
settings:
1:
door_shuffle: basic
dropshuffle: true
experimental: true
goal: ganon
hints: true
intensity: 3
overworld_map: compass
pseudoboots: true
pottery: keys
shopsanity: true
shuffle: crossed
shufflelinks: true
shufflebosses: unique
item_pool:
1:
Blue Boomerang: 3
Bombos: 3
Book of Mudora: 3
Boss Heart Container: 30
Bottle (Random): 12
Bug Catching Net: 3
Cane of Byrna: 3
Cane of Somaria: 3
Cape: 3
Ether: 3
Fire Rod: 3
Flippers: 3
Hammer: 3
Hookshot: 3
Ice Rod: 3
Lamp: 3
Magic Mirror: 3
Magic Powder: 3
Magic Upgrade (1/2): 3
Moon Pearl: 3
Mushroom: 3
Ocarina: 3
Pegasus Boots: 3
Piece of Heart: 24
Progressive Armor: 6
Progressive Bow: 6
Progressive Glove: 6
Progressive Shield: 9
Progressive Sword: 12
Quake: 3
Red Boomerang: 3
Rupees (300): 10
Bombs (3): 20
Sanctuary Heart Container: 3
Shovel: 3
Single Arrow: 1
placements:
1:
Palace of Darkness - Big Chest: Hammer
Capacity Upgrade - Left: Moon Pearl
Turtle Rock - Pokey 2 Key Drop: Ice Rod
entrances:
1:
entrances:
Bat Cave Drop: Pyramid
Dam: Chicken House
Snitch Lady (West): Cave 45
exits:
Misery Mire: Chris Houlihan Room Exit
two-way:
Bat Cave Cave: Pyramid Exit
Bumper Cave (Bottom): Bumper Cave Exit (Bottom)
Bumper Cave (Top): Bumper Cave Exit (Top)
Skull Woods Final Section: Ganons Tower Exit
Misery Mire: Links House Exit
doors:
1:
lobbies:
Sanctuary: Sewers Pull Switch S
Skull 2 East: Skull 1 Lobby S
Skull 3: Skull 2 West Lobby S
Turtle Rock Lazy Eyes: TR Roller Room SW
doors:
Hyrule Dungeon Guardroom Abyss Edge: Hyrule Castle Lobby W
Sewers Rat Path WS:
dest: Sewers Secret Room ES
type: Key Door
Sewers Rat Path WN:
dest: Sewers Secret Room EN
type: Key Door
Tower Pacifist Run WS:
dest: Tower Circle of Pots ES
type: Key Door
Hera Lobby Up Stairs: Hera 4F Down Stairs
medallions:
1:
Misery Mire: Bombos
bosses:
1:
Palace of Darkness: Arrghus
Thieves Town: Blind
Ganons Tower (top): Vitreous
startinventory:
1:
- Pegasus Boots

View File

@@ -0,0 +1,17 @@
meta:
algorithm: balanced
players: 3
seed: 722495389
names: Link,Zelda,Ganon
settings:
1: player1.yml
2: player2.yml
3: player3.yml
start_inventory:
1:
- Progressive Sword
- Pegasus Boots
2:
- Lamp
3:
- Flippers

19
docs/player1.yml Normal file
View File

@@ -0,0 +1,19 @@
description: I like my keysanity distributed weirdly, but no doors please
door_shuffle: vanilla
dungeon_items:
standard: 12
m: 3
c: 3
s: 3
b: 3
mc: 2
ms: 2
mb: 2
cs: 2
cb: 2
sb: 2
mcs: 3
mcb: 3
msb: 3
csb: 3
full: 12

16
docs/player2.yml Normal file
View File

@@ -0,0 +1,16 @@
description: Some sort of er with unknown goals
door_shuffle: basic
entrance_shuffle:
dungeonssimple: 3
dungeonsfull: 2
simple: 2
restricted: 2
full: 2
crossed: 3
insanity: 1
goals:
ganon: 2
fast_ganon: 2
dungeons: 1
pedestal: 2
triforce-hunt: 1

25
docs/player3.yml Normal file
View File

@@ -0,0 +1,25 @@
description: Door feature fan
door_shuffle: crossed
world_state:
standard: 2
open: 2
inverted: 1
intensity:
1: 1
2: 1
3: 2
keydropshuffle:
on: 1
off: 1
shopsanity:
on: 1
off: 4
restrict_boss_items:
none: 1
dungeon: 1
boss_shuffle:
none: 4
simple: 1
full: 1
unique: 1
random: 1

View File

@@ -132,12 +132,10 @@
"full",
"crossed",
"insanity",
"restricted_legacy",
"full_legacy",
"madness_legacy",
"insanity_legacy",
"dungeonsfull",
"dungeonssimple"
"dungeonssimple",
"lite",
"lean"
]
},
"door_shuffle": {

View File

@@ -281,6 +281,7 @@
"bombbag": ["Start with 0 bomb capacity. Two capacity upgrades (+10) are added to the pool (default: %(default)s)" ],
"startinventory": [ "Specifies a list of items that will be in your starting inventory (separated by commas). (default: %(default)s)" ],
"usestartinventory": [ "Toggle usage of Starting Inventory." ],
"customizer": ["Path to a customizer file."],
"custom": [ "Not supported." ],
"customitemarray": [ "Not supported." ],
"accessibility": [

View File

@@ -131,15 +131,13 @@
"randomizer.entrance.entranceshuffle": "Entrance Shuffle",
"randomizer.entrance.entranceshuffle.vanilla": "Vanilla",
"randomizer.entrance.entranceshuffle.lite": "Lite",
"randomizer.entrance.entranceshuffle.lean": "Lean",
"randomizer.entrance.entranceshuffle.simple": "Simple",
"randomizer.entrance.entranceshuffle.restricted": "Restricted",
"randomizer.entrance.entranceshuffle.full": "Full",
"randomizer.entrance.entranceshuffle.crossed": "Crossed",
"randomizer.entrance.entranceshuffle.insanity": "Insanity",
"randomizer.entrance.entranceshuffle.restricted_legacy": "Restricted (Legacy)",
"randomizer.entrance.entranceshuffle.full_legacy": "Full (Legacy)",
"randomizer.entrance.entranceshuffle.madness_legacy": "Madness (Legacy)",
"randomizer.entrance.entranceshuffle.insanity_legacy": "Insanity (Legacy)",
"randomizer.entrance.entranceshuffle.dungeonsfull": "Dungeons + Full",
"randomizer.entrance.entranceshuffle.dungeonssimple": "Dungeons + Simple",
@@ -192,6 +190,7 @@
"randomizer.generation.createspoiler": "Create Spoiler Log",
"randomizer.generation.createrom": "Create Patched ROM",
"randomizer.generation.calcplaythrough": "Calculate Playthrough",
"randomizer.generation.print_custom_yaml": "Print Customizer File",
"randomizer.generation.usestartinventory": "Use Starting Inventory",
"randomizer.generation.usecustompool": "Use Custom Item Pool",

View File

@@ -6,15 +6,13 @@
"type": "selectbox",
"options": [
"vanilla",
"lite",
"lean",
"simple",
"restricted",
"full",
"crossed",
"insanity",
"restricted_legacy",
"full_legacy",
"madness_legacy",
"insanity_legacy",
"dungeonsfull",
"dungeonssimple"
]

View File

@@ -4,6 +4,7 @@
"bps": { "type": "checkbox" },
"createspoiler": { "type": "checkbox" },
"calcplaythrough": { "type": "checkbox" },
"print_custom_yaml": { "type": "checkbox" },
"usestartinventory": { "type": "checkbox" },
"usecustompool": { "type": "checkbox" }
}

View File

@@ -0,0 +1,329 @@
import os
import urllib.request
import urllib.parse
import yaml
from yaml.representer import Representer
from collections import defaultdict
import RaceRandom as random
from BaseClasses import LocationType, DoorType
from source.tools.MysteryUtils import roll_settings, get_weights
class CustomSettings(object):
def __init__(self):
self.file_source = None
self.relative_dir = None
self.world_rep = {}
self.player_range = None
def load_yaml(self, file):
self.file_source = load_yaml(file)
head, filename = os.path.split(file)
self.relative_dir = head
def determine_seed(self):
if 'meta' not in self.file_source:
return None
meta = defaultdict(lambda: None, self.file_source['meta'])
seed = meta['seed']
if seed:
random.seed(seed)
return seed
return None
def determine_players(self):
if 'meta' not in self.file_source:
return None
meta = defaultdict(lambda: None, self.file_source['meta'])
return meta['players']
def adjust_args(self, args):
def get_setting(value, default):
if value:
return value
return default
if 'meta' in self.file_source:
meta = defaultdict(lambda: None, self.file_source['meta'])
args.multi = get_setting(meta['players'], args.multi)
args.algorithm = get_setting(meta['algorithm'], args.algorithm)
args.outputname = get_setting(meta['name'], args.outputname)
args.bps = get_setting(meta['bps'], args.bps)
args.suppress_rom = get_setting(meta['suppress_rom'], args.suppress_rom)
args.names = get_setting(meta['names'], args.names)
self.player_range = range(1, args.multi + 1)
if 'settings' in self.file_source:
for p in self.player_range:
player_setting = self.file_source['settings'][p]
if isinstance(player_setting, str):
weights = get_weights(os.path.join(self.relative_dir, player_setting))
settings = defaultdict(lambda: None, vars(roll_settings(weights)))
else:
settings = defaultdict(lambda: None, player_setting)
args.shuffle[p] = get_setting(settings['shuffle'], args.shuffle[p])
args.door_shuffle[p] = get_setting(settings['door_shuffle'], args.door_shuffle[p])
args.logic[p] = get_setting(settings['logic'], args.logic[p])
args.mode[p] = get_setting(settings['mode'], args.mode[p])
args.swords[p] = get_setting(settings['swords'], args.swords[p])
args.item_functionality[p] = get_setting(settings['item_functionality'], args.item_functionality[p])
args.goal[p] = get_setting(settings['goal'], args.goal[p])
args.difficulty[p] = get_setting(settings['difficulty'], args.difficulty[p])
args.accessibility[p] = get_setting(settings['accessibility'], args.accessibility[p])
args.retro[p] = get_setting(settings['retro'], args.retro[p])
args.hints[p] = get_setting(settings['hints'], args.hints[p])
args.shopsanity[p] = get_setting(settings['shopsanity'], args.shopsanity[p])
args.dropshuffle[p] = get_setting(settings['dropshuffle'], args.dropshuffle[p])
args.pottery[p] = get_setting(settings['pottery'], args.pottery[p])
args.mixed_travel[p] = get_setting(settings['mixed_travel'], args.mixed_travel[p])
args.standardize_palettes[p] = get_setting(settings['standardize_palettes'],
args.standardize_palettes[p])
args.intensity[p] = get_setting(settings['intensity'], args.intensity[p])
args.dungeon_counters[p] = get_setting(settings['dungeon_counters'], args.dungeon_counters[p])
args.crystals_gt[p] = get_setting(settings['crystals_gt'], args.crystals_gt[p])
args.crystals_ganon[p] = get_setting(settings['crystals_ganon'], args.crystals_ganon[p])
args.experimental[p] = get_setting(settings['experimental'], args.experimental[p])
args.openpyramid[p] = get_setting(settings['openpyramid'], args.openpyramid[p])
args.bigkeyshuffle[p] = get_setting(settings['bigkeyshuffle'], args.bigkeyshuffle[p])
args.keyshuffle[p] = get_setting(settings['keyshuffle'], args.keyshuffle[p])
args.mapshuffle[p] = get_setting(settings['mapshuffle'], args.mapshuffle[p])
args.compassshuffle[p] = get_setting(settings['compassshuffle'], args.compassshuffle[p])
args.shufflebosses[p] = get_setting(settings['shufflebosses'], args.shufflebosses[p])
args.shuffleenemies[p] = get_setting(settings['shuffleenemies'], args.shuffleenemies[p])
args.enemy_health[p] = get_setting(settings['enemy_health'], args.enemy_health[p])
args.enemy_damage[p] = get_setting(settings['enemy_damage'], args.enemy_damage[p])
args.shufflepots[p] = get_setting(settings['shufflepots'], args.shufflepots[p])
args.bombbag[p] = get_setting(settings['bombbag'], args.bombbag[p])
args.shufflelinks[p] = get_setting(settings['shufflelinks'], args.shufflelinks[p])
args.restrict_boss_items[p] = get_setting(settings['restrict_boss_items'], args.restrict_boss_items[p])
args.overworld_map[p] = get_setting(settings['overworld_map'], args.overworld_map[p])
args.pseudoboots[p] = get_setting(settings['pseudoboots'], args.pseudoboots[p])
args.triforce_goal[p] = get_setting(settings['triforce_goal'], args.triforce_goal[p])
args.triforce_pool[p] = get_setting(settings['triforce_pool'], args.triforce_pool[p])
args.beemizer[p] = get_setting(settings['beemizer'], args.beemizer[p])
# mystery usage
args.usestartinventory[p] = get_setting(settings['usestartinventory'], args.usestartinventory[p])
args.startinventory[p] = get_setting(settings['startinventory'], args.startinventory[p])
# rom adjust stuff
args.sprite[p] = get_setting(settings['sprite'], args.sprite[p])
args.disablemusic[p] = get_setting(settings['disablemusic'], args.disablemusic[p])
args.quickswap[p] = get_setting(settings['quickswap'], args.quickswap[p])
args.reduce_flashing[p] = get_setting(settings['reduce_flashing'], args.reduce_flashing[p])
args.fastmenu[p] = get_setting(settings['fastmenu'], args.fastmenu[p])
args.heartcolor[p] = get_setting(settings['heartcolor'], args.heartcolor[p])
args.heartbeep[p] = get_setting(settings['heartbeep'], args.heartbeep[p])
args.ow_palettes[p] = get_setting(settings['ow_palettes'], args.ow_palettes[p])
args.uw_palettes[p] = get_setting(settings['uw_palettes'], args.uw_palettes[p])
args.shuffle_sfx[p] = get_setting(settings['shuffle_sfx'], args.shuffle_sfx[p])
def get_item_pool(self):
if 'item_pool' in self.file_source:
return self.file_source['item_pool']
return None
def get_placements(self):
if 'placements' in self.file_source:
return self.file_source['placements']
return None
def get_entrances(self):
if 'entrances' in self.file_source:
return self.file_source['entrances']
return None
def get_doors(self):
if 'doors' in self.file_source:
return self.file_source['doors']
return None
def get_bosses(self):
if 'bosses' in self.file_source:
return self.file_source['bosses']
return None
def get_start_inventory(self):
if 'start_inventory' in self.file_source:
return self.file_source['start_inventory']
return None
def get_medallions(self):
if 'medallions' in self.file_source:
return self.file_source['medallions']
return None
def create_from_world(self, world):
self.player_range = range(1, world.players + 1)
settings_dict, meta_dict = {}, {}
self.world_rep['meta'] = meta_dict
meta_dict['players'] = world.players
meta_dict['algorithm'] = world.algorithm
meta_dict['seed'] = world.seed
self.world_rep['settings'] = settings_dict
for p in self.player_range:
settings_dict[p] = {}
settings_dict[p]['shuffle'] = world.shuffle[p]
settings_dict[p]['door_shuffle'] = world.doorShuffle[p]
settings_dict[p]['intensity'] = world.intensity[p]
settings_dict[p]['logic'] = world.logic[p]
settings_dict[p]['mode'] = world.mode[p]
settings_dict[p]['swords'] = world.swords[p]
settings_dict[p]['difficulty'] = world.difficulty[p]
settings_dict[p]['goal'] = world.goal[p]
settings_dict[p]['accessibility'] = world.accessibility[p]
settings_dict[p]['item_functionality'] = world.difficulty_adjustments[p]
settings_dict[p]['retro'] = world.retro[p]
settings_dict[p]['hints'] = world.hints[p]
settings_dict[p]['shopsanity'] = world.shopsanity[p]
settings_dict[p]['dropshuffle'] = world.dropshuffle[p]
settings_dict[p]['pottery'] = world.pottery[p]
settings_dict[p]['mixed_travel'] = world.mixed_travel[p]
settings_dict[p]['standardize_palettes'] = world.standardize_palettes[p]
settings_dict[p]['dungeon_counters'] = world.dungeon_counters[p]
settings_dict[p]['crystals_gt'] = world.crystals_gt_orig[p]
settings_dict[p]['crystals_ganon'] = world.crystals_ganon_orig[p]
settings_dict[p]['experimental'] = world.experimental[p]
settings_dict[p]['openpyramid'] = world.open_pyramid[p]
settings_dict[p]['bigkeyshuffle'] = world.bigkeyshuffle[p]
settings_dict[p]['keyshuffle'] = world.keyshuffle[p]
settings_dict[p]['mapshuffle'] = world.mapshuffle[p]
settings_dict[p]['compassshuffle'] = world.compassshuffle[p]
settings_dict[p]['shufflebosses'] = world.boss_shuffle[p]
settings_dict[p]['shuffleenemies'] = world.enemy_shuffle[p]
settings_dict[p]['enemy_health'] = world.enemy_health[p]
settings_dict[p]['enemy_damage'] = world.enemy_damage[p]
settings_dict[p]['shufflepots'] = world.potshuffle[p]
settings_dict[p]['bombbag'] = world.bombbag[p]
settings_dict[p]['shufflelinks'] = world.shufflelinks[p]
settings_dict[p]['overworld_map'] = world.overworld_map[p]
settings_dict[p]['pseudoboots'] = world.pseudoboots[p]
settings_dict[p]['triforce_goal'] = world.treasure_hunt_count[p]
settings_dict[p]['triforce_pool'] = world.treasure_hunt_total[p]
settings_dict[p]['beemizer'] = world.beemizer[p]
# rom adjust stuff
# settings_dict[p]['sprite'] = world.sprite[p]
# settings_dict[p]['disablemusic'] = world.disablemusic[p]
# settings_dict[p]['quickswap'] = world.quickswap[p]
# settings_dict[p]['reduce_flashing'] = world.reduce_flashing[p]
# settings_dict[p]['fastmenu'] = world.fastmenu[p]
# settings_dict[p]['heartcolor'] = world.heartcolor[p]
# settings_dict[p]['heartbeep'] = world.heartbeep[p]
# settings_dict[p]['ow_palettes'] = world.ow_palettes[p]
# settings_dict[p]['uw_palettes'] = world.uw_palettes[p]
# settings_dict[p]['shuffle_sfx'] = world.shuffle_sfx[p]
# more settings?
def record_info(self, world):
self.world_rep['bosses'] = bosses = {}
self.world_rep['start_inventory'] = start_inv = {}
for p in self.player_range:
bosses[p] = {}
start_inv[p] = []
for dungeon in world.dungeons:
for level, boss in dungeon.bosses.items():
location = dungeon.name if level is None else f'{dungeon.name} ({level})'
if boss and 'Agahnim' not in boss.name:
bosses[dungeon.player][location] = boss.name
for item in world.precollected_items:
start_inv[item.player].append(item.name)
def record_item_pool(self, world):
self.world_rep['item_pool'] = item_pool = {}
self.world_rep['medallions'] = medallions = {}
for p in self.player_range:
item_pool[p] = defaultdict(int)
medallions[p] = {}
for item in world.itempool:
item_pool[item.player][item.name] += 1
for p, req_medals in world.required_medallions.items():
medallions[p]['Misery Mire'] = req_medals[0]
medallions[p]['Turtle Rock'] = req_medals[1]
def record_item_placements(self, world):
self.world_rep['placements'] = placements = {}
for p in self.player_range:
placements[p] = {}
for location in world.get_locations():
if location.type != LocationType.Logical and not location.skip:
if location.player != location.item.player:
placements[location.player][location.name] = f'{location.item.name}#{location.item.player}'
else:
placements[location.player][location.name] = location.item.name
def record_entrances(self, world):
self.world_rep['entrances'] = entrances = {}
world.custom_entrances = {}
for p in self.player_range:
connections = entrances[p] = {}
connections['entrances'] = {}
connections['exits'] = {}
connections['two-way'] = {}
for key, data in world.spoiler.entrances.items():
player = data['player'] if 'player' in data else 1
connections = entrances[player]
sub = 'two-way' if data['direction'] == 'both' else 'exits' if data['direction'] == 'exit' else 'entrances'
connections[sub][data['entrance']] = data['exit']
def record_doors(self, world):
self.world_rep['doors'] = doors = {}
for p in self.player_range:
meta_doors = doors[p] = {}
lobbies = meta_doors['lobbies'] = {}
door_map = meta_doors['doors'] = {}
for portal in world.dungeon_portals[p]:
lobbies[portal.name] = portal.door.name
door_types = {DoorType.Normal, DoorType.SpiralStairs, DoorType.Interior}
if world.intensity[p] > 1:
door_types.update([DoorType.Open, DoorType.StraightStairs, DoorType.Ladder])
door_kinds, skip = {}, set()
for key, info in world.spoiler.doorTypes.items():
if key[1] == p:
if ' <-> ' in info['doorNames']:
dns = info['doorNames'].split(' <-> ')
for dn in dns:
door_kinds[dn] = info['type'] # Key Door, Bomb Door, Dash Door
else:
door_kinds[info['doorNames']] = info['type']
for door in world.doors:
if door.player == p and not door.entranceFlag and door.type in door_types and door not in skip:
if door.type == DoorType.Interior:
if door.name in door_types:
door_value = {'type': door_kinds[door.name]}
door_map[door.name] = door_value # intra-tile note
skip.add(door.dest)
elif door.dest:
if door.dest.dest == door:
door_value = door.dest.name
skip.add(door.dest)
if door.name in door_kinds:
door_value = {'dest': door_value, 'type': door_kinds[door.name]}
if door.name not in door_kinds and door.dest.name in door_kinds:
# tricky swap thing
door_value = {'dest': door.name, 'type': door_kinds[door.dest.name]}
door = door.dest # this is weird
elif door.name in door_kinds:
door_value = {'dest': door.dest.name, 'one-way': True, 'type': door_kinds[door.name]}
else:
door_value = {'dest': door.dest.name, 'one-way': True}
door_map[door.name] = door_value
def record_medallions(self):
pass
def write_to_file(self, destination):
yaml.add_representer(defaultdict, Representer.represent_dict)
with open(destination, 'w') as file:
yaml.dump(self.world_rep, file)
def load_yaml(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:
return yaml.load(f, Loader=yaml.SafeLoader)
except Exception as e:
raise Exception(f'Failed to read customizer file: {e}')

View File

@@ -122,6 +122,7 @@ SETTINGSTOPROCESS = {
"createspoiler": "create_spoiler",
"createrom": "create_rom",
"calcplaythrough": "calc_playthrough",
"print_custom_yaml": "print_custom_yaml",
"usestartinventory": "usestartinventory",
"usecustompool": "custom",
"saveonexit": "saveonexit"

View File

@@ -79,6 +79,48 @@ def generation_page(parent,settings):
# frame: pack
self.widgets[widget].pieces["frame"].pack(fill=X)
self.frames["customizer"] = Frame(self)
self.frames["customizer"].pack(anchor=W, fill=X)
## Customizer file
# This one's more-complicated, build it and stuff it
# widget ID
widget = "customizer"
# Empty object
self.widgets[widget] = Empty()
# pieces
self.widgets[widget].pieces = {}
# frame
self.widgets[widget].pieces["frame"] = Frame(self.frames["customizer"])
# frame: label
self.widgets[widget].pieces["frame"].label = Label(self.widgets[widget].pieces["frame"], text='Customizer File: ')
# storage var
self.widgets[widget].storageVar = StringVar()
# textbox
self.widgets[widget].pieces["textbox"] = Entry(self.widgets[widget].pieces["frame"],
textvariable=self.widgets[widget].storageVar)
self.widgets[widget].storageVar.set(settings["customizer"])
# FIXME: Translate these
def FileSelect():
file = filedialog.askopenfilename(filetypes=[("Yaml Files", (".yaml", ".yml")), ("All Files", "*")],
initialdir=os.path.join("."))
self.widgets["customizer"].storageVar.set(file)
# dialog button
self.widgets[widget].pieces["button"] = Button(self.widgets[widget].pieces["frame"],
text='Select File', command=FileSelect)
# frame label: pack
self.widgets[widget].pieces["frame"].label.pack(side=LEFT)
# textbox: pack
self.widgets[widget].pieces["textbox"].pack(side=LEFT, fill=X, expand=True)
# button: pack
self.widgets[widget].pieces["button"].pack(side=LEFT)
# frame: pack
self.widgets[widget].pieces["frame"].pack(fill=X)
## Run Diagnostics
# This one's more-complicated, build it and stuff it
# widget ID

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,190 @@
import argparse
import RaceRandom as random
import urllib.request
import urllib.parse
import yaml
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:
return yaml.load(f, Loader=yaml.SafeLoader)
except Exception as e:
raise Exception(f'Failed to read weights file: {e}')
def roll_settings(weights):
def get_choice(option, root=None):
root = weights if root is None else root
if option not in root:
return None
if type(root[option]) is not dict:
return root[option]
if not root[option]:
return None
return random.choices(list(root[option].keys()), weights=list(map(int, root[option].values())))[0]
def get_choice_default(option, root=weights, default=None):
choice = get_choice(option, root)
if choice is None and default is not None:
return default
return choice
while True:
subweights = weights.get('subweights', {})
if len(subweights) == 0:
break
chances = ({k: int(v['chance']) for (k, v) in subweights.items()})
subweight_name = random.choices(list(chances.keys()), weights=list(chances.values()))[0]
subweights = weights.get('subweights', {}).get(subweight_name, {}).get('weights', {})
subweights['subweights'] = subweights.get('subweights', {})
weights = {**weights, **subweights}
ret = argparse.Namespace()
ret.algorithm = get_choice('algorithm')
ret.mystery = get_choice_default('mystery', default=True)
glitch_map = {'none': 'noglitches', 'no_logic': 'nologic', 'owglitches': 'owglitches',
'minorglitches': 'minorglitches'}
glitches_required = get_choice('glitches_required')
if glitches_required is not None:
if glitches_required not in glitch_map.keys():
print(f'Logic did not match one of: {", ".join(glitch_map.keys())}')
glitches_required = 'none'
ret.logic = glitch_map[glitches_required]
# item_placement = get_choice('item_placement')
# not supported in ER
dungeon_items = get_choice('dungeon_items')
dungeon_items = '' if dungeon_items == 'standard' or dungeon_items is None else dungeon_items
dungeon_items = 'mcsb' if dungeon_items == 'full' else dungeon_items
ret.mapshuffle = get_choice('map_shuffle') == 'on' if 'map_shuffle' in weights else 'm' in dungeon_items
ret.compassshuffle = get_choice('compass_shuffle') == 'on' if 'compass_shuffle' in weights else 'c' in dungeon_items
ret.keyshuffle = get_choice('smallkey_shuffle') == 'on' if 'smallkey_shuffle' in weights else 's' in dungeon_items
ret.bigkeyshuffle = get_choice('bigkey_shuffle') == 'on' if 'bigkey_shuffle' in weights else 'b' in dungeon_items
ret.accessibility = get_choice('accessibility')
ret.restrict_boss_items = get_choice('restrict_boss_items')
entrance_shuffle = get_choice('entrance_shuffle')
ret.shuffle = entrance_shuffle if entrance_shuffle != 'none' else 'vanilla'
overworld_map = get_choice('overworld_map')
ret.overworld_map = overworld_map if overworld_map != 'default' else 'default'
door_shuffle = get_choice('door_shuffle')
ret.door_shuffle = door_shuffle if door_shuffle != 'none' else 'vanilla'
ret.intensity = get_choice('intensity')
ret.experimental = get_choice('experimental') == 'on'
ret.collection_rate = get_choice('collection_rate') == 'on'
ret.dungeon_counters = get_choice('dungeon_counters') if 'dungeon_counters' in weights else 'default'
if ret.dungeon_counters == 'default':
ret.dungeon_counters = 'pickup' if ret.door_shuffle != 'vanilla' or ret.compassshuffle == 'on' else 'off'
ret.shufflelinks = get_choice('shufflelinks') == 'on'
ret.pseudoboots = get_choice('pseudoboots') == 'on'
ret.shopsanity = get_choice('shopsanity') == 'on'
keydropshuffle = get_choice('keydropshuffle') == 'on'
ret.dropshuffle = get_choice('dropshuffle') == 'on' or keydropshuffle
ret.pottery = get_choice('pottery') if 'pottery' in weights else 'none'
ret.pottery = 'keys' if ret.pottery == 'none' and keydropshuffle else ret.pottery
ret.shufflepots = get_choice('pot_shuffle') == 'on'
ret.mixed_travel = get_choice('mixed_travel') if 'mixed_travel' in weights else 'prevent'
ret.standardize_palettes = (get_choice('standardize_palettes') if 'standardize_palettes' in weights
else 'standardize')
goal = get_choice('goals')
if goal is not None:
ret.goal = {'ganon': 'ganon',
'fast_ganon': 'crystals',
'dungeons': 'dungeons',
'pedestal': 'pedestal',
'triforce-hunt': 'triforcehunt'
}[goal]
ret.openpyramid = goal == 'fast_ganon' if ret.shuffle in ['vanilla', 'dungeonsfull', 'dungeonssimple'] else False
ret.crystals_gt = get_choice('tower_open')
ret.crystals_ganon = get_choice('ganon_open')
goal_min = get_choice_default('triforce_goal_min', default=20)
goal_max = get_choice_default('triforce_goal_max', default=20)
pool_min = get_choice_default('triforce_pool_min', default=30)
pool_max = get_choice_default('triforce_pool_max', default=30)
ret.triforce_goal = random.randint(int(goal_min), int(goal_max))
min_diff = get_choice_default('triforce_min_difference', default=10)
ret.triforce_pool = random.randint(max(int(pool_min), ret.triforce_goal + int(min_diff)), int(pool_max))
ret.mode = get_choice('world_state')
if ret.mode == 'retro':
ret.mode = 'open'
ret.retro = True
ret.retro = get_choice('retro') == 'on' # this overrides world_state if used
ret.bombbag = get_choice('bombbag') == 'on'
ret.hints = get_choice('hints') == 'on'
swords = get_choice('weapons')
if swords is not None:
ret.swords = {'randomized': 'random',
'assured': 'assured',
'vanilla': 'vanilla',
'swordless': 'swordless'
}[swords]
ret.difficulty = get_choice('item_pool')
ret.item_functionality = get_choice('item_functionality')
old_style_bosses = {'basic': 'simple',
'normal': 'full',
'chaos': 'random'}
boss_choice = get_choice('boss_shuffle')
if boss_choice in old_style_bosses.keys():
boss_choice = old_style_bosses[boss_choice]
ret.shufflebosses = boss_choice
enemy_choice = get_choice('enemy_shuffle')
if enemy_choice == 'chaos':
enemy_choice = 'random'
ret.shuffleenemies = enemy_choice
old_style_damage = {'none': 'default',
'chaos': 'random'}
damage_choice = get_choice('enemy_damage')
if damage_choice in old_style_damage:
damage_choice = old_style_damage[damage_choice]
ret.enemy_damage = damage_choice
ret.enemy_health = get_choice('enemy_health')
ret.beemizer = get_choice('beemizer') if 'beemizer' in weights else '0'
inventoryweights = weights.get('startinventory', {})
startitems = []
for item in inventoryweights.keys():
if get_choice(item, inventoryweights) == 'on':
startitems.append(item)
ret.startinventory = ','.join(startitems)
if len(startitems) > 0:
ret.usestartinventory = True
if 'rom' in weights:
romweights = weights['rom']
ret.sprite = get_choice('sprite', romweights)
ret.disablemusic = get_choice('disablemusic', romweights) == 'on'
ret.quickswap = get_choice('quickswap', romweights) == 'on'
ret.reduce_flashing = get_choice('reduce_flashing', romweights) == 'on'
ret.fastmenu = get_choice('menuspeed', romweights)
ret.heartcolor = get_choice('heartcolor', romweights)
ret.heartbeep = get_choice('heartbeep', romweights)
ret.ow_palettes = get_choice('ow_palettes', romweights)
ret.uw_palettes = get_choice('uw_palettes', romweights)
ret.shuffle_sfx = get_choice('shuffle_sfx', romweights) == 'on'
return ret

View File

@@ -0,0 +1,154 @@
import RaceRandom as random
import logging
import time
from collections import Counter, defaultdict
from source.overworld.EntranceShuffle2 import link_entrances_new
from EntranceShuffle import link_entrances, link_inverted_entrances
from BaseClasses import World
from Regions import create_regions, create_dungeon_regions
from InvertedRegions import create_inverted_regions
# tested: open + crossed (lh) Mar. 17 (made changes)
# tested: open + simple (lh) Mar. 22
def run_stats():
# logging.basicConfig(format='%(message)s', level=logging.DEBUG)
random.seed(None)
tests = 10000
for main_mode in ['open', 'standard', 'inverted']:
for shuffle_mode in ['dungeonssimple', 'dungeonsfull',
'simple', 'restricted', 'full', 'crossed']:
for ls in [True, False]:
if ls and (main_mode == 'standard' or shuffle_mode in ['dungeonssimple', 'dungeonsfull']):
continue
def runner_new(world):
link_entrances_new(world, 1)
def runner_old(world):
if main_mode == 'inverted':
link_inverted_entrances(world, 1)
else:
link_entrances(world, 1)
compare_tests(tests, shuffle_mode, main_mode, ls, runner_old, runner_new)
def run_test_stats():
# logging.basicConfig(format='%(message)s', level=logging.DEBUG)
random.seed(None)
tests = 10000
for main_mode in ['open']:
for shuffle_mode in [#'dungeonssimple', 'dungeonsfull',
'simple']:#, 'restricted', 'full', 'crossed']:
for ls in [True, False]:
if ls and (main_mode == 'standard' or shuffle_mode in ['dungeonssimple', 'dungeonsfull']):
continue
def runner_new(world):
link_entrances_new(world, 1)
run_tests(tests, shuffle_mode, main_mode, ls, runner_new)
def run_old_stats():
# logging.basicConfig(format='%(message)s', level=logging.DEBUG)
random.seed(None)
tests = 10000
for main_mode in ['open']:
for shuffle_mode in [#'dungeonssimple', 'dungeonsfull',
'simple']:#, 'restricted', 'full', 'crossed']:
for ls in [True, False]:
if ls and (main_mode == 'standard' or shuffle_mode in ['dungeonssimple', 'dungeonsfull']):
continue
def runner(world):
if main_mode == 'inverted':
link_inverted_entrances(world, 1)
else:
link_entrances(world, 1)
run_tests(tests, shuffle_mode, main_mode, ls, runner)
def compare_tests(tests, shuffle_mode, main_mode, links=False, runner_old=None, runner_new=None):
print(f'Testing {shuffle_mode} {main_mode}')
entrance_set = set()
exit_set = set()
ctr = defaultdict(Counter)
start = time.time()
test_loop(tests, entrance_set, exit_set, ctr, shuffle_mode, main_mode, links, runner_old)
print(f'Old test took {time.time() - start}s')
start = time.time()
test_loop(tests, entrance_set, exit_set, ctr, shuffle_mode, main_mode, links, runner_new, -1)
# test_loop(tests, entrance_set, exit_set, ctr, shuffle_mode, main_mode, links, runner_new)
print(f'New test took {time.time() - start}s')
dump_file(tests, entrance_set, exit_set, ctr, shuffle_mode, main_mode, links)
def run_tests(tests, shuffle_mode, main_mode, links=False, runner=None):
print(f'Testing {shuffle_mode} {main_mode}')
entrance_set = set()
exit_set = set()
ctr = defaultdict(Counter)
test_loop(tests, entrance_set, exit_set, ctr, shuffle_mode, main_mode, links, runner)
def test_loop(tests, entrance_set, exit_set, ctr, shuffle_mode, main_mode, links, runner, value=1):
for i in range(0, tests):
if i % 1000 == 0:
print(f'Test {i}')
seed = random.randint(0, 999999999)
# seed = 635441530
random.seed(seed)
world = World(1, {1: shuffle_mode}, {1: 'vanilla'}, {1: 'noglitches'}, {1: main_mode}, {}, {}, {},
{}, {}, {}, {}, {}, True, {}, {}, [], {})
world.customizer = False
world.shufflelinks = {1: links}
if world.mode[1] != 'inverted':
create_regions(world, 1)
else:
create_inverted_regions(world, 1)
create_dungeon_regions(world, 1)
# print(f'Linking seed {seed}')
# try:
runner(world)
# except Exception as e:
# print(f'Failure during seed {seed} with {e}')
for data in world.spoiler.entrances.values():
ent = data['entrance']
ext = data['exit']
# drc = data['direction']
entrance_set.add(ent)
exit_set.add(ext)
ctr[ent][ext] += value
def dump_file(tests, entrance_set, exit_set, ctr, shuffle_mode, main_mode, links):
columns = sorted(list(exit_set))
rows = sorted(list(entrance_set))
filename = f'er_stats_{shuffle_mode}_{main_mode}_{"ls" if links else "v"}.csv'
with open(filename, 'w') as stat_file:
stat_file.write(',')
stat_file.write(','.join(columns))
stat_file.write('\n')
for r in rows:
stat_file.write(f'{r},')
formatted = []
for c in columns:
occurance = ctr[r][c] / tests
formatted.append(f'{occurance:.5%}')
stat_file.write(','.join(formatted))
stat_file.write('\n')
if __name__ == "__main__":
# logging.basicConfig(format='%(message)s', level=logging.DEBUG)
# run_tests(1, 'restricted', 'inverted', True, lambda world: link_entrances_new(world, 1))
# run_test_stats()
# run_old_stats()
run_stats()

0
test/stats/__init__.py Normal file
View File