Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

The Messenger: more optimizations #2451

Merged
20 changes: 6 additions & 14 deletions worlds/messenger/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,13 +62,12 @@ class MessengerWorld(World):
"Money Wrench",
], base_offset)}

data_version = 3
required_client_version = (0, 4, 0)
required_client_version = (0, 4, 1)

web = MessengerWeb()

total_seals: int = 0
required_seals: int = 0
required_seals: int = 1
alwaysintreble marked this conversation as resolved.
Show resolved Hide resolved
total_shards: int = 0
shop_prices: Dict[str, int]
figurine_prices: Dict[str, int]
Expand Down Expand Up @@ -148,19 +147,12 @@ def set_rules(self) -> None:
MessengerOOBRules(self).set_messenger_rules()

def fill_slot_data(self) -> Dict[str, Any]:
shop_prices = {SHOP_ITEMS[item].internal_name: price for item, price in self.shop_prices.items()}
figure_prices = {FIGURINES[item].internal_name: price for item, price in self.figurine_prices.items()}

return {
"deathlink": self.options.death_link.value,
"goal": self.options.goal.current_key,
"music_box": self.options.music_box.value,
"required_seals": self.required_seals,
"mega_shards": self.options.shuffle_shards.value,
"logic": self.options.logic_level.current_key,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it intentional that the key "logic" is being removed and replaced by "logic_level"?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes. this is just for the tracker, and the newer way both reduces the slot data by a tad bit and looks cleaner imo.

"shop": shop_prices,
"figures": figure_prices,
"shop": {SHOP_ITEMS[item].internal_name: price for item, price in self.shop_prices.items()},
"figures": {FIGURINES[item].internal_name: price for item, price in self.figurine_prices.items()},
"max_price": self.total_shards,
"required_seals": self.required_seals,
**self.options.as_dict("music_box", "death_link", "logic_level"),
}

def get_filler_item_name(self) -> str:
Expand Down
4 changes: 3 additions & 1 deletion worlds/messenger/regions.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"Menu": [],
"Tower HQ": [],
"The Shop": [],
"The Craftsman's Corner": [],
"Tower of Time": [],
"Ninja Village": ["Ninja Village - Candle", "Ninja Village - Astral Seed"],
"Autumn Hills": ["Autumn Hills - Climbing Claws", "Autumn Hills - Key of Hope", "Autumn Hills - Leaf Golem"],
Expand Down Expand Up @@ -82,7 +83,8 @@
REGION_CONNECTIONS: Dict[str, Set[str]] = {
"Menu": {"Tower HQ"},
"Tower HQ": {"Autumn Hills", "Howling Grotto", "Searing Crags", "Glacial Peak", "Tower of Time",
"Riviere Turquoise Entrance", "Sunken Shrine", "Corrupted Future", "The Shop", "Music Box"},
"Riviere Turquoise Entrance", "Sunken Shrine", "Corrupted Future", "The Shop",
"The Craftsman's Corner", "Music Box"},
"Autumn Hills": {"Ninja Village", "Forlorn Temple", "Catacombs"},
"Forlorn Temple": {"Catacombs", "Bamboo Creek"},
"Catacombs": {"Autumn Hills", "Bamboo Creek", "Dark Cave"},
Expand Down
42 changes: 19 additions & 23 deletions worlds/messenger/rules.py
Original file line number Diff line number Diff line change
@@ -1,26 +1,29 @@
from typing import Callable, Dict, TYPE_CHECKING

from BaseClasses import CollectionState
from worlds.generic.Rules import add_rule, allow_self_locking_items
from worlds.generic.Rules import add_rule, allow_self_locking_items, CollectionRule
from .constants import NOTES, PHOBEKINS
from .options import MessengerAccessibility

if TYPE_CHECKING:
from . import MessengerWorld
else:
MessengerWorld = object


class MessengerRules:
player: int
world: MessengerWorld
region_rules: Dict[str, Callable[[CollectionState], bool]]
location_rules: Dict[str, Callable[[CollectionState], bool]]
world: "MessengerWorld"
region_rules: Dict[str, CollectionRule]
location_rules: Dict[str, CollectionRule]

def __init__(self, world: MessengerWorld) -> None:
def __init__(self, world: "MessengerWorld") -> None:
self.player = world.player
self.world = world

# these locations are at the top of the shop tree, and the entire shop tree needs to be purchased
maximum_price = (world.multiworld.get_location("The Shop - Demon's Bane", self.player).cost +
world.multiworld.get_location("The Shop - Focused Power Sense", self.player).cost)
self.maximum_price = min(maximum_price, world.total_shards)

self.region_rules = {
"Ninja Village": self.has_wingsuit,
"Autumn Hills": self.has_wingsuit,
Expand All @@ -37,8 +40,8 @@ def __init__(self, world: MessengerWorld) -> None:
"Glacial Peak": self.has_vertical,
"Elemental Skylands": lambda state: state.has("Magic Firefly", self.player) and self.has_wingsuit(state),
"Music Box": lambda state: (state.has_all(set(NOTES), self.player)
or state.has("Power Seal", self.player, max(1, self.world.required_seals)))
and self.has_dart(state),
or self.has_enough_seals(state)) and self.has_dart(state),
"The Craftsman's Corner": lambda state: state.has("Money Wrench", self.player) and self.can_shop(state),
}

self.location_rules = {
Expand Down Expand Up @@ -110,7 +113,7 @@ 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:
return not self.world.required_seals or state.has("Power Seal", self.player, self.world.required_seals)
return state.has("Power Seal", self.player, self.world.required_seals)

def can_destroy_projectiles(self, state: CollectionState) -> bool:
return state.has("Strike of the Ninja", self.player)
Expand All @@ -127,9 +130,7 @@ def true(self, state: CollectionState) -> bool:
return True

def can_shop(self, state: CollectionState) -> bool:
prices = self.world.shop_prices
most_expensive_loc = max(prices, key=prices.get)
return state.can_reach(f"The Shop - {most_expensive_loc}", "Location", self.player)
return state.has("Shards", self.player, self.maximum_price)

def set_messenger_rules(self) -> None:
multiworld = self.world.multiworld
Expand All @@ -141,19 +142,16 @@ def set_messenger_rules(self) -> None:
for loc in region.locations:
if loc.name in self.location_rules:
loc.access_rule = self.location_rules[loc.name]
if region.name == "The Shop":
for loc in region.locations:
loc.access_rule = loc.can_afford

multiworld.completion_condition[self.player] = lambda state: state.has("Rescue Phantom", self.player)
if multiworld.accessibility[self.player]: # not locations accessibility
set_self_locking_items(self.world, self.player)


class MessengerHardRules(MessengerRules):
extra_rules: Dict[str, Callable[[CollectionState], bool]]
extra_rules: Dict[str, CollectionRule]

def __init__(self, world: MessengerWorld) -> None:
def __init__(self, world: "MessengerWorld") -> None:
super().__init__(world)

self.region_rules.update({
Expand Down Expand Up @@ -215,14 +213,14 @@ def set_messenger_rules(self) -> None:


class MessengerOOBRules(MessengerRules):
def __init__(self, world: MessengerWorld) -> None:
def __init__(self, world: "MessengerWorld") -> None:
self.world = world
self.player = world.player

self.region_rules = {
"Elemental Skylands":
lambda state: state.has_any({"Windmill Shuriken", "Wingsuit", "Rope Dart", "Magic Firefly"}, self.player),
"Music Box": lambda state: state.has_all(set(NOTES), self.player)
"Music Box": lambda state: state.has_all(set(NOTES), self.player) or self.has_enough_seals(state),
alwaysintreble marked this conversation as resolved.
Show resolved Hide resolved
}

self.location_rules = {
Expand All @@ -238,16 +236,14 @@ def __init__(self, world: MessengerWorld) -> None:
"Underworld Seal - Fireball Wave": lambda state: state.has_any({"Wingsuit", "Windmill Shuriken"},
self.player),
"Tower of Time Seal - Time Waster": self.has_dart,
"Shop Chest": self.has_enough_seals
alwaysintreble marked this conversation as resolved.
Show resolved Hide resolved
}

def set_messenger_rules(self) -> None:
super().set_messenger_rules()
self.world.multiworld.completion_condition[self.player] = lambda state: True
self.world.options.accessibility.value = MessengerAccessibility.option_minimal


def set_self_locking_items(world: MessengerWorld, player: int) -> None:
def set_self_locking_items(world: "MessengerWorld", player: int) -> None:
multiworld = world.multiworld

# do the ones for seal shuffle on and off first
Expand Down
14 changes: 4 additions & 10 deletions worlds/messenger/subclasses.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@

from BaseClasses import CollectionState, Item, ItemClassification, Location, Region
from .constants import NOTES, PHOBEKINS, PROG_ITEMS, USEFUL_ITEMS
from .options import Goal
from .regions import MEGA_SHARDS, REGIONS, SEALS
from .shop import FIGURINES, PROG_SHOP_ITEMS, SHOP_ITEMS, USEFUL_SHOP_ITEMS

Expand All @@ -19,8 +18,10 @@ def __init__(self, name: str, world: "MessengerWorld") -> None:
if self.name == "The Shop":
shop_locations = {f"The Shop - {shop_loc}": world.location_name_to_id[f"The Shop - {shop_loc}"]
for shop_loc in SHOP_ITEMS}
shop_locations.update(**{figurine: world.location_name_to_id[figurine] for figurine in FIGURINES})
self.add_locations(shop_locations, MessengerShopLocation)
elif self.name == "The Craftsman's Corner":
self.add_locations({figurine: world.location_name_to_id[figurine] for figurine in FIGURINES},
MessengerLocation)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't the figurine locations need some access rules that take their price into account?

Copy link
Collaborator Author

@alwaysintreble alwaysintreble Nov 16, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, because 90% of the time the maximum price to get to the craftsman's corner will be world.total_shards, so adding more rules that need to be checked after that won't do anything but slow it down, and I don't think they're worth adding for the incredibly slim chance that the total shards is actually greater than those shop slots prices, since shards are easy enough to grind for in game. The main reason they're in logic is just because money grinding isn't fun.

elif self.name == "Tower HQ":
locations.append("Money Wrench")
if world.options.shuffle_seals and self.name in SEALS:
Expand All @@ -46,10 +47,6 @@ class MessengerShopLocation(MessengerLocation):
def cost(self) -> int:
name = self.name.replace("The Shop - ", "") # TODO use `remove_prefix` when 3.8 finally gets dropped
world = cast("MessengerWorld", self.parent_region.multiworld.worlds[self.player])
# short circuit figurines which all require demon's bane be purchased, but nothing else
if "Figurine" in name:
return world.figurine_prices[name] +\
cast(MessengerShopLocation, world.multiworld.get_location("The Shop - Demon's Bane", self.player)).cost
shop_data = SHOP_ITEMS[name]
if shop_data.prerequisite:
prereq_cost = 0
Expand All @@ -65,12 +62,9 @@ def cost(self) -> int:
return world.shop_prices[name] + prereq_cost
return world.shop_prices[name]

def can_afford(self, state: CollectionState) -> bool:
def access_rule(self, state: CollectionState) -> bool:
world = cast("MessengerWorld", state.multiworld.worlds[self.player])
can_afford = state.has("Shards", self.player, min(self.cost, world.total_shards))
if "Figurine" in self.name:
can_afford = state.has("Money Wrench", self.player) and can_afford\
and state.can_reach("Money Wrench", "Location", self.player)
return can_afford


Expand Down
1 change: 0 additions & 1 deletion worlds/messenger/test/test_logic.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,4 +111,3 @@ def test_access(self) -> None:
for loc in all_locations:
with self.subTest("Default unreachables", location=loc):
self.assertFalse(self.can_reach_location(loc))
self.assertBeatable(True)
1 change: 0 additions & 1 deletion worlds/messenger/test/test_shop.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,5 @@ def test_costs(self) -> None:
elif loc == "Demon Hive Figurine":
self.assertIn(price, self.options["shop_price_plan"]["Demon Hive Figurine"])

self.assertLessEqual(price, self.multiworld.get_location(loc, self.player).cost)
self.assertTrue(loc in FIGURINES)
self.assertEqual(len(figures), len(FIGURINES))
Loading