-
Notifications
You must be signed in to change notification settings - Fork 27
/
Copy pathworldSetup.py
300 lines (268 loc) · 12.9 KB
/
worldSetup.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
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
import patches.enemies
import logic.main
import randomizer
from locations.items import *
from entranceInfo import ENTRANCE_INFO
from patches import bingo
from patches import maze
import cavegen
MULTI_CHEST_OPTIONS = [MAGIC_POWDER, BOMB, MEDICINE, RUPEES_50, RUPEES_20, RUPEES_100, RUPEES_200, RUPEES_500, SEASHELL, GEL, ARROWS_10, SINGLE_ARROW]
MULTI_CHEST_WEIGHTS = [20, 20, 20, 50, 50, 20, 10, 5, 5, 20, 10, 10]
# List of all the possible locations where we can place our starting house
start_locations = [
"phone_d8",
"rooster_house",
"writes_phone",
"castle_phone",
"photo_house",
"start_house",
"prairie_right_phone",
"banana_seller",
"prairie_low_phone",
"animal_phone",
]
class WorldSetup:
def __init__(self):
self.entrance_mapping = {k: f"{k}:inside" for k in ENTRANCE_INFO.keys()}
self.entrance_mapping.update({f"{k}:inside": k for k in ENTRANCE_INFO.keys()})
self.boss_mapping = list(range(9))
self.miniboss_mapping = {
# Main minibosses
0: "ROLLING_BONES", 1: "HINOX", 2: "DODONGO", 3: "CUE_BALL", 4: "GHOMA", 5: "SMASHER", 6: "GRIM_CREEPER", 7: "BLAINO",
# Color dungeon needs to be special, as always.
"c1": "AVALAUNCH", "c2": "GIANT_BUZZ_BLOB",
# Overworld
"moblin_cave": "MOBLIN_KING",
"armos_temple": "ARMOS_KNIGHT",
}
self.goal = None
self.bingo_goals = None
self.sign_maze = None
self.multichest = RUPEES_20
self.map = None # Randomly generated map data
self.cavegen = None
self.dungeon_chain = None
self.inside_to_outside = True
self.keep_two_way = True
self.one_on_one = True
def getEntrancePool(self, settings, connectorsOnly=False):
entrances = []
if connectorsOnly:
if settings.entranceshuffle in {"split", "mixed", "wild", "chaos", "insane", "madness"}:
entrances = [k for k, v in ENTRANCE_INFO.items() if v.type == "connector"]
entrances += [f"{k}:inside" for k in entrances]
return entrances
if settings.dungeonshuffle and settings.entranceshuffle == "none":
entrances = [k for k, v in ENTRANCE_INFO.items() if v.type == "dungeon"]
if settings.entranceshuffle in {"simple", "split", "mixed", "wild", "chaos", "insane", "madness"}:
types = {"single"}
if settings.tradequest:
types.add("trade")
if settings.shufflejunk:
types.update(["dummy", "trade"])
if settings.shuffleannoying:
types.add("insanity")
if settings.shufflewater:
types.add("water")
if settings.randomstartlocation:
types.add("start")
if settings.dungeonshuffle:
types.add("dungeon")
if settings.entranceshuffle in {"mixed", "wild", "chaos", "insane", "madness"}:
types.add("connector")
entrances = [k for k, v in ENTRANCE_INFO.items() if v.type in types]
entrances += [f"{k}:inside" for k in entrances]
return entrances
def _swapEntrances(self, a, b):
# Two two two-way entrances to connect disconnecting islands
assert self.keep_two_way
temp = self.entrance_mapping[a]
self.entrance_mapping[a] = self.entrance_mapping[b]
self.entrance_mapping[b] = temp
self.entrance_mapping[self.entrance_mapping[a]] = a
self.entrance_mapping[self.entrance_mapping[b]] = b
def _injectEntrance(self, source, target):
# Inject an entrance into a chain of entrances with decoupled mode
assert not self.keep_two_way
to_source = None
for k, v in self.entrance_mapping.items():
if v == source:
to_source = k
break
assert to_source is not None
temp = self.entrance_mapping[target]
self.entrance_mapping[target] = source
self.entrance_mapping[to_source] = temp
def _addConnectionTowards(self, sources, rnd, target):
# When no one-on-one requirement is needed, we can simply disconnect
# one of the doulble connected entrances.
entrance_to = {}
for s in sources:
if self.entrance_mapping[s] not in entrance_to:
entrance_to[self.entrance_mapping[s]] = []
entrance_to[self.entrance_mapping[s]].append(s)
options = []
for k, v in entrance_to.items():
if len(v) > 1:
options += v
option = rnd.choice(options)
self.entrance_mapping[option] = target
def inaccessibleEntrances(self, settings, entrancePool):
log = logic.main.Logic(settings, world_setup=self)
return [x for x in entrancePool if log.world.entrances[x].location and log.world.entrances[x].location not in log.location_list]
def _randomizeEntrances(self, rnd, entrancePool):
unmappedEntrances = list(entrancePool)
done = set()
for entrance in [x for x in entrancePool]:
if entrance in done:
continue
while entrance not in done:
pick_idx = rnd.randrange(len(unmappedEntrances))
pick = unmappedEntrances[pick_idx]
if pick == entrance:
if len(unmappedEntrances) < 2:
raise randomizer.Error("Cannot map entrance to itself")
continue
if self.inside_to_outside and entrance.endswith(":inside") == pick.endswith(":inside"):
continue
if self.one_on_one:
unmappedEntrances.pop(pick_idx)
self.entrance_mapping[entrance] = pick
done.add(entrance)
if self.keep_two_way:
unmappedEntrances.remove(entrance)
self.entrance_mapping[pick] = entrance
done.add(pick)
def pickEntrances(self, settings, rnd):
if settings.overworld in {"random", "dungeonchain", "alttp"}:
return
if settings.overworld == "dungeondive":
self.entrance_mapping = {"d%d" % (n): "d%d:inside" % (n) for n in range(9)}
self.entrance_mapping.update({"d%d:inside" % (n): "d%d" % (n) for n in range(9)})
if settings.randomstartlocation and settings.entranceshuffle == "none":
start_location = start_locations[rnd.randrange(len(start_locations))]
if start_location != "start_house":
self.entrance_mapping[start_location] = "start_house:inside"
self.entrance_mapping["start_house:inside"] = start_location
self.entrance_mapping["start_house"] = f"{start_location}:inside"
self.entrance_mapping[f"{start_location}:inside"] = "start_house"
entrancePool = self.getEntrancePool(settings)
self._randomizeEntrances(rnd, entrancePool)
if settings.entranceshuffle == 'split':
# Shuffle connectors among themselves
# entrancePool is intentionally overwritten so we're only swapping connectors
entrancePool = self.getEntrancePool(settings, connectorsOnly=True)
self._randomizeEntrances(rnd, entrancePool)
# Make sure all entrances in the pool are accessible
for _ in range(1000):
islands = self.inaccessibleEntrances(settings, entrancePool)
if not islands:
break
island = rnd.choice(islands)
mains = [x for x in entrancePool if x not in islands]
main = rnd.choice(mains)
if self.inside_to_outside:
if island.endswith(":inside") != main.endswith(":inside"):
continue
if not self.one_on_one:
self._addConnectionTowards(mains, rnd, island)
elif self.keep_two_way:
self._swapEntrances(island, main)
else:
self._injectEntrance(island, main)
if self.inaccessibleEntrances(settings, entrancePool):
raise randomizer.Error("Failed to make all entrances accessible after a bunch of retries")
self._checkEntranceRules()
def _checkEntranceRules(self):
if self.inside_to_outside:
for k, v in self.entrance_mapping.items():
if k.endswith(":inside"):
assert not v.endswith(":inside"), f"inside-to-outside rule violated: {k}->{v}"
else:
assert v.endswith(":inside"), f"inside-to-outside rule violated: {k}->{v}"
if self.keep_two_way:
for k, v in self.entrance_mapping.items():
assert self.entrance_mapping[v] == k, f"keep-two-way rule violated: {k}->{v}"
if self.one_on_one:
found = set()
for k, v in self.entrance_mapping.items():
assert v not in found, f"one-on-one rule violated: {k}->{v}"
found.add(v)
def randomize(self, settings, rnd):
if settings.boss != "default":
values = list(range(9))
if settings.heartcontainers:
# Color dungeon boss does not drop a heart container so we cannot shuffle him when we
# have heart container shuffling
values.remove(8)
self.boss_mapping = []
for n in range(8 if settings.heartcontainers else 9):
value = rnd.choice(values)
self.boss_mapping.append(value)
if value in (3, 6) or settings.boss == "shuffle":
values.remove(value)
if settings.heartcontainers:
self.boss_mapping += [8]
if settings.miniboss != "default":
values = [name for name in self.miniboss_mapping.values()]
for key in self.miniboss_mapping.keys():
self.miniboss_mapping[key] = rnd.choice(values)
if settings.miniboss == 'shuffle':
values.remove(self.miniboss_mapping[key])
if settings.goal == 'random':
self.goal = rnd.randint(-1, 8)
elif settings.goal == 'open':
self.goal = -1
elif settings.goal in {"seashells", "bingo", "bingo-double", "bingo-triple", "bingo-full"}:
self.goal = settings.goal
elif settings.goal in {"maze"}:
self.goal = settings.goal
self.sign_maze = maze.buildMaze(rnd)
elif settings.goal == "specific":
instruments = [c for c in "12345678"]
rnd.shuffle(instruments)
self.goal = "=" + "".join(instruments[:4])
elif "-" in settings.goal:
a, b = settings.goal.split("-")
if a == "open":
a = -1
self.goal = rnd.randint(int(a), int(b))
else:
self.goal = int(settings.goal)
if self.goal in {"bingo", "bingo-double", "bingo-triple", "bingo-full"}:
self.bingo_goals = bingo.randomizeGoals(rnd, settings)
if settings.overworld == "dungeonchain":
self._buildDungeonChain(rnd)
self.multichest = rnd.choices(MULTI_CHEST_OPTIONS, MULTI_CHEST_WEIGHTS)[0]
self.inside_to_outside = settings.entranceshuffle not in {"wild", "insane", "madness"}
self.keep_two_way = settings.entranceshuffle not in {"chaos", "insane", "madness"}
self.one_on_one = settings.entranceshuffle not in {"madness"}
self.pickEntrances(settings, rnd)
def _buildDungeonChain(self, rnd):
# Build a chain of 5 dungeons
self.dungeon_chain = [1, 2, 3, 4, 5, 6, 7, 8]
if rnd.randrange(0, 100) < 50: # Reduce the chance D0 is in the chain.
self.dungeon_chain.append(0)
rnd.shuffle(self.dungeon_chain)
self.dungeon_chain = self.dungeon_chain[:5]
# Check if we randomly replace one of the dungeons with a cavegen
if rnd.randrange(0, 100) < 80:
self.cavegen = cavegen.Generator(rnd)
self.cavegen.generate()
# cavegen.dump("cave.svg", self.cavegen.start)
self.dungeon_chain[rnd.randint(0, len(self.dungeon_chain) - 2)] = "cavegen"
# Check if we want a random extra insert.
if rnd.randrange(0, 100) < 80:
inserts = ["shop", "mamu", "trendy", "dream", "chestcave"]
self.dungeon_chain.insert(rnd.randint(1, 4), rnd.choice(inserts))
def loadFromRom(self, rom):
import patches.overworld
if patches.overworld.isNormalOverworld(rom):
import patches.entrances
self.entrance_mapping = patches.entrances.readEntrances(rom)
else:
self.entrance_mapping = {"d%d" % (n): "d%d" % (n) for n in range(9)}
self.boss_mapping = patches.enemies.readBossMapping(rom)
self.miniboss_mapping = patches.enemies.readMiniBossMapping(rom)
self.goal = 8 # Better then nothing
self.dungeon_chain = [0, 1, 2, 3, 4, 5, 6, 7, 8] # TODO Actually read this from rom