@@ -1369,6 +1369,157 @@ def parse_data(self):
1369
1369
self .bosses [str (player )]["Ganons Tower" ] = "Agahnim 2"
1370
1370
self .bosses [str (player )]["Ganon" ] = "Ganon"
1371
1371
1372
+ def create_playthrough (self , create_paths : bool = True ):
1373
+ """Destructive to the world while it is run, damage gets repaired afterwards."""
1374
+ from itertools import chain
1375
+ # get locations containing progress items
1376
+ multiworld = self .multiworld
1377
+ prog_locations = {location for location in multiworld .get_filled_locations () if location .item .advancement }
1378
+ state_cache = [None ]
1379
+ collection_spheres : List [Set [Location ]] = []
1380
+ state = CollectionState (multiworld )
1381
+ sphere_candidates = set (prog_locations )
1382
+ logging .debug ('Building up collection spheres.' )
1383
+ while sphere_candidates :
1384
+
1385
+ # build up spheres of collection radius.
1386
+ # Everything in each sphere is independent from each other in dependencies and only depends on lower spheres
1387
+
1388
+ sphere = {location for location in sphere_candidates if state .can_reach (location )}
1389
+
1390
+ for location in sphere :
1391
+ state .collect (location .item , True , location )
1392
+
1393
+ sphere_candidates -= sphere
1394
+ collection_spheres .append (sphere )
1395
+ state_cache .append (state .copy ())
1396
+
1397
+ logging .debug ('Calculated sphere %i, containing %i of %i progress items.' , len (collection_spheres ),
1398
+ len (sphere ),
1399
+ len (prog_locations ))
1400
+ if not sphere :
1401
+ logging .debug ('The following items could not be reached: %s' , ['%s (Player %d) at %s (Player %d)' % (
1402
+ location .item .name , location .item .player , location .name , location .player ) for location in
1403
+ sphere_candidates ])
1404
+ if any ([multiworld .accessibility [location .item .player ] != 'minimal' for location in sphere_candidates ]):
1405
+ raise RuntimeError (f'Not all progression items reachable ({ sphere_candidates } ). '
1406
+ f'Something went terribly wrong here.' )
1407
+ else :
1408
+ self .unreachables = sphere_candidates
1409
+ break
1410
+
1411
+ # in the second phase, we cull each sphere such that the game is still beatable,
1412
+ # reducing each range of influence to the bare minimum required inside it
1413
+ restore_later = {}
1414
+ for num , sphere in reversed (tuple (enumerate (collection_spheres ))):
1415
+ to_delete = set ()
1416
+ for location in sphere :
1417
+ # we remove the item at location and check if game is still beatable
1418
+ logging .debug ('Checking if %s (Player %d) is required to beat the game.' , location .item .name ,
1419
+ location .item .player )
1420
+ old_item = location .item
1421
+ location .item = None
1422
+ if multiworld .can_beat_game (state_cache [num ]):
1423
+ to_delete .add (location )
1424
+ restore_later [location ] = old_item
1425
+ else :
1426
+ # still required, got to keep it around
1427
+ location .item = old_item
1428
+
1429
+ # cull entries in spheres for spoiler walkthrough at end
1430
+ sphere -= to_delete
1431
+
1432
+ # second phase, sphere 0
1433
+ removed_precollected = []
1434
+ for item in (i for i in chain .from_iterable (multiworld .precollected_items .values ()) if i .advancement ):
1435
+ logging .debug ('Checking if %s (Player %d) is required to beat the game.' , item .name , item .player )
1436
+ multiworld .precollected_items [item .player ].remove (item )
1437
+ multiworld .state .remove (item )
1438
+ if not multiworld .can_beat_game ():
1439
+ multiworld .push_precollected (item )
1440
+ else :
1441
+ removed_precollected .append (item )
1442
+
1443
+ # we are now down to just the required progress items in collection_spheres. Unfortunately
1444
+ # the previous pruning stage could potentially have made certain items dependant on others
1445
+ # in the same or later sphere (because the location had 2 ways to access but the item originally
1446
+ # used to access it was deemed not required.) So we need to do one final sphere collection pass
1447
+ # to build up the correct spheres
1448
+
1449
+ required_locations = {item for sphere in collection_spheres for item in sphere }
1450
+ state = CollectionState (multiworld )
1451
+ collection_spheres = []
1452
+ while required_locations :
1453
+ state .sweep_for_events (key_only = True )
1454
+
1455
+ sphere = set (filter (state .can_reach , required_locations ))
1456
+
1457
+ for location in sphere :
1458
+ state .collect (location .item , True , location )
1459
+
1460
+ required_locations -= sphere
1461
+
1462
+ collection_spheres .append (sphere )
1463
+
1464
+ logging .debug ('Calculated final sphere %i, containing %i of %i progress items.' , len (collection_spheres ),
1465
+ len (sphere ), len (required_locations ))
1466
+ if not sphere :
1467
+ raise RuntimeError (f'Not all required items reachable. Unreachable locations: { required_locations } ' )
1468
+
1469
+ # we can finally output our playthrough
1470
+ self .playthrough = {"0" : sorted ([str (item ) for item in
1471
+ chain .from_iterable (multiworld .precollected_items .values ())
1472
+ if item .advancement ])}
1473
+
1474
+ for i , sphere in enumerate (collection_spheres ):
1475
+ self .playthrough [str (i + 1 )] = {
1476
+ str (location ): str (location .item ) for location in sorted (sphere )}
1477
+ if create_paths :
1478
+ self .create_paths (state , collection_spheres )
1479
+
1480
+ # repair the multiworld again
1481
+ for location , item in restore_later .items ():
1482
+ location .item = item
1483
+
1484
+ for item in removed_precollected :
1485
+ multiworld .push_precollected (item )
1486
+
1487
+ def create_paths (self , state : CollectionState , collection_spheres : List [Set [Location ]]):
1488
+ from itertools import zip_longest
1489
+ multiworld = self .multiworld
1490
+
1491
+ def flist_to_iter (node ):
1492
+ while node :
1493
+ value , node = node
1494
+ yield value
1495
+
1496
+ def get_path (state , region ):
1497
+ reversed_path_as_flist = state .path .get (region , (region , None ))
1498
+ string_path_flat = reversed (list (map (str , flist_to_iter (reversed_path_as_flist ))))
1499
+ # Now we combine the flat string list into (region, exit) pairs
1500
+ pathsiter = iter (string_path_flat )
1501
+ pathpairs = zip_longest (pathsiter , pathsiter )
1502
+ return list (pathpairs )
1503
+
1504
+ self .paths = {}
1505
+ topology_worlds = (player for player in multiworld .player_ids if multiworld .worlds [player ].topology_present )
1506
+ for player in topology_worlds :
1507
+ self .paths .update (
1508
+ {str (location ): get_path (state , location .parent_region )
1509
+ for sphere in collection_spheres for location in sphere
1510
+ if location .player == player })
1511
+ if player in multiworld .get_game_players ("A Link to the Past" ):
1512
+ # If Pyramid Fairy Entrance needs to be reached, also path to Big Bomb Shop
1513
+ # Maybe move the big bomb over to the Event system instead?
1514
+ if any (exit_path == 'Pyramid Fairy' for path in self .paths .values ()
1515
+ for (_ , exit_path ) in path ):
1516
+ if multiworld .mode [player ] != 'inverted' :
1517
+ self .paths [str (multiworld .get_region ('Big Bomb Shop' , player ))] = \
1518
+ get_path (state , multiworld .get_region ('Big Bomb Shop' , player ))
1519
+ else :
1520
+ self .paths [str (multiworld .get_region ('Inverted Big Bomb Shop' , player ))] = \
1521
+ get_path (state , multiworld .get_region ('Inverted Big Bomb Shop' , player ))
1522
+
1372
1523
def to_json (self ):
1373
1524
self .parse_data ()
1374
1525
out = OrderedDict ()
0 commit comments