Skip to content

Commit

Permalink
The Messenger: Add more difficult logic options (#1550)
Browse files Browse the repository at this point in the history
  • Loading branch information
alwaysintreble authored Mar 21, 2023
1 parent 9150250 commit 1c69fb3
Show file tree
Hide file tree
Showing 9 changed files with 277 additions and 50 deletions.
8 changes: 6 additions & 2 deletions test/TestBase.py
Original file line number Diff line number Diff line change
Expand Up @@ -199,11 +199,15 @@ def assertAccessDependency(self,

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):
Expand Down
18 changes: 14 additions & 4 deletions worlds/messenger/Options.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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,
Expand Down
126 changes: 114 additions & 12 deletions worlds/messenger/Rules.py
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -14,26 +14,28 @@
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,
"Bamboo Creek": self.has_wingsuit,
"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
Expand Down Expand Up @@ -88,8 +90,11 @@ def has_vertical(self, state: CollectionState) -> bool:
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
Expand All @@ -105,14 +110,111 @@ def set_messenger_rules(self) -> None:
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
multiworld.completion_condition[self.player] = lambda state: state.has("Rescue Phantom", self.player)
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")
Expand Down
8 changes: 4 additions & 4 deletions worlds/messenger/SubClasses.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@


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)
Expand All @@ -38,7 +38,7 @@ def add_exits(self, exits: Set[str]) -> None:
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))
Expand All @@ -47,8 +47,8 @@ def __init__(self, name: str, parent: MessengerRegion, loc_id: Optional[int]):
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
Expand Down
21 changes: 16 additions & 5 deletions worlds/messenger/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@
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):
Expand Down Expand Up @@ -100,7 +100,15 @@ def create_items(self) -> None:
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]] = {}
Expand All @@ -114,12 +122,15 @@ def fill_slot_data(self) -> Dict[str, Any]:
"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:
return "Time Shard"

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)
30 changes: 9 additions & 21 deletions worlds/messenger/test/TestAccess.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
from . import MessengerTestBase
from ..Constants import NOTES, PHOBEKINS
from ..Options import MessengerAccessibility


class AccessTest(MessengerTestBase):
Expand Down Expand Up @@ -46,22 +45,22 @@ def testVertical(self) -> None:
"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)

Expand Down Expand Up @@ -116,8 +115,8 @@ def testGoal(self) -> None:

class ItemsAccessTest(MessengerTestBase):
options = {
"shuffle_seals": False,
"accessibility": MessengerAccessibility.option_items
"shuffle_seals": "false",
"accessibility": "items"
}

def testSelfLockingItems(self) -> None:
Expand All @@ -136,14 +135,3 @@ def testSelfLockingItems(self) -> None:
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)
Loading

0 comments on commit 1c69fb3

Please sign in to comment.