forked from matthewkirby/plando-random-settings
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathroll_settings.py
249 lines (203 loc) · 11 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
import os
import sys
import datetime
import json
import random
import conditionals as conds
from version import version_hash_1, version_hash_2
sys.path.append("randomizer")
from randomizer.SettingsList import get_settings_from_tab, get_setting_info
from randomizer.StartingItems import inventory, songs, equipment
from randomizer.Spoiler import HASH_ICONS
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", weights_fname)
if os.path.isfile(fpath):
with open(fpath) as fin:
datain = json.load(fin)
weight_options = datain["options"] if "options" in datain else None
weight_multiselect = datain["multiselect"] if "multiselect" in datain else None
weight_dict = datain["weights"]
return weight_options, 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 geometric_weights(N, startat=0, rtype="list"):
""" Compute weights according to a geometric distribution """
if rtype == "list":
return [50.0/2**i for i in range(N)]
elif rtype == "dict":
return {str(startat+i): 50.0/2**i for i in range(N)}
def draw_starting_item_pool(random_settings, start_with):
""" Select starting items, songs, and equipment. """
random_settings["starting_items"] = draw_choices_from_pool(inventory)
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 resolve_multiselect_weights(setting, options):
""" Given a multiselect weights block, resolve into the plando options.
A multiselect block should contain the following elements in addition to individual weights
global_enable_percentage [0,100] - the chance at rolling any on in the first place
geometric [true/false] - If true, ignore individual weights and chose a random number
to enable according to the geometric distribution
"""
if random.random()*100 > options["global_enable_percentage"]:
return []
if "geometric" in options.keys() and options["geometric"]:
nopts = len(get_setting_info(setting).choices)
N = random.choices(range(nopts+1), weights=geometric_weights(nopts+1))[0]
return random.sample(list(get_setting_info(setting).choices.keys()), N)
# Randomly draw which multiselects should be enabled
if not "opt_percentage" in options.keys():
return []
return [msopt for msopt, perc in options["opt_percentage"].items() if random.random()*100 < perc]
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_plando(weights, override_weights_fname, no_seed):
# Load the weight dictionary
if weights == "RSL":
weight_options, weight_multiselect, weight_dict = load_weights_file("rsl_season4.json")
if weights == "DEVM":
weight_options, weight_multiselect, weight_dict = load_weights_file("devM.json")
elif weights == "full-random":
weight_options = None
weight_dict = generate_balanced_weights(None)
else:
weight_options, weight_multiselect, weight_dict = load_weights_file(weights)
# If an override_weights file name is provided, load it
start_with = {"starting_items":[], "starting_songs":[], "starting_equipment":[]}
if override_weights_fname is not None:
print(f"RSL GENERATOR: LOADING OVERRIDE WEIGHTS from {override_weights_fname}")
override_options, 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 multiselects
if override_multiselect is not None:
for key, value in override_multiselect.items():
weight_multiselect[key] = value
####################################################################################
# Make a new function that parses the weights file that does this stuff
####################################################################################
# Generate even weights for tokens 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_multiselect is not None:
for setting, options in weight_multiselect.items():
random_settings[setting] = resolve_multiselect_weights(setting, options)
# Add starting items, conditionals, tricks and excluded locations
if weight_options is not None:
if "conditionals" in weight_options:
conds.parse_conditionals(weight_options["conditionals"], weight_dict, random_settings, start_with)
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"]
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_items", "starting_songs", "starting_equipment", "hint_dist_user", "dungeon_shortcuts"] + list(weight_multiselect.keys()):
raise NotImplementedError(f'{setting} has an unsupported setting type: {setting_type!r}')
random_settings[setting] = value
# Save the output plando
output = {
"settings": random_settings,
"file_hash": [version_hash_1, version_hash_2, *random.choices(HASH_ICONS, k=3)]
}
plando_filename = f'random_settings_{datetime.datetime.utcnow():%Y-%m-%d_%H-%M-%S_%f}.json'
# plando_filename = f'random_settings.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