Archipelago/worlds/sm/variaRandomizer/utils/utils.py

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