Added Super Metroid support (#46)

Varia Randomizer based implementation
LttPClient -> SNIClient
This commit is contained in:
lordlou
2021-11-12 08:00:11 -05:00
committed by GitHub
parent 61ae51b30c
commit 77ec8d4141
141 changed files with 43859 additions and 106 deletions

View 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()

View 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

View 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

View 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)

View 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)