Merge branch 'DoorPerf' of https://github.com/compiling/ALttPEntranceRandomizer into compiling-DoorPerf

This commit is contained in:
aerinon
2020-05-26 14:35:12 -06:00
12 changed files with 222 additions and 207 deletions

View File

@@ -1,12 +1,18 @@
import copy
from enum import Enum, unique, Flag
import logging
import json
from collections import OrderedDict, deque, defaultdict
import logging
from collections import OrderedDict, Counter, deque, defaultdict
from enum import Enum, unique
try:
from fast_enum import FastEnum
except ImportError:
from enum import Flag
FastEnum = Flag
from source.classes.BabelFish import BabelFish
from EntranceShuffle import door_addresses
from _vendor.collections_extended import bag
from EntranceShuffle import door_addresses, indirect_connections
from Utils import int16_as_bytes
from Tables import normal_offset_table, spiral_offset_table, multiply_lookup, divisor_lookup
from RoomData import Room
@@ -126,6 +132,12 @@ class World(object):
for region in regions if regions else self.regions:
region.world = self
self._region_cache[region.player][region.name] = region
for exit in region.exits:
self._entrance_cache[(exit.name, exit.player)] = exit
def initialize_doors(self, doors):
for door in doors:
self._door_cache[(door.name, door.player)] = door
def get_regions(self, player=None):
return self.regions if player is None else self._region_cache[player].values()
@@ -235,41 +247,41 @@ class World(object):
if ret.has('Golden Sword', item.player):
pass
elif ret.has('Tempered Sword', item.player) and self.difficulty_requirements[item.player].progressive_sword_limit >= 4:
ret.prog_items.add(('Golden Sword', item.player))
ret.prog_items['Golden Sword', item.player] += 1
elif ret.has('Master Sword', item.player) and self.difficulty_requirements[item.player].progressive_sword_limit >= 3:
ret.prog_items.add(('Tempered Sword', item.player))
ret.prog_items['Tempered Sword', item.player] += 1
elif ret.has('Fighter Sword', item.player) and self.difficulty_requirements[item.player].progressive_sword_limit >= 2:
ret.prog_items.add(('Master Sword', item.player))
ret.prog_items['Master Sword', item.player] += 1
elif self.difficulty_requirements[item.player].progressive_sword_limit >= 1:
ret.prog_items.add(('Fighter Sword', item.player))
ret.prog_items['Fighter Sword', item.player] += 1
elif 'Glove' in item.name:
if ret.has('Titans Mitts', item.player):
pass
elif ret.has('Power Glove', item.player):
ret.prog_items.add(('Titans Mitts', item.player))
ret.prog_items['Titans Mitts', item.player] += 1
else:
ret.prog_items.add(('Power Glove', item.player))
ret.prog_items['Power Glove', item.player] += 1
elif 'Shield' in item.name:
if ret.has('Mirror Shield', item.player):
pass
elif ret.has('Red Shield', item.player) and self.difficulty_requirements[item.player].progressive_shield_limit >= 3:
ret.prog_items.add(('Mirror Shield', item.player))
ret.prog_items['Mirror Shield', item.player] += 1
elif ret.has('Blue Shield', item.player) and self.difficulty_requirements[item.player].progressive_shield_limit >= 2:
ret.prog_items.add(('Red Shield', item.player))
ret.prog_items['Red Shield', item.player] += 1
elif self.difficulty_requirements[item.player].progressive_shield_limit >= 1:
ret.prog_items.add(('Blue Shield', item.player))
ret.prog_items['Blue Shield', item.player] += 1
elif 'Bow' in item.name:
if ret.has('Silver Arrows', item.player):
pass
elif ret.has('Bow', item.player) and self.difficulty_requirements[item.player].progressive_bow_limit >= 2:
ret.prog_items.add(('Silver Arrows', item.player))
ret.prog_items['Silver Arrows', item.player] += 1
elif self.difficulty_requirements[item.player].progressive_bow_limit >= 1:
ret.prog_items.add(('Bow', item.player))
ret.prog_items['Bow', item.player] += 1
elif item.name.startswith('Bottle'):
if ret.bottle_count(item.player) < self.difficulty_requirements[item.player].progressive_bottle_limit:
ret.prog_items.add((item.name, item.player))
ret.prog_items[item.name, item.player] += 1
elif item.advancement or item.smallkey or item.bigkey:
ret.prog_items.add((item.name, item.player))
ret.prog_items[item.name, item.player] += 1
for item in self.itempool:
soft_collect(item)
@@ -384,7 +396,6 @@ class World(object):
prog_locations = [location for location in self.get_locations() if location.item is not None and (location.item.advancement or location.event) and location not in state.locations_checked]
while prog_locations:
state.sweep_for_crystal_access()
sphere = []
# build up spheres of collection radius. Everything in each sphere is independent from each other in dependencies and only depends on lower spheres
for location in prog_locations:
@@ -408,11 +419,10 @@ class World(object):
class CollectionState(object):
def __init__(self, parent):
self.prog_items = bag()
self.prog_items = Counter()
self.world = parent
self.reachable_regions = {player: set() for player in range(1, parent.players + 1)}
self.colored_regions = {player: {} for player in range(1, parent.players + 1)}
self.blocked_color_regions = {player: set() for player in range(1, parent.players + 1)}
self.reachable_regions = {player: dict() for player in range(1, parent.players + 1)}
self.blocked_connections = {player: dict() for player in range(1, parent.players + 1)}
self.events = []
self.path = {}
self.locations_checked = set()
@@ -421,88 +431,71 @@ class CollectionState(object):
self.collect(item, True)
def update_reachable_regions(self, player):
player_regions = self.world.get_regions(player)
self.stale[player] = False
rrp = self.reachable_regions[player]
ccr = self.colored_regions[player]
blocked = self.blocked_color_regions[player]
new_regions = True
reachable_regions_count = len(rrp)
while new_regions:
player_regions = [region for region in player_regions if region not in rrp]
for candidate in player_regions:
if candidate.can_reach_private(self):
rrp.add(candidate)
if candidate.type == RegionType.Dungeon:
c_switch_present = False
for ext in candidate.exits:
door = self.world.check_for_door(ext.name, player)
if door is not None and door.crystal == CrystalBarrier.Either:
c_switch_present = True
break
if c_switch_present:
ccr[candidate] = CrystalBarrier.Either
self.spread_crystal_access(candidate, CrystalBarrier.Either, rrp, ccr, player)
for ext in candidate.exits:
connect = ext.connected_region
if connect in rrp and not ext.can_reach(self):
blocked.add(candidate)
else:
color_type = CrystalBarrier.Null
for entrance in candidate.entrances:
if entrance.parent_region in rrp:
if entrance.can_reach(self):
door = self.world.check_for_door(entrance.name, player)
if door is None or entrance.parent_region.type != RegionType.Dungeon:
color_type |= CrystalBarrier.Orange
elif entrance.parent_region in ccr.keys():
color_type |= (ccr[entrance.parent_region] & (door.crystal or CrystalBarrier.Either))
else:
blocked.add(entrance.parent_region)
if color_type:
ccr[candidate] = color_type
for ext in candidate.exits:
connect = ext.connected_region
if connect in rrp and connect in ccr:
door = self.world.check_for_door(ext.name, player)
if door is not None and not door.blocked:
if ext.can_reach(self):
new_color = ccr[connect] | (ccr[candidate] & (door.crystal or CrystalBarrier.Either))
if new_color != ccr[connect]:
self.spread_crystal_access(candidate, new_color, rrp, ccr, player)
else:
blocked.add(candidate)
new_regions = len(rrp) > reachable_regions_count
reachable_regions_count = len(rrp)
bc = self.blocked_connections[player]
def spread_crystal_access(self, region, crystal, rrp, ccr, player):
queue = deque([(region, crystal)])
visited = set()
updated = False
while len(queue) > 0:
region, crystal = queue.popleft()
visited.add(region)
for ext in region.exits:
connect = ext.connected_region
if connect is not None and connect.type == RegionType.Dungeon:
if connect not in visited and connect in rrp and connect in ccr:
if ext.can_reach(self):
door = self.world.check_for_door(ext.name, player)
# init on first call - this can't be done on construction since the regions don't exist yet
start = self.world.get_region('Menu', player)
if not start in rrp:
rrp[start] = CrystalBarrier.Orange
for exit in start.exits:
bc[exit] = CrystalBarrier.Orange
queue = deque(self.blocked_connections[player].items())
# run BFS on all connections, and keep track of those blocked by missing items
while True:
try:
connection, crystal_state = queue.popleft()
new_region = connection.connected_region
if new_region is None or new_region in rrp and (new_region.type != RegionType.Dungeon or (rrp[new_region] & crystal_state) == crystal_state):
bc.pop(connection, None)
elif connection.can_reach(self):
if new_region.type == RegionType.Dungeon:
new_crystal_state = crystal_state
for exit in new_region.exits:
door = exit.door
if door is not None and door.crystal == CrystalBarrier.Either:
new_crystal_state = CrystalBarrier.Either
break
if new_region in rrp:
new_crystal_state |= rrp[new_region]
rrp[new_region] = new_crystal_state
for exit in new_region.exits:
door = exit.door
if door is not None and not door.blocked:
current_crystal = ccr[connect]
new_crystal = current_crystal | (crystal & (door.crystal or CrystalBarrier.Either))
if current_crystal != new_crystal:
updated = True
ccr[connect] = new_crystal
queue.append((connect, new_crystal))
return updated
door_crystal_state = new_crystal_state & (door.crystal or CrystalBarrier.Either)
bc[exit] = door_crystal_state
queue.append((exit, door_crystal_state))
elif door is None:
queue.append((exit, new_crystal_state))
else:
new_crystal_state = CrystalBarrier.Orange
rrp[new_region] = new_crystal_state
bc.pop(connection, None)
for exit in new_region.exits:
bc[exit] = new_crystal_state
queue.append((exit, new_crystal_state))
self.path[new_region] = (new_region.name, self.path.get(connection, None))
# Retry connections if the new region can unblock them
if new_region.name in indirect_connections:
new_entrance = self.world.get_entrance(indirect_connections[new_region.name], player)
if new_entrance in bc and new_entrance not in queue and new_entrance.parent_region in rrp:
queue.append((new_entrance, rrp[new_entrance.parent_region]))
except IndexError:
break
def copy(self):
ret = CollectionState(self.world)
ret.prog_items = self.prog_items.copy()
ret.reachable_regions = {player: copy.copy(self.reachable_regions[player]) for player in range(1, self.world.players + 1)}
ret.colored_regions = {player: copy.copy(self.colored_regions[player]) for player in range(1, self.world.players + 1)}
ret.blocked_color_regions = {player: copy.copy(self.blocked_color_regions[player]) for player in range(1, self.world.players + 1)}
ret.blocked_connections = {player: copy.copy(self.blocked_connections[player]) for player in range(1, self.world.players + 1)}
ret.events = copy.copy(self.events)
ret.path = copy.copy(self.path)
ret.locations_checked = copy.copy(self.locations_checked)
@@ -523,19 +516,6 @@ class CollectionState(object):
return spot.can_reach(self)
def sweep_for_crystal_access(self):
for player, rrp in self.reachable_regions.items():
updated = True
while updated:
if self.stale[player]:
self.update_reachable_regions(player)
updated = False
dungeon_regions = self.blocked_color_regions[player]
ccr = self.colored_regions[player]
for region in dungeon_regions.copy():
if region in ccr.keys():
updated |= self.spread_crystal_access(region, ccr[region], rrp, ccr, player)
self.stale[player] = updated
def sweep_for_events(self, key_only=False, locations=None):
# this may need improvement
@@ -554,18 +534,13 @@ class CollectionState(object):
self.collect(event.item, True, event)
new_locations = len(reachable_events) > checked_locations
checked_locations = len(reachable_events)
if new_locations:
self.sweep_for_crystal_access()
def can_reach_blue(self, region, player):
if region not in self.colored_regions[player].keys():
return False
return self.colored_regions[player][region] in [CrystalBarrier.Blue, CrystalBarrier.Either]
return region in self.reachable_regions[player] and self.reachable_regions[player][region] in [CrystalBarrier.Blue, CrystalBarrier.Either]
def can_reach_orange(self, region, player):
if region not in self.colored_regions[player].keys():
return False
return self.colored_regions[player][region] in [CrystalBarrier.Orange, CrystalBarrier.Either]
return region in self.reachable_regions[player] and self.reachable_regions[player][region] in [CrystalBarrier.Orange, CrystalBarrier.Either]
def _do_not_flood_the_keys(self, reachable_events):
adjusted_checks = list(reachable_events)
@@ -584,14 +559,14 @@ class CollectionState(object):
def has(self, item, player, count=1):
if count == 1:
return (item, player) in self.prog_items
return self.prog_items.count((item, player)) >= count
return self.prog_items[item, player] >= count
def has_key(self, item, player, count=1):
if self.world.retro[player]:
return self.can_buy_unlimited('Small Key (Universal)', player)
if count == 1:
return (item, player) in self.prog_items
return self.prog_items.count((item, player)) >= count
return self.prog_items[item, player] >= count
def can_buy_unlimited(self, item, player):
for shop in self.world.shops:
@@ -600,7 +575,7 @@ class CollectionState(object):
return False
def item_count(self, item, player):
return self.prog_items.count((item, player))
return self.prog_items[item, player]
def has_crystals(self, count, player):
crystals = ['Crystal 1', 'Crystal 2', 'Crystal 3', 'Crystal 4', 'Crystal 5', 'Crystal 6', 'Crystal 7']
@@ -634,9 +609,9 @@ class CollectionState(object):
def can_extend_magic(self, player, smallmagic=16, fullrefill=False): #This reflects the total magic Link has, not the total extra he has.
basemagic = 8
if self.has('Quarter Magic', player):
if self.has('Magic Upgrade (1/4)', player):
basemagic = 32
elif self.has('Half Magic', player):
elif self.has('Magic Upgrade (1/2)', player):
basemagic = 16
if self.can_buy_unlimited('Green Potion', player) or self.can_buy_unlimited('Blue Potion', player):
if self.world.difficulty_adjustments[player] == 'hard' and not fullrefill:
@@ -657,9 +632,8 @@ class CollectionState(object):
def can_shoot_arrows(self, player):
if self.world.retro[player]:
#TODO: need to decide how we want to handle wooden arrows longer-term (a can-buy-a check, or via dynamic shop location)
#FIXME: Should do something about hard+ ganon only silvers. For the moment, i believe they effective grant wooden, so we are safe
return self.has('Bow', player) and (self.has('Silver Arrows', player) or self.can_buy_unlimited('Single Arrow', player))
#todo: Non-progressive silvers grant wooden arrows, but progressive bows do not. Always require shop arrows to be safe
return self.has('Bow', player) and self.can_buy_unlimited('Single Arrow', player)
return self.has('Bow', player)
def can_get_good_bee(self, player):
@@ -734,63 +708,63 @@ class CollectionState(object):
if self.has('Golden Sword', item.player):
pass
elif self.has('Tempered Sword', item.player) and self.world.difficulty_requirements[item.player].progressive_sword_limit >= 4:
self.prog_items.add(('Golden Sword', item.player))
self.prog_items['Golden Sword', item.player] += 1
changed = True
elif self.has('Master Sword', item.player) and self.world.difficulty_requirements[item.player].progressive_sword_limit >= 3:
self.prog_items.add(('Tempered Sword', item.player))
self.prog_items['Tempered Sword', item.player] += 1
changed = True
elif self.has('Fighter Sword', item.player) and self.world.difficulty_requirements[item.player].progressive_sword_limit >= 2:
self.prog_items.add(('Master Sword', item.player))
self.prog_items['Master Sword', item.player] += 1
changed = True
elif self.world.difficulty_requirements[item.player].progressive_sword_limit >= 1:
self.prog_items.add(('Fighter Sword', item.player))
self.prog_items['Fighter Sword', item.player] += 1
changed = True
elif 'Glove' in item.name:
if self.has('Titans Mitts', item.player):
pass
elif self.has('Power Glove', item.player):
self.prog_items.add(('Titans Mitts', item.player))
self.prog_items['Titans Mitts', item.player] += 1
changed = True
else:
self.prog_items.add(('Power Glove', item.player))
self.prog_items['Power Glove', item.player] += 1
changed = True
elif 'Shield' in item.name:
if self.has('Mirror Shield', item.player):
pass
elif self.has('Red Shield', item.player) and self.world.difficulty_requirements[item.player].progressive_shield_limit >= 3:
self.prog_items.add(('Mirror Shield', item.player))
self.prog_items['Mirror Shield', item.player] += 1
changed = True
elif self.has('Blue Shield', item.player) and self.world.difficulty_requirements[item.player].progressive_shield_limit >= 2:
self.prog_items.add(('Red Shield', item.player))
self.prog_items['Red Shield', item.player] += 1
changed = True
elif self.world.difficulty_requirements[item.player].progressive_shield_limit >= 1:
self.prog_items.add(('Blue Shield', item.player))
self.prog_items['Blue Shield', item.player] += 1
changed = True
elif 'Bow' in item.name:
if self.has('Silver Arrows', item.player):
pass
elif self.has('Bow', item.player):
self.prog_items.add(('Silver Arrows', item.player))
self.prog_items['Silver Arrows', item.player] += 1
changed = True
else:
self.prog_items.add(('Bow', item.player))
self.prog_items['Bow', item.player] += 1
changed = True
elif 'Armor' in item.name:
if self.has('Red Mail', item.player):
pass
elif self.has('Blue Mail', item.player):
self.prog_items.add(('Red Mail', item.player))
self.prog_items['Red Mail', item.player] += 1
changed = True
else:
self.prog_items.add(('Blue Mail', item.player))
self.prog_items['Blue Mail', item.player] += 1
changed = True
elif item.name.startswith('Bottle'):
if self.bottle_count(item.player) < self.world.difficulty_requirements[item.player].progressive_bottle_limit:
self.prog_items.add((item.name, item.player))
self.prog_items[item.name, item.player] += 1
changed = True
elif event or item.advancement:
self.prog_items.add((item.name, item.player))
self.prog_items[item.name, item.player] += 1
changed = True
self.stale[item.player] = True
@@ -839,13 +813,13 @@ class CollectionState(object):
to_remove = None
if to_remove is not None:
try:
self.prog_items.remove((to_remove, item.player))
except ValueError:
return
self.prog_items[to_remove, item.player] -= 1
if self.prog_items[to_remove, item.player] < 1:
del (self.prog_items[to_remove, item.player])
# invalidate caches, nothing can be trusted anymore now
self.reachable_regions[item.player] = set()
self.reachable_regions[item.player] = dict()
self.blocked_connections[item.player] = set()
self.stale[item.player] = True
def __getattr__(self, item):
@@ -935,10 +909,11 @@ class Entrance(object):
self.access_rule = lambda state: True
self.player = player
self.door = None
self.hide_path = False
def can_reach(self, state):
if self.parent_region.can_reach(state) and self.access_rule(state):
if not self in state.path:
if not self.hide_path and not self in state.path:
state.path[self] = (self.name, state.path.get(self.parent_region, (self.parent_region.name, None)))
return True
@@ -1135,8 +1110,7 @@ class PolSlot(Enum):
EastWest = 1
Stairs = 2
@unique
class CrystalBarrier(Flag):
class CrystalBarrier(FastEnum):
Null = 0 # no special requirement
Blue = 1 # blue must be down and explore state set to Blue
Orange = 2 # orange must be down and explore state set to Orange