forked from aerinon/ALttPDoorRandomizer
-
Notifications
You must be signed in to change notification settings - Fork 5
/
Copy pathFill.py
1265 lines (1121 loc) · 64.3 KB
/
Fill.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
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
import RaceRandom as random
import collections
import itertools
import logging
import math
from collections import Counter
from contextlib import suppress
from BaseClasses import CollectionState, FillError, LocationType
from Items import ItemFactory
from Regions import shop_to_location_table, retro_shops
from source.item.FillUtil import filter_locations, classify_major_items, replace_trash_item, vanilla_fallback
from source.item.FillUtil import filter_special_locations, valid_pot_items
def get_dungeon_item_pool(world):
dungeon_items = [item for dungeon in world.dungeons for item in dungeon.all_items if item.location is None]
for player in range(1, world.players+1):
if world.prizeshuffle[player] != 'none':
dungeon_items.extend(ItemFactory(['Red Pendant', 'Blue Pendant', 'Green Pendant', 'Crystal 1', 'Crystal 2', 'Crystal 3', 'Crystal 4', 'Crystal 7', 'Crystal 5', 'Crystal 6'], player))
return dungeon_items
def promote_dungeon_items(world):
world.itempool += get_dungeon_item_pool(world)
for item in world.get_items():
if item.smallkey or item.bigkey or item.prize:
item.advancement = True
elif item.map or item.compass:
item.priority = True
def dungeon_tracking(world):
for dungeon in world.dungeons:
layout = world.dungeon_layouts[dungeon.player][dungeon.name]
layout.dungeon_items = len([i for i in dungeon.all_items if i.is_inside_dungeon_item(world)])
if world.prizeshuffle[dungeon.player] in ['dungeon', 'nearby'] and not dungeon.prize:
from Dungeons import dungeon_table
if dungeon_table[dungeon.name].prize:
layout.dungeon_items += 1
layout.free_items = layout.location_cnt - layout.dungeon_items
def fill_dungeons_restrictive(world, shuffled_locations):
# with shuffled dungeon items they are distributed as part of the normal item pool
for item in world.get_items():
if ((item.prize and world.prizeshuffle[item.player] != 'none')
or (item.smallkey and world.keyshuffle[item.player] != 'none')
or (item.bigkey and world.bigkeyshuffle[item.player] != 'none')):
item.advancement = True
elif (item.map and world.mapshuffle[item.player] not in ['none', 'nearby']) or (item.compass and world.compassshuffle[item.player] not in ['none', 'nearby']):
item.priority = True
dungeon_items = [item for item in get_dungeon_item_pool(world) if item.is_inside_dungeon_item(world) or item.is_near_dungeon_item(world)]
bigs, smalls, prizes, others = [], [], [], []
for i in dungeon_items:
(bigs if i.bigkey else smalls if i.smallkey else prizes if i.prize else others).append(i)
unplaced_smalls = list(smalls)
for i in world.itempool:
if i.smallkey and world.keyshuffle[i.player] not in ['none', 'nearby']:
unplaced_smalls.append(i)
def fill(base_state, items, locations, key_pool=None):
fill_restrictive(world, base_state, locations, items, key_pool, True)
all_state_base = world.get_all_state()
for player in range(1, world.players + 1):
if world.logic[player] == 'hybridglitches' and world.keyshuffle[player] in ['none', 'nearby'] \
and world.pottery[player] not in ['none', 'cave']:
# remove 2 keys from main pool
count_to_remove = 2
to_remove = []
for wix, wi in enumerate(smalls):
if wi.name == 'Small Key (Swamp Palace)' and wi.player == player:
to_remove.append(wix)
if count_to_remove == len(to_remove):
break
for wix in reversed(to_remove):
del smalls[wix]
# remove 2 swamp locations from pool
hybrid_locations = []
to_remove = []
for i, loc in enumerate(shuffled_locations):
if loc.name in ['Swamp Palace - Trench 1 Pot Key', 'Swamp Palace - Pot Row Pot Key'] and loc.player == player:
to_remove.append(i)
hybrid_locations.append(loc)
if count_to_remove == len(to_remove):
break
for i in reversed(to_remove):
shuffled_locations.pop(i)
# place 2 HMG keys
hybrid_state_base = all_state_base.copy()
for x in bigs + smalls + prizes + others:
hybrid_state_base.collect(x, True)
hybrid_smalls = [ItemFactory('Small Key (Swamp Palace)', player)] * 2
fill(hybrid_state_base, hybrid_smalls, hybrid_locations, unplaced_smalls)
big_state_base = all_state_base.copy()
for x in smalls + prizes + others:
big_state_base.collect(x, True)
fill(big_state_base, bigs, shuffled_locations, unplaced_smalls)
random.shuffle(shuffled_locations)
small_state_base = all_state_base.copy()
for x in prizes + others:
small_state_base.collect(x, True)
fill(small_state_base, smalls, shuffled_locations, unplaced_smalls)
prizes_copy = prizes.copy()
for attempt in range(15):
try:
for player in range(1, world.players + 1):
if world.prizeshuffle[player] == 'nearby':
dungeon_pool = []
for dungeon in world.dungeons:
from Dungeons import dungeon_table
if dungeon.player == player and dungeon_table[dungeon.name].prize:
dungeon_pool.append(dungeon)
random.shuffle(dungeon_pool)
for item in prizes:
if item.player == player:
dungeon = dungeon_pool.pop()
dungeon.prize = item
item.dungeon_object = dungeon
random.shuffle(prizes)
random.shuffle(shuffled_locations)
prize_state_base = all_state_base.copy()
for x in others:
prize_state_base.collect(x, True)
fill(prize_state_base, prizes, shuffled_locations)
except FillError as e:
logging.getLogger('').info("Failed to place dungeon prizes (%s). Will retry %s more times", e, 14 - attempt)
prizes = prizes_copy.copy()
for dungeon in world.dungeons:
dungeon.prize = None
for prize in prizes:
if prize.location:
prize.location.item = None
prize.location = None
continue
break
else:
raise FillError(f'Unable to place dungeon prizes: {", ".join(list(map(lambda d: d.name, prizes)))}')
random.shuffle(shuffled_locations)
fill(all_state_base, others, shuffled_locations)
def fill_restrictive(world, base_state, locations, itempool, key_pool=None, single_player_placement=False,
vanilla=False):
def sweep_from_pool(placing_items=None):
new_state = base_state.copy()
for item in itempool:
new_state.collect(item, True)
new_state.placing_items = placing_items
new_state.sweep_for_events()
new_state.placing_items = None
return new_state
unplaced_items = []
no_access_checks = {}
reachable_items = {}
for item in itempool:
if world.accessibility[item.player] == 'none':
no_access_checks.setdefault(item.player, []).append(item)
else:
reachable_items.setdefault(item.player, []).append(item)
for player_items in [no_access_checks, reachable_items]:
while any(player_items.values()) and locations:
items_to_place = [[itempool.remove(items[-1]), items.pop()][-1] for items in player_items.values() if items]
maximum_exploration_state = sweep_from_pool(placing_items=items_to_place)
has_beaten_game = world.has_beaten_game(maximum_exploration_state)
for item_to_place in items_to_place:
perform_access_check = True
if world.accessibility[item_to_place.player] == 'none':
perform_access_check = not world.has_beaten_game(maximum_exploration_state, item_to_place.player) if single_player_placement else not has_beaten_game
spot_to_fill = None
item_locations = filter_locations(item_to_place, locations, world, vanilla)
verify(item_to_place, item_locations, maximum_exploration_state, single_player_placement,
perform_access_check, key_pool, world)
for location in item_locations:
spot_to_fill = verify_spot_to_fill(location, item_to_place, maximum_exploration_state,
single_player_placement, perform_access_check, key_pool, world)
if spot_to_fill:
break
if spot_to_fill is None:
if vanilla:
unplaced_items.insert(0, item_to_place)
continue
spot_to_fill = recovery_placement(item_to_place, locations, world, maximum_exploration_state,
base_state, itempool, perform_access_check, item_locations,
key_pool, single_player_placement)
if spot_to_fill is None:
# we filled all reachable spots. Maybe the game can be beaten anyway?
unplaced_items.insert(0, item_to_place)
if world.can_beat_game():
if world.accessibility[item_to_place.player] != 'none':
logging.getLogger('').warning('Not all items placed. Game beatable anyway.'
f' (Could not place {item_to_place})')
continue
raise FillError('No more spots to place %s' % item_to_place)
world.push_item(spot_to_fill, item_to_place, False)
track_outside_keys(item_to_place, spot_to_fill, world)
track_dungeon_items(item_to_place, spot_to_fill, world)
locations.remove(spot_to_fill)
spot_to_fill.event = True
itempool.extend(unplaced_items)
def verify_spot_to_fill(location, item_to_place, max_exp_state, single_player_placement, perform_access_check,
key_pool, world):
if item_to_place.smallkey or item_to_place.bigkey or item_to_place.prize: # a better test to see if a key can go there
location.item = item_to_place
location.event = True
if item_to_place.smallkey:
with suppress(ValueError):
key_pool.remove(item_to_place)
test_state = max_exp_state.copy()
test_state.stale[item_to_place.player] = True
else:
test_state = max_exp_state
if not single_player_placement or location.player == item_to_place.player:
test_state.sweep_for_events()
if location.can_fill(test_state, item_to_place, perform_access_check):
if valid_key_placement(item_to_place, location, key_pool, test_state, world):
if (item_to_place.prize and world.prizeshuffle[item_to_place.player] == 'none') \
or valid_dungeon_placement(item_to_place, location, world):
return location
if item_to_place.smallkey or item_to_place.bigkey or item_to_place.prize:
location.item = None
location.event = False
if item_to_place.smallkey:
key_pool.append(item_to_place)
return None
def valid_key_placement(item, location, key_pool, collection_state, world):
if not valid_reserved_placement(item, location, world):
return False
if ((not item.smallkey and not item.bigkey) or item.player != location.player
or world.keyshuffle[item.player] == 'universal' or world.logic[item.player] == 'nologic'):
return True
dungeon = location.parent_region.dungeon
if not dungeon and item.is_near_dungeon_item(world):
check_dungeon = world.get_dungeon(item.dungeon, item.player) if item.dungeon else item.dungeon_object
if len([d for d in location.parent_region.districts if d in check_dungeon.districts]):
dungeon = check_dungeon
if dungeon:
if dungeon.name not in item.name and (dungeon.name != 'Hyrule Castle' or 'Escape' not in item.name):
return True
# Small key and big key in Swamp and Hera are placed without logic
if world.logic[item.player] == 'hybridglitches' and dungeon.name in ['Tower of Hera', 'Swamp Palace'] and dungeon.name in item.name:
return True
key_logic = world.key_logic[item.player][dungeon.name]
unplaced_keys = len([x for x in key_pool if x.name == key_logic.small_key_name and x.player == item.player])
prize_loc = None
if key_logic.prize_location and dungeon.prize and dungeon.prize.location and dungeon.prize.location.player == item.player:
prize_loc = dungeon.prize.location
cr_count = world.crystals_needed_for_gt[location.player]
wild_keys = world.keyshuffle[item.player] != 'none'
if wild_keys:
reached_keys = {x for x in collection_state.locations_checked
if x.item and x.item.name == key_logic.small_key_name and x.item.player == item.player}
else:
reached_keys = set() # will be calculated using key logic in a moment
self_locking_keys = sum(1 for d, rule in key_logic.door_rules.items() if rule.allow_small
and rule.small_location.item and rule.small_location.item.name == key_logic.small_key_name)
return key_logic.check_placement(unplaced_keys, wild_keys, reached_keys, self_locking_keys,
location if item.bigkey else None, prize_loc, cr_count)
else:
return not item.is_inside_dungeon_item(world)
def valid_reserved_placement(item, location, world):
if item.player == location.player and item.is_inside_dungeon_item(world):
return location.name not in world.item_pool_config.reserved_locations[location.player]
return True
def valid_dungeon_placement(item, location, world):
dungeon = location.parent_region.dungeon
if not dungeon and item.is_near_dungeon_item(world):
check_dungeon = world.get_dungeon(item.dungeon, item.player) if item.dungeon else item.dungeon_object
if len([d for d in location.parent_region.districts if d in check_dungeon.districts]):
dungeon = check_dungeon
if dungeon:
layout = world.dungeon_layouts[location.player][dungeon.name]
if not is_dungeon_item(item, world) or item.player != location.player:
if item.prize and item.is_near_dungeon_item(world):
return item.dungeon_object == dungeon and layout.free_items > 0
return layout.free_items > 0
elif item.prize:
return not dungeon.prize and layout.dungeon_items > 0
else:
# the second half probably doesn't matter much - should always return true
return item.dungeon == dungeon.name and layout.dungeon_items > 0
return not is_dungeon_item(item, world)
def track_outside_keys(item, location, world):
if not item.smallkey:
return
item_dungeon = item.dungeon
if location.player == item.player:
loc_dungeon = location.parent_region.dungeon
if loc_dungeon and loc_dungeon.name == item_dungeon:
return # this is an inside key
world.key_logic[item.player][item_dungeon].outside_keys += 1
world.key_logic[item.player][item_dungeon].outside_keys_locations.add(location)
def track_dungeon_items(item, location, world):
if location.parent_region.dungeon and (not item.prize or world.prizeshuffle[item.player] == 'dungeon'):
layout = world.dungeon_layouts[location.player][location.parent_region.dungeon.name]
if is_dungeon_item(item, world) and item.player == location.player:
layout.dungeon_items -= 1
else:
layout.free_items -= 1
if item.prize:
location.parent_region.dungeon.prize = item
item.dungeon_object = location.parent_region.dungeon
def is_dungeon_item(item, world):
return ((item.prize and world.prizeshuffle[item.player] in ['none', 'dungeon'])
or (item.smallkey and world.keyshuffle[item.player] == 'none')
or (item.bigkey and world.bigkeyshuffle[item.player] == 'none')
or (item.compass and world.compassshuffle[item.player] == 'none')
or (item.map and world.mapshuffle[item.player] == 'none'))
def recovery_placement(item_to_place, locations, world, state, base_state, itempool, perform_access_check, attempted,
key_pool=None, single_player_placement=False):
logging.getLogger('').debug(f'Could not place {item_to_place} attempting recovery')
if world.algorithm in ['balanced', 'equitable']:
return last_ditch_placement(item_to_place, locations, world, state, base_state, itempool, key_pool,
single_player_placement)
elif world.algorithm == 'vanilla_fill':
if item_to_place.prize:
possible_swaps = [x for x in state.locations_checked if x.item.prize]
return try_possible_swaps(possible_swaps, item_to_place, locations, world, base_state, itempool,
key_pool, single_player_placement)
else:
i, config = 0, world.item_pool_config
tried = set(attempted)
if not item_to_place.is_inside_dungeon_item(world):
while i < len(config.location_groups[item_to_place.player]):
fallback_locations = config.location_groups[item_to_place.player][i].locations
other_locs = [x for x in locations if x.name in fallback_locations]
for location in other_locs:
spot_to_fill = verify_spot_to_fill(location, item_to_place, state, single_player_placement,
perform_access_check, key_pool, world)
if spot_to_fill:
return spot_to_fill
i += 1
tried.update(other_locs)
else:
other_locations = vanilla_fallback(item_to_place, locations, world)
for location in other_locations:
spot_to_fill = verify_spot_to_fill(location, item_to_place, state, single_player_placement,
perform_access_check, key_pool, world)
if spot_to_fill:
return spot_to_fill
tried.update(other_locations)
other_locations = [x for x in locations if x not in tried]
for location in other_locations:
spot_to_fill = verify_spot_to_fill(location, item_to_place, state, single_player_placement,
perform_access_check, key_pool, world)
if spot_to_fill:
return spot_to_fill
return None
# explicitly fail these cases
elif world.algorithm in ['dungeon_only', 'major_only', 'district']:
raise FillError(f'Rare placement for {world.algorithm} detected. {item_to_place} unable to be placed.'
f' Try a different seed')
# I don't think any algorithm uses fallback placement anymore, vanilla is special. Others simply fail.
else:
other_locations = [x for x in locations if x not in attempted]
for location in other_locations:
spot_to_fill = verify_spot_to_fill(location, item_to_place, state, single_player_placement,
perform_access_check, key_pool, world)
if spot_to_fill:
return spot_to_fill
return None
def last_ditch_placement(item_to_place, locations, world, state, base_state, itempool,
key_pool=None, single_player_placement=False):
def location_preference(loc):
if not loc.item.advancement:
return 1
if loc.item.type and loc.item.type != 'Sword':
if loc.item.type in ['Map', 'Compass']:
return 2
else:
return 3
return 4
# TODO: Verify correctness in using item player in multiworld situations
if item_to_place.prize and world.prizeshuffle[item_to_place.player] == 'none':
possible_swaps = [x for x in state.locations_checked if x.item.prize]
else:
ignored_types = ['Event']
if world.prizeshuffle[item_to_place.player] == 'none':
ignored_types.append('Prize')
possible_swaps = [x for x in state.locations_checked
if x.item.type not in ignored_types and not x.forced_item and not x.locked]
swap_locations = sorted(possible_swaps, key=location_preference)
return try_possible_swaps(swap_locations, item_to_place, locations, world, base_state, itempool,
key_pool, single_player_placement)
def try_possible_swaps(swap_locations, item_to_place, locations, world, base_state, itempool,
key_pool=None, single_player_placement=False):
for location in swap_locations:
old_item = location.item
new_pool = list(itempool) + [old_item]
new_spot = find_spot_for_item(item_to_place, [location], world, base_state, new_pool,
key_pool, single_player_placement)
if new_spot:
restore_item = new_spot.item
new_spot.item = item_to_place
swap_spot = find_spot_for_item(old_item, locations, world, base_state, itempool,
key_pool, single_player_placement)
if swap_spot:
logging.getLogger('').debug(f'Swapping {old_item} for {item_to_place}')
world.push_item(swap_spot, old_item, False)
swap_spot.event = True
locations.remove(swap_spot)
locations.append(new_spot)
return new_spot
else:
new_spot.item = restore_item
else:
location.item = old_item
return None
def find_spot_for_item(item_to_place, locations, world, base_state, pool,
keys_in_itempool=None, single_player_placement=False):
def sweep_from_pool():
new_state = base_state.copy()
for item in pool:
new_state.collect(item, True)
new_state.sweep_for_events()
return new_state
for location in locations:
maximum_exploration_state = sweep_from_pool()
perform_access_check = True
old_item = None
if world.accessibility[item_to_place.player] == 'none':
perform_access_check = not world.has_beaten_game(maximum_exploration_state, item_to_place.player) if single_player_placement else not world.has_beaten_game(maximum_exploration_state)
if item_to_place.smallkey or item_to_place.bigkey: # a better test to see if a key can go there
old_item = location.item
location.item = item_to_place
test_state = maximum_exploration_state.copy()
test_state.stale[item_to_place.player] = True
else:
test_state = maximum_exploration_state
if (not single_player_placement or location.player == item_to_place.player) \
and location.can_fill(test_state, item_to_place, perform_access_check) \
and valid_key_placement(item_to_place, location,
pool if (keys_in_itempool and keys_in_itempool[item_to_place.player]) else world.itempool,
test_state, world):
return location
if item_to_place.smallkey or item_to_place.bigkey:
location.item = old_item
return None
def distribute_items_restrictive(world, gftower_trash=False, fill_locations=None):
# If not passed in, then get a shuffled list of locations to fill in
if not fill_locations:
fill_locations = world.get_unfilled_locations()
random.shuffle(fill_locations)
# get items to distribute
classify_major_items(world)
# handle special case fast fill
locations_used = False
location_item_pool = collections.defaultdict(list)
# guarantee one big magic in a bonk location
for player in range(1, world.players + 1):
if world.shuffle_bonk_drops[player]:
for item in world.itempool:
if item.name in ['Big Magic'] and item.player == player:
location_item_pool[player].append(item)
break
from Regions import bonk_prize_table
for player, magic_pool in location_item_pool.items():
if len(magic_pool) > 0:
world.itempool.remove(magic_pool[0])
bonk_locations = [location for location in fill_locations if location.player == player
and location.name in [n for n, (_, _, aga, _, _, _) in bonk_prize_table.items() if not aga]]
bonk_locations = filter_special_locations(bonk_locations, world, lambda l: l.name == 'Kakariko Portal Tree')
fast_fill_helper(world, magic_pool, bonk_locations)
locations_used = True
if locations_used:
fill_locations = world.get_unfilled_locations()
random.shuffle(fill_locations)
random.shuffle(world.itempool)
config_sort(world)
progitempool = [item for item in world.itempool if item.advancement]
prioitempool = [item for item in world.itempool if not item.advancement and item.priority]
restitempool = [item for item in world.itempool if not item.advancement and not item.priority]
gftower_trash &= world.algorithm in ['balanced', 'equitable', 'dungeon_only']
# dungeon only may fill up the dungeon... and push items out into the overworld
# fill in gtower locations with trash first
for player in range(1, world.players + 1):
if (not gftower_trash or not world.ganonstower_vanilla[player]
or world.logic[player] in ['owglitches', 'hybridglitches', 'nologic']):
continue
gt_count, total_count = calc_trash_locations(world, player)
scale_factor = .75 * (world.crystals_needed_for_gt[player] / 7)
if world.algorithm == 'dungeon_only':
reserved_space = sum(1 for i in progitempool+prioitempool if i.player == player)
max_trash = max(0, min(gt_count, total_count - reserved_space))
else:
max_trash = gt_count
scaled_trash = math.floor(max_trash * scale_factor)
if world.goal[player] in ['triforcehunt', 'trinity', 'ganonhunt'] or world.algorithm == 'dungeon_only':
gftower_trash_count = random.randint(scaled_trash, max_trash)
else:
gftower_trash_count = random.randint(0, scaled_trash)
gtower_locations = [location for location in fill_locations if location.parent_region.dungeon
and location.parent_region.dungeon.name == 'Ganons Tower' and location.player == player]
random.shuffle(gtower_locations)
trashcnt = 0
while gtower_locations and restitempool and trashcnt < gftower_trash_count:
spot_to_fill = gtower_locations.pop()
item_to_place = restitempool.pop()
world.push_item(spot_to_fill, item_to_place, False)
fill_locations.remove(spot_to_fill)
trashcnt += 1
random.shuffle(fill_locations)
fill_locations.reverse()
# Make sure the escape keys ire placed first in standard to prevent running out of spots
def std_item_sort(item):
if world.mode[item.player] == 'standard':
if item.name == 'Small Key (Escape)':
return 1
if item.name == 'Big Key (Escape)':
return 2
return 0
progitempool.sort(key=std_item_sort)
key_pool = [x for x in progitempool if x.smallkey]
# sort maps and compasses to the back -- this may not be viable in equitable & ambrosia
progitempool.sort(key=lambda item: 0 if item.map or item.compass else 1)
if world.algorithm == 'vanilla_fill':
fill_restrictive(world, world.state, fill_locations, progitempool, key_pool, vanilla=True)
fill_restrictive(world, world.state, fill_locations, progitempool, key_pool)
random.shuffle(fill_locations)
if world.algorithm == 'vanilla_fill':
fast_vanilla_fill(world, prioitempool, fill_locations)
elif world.algorithm in ['balanced', 'major_only', 'dungeon_only', 'district']:
filtered_fill(world, prioitempool, fill_locations)
else: # just need to ensure dungeon items still get placed in dungeons
fast_equitable_fill(world, prioitempool, fill_locations)
# placeholder work
if world.algorithm == 'district':
random.shuffle(fill_locations)
placeholder_items = [item for item in world.itempool if item.name == 'Rupee (1)']
num_ph_items = len(placeholder_items)
if num_ph_items > 0:
placeholder_locations = filter_locations('Placeholder', fill_locations, world)
num_ph_locations = len(placeholder_locations)
if num_ph_items < num_ph_locations < len(fill_locations):
for _ in range(num_ph_locations - num_ph_items):
placeholder_items.append(replace_trash_item(restitempool, 'Rupee (1)'))
assert len(placeholder_items) == len(placeholder_locations)
for i in placeholder_items:
restitempool.remove(i)
for l in placeholder_locations:
fill_locations.remove(l)
filtered_fill(world, placeholder_items, placeholder_locations)
if world.players > 1:
fast_fill_pot_for_multiworld(world, restitempool, fill_locations)
# todo: fast fill drops?
if world.algorithm == 'vanilla_fill':
fast_vanilla_fill(world, restitempool, fill_locations)
else:
fast_fill(world, restitempool, fill_locations)
unplaced = [item.name for item in prioitempool + restitempool]
unfilled = [location.name for location in fill_locations]
if unplaced or unfilled:
logging.warning('Unplaced items: %s - Unfilled Locations: %s', unplaced, unfilled)
ensure_good_items(world)
def config_sort(world):
if world.item_pool_config.verify:
config_sort_helper(world, world.item_pool_config.verify)
elif world.item_pool_config.preferred:
config_sort_helper_random(world, world.item_pool_config.preferred)
def config_sort_helper(world, sort_dict):
pref = list(sort_dict.keys())
pref_len = len(pref)
world.itempool.sort(key=lambda i: pref_len - pref.index((i.name, i.player))
if (i.name, i.player) in sort_dict else 0)
def config_sort_helper_random(world, sort_dict):
world.itempool.sort(key=lambda i: 1 if (i.name, i.player) in sort_dict else 0)
def calc_trash_locations(world, player):
total_count, gt_count = 0, 0
for loc in world.get_locations():
if (loc.player == player and loc.item is None
and (loc.type not in {LocationType.Bonk, LocationType.Pot, LocationType.Drop, LocationType.Normal} or not loc.forced_item)
and (loc.type != LocationType.Shop or world.shopsanity[player])
and loc.parent_region.dungeon):
total_count += 1
if loc.parent_region.dungeon.name == 'Ganons Tower':
gt_count += 1
return gt_count, total_count
def ensure_good_items(world, write_skips=False):
for loc in world.get_locations():
if loc.item is None:
loc.item = ItemFactory('Nothing', loc.player)
# convert Arrows 5 and Nothing when necessary
if (loc.item.name in {'Arrows (5)', 'Nothing'}
and (loc.type != LocationType.Pot or loc.item.player != loc.player)):
loc.item = ItemFactory(invalid_location_replacement[loc.item.name], loc.item.player)
# do the arrow retro check
if world.bow_mode[loc.item.player].startswith('retro') and loc.item.name in {'Arrows (5)', 'Arrows (10)'}:
loc.item = ItemFactory('Rupees (5)', loc.item.player)
# don't write out all pots to spoiler
# todo: skip uninteresting enemy drops
if write_skips:
if loc.type in [LocationType.Pot, LocationType.Bonk] and loc.item.name in valid_pot_items:
loc.skip = True
invalid_location_replacement = {'Arrows (5)': 'Arrows (10)', 'Nothing': 'Rupees (5)',
'Chicken': 'Rupees (5)', 'Big Magic': 'Small Magic', 'Fairy': 'Small Heart'}
def fast_fill_helper(world, item_pool, fill_locations):
if world.algorithm == 'vanilla_fill':
fast_vanilla_fill(world, item_pool, fill_locations)
else:
fast_fill(world, item_pool, fill_locations)
# todo: other fast fill methods?
def fast_fill(world, item_pool, fill_locations):
config = world.item_pool_config
fast_pool = [x for x in item_pool if (x.name, x.player) not in config.restricted]
filtered_pool = [x for x in item_pool if (x.name, x.player) in config.restricted]
filtered_fill(world, filtered_pool, fill_locations)
while fast_pool and fill_locations:
spot_to_fill = fill_locations.pop()
item_to_place = fast_pool.pop()
world.push_item(spot_to_fill, item_to_place, False)
item_pool.clear()
item_pool.extend(filtered_pool)
item_pool.extend(fast_pool)
def fast_fill_pot_for_multiworld(world, item_pool, fill_locations):
pot_item_pool = collections.defaultdict(list)
pot_fill_locations = collections.defaultdict(list)
for item in item_pool:
if item.name in valid_pot_items:
pot_item_pool[item.player].append(item)
for loc in fill_locations:
if loc.type == LocationType.Pot:
pot_fill_locations[loc.player].append(loc)
for player in range(1, world.players+1):
flex = 256 - world.data_tables[player].pot_secret_table.multiworld_count
fill_count = len(pot_fill_locations[player]) - flex
if fill_count > 0:
fill_spots = random.sample(pot_fill_locations[player], fill_count)
fill_items = random.sample(pot_item_pool[player], fill_count)
for x in fill_items:
item_pool.remove(x)
for x in fill_spots:
fill_locations.remove(x)
fast_fill(world, fill_items, fill_spots)
def filtered_fill(world, item_pool, fill_locations):
while item_pool and fill_locations:
item_to_place = item_pool.pop()
item_locations = filter_locations(item_to_place, fill_locations, world)
spot_to_fill = next(iter(item_locations))
fill_locations.remove(spot_to_fill)
world.push_item(spot_to_fill, item_to_place, False)
# sweep once to pick up preplaced items
world.state.sweep_for_events()
def fast_vanilla_fill(world, item_pool, fill_locations):
next_item_pool = []
while item_pool and fill_locations:
item_to_place = item_pool.pop()
locations = filter_locations(item_to_place, fill_locations, world, vanilla_skip=True)
if len(locations):
spot_to_fill = locations.pop()
fill_locations.remove(spot_to_fill)
world.push_item(spot_to_fill, item_to_place, False)
else:
next_item_pool.append(item_to_place)
while next_item_pool and fill_locations:
item_to_place = next_item_pool.pop()
spot_to_fill = next(iter(filter_locations(item_to_place, fill_locations, world)))
fill_locations.remove(spot_to_fill)
world.push_item(spot_to_fill, item_to_place, False)
def filtered_equitable_fill(world, item_pool, fill_locations):
while item_pool and fill_locations:
item_to_place = item_pool.pop()
item_locations = filter_locations(item_to_place, fill_locations, world)
spot_to_fill = next(l for l in item_locations if valid_dungeon_placement(item_to_place, l, world))
fill_locations.remove(spot_to_fill)
world.push_item(spot_to_fill, item_to_place, False)
track_dungeon_items(item_to_place, spot_to_fill, world)
def fast_equitable_fill(world, item_pool, fill_locations):
while item_pool and fill_locations:
item_to_place = item_pool.pop()
spot_to_fill = next(l for l in fill_locations if valid_dungeon_placement(item_to_place, l, world))
fill_locations.remove(spot_to_fill)
world.push_item(spot_to_fill, item_to_place, False)
track_dungeon_items(item_to_place, spot_to_fill, world)
def lock_shop_locations(world, player):
for shop, loc_names in shop_to_location_table.items():
for loc in loc_names:
world.get_location(loc, player).locked = True
def sell_potions(world, player):
loc_choices = []
for shop in world.shops[player]:
# potions are excluded from the cap fairy due to visual problem
if shop.region.name in shop_to_location_table and shop.region.name != 'Capacity Upgrade':
loc_choices += [world.get_location(loc, player) for loc in shop_to_location_table[shop.region.name]]
locations = [loc for loc in loc_choices if not loc.item]
for potion in ['Green Potion', 'Blue Potion', 'Red Potion']:
location = random.choice(filter_locations(ItemFactory(potion, player), locations, world, potion=True))
locations.remove(location)
p_item = next((item for item in world.itempool if item.name == potion and item.player == player), None)
if p_item:
world.push_item(location, p_item, collect=False)
world.itempool.remove(p_item)
def sell_keys(world, player):
# exclude the old man or take any caves because free keys are too good
shop_names = {shop.region.name: shop for shop in world.shops[player] if shop.region.name in shop_to_location_table}
choices = [(world.get_location(loc, player), shop) for shop in shop_names for loc in shop_to_location_table[shop]]
locations = [l for l, shop in choices]
locations = filter_locations(ItemFactory('Small Key (Universal)', player), locations, world)
locations = [(loc, shop) for loc, shop in choices if not loc.item and loc in locations]
location, shop = random.choice(locations)
universal_key = next(i for i in world.itempool if i.name == 'Small Key (Universal)' and i.player == player)
world.push_item(location, universal_key, collect=False)
idx = shop_to_location_table[shop_names[shop].region.name].index(location.name)
shop_names[shop].add_inventory(idx, 'Small Key (Universal)', 100)
world.itempool.remove(universal_key)
def verify(item_to_place, item_locations, state, spp, pac, key_pool, world):
if world.item_pool_config.verify:
logger = logging.getLogger('')
item_name = 'Bottle' if item_to_place.name.startswith('Bottle') else item_to_place.name
item_player = item_to_place.player
config = world.item_pool_config
if (item_name, item_player) in config.verify:
tests = config.verify[(item_name, item_player)]
issues = []
for location in item_locations:
if location.name in tests:
expected = tests[location.name]
spot = verify_spot_to_fill(location, item_to_place, state, spp, pac, key_pool, world)
if spot and (item_to_place.smallkey or item_to_place.bigkey):
location.item = None
location.event = False
if item_to_place.smallkey:
key_pool.append(item_to_place)
if (expected and spot) or (not expected and spot is None):
logger.debug(f'Placing {item_name} ({item_player}) at {location.name} was {expected}')
config.verify_count += 1
if config.verify_count >= config.verify_target:
exit()
else:
issues.append((item_name, item_player, location.name, expected))
if len(issues) > 0:
for name, player, loc, expected in issues:
if expected:
logger.error(f'Could not place {name} ({player}) at {loc}')
else:
logger.error(f'{name} ({player}) should not be allowed at {loc}')
raise Exception(f'Test failed placing {name}')
def balance_multiworld_progression(world):
state = CollectionState(world)
checked_locations = set()
unchecked_locations = set(world.get_locations())
total_locations_count = Counter(location.player for location in world.get_locations() if not location.locked and not location.forced_item)
reachable_locations_count = {}
for player in range(1, world.players + 1):
reachable_locations_count[player] = 0
sphere_num = 1
moved_item_count = 0
def get_sphere_locations(sphere_state, locations):
sphere_state.sweep_for_events(key_only=True, locations=locations)
return {loc for loc in locations if sphere_state.can_reach(loc) and sphere_state.not_flooding_a_key(sphere_state.world, loc)}
def item_percentage(player, num):
return num / total_locations_count[player]
while True:
sphere_locations = get_sphere_locations(state, unchecked_locations)
for location in sphere_locations:
unchecked_locations.remove(location)
if not location.locked and not location.forced_item:
reachable_locations_count[location.player] += 1
logging.debug(f'Sphere {sphere_num}')
logging.debug(f'Reachable locations: {reachable_locations_count}')
debug_percentages = {
player: round(item_percentage(player, num), 2)
for player, num in reachable_locations_count.items()
}
logging.debug(f'Reachable percentages: {debug_percentages}\n')
sphere_num += 1
if checked_locations:
max_percentage = max(map(lambda p: item_percentage(p, reachable_locations_count[p]), reachable_locations_count))
threshold_percentages = {player: max_percentage * .8 for player in range(1, world.players + 1)}
logging.debug(f'Thresholds: {threshold_percentages}')
balancing_players = {player for player, reachables in reachable_locations_count.items()
if item_percentage(player, reachables) < threshold_percentages[player]}
if balancing_players:
balancing_state = state.copy()
balancing_unchecked_locations = unchecked_locations.copy()
balancing_reachables = reachable_locations_count.copy()
balancing_sphere = sphere_locations.copy()
candidate_items = collections.defaultdict(set)
while True:
for location in balancing_sphere:
if location.event and (world.keyshuffle[location.item.player] != 'none' or not location.item.smallkey) \
and (world.bigkeyshuffle[location.item.player] != 'none' or not location.item.bigkey):
balancing_state.collect(location.item, True, location)
player = location.item.player
if player in balancing_players and not location.locked and location.player != player:
candidate_items[player].add(location)
balancing_sphere = get_sphere_locations(balancing_state, balancing_unchecked_locations)
for location in balancing_sphere:
balancing_unchecked_locations.remove(location)
balancing_reachables[location.player] += 1
if world.has_beaten_game(balancing_state) or all(item_percentage(player, reachables) >= threshold_percentages[player]
for player, reachables in balancing_reachables.items()):
break
elif not balancing_sphere:
raise RuntimeError('Not all required items reachable. Something went terribly wrong here.')
unlocked_locations = collections.defaultdict(set)
for l in unchecked_locations:
if l not in balancing_unchecked_locations:
unlocked_locations[l.player].add(l)
items_to_replace = []
for player in balancing_players:
locations_to_test = unlocked_locations[player]
items_to_test = candidate_items[player]
while items_to_test:
testing = items_to_test.pop()
reducing_state = state.copy()
for location in itertools.chain((l for l in items_to_replace if l.item.player == player),
items_to_test):
reducing_state.collect(location.item, True, location)
reducing_state.sweep_for_events(locations=locations_to_test)
if world.has_beaten_game(balancing_state):
if not world.has_beaten_game(reducing_state):
items_to_replace.append(testing)
else:
reduced_sphere = get_sphere_locations(reducing_state, locations_to_test)
p = item_percentage(player, reachable_locations_count[player] + len(reduced_sphere))
if p < threshold_percentages[player]:
items_to_replace.append(testing)
replaced_items = False
# sort then shuffle to maintain deterministic behaviour,
# while allowing use of set for better algorithm growth behaviour elsewhere
replacement_locations = sorted((l for l in checked_locations if not l.event and not l.locked),
key=lambda loc: (loc.name, loc.player))
random.shuffle(replacement_locations)
items_to_replace.sort(key=lambda item: (item.name, item.player))
random.shuffle(items_to_replace)
while replacement_locations and items_to_replace:
old_location = items_to_replace.pop()
for new_location in replacement_locations:
if (new_location.can_fill(state, old_location.item, False) and
old_location.can_fill(state, new_location.item, False)):
replacement_locations.remove(new_location)
new_location.item, old_location.item = old_location.item, new_location.item
if world.shopsanity[new_location.player]:
check_shop_swap(new_location)
if world.shopsanity[old_location.player]:
check_shop_swap(old_location)
new_location.event, old_location.event = True, False
logging.debug(f"Progression balancing moved {new_location.item} to {new_location}, "
f"displacing {old_location.item} into {old_location}")
moved_item_count += 1
state.collect(new_location.item, True, new_location)
replaced_items = True
break
else:
logging.warning(f"Could not Progression Balance {old_location.item}")
if replaced_items:
logging.debug(f'Moved {moved_item_count} items so far\n')
unlocked = {fresh for player in balancing_players for fresh in unlocked_locations[player]}
for location in get_sphere_locations(state, unlocked):
unchecked_locations.remove(location)
reachable_locations_count[location.player] += 1
sphere_locations.add(location)
for location in sphere_locations:
if location.event and (world.keyshuffle[location.item.player] != 'none' or not location.item.smallkey) \
and (world.bigkeyshuffle[location.item.player] != 'none' or not location.item.bigkey):
state.collect(location.item, True, location)
checked_locations |= sphere_locations
if world.has_beaten_game(state):
break
elif not sphere_locations:
logging.warning('Progression Balancing ran out of paths.')
break
def check_shop_swap(l, make_item_free=False):
if l.parent_region.name in shop_to_location_table:
if l.name in shop_to_location_table[l.parent_region.name]:
idx = shop_to_location_table[l.parent_region.name].index(l.name)
inv_slot = l.parent_region.shop.inventory[idx]
inv_slot['item'] = l.item.name
if make_item_free:
inv_slot['price'] = 0
elif l.parent_region in retro_shops:
idx = retro_shops[l.parent_region.name].index(l.name)
inv_slot = l.parent_region.shop.inventory[idx]
inv_slot['item'] = l.item.name
def balance_money_progression(world):
logger = logging.getLogger('')
state = CollectionState(world)
unchecked_locations = world.get_locations().copy()
wallet = {player: 0 for player in range(1, world.players+1)}
kiki_check = {player: False for player in range(1, world.players+1)}
kiki_paid = {player: False for player in range(1, world.players+1)}
rooms_visited = {player: set() for player in range(1, world.players+1)}
balance_locations = {player: set() for player in range(1, world.players+1)}
pay_for_locations = {'Bottle Merchant': 100, 'Chest Game': 30, 'Digging Game': 80,
'King Zora': 500, 'Blacksmith': 10}
rupee_chart = {'Rupee (1)': 1, 'Rupees (5)': 5, 'Rupees (20)': 20, 'Rupees (50)': 50,
'Rupees (100)': 100, 'Rupees (300)': 300}
rupee_rooms = {'Eastern Rupees': 90, 'Mire Key Rupees': 45, 'Mire Shooter Rupees': 90,