Customizer main work
This commit is contained in:
@@ -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}
|
||||
|
||||
21
Bosses.py
21
Bosses.py
@@ -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
25
CLI.py
@@ -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',
|
||||
|
||||
254
DoorShuffle.py
254
DoorShuffle.py
@@ -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'
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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'),
|
||||
|
||||
2
Fill.py
2
Fill.py
@@ -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
2
Gui.py
@@ -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):
|
||||
|
||||
129
ItemList.py
129
ItemList.py
@@ -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
43
Main.py
@@ -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 = {}
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
92
Rom.py
@@ -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'
|
||||
|
||||
12
RoomData.py
12
RoomData.py
@@ -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__())
|
||||
|
||||
|
||||
2
Rules.py
2
Rules.py
@@ -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
182
docs/Customizer.md
Normal 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.
|
||||
|
||||
|
||||
108
docs/customizer_example.yaml
Normal file
108
docs/customizer_example.yaml
Normal 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
|
||||
|
||||
17
docs/multi_mystery_example.yaml
Normal file
17
docs/multi_mystery_example.yaml
Normal 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
19
docs/player1.yml
Normal 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
16
docs/player2.yml
Normal 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
25
docs/player3.yml
Normal 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
|
||||
@@ -132,12 +132,10 @@
|
||||
"full",
|
||||
"crossed",
|
||||
"insanity",
|
||||
"restricted_legacy",
|
||||
"full_legacy",
|
||||
"madness_legacy",
|
||||
"insanity_legacy",
|
||||
"dungeonsfull",
|
||||
"dungeonssimple"
|
||||
"dungeonssimple",
|
||||
"lite",
|
||||
"lean"
|
||||
]
|
||||
},
|
||||
"door_shuffle": {
|
||||
|
||||
@@ -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": [
|
||||
|
||||
@@ -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",
|
||||
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"bps": { "type": "checkbox" },
|
||||
"createspoiler": { "type": "checkbox" },
|
||||
"calcplaythrough": { "type": "checkbox" },
|
||||
"print_custom_yaml": { "type": "checkbox" },
|
||||
"usestartinventory": { "type": "checkbox" },
|
||||
"usecustompool": { "type": "checkbox" }
|
||||
}
|
||||
|
||||
329
source/classes/CustomSettings.py
Normal file
329
source/classes/CustomSettings.py
Normal 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}')
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
3003
source/overworld/EntranceShuffle2.py
Normal file
3003
source/overworld/EntranceShuffle2.py
Normal file
File diff suppressed because it is too large
Load Diff
190
source/tools/MysteryUtils.py
Normal file
190
source/tools/MysteryUtils.py
Normal 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
|
||||
154
test/stats/EntranceShuffleStats.py
Normal file
154
test/stats/EntranceShuffleStats.py
Normal 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
0
test/stats/__init__.py
Normal file
Reference in New Issue
Block a user