1
1
import functools
2
+ import itertools
2
3
import queue
3
4
import random
4
5
from collections import deque
@@ -18,6 +19,18 @@ def __init__(self):
18
19
def __bool__ (self ):
19
20
return bool (self ._lookup )
20
21
22
+ def __getitem__ (self , item : str ) -> List [Entrance ]:
23
+ return self ._lookup .get (item , [])
24
+
25
+ def __iter__ (self ):
26
+ return itertools .chain .from_iterable (self ._lookup .values ())
27
+
28
+ def __str__ (self ):
29
+ return str (self ._lookup )
30
+
31
+ def __repr__ (self ):
32
+ return self .__str__ ()
33
+
21
34
def add (self , entrance : Entrance ) -> None :
22
35
group = self ._lookup .setdefault (entrance .er_group , [])
23
36
group .append (entrance )
@@ -28,9 +41,6 @@ def remove(self, entrance: Entrance) -> None:
28
41
if not group :
29
42
del self ._lookup [entrance .er_group ]
30
43
31
- def __getitem__ (self , item : str ) -> List [Entrance ]:
32
- return self ._lookup .get (item , [])
33
-
34
44
rng : random .Random
35
45
dead_ends : GroupLookup
36
46
others : GroupLookup
@@ -272,13 +282,49 @@ def randomize_entrances(
272
282
2. All placeholder entrances to regions will have been removed.
273
283
"""
274
284
state = ERPlacementState (world , coupled )
275
- # exits which had no candidate exits found in the non-dead-end stage.
276
- # they will never be placeable until we start placing dead ends so
277
- # hold them here and stop trying.
278
- unplaceable_exits : List [Entrance ] = []
279
285
280
286
entrance_lookup = EntranceLookup (rng )
281
287
288
+ def find_pairing (dead_end : bool , require_new_regions : bool ) -> Optional [Tuple [Entrance , Entrance ]]:
289
+ for source_exit in state ._placeable_exits :
290
+ target_groups = get_target_groups (source_exit .er_group )
291
+ # anything can connect to the default group - if people don't like it the fix is to
292
+ # assign a non-default group
293
+ if 'Default' not in target_groups :
294
+ target_groups .append ('Default' )
295
+ for target_entrance in entrance_lookup .get_targets (target_groups , dead_end , preserve_group_order ):
296
+ # todo - requiring new regions is a proxy for requiring new entrances to be unlocked, which is
297
+ # not quite full fidelity so we may need to revisit this in the future
298
+ region_requirement_satisfied = (not require_new_regions
299
+ or target_entrance .connected_region not in state .placed_regions )
300
+ if region_requirement_satisfied and source_exit .can_connect_to (target_entrance , state ):
301
+ return source_exit , target_entrance
302
+ else :
303
+ # no source exits had any valid target so this stage is deadlocked. swap may be implemented if early
304
+ # deadlocking is a frequent issue.
305
+ lookup = entrance_lookup .dead_ends if dead_end else entrance_lookup .others
306
+
307
+ # if we're in a stage where we're trying to get to new regions, we could also enter this
308
+ # branch in a success state (when all regions of the preferred type have been placed, but there are still
309
+ # additional unplaced entrances into those regions)
310
+ if require_new_regions :
311
+ if all (e .connected_region in state .placed_regions for e in lookup ):
312
+ return None
313
+
314
+ raise Exception (f"None of the available entrances had valid targets.\n "
315
+ f"Available exits: { state ._placeable_exits } \n "
316
+ f"Available entrances: { lookup } " )
317
+
318
+ def do_placement (source_exit : Entrance , target_entrance : Entrance ):
319
+ removed_entrances = state .connect (source_exit , target_entrance )
320
+ # remove the paired items from consideration
321
+ state ._placeable_exits .remove (source_exit )
322
+ for entrance in removed_entrances :
323
+ entrance_lookup .remove (entrance )
324
+ # place and propagate
325
+ state .place (target_entrance )
326
+ state .sweep_pending_exits ()
327
+
282
328
for region in regions :
283
329
for entrance in region .entrances :
284
330
if not entrance .parent_region :
@@ -287,84 +333,33 @@ def randomize_entrances(
287
333
# place the menu region and connected start region(s)
288
334
state .place (world .multiworld .get_region ('Menu' , world .player ))
289
335
290
- while state .has_placeable_exits () and entrance_lookup .others :
336
+ # stage 1 - try to place all the non-dead-end entrances
337
+ while entrance_lookup .others :
291
338
# todo - this access to placeable_exits is ugly
292
- # todo - this should iterate placeable exits instead of immediately
293
- # giving up; can_connect_to may be stateful
294
- # todo - this doesn't prioritize placing new rooms like the original did;
295
- # that's problematic because early loops would lead to failures
296
339
# this is needed to reduce bias; otherwise newer exits are prioritized
297
340
rng .shuffle (state ._placeable_exits )
298
- source_exit = state ._placeable_exits .pop ()
299
-
300
- target_groups = get_target_groups (source_exit .er_group )
301
- # anything can connect to the default group - if people don't like it the fix is to
302
- # assign a non-default group
303
- if 'Default' not in target_groups :
304
- target_groups .append ('Default' )
305
- for target_entrance in entrance_lookup .get_targets (target_groups , False , preserve_group_order ):
306
- if source_exit .can_connect_to (target_entrance , state ):
307
- # we found a valid, connectable target entrance. We'll connect it in a moment
308
- break
309
- else :
310
- # there were no valid non-dead-end targets for this source, so give up on it for now
311
- unplaceable_exits .append (source_exit )
312
- continue
313
-
314
- # place the new pairing. it is important to do connections first so that can_reach will function.
315
- removed_entrances = state .connect (source_exit , target_entrance )
316
- state .place (target_entrance )
317
- state .sweep_pending_exits ()
318
- # remove paired entrances from the lookup so they don't get re-randomized
319
- for entrance in removed_entrances :
320
- entrance_lookup .remove (entrance )
321
-
322
- if entrance_lookup .others :
323
- # this is generally an unsalvagable failure, we would need to implement swap earlier in the process
324
- # to prevent it. A stateful can_connect_to implementation may make this recoverable in some worlds as well.
325
- # why? there are no placeable exits, which means none of them have valid targets, and conversely
326
- # none of the existing targets can pair to the existing sources. Since dead ends will never add new sources
327
- # this means the current targets can never be paired (in most cases)
328
- # todo - investigate ways to prevent this case
329
- print ("Unable to place all non-dead-end entrances with available source exits" )
330
- return state # this short circuts the exception for testing purposes in order to see how far ER got.
331
- raise Exception ("Unable to place all non-dead-end entrances with available source exits" )
332
-
333
- # anything we couldn't place before might be placeable now
334
- state ._placeable_exits .extend (unplaceable_exits )
335
- unplaceable_exits .clear ()
336
-
337
- # repeat the above but try to place dead ends
338
- while state .has_placeable_exits () and entrance_lookup .dead_ends :
341
+ pairing = find_pairing (False , True )
342
+ if not pairing :
343
+ break
344
+ do_placement (* pairing )
345
+ # stage 2 - try to place all the dead-end entrances
346
+ while entrance_lookup .dead_ends :
339
347
rng .shuffle (state ._placeable_exits )
340
- source_exit = state ._placeable_exits .pop ()
341
-
342
- target_groups = get_target_groups (source_exit .er_group )
343
- # anything can connect to the default group - if people don't like it the fix is to
344
- # assign a non-default group
345
- if 'Default' not in target_groups :
346
- target_groups .append ('Default' )
347
- for target_entrance in entrance_lookup .get_targets (target_groups , True , preserve_group_order ):
348
- if source_exit .can_connect_to (target_entrance , state ):
349
- # we found a valid, connectable target entrance. We'll connect it in a moment
350
- break
351
- else :
352
- # there were no valid dead-end targets for this source, so give up
353
- # todo - similar to above we should try and prevent this state.
354
- # also it can_connect_to may be stateful.
355
- print ("Unable to place all dead-end entrances with available source exits" )
356
- return state # this short circuts the exception for testing purposes in order to see how far ER got.
357
- raise Exception ("Unable to place all dead-end entrances with available source exits" )
358
-
359
- # place the new pairing. it is important to do connections first so that can_reach will function.
360
- removed_entrances = state .connect (source_exit , target_entrance )
361
- state .place (target_entrance )
362
- state .sweep_pending_exits ()
363
- # remove paired entrances from the lookup so they don't get re-randomized
364
- for entrance in removed_entrances :
365
- entrance_lookup .remove (entrance )
366
-
367
- if state .has_placeable_exits ():
368
- raise Exception ("There are more exits than entrances" )
348
+ pairing = find_pairing (True , True )
349
+ if not pairing :
350
+ break
351
+ do_placement (* pairing )
352
+ # todo - stages 3 and 4 should ideally run "together" ie without respect to dead-endedness
353
+ # as we are just trying to tie off loose ends rather than get you somewhere new
354
+ # stage 3 - connect any dangling entrances that remain
355
+ while entrance_lookup .others :
356
+ rng .shuffle (state ._placeable_exits )
357
+ pairing = find_pairing (False , False )
358
+ do_placement (* pairing )
359
+ # stage 4 - last chance for dead ends
360
+ while entrance_lookup .dead_ends :
361
+ rng .shuffle (state ._placeable_exits )
362
+ pairing = find_pairing (True , False )
363
+ do_placement (* pairing )
369
364
370
365
return state
0 commit comments