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 sys
import time
class Game:
@ -26,20 +27,11 @@ class Game:
self.queue = list()
self.majorItemLocations = list()
self.minorItemLocations = list()
self.itemLocations = list()
self.itemLocations = set()
self.patcher = dict()
self.graph.clear()
self.areaConnections.clear()
self.areaConnectionOffsets.clear()
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()
self.oldtime = 0.0
self.bfstime = 0.0
self.dfstime = 0.0
# print('DEBUG: Opening ROM to pick stuff')
@ -96,6 +88,8 @@ class Game:
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):
self.settings[setting] = value
@ -182,29 +176,25 @@ class Game:
def add_to_majors(self, item):
if item not in self.itemLocations:
self.itemLocations.append(item)
self.itemLocations.add(item)
if item not in self.majorItemLocations:
self.majorItemLocations.append(item)
def add_list_to_majors(self, locations):
for item in locations:
if item not in self.itemLocations:
self.itemLocations.append(item)
self.itemLocations.add(item)
if item not in self.majorItemLocations:
self.majorItemLocations.append(item)
def add_to_minors(self, item):
if item not in self.itemLocations:
self.itemLocations.append(item)
self.itemLocations.add(item)
if item not in self.minorItemLocations:
self.minorItemLocations.append(item)
def add_list_to_minors(self, locations):
for item in locations:
if item not in self.itemLocations:
self.itemLocations.append(item)
self.itemLocations.add(item)
if item not in self.minorItemLocations:
self.minorItemLocations.append(item)
@ -213,6 +203,15 @@ class Game:
return self.requirements.get(checkRequirement)
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:
self.visited.clear()
self.queue.clear()
@ -230,7 +229,7 @@ class Game:
continue
if point in path:
continue
if LimitArea:
if LimitArea and point != end:
for area in range(0, 7):
if 'S{}'.format(area) in start:
if 'S{}'.format(area) not in point:
@ -244,12 +243,118 @@ class Game:
node = edge[1]
pathReqs = self.get_requirements(start, node)
if pathReqs == None:
newpath = self.get_path(node, end, LimitArea, path, depth)
newpath = self._get_path(node, end, LimitArea, path, depth)
if newpath:
path = path + [node]
return newpath
elif pathReqs == True:
newpath = self.get_path(node, end, LimitArea, path, depth)
newpath = self._get_path(node, end, LimitArea, path, depth)
if newpath:
path = path + [node]
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):
check = int(StartLocation[1:2])
AreaOpen[check] = StartLocation
AreaOpen[int(StartLocation[1:2])] = StartLocation
if AreaOpen[0] == None:
check = 'S0-32'
path = graph.get_path(StartLocation, check)
@ -1633,94 +1633,33 @@ def find_available_areas(graph):
path = graph.get_path(check, StartLocation)
if path != None:
AreaOpen[0] = check
if AreaOpen[1] == None:
check = 'S1-00'
path = graph.get_path(StartLocation, check)
if path != None:
path = graph.get_path(check, StartLocation)
def check_sector(area, check, tunnel1, tunnel2):
if AreaOpen[area] == None:
path = graph.get_path(StartLocation, check)
if path != None:
AreaOpen[1] = check
if ScrewAttack:
if AreaOpen[1] == None:
check = 'S1-6B'
path = graph.get_path(StartLocation, check)
path = graph.get_path(check, StartLocation)
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:
AreaOpen[1] = check
if AreaOpen[1] == None:
check = 'S1-68'
path = graph.get_path(StartLocation, check)
if path != None:
path = graph.get_path(check, StartLocation)
path = graph.get_path(tunnel1, StartLocation)
if path != None:
AreaOpen[area] = tunnel1
if AreaOpen[area] == None:
path = graph.get_path(StartLocation, tunnel2)
if path != None:
AreaOpen[1] = check
if AreaOpen[2] == None:
check = 'S2-00'
path = graph.get_path(StartLocation, check)
if path != None:
path = graph.get_path(check, StartLocation)
if path != None:
AreaOpen[2] = check
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
path = graph.get_path(tunnel2, StartLocation)
if path != None:
AreaOpen[area] = tunnel2
check_sector(1, 'S1-00', 'S1-6B', 'S1-68')
check_sector(2, 'S2-00', 'S2-7F', 'S2-82')
check_sector(3, 'S3-00', 'S3-56', 'S3-59')
check_sector(4, 'S4-00', 'S4-6A', 'S4-6C')
if AreaOpen[5] == None:
check = 'S5-00'
path = graph.get_path(StartLocation, check)
@ -1736,28 +1675,9 @@ def find_available_areas(graph):
path = graph.get_path(check, StartLocation)
if path != None:
AreaOpen[5] = check
if AreaOpen[6] == None:
check = 'S6-00'
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
check_sector(6, 'S6-00', 'S6-51', 'S6-54')
if BlueDoors == False:
path = graph.get_path(StartLocation, 'Security-Level-1', depth=250)
if path != None:
@ -2253,6 +2173,7 @@ def randomize_game(graph):
find_available_areas(graph)
for location in MajorLocations:
# potential find_all_available_items
path = graph.get_path(StartLocation, location)
if path != None:
AccessibleLocations.append(location)
@ -2329,8 +2250,9 @@ def randomize_game(graph):
if location not in UsedLocations:
if location not in AccessibleLocations:
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:
# potential find_all_available_items
for area in range(0, 7):
if location in AreaItemLocations[area]:
if AreaOpen[area]:
@ -2402,6 +2324,7 @@ def randomize_game(graph):
tankCheck = math.ceil(healthCheck / MissileDamage / 5)
PossibleMissileTanks.clear()
if SeedSettings['MajorMinor']:
# potential find_all_available_items + could probably reuse previous result
for missileLocation in MinorLocations:
if missileLocation not in UsedLocations:
missilePath = None
@ -2928,9 +2851,9 @@ def patch_game():
itemProgression.update({ UsedLocations[x]: PlacedItems[x] })
spoilerLog.update({ 'Item order': itemProgression })
itemDict = dict()
World.itemLocations.sort()
for x in range(0, len(World.itemLocations)):
itemDict.update({ World.itemLocations[x]: 0 })
itemLocations = sorted(World.itemLocations)
for x in range(0, len(itemLocations)):
itemDict.update({ itemLocations[x]: 0 })
for x in range(0, len(PlacedItems)):
itemDict.update({ UsedLocations[x]: PlacedItems[x] })
spoilerLog.update({ 'Items': itemDict })
@ -3135,6 +3058,9 @@ def start_randomizer(rom, settings):
seedTime = time.time() - startTime
print(str(FileName))
print('Randomized in:', seedTime)
World.print_stats()
print("water lowered:", WaterLowered)
print("len(world.graph) = ", len(World.graph))
totalRandoTime = time.time() - totalRandoTime
print('All seeds took:', totalRandoTime)