Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Super Metroid: Replace random module with world random in variaRandomizer #4429

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion worlds/sm/__init__.py
Mysteryem marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ def generate_early(self):
Logic.factory('vanilla')

dummy_rom_file = Utils.user_path(SMSettings.RomFile.copy_to) # actual rom set in generate_output
self.variaRando = VariaRandomizer(self.options, dummy_rom_file, self.player)
self.variaRando = VariaRandomizer(self.options, dummy_rom_file, self.player, self.multiworld.seed, self.random)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think passing self.multiworld.seed might not be what we want.
This is an issue I've run into in other games.

If there are 2 Super Metroid players in the same multiworld, with similar enough settings, they might find that they both have certain things randomized to the same result.
For example: 2 Super Metroid players both turn on area rando, and they both end up with the exact same area connections - which is not what they expect.

I haven't tested whether this is the case (if not with area rando, then it might be with something else).

It might be better to pass self.multiworld.seed + self.player, or maybe just something from self.random.randrange

Copy link
Contributor Author

@Mysteryem Mysteryem Feb 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The only use of the seed that I could find is in APPostPatchRom to call romPatcher.writeSeed(seed). The values written are used for the seed display (picks four enemy names to display at the top of the screen) after the save file selection screen.

Set into patcherSettings["seed"]

romPatcher is created and customPostPatchApply(romPatcher) is called, calling SMWorld.APPostPatchRom(romPatcher)

if args.rom is not None:
# patch local rom
# romFileName = args.rom
# shutil.copyfile(romFileName, outputFilename)
romPatcher = RomPatcher(settings=patcherSettings, magic=args.raceMagic, player=self.player)
else:
romPatcher = RomPatcher(settings=patcherSettings, magic=args.raceMagic)
if customPrePatchApply != None:
customPrePatchApply(romPatcher)
romPatcher.patchRom()
if customPostPatchApply != None:
customPostPatchApply(romPatcher)

romPatcher.writeSeed() is called with the seed as its argument

if not romPatcher.settings["isPlando"]:
romPatcher.writeSeed(romPatcher.settings["seed"]) # lol if race mode

Writes seed values starting at 0xdfff00

def writeSeed(self, seed):
random.seed(seed)
seedInfo = random.randint(0, 0xFFFF)
seedInfo2 = random.randint(0, 0xFFFF)
self.romFile.writeWord(seedInfo, snes_to_pc(0xdfff00))
self.romFile.writeWord(seedInfo2)

https://github.com/theonlydude/RandomMetroidSolver/blob/master/patches/common/src/seed_display.asm uses that same 0xdfff00 address.

@lordlou would know better if the seed controls anything else, and whether it should be the same for multiple SM players in the same multiworld (so use the multiworld's seed) or if it should be different, but still deterministic for each individual SM player in the same multiworld (so use each world's .random to create a seed). The PR is currently using the multiworld's seed.

self.multiworld.state.smbm[self.player] = SMBoolManager(self.player, self.variaRando.maxDifficulty)

# keeps Nothing items local so no player will ever pickup Nothing
Expand Down
26 changes: 16 additions & 10 deletions worlds/sm/variaRandomizer/graph/graph_utils.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import copy
import random
from ..logic.logic import Logic
from ..utils.parameters import Knows
from ..graph.location import locationsDict
Expand Down Expand Up @@ -136,7 +135,8 @@ def getPossibleStartAPs(areaMode, maxDiff, morphPlacement, player):
refused[apName] = cause
return ret, refused

def updateLocClassesStart(startGraphArea, split, possibleMajLocs, preserveMajLocs, nLocs):
@staticmethod
def updateLocClassesStart(startGraphArea, split, possibleMajLocs, preserveMajLocs, nLocs, random):
locs = locationsDict
preserveMajLocs = [locs[locName] for locName in preserveMajLocs if locs[locName].isClass(split)]
possLocs = [locs[locName] for locName in possibleMajLocs][:nLocs]
Expand All @@ -160,7 +160,8 @@ def getGraphPatches(startApName):
ap = getAccessPoint(startApName)
return ap.Start['patches'] if 'patches' in ap.Start else []

def createBossesTransitions():
@staticmethod
def createBossesTransitions(random):
transitions = vanillaBossesTransitions
def isVanilla():
for t in vanillaBossesTransitions:
Expand All @@ -180,13 +181,15 @@ def isVanilla():
transitions.append((src,dst))
return transitions

def createAreaTransitions(lightAreaRando=False):
@staticmethod
def createAreaTransitions(lightAreaRando=False, *, random):
if lightAreaRando:
return GraphUtils.createLightAreaTransitions()
return GraphUtils.createLightAreaTransitions(random=random)
else:
return GraphUtils.createRegularAreaTransitions()
return GraphUtils.createRegularAreaTransitions(random=random)

def createRegularAreaTransitions(apList=None, apPred=None):
@staticmethod
def createRegularAreaTransitions(apList=None, apPred=None, *, random):
if apList is None:
apList = Logic.accessPoints
if apPred is None:
Expand Down Expand Up @@ -239,7 +242,8 @@ def loopUnusedTransitions(transitions, apList=None):
transitions.append((ap.Name, ap.Name))

# crateria can be forced in corner cases
def createMinimizerTransitions(startApName, locLimit, forcedAreas=None):
@staticmethod
def createMinimizerTransitions(startApName, locLimit, forcedAreas=None, *, random):
if forcedAreas is None:
forcedAreas = []
if startApName == 'Ceres':
Expand Down Expand Up @@ -316,7 +320,8 @@ def openTransitions():
GraphUtils.log.debug("FINAL MINIMIZER areas: "+str(areas))
return transitions

def createLightAreaTransitions():
@staticmethod
def createLightAreaTransitions(random):
# group APs by area
aps = {}
totalCount = 0
Expand Down Expand Up @@ -407,7 +412,8 @@ def getRooms():

return rooms

def escapeAnimalsTransitions(graph, possibleTargets, firstEscape):
@staticmethod
def escapeAnimalsTransitions(graph, possibleTargets, firstEscape, random):
n = len(possibleTargets)
assert (n < 4 and firstEscape is not None) or (n <= 4 and firstEscape is None), "Invalid possibleTargets list: " + str(possibleTargets)
GraphUtils.log.debug("escapeAnimalsTransitions. possibleTargets="+str(possibleTargets)+", firstEscape="+str(firstEscape))
Expand Down
22 changes: 11 additions & 11 deletions worlds/sm/variaRandomizer/rando/Choice.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import random
from ..utils import log
from ..utils.utils import getRangeDict, chooseFromRange
from ..rando.ItemLocContainer import ItemLocation
Expand All @@ -23,8 +22,9 @@ def getLocList(self, itemLocDict, item):

# simple random choice, that chooses an item first, then a locatio to put it in
class ItemThenLocChoice(Choice):
def __init__(self, restrictions):
def __init__(self, restrictions, random):
super(ItemThenLocChoice, self).__init__(restrictions)
self.random = random

def chooseItemLoc(self, itemLocDict, isProg):
itemList = self.getItemList(itemLocDict)
Expand All @@ -49,7 +49,7 @@ def chooseItemProg(self, itemList):
return self.chooseItemRandom(itemList)

def chooseItemRandom(self, itemList):
return random.choice(itemList)
return self.random.choice(itemList)

def chooseLocation(self, locList, item, isProg):
if len(locList) == 0:
Expand All @@ -63,12 +63,12 @@ def chooseLocationProg(self, locList, item):
return self.chooseLocationRandom(locList)

def chooseLocationRandom(self, locList):
return random.choice(locList)
return self.random.choice(locList)

# Choice specialization for prog speed based filler
class ItemThenLocChoiceProgSpeed(ItemThenLocChoice):
def __init__(self, restrictions, progSpeedParams, distanceProp, services):
super(ItemThenLocChoiceProgSpeed, self).__init__(restrictions)
def __init__(self, restrictions, progSpeedParams, distanceProp, services, random):
super(ItemThenLocChoiceProgSpeed, self).__init__(restrictions, random)
self.progSpeedParams = progSpeedParams
self.distanceProp = distanceProp
self.services = services
Expand Down Expand Up @@ -104,7 +104,7 @@ def chooseItemLoc(self, itemLocDict, isProg, progressionItemLocs, ap, container)
if self.restrictions.isLateMorph() and canRollback and len(itemLocDict) == 1:
item, locList = list(itemLocDict.items())[0]
if item.Type == 'Morph':
morphLocs = self.restrictions.lateMorphCheck(container, locList)
morphLocs = self.restrictions.lateMorphCheck(container, locList, self.random)
if morphLocs is not None:
itemLocDict[item] = morphLocs
else:
Expand All @@ -115,7 +115,7 @@ def chooseItemLoc(self, itemLocDict, isProg, progressionItemLocs, ap, container)
assert len(locs) == 1 and locs[0].Name == item.Name
return ItemLocation(item, locs[0])
# late doors check for random door colors
if self.restrictions.isLateDoors() and random.random() < self.lateDoorsProb:
if self.restrictions.isLateDoors() and self.random.random() < self.lateDoorsProb:
self.processLateDoors(itemLocDict, ap, container)
self.progressionItemLocs = progressionItemLocs
self.ap = ap
Expand Down Expand Up @@ -145,14 +145,14 @@ def chooseItemProg(self, itemList):

def chooseLocationProg(self, locs, item):
locs = self.getLocsSpreadProgression(locs)
random.shuffle(locs)
self.random.shuffle(locs)
ret = self.getChooseFunc(self.chooseLocRanges, self.chooseLocFuncs)(locs)
self.log.debug('chooseLocationProg. ret='+ret.Name)
return ret

# get choose function from a weighted dict
def getChooseFunc(self, rangeDict, funcDict):
v = chooseFromRange(rangeDict)
v = chooseFromRange(rangeDict, self.random)

return funcDict[v]

Expand Down Expand Up @@ -209,6 +209,6 @@ def getLocsSpreadProgression(self, availableLocations):
for i in range(len(availableLocations)):
loc = availableLocations[i]
d = distances[i]
if d == maxDist or random.random() >= self.spreadProb:
if d == maxDist or self.random.random() >= self.spreadProb:
locs.append(loc)
return locs
12 changes: 6 additions & 6 deletions worlds/sm/variaRandomizer/rando/Filler.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@

import copy, time, random
import copy, time
from ..utils import log
from ..logic.cache import RequestCache
from ..rando.RandoServices import RandoServices
Expand All @@ -15,11 +15,11 @@
# item pool is not empty).
# entry point is generateItems
class Filler(object):
def __init__(self, startAP, graph, restrictions, emptyContainer, endDate=infinity):
def __init__(self, startAP, graph, restrictions, emptyContainer, endDate=infinity, *, random):
self.startAP = startAP
self.cache = RequestCache()
self.graph = graph
self.services = RandoServices(graph, restrictions, self.cache)
self.services = RandoServices(graph, restrictions, self.cache, random=random)
self.restrictions = restrictions
self.settings = restrictions.settings
self.endDate = endDate
Expand Down Expand Up @@ -108,9 +108,9 @@ def step(self):

# very simple front fill algorithm with no rollback and no "softlock checks" (== dessy algorithm)
class FrontFiller(Filler):
def __init__(self, startAP, graph, restrictions, emptyContainer, endDate=infinity):
super(FrontFiller, self).__init__(startAP, graph, restrictions, emptyContainer, endDate)
self.choice = ItemThenLocChoice(restrictions)
def __init__(self, startAP, graph, restrictions, emptyContainer, endDate=infinity, *, random):
super(FrontFiller, self).__init__(startAP, graph, restrictions, emptyContainer, endDate, random=random)
self.choice = ItemThenLocChoice(restrictions, random)
self.stdStart = GraphUtils.isStandardStart(self.startAP)

def isEarlyGame(self):
Expand Down
17 changes: 9 additions & 8 deletions worlds/sm/variaRandomizer/rando/GraphBuilder.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@

import random, copy
import copy
from ..utils import log
from ..graph.graph_utils import GraphUtils, vanillaTransitions, vanillaBossesTransitions, escapeSource, escapeTargets, graphAreas, getAccessPoint
from ..logic.logic import Logic
Expand All @@ -11,13 +11,14 @@

# creates graph and handles randomized escape
class GraphBuilder(object):
def __init__(self, graphSettings):
def __init__(self, graphSettings, random):
self.graphSettings = graphSettings
self.areaRando = graphSettings.areaRando
self.bossRando = graphSettings.bossRando
self.escapeRando = graphSettings.escapeRando
self.minimizerN = graphSettings.minimizerN
self.log = log.get('GraphBuilder')
self.random = random

# builds everything but escape transitions
def createGraph(self, maxDiff):
Expand Down Expand Up @@ -48,18 +49,18 @@ def createGraph(self, maxDiff):
objForced = forcedAreas.intersection(escAreas)
escAreasList = sorted(list(escAreas))
while len(objForced) < n and len(escAreasList) > 0:
objForced.add(escAreasList.pop(random.randint(0, len(escAreasList)-1)))
objForced.add(escAreasList.pop(self.random.randint(0, len(escAreasList)-1)))
forcedAreas = forcedAreas.union(objForced)
transitions = GraphUtils.createMinimizerTransitions(self.graphSettings.startAP, self.minimizerN, sorted(list(forcedAreas)))
transitions = GraphUtils.createMinimizerTransitions(self.graphSettings.startAP, self.minimizerN, sorted(list(forcedAreas)), random=self.random)
else:
if not self.bossRando:
transitions += vanillaBossesTransitions
else:
transitions += GraphUtils.createBossesTransitions()
transitions += GraphUtils.createBossesTransitions(self.random)
if not self.areaRando:
transitions += vanillaTransitions
else:
transitions += GraphUtils.createAreaTransitions(self.graphSettings.lightAreaRando)
transitions += GraphUtils.createAreaTransitions(self.graphSettings.lightAreaRando, random=self.random)
ret = AccessGraph(Logic.accessPoints, transitions, self.graphSettings.dotFile)
Objectives.objDict[self.graphSettings.player].setGraph(ret, maxDiff)
return ret
Expand Down Expand Up @@ -100,7 +101,7 @@ def escapeGraph(self, container, graph, maxDiff, escapeTrigger):
self.escapeTimer(graph, paths, self.areaRando or escapeTrigger is not None)
self.log.debug("escapeGraph: ({}, {}) timer: {}".format(escapeSource, dst, graph.EscapeAttributes['Timer']))
# animals
GraphUtils.escapeAnimalsTransitions(graph, possibleTargets, dst)
GraphUtils.escapeAnimalsTransitions(graph, possibleTargets, dst, self.random)
return True

def _getTargets(self, sm, graph, maxDiff):
Expand All @@ -110,7 +111,7 @@ def _getTargets(self, sm, graph, maxDiff):
if len(possibleTargets) == 0:
self.log.debug("Can't randomize escape, fallback to vanilla")
possibleTargets.append('Climb Bottom Left')
random.shuffle(possibleTargets)
self.random.shuffle(possibleTargets)
return possibleTargets

def getPossibleEscapeTargets(self, emptyContainer, graph, maxDiff):
Expand Down
Loading
Loading