Compare commits

...

6 Commits

Author SHA1 Message Date
magical d65898994a note potential optimization locations 2024-02-07 23:45:07 -08:00
magical 2e972d876f fix the order of this get_path call
this is the first half of an item check, where we're trying to figure
out whether samus get from her initial location to the item. (the second
half, where we want to figure out if she can get back, is below)

i'm not sure why these two items are singled out to get a full search
(no LimitArea).  well - Item S0-05-16 makes sense since that's the
quarantine area item and you have to go through sector 6 to get to it -
but why the main deck data room?
2024-02-07 23:45:07 -08:00
magical b95fef681f refactor find_available_areas a bit
no semantic changes, just condensing some repeated code for clarity.
2024-02-07 23:45:07 -08:00
magical ab089feb6e WIP faster paths 2024-02-07 23:45:07 -08:00
magical 6194d555b8 make itemLocations a set
should speed up get_path somewhat
2024-02-07 23:45:05 -08:00
magical cbb85e3baa fix items not being placed on bosses
bosses were considered unreachable when LimitArea was in effect because
they don't have a sector identifier (e.g. "S2") in their names.

this also affected non-boss things like the "Pump Control". now the
randomizer can actually require you to lower the water!
2024-02-07 23:26:43 -08:00
2 changed files with 167 additions and 136 deletions

View File

@ -12,6 +12,7 @@
import struct import struct
import sys import sys
import time
class Game: class Game:
@ -26,20 +27,11 @@ class Game:
self.queue = list() self.queue = list()
self.majorItemLocations = list() self.majorItemLocations = list()
self.minorItemLocations = list() self.minorItemLocations = list()
self.itemLocations = list() self.itemLocations = set()
self.patcher = dict() self.patcher = dict()
self.graph.clear() self.oldtime = 0.0
self.areaConnections.clear() self.bfstime = 0.0
self.areaConnectionOffsets.clear() self.dfstime = 0.0
self.doorConnections.clear()
self.rooms.clear()
self.requirements.clear()
self.visited.clear()
self.queue.clear()
self.majorItemLocations.clear()
self.minorItemLocations.clear()
self.itemLocations.clear()
self.patcher.clear()
# print('DEBUG: Opening ROM to pick stuff') # print('DEBUG: Opening ROM to pick stuff')
@ -96,6 +88,8 @@ class Game:
sys.exit(1) sys.exit(1)
def print_stats(self):
print("Path search time: old search = {}s, bfs time = {}s, dfs time = {}s".format(self.oldtime, self.bfstime, self.dfstime))
def set_setting(self, setting, value): def set_setting(self, setting, value):
self.settings[setting] = value self.settings[setting] = value
@ -182,29 +176,25 @@ class Game:
def add_to_majors(self, item): def add_to_majors(self, item):
if item not in self.itemLocations: self.itemLocations.add(item)
self.itemLocations.append(item)
if item not in self.majorItemLocations: if item not in self.majorItemLocations:
self.majorItemLocations.append(item) self.majorItemLocations.append(item)
def add_list_to_majors(self, locations): def add_list_to_majors(self, locations):
for item in locations: for item in locations:
if item not in self.itemLocations: self.itemLocations.add(item)
self.itemLocations.append(item)
if item not in self.majorItemLocations: if item not in self.majorItemLocations:
self.majorItemLocations.append(item) self.majorItemLocations.append(item)
def add_to_minors(self, item): def add_to_minors(self, item):
if item not in self.itemLocations: self.itemLocations.add(item)
self.itemLocations.append(item)
if item not in self.minorItemLocations: if item not in self.minorItemLocations:
self.minorItemLocations.append(item) self.minorItemLocations.append(item)
def add_list_to_minors(self, locations): def add_list_to_minors(self, locations):
for item in locations: for item in locations:
if item not in self.itemLocations: self.itemLocations.add(item)
self.itemLocations.append(item)
if item not in self.minorItemLocations: if item not in self.minorItemLocations:
self.minorItemLocations.append(item) self.minorItemLocations.append(item)
@ -213,6 +203,15 @@ class Game:
return self.requirements.get(checkRequirement) return self.requirements.get(checkRequirement)
def get_path(self, start, end, LimitArea=False, path=None, depth=100): def get_path(self, start, end, LimitArea=False, path=None, depth=100):
#return [start,end] if self.has_path(start,end,LimitArea, depth) else None
t = time.perf_counter()
path = self._get_path(start, end, LimitArea, path, depth)
self.oldtime += time.perf_counter() - t
return path
def _get_path(self, start, end, LimitArea, path, depth):
if path == None: if path == None:
self.visited.clear() self.visited.clear()
self.queue.clear() self.queue.clear()
@ -230,7 +229,7 @@ class Game:
continue continue
if point in path: if point in path:
continue continue
if LimitArea: if LimitArea and point != end:
for area in range(0, 7): for area in range(0, 7):
if 'S{}'.format(area) in start: if 'S{}'.format(area) in start:
if 'S{}'.format(area) not in point: if 'S{}'.format(area) not in point:
@ -244,12 +243,118 @@ class Game:
node = edge[1] node = edge[1]
pathReqs = self.get_requirements(start, node) pathReqs = self.get_requirements(start, node)
if pathReqs == None: if pathReqs == None:
newpath = self.get_path(node, end, LimitArea, path, depth) newpath = self._get_path(node, end, LimitArea, path, depth)
if newpath: if newpath:
path = path + [node] path = path + [node]
return newpath return newpath
elif pathReqs == True: elif pathReqs == True:
newpath = self.get_path(node, end, LimitArea, path, depth) newpath = self._get_path(node, end, LimitArea, path, depth)
if newpath: if newpath:
path = path + [node] path = path + [node]
return newpath return newpath
def has_path(self, start, end, LimitArea=False, depth=100):
if LimitArea:
area = self.itemArea.get(start)
if area == None:
for n in range(0, 7):
if 'S{}'.format(n) in start:
area = n
if area == None:
for n in range(0, 7):
if 'S{}'.format(n) in end:
area = n
else:
area = None
t = time.perf_counter()
result = self.has_path_bfs(start, end, area=area, max_depth=depth)
self.bfstime += time.perf_counter() - t
t = time.perf_counter()
r2 =self.has_path_dfs(start, end, area=area)
assert result == r2, (start, end, result, r2, area)
self.dfstime += time.perf_counter() - t
return result
# NOTE: both the start and end node need to be excluded from
# area checks because they can be things like bosses or the Water Pump
# that aren't associated with an area. all the intermediate nodes
# should be doors, and doors all have their sector number in their names.
def has_path_bfs(self, start, end, area=None, max_depth=100):
if start not in self.graph:
return False
if start == end:
return True
if area != None:
areaStr = 'S{}'.format(area)
seen = {start}
frontier = [start]
next_frontier = []
depth = 0
while frontier:
depth += 1
#if depth > max_depth: break
for node in frontier:
for neighbor in self.graph[node]:
if neighbor in seen:
continue
if neighbor in self.itemLocations:
if neighbor == end:
pathReqs = self.get_requirements(node, neighbor)
if pathReqs == None or pathReqs == True:
return True
#seen.add(neighbor)
continue
if area is not None:
if areaStr not in neighbor and neighbor != end:
continue
pathReqs = self.get_requirements(node, neighbor)
if pathReqs != None and pathReqs != True:
continue
if neighbor == end:
return True
next_frontier.append(neighbor)
# since this is a breadth-first search on an unweighted graph,
# we know that no other paths can be shorter than this one,
# so we can immediately mark the node as seen to prevent
# adding it to the frontier again
seen.add(neighbor)
frontier.clear()
frontier, next_frontier = next_frontier, frontier
return False
def has_path_dfs(self, start, end, area=None):
if start not in self.graph:
return False
if start == end:
return True
visited = set()
stack = [start]
if area != None:
areaStr = 'S{}'.format(area)
while stack:
node = stack.pop()
if node in visited:
continue
visited.add(node)
if node == end:
return True
if node is not start:
if node in self.itemLocations:
continue
if area is not None:
if areaStr not in node:
continue
for neighbor in reversed(self.graph[node]):
if neighbor in visited:
continue
pathReqs = self.get_requirements(node, neighbor)
if pathReqs != None and pathReqs != True:
continue
stack.append(neighbor)
return False

View File

@ -1624,8 +1624,8 @@ def update_requirements(graph):
def find_available_areas(graph): def find_available_areas(graph):
check = int(StartLocation[1:2]) AreaOpen[int(StartLocation[1:2])] = StartLocation
AreaOpen[check] = StartLocation
if AreaOpen[0] == None: if AreaOpen[0] == None:
check = 'S0-32' check = 'S0-32'
path = graph.get_path(StartLocation, check) path = graph.get_path(StartLocation, check)
@ -1633,94 +1633,33 @@ def find_available_areas(graph):
path = graph.get_path(check, StartLocation) path = graph.get_path(check, StartLocation)
if path != None: if path != None:
AreaOpen[0] = check AreaOpen[0] = check
if AreaOpen[1] == None:
check = 'S1-00' def check_sector(area, check, tunnel1, tunnel2):
path = graph.get_path(StartLocation, check) if AreaOpen[area] == None:
if path != None: path = graph.get_path(StartLocation, check)
path = graph.get_path(check, StartLocation)
if path != None: if path != None:
AreaOpen[1] = check path = graph.get_path(check, StartLocation)
if ScrewAttack:
if AreaOpen[1] == None:
check = 'S1-6B'
path = graph.get_path(StartLocation, check)
if path != None: if path != None:
path = graph.get_path(check, StartLocation) AreaOpen[area] = check
if ScrewAttack:
if AreaOpen[area] == None:
path = graph.get_path(StartLocation, tunnel1)
if path != None: if path != None:
AreaOpen[1] = check path = graph.get_path(tunnel1, StartLocation)
if AreaOpen[1] == None: if path != None:
check = 'S1-68' AreaOpen[area] = tunnel1
path = graph.get_path(StartLocation, check) if AreaOpen[area] == None:
if path != None: path = graph.get_path(StartLocation, tunnel2)
path = graph.get_path(check, StartLocation)
if path != None: if path != None:
AreaOpen[1] = check path = graph.get_path(tunnel2, StartLocation)
if AreaOpen[2] == None: if path != None:
check = 'S2-00' AreaOpen[area] = tunnel2
path = graph.get_path(StartLocation, check)
if path != None: check_sector(1, 'S1-00', 'S1-6B', 'S1-68')
path = graph.get_path(check, StartLocation) check_sector(2, 'S2-00', 'S2-7F', 'S2-82')
if path != None: check_sector(3, 'S3-00', 'S3-56', 'S3-59')
AreaOpen[2] = check check_sector(4, 'S4-00', 'S4-6A', 'S4-6C')
if ScrewAttack:
if AreaOpen[2] == None:
check = 'S2-7F'
path = graph.get_path(StartLocation, check)
if path != None:
path = graph.get_path(check, StartLocation)
if path != None:
AreaOpen[2] = check
if AreaOpen[2] == None:
check = 'S2-82'
path = graph.get_path(StartLocation, check)
if path != None:
path = graph.get_path(check, StartLocation)
if path != None:
AreaOpen[2] = check
if AreaOpen[3] == None:
check = 'S3-00'
path = graph.get_path(StartLocation, check)
if path != None:
path = graph.get_path(check, StartLocation)
if path != None:
AreaOpen[3] = check
if ScrewAttack:
if AreaOpen[3] == None:
check = 'S3-56'
path = graph.get_path(StartLocation, check)
if path != None:
path = graph.get_path(check, StartLocation)
if path != None:
AreaOpen[3] = check
if AreaOpen[3] == None:
check = 'S3-59'
path = graph.get_path(StartLocation, check)
if path != None:
path = graph.get_path(check, StartLocation)
if path != None:
AreaOpen[3] = check
if AreaOpen[4] == None:
check = 'S4-00'
path = graph.get_path(StartLocation, check)
if path != None:
path = graph.get_path(check, StartLocation)
if path != None:
AreaOpen[4] = check
if ScrewAttack:
if AreaOpen[4] == None:
check = 'S4-6A'
path = graph.get_path(StartLocation, check)
if path != None:
path = graph.get_path(check, StartLocation)
if path != None:
AreaOpen[4] = check
if AreaOpen[4] == None:
check = 'S4-6C'
path = graph.get_path(StartLocation, check)
if path != None:
path = graph.get_path(check, StartLocation)
if path != None:
AreaOpen[4] = check
if AreaOpen[5] == None: if AreaOpen[5] == None:
check = 'S5-00' check = 'S5-00'
path = graph.get_path(StartLocation, check) path = graph.get_path(StartLocation, check)
@ -1736,28 +1675,9 @@ def find_available_areas(graph):
path = graph.get_path(check, StartLocation) path = graph.get_path(check, StartLocation)
if path != None: if path != None:
AreaOpen[5] = check AreaOpen[5] = check
if AreaOpen[6] == None:
check = 'S6-00' check_sector(6, 'S6-00', 'S6-51', 'S6-54')
path = graph.get_path(StartLocation, check)
if path != None:
path = graph.get_path(check, StartLocation)
if path != None:
AreaOpen[6] = check
if ScrewAttack:
if AreaOpen[6] == None:
check = 'S6-51'
path = graph.get_path(StartLocation, check)
if path != None:
path = graph.get_path(check, StartLocation)
if path != None:
AreaOpen[6] = check
if AreaOpen[6] == None:
check = 'S6-54'
path = graph.get_path(StartLocation, check)
if path != None:
path = graph.get_path(check, StartLocation)
if path != None:
AreaOpen[6] = check
if BlueDoors == False: if BlueDoors == False:
path = graph.get_path(StartLocation, 'Security-Level-1', depth=250) path = graph.get_path(StartLocation, 'Security-Level-1', depth=250)
if path != None: if path != None:
@ -2253,6 +2173,7 @@ def randomize_game(graph):
find_available_areas(graph) find_available_areas(graph)
for location in MajorLocations: for location in MajorLocations:
# potential find_all_available_items
path = graph.get_path(StartLocation, location) path = graph.get_path(StartLocation, location)
if path != None: if path != None:
AccessibleLocations.append(location) AccessibleLocations.append(location)
@ -2329,8 +2250,9 @@ def randomize_game(graph):
if location not in UsedLocations: if location not in UsedLocations:
if location not in AccessibleLocations: if location not in AccessibleLocations:
if location == 'Data S0' or location == 'Item S0-05-16': if location == 'Data S0' or location == 'Item S0-05-16':
path = graph.get_path(location, StartLocation, depth=250) path = graph.get_path(StartLocation, location, depth=250)
else: else:
# potential find_all_available_items
for area in range(0, 7): for area in range(0, 7):
if location in AreaItemLocations[area]: if location in AreaItemLocations[area]:
if AreaOpen[area]: if AreaOpen[area]:
@ -2402,6 +2324,7 @@ def randomize_game(graph):
tankCheck = math.ceil(healthCheck / MissileDamage / 5) tankCheck = math.ceil(healthCheck / MissileDamage / 5)
PossibleMissileTanks.clear() PossibleMissileTanks.clear()
if SeedSettings['MajorMinor']: if SeedSettings['MajorMinor']:
# potential find_all_available_items + could probably reuse previous result
for missileLocation in MinorLocations: for missileLocation in MinorLocations:
if missileLocation not in UsedLocations: if missileLocation not in UsedLocations:
missilePath = None missilePath = None
@ -2928,9 +2851,9 @@ def patch_game():
itemProgression.update({ UsedLocations[x]: PlacedItems[x] }) itemProgression.update({ UsedLocations[x]: PlacedItems[x] })
spoilerLog.update({ 'Item order': itemProgression }) spoilerLog.update({ 'Item order': itemProgression })
itemDict = dict() itemDict = dict()
World.itemLocations.sort() itemLocations = sorted(World.itemLocations)
for x in range(0, len(World.itemLocations)): for x in range(0, len(itemLocations)):
itemDict.update({ World.itemLocations[x]: 0 }) itemDict.update({ itemLocations[x]: 0 })
for x in range(0, len(PlacedItems)): for x in range(0, len(PlacedItems)):
itemDict.update({ UsedLocations[x]: PlacedItems[x] }) itemDict.update({ UsedLocations[x]: PlacedItems[x] })
spoilerLog.update({ 'Items': itemDict }) spoilerLog.update({ 'Items': itemDict })
@ -3135,6 +3058,9 @@ def start_randomizer(rom, settings):
seedTime = time.time() - startTime seedTime = time.time() - startTime
print(str(FileName)) print(str(FileName))
print('Randomized in:', seedTime) print('Randomized in:', seedTime)
World.print_stats()
print("water lowered:", WaterLowered)
print("len(world.graph) = ", len(World.graph))
totalRandoTime = time.time() - totalRandoTime totalRandoTime = time.time() - totalRandoTime
print('All seeds took:', totalRandoTime) print('All seeds took:', totalRandoTime)