Added Super Metroid support (#46)
Varia Randomizer based implementation LttPClient -> SNIClient
This commit is contained in:
0
worlds/sm/variaRandomizer/logic/__init__.py
Normal file
0
worlds/sm/variaRandomizer/logic/__init__.py
Normal file
63
worlds/sm/variaRandomizer/logic/cache.py
Normal file
63
worlds/sm/variaRandomizer/logic/cache.py
Normal file
@@ -0,0 +1,63 @@
|
||||
# the caching decorator for helpers functions
|
||||
class VersionedCache(object):
|
||||
__slots__ = ( 'cache', 'masterCache', 'nextSlot', 'size')
|
||||
|
||||
def __init__(self):
|
||||
self.cache = []
|
||||
self.masterCache = {}
|
||||
self.nextSlot = 0
|
||||
self.size = 0
|
||||
|
||||
def reset(self):
|
||||
# reinit the whole cache
|
||||
self.masterCache = {}
|
||||
self.update(0)
|
||||
|
||||
def update(self, newKey):
|
||||
cache = self.masterCache.get(newKey, None)
|
||||
if cache is None:
|
||||
cache = [ None ] * self.size
|
||||
self.masterCache[newKey] = cache
|
||||
self.cache = cache
|
||||
|
||||
def decorator(self, func):
|
||||
return self._decorate(func.__name__, self._new_slot(), func)
|
||||
|
||||
# for lambdas
|
||||
def ldeco(self, func):
|
||||
return self._decorate(func.__code__, self._new_slot(), func)
|
||||
|
||||
def _new_slot(self):
|
||||
slot = self.nextSlot
|
||||
self.nextSlot += 1
|
||||
self.size += 1
|
||||
return slot
|
||||
|
||||
def _decorate(self, name, slot, func):
|
||||
def _decorator(arg):
|
||||
#ret = self.cache[slot]
|
||||
#if ret is not None:
|
||||
# return ret
|
||||
#else:
|
||||
ret = func(arg)
|
||||
# self.cache[slot] = ret
|
||||
return ret
|
||||
return _decorator
|
||||
|
||||
Cache = VersionedCache()
|
||||
|
||||
class RequestCache(object):
|
||||
def __init__(self):
|
||||
self.results = {}
|
||||
|
||||
def request(self, request, *args):
|
||||
return ''.join([request] + [str(arg) for arg in args])
|
||||
|
||||
def store(self, request, result):
|
||||
self.results[request] = result
|
||||
|
||||
def get(self, request):
|
||||
return self.results[request] if request in self.results else None
|
||||
|
||||
def reset(self):
|
||||
self.results.clear()
|
||||
831
worlds/sm/variaRandomizer/logic/helpers.py
Normal file
831
worlds/sm/variaRandomizer/logic/helpers.py
Normal file
@@ -0,0 +1,831 @@
|
||||
|
||||
import math
|
||||
|
||||
from logic.cache import Cache
|
||||
from logic.smbool import SMBool, smboolFalse
|
||||
from utils.parameters import Settings, easy, medium, diff2text
|
||||
from rom.rom_patches import RomPatches
|
||||
from utils.utils import normalizeRounding
|
||||
|
||||
|
||||
class Helpers(object):
|
||||
def __init__(self, smbm):
|
||||
self.smbm = smbm
|
||||
|
||||
# return bool
|
||||
def haveItemCount(self, item, count):
|
||||
return self.smbm.itemCount(item) >= count
|
||||
|
||||
# return integer
|
||||
@Cache.decorator
|
||||
def energyReserveCount(self):
|
||||
return self.smbm.itemCount('ETank') + self.smbm.itemCount('Reserve')
|
||||
|
||||
def energyReserveCountOkDiff(self, difficulties, mult=1.0):
|
||||
if difficulties is None or len(difficulties) == 0:
|
||||
return smboolFalse
|
||||
|
||||
def f(difficulty):
|
||||
return self.smbm.energyReserveCountOk(normalizeRounding(difficulty[0] / mult), difficulty=difficulty[1])
|
||||
|
||||
result = f(difficulties[0])
|
||||
for difficulty in difficulties[1:]:
|
||||
result = self.smbm.wor(result, f(difficulty))
|
||||
return result
|
||||
|
||||
def energyReserveCountOkHellRun(self, hellRunName, mult=1.0):
|
||||
difficulties = Settings.hellRuns[hellRunName]
|
||||
result = self.energyReserveCountOkDiff(difficulties, mult)
|
||||
|
||||
if result == True:
|
||||
result.knows = [hellRunName+'HellRun']
|
||||
|
||||
return result
|
||||
|
||||
# gives damage reduction factor with the current suits
|
||||
# envDmg : if true (default) will return environmental damage reduction
|
||||
def getDmgReduction(self, envDmg=True):
|
||||
ret = 1.0
|
||||
sm = self.smbm
|
||||
hasVaria = sm.haveItem('Varia')
|
||||
hasGrav = sm.haveItem('Gravity')
|
||||
items = []
|
||||
if RomPatches.has(sm.player, RomPatches.NoGravityEnvProtection):
|
||||
if hasVaria:
|
||||
items = ['Varia']
|
||||
if envDmg:
|
||||
ret = 4.0
|
||||
else:
|
||||
ret = 2.0
|
||||
if hasGrav and not envDmg:
|
||||
ret = 4.0
|
||||
items = ['Gravity']
|
||||
elif RomPatches.has(sm.player, RomPatches.ProgressiveSuits):
|
||||
if hasVaria:
|
||||
items.append('Varia')
|
||||
ret *= 2
|
||||
if hasGrav:
|
||||
items.append('Gravity')
|
||||
ret *= 2
|
||||
else:
|
||||
if hasVaria:
|
||||
ret = 2.0
|
||||
items = ['Varia']
|
||||
if hasGrav:
|
||||
ret = 4.0
|
||||
items = ['Gravity']
|
||||
return (ret, items)
|
||||
|
||||
# higher values for mult means room is that much "easier" (HP mult)
|
||||
def energyReserveCountOkHardRoom(self, roomName, mult=1.0):
|
||||
difficulties = Settings.hardRooms[roomName]
|
||||
(dmgRed, items) = self.getDmgReduction()
|
||||
mult *= dmgRed
|
||||
result = self.energyReserveCountOkDiff(difficulties, mult)
|
||||
|
||||
if result == True:
|
||||
result.knows = ['HardRoom-'+roomName]
|
||||
if dmgRed != 1.0:
|
||||
result._items.append(items)
|
||||
return result
|
||||
|
||||
@Cache.decorator
|
||||
def heatProof(self):
|
||||
sm = self.smbm
|
||||
return sm.wor(sm.haveItem('Varia'),
|
||||
sm.wand(sm.wnot(RomPatches.has(sm.player, RomPatches.NoGravityEnvProtection)),
|
||||
sm.wnot(RomPatches.has(sm.player, RomPatches.ProgressiveSuits)),
|
||||
sm.haveItem('Gravity')))
|
||||
|
||||
# helper here because we can't define "sublambdas" in locations
|
||||
def getPiratesPseudoScrewCoeff(self):
|
||||
sm = self.smbm
|
||||
ret = 1.0
|
||||
if RomPatches.has(sm.player, RomPatches.NerfedCharge).bool == True:
|
||||
ret = 4.0
|
||||
return ret
|
||||
|
||||
@Cache.decorator
|
||||
def canFireChargedShots(self):
|
||||
sm = self.smbm
|
||||
return sm.wor(sm.haveItem('Charge'), RomPatches.has(sm.player, RomPatches.NerfedCharge))
|
||||
|
||||
# higher values for mult means hell run is that much "easier" (HP mult)
|
||||
def canHellRun(self, hellRun, mult=1.0, minE=2):
|
||||
sm = self.smbm
|
||||
|
||||
items = []
|
||||
isHeatProof = sm.heatProof()
|
||||
if isHeatProof == True:
|
||||
return isHeatProof
|
||||
if sm.wand(RomPatches.has(sm.player, RomPatches.ProgressiveSuits), sm.haveItem('Gravity')).bool == True:
|
||||
# half heat protection
|
||||
mult *= 2.0
|
||||
minE /= 2.0
|
||||
items.append('Gravity')
|
||||
if self.energyReserveCount() >= minE:
|
||||
if hellRun != 'LowerNorfair':
|
||||
ret = self.energyReserveCountOkHellRun(hellRun, mult)
|
||||
if ret.bool == True:
|
||||
ret._items.append(items)
|
||||
return ret
|
||||
else:
|
||||
tanks = self.energyReserveCount()
|
||||
multCF = mult
|
||||
if tanks >= 14:
|
||||
multCF *= 2.0
|
||||
nCF = int(math.ceil(2/multCF))
|
||||
ret = sm.wand(self.energyReserveCountOkHellRun(hellRun, mult),
|
||||
self.canCrystalFlash(nCF))
|
||||
if ret.bool == True:
|
||||
if sm.haveItem('Gravity') == True:
|
||||
ret.difficulty *= 0.7
|
||||
ret._items.append('Gravity')
|
||||
elif sm.haveItem('ScrewAttack') == True:
|
||||
ret.difficulty *= 0.7
|
||||
ret._items.append('ScrewAttack')
|
||||
#nPB = self.smbm.itemCount('PowerBomb')
|
||||
#print("canHellRun LN. tanks=" + str(tanks) + ", nCF=" + str(nCF) + ", nPB=" + str(nPB) + ", mult=" + str(mult) + ", heatProof=" + str(isHeatProof.bool) + ", ret=" + str(ret))
|
||||
return ret
|
||||
else:
|
||||
return smboolFalse
|
||||
|
||||
@Cache.decorator
|
||||
def canMockball(self):
|
||||
sm = self.smbm
|
||||
return sm.wand(sm.haveItem('Morph'),
|
||||
sm.knowsMockball())
|
||||
|
||||
@Cache.decorator
|
||||
def canFly(self):
|
||||
sm = self.smbm
|
||||
return sm.wor(sm.haveItem('SpaceJump'),
|
||||
sm.canInfiniteBombJump())
|
||||
|
||||
@Cache.decorator
|
||||
def canSimpleShortCharge(self):
|
||||
sm = self.smbm
|
||||
return sm.wand(sm.haveItem('SpeedBooster'),
|
||||
sm.wor(sm.knowsSimpleShortCharge(),
|
||||
sm.knowsShortCharge()))
|
||||
|
||||
@Cache.decorator
|
||||
def canShortCharge(self):
|
||||
sm = self.smbm
|
||||
return sm.wand(sm.haveItem('SpeedBooster'), sm.knowsShortCharge())
|
||||
|
||||
@Cache.decorator
|
||||
def canUseBombs(self):
|
||||
sm = self.smbm
|
||||
return sm.wand(sm.haveItem('Morph'), sm.haveItem('Bomb'))
|
||||
|
||||
@Cache.decorator
|
||||
def canInfiniteBombJump(self):
|
||||
sm = self.smbm
|
||||
return sm.wand(sm.haveItem('Morph'), sm.haveItem('Bomb'), sm.knowsInfiniteBombJump())
|
||||
|
||||
@Cache.decorator
|
||||
def canInfiniteBombJumpSuitless(self):
|
||||
sm = self.smbm
|
||||
return sm.wand(sm.haveItem('Morph'), sm.haveItem('Bomb'), sm.knowsInfiniteBombJumpSuitless())
|
||||
|
||||
@Cache.decorator
|
||||
def haveMissileOrSuper(self):
|
||||
sm = self.smbm
|
||||
return sm.wor(sm.haveItem('Missile'), sm.haveItem('Super'))
|
||||
|
||||
@Cache.decorator
|
||||
def canOpenRedDoors(self):
|
||||
sm = self.smbm
|
||||
return sm.wor(sm.wand(sm.wnot(RomPatches.has(sm.player, RomPatches.RedDoorsMissileOnly)),
|
||||
sm.haveMissileOrSuper()),
|
||||
sm.haveItem('Missile'))
|
||||
|
||||
@Cache.decorator
|
||||
def canOpenEyeDoors(self):
|
||||
sm = self.smbm
|
||||
return sm.wor(RomPatches.has(sm.player, RomPatches.NoGadoras),
|
||||
sm.haveMissileOrSuper())
|
||||
|
||||
@Cache.decorator
|
||||
def canOpenGreenDoors(self):
|
||||
return self.smbm.haveItem('Super')
|
||||
|
||||
@Cache.decorator
|
||||
def canGreenGateGlitch(self):
|
||||
sm = self.smbm
|
||||
return sm.wand(sm.haveItem('Super'),
|
||||
sm.knowsGreenGateGlitch())
|
||||
|
||||
@Cache.decorator
|
||||
def canBlueGateGlitch(self):
|
||||
sm = self.smbm
|
||||
return sm.wand(sm.haveMissileOrSuper(),
|
||||
sm.knowsGreenGateGlitch())
|
||||
@Cache.decorator
|
||||
def canUsePowerBombs(self):
|
||||
sm = self.smbm
|
||||
return sm.wand(sm.haveItem('Morph'), sm.haveItem('PowerBomb'))
|
||||
|
||||
canOpenYellowDoors = canUsePowerBombs
|
||||
|
||||
@Cache.decorator
|
||||
def canUseSpringBall(self):
|
||||
sm = self.smbm
|
||||
return sm.wand(sm.haveItem('Morph'),
|
||||
sm.haveItem('SpringBall'))
|
||||
|
||||
@Cache.decorator
|
||||
def canSpringBallJump(self):
|
||||
sm = self.smbm
|
||||
return sm.wand(sm.canUseSpringBall(),
|
||||
sm.knowsSpringBallJump())
|
||||
|
||||
@Cache.decorator
|
||||
def canDoubleSpringBallJump(self):
|
||||
sm = self.smbm
|
||||
return sm.wand(sm.canUseSpringBall(),
|
||||
sm.haveItem('HiJump'),
|
||||
sm.knowsDoubleSpringBallJump())
|
||||
|
||||
@Cache.decorator
|
||||
def canSpringBallJumpFromWall(self):
|
||||
sm = self.smbm
|
||||
return sm.wand(sm.canUseSpringBall(),
|
||||
sm.knowsSpringBallJumpFromWall())
|
||||
|
||||
@Cache.decorator
|
||||
def canDestroyBombWalls(self):
|
||||
sm = self.smbm
|
||||
return sm.wor(sm.wand(sm.haveItem('Morph'),
|
||||
sm.wor(sm.haveItem('Bomb'),
|
||||
sm.haveItem('PowerBomb'))),
|
||||
sm.haveItem('ScrewAttack'))
|
||||
|
||||
@Cache.decorator
|
||||
def canDestroyBombWallsUnderwater(self):
|
||||
sm = self.smbm
|
||||
return sm.wor(sm.wand(sm.haveItem('Gravity'),
|
||||
sm.canDestroyBombWalls()),
|
||||
sm.wand(sm.haveItem('Morph'),
|
||||
sm.wor(sm.haveItem('Bomb'),
|
||||
sm.haveItem('PowerBomb'))))
|
||||
|
||||
@Cache.decorator
|
||||
def canPassBombPassages(self):
|
||||
sm = self.smbm
|
||||
return sm.wor(sm.canUseBombs(),
|
||||
sm.canUsePowerBombs())
|
||||
|
||||
@Cache.decorator
|
||||
def canMorphJump(self):
|
||||
# small hop in morph ball form
|
||||
sm = self.smbm
|
||||
return sm.wor(sm.canPassBombPassages(), sm.haveItem('SpringBall'))
|
||||
|
||||
def canCrystalFlash(self, n=1):
|
||||
sm = self.smbm
|
||||
return sm.wand(sm.canUsePowerBombs(),
|
||||
sm.itemCountOk('Missile', 2*n),
|
||||
sm.itemCountOk('Super', 2*n),
|
||||
sm.itemCountOk('PowerBomb', 2*n+1))
|
||||
|
||||
@Cache.decorator
|
||||
def canCrystalFlashClip(self):
|
||||
sm = self.smbm
|
||||
return sm.wand(sm.canCrystalFlash(),
|
||||
sm.wor(sm.wand(sm.haveItem('Gravity'),
|
||||
sm.canUseBombs(),
|
||||
sm.knowsCrystalFlashClip()),
|
||||
sm.wand(sm.knowsSuitlessCrystalFlashClip(),
|
||||
sm.itemCountOk('PowerBomb', 4))))
|
||||
|
||||
@Cache.decorator
|
||||
def canDoLowGauntlet(self):
|
||||
sm = self.smbm
|
||||
return sm.wand(sm.canShortCharge(),
|
||||
sm.canUsePowerBombs(),
|
||||
sm.itemCountOk('ETank', 1),
|
||||
sm.knowsLowGauntlet())
|
||||
|
||||
@Cache.decorator
|
||||
def canUseHyperBeam(self):
|
||||
sm = self.smbm
|
||||
return sm.haveItem('Hyper')
|
||||
|
||||
@Cache.decorator
|
||||
def getBeamDamage(self):
|
||||
sm = self.smbm
|
||||
standardDamage = 20
|
||||
|
||||
if sm.wand(sm.haveItem('Ice'),
|
||||
sm.haveItem('Wave'),
|
||||
sm.haveItem('Plasma')) == True:
|
||||
standardDamage = 300
|
||||
elif sm.wand(sm.haveItem('Wave'),
|
||||
sm.haveItem('Plasma')) == True:
|
||||
standardDamage = 250
|
||||
elif sm.wand(sm.haveItem('Ice'),
|
||||
sm.haveItem('Plasma')) == True:
|
||||
standardDamage = 200
|
||||
elif sm.haveItem('Plasma') == True:
|
||||
standardDamage = 150
|
||||
elif sm.wand(sm.haveItem('Ice'),
|
||||
sm.haveItem('Wave'),
|
||||
sm.haveItem('Spazer')) == True:
|
||||
standardDamage = 100
|
||||
elif sm.wand(sm.haveItem('Wave'),
|
||||
sm.haveItem('Spazer')) == True:
|
||||
standardDamage = 70
|
||||
elif sm.wand(sm.haveItem('Ice'),
|
||||
sm.haveItem('Spazer')) == True:
|
||||
standardDamage = 60
|
||||
elif sm.wand(sm.haveItem('Ice'),
|
||||
sm.haveItem('Wave')) == True:
|
||||
standardDamage = 60
|
||||
elif sm.haveItem('Wave') == True:
|
||||
standardDamage = 50
|
||||
elif sm.haveItem('Spazer') == True:
|
||||
standardDamage = 40
|
||||
elif sm.haveItem('Ice') == True:
|
||||
standardDamage = 30
|
||||
|
||||
return standardDamage
|
||||
|
||||
# returns a tuple with :
|
||||
#
|
||||
# - a floating point number : 0 if boss is unbeatable with
|
||||
# current equipment, and an ammo "margin" (ex : 1.5 means we have 50%
|
||||
# more firepower than absolutely necessary). Useful to compute boss
|
||||
# difficulty when not having charge. If player has charge, the actual
|
||||
# value is not useful, and is guaranteed to be > 2.
|
||||
#
|
||||
# - estimation of the fight duration in seconds (well not really, it
|
||||
# is if you fire and land shots perfectly and constantly), giving info
|
||||
# to compute boss fight difficulty
|
||||
def canInflictEnoughDamages(self, bossEnergy, doubleSuper=False, charge=True, power=False, givesDrops=True, ignoreMissiles=False, ignoreSupers=False):
|
||||
# TODO: handle special beam attacks ? (http://deanyd.net/sm/index.php?title=Charge_Beam_Combos)
|
||||
sm = self.smbm
|
||||
items = []
|
||||
|
||||
# http://deanyd.net/sm/index.php?title=Damage
|
||||
standardDamage = 0
|
||||
if sm.canFireChargedShots().bool == True and charge == True:
|
||||
standardDamage = self.getBeamDamage()
|
||||
items.append('Charge')
|
||||
# charge triples the damage
|
||||
chargeDamage = standardDamage
|
||||
if sm.haveItem('Charge').bool == True:
|
||||
chargeDamage *= 3.0
|
||||
|
||||
# missile 100 damages, super missile 300 damages, PBs 200 dmg, 5 in each extension
|
||||
missilesAmount = sm.itemCount('Missile') * 5
|
||||
if ignoreMissiles == True:
|
||||
missilesDamage = 0
|
||||
else:
|
||||
missilesDamage = missilesAmount * 100
|
||||
if missilesAmount > 0:
|
||||
items.append('Missile')
|
||||
supersAmount = sm.itemCount('Super') * 5
|
||||
if ignoreSupers == True:
|
||||
oneSuper = 0
|
||||
else:
|
||||
oneSuper = 300.0
|
||||
if supersAmount > 0:
|
||||
items.append('Super')
|
||||
if doubleSuper == True:
|
||||
oneSuper *= 2
|
||||
supersDamage = supersAmount * oneSuper
|
||||
powerDamage = 0
|
||||
powerAmount = 0
|
||||
if power == True and sm.haveItem('PowerBomb') == True:
|
||||
powerAmount = sm.itemCount('PowerBomb') * 5
|
||||
powerDamage = powerAmount * 200
|
||||
items.append('PowerBomb')
|
||||
|
||||
canBeatBoss = chargeDamage > 0 or givesDrops or (missilesDamage + supersDamage + powerDamage) >= bossEnergy
|
||||
if not canBeatBoss:
|
||||
return (0, 0, [])
|
||||
|
||||
ammoMargin = (missilesDamage + supersDamage + powerDamage) / bossEnergy
|
||||
if chargeDamage > 0:
|
||||
ammoMargin += 2
|
||||
|
||||
missilesDPS = Settings.algoSettings['missilesPerSecond'] * 100.0
|
||||
supersDPS = Settings.algoSettings['supersPerSecond'] * 300.0
|
||||
if doubleSuper == True:
|
||||
supersDPS *= 2
|
||||
if powerDamage > 0:
|
||||
powerDPS = Settings.algoSettings['powerBombsPerSecond'] * 200.0
|
||||
else:
|
||||
powerDPS = 0.0
|
||||
chargeDPS = chargeDamage * Settings.algoSettings['chargedShotsPerSecond']
|
||||
# print("chargeDPS=" + str(chargeDPS))
|
||||
dpsDict = {
|
||||
missilesDPS: (missilesAmount, 100.0),
|
||||
supersDPS: (supersAmount, oneSuper),
|
||||
powerDPS: (powerAmount, 200.0),
|
||||
# no boss will take more 10000 charged shots
|
||||
chargeDPS: (10000, chargeDamage)
|
||||
}
|
||||
secs = 0
|
||||
for dps in sorted(dpsDict, reverse=True):
|
||||
amount = dpsDict[dps][0]
|
||||
one = dpsDict[dps][1]
|
||||
if dps == 0 or one == 0 or amount == 0:
|
||||
continue
|
||||
fire = min(bossEnergy / one, amount)
|
||||
secs += fire * (one / dps)
|
||||
bossEnergy -= fire * one
|
||||
if bossEnergy <= 0:
|
||||
break
|
||||
if bossEnergy > 0:
|
||||
# print ('!! drops !! ')
|
||||
secs += bossEnergy * Settings.algoSettings['missileDropsPerMinute'] * 100 / 60
|
||||
# print('ammoMargin = ' + str(ammoMargin) + ', secs = ' + str(secs))
|
||||
|
||||
return (ammoMargin, secs, items)
|
||||
|
||||
# return diff score, or -1 if below minimum energy in diffTbl
|
||||
def computeBossDifficulty(self, ammoMargin, secs, diffTbl, energyDiff=0):
|
||||
sm = self.smbm
|
||||
|
||||
# actual fight duration :
|
||||
rate = None
|
||||
if 'Rate' in diffTbl:
|
||||
rate = float(diffTbl['Rate'])
|
||||
if rate is None:
|
||||
duration = 120.0
|
||||
else:
|
||||
duration = secs / rate
|
||||
# print('rate=' + str(rate) + ', duration=' + str(duration))
|
||||
(suitsCoeff, items) = sm.getDmgReduction(envDmg=False)
|
||||
suitsCoeff /= 2.0
|
||||
energyCount = self.energyReserveCount()
|
||||
energy = suitsCoeff * (1 + energyCount + energyDiff)
|
||||
# print("energy="+str(energy)+", energyCount="+str(energyCount)+",energyDiff="+str(energyDiff)+",suitsCoeff="+str(suitsCoeff))
|
||||
|
||||
# add all energy in used items
|
||||
items += sm.energyReserveCountOk(energyCount).items
|
||||
|
||||
energyDict = None
|
||||
if 'Energy' in diffTbl:
|
||||
energyDict = diffTbl['Energy']
|
||||
difficulty = medium
|
||||
# get difficulty by energy
|
||||
if energyDict:
|
||||
energyDict = {float(k):float(v) for k,v in energyDict.items()}
|
||||
keyz = sorted(energyDict.keys())
|
||||
if len(keyz) > 0:
|
||||
current = keyz[0]
|
||||
if energy < current:
|
||||
return (-1, [])
|
||||
sup = None
|
||||
difficulty = energyDict[current]
|
||||
for k in keyz:
|
||||
if k > energy:
|
||||
sup=k
|
||||
break
|
||||
current = k
|
||||
difficulty = energyDict[k]
|
||||
# interpolate if we can
|
||||
if energy > current and sup is not None:
|
||||
difficulty += (energyDict[sup] - difficulty)/(sup - current) * (energy - current)
|
||||
# print("energy=" + str(energy) + ", base diff=" + str(difficulty))
|
||||
# adjust by fight duration
|
||||
difficulty *= (duration / 120)
|
||||
# and by ammo margin
|
||||
# only augment difficulty in case of no charge, don't lower it.
|
||||
# if we have charge, ammoMargin will have a huge value (see canInflictEnoughDamages),
|
||||
# so this does not apply
|
||||
diffAdjust = (1 - (ammoMargin - Settings.algoSettings['ammoMarginIfNoCharge']))
|
||||
if diffAdjust > 1:
|
||||
difficulty *= diffAdjust
|
||||
# print("final diff: "+str(round(difficulty, 2)))
|
||||
|
||||
return (round(difficulty, 2), items)
|
||||
|
||||
@Cache.decorator
|
||||
def enoughStuffSporeSpawn(self):
|
||||
sm = self.smbm
|
||||
return sm.wor(sm.haveItem('Missile'), sm.haveItem('Super'), sm.haveItem('Charge'))
|
||||
|
||||
@Cache.decorator
|
||||
def enoughStuffCroc(self):
|
||||
sm = self.smbm
|
||||
# say croc has ~5000 energy, and ignore its useless drops
|
||||
(ammoMargin, secs, items) = self.canInflictEnoughDamages(5000, givesDrops=False)
|
||||
if ammoMargin == 0:
|
||||
return sm.wand(sm.knowsLowAmmoCroc(),
|
||||
sm.wor(sm.itemCountOk("Missile", 2),
|
||||
sm.wand(sm.haveItem('Missile'),
|
||||
sm.haveItem('Super'))))
|
||||
else:
|
||||
return SMBool(True, easy, items=items)
|
||||
|
||||
@Cache.decorator
|
||||
def enoughStuffBotwoon(self):
|
||||
sm = self.smbm
|
||||
(ammoMargin, secs, items) = self.canInflictEnoughDamages(6000, givesDrops=False)
|
||||
diff = SMBool(True, easy, [], items)
|
||||
lowStuff = sm.knowsLowStuffBotwoon()
|
||||
if ammoMargin == 0 and lowStuff.bool:
|
||||
(ammoMargin, secs, items) = self.canInflictEnoughDamages(3500, givesDrops=False)
|
||||
diff = SMBool(lowStuff.bool, lowStuff.difficulty, lowStuff.knows, items)
|
||||
if ammoMargin == 0:
|
||||
return smboolFalse
|
||||
fight = sm.wor(sm.energyReserveCountOk(math.ceil(4/sm.getDmgReduction(envDmg=False)[0])),
|
||||
lowStuff)
|
||||
return sm.wandmax(fight, diff)
|
||||
|
||||
@Cache.decorator
|
||||
def enoughStuffGT(self):
|
||||
sm = self.smbm
|
||||
hasBeams = sm.wand(sm.haveItem('Charge'), sm.haveItem('Plasma')).bool
|
||||
(ammoMargin, secs, items) = self.canInflictEnoughDamages(9000, ignoreMissiles=True, givesDrops=hasBeams)
|
||||
diff = SMBool(True, easy, [], items)
|
||||
lowStuff = sm.knowsLowStuffGT()
|
||||
if ammoMargin == 0 and lowStuff.bool:
|
||||
(ammoMargin, secs, items) = self.canInflictEnoughDamages(3000, ignoreMissiles=True)
|
||||
diff = SMBool(lowStuff.bool, lowStuff.difficulty, lowStuff.knows, items)
|
||||
if ammoMargin == 0:
|
||||
return smboolFalse
|
||||
fight = sm.wor(sm.energyReserveCountOk(math.ceil(8/sm.getDmgReduction(envDmg=False)[0])),
|
||||
lowStuff)
|
||||
return sm.wandmax(fight, diff)
|
||||
|
||||
@Cache.decorator
|
||||
def enoughStuffsRidley(self):
|
||||
sm = self.smbm
|
||||
if not sm.haveItem('Morph') and not sm.haveItem('ScrewAttack'):
|
||||
return smboolFalse
|
||||
(ammoMargin, secs, ammoItems) = self.canInflictEnoughDamages(18000, doubleSuper=True, power=True, givesDrops=False)
|
||||
if ammoMargin == 0:
|
||||
return smboolFalse
|
||||
|
||||
# print('RIDLEY', ammoMargin, secs)
|
||||
(diff, defenseItems) = self.computeBossDifficulty(ammoMargin, secs,
|
||||
Settings.bossesDifficulty['Ridley'])
|
||||
if diff < 0:
|
||||
return smboolFalse
|
||||
else:
|
||||
return SMBool(True, diff, items=ammoItems+defenseItems)
|
||||
|
||||
@Cache.decorator
|
||||
def enoughStuffsKraid(self):
|
||||
sm = self.smbm
|
||||
(ammoMargin, secs, ammoItems) = self.canInflictEnoughDamages(1000)
|
||||
if ammoMargin == 0:
|
||||
return smboolFalse
|
||||
#print('KRAID True ', ammoMargin, secs)
|
||||
(diff, defenseItems) = self.computeBossDifficulty(ammoMargin, secs,
|
||||
Settings.bossesDifficulty['Kraid'])
|
||||
if diff < 0:
|
||||
return smboolFalse
|
||||
|
||||
return SMBool(True, diff, items=ammoItems+defenseItems)
|
||||
|
||||
def adjustHealthDropDiff(self, difficulty):
|
||||
(dmgRed, items) = self.getDmgReduction(envDmg=False)
|
||||
# 2 is Varia suit, considered standard eqt for boss fights
|
||||
# there's certainly a smarter way to do this but...
|
||||
if dmgRed < 2:
|
||||
difficulty *= Settings.algoSettings['dmgReductionDifficultyFactor']
|
||||
elif dmgRed > 2:
|
||||
difficulty /= Settings.algoSettings['dmgReductionDifficultyFactor']
|
||||
return difficulty
|
||||
|
||||
@Cache.decorator
|
||||
def enoughStuffsDraygon(self):
|
||||
sm = self.smbm
|
||||
if not sm.haveItem('Morph') and not sm.haveItem('Gravity'):
|
||||
return smboolFalse
|
||||
# some ammo to destroy the turrets during the fight
|
||||
if not sm.haveMissileOrSuper():
|
||||
return smboolFalse
|
||||
(ammoMargin, secs, ammoItems) = self.canInflictEnoughDamages(6000)
|
||||
# print('DRAY', ammoMargin, secs)
|
||||
if ammoMargin > 0:
|
||||
(diff, defenseItems) = self.computeBossDifficulty(ammoMargin, secs,
|
||||
Settings.bossesDifficulty['Draygon'])
|
||||
if diff < 0:
|
||||
fight = smboolFalse
|
||||
else:
|
||||
fight = SMBool(True, diff, items=ammoItems+defenseItems)
|
||||
if sm.haveItem('Gravity') == False:
|
||||
fight.difficulty *= Settings.algoSettings['draygonNoGravityMalus']
|
||||
else:
|
||||
fight._items.append('Gravity')
|
||||
if not sm.haveItem('Morph'):
|
||||
fight.difficulty *= Settings.algoSettings['draygonNoMorphMalus']
|
||||
if sm.haveItem('Gravity') and sm.haveItem('ScrewAttack'):
|
||||
fight.difficulty /= Settings.algoSettings['draygonScrewBonus']
|
||||
fight.difficulty = self.adjustHealthDropDiff(fight.difficulty)
|
||||
else:
|
||||
fight = smboolFalse
|
||||
# for grapple kill considers energy drained by wall socket + 2 spankings by Dray
|
||||
# (original 99 energy used for rounding)
|
||||
nTanksGrapple = (240/sm.getDmgReduction(envDmg=True)[0] + 2*160/sm.getDmgReduction(envDmg=False)[0])/100
|
||||
return sm.wor(fight,
|
||||
sm.wand(sm.knowsDraygonGrappleKill(),
|
||||
sm.haveItem('Grapple'),
|
||||
sm.energyReserveCountOk(nTanksGrapple)),
|
||||
sm.wand(sm.knowsMicrowaveDraygon(),
|
||||
sm.haveItem('Plasma'),
|
||||
sm.canFireChargedShots(),
|
||||
sm.haveItem('XRayScope')),
|
||||
sm.wand(sm.haveItem('Gravity'),
|
||||
sm.energyReserveCountOk(3),
|
||||
sm.knowsDraygonSparkKill(),
|
||||
sm.haveItem('SpeedBooster')))
|
||||
|
||||
@Cache.decorator
|
||||
def enoughStuffsPhantoon(self):
|
||||
sm = self.smbm
|
||||
(ammoMargin, secs, ammoItems) = self.canInflictEnoughDamages(2500, doubleSuper=True)
|
||||
if ammoMargin == 0:
|
||||
return smboolFalse
|
||||
# print('PHANTOON', ammoMargin, secs)
|
||||
(difficulty, defenseItems) = self.computeBossDifficulty(ammoMargin, secs,
|
||||
Settings.bossesDifficulty['Phantoon'])
|
||||
if difficulty < 0:
|
||||
return smboolFalse
|
||||
hasCharge = sm.canFireChargedShots()
|
||||
hasScrew = sm.haveItem('ScrewAttack')
|
||||
if hasScrew:
|
||||
difficulty /= Settings.algoSettings['phantoonFlamesAvoidBonusScrew']
|
||||
defenseItems += hasScrew.items
|
||||
elif hasCharge:
|
||||
difficulty /= Settings.algoSettings['phantoonFlamesAvoidBonusCharge']
|
||||
defenseItems += hasCharge.items
|
||||
elif not hasCharge and sm.itemCount('Missile') <= 2: # few missiles is harder
|
||||
difficulty *= Settings.algoSettings['phantoonLowMissileMalus']
|
||||
difficulty = self.adjustHealthDropDiff(difficulty)
|
||||
fight = SMBool(True, difficulty, items=ammoItems+defenseItems)
|
||||
|
||||
return sm.wor(fight,
|
||||
sm.wand(sm.knowsMicrowavePhantoon(),
|
||||
sm.haveItem('Plasma'),
|
||||
sm.canFireChargedShots(),
|
||||
sm.haveItem('XRayScope')))
|
||||
|
||||
def mbEtankCheck(self):
|
||||
sm = self.smbm
|
||||
if sm.wor(RomPatches.has(sm.player, RomPatches.NerfedRainbowBeam), RomPatches.has(sm.player, RomPatches.TourianSpeedup)):
|
||||
# "add" energy for difficulty calculations
|
||||
energy = 2.8 if sm.haveItem('Varia') else 2.6
|
||||
return (True, energy)
|
||||
nTanks = sm.energyReserveCount()
|
||||
energyDiff = 0
|
||||
if sm.haveItem('Varia') == False:
|
||||
# "remove" 3 etanks (accounting for rainbow beam damage without varia)
|
||||
if nTanks < 6:
|
||||
return (False, 0)
|
||||
energyDiff = -3
|
||||
elif nTanks < 3:
|
||||
return (False, 0)
|
||||
return (True, energyDiff)
|
||||
|
||||
@Cache.decorator
|
||||
def enoughStuffsMotherbrain(self):
|
||||
sm = self.smbm
|
||||
# MB1 can't be hit by charge beam
|
||||
(ammoMargin, secs, _) = self.canInflictEnoughDamages(3000, charge=False, givesDrops=False)
|
||||
if ammoMargin == 0:
|
||||
return smboolFalse
|
||||
# requires 10-10 to break the glass
|
||||
if sm.itemCount('Missile') <= 1 or sm.itemCount('Super') <= 1:
|
||||
return smboolFalse
|
||||
# we actually don't give a shit about MB1 difficulty,
|
||||
# since we embark its health in the following calc
|
||||
(ammoMargin, secs, ammoItems) = self.canInflictEnoughDamages(18000 + 3000, givesDrops=False)
|
||||
if ammoMargin == 0:
|
||||
return smboolFalse
|
||||
(possible, energyDiff) = self.mbEtankCheck()
|
||||
if possible == False:
|
||||
return smboolFalse
|
||||
# print('MB2', ammoMargin, secs)
|
||||
#print("ammoMargin: {}, secs: {}, settings: {}, energyDiff: {}".format(ammoMargin, secs, Settings.bossesDifficulty['MotherBrain'], energyDiff))
|
||||
(diff, defenseItems) = self.computeBossDifficulty(ammoMargin, secs, Settings.bossesDifficulty['MotherBrain'], energyDiff)
|
||||
if diff < 0:
|
||||
return smboolFalse
|
||||
return SMBool(True, diff, items=ammoItems+defenseItems)
|
||||
|
||||
@Cache.decorator
|
||||
def canPassMetroids(self):
|
||||
sm = self.smbm
|
||||
return sm.wor(sm.wand(sm.haveItem('Ice'), sm.haveMissileOrSuper()),
|
||||
# to avoid leaving tourian to refill power bombs
|
||||
sm.itemCountOk('PowerBomb', 3))
|
||||
|
||||
@Cache.decorator
|
||||
def canPassZebetites(self):
|
||||
sm = self.smbm
|
||||
return sm.wor(sm.wand(sm.haveItem('Ice'), sm.knowsIceZebSkip()),
|
||||
sm.wand(sm.haveItem('SpeedBooster'), sm.knowsSpeedZebSkip()),
|
||||
# account for one zebetite, refill may be necessary
|
||||
SMBool(self.canInflictEnoughDamages(1100, charge=False, givesDrops=False, ignoreSupers=True)[0] >= 1, 0))
|
||||
|
||||
@Cache.decorator
|
||||
def enoughStuffTourian(self):
|
||||
sm = self.smbm
|
||||
ret = self.smbm.wand(sm.wor(RomPatches.has(sm.player, RomPatches.TourianSpeedup),
|
||||
sm.wand(sm.canPassMetroids(), sm.canPassZebetites())),
|
||||
sm.canOpenRedDoors(),
|
||||
sm.enoughStuffsMotherbrain(),
|
||||
sm.wor(RomPatches.has(sm.player, RomPatches.OpenZebetites), sm.haveItem('Morph')))
|
||||
return ret
|
||||
|
||||
class Pickup:
|
||||
def __init__(self, itemsPickup):
|
||||
self.itemsPickup = itemsPickup
|
||||
|
||||
def enoughMinors(self, smbm, minorLocations):
|
||||
if self.itemsPickup == 'all':
|
||||
return len(minorLocations) == 0
|
||||
else:
|
||||
return True
|
||||
|
||||
def enoughMajors(self, smbm, majorLocations):
|
||||
if self.itemsPickup == 'all':
|
||||
return len(majorLocations) == 0
|
||||
else:
|
||||
return True
|
||||
|
||||
class Bosses:
|
||||
# bosses helpers to know if they are dead
|
||||
areaBosses = {
|
||||
# classic areas
|
||||
'Brinstar': 'Kraid',
|
||||
'Norfair': 'Ridley',
|
||||
'LowerNorfair': 'Ridley',
|
||||
'WreckedShip': 'Phantoon',
|
||||
'Maridia': 'Draygon',
|
||||
# solver areas
|
||||
'Blue Brinstar': 'Kraid',
|
||||
'Brinstar Hills': 'Kraid',
|
||||
'Bubble Norfair': 'Ridley',
|
||||
'Bubble Norfair Bottom': 'Ridley',
|
||||
'Bubble Norfair Reserve': 'Ridley',
|
||||
'Bubble Norfair Speed': 'Ridley',
|
||||
'Bubble Norfair Wave': 'Ridley',
|
||||
'Draygon Boss': 'Draygon',
|
||||
'Green Brinstar': 'Kraid',
|
||||
'Green Brinstar Reserve': 'Kraid',
|
||||
'Kraid': 'Kraid',
|
||||
'Kraid Boss': 'Kraid',
|
||||
'Left Sandpit': 'Draygon',
|
||||
'Lower Norfair After Amphitheater': 'Ridley',
|
||||
'Lower Norfair Before Amphitheater': 'Ridley',
|
||||
'Lower Norfair Screw Attack': 'Ridley',
|
||||
'Maridia Forgotten Highway': 'Draygon',
|
||||
'Maridia Green': 'Draygon',
|
||||
'Maridia Pink Bottom': 'Draygon',
|
||||
'Maridia Pink Top': 'Draygon',
|
||||
'Maridia Sandpits': 'Draygon',
|
||||
'Norfair Entrance': 'Ridley',
|
||||
'Norfair Grapple Escape': 'Ridley',
|
||||
'Norfair Ice': 'Ridley',
|
||||
'Phantoon Boss': 'Phantoon',
|
||||
'Pink Brinstar': 'Kraid',
|
||||
'Red Brinstar': 'Kraid',
|
||||
'Red Brinstar Top': 'Kraid',
|
||||
'Ridley Boss': 'Ridley',
|
||||
'Right Sandpit': 'Draygon',
|
||||
'Warehouse': 'Kraid',
|
||||
'WreckedShip': 'Phantoon',
|
||||
'WreckedShip Back': 'Phantoon',
|
||||
'WreckedShip Bottom': 'Phantoon',
|
||||
'WreckedShip Gravity': 'Phantoon',
|
||||
'WreckedShip Main': 'Phantoon',
|
||||
'WreckedShip Top': 'Phantoon'
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def Golden4():
|
||||
return ['Draygon', 'Kraid', 'Phantoon', 'Ridley']
|
||||
|
||||
@staticmethod
|
||||
def bossDead(sm, boss):
|
||||
return sm.haveItem(boss)
|
||||
|
||||
@staticmethod
|
||||
def areaBossDead(sm, area):
|
||||
if area not in Bosses.areaBosses:
|
||||
return True
|
||||
return Bosses.bossDead(sm, Bosses.areaBosses[area])
|
||||
|
||||
@staticmethod
|
||||
def allBossesDead(smbm):
|
||||
return smbm.wand(Bosses.bossDead(smbm, 'Kraid'),
|
||||
Bosses.bossDead(smbm, 'Phantoon'),
|
||||
Bosses.bossDead(smbm, 'Draygon'),
|
||||
Bosses.bossDead(smbm, 'Ridley'))
|
||||
|
||||
def diffValue2txt(diff):
|
||||
last = 0
|
||||
for d in sorted(diff2text.keys()):
|
||||
if diff >= last and diff < d:
|
||||
return diff2text[last]
|
||||
last = d
|
||||
return None
|
||||
26
worlds/sm/variaRandomizer/logic/logic.py
Normal file
26
worlds/sm/variaRandomizer/logic/logic.py
Normal file
@@ -0,0 +1,26 @@
|
||||
# entry point for the logic implementation
|
||||
|
||||
class Logic(object):
|
||||
@staticmethod
|
||||
def factory(implementation):
|
||||
if implementation == 'vanilla':
|
||||
from graph.vanilla.graph_helpers import HelpersGraph
|
||||
from graph.vanilla.graph_access import accessPoints
|
||||
from graph.vanilla.graph_locations import locations
|
||||
from graph.vanilla.graph_locations import LocationsHelper
|
||||
Logic.locations = locations
|
||||
Logic.accessPoints = accessPoints
|
||||
Logic.HelpersGraph = HelpersGraph
|
||||
Logic.patches = implementation
|
||||
Logic.LocationsHelper = LocationsHelper
|
||||
elif implementation == 'rotation':
|
||||
from graph.rotation.graph_helpers import HelpersGraph
|
||||
from graph.rotation.graph_access import accessPoints
|
||||
from graph.rotation.graph_locations import locations
|
||||
from graph.rotation.graph_locations import LocationsHelper
|
||||
Logic.locations = locations
|
||||
Logic.accessPoints = accessPoints
|
||||
Logic.HelpersGraph = HelpersGraph
|
||||
Logic.patches = implementation
|
||||
Logic.LocationsHelper = LocationsHelper
|
||||
Logic.implementation = implementation
|
||||
122
worlds/sm/variaRandomizer/logic/smbool.py
Normal file
122
worlds/sm/variaRandomizer/logic/smbool.py
Normal file
@@ -0,0 +1,122 @@
|
||||
def flatten(l):
|
||||
if type(l) is list:
|
||||
return [ y for x in l for y in flatten(x) ]
|
||||
else:
|
||||
return [ l ]
|
||||
|
||||
# super metroid boolean
|
||||
class SMBool:
|
||||
__slots__ = ('bool', 'difficulty', '_knows', '_items')
|
||||
def __init__(self, boolean, difficulty=0, knows=[], items=[]):
|
||||
self.bool = boolean
|
||||
self.difficulty = difficulty
|
||||
self._knows = knows
|
||||
self._items = items
|
||||
|
||||
@property
|
||||
def knows(self):
|
||||
self._knows = list(set(flatten(self._knows)))
|
||||
return self._knows
|
||||
|
||||
@knows.setter
|
||||
def knows(self, knows):
|
||||
self._knows = knows
|
||||
|
||||
@property
|
||||
def items(self):
|
||||
self._items = list(set(flatten(self._items)))
|
||||
return self._items
|
||||
|
||||
@items.setter
|
||||
def items(self, items):
|
||||
self._items = items
|
||||
|
||||
def __repr__(self):
|
||||
# to display the smbool as a string
|
||||
return 'SMBool({}, {}, {}, {})'.format(self.bool, self.difficulty, sorted(self.knows), sorted(self.items))
|
||||
|
||||
def __getitem__(self, index):
|
||||
# to acces the smbool as [0] for the bool and [1] for the difficulty.
|
||||
# required when we load a json preset where the smbool is stored as a list,
|
||||
# and we add missing smbools to it, so we have a mix of lists and smbools.
|
||||
if index == 0:
|
||||
return self.bool
|
||||
elif index == 1:
|
||||
return self.difficulty
|
||||
|
||||
def __bool__(self):
|
||||
# when used in boolean expressions (with and/or/not) (python3)
|
||||
return self.bool
|
||||
|
||||
def __eq__(self, other):
|
||||
# for ==
|
||||
return self.bool == other
|
||||
|
||||
def __ne__(self, other):
|
||||
# for !=
|
||||
return self.bool != other
|
||||
|
||||
def __lt__(self, other):
|
||||
# for <
|
||||
if self.bool and other.bool:
|
||||
return self.difficulty < other.difficulty
|
||||
else:
|
||||
return self.bool
|
||||
|
||||
def __copy__(self):
|
||||
return SMBool(self.bool, self.difficulty, self._knows, self._items)
|
||||
|
||||
def json(self):
|
||||
# as we have slots instead of dict
|
||||
return {'bool': self.bool, 'difficulty': self.difficulty, 'knows': self.knows, 'items': self.items}
|
||||
|
||||
def wand(*args):
|
||||
# looping here is faster than using "if ... in" construct
|
||||
for smb in args:
|
||||
if not smb.bool:
|
||||
return smboolFalse
|
||||
|
||||
difficulty = 0
|
||||
|
||||
for smb in args:
|
||||
difficulty += smb.difficulty
|
||||
|
||||
return SMBool(True,
|
||||
difficulty,
|
||||
[ smb._knows for smb in args ],
|
||||
[ smb._items for smb in args ])
|
||||
|
||||
def wandmax(*args):
|
||||
# looping here is faster than using "if ... in" construct
|
||||
for smb in args:
|
||||
if not smb.bool:
|
||||
return smboolFalse
|
||||
|
||||
difficulty = 0
|
||||
|
||||
for smb in args:
|
||||
if smb.difficulty > difficulty:
|
||||
difficulty = smb.difficulty
|
||||
|
||||
return SMBool(True,
|
||||
difficulty,
|
||||
[ smb._knows for smb in args ],
|
||||
[ smb._items for smb in args ])
|
||||
|
||||
def wor(*args):
|
||||
# looping here is faster than using "if ... in" construct
|
||||
for smb in args:
|
||||
if smb.bool:
|
||||
return min(args)
|
||||
|
||||
return smboolFalse
|
||||
|
||||
# negates boolean part of the SMBool
|
||||
def wnot(a):
|
||||
return smboolFalse if a.bool else SMBool(True, a.difficulty)
|
||||
|
||||
__and__ = wand
|
||||
__or__ = wor
|
||||
__not__ = wnot
|
||||
|
||||
smboolFalse = SMBool(False)
|
||||
241
worlds/sm/variaRandomizer/logic/smboolmanager.py
Normal file
241
worlds/sm/variaRandomizer/logic/smboolmanager.py
Normal file
@@ -0,0 +1,241 @@
|
||||
# object to handle the smbools and optimize them
|
||||
|
||||
from logic.cache import Cache
|
||||
from logic.smbool import SMBool, smboolFalse
|
||||
from logic.helpers import Bosses
|
||||
from logic.logic import Logic
|
||||
from utils.doorsmanager import DoorsManager
|
||||
from utils.parameters import Knows, isKnows
|
||||
import logging
|
||||
import sys
|
||||
|
||||
class SMBoolManager(object):
|
||||
items = ['ETank', 'Missile', 'Super', 'PowerBomb', 'Bomb', 'Charge', 'Ice', 'HiJump', 'SpeedBooster', 'Wave', 'Spazer', 'SpringBall', 'Varia', 'Plasma', 'Grapple', 'Morph', 'Reserve', 'Gravity', 'XRayScope', 'SpaceJump', 'ScrewAttack', 'Nothing', 'NoEnergy', 'MotherBrain', 'Hyper'] + Bosses.Golden4()
|
||||
countItems = ['Missile', 'Super', 'PowerBomb', 'ETank', 'Reserve']
|
||||
|
||||
def __init__(self, player=0, maxDiff=sys.maxsize):
|
||||
self._items = { }
|
||||
self._counts = { }
|
||||
|
||||
self.player = player
|
||||
self.maxDiff = maxDiff
|
||||
|
||||
# cache related
|
||||
self.cacheKey = 0
|
||||
self.computeItemsPositions()
|
||||
Cache.reset()
|
||||
Logic.factory('vanilla')
|
||||
self.helpers = Logic.HelpersGraph(self)
|
||||
self.doorsManager = DoorsManager()
|
||||
self.createFacadeFunctions()
|
||||
self.createKnowsFunctions(player)
|
||||
self.resetItems()
|
||||
|
||||
def computeItemsPositions(self):
|
||||
# compute index in cache key for each items
|
||||
self.itemsPositions = {}
|
||||
maxBitsForCountItem = 7 # 128 values with 7 bits
|
||||
for (i, item) in enumerate(self.countItems):
|
||||
pos = i*maxBitsForCountItem
|
||||
bitMask = (2<<(maxBitsForCountItem-1))-1
|
||||
bitMask = bitMask << pos
|
||||
self.itemsPositions[item] = (pos, bitMask)
|
||||
for (i, item) in enumerate(self.items, (i+1)*maxBitsForCountItem+1):
|
||||
if item in self.countItems:
|
||||
continue
|
||||
self.itemsPositions[item] = (i, 1<<i)
|
||||
|
||||
def computeNewCacheKey(self, item, value):
|
||||
# generate an unique integer for each items combinations which is use as key in the cache.
|
||||
if item in ['Nothing', 'NoEnergy']:
|
||||
return
|
||||
(pos, bitMask) = self.itemsPositions[item]
|
||||
# print("--------------------- {} {} ----------------------------".format(item, value))
|
||||
# print("old: "+format(self.cacheKey, '#067b'))
|
||||
self.cacheKey = (self.cacheKey & (~bitMask)) | (value<<pos)
|
||||
# print("new: "+format(self.cacheKey, '#067b'))
|
||||
# self.printItemsInKey(self.cacheKey)
|
||||
|
||||
def printItemsInKey(self, key):
|
||||
# for debug purpose
|
||||
print("key: "+format(key, '#067b'))
|
||||
msg = ""
|
||||
for (item, (pos, bitMask)) in self.itemsPositions.items():
|
||||
value = (key & bitMask) >> pos
|
||||
if value != 0:
|
||||
msg += " {}: {}".format(item, value)
|
||||
print("items:{}".format(msg))
|
||||
|
||||
def isEmpty(self):
|
||||
for item in self.items:
|
||||
if self.haveItem(item):
|
||||
return False
|
||||
for item in self.countItems:
|
||||
if self.itemCount(item) > 0:
|
||||
return False
|
||||
return True
|
||||
|
||||
def getItems(self):
|
||||
# get a dict of collected items and how many (to be displayed on the solver spoiler)
|
||||
itemsDict = {}
|
||||
for item in self.items:
|
||||
itemsDict[item] = 1 if self._items[item] == True else 0
|
||||
for item in self.countItems:
|
||||
itemsDict[item] = self._counts[item]
|
||||
return itemsDict
|
||||
|
||||
def withItem(self, item, func):
|
||||
self.addItem(item)
|
||||
ret = func(self)
|
||||
self.removeItem(item)
|
||||
return ret
|
||||
|
||||
def resetItems(self):
|
||||
self._items = { item : smboolFalse for item in self.items }
|
||||
self._counts = { item : 0 for item in self.countItems }
|
||||
|
||||
self.cacheKey = 0
|
||||
Cache.update(self.cacheKey)
|
||||
|
||||
def addItem(self, item):
|
||||
# a new item is available
|
||||
self._items[item] = SMBool(True, items=[item])
|
||||
if self.isCountItem(item):
|
||||
count = self._counts[item] + 1
|
||||
self._counts[item] = count
|
||||
self.computeNewCacheKey(item, count)
|
||||
else:
|
||||
self.computeNewCacheKey(item, 1)
|
||||
|
||||
Cache.update(self.cacheKey)
|
||||
|
||||
def addItems(self, items):
|
||||
if len(items) == 0:
|
||||
return
|
||||
for item in items:
|
||||
self._items[item] = SMBool(True, items=[item])
|
||||
if self.isCountItem(item):
|
||||
count = self._counts[item] + 1
|
||||
self._counts[item] = count
|
||||
self.computeNewCacheKey(item, count)
|
||||
else:
|
||||
self.computeNewCacheKey(item, 1)
|
||||
|
||||
Cache.update(self.cacheKey)
|
||||
|
||||
def removeItem(self, item):
|
||||
# randomizer removed an item (or the item was added to test a post available)
|
||||
if self.isCountItem(item):
|
||||
count = self._counts[item] - 1
|
||||
self._counts[item] = count
|
||||
if count == 0:
|
||||
self._items[item] = smboolFalse
|
||||
self.computeNewCacheKey(item, count)
|
||||
else:
|
||||
self._items[item] = smboolFalse
|
||||
self.computeNewCacheKey(item, 0)
|
||||
|
||||
Cache.update(self.cacheKey)
|
||||
|
||||
def createFacadeFunctions(self):
|
||||
for fun in dir(self.helpers):
|
||||
if fun != 'smbm' and fun[0:2] != '__':
|
||||
setattr(self, fun, getattr(self.helpers, fun))
|
||||
|
||||
def traverse(self, doorName):
|
||||
return self.doorsManager.traverse(self, doorName)
|
||||
|
||||
def createKnowsFunctions(self, player):
|
||||
# for each knows we have a function knowsKnows (ex: knowsAlcatrazEscape()) which
|
||||
# take no parameter
|
||||
for knows in Knows.__dict__:
|
||||
if isKnows(knows):
|
||||
if knows in Knows.knowsDict[player].__dict__:
|
||||
setattr(self, 'knows'+knows, lambda knows=knows: SMBool(Knows.knowsDict[player].__dict__[knows].bool,
|
||||
Knows.knowsDict[player].__dict__[knows].difficulty,
|
||||
knows=[knows]))
|
||||
else:
|
||||
# if knows not in preset, use default values
|
||||
setattr(self, 'knows'+knows, lambda knows=knows: SMBool(Knows.__dict__[knows].bool,
|
||||
Knows.__dict__[knows].difficulty,
|
||||
knows=[knows]))
|
||||
|
||||
def isCountItem(self, item):
|
||||
return item in self.countItems
|
||||
|
||||
def itemCount(self, item):
|
||||
# return integer
|
||||
#self.state.item_count(item, self.player)
|
||||
return self._counts[item]
|
||||
|
||||
def haveItem(self, item):
|
||||
#return self.state.has(item, self.player)
|
||||
return self._items[item]
|
||||
|
||||
wand = staticmethod(SMBool.wand)
|
||||
wandmax = staticmethod(SMBool.wandmax)
|
||||
wor = staticmethod(SMBool.wor)
|
||||
wnot = staticmethod(SMBool.wnot)
|
||||
|
||||
def itemCountOk(self, item, count, difficulty=0):
|
||||
if self.itemCount(item) >= count:
|
||||
if item in ['ETank', 'Reserve']:
|
||||
item = str(count)+'-'+item
|
||||
return SMBool(True, difficulty, items = [item])
|
||||
else:
|
||||
return smboolFalse
|
||||
|
||||
def energyReserveCountOk(self, count, difficulty=0):
|
||||
if self.energyReserveCount() >= count:
|
||||
nEtank = self.itemCount('ETank')
|
||||
if nEtank > count:
|
||||
nEtank = int(count)
|
||||
items = str(nEtank)+'-ETank'
|
||||
nReserve = self.itemCount('Reserve')
|
||||
if nEtank < count:
|
||||
nReserve = int(count) - nEtank
|
||||
items += ' - '+str(nReserve)+'-Reserve'
|
||||
return SMBool(True, difficulty, items = [items])
|
||||
else:
|
||||
return smboolFalse
|
||||
|
||||
class SMBoolManagerPlando(SMBoolManager):
|
||||
def __init__(self):
|
||||
super(SMBoolManagerPlando, self).__init__()
|
||||
|
||||
def addItem(self, item):
|
||||
# a new item is available
|
||||
already = self.haveItem(item)
|
||||
isCount = self.isCountItem(item)
|
||||
if isCount or not already:
|
||||
self._items[item] = SMBool(True, items=[item])
|
||||
else:
|
||||
# handle duplicate major items (plandos)
|
||||
self._items['dup_'+item] = True
|
||||
if isCount:
|
||||
count = self._counts[item] + 1
|
||||
self._counts[item] = count
|
||||
self.computeNewCacheKey(item, count)
|
||||
else:
|
||||
self.computeNewCacheKey(item, 1)
|
||||
|
||||
Cache.update(self.cacheKey)
|
||||
|
||||
def removeItem(self, item):
|
||||
# randomizer removed an item (or the item was added to test a post available)
|
||||
if self.isCountItem(item):
|
||||
count = self._counts[item] - 1
|
||||
self._counts[item] = count
|
||||
if count == 0:
|
||||
self._items[item] = smboolFalse
|
||||
self.computeNewCacheKey(item, count)
|
||||
else:
|
||||
dup = 'dup_'+item
|
||||
if self._items.get(dup, None) is None:
|
||||
self._items[item] = smboolFalse
|
||||
self.computeNewCacheKey(item, 0)
|
||||
else:
|
||||
del self._items[dup]
|
||||
self.computeNewCacheKey(item, 1)
|
||||
|
||||
Cache.update(self.cacheKey)
|
||||
Reference in New Issue
Block a user