The Messenger: Add more difficult logic options (#1550)

This commit is contained in:
alwaysintreble 2023-03-21 15:21:27 -05:00 committed by GitHub
parent 91502505a1
commit 1c69fb3c3c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 277 additions and 50 deletions

View File

@ -199,11 +199,15 @@ class WorldTestBase(unittest.TestCase):
self.collect_all_but(all_items)
for location in self.multiworld.get_locations():
self.assertEqual(self.multiworld.state.can_reach(location), location.name not in locations)
loc_reachable = self.multiworld.state.can_reach(location)
self.assertEqual(loc_reachable, location.name not in locations,
f"{location.name} is reachable without {all_items}" if loc_reachable
else f"{location.name} is not reachable without {all_items}")
for item_names in possible_items:
items = self.collect_by_name(item_names)
for location in locations:
self.assertTrue(self.can_reach_location(location))
self.assertTrue(self.can_reach_location(location),
f"{location} not reachable with {item_names}")
self.remove(items)
def assertBeatable(self, beatable: bool):

View File

@ -7,9 +7,19 @@ class MessengerAccessibility(Accessibility):
__doc__ = Accessibility.__doc__.replace(f"default {Accessibility.default}", f"default {default}")
class Logic(DefaultOnToggle):
"""Whether the seed should be guaranteed completable."""
display_name = "Use Logic"
class Logic(Choice):
"""
The level of logic to use when determining what locations in your world are accessible.
Normal can require damage boosts, but otherwise approachable for someone who has beaten the game.
Hard has some easier speedrunning tricks in logic. May need to leash.
Challenging contains more medium and hard difficulty speedrunning tricks.
OoB places everything with the minimum amount of rules possible. Expect to do OoB. Not guaranteed completable.
"""
display_name = "Logic Level"
option_normal = 0
option_hard = 1
option_challenging = 2
option_oob = 3
class PowerSeals(DefaultOnToggle):
@ -55,7 +65,7 @@ class RequiredSeals(Range):
messenger_options = {
"accessibility": MessengerAccessibility,
"enable_logic": Logic,
"logic_level": Logic,
"shuffle_seals": PowerSeals,
"goal": Goal,
"music_box": MusicBox,

View File

@ -1,7 +1,7 @@
from typing import Dict, Callable, TYPE_CHECKING
from BaseClasses import CollectionState, MultiWorld
from worlds.generic.Rules import set_rule, allow_self_locking_items
from worlds.generic.Rules import set_rule, allow_self_locking_items, add_rule
from .Options import MessengerAccessibility, Goal
from .Constants import NOTES, PHOBEKINS
@ -14,12 +14,14 @@ else:
class MessengerRules:
player: int
world: MessengerWorld
region_rules: Dict[str, Callable[[CollectionState], bool]]
location_rules: Dict[str, Callable[[CollectionState], bool]]
def __init__(self, world: MessengerWorld):
def __init__(self, world: MessengerWorld) -> None:
self.player = world.player
self.world = world
self.region_rules: Dict[str, Callable[[CollectionState], bool]] = {
self.region_rules = {
"Ninja Village": self.has_wingsuit,
"Autumn Hills": self.has_wingsuit,
"Catacombs": self.has_wingsuit,
@ -27,13 +29,13 @@ class MessengerRules:
"Searing Crags Upper": self.has_vertical,
"Cloud Ruins": lambda state: self.has_wingsuit(state) and state.has("Ruxxtin's Amulet", self.player),
"Underworld": self.has_tabi,
"Forlorn Temple": lambda state: state.has_all(PHOBEKINS, self.player) and self.has_wingsuit(state),
"Forlorn Temple": lambda state: state.has_all({"Wingsuit", *PHOBEKINS}, self.player),
"Glacial Peak": self.has_vertical,
"Elemental Skylands": lambda state: state.has("Fairy Bottle", self.player),
"Music Box": lambda state: state.has_all(NOTES, self.player)
"Music Box": lambda state: state.has_all(set(NOTES), self.player) and self.has_vertical(state)
}
self.location_rules: Dict[str, Callable[[CollectionState], bool]] = {
self.location_rules = {
# ninja village
"Ninja Village Seal - Tree House": self.has_dart,
# autumn hills
@ -88,8 +90,11 @@ class MessengerRules:
return self.has_wingsuit(state) or self.has_dart(state)
def has_enough_seals(self, state: CollectionState) -> bool:
required_seals = state.multiworld.worlds[self.player].required_seals
return state.has("Power Seal", self.player, required_seals)
return not self.world.required_seals or state.has("Power Seal", self.player, self.world.required_seals)
def true(self, state: CollectionState) -> bool:
"""I know this is stupid, but it's easier to read in the dicts."""
return True
def set_messenger_rules(self) -> None:
multiworld = self.world.multiworld
@ -105,14 +110,111 @@ class MessengerRules:
set_rule(multiworld.get_entrance("Tower HQ -> Music Box", self.player),
lambda state: state.has("Shop Chest", self.player))
if multiworld.enable_logic[self.player]:
multiworld.completion_condition[self.player] = lambda state: state.has("Rescue Phantom", self.player)
else:
multiworld.accessibility[self.player].value = MessengerAccessibility.option_minimal
if multiworld.accessibility[self.player] > MessengerAccessibility.option_locations:
set_self_locking_items(multiworld, self.player)
class MessengerHardRules(MessengerRules):
extra_rules: Dict[str, Callable[[CollectionState], bool]]
def __init__(self, world: MessengerWorld) -> None:
super().__init__(world)
self.region_rules.update({
"Ninja Village": self.has_vertical,
"Autumn Hills": self.has_vertical,
"Catacombs": self.has_vertical,
"Bamboo Creek": self.has_vertical,
"Forlorn Temple": lambda state: self.has_vertical(state) and state.has_all(set(PHOBEKINS), self.player),
"Searing Crags Upper": self.true,
"Glacial Peak": self.true,
})
self.location_rules.update({
"Howling Grotto Seal - Windy Saws and Balls": self.true,
"Glacial Peak Seal - Projectile Spike Pit": self.true,
})
self.extra_rules = {
"Climbing Claws": self.has_dart,
"Astral Seed": self.has_dart,
"Candle": self.has_dart,
"Key of Strength": lambda state: state.has("Power Thistle", self.player) or
self.has_dart(state) or
self.has_windmill(state),
"Key of Symbiosis": self.has_windmill,
"Autumn Hills Seal - Spike Ball Darts": lambda state: (self.has_dart(state) and self.has_windmill(state))
or self.has_wingsuit(state),
"Glacial Peak Seal - Glacial Air Swag": self.has_windmill,
"Underworld Seal - Fireball Wave": lambda state: self.has_wingsuit(state)
or state.has_all({"Ninja Tabi", "Windmill Shuriken"},
self.player),
}
def has_windmill(self, state: CollectionState) -> bool:
return state.has("Windmill Shuriken", self.player)
def set_messenger_rules(self) -> None:
super().set_messenger_rules()
for loc, rule in self.extra_rules.items():
add_rule(self.world.multiworld.get_location(loc, self.player), rule, "or")
class MessengerChallengeRules(MessengerHardRules):
def __init__(self, world: MessengerWorld) -> None:
super().__init__(world)
self.region_rules.update({
"Forlorn Temple": lambda state: (self.has_vertical(state) and state.has_all(set(PHOBEKINS), self.player))
or state.has_all({"Wingsuit", "Windmill Shuriken"}, self.player),
"Elemental Skylands": lambda state: self.has_wingsuit(state) or state.has("Fairy Bottle", self.player)
})
self.location_rules.update({
"Fairy Bottle": self.true,
"Howling Grotto Seal - Crushing Pits": self.true,
"Underworld Seal - Sharp and Windy Climb": self.true,
"Riviere Turquoise Seal - Flower Power": self.true,
})
self.extra_rules.update({
"Key of Hope": self.has_vertical,
"Key of Symbiosis": lambda state: self.has_vertical(state) or self.has_windmill(state),
})
class MessengerOOBRules(MessengerRules):
def __init__(self, world: MessengerWorld) -> None:
self.world = world
self.player = world.player
self.region_rules = {
"Elemental Skylands": lambda state: state.has_any({"Wingsuit", "Rope Dart", "Fairy Bottle"}, self.player),
"Music Box": lambda state: state.has_all(set(NOTES), self.player)
}
self.location_rules = {
"Claustro": self.has_wingsuit,
"Key of Strength": self.has_wingsuit,
"Key of Love": lambda state: state.has_all({"Sun Crest", "Moon Crest"}, self.player),
"Pyro": self.has_tabi,
"Key of Chaos": self.has_tabi,
"Key of Courage": lambda state: state.has_all({"Demon King Crown", "Fairy Bottle"}, self.player),
"Autumn Hills Seal - Spike Ball Darts": self.has_dart,
"Ninja Village Seal - Tree House": self.has_dart,
"Underworld Seal - Fireball Wave": lambda state: state.has_any({"Wingsuit", "Windmill Shuriken"},
self.player),
"Tower of Time Seal - Time Waster Seal": self.has_dart,
"Shop Chest": self.has_enough_seals
}
def set_messenger_rules(self) -> None:
super().set_messenger_rules()
self.world.multiworld.completion_condition[self.player] = lambda state: True
self.world.multiworld.accessibility[self.player].value = MessengerAccessibility.option_minimal
def set_self_locking_items(multiworld: MultiWorld, player: int) -> None:
# do the ones for seal shuffle on and off first
allow_self_locking_items(multiworld.get_location("Key of Strength", player), "Power Thistle")

View File

@ -12,7 +12,7 @@ else:
class MessengerRegion(Region):
def __init__(self, name: str, world: MessengerWorld):
def __init__(self, name: str, world: MessengerWorld) -> None:
super().__init__(name, world.player, world.multiworld)
self.add_locations(self.multiworld.worlds[self.player].location_name_to_id)
world.multiworld.regions.append(self)
@ -38,7 +38,7 @@ class MessengerRegion(Region):
class MessengerLocation(Location):
game = "The Messenger"
def __init__(self, name: str, parent: MessengerRegion, loc_id: Optional[int]):
def __init__(self, name: str, parent: MessengerRegion, loc_id: Optional[int]) -> None:
super().__init__(parent.player, name, loc_id, parent)
if loc_id is None:
self.place_locked_item(MessengerItem(name, parent.player, None))
@ -47,8 +47,8 @@ class MessengerLocation(Location):
class MessengerItem(Item):
game = "The Messenger"
def __init__(self, name: str, player: int, item_id: Optional[int] = None):
if name in {*NOTES, *PROG_ITEMS, *PHOBEKINS} or item_id is None:
def __init__(self, name: str, player: int, item_id: Optional[int] = None, override_progression: bool = False) -> None:
if name in {*NOTES, *PROG_ITEMS, *PHOBEKINS} or item_id is None or override_progression:
item_class = ItemClassification.progression
elif name in USEFUL_ITEMS:
item_class = ItemClassification.useful

View File

@ -3,10 +3,10 @@ from typing import Dict, Any, List, Optional
from BaseClasses import Tutorial, ItemClassification
from worlds.AutoWorld import World, WebWorld
from .Constants import NOTES, PROG_ITEMS, PHOBEKINS, USEFUL_ITEMS, ALWAYS_LOCATIONS, SEALS, ALL_ITEMS
from .Options import messenger_options, NotesNeeded, Goal, PowerSeals
from .Options import messenger_options, NotesNeeded, Goal, PowerSeals, Logic
from .Regions import REGIONS, REGION_CONNECTIONS
from .Rules import MessengerRules
from .SubClasses import MessengerRegion, MessengerItem
from . import Rules
class MessengerWeb(WebWorld):
@ -100,7 +100,15 @@ class MessengerWorld(World):
self.multiworld.itempool += itempool
def set_rules(self) -> None:
MessengerRules(self).set_messenger_rules()
logic = self.multiworld.logic_level[self.player]
if logic == Logic.option_normal:
Rules.MessengerRules(self).set_messenger_rules()
elif logic == Logic.option_hard:
Rules.MessengerHardRules(self).set_messenger_rules()
elif logic == Logic.option_challenging:
Rules.MessengerChallengeRules(self).set_messenger_rules()
else:
Rules.MessengerOOBRules(self).set_messenger_rules()
def fill_slot_data(self) -> Dict[str, Any]:
locations: Dict[int, List[str]] = {}
@ -114,7 +122,8 @@ class MessengerWorld(World):
"music_box": self.multiworld.music_box[self.player].value,
"required_seals": self.required_seals,
"locations": locations,
"settings": {"Difficulty": "Basic" if not self.multiworld.shuffle_seals[self.player] else "Advanced"}
"settings": {"Difficulty": "Basic" if not self.multiworld.shuffle_seals[self.player] else "Advanced"},
"logic": self.multiworld.logic_level[self.player].current_key,
}
def get_filler_item_name(self) -> str:
@ -122,4 +131,6 @@ class MessengerWorld(World):
def create_item(self, name: str) -> MessengerItem:
item_id: Optional[int] = self.item_name_to_id.get(name, None)
return MessengerItem(name, self.player, item_id)
override_prog = name in {"Windmill Shuriken"} and getattr(self, "multiworld") is not None \
and self.multiworld.logic_level[self.player] > Logic.option_normal
return MessengerItem(name, self.player, item_id, override_prog)

View File

@ -1,6 +1,5 @@
from . import MessengerTestBase
from ..Constants import NOTES, PHOBEKINS
from ..Options import MessengerAccessibility
class AccessTest(MessengerTestBase):
@ -46,22 +45,22 @@ class AccessTest(MessengerTestBase):
"Glacial Peak Seal - Ice Climbers", "Tower of Time Seal - Time Waster Seal",
"Underworld Seal - Rising Fanta", "Key of Symbiosis",
"Elemental Skylands Seal - Water", "Elemental Skylands Seal - Fire", "Candle",
"Ninja Village Seal - Tree House", "Climbing Claws", "Key of Hope",
"Autumn Hills Seal - Trip Saws", "Autumn Hills Seal - Double Swing Saws",
"Autumn Hills Seal - Spike Ball Swing", "Autumn Hills Seal - Spike Ball Darts", "Necro",
"Ruxxtin's Amulet", "Catacombs Seal - Triple Spike Crushers", "Catacombs Seal - Crusher Gauntlet",
"Climbing Claws", "Key of Hope", "Autumn Hills Seal - Trip Saws",
"Autumn Hills Seal - Double Swing Saws", "Autumn Hills Seal - Spike Ball Swing",
"Autumn Hills Seal - Spike Ball Darts", "Necro", "Ruxxtin's Amulet",
"Catacombs Seal - Triple Spike Crushers", "Catacombs Seal - Crusher Gauntlet",
"Catacombs Seal - Dirty Pond", "Claustro", "Acro", "Bamboo Creek Seal - Spike Crushers and Doors",
"Bamboo Creek Seal - Spike Ball Pits", "Bamboo Creek Seal - Spike Crushers and Doors v2",
"Howling Grotto Seal - Crushing Pits", "Howling Grotto Seal - Windy Saws and Balls",
"Tower of Time Seal - Lantern Climb", "Demon King Crown", "Cloud Ruins Seal - Ghost Pit",
"Cloud Ruins Seal - Toothbrush Alley", "Cloud Ruins Seal - Saw Pit", "Cloud Ruins Seal - Money Farm Room",
"Demon King Crown", "Cloud Ruins Seal - Ghost Pit", "Cloud Ruins Seal - Toothbrush Alley",
"Cloud Ruins Seal - Saw Pit", "Cloud Ruins Seal - Money Farm Room",
"Tower of Time Seal - Lantern Climb", "Tower of Time Seal - Arcane Orbs",
"Underworld Seal - Sharp and Windy Climb", "Underworld Seal - Fireball Wave",
"Elemental Skylands Seal - Air", "Forlorn Temple Seal - Rocket Maze", "Forlorn Temple Seal - Rocket Sunset",
"Power Thistle", "Key of Strength", "Glacial Peak Seal - Projectile Spike Pit",
"Glacial Peak Seal - Glacial Air Swag", "Fairy Bottle", "Riviere Turquoise Seal - Flower Power",
"Searing Crags Seal - Triple Ball Spinner", "Searing Crags Seal - Raining Rocks",
"Searing Crags Seal - Rhythm Rocks", "Astral Seed", "Astral Tea Leaves"]
"Searing Crags Seal - Rhythm Rocks", "Astral Seed", "Astral Tea Leaves", "Rescue Phantom"]
items = [["Wingsuit", "Rope Dart"]]
self.assertAccessDependency(locations, items)
@ -116,8 +115,8 @@ class AccessTest(MessengerTestBase):
class ItemsAccessTest(MessengerTestBase):
options = {
"shuffle_seals": False,
"accessibility": MessengerAccessibility.option_items
"shuffle_seals": "false",
"accessibility": "items"
}
def testSelfLockingItems(self) -> None:
@ -136,14 +135,3 @@ class ItemsAccessTest(MessengerTestBase):
with self.subTest("Fulfills Accessibility", location=loc, item=item_name):
self.assertTrue(self.multiworld.get_location(loc, self.player).can_fill(self.multiworld.state, item, True))
class NoLogicTest(MessengerTestBase):
options = {
"enable_logic": "false"
}
def testNoLogic(self) -> None:
"""Test some funny locations to make sure they aren't reachable but we can still win"""
self.assertEqual(self.can_reach_location("Pyro"), False)
self.assertEqual(self.can_reach_location("Rescue Phantom"), False)
self.assertBeatable(True)

View File

@ -0,0 +1,107 @@
from BaseClasses import ItemClassification
from . import MessengerTestBase
class HardLogicTest(MessengerTestBase):
options = {
"logic_level": "hard"
}
def testVertical(self) -> None:
"""Test the locations that still require wingsuit or rope dart."""
locations = [
# tower of time
"Tower of Time Seal - Time Waster Seal", "Tower of Time Seal - Lantern Climb",
"Tower of Time Seal - Arcane Orbs",
# ninja village
"Candle", "Astral Seed", "Ninja Village Seal - Tree House",
# autumn hills
"Climbing Claws", "Key of Hope",
"Autumn Hills Seal - Trip Saws", "Autumn Hills Seal - Double Swing Saws",
"Autumn Hills Seal - Spike Ball Swing", "Autumn Hills Seal - Spike Ball Darts",
# forlorn temple
"Demon King Crown",
"Forlorn Temple Seal - Rocket Maze", "Forlorn Temple Seal - Rocket Sunset",
# catacombs
"Necro", "Ruxxtin's Amulet",
"Catacombs Seal - Triple Spike Crushers", "Catacombs Seal - Crusher Gauntlet", "Catacombs Seal - Dirty Pond",
# bamboo creek
"Claustro",
"Bamboo Creek Seal - Spike Crushers and Doors", "Bamboo Creek Seal - Spike Ball Pits",
"Bamboo Creek Seal - Spike Crushers and Doors v2",
# howling grotto
"Howling Grotto Seal - Crushing Pits", "Howling Grotto Seal - Crushing Pits",
# glacial peak
"Glacial Peak Seal - Ice Climbers",
# cloud ruins
"Acro", "Cloud Ruins Seal - Ghost Pit",
"Cloud Ruins Seal - Toothbrush Alley", "Cloud Ruins Seal - Saw Pit", "Cloud Ruins Seal - Money Farm Room",
# underworld
"Underworld Seal - Rising Fanta", "Underworld Seal - Sharp and Windy Climb",
# riviere turquoise
"Fairy Bottle", "Riviere Turquoise Seal - Flower Power",
# elemental skylands
"Elemental Skylands Seal - Air", "Elemental Skylands Seal - Water", "Elemental Skylands Seal - Fire",
# phantom
"Rescue Phantom",
]
items = [["Wingsuit", "Rope Dart"]]
self.assertAccessDependency(locations, items)
def testWindmill(self) -> None:
"""Windmill Shuriken isn't progression on normal difficulty, so test it's marked correctly and required."""
self.assertEqual(ItemClassification.progression, self.get_item_by_name("Windmill Shuriken").classification)
windmill_locs = [
"Key of Strength",
"Key of Symbiosis",
"Underworld Seal - Fireball Wave"
]
for loc in windmill_locs:
with self.subTest("can't reach location with nothing", location=loc):
self.assertFalse(self.can_reach_location(loc))
items = self.get_items_by_name(["Windmill Shuriken", "Ninja Tabi", "Fairy Bottle"])
self.collect(items)
for loc in windmill_locs:
with self.subTest("can reach with Windmill", location=loc):
self.assertTrue(self.can_reach_location(loc))
special_loc = "Autumn Hills Seal - Spike Ball Darts"
item = self.get_item_by_name("Wingsuit")
self.collect(item)
self.assertTrue(self.can_reach_location(special_loc))
self.remove(item)
item = self.get_item_by_name("Rope Dart")
self.collect(item)
self.assertTrue(self.can_reach_location(special_loc))
class ChallengingLogicTest(MessengerTestBase):
options = {
"logic_level": "challenging"
}
class NoLogicTest(MessengerTestBase):
options = {
"logic_level": "oob"
}
def testAccess(self) -> None:
"""Test the locations with rules still require things."""
all_locations = [
"Claustro", "Key of Strength", "Key of Symbiosis", "Key of Love", "Pyro", "Key of Chaos", "Key of Courage",
"Autumn Hills Seal - Spike Ball Darts", "Ninja Village Seal - Tree House", "Underworld Seal - Fireball Wave",
"Tower of Time Seal - Time Waster Seal", "Rescue Phantom", "Elemental Skylands Seal - Air",
"Elemental Skylands Seal - Water", "Elemental Skylands Seal - Fire",
]
for loc in all_locations:
with self.subTest("Default unreachables", location=loc):
self.assertFalse(self.can_reach_location(loc))
def testNoLogic(self) -> None:
"""Test some funny locations to make sure they aren't reachable, but we can still win"""
self.assertEqual(self.can_reach_location("Pyro"), False)
self.assertEqual(self.can_reach_location("Rescue Phantom"), False)
self.assertBeatable(True)

View File

@ -27,4 +27,9 @@ class DefaultGoalTest(MessengerTestBase):
def testGoal(self) -> None:
self.assertBeatable(False)
self.collect_by_name(NOTES)
rope_dart = self.get_item_by_name("Rope Dart")
self.collect(rope_dart)
self.assertBeatable(True)
self.remove(rope_dart)
self.collect_by_name("Wingsuit")
self.assertBeatable(True)

View File

@ -4,11 +4,11 @@ from . import MessengerTestBase
class NoLogicTest(MessengerTestBase):
options = {
"enable_logic": "false",
"logic_level": "oob",
"goal": "power_seal_hunt",
}
def testChestAccess(self):
def testChestAccess(self) -> None:
"""Test to make sure we can win even though we can't reach the chest."""
self.assertEqual(self.can_reach_location("Shop Chest"), False)
self.assertBeatable(True)