500 lines
19 KiB
Python
500 lines
19 KiB
Python
import io
|
|
import os, json, re, random
|
|
import pathlib
|
|
import sys
|
|
from typing import Any
|
|
import zipfile
|
|
|
|
from ..utils.parameters import Knows, Settings, Controller, isKnows, isSettings, isButton
|
|
from ..utils.parameters import easy, medium, hard, harder, hardcore, mania, text2diff
|
|
from ..logic.smbool import SMBool
|
|
|
|
|
|
# support for AP world
|
|
isAPWorld = ".apworld" in sys.modules[__name__].__file__
|
|
|
|
def getZipFile():
|
|
filename = sys.modules[__name__].__file__
|
|
apworldExt = ".apworld"
|
|
zipPath = pathlib.Path(filename[:filename.index(apworldExt) + len(apworldExt)])
|
|
return (zipfile.ZipFile(zipPath), zipPath.stem)
|
|
|
|
def openFile(resource: str, mode: str = "r", encoding: None = None):
|
|
if isAPWorld:
|
|
(zipFile, stem) = getZipFile()
|
|
with zipFile as zf:
|
|
zipFilePath = resource[resource.index(stem + "/"):]
|
|
if mode == 'rb':
|
|
return zf.open(zipFilePath, 'r')
|
|
else:
|
|
return io.TextIOWrapper(zf.open(zipFilePath, mode), encoding)
|
|
else:
|
|
return open(resource, mode)
|
|
|
|
def listDir(resource: str):
|
|
if isAPWorld:
|
|
(zipFile, stem) = getZipFile()
|
|
with zipFile as zf:
|
|
zipFilePath = resource[resource.index(stem + "/"):]
|
|
path = zipfile.Path(zf, zipFilePath + "/")
|
|
files = [f.at[len(zipFilePath)+1:] for f in path.iterdir()]
|
|
return files
|
|
else:
|
|
return os.listdir(resource)
|
|
|
|
def exists(resource: str):
|
|
if isAPWorld:
|
|
(zipFile, stem) = getZipFile()
|
|
with zipFile as zf:
|
|
if (stem in resource):
|
|
zipFilePath = resource[resource.index(stem + "/"):]
|
|
path = zipfile.Path(zf, zipFilePath)
|
|
return path.exists()
|
|
else:
|
|
return False
|
|
else:
|
|
return os.path.exists(resource)
|
|
|
|
def isStdPreset(preset):
|
|
return preset in ['newbie', 'casual', 'regular', 'veteran', 'expert', 'master', 'samus', 'solution', 'Season_Races', 'SMRAT2021']
|
|
|
|
def getPresetDir(preset) -> str:
|
|
if isStdPreset(preset):
|
|
return 'worlds/sm/variaRandomizer/standard_presets'
|
|
else:
|
|
return 'worlds/sm/variaRandomizer/community_presets'
|
|
|
|
def removeChars(string, toRemove):
|
|
return re.sub('[{}]+'.format(toRemove), '', string)
|
|
|
|
def range_union(ranges):
|
|
ret = []
|
|
for rg in sorted([[r.start, r.stop] for r in ranges]):
|
|
begin, end = rg[0], rg[-1]
|
|
if ret and ret[-1][1] > begin:
|
|
ret[-1][1] = max(ret[-1][1], end)
|
|
else:
|
|
ret.append([begin, end])
|
|
return [range(r[0], r[1]) for r in ret]
|
|
|
|
# https://github.com/robotools/fontParts/commit/7cb561033929cfb4a723d274672e7257f5e68237
|
|
def normalizeRounding(n):
|
|
# Normalizes rounding as Python 2 and Python 3 handing the rounding of halves (0.5, 1.5, etc) differently.
|
|
# This normalizes rounding to be the same in both environments.
|
|
if round(0.5) != 1 and n % 1 == .5 and not int(n) % 2:
|
|
return int((round(n) + (abs(n) / n) * 1))
|
|
else:
|
|
return int(round(n))
|
|
|
|
# gauss random in [0, r] range
|
|
# the higher the slope, the less probable extreme values are.
|
|
def randGaussBounds(r, slope=5):
|
|
r = float(r)
|
|
n = normalizeRounding(random.gauss(r/2, r/slope))
|
|
if n < 0:
|
|
n = 0
|
|
if n > r:
|
|
n = int(r)
|
|
return n
|
|
|
|
# from a relative weight dictionary, gives a normalized range dictionary
|
|
# example :
|
|
# { 'a' : 10, 'b' : 17, 'c' : 3 } => {'c': 0.1, 'a':0.4333333, 'b':1 }
|
|
def getRangeDict(weightDict):
|
|
total = float(sum(weightDict.values()))
|
|
rangeDict = {}
|
|
current = 0.0
|
|
for k in sorted(weightDict, key=weightDict.get):
|
|
w = float(weightDict[k]) / total
|
|
current += w
|
|
rangeDict[k] = current
|
|
|
|
return rangeDict
|
|
|
|
def chooseFromRange(rangeDict):
|
|
r = random.random()
|
|
val = None
|
|
for v in sorted(rangeDict, key=rangeDict.get):
|
|
val = v
|
|
if r < rangeDict[v]:
|
|
return v
|
|
return val
|
|
|
|
class PresetLoader(object):
|
|
@staticmethod
|
|
def factory(params):
|
|
# can be a json, a python file or a dict with the parameters
|
|
if type(params) == str:
|
|
ext = os.path.splitext(params)
|
|
if ext[1].lower() == '.json':
|
|
return PresetLoaderJson(params)
|
|
else:
|
|
raise Exception("PresetLoader: wrong parameters file type: {}".format(ext[1]))
|
|
elif type(params) is dict:
|
|
return PresetLoaderDict(params)
|
|
else:
|
|
raise Exception("wrong parameters input, is neither a string nor a json file name: {}::{}".format(params, type(params)))
|
|
|
|
def __init__(self):
|
|
if 'Knows' not in self.params:
|
|
if 'knows' in self.params:
|
|
self.params['Knows'] = self.params['knows']
|
|
else:
|
|
self.params['Knows'] = {}
|
|
if 'Settings' not in self.params:
|
|
if 'settings' in self.params:
|
|
self.params['Settings'] = self.params['settings']
|
|
else:
|
|
self.params['Settings'] = {}
|
|
if 'Controller' not in self.params:
|
|
if 'controller' in self.params:
|
|
self.params['Controller'] = self.params['controller']
|
|
else:
|
|
self.params['Controller'] = {}
|
|
self.params['score'] = self.computeScore()
|
|
|
|
def load(self, player):
|
|
# update the parameters in the parameters classes: Knows, Settings
|
|
Knows.knowsDict[player] = Knows()
|
|
Settings.SettingsDict[player] = Settings()
|
|
Controller.ControllerDict[player] = Controller()
|
|
|
|
# Knows
|
|
for param in self.params['Knows']:
|
|
if isKnows(param) and hasattr(Knows, param):
|
|
setattr(Knows.knowsDict[player], param, SMBool( self.params['Knows'][param][0],
|
|
self.params['Knows'][param][1],
|
|
['{}'.format(param)]))
|
|
# Settings
|
|
## hard rooms
|
|
for hardRoom in ['X-Ray', 'Gauntlet']:
|
|
if hardRoom in self.params['Settings']:
|
|
Settings.SettingsDict[player].hardRooms[hardRoom] = Settings.hardRoomsPresets[hardRoom][self.params['Settings'][hardRoom]]
|
|
|
|
## bosses
|
|
for boss in ['Kraid', 'Phantoon', 'Draygon', 'Ridley', 'MotherBrain']:
|
|
if boss in self.params['Settings']:
|
|
Settings.SettingsDict[player].bossesDifficulty[boss] = Settings.bossesDifficultyPresets[boss][self.params['Settings'][boss]]
|
|
|
|
## hellruns
|
|
for hellRun in ['Ice', 'MainUpperNorfair', 'LowerNorfair']:
|
|
if hellRun in self.params['Settings']:
|
|
Settings.SettingsDict[player].hellRuns[hellRun] = Settings.hellRunPresets[hellRun][self.params['Settings'][hellRun]]
|
|
|
|
# Controller
|
|
for button in self.params['Controller']:
|
|
if isButton(button):
|
|
setattr(Controller.ControllerDict[player], button, self.params['Controller'][button])
|
|
|
|
def dump(self, fileName):
|
|
with open(fileName, 'w') as jsonFile:
|
|
json.dump(self.params, jsonFile)
|
|
|
|
def printToScreen(self):
|
|
print("self.params: {}".format(self.params))
|
|
|
|
print("loaded knows: ")
|
|
for knows in Knows.__dict__:
|
|
if isKnows(knows):
|
|
print("{}: {}".format(knows, Knows.__dict__[knows]))
|
|
print("loaded settings:")
|
|
for setting in Settings.__dict__:
|
|
if isSettings(setting):
|
|
print("{}: {}".format(setting, Settings.__dict__[setting]))
|
|
print("loaded controller:")
|
|
for button in Controller.__dict__:
|
|
if isButton(button):
|
|
print("{}: {}".format(button, Controller.__dict__[button]))
|
|
print("loaded score: {}".format(self.params['score']))
|
|
|
|
def computeScore(self):
|
|
# the more techniques you know and the smaller the difficulty of the techniques, the higher the score
|
|
diff2score = {
|
|
easy: 6,
|
|
medium: 5,
|
|
hard: 4,
|
|
harder: 3,
|
|
hardcore: 2,
|
|
mania: 1
|
|
}
|
|
|
|
boss2score = {
|
|
"He's annoying": 1,
|
|
'A lot of trouble': 1,
|
|
"I'm scared!": 1,
|
|
"It can get ugly": 1,
|
|
'Default': 2,
|
|
'Quick Kill': 3,
|
|
'Used to it': 3,
|
|
'Is this really the last boss?': 3,
|
|
'No problemo': 4,
|
|
'Piece of cake': 4,
|
|
'Nice cutscene bro': 4
|
|
}
|
|
|
|
hellrun2score = {
|
|
'No thanks': 0,
|
|
'Solution': 0,
|
|
'Gimme energy': 2,
|
|
'Default': 4,
|
|
'Bring the heat': 6,
|
|
'I run RBO': 8
|
|
}
|
|
|
|
hellrunLN2score = {
|
|
'Default': 0,
|
|
'Solution': 0,
|
|
'Bring the heat': 6,
|
|
'I run RBO': 12
|
|
}
|
|
|
|
xray2score = {
|
|
'Aarghh': 0,
|
|
'Solution': 0,
|
|
"I don't like spikes": 1,
|
|
'Default': 2,
|
|
"I don't mind spikes": 3,
|
|
'D-Boost master': 4
|
|
}
|
|
|
|
gauntlet2score = {
|
|
'Aarghh': 0,
|
|
"I don't like acid": 1,
|
|
'Default': 2
|
|
}
|
|
|
|
score = 0
|
|
|
|
# knows
|
|
for know in Knows.__dict__:
|
|
if isKnows(know):
|
|
if know in self.params['Knows']:
|
|
if self.params['Knows'][know][0] == True:
|
|
score += diff2score[self.params['Knows'][know][1]]
|
|
else:
|
|
# if old preset with not all the knows, use default values for the know
|
|
if Knows.__dict__[know].bool == True:
|
|
score += diff2score[Knows.__dict__[know].difficulty]
|
|
|
|
# hard rooms
|
|
hardRoom = 'X-Ray'
|
|
if hardRoom in self.params['Settings']:
|
|
score += xray2score[self.params['Settings'][hardRoom]]
|
|
|
|
hardRoom = 'Gauntlet'
|
|
if hardRoom in self.params['Settings']:
|
|
score += gauntlet2score[self.params['Settings'][hardRoom]]
|
|
|
|
# bosses
|
|
for boss in ['Kraid', 'Phantoon', 'Draygon', 'Ridley', 'MotherBrain']:
|
|
if boss in self.params['Settings']:
|
|
score += boss2score[self.params['Settings'][boss]]
|
|
|
|
# hellruns
|
|
for hellRun in ['Ice', 'MainUpperNorfair']:
|
|
if hellRun in self.params['Settings']:
|
|
score += hellrun2score[self.params['Settings'][hellRun]]
|
|
|
|
hellRun = 'LowerNorfair'
|
|
if hellRun in self.params['Settings']:
|
|
score += hellrunLN2score[self.params['Settings'][hellRun]]
|
|
|
|
return score
|
|
|
|
class PresetLoaderJson(PresetLoader):
|
|
# when called from the test suite
|
|
def __init__(self, jsonFileName):
|
|
with openFile(jsonFileName) as jsonFile:
|
|
self.params = json.load(jsonFile)
|
|
super(PresetLoaderJson, self).__init__()
|
|
|
|
class PresetLoaderDict(PresetLoader):
|
|
# when called from the website
|
|
def __init__(self, params):
|
|
self.params = params
|
|
super(PresetLoaderDict, self).__init__()
|
|
|
|
def getDefaultMultiValues():
|
|
from ..graph.graph_utils import GraphUtils
|
|
defaultMultiValues = {
|
|
'startLocation': GraphUtils.getStartAccessPointNames(),
|
|
'majorsSplit': ['Full', 'FullWithHUD', 'Major', 'Chozo', 'Scavenger'],
|
|
'progressionSpeed': ['slowest', 'slow', 'medium', 'fast', 'fastest', 'basic', 'VARIAble', 'speedrun'],
|
|
'progressionDifficulty': ['easier', 'normal', 'harder'],
|
|
'morphPlacement': ['early', 'normal'], #['early', 'late', 'normal'],
|
|
'energyQty': ['ultra sparse', 'sparse', 'medium', 'vanilla'],
|
|
'gravityBehaviour': ['Vanilla', 'Balanced', 'Progressive']
|
|
}
|
|
return defaultMultiValues
|
|
|
|
def getPresetValues():
|
|
return [
|
|
"newbie",
|
|
"casual",
|
|
"regular",
|
|
"veteran",
|
|
"expert",
|
|
"master",
|
|
"samus",
|
|
"Season_Races",
|
|
"SMRAT2021",
|
|
"solution",
|
|
"custom",
|
|
"varia_custom"
|
|
]
|
|
|
|
# from web to cli
|
|
def convertParam(randoParams, param, inverse=False):
|
|
value = randoParams.get(param, "off" if inverse == False else "on")
|
|
if value == "on":
|
|
return True if inverse == False else False
|
|
elif value == "off":
|
|
return False if inverse == False else True
|
|
elif value == "random":
|
|
return "random"
|
|
raise Exception("invalid value for parameter {}".format(param))
|
|
|
|
def loadRandoPreset(world, player, args):
|
|
defaultMultiValues = getDefaultMultiValues()
|
|
diffs = ["easy", "medium", "hard", "harder", "hardcore", "mania", "infinity"]
|
|
presetValues = getPresetValues()
|
|
|
|
args.animals = world.animals[player].value
|
|
args.noVariaTweaks = not world.varia_tweaks[player].value
|
|
args.maxDifficulty = diffs[world.max_difficulty[player].value]
|
|
#args.suitsRestriction = world.suits_restriction[player].value
|
|
#args.hideItems = world.hide_items[player].value
|
|
args.strictMinors = world.strict_minors[player].value
|
|
args.noLayout = not world.layout_patches[player].value
|
|
args.gravityBehaviour = defaultMultiValues["gravityBehaviour"][world.gravity_behaviour[player].value]
|
|
args.nerfedCharge = world.nerfed_charge[player].value
|
|
args.area = world.area_randomization[player].value != 0
|
|
if args.area:
|
|
args.areaLayoutBase = not world.area_layout[player].value
|
|
args.lightArea = world.area_randomization[player].value == 1
|
|
#args.escapeRando
|
|
#args.noRemoveEscapeEnemies
|
|
args.doorsColorsRando = world.doors_colors_rando[player].value
|
|
args.allowGreyDoors = world.allow_grey_doors[player].value
|
|
args.bosses = world.boss_randomization[player].value
|
|
if world.fun_combat[player].value:
|
|
args.superFun.append("Combat")
|
|
if world.fun_movement[player].value:
|
|
args.superFun.append("Movement")
|
|
if world.fun_suits[player].value:
|
|
args.superFun.append("Suits")
|
|
|
|
ipsPatches = {"spin_jump_restart":"spinjumprestart", "rando_speed":"rando_speed", "elevators_doors_speed":"elevators_doors_speed", "refill_before_save":"refill_before_save"}
|
|
for settingName, patchName in ipsPatches.items():
|
|
if hasattr(world, settingName) and getattr(world, settingName)[player].value:
|
|
args.patches.append(patchName + '.ips')
|
|
|
|
patches = {"no_music":"No_Music", "infinite_space_jump":"Infinite_Space_Jump"}
|
|
for settingName, patchName in patches.items():
|
|
if hasattr(world, settingName) and getattr(world, settingName)[player].value:
|
|
args.patches.append(patchName)
|
|
|
|
args.hud = world.hud[player].value
|
|
args.morphPlacement = defaultMultiValues["morphPlacement"][world.morph_placement[player].value]
|
|
#args.majorsSplit
|
|
#args.scavNumLocs
|
|
#args.scavRandomized
|
|
#args.scavEscape
|
|
args.startLocation = defaultMultiValues["startLocation"][world.start_location[player].value]
|
|
#args.progressionDifficulty
|
|
#args.progressionSpeed
|
|
args.missileQty = world.missile_qty[player].value / float(10)
|
|
args.superQty = world.super_qty[player].value / float(10)
|
|
args.powerBombQty = world.power_bomb_qty[player].value / float(10)
|
|
args.minorQty = world.minor_qty[player].value
|
|
args.energyQty = defaultMultiValues["energyQty"][world.energy_qty[player].value]
|
|
#args.minimizerN
|
|
#args.minimizerTourian
|
|
|
|
return presetValues[world.preset[player].value]
|
|
|
|
def getRandomizerDefaultParameters():
|
|
defaultParams = {}
|
|
defaultMultiValues = getDefaultMultiValues()
|
|
|
|
defaultParams['complexity'] = "simple"
|
|
defaultParams['preset'] = 'regular'
|
|
defaultParams['randoPreset'] = ""
|
|
defaultParams['raceMode'] = "off"
|
|
defaultParams['majorsSplit'] = "Full"
|
|
defaultParams['majorsSplitMultiSelect'] = defaultMultiValues['majorsSplit']
|
|
defaultParams['scavNumLocs'] = "10"
|
|
defaultParams['scavRandomized'] = "off"
|
|
defaultParams['scavEscape'] = "off"
|
|
defaultParams['startLocation'] = "Landing Site"
|
|
defaultParams['startLocationMultiSelect'] = defaultMultiValues['startLocation']
|
|
defaultParams['maxDifficulty'] = 'hardcore'
|
|
defaultParams['progressionSpeed'] = "medium"
|
|
defaultParams['progressionSpeedMultiSelect'] = defaultMultiValues['progressionSpeed']
|
|
defaultParams['progressionDifficulty'] = 'normal'
|
|
defaultParams['progressionDifficultyMultiSelect'] = defaultMultiValues['progressionDifficulty']
|
|
defaultParams['morphPlacement'] = "early"
|
|
defaultParams['morphPlacementMultiSelect'] = defaultMultiValues['morphPlacement']
|
|
defaultParams['suitsRestriction'] = "on"
|
|
defaultParams['hideItems'] = "off"
|
|
defaultParams['strictMinors'] = "off"
|
|
defaultParams['missileQty'] = "3"
|
|
defaultParams['superQty'] = "2"
|
|
defaultParams['powerBombQty'] = "1"
|
|
defaultParams['minorQty'] = "100"
|
|
defaultParams['energyQty'] = "vanilla"
|
|
defaultParams['energyQtyMultiSelect'] = defaultMultiValues['energyQty']
|
|
defaultParams['areaRandomization'] = "off"
|
|
defaultParams['areaLayout'] = "off"
|
|
defaultParams['lightAreaRandomization'] = "off"
|
|
defaultParams['doorsColorsRando'] = "off"
|
|
defaultParams['allowGreyDoors'] = "off"
|
|
defaultParams['escapeRando'] = "off"
|
|
defaultParams['removeEscapeEnemies'] = "off"
|
|
defaultParams['bossRandomization'] = "off"
|
|
defaultParams['minimizer'] = "off"
|
|
defaultParams['minimizerQty'] = "45"
|
|
defaultParams['minimizerTourian'] = "off"
|
|
defaultParams['funCombat'] = "off"
|
|
defaultParams['funMovement'] = "off"
|
|
defaultParams['funSuits'] = "off"
|
|
defaultParams['layoutPatches'] = "on"
|
|
defaultParams['variaTweaks'] = "on"
|
|
defaultParams['gravityBehaviour'] = "Balanced"
|
|
defaultParams['gravityBehaviourMultiSelect'] = defaultMultiValues['gravityBehaviour']
|
|
defaultParams['nerfedCharge'] = "off"
|
|
defaultParams['itemsounds'] = "on"
|
|
defaultParams['elevators_doors_speed'] = "on"
|
|
defaultParams['spinjumprestart'] = "off"
|
|
defaultParams['rando_speed'] = "off"
|
|
defaultParams['Infinite_Space_Jump'] = "off"
|
|
defaultParams['refill_before_save'] = "off"
|
|
defaultParams['hud'] = "off"
|
|
defaultParams['animals'] = "off"
|
|
defaultParams['No_Music'] = "off"
|
|
defaultParams['random_music'] = "off"
|
|
|
|
return defaultParams
|
|
|
|
def fixEnergy(items):
|
|
# display number of energy used
|
|
energies = [i for i in items if i.find('ETank') != -1]
|
|
if len(energies) > 0:
|
|
(maxETank, maxReserve, maxEnergy) = (0, 0, 0)
|
|
for energy in energies:
|
|
nETank = int(energy[0:energy.find('-ETank')])
|
|
if energy.find('-Reserve') != -1:
|
|
nReserve = int(energy[energy.find(' - ')+len(' - '):energy.find('-Reserve')])
|
|
else:
|
|
nReserve = 0
|
|
nEnergy = nETank + nReserve
|
|
if nEnergy > maxEnergy:
|
|
maxEnergy = nEnergy
|
|
maxETank = nETank
|
|
maxReserve = nReserve
|
|
items.remove(energy)
|
|
items.append('{}-ETank'.format(maxETank))
|
|
if maxReserve > 0:
|
|
items.append('{}-Reserve'.format(maxReserve))
|
|
return items
|