-
Notifications
You must be signed in to change notification settings - Fork 11
/
Copy pathroll_settings.py
282 lines (232 loc) · 13.1 KB
/
roll_settings.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
import os
import sys
import datetime
import json
import random
import conditionals as conds
from multiselects import resolve_multiselects, ms_option_lookup
from rslversion import __version__
sys.path.append("randomizer")
from randomizer.ItemPool import trade_items, child_trade_items
from randomizer.SettingsList import get_settings_from_tab, get_settings_from_section, SettingInfos
from randomizer.StartingItems import inventory, songs, equipment
from utils import geometric_weights
def get_setting_info(setting_name):
""" Quick replacement for removed function in the randomizer. """
return SettingInfos.setting_infos[setting_name]
def load_weights_file(weights_fname):
""" Given a weights filename, open it up. If the file does not exist, make it with even weights """
fpath = os.path.join(weights_fname)
if os.path.isfile(fpath):
with open(fpath) as fin:
datain = json.load(fin)
return parse_weights(datain)
def parse_weights(datain):
weight_options = datain["options"] if "options" in datain else None
conditionals = datain["conditionals"] if "conditionals" in datain else None
weight_multiselect = datain["multiselect"] if "multiselect" in datain else None
weight_dict = datain["weights"]
return weight_options, conditionals, weight_multiselect, weight_dict
def generate_balanced_weights(fname="default_weights.json"):
""" Generate a file with even weights for each setting. """
settings_to_randomize = list(get_settings_from_tab("main_tab"))[1:] + \
list(get_settings_from_tab("detailed_tab")) + \
list(get_settings_from_tab("other_tab")) + \
list(get_settings_from_tab("starting_tab"))
exclude_from_weights = ["bridge_tokens", "ganon_bosskey_tokens", "bridge_hearts", "ganon_bosskey_hearts",
"triforce_goal_per_world", "triforce_count_per_world", "disabled_locations",
"allowed_tricks", "starting_equipment", "starting_items", "starting_songs"]
weight_dict = {}
for name in settings_to_randomize:
if name not in exclude_from_weights:
opts = list(get_setting_info(name).choices.keys())
optsdict = {o: 100./len(opts) for o in opts}
weight_dict[name] = optsdict
if fname is not None:
with open(fname, 'w') as fp:
json.dump(weight_dict, fp, indent=4)
return weight_dict
def draw_starting_item_pool(random_settings, start_with):
""" Select starting items, songs, and equipment. """
random_settings["starting_inventory"] = draw_choices_from_pool({
name: info
for name, info in inventory.items()
if (info.item_name not in trade_items or info.item_name in random_settings["adult_trade_start"])
and (info.item_name not in child_trade_items or info.item_name in random_settings["shuffle_child_trade"] or info.item_name == 'Zeldas Letter')
})
random_settings["starting_songs"] = draw_choices_from_pool(songs)
random_settings["starting_equipment"] = draw_choices_from_pool(equipment)
for key, val in start_with.items():
for thing in val:
if thing not in random_settings[key]:
random_settings[key] += [thing]
def draw_choices_from_pool(itempool):
N = random.choices(range(len(itempool)), weights=geometric_weights(len(itempool)))[0]
return random.sample(list(itempool.keys()), N)
def remove_plando_if_random(random_settings):
""" For settings that have a _random option, remove the specific plando if _random is true """
settings_to_check = ["trials", "chicken_count", "big_poe_count"]
for setting in settings_to_check:
if random_settings[setting+'_random'] == "true":
random_settings.pop(setting)
def remove_redundant_settings(random_settings):
""" Disable settings that the randomizer expects to be disabled.
The randomizer will reject plandos with disabled settings set to non-default values.
Settings are considered disabled if they are disabled in the randomizer GUI by another setting.
"""
settings_list = list(random_settings.keys())
for setting in settings_list:
# As we're iterating, the setting may already have been deleted/disabled by a previous setting
if setting in random_settings.keys():
info = get_setting_info(setting)
choice = random_settings[setting]
if info.disable is not None:
for option, disabling in info.disable.items():
negative = False
if isinstance(option, str) and option[0] == '!':
negative = True
option = option[1:]
if (choice == option) != negative:
for other_setting in disabling.get('settings', []):
remove_disabled_setting(random_settings, other_setting)
for section in disabling.get('sections', []):
for other_setting in get_settings_from_section(section):
remove_disabled_setting(random_settings, other_setting)
for tab in disabling.get('tabs', []):
for other_setting in get_settings_from_tab(tab):
remove_disabled_setting(random_settings, other_setting)
def remove_disabled_setting(random_settings, other_setting):
if other_setting in random_settings.keys():
random_settings.pop(other_setting)
def draw_dungeon_shortcuts(random_settings):
""" Decide how many dungeon shortcuts to enable and randomly select them """
N = random.choices(range(8), weights=geometric_weights(8))[0]
dungeon_shortcuts_opts = ['deku_tree', 'dodongos_cavern', 'jabu_jabus_belly', 'forest_temple', 'fire_temple', 'shadow_temple', 'spirit_temple']
random_settings["dungeon_shortcuts"] = random.sample(dungeon_shortcuts_opts, N)
def generate_weights_override(weights, override_weights_fname):
# Load the weight dictionary
if weights == "RSL":
weight_options, conditionals, weight_multiselect, weight_dict = load_weights_file("weights/rsl_season7.json")
elif weights == "full-random":
weight_options = None
weight_dict = generate_balanced_weights(None)
else:
weight_options, conditionals, weight_multiselect, weight_dict = load_weights_file(weights)
# If an override_weights file name is provided, load it
start_with = {"starting_inventory":[], "starting_songs":[], "starting_equipment":[]}
if override_weights_fname is not None:
if override_weights_fname == '-':
print("RSL GENERATOR: LOADING OVERRIDE WEIGHTS from standard input")
override_options, override_conditionals, override_multiselect, override_weights = parse_weights(json.load(sys.stdin))
else:
print(f"RSL GENERATOR: LOADING OVERRIDE WEIGHTS from {override_weights_fname}")
override_options, override_conditionals, override_multiselect, override_weights = load_weights_file(override_weights_fname)
# Check for starting items, songs and equipment
for key in start_with.keys():
if key in override_weights.keys():
start_with[key] = override_weights[key]
override_weights.pop(key)
# Replace the options
if override_options is not None:
for key, value in override_options.items():
# Handling overwrite
if not (key.startswith("extra_") or key.startswith("remove_")):
weight_options[key] = value
continue
# Handling extras
if key.startswith("extra_"):
option = key.split("extra_")[1]
if option not in weight_options:
weight_options[option] = value
else: # Both existing options and extra options
if isinstance(weight_options[option], dict):
weight_options[option].update(value)
else:
weight_options[option] += value
weight_options[option] = list(set(weight_options[option]))
# Handling removes
if key.startswith("remove_"):
option = key.split("remove_")[1]
if option in weight_options:
for item in value:
if item in weight_options[option]:
weight_options[option].remove(item)
# Replace the weights
for key, value in override_weights.items():
weight_dict[key] = value
# Replace the conditionals
if override_conditionals is not None:
for name, state in override_conditionals.items():
conditionals[name] = state
# Replace the multiselects
if override_multiselect is not None:
for key, value in override_multiselect.items():
weight_multiselect[key] = value
return weight_options, conditionals, weight_multiselect, weight_dict, start_with
def generate_plando(weights, override_weights_fname, no_seed, plando_filename_base='random_settings'):
weight_options, conditionals, weight_multiselects, weight_dict, start_with = generate_weights_override(weights, override_weights_fname)
####################################################################################
# Make a new function that parses the weights file that does this stuff
####################################################################################
# Generate even weights for tokens, hearts, and triforce pieces given the max value (Maybe put this into the step that loads the weights)
for nset in ["bridge_tokens", "ganon_bosskey_tokens", "bridge_hearts", "ganon_bosskey_hearts", "triforce_goal_per_world", "triforce_count_per_world"]:
kwx = nset + "_max"
kwn = nset + "_min"
nmax = weight_options[kwx] if kwx in weight_options else 100
nmin = weight_options[kwn] if kwn in weight_options else 1
weight_dict[nset] = {i: 100./(nmax - nmin + 1) for i in range(nmin, nmax + 1)}
if kwx in weight_dict:
weight_dict.pop(kwx)
if kwn in weight_dict:
weight_dict.pop(kwn)
####################################################################################
# Draw the random settings
random_settings = {}
for setting, options in weight_dict.items():
random_settings[setting] = random.choices(list(options.keys()), weights=list(options.values()))[0]
# Draw the multiselects
if weight_multiselects is not None:
random_settings.update(resolve_multiselects(weight_multiselects))
# Set the conditionals
if conditionals is not None:
conds.parse_conditionals(conditionals, weight_dict, random_settings, start_with)
# Add starting items, tricks, and excluded locations
if weight_options is not None:
if "tricks" in weight_options:
random_settings["allowed_tricks"] = weight_options["tricks"]
if "disabled_locations" in weight_options:
random_settings["disabled_locations"] = weight_options["disabled_locations"]
random_settings["misc_hints"] = weight_options["misc_hints"] if "misc_hints" in weight_options else []
if "starting_items" in weight_options and weight_options["starting_items"] == True:
draw_starting_item_pool(random_settings, start_with)
# Remove plando setting if a _random setting is true
remove_plando_if_random(random_settings)
# Format numbers and bools to not be strings
for setting, value in random_settings.items():
setting_type = get_setting_info(setting).type
if setting_type is bool:
if value == "true":
value = True
elif value == "false":
value = False
else:
raise TypeError(f'Value for setting {setting!r} must be "true" or "false"')
elif setting_type is int:
value = int(value)
elif setting_type is not str and setting not in ["allowed_tricks", "disabled_locations", "starting_inventory", "misc_hints", "starting_songs", "starting_equipment", "hint_dist_user", "dungeon_shortcuts"] + list(ms_option_lookup.keys()):
raise NotImplementedError(f'{setting} has an unsupported setting type: {setting_type!r}')
random_settings[setting] = value
# Remove conflicting "dead" settings since rando won't ignore them anymore
remove_redundant_settings(random_settings)
# Add the RSL Script version to the plando
random_settings['user_message'] = f'RSL Script v{__version__}'
# Save the output plando
output = {"settings": random_settings}
plando_filename = f'{plando_filename_base}_{datetime.datetime.now(datetime.timezone.utc):%Y-%m-%d_%H-%M-%S_%f}.json'
if not os.path.isdir("data"):
os.mkdir("data")
with open(os.path.join("data", plando_filename), 'w') as fp:
json.dump(output, fp, indent=4)
if no_seed:
print(f"Plando File: {plando_filename}")
return plando_filename