Replace world exploration with a faster algorithm - use BFS and keep track of all entrances that are currently blocked by progression items.
New algorithm also obsoletes sweep_for_crystal_access Set up door and entrance caches in advance Replace CrystalBarrier with FastEnum for bitfield arithmetic
This commit is contained in:
190
BaseClasses.py
190
BaseClasses.py
@@ -1,12 +1,20 @@
|
||||
import copy
|
||||
from enum import Enum, unique, Flag
|
||||
import logging
|
||||
import json
|
||||
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
|
||||
# Bitflag logic is significantly faster when not using normal python enums.
|
||||
logging.info('fast-enum module not found - falling back to slow enums. Run `pip install fast-enum` to remove this warning.')
|
||||
|
||||
|
||||
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 +134,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()
|
||||
@@ -384,7 +398,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:
|
||||
@@ -410,9 +423,8 @@ class CollectionState(object):
|
||||
def __init__(self, parent):
|
||||
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 +433,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 +518,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 +536,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)
|
||||
@@ -844,7 +821,8 @@ class CollectionState(object):
|
||||
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):
|
||||
@@ -934,10 +912,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
|
||||
|
||||
@@ -1134,8 +1113,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
|
||||
|
||||
Reference in New Issue
Block a user