SM update ()

* fixed generations failing when only bosses are unreachable

* - replaced setting maxDiff to infinity with a bool only affecting boss logics if only bosses are left to finish

* fixed failling generations when using 'fun' settings

Accessibility checks are forced to 'items' if restricted locations are used by VARIA following usage of 'fun' settings

* fixed debug logger

* removed unsupported "suits_restriction" option

* fixed generations failing when only bosses are unreachable (using a less intrusive approach for AP)

* - fixed deathlink emptying reserves

- added death_link_survive option that lets player survive when receiving a deathlink if the have non-empty reserves

* - merged death_link and death_link_survive options
This commit is contained in:
lordlou 2021-12-02 00:11:42 -05:00 committed by GitHub
parent f673dfb7cf
commit 83cfd6ec05
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 64 additions and 27 deletions

View File

@ -175,15 +175,22 @@ async def deathlink_kill_player(ctx: Context):
snes_buffered_write(ctx, WRAM_START + 0x0373, bytes([8])) # deal 1 full heart of damage at next opportunity
elif ctx.game == GAME_SM:
snes_buffered_write(ctx, WRAM_START + 0x09C2, bytes([0, 0])) # set current health to 0
if not ctx.death_link_allow_survive:
snes_buffered_write(ctx, WRAM_START + 0x09D6, bytes([0, 0])) # set current reserve to 0
await snes_flush_writes(ctx)
await asyncio.sleep(1)
gamemode = None
if ctx.game == GAME_ALTTP:
gamemode = await snes_read(ctx, WRAM_START + 0x10, 1)
if not gamemode or gamemode[0] in DEATH_MODES:
ctx.death_state = DeathState.dead
elif ctx.game == GAME_SM:
gamemode = await snes_read(ctx, WRAM_START + 0x0998, 1)
if not gamemode or gamemode[0] in (DEATH_MODES if ctx.game == GAME_ALTTP else SM_DEATH_MODES):
ctx.death_state = DeathState.dead
health = await snes_read(ctx, WRAM_START + 0x09C2, 2)
if health is not None:
health = health[0] | (health[1] << 8)
if not gamemode or gamemode[0] in SM_DEATH_MODES or (ctx.death_link_allow_survive and health is not None and health > 0):
ctx.death_state = DeathState.dead
ctx.last_death_link = time.time()
@ -884,6 +891,7 @@ async def game_watcher(ctx: Context):
if not ctx.rom:
ctx.finished_game = False
ctx.death_link_allow_survive = False
game_name = await snes_read(ctx, SM_ROMNAME_START, 2)
if game_name is None:
continue
@ -900,6 +908,7 @@ async def game_watcher(ctx: Context):
death_link = await snes_read(ctx, DEATH_LINK_ACTIVE_ADDR if ctx.game == GAME_ALTTP else
SM_DEATH_LINK_ACTIVE_ADDR, 1)
if death_link:
ctx.death_link_allow_survive = bool(death_link[0] & 0b10)
await ctx.update_death_link(bool(death_link[0] & 0b1))
if not ctx.prev_rom or ctx.prev_rom != ctx.rom:
ctx.locations_checked = set()

View File

@ -40,6 +40,14 @@ class StartLocation(Choice):
option_Golden_Four = 14
default = 1
class DeathLinkSurvive(Choice):
"""When DeathLink is enabled and someone dies, you can survive with (enable_survive) if you have non-empty reserve tank."""
displayname = "Death Link Survive"
option_disable = 0
option_enable = 1
option_enable_survive = 3
default = 0
class MaxDifficulty(Choice):
displayname = "Maximum Difficulty"
option_easy = 0
@ -57,9 +65,6 @@ class MorphPlacement(Choice):
option_normal = 1
default = 0
class SuitsRestriction(DefaultOnToggle):
displayname = "Suits Restriction"
class StrictMinors(Toggle):
displayname = "Strict Minors"
@ -117,12 +122,15 @@ class BossRandomization(Toggle):
displayname = "Boss Randomization"
class FunCombat(Toggle):
"""if used, might force 'items' accessibility"""
displayname = "Fun Combat"
class FunMovement(Toggle):
"""if used, might force 'items' accessibility"""
displayname = "Fun Movement"
class FunSuits(Toggle):
"""if used, might force 'items' accessibility"""
displayname = "Fun Suits"
class LayoutPatches(DefaultOnToggle):
@ -188,7 +196,7 @@ sm_options: typing.Dict[str, type(Option)] = {
"start_inventory_removes_from_pool": StartItemsRemovesFromPool,
"preset": Preset,
"start_location": StartLocation,
"death_link": DeathLink,
"death_link_survive": DeathLinkSurvive,
#"majors_split": "Full",
#"scav_num_locs": "10",
#"scav_randomized": "off",
@ -197,7 +205,7 @@ sm_options: typing.Dict[str, type(Option)] = {
#"progression_speed": "medium",
#"progression_difficulty": "normal",
"morph_placement": MorphPlacement,
"suits_restriction": SuitsRestriction,
#"suits_restriction": SuitsRestriction,
#"hide_items": "off",
"strict_minors": StrictMinors,
"missile_qty": MissileQty,

View File

@ -2,7 +2,7 @@ import logging
import copy
import os
import threading
from typing import Set
from typing import Set, List
logger = logging.getLogger("Super Metroid")
@ -59,7 +59,7 @@ class SMWorld(World):
def sm_init(self, parent: MultiWorld):
if (hasattr(parent, "state")): # for unit tests where MultiWorld is instanciated before worlds
self.smbm = {player: SMBoolManager(player, parent.state.smbm[player].maxDiff) for player in parent.get_game_players("Super Metroid")}
self.smbm = {player: SMBoolManager(player, parent.state.smbm[player].maxDiff, parent.state.smbm[player].onlyBossLeft) for player in parent.get_game_players("Super Metroid")}
orig_init(self, parent)
@ -88,6 +88,10 @@ class SMWorld(World):
if (self.variaRando.args.morphPlacement == "early"):
self.world.local_items[self.player].value.add('Morph')
if (len(self.variaRando.randoExec.setup.restrictedLocs) > 0):
self.world.accessibility[self.player] = self.world.accessibility[self.player].from_text("items")
logger.warning(f"accessibility forced to 'items' for player {self.world.get_player_name(self.player)} because of 'fun' settings")
def generate_basic(self):
itemPool = self.variaRando.container.itemPool
@ -274,7 +278,7 @@ class SMWorld(World):
openTourianGreyDoors = {0x07C823 + 5: [0x0C], 0x07C831 + 5: [0x0C]}
deathLink = {0x277f04: [int(self.world.death_link[self.player])]}
deathLink = {0x277f04: [self.world.death_link_survive[self.player].value]}
playerNames = {}
playerNameIDMap = {}
@ -476,17 +480,6 @@ class SMWorld(World):
item.player != self.player or
item.name != "Morph Ball"]
def post_fill(self):
# increase maxDifficulty if only bosses is too difficult to beat game
new_state = CollectionState(self.world)
for item in self.world.itempool:
if item.player == self.player:
new_state.collect(item, True)
new_state.sweep_for_events()
if (any(not self.world.get_location(bossLoc, self.player).can_reach(new_state) for bossLoc in self.locked_items)):
if (self.variaRando.randoExec.setup.services.onlyBossesLeft(self.variaRando.randoExec.setup.startAP, self.variaRando.randoExec.setup.container)):
self.world.state.smbm[self.player].maxDiff = infinity
@classmethod
def stage_fill_hook(cls, world, progitempool, nonexcludeditempool, localrestitempool, nonlocalrestitempool,
restitempool, fill_locations):
@ -494,6 +487,22 @@ class SMWorld(World):
progitempool.sort(
key=lambda item: 1 if (item.name == 'Morph Ball') else 0)
def post_fill(self):
new_state = CollectionState(self.world)
progitempool = []
for item in self.world.itempool:
if item.player == self.player and item.advancement:
progitempool.append(item)
for item in progitempool:
new_state.collect(item, True)
bossesLoc = ['Draygon', 'Kraid', 'Ridley', 'Phantoon', 'Mother Brain']
for bossLoc in bossesLoc:
if (not self.world.get_location(bossLoc, self.player).can_reach(new_state)):
self.world.state.smbm[self.player].onlyBossLeft = True
break
def create_locations(self, player: int):
for name, id in locations_lookup_name_to_id.items():
self.locations[name] = SMLocation(player, name, id)

View File

@ -134,9 +134,9 @@ class AccessGraph(object):
def printGraph(self):
if self.log.getEffectiveLevel() == logging.DEBUG:
self.log("Area graph:")
self.log.debug("Area graph:")
for s, d in self.InterAreaTransitions:
self.log("{} -> {}".format(s.Name, d.Name))
self.log.debug("{} -> {}".format(s.Name, d.Name))
def addAccessPoint(self, ap):
ap.distance = 0

View File

@ -566,6 +566,8 @@ class Helpers(object):
# print('RIDLEY', ammoMargin, secs)
(diff, defenseItems) = self.computeBossDifficulty(ammoMargin, secs,
Settings.bossesDifficulty['Ridley'])
if (sm.onlyBossLeft):
diff = 1
if diff < 0:
return smboolFalse
else:
@ -580,6 +582,8 @@ class Helpers(object):
#print('KRAID True ', ammoMargin, secs)
(diff, defenseItems) = self.computeBossDifficulty(ammoMargin, secs,
Settings.bossesDifficulty['Kraid'])
if (sm.onlyBossLeft):
diff = 1
if diff < 0:
return smboolFalse
@ -621,6 +625,8 @@ class Helpers(object):
if sm.haveItem('Gravity') and sm.haveItem('ScrewAttack'):
fight.difficulty /= Settings.algoSettings['draygonScrewBonus']
fight.difficulty = self.adjustHealthDropDiff(fight.difficulty)
if (sm.onlyBossLeft):
fight.difficulty = 1
else:
fight = smboolFalse
# for grapple kill considers energy drained by wall socket + 2 spankings by Dray
@ -661,6 +667,8 @@ class Helpers(object):
elif not hasCharge and sm.itemCount('Missile') <= 2: # few missiles is harder
difficulty *= Settings.algoSettings['phantoonLowMissileMalus']
difficulty = self.adjustHealthDropDiff(difficulty)
if (sm.onlyBossLeft):
difficulty = 1
fight = SMBool(True, difficulty, items=ammoItems+defenseItems)
return sm.wor(fight,
@ -707,6 +715,8 @@ class Helpers(object):
# 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 (sm.onlyBossLeft):
diff = 1
if diff < 0:
return smboolFalse
return SMBool(True, diff, items=ammoItems+defenseItems)

View File

@ -13,12 +13,13 @@ 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):
def __init__(self, player=0, maxDiff=sys.maxsize, onlyBossLeft = False):
self._items = { }
self._counts = { }
self.player = player
self.maxDiff = maxDiff
self.onlyBossLeft = onlyBossLeft
# cache related
self.cacheKey = 0

View File

@ -76,7 +76,7 @@ class ItemLocContainer(object):
locs = copy.copy(self.unusedLocations)
# we don't copy restriction state on purpose: it depends on
# outside context we don't want to bring to the copy
ret = ItemLocContainer(SMBoolManager(self.sm.player, self.sm.maxDiff),
ret = ItemLocContainer(SMBoolManager(self.sm.player, self.sm.maxDiff, self.sm.onlyBossLeft),
self.itemPoolBackup[:] if self.itemPoolBackup != None else self.itemPool[:],
locs)
ret.currentItems = self.currentItems[:]
@ -103,7 +103,7 @@ class ItemLocContainer(object):
# transfer collected items/locations to another container
def transferCollected(self, dest):
dest.currentItems = self.currentItems[:]
dest.sm = SMBoolManager(self.sm.player, self.sm.maxDiff)
dest.sm = SMBoolManager(self.sm.player, self.sm.maxDiff, self.sm.onlyBossLeft)
dest.sm.addItems([item.Type for item in dest.currentItems])
dest.itemLocations = copy.copy(self.itemLocations)
dest.unrestrictedItems = copy.copy(self.unrestrictedItems)

View File

@ -311,7 +311,7 @@ def loadRandoPreset(world, player, args):
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.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