The Messenger: Transition Shuffle (#4402)

* The Messenger: transition rando

* remove unused import

* always link both directions for plando when using coupled transitions

* er_type was renamed to randomization_type

* use frozenset for things that shouldn't change

* review suggestions

* do portal and transition shuffle in `connect_entrances`

* remove some unnecessary connections that were causing entrance caching collisions

* add test for strictest possible ER settings

* use unittest.skip on the skipped test, so we don't waste time doing setUp and tearDown

* use the world helpers

* make the plando connection description more verbose

* always add searing crags portal if portal shuffle is disabled

* guarantee an arbitrary number of locations with first connection

* make the constraints more lenient for a bit more variety
This commit is contained in:
Aaron Wagener
2025-03-10 10:16:09 -05:00
committed by GitHub
parent 4882366ffc
commit 7c30c4a169
7 changed files with 258 additions and 75 deletions

View File

@@ -1,7 +1,7 @@
import logging
from typing import Any, ClassVar, TextIO
from BaseClasses import CollectionState, Entrance, Item, ItemClassification, MultiWorld, Tutorial
from BaseClasses import CollectionState, Entrance, EntranceType, Item, ItemClassification, MultiWorld, Tutorial
from Options import Accessibility
from Utils import output_path
from settings import FilePath, Group
@@ -17,6 +17,7 @@ from .regions import LEVELS, MEGA_SHARDS, LOCATIONS, REGION_CONNECTIONS
from .rules import MessengerHardRules, MessengerOOBRules, MessengerRules
from .shop import FIGURINES, PROG_SHOP_ITEMS, SHOP_ITEMS, USEFUL_SHOP_ITEMS, shuffle_shop_prices
from .subclasses import MessengerEntrance, MessengerItem, MessengerRegion, MessengerShopLocation
from .transitions import shuffle_transitions
components.append(
Component("The Messenger", component_type=Type.CLIENT, func=launch_game, game_name="The Messenger", supports_uri=True)
@@ -128,7 +129,7 @@ class MessengerWorld(World):
spoiler_portal_mapping: dict[str, str]
portal_mapping: list[int]
transitions: list[Entrance]
reachable_locs: int = 0
reachable_locs: bool = False
filler: dict[str, int]
def generate_early(self) -> None:
@@ -145,13 +146,13 @@ class MessengerWorld(World):
self.shop_prices, self.figurine_prices = shuffle_shop_prices(self)
starting_portals = ["Autumn Hills", "Howling Grotto", "Glacial Peak", "Riviere Turquoise", "Sunken Shrine", "Searing Crags"]
starting_portals = ["Autumn Hills", "Howling Grotto", "Glacial Peak", "Riviere Turquoise", "Sunken Shrine",
"Searing Crags"]
self.starting_portals = [f"{portal} Portal"
for portal in starting_portals[:3] +
self.random.sample(starting_portals[3:], k=self.options.available_portals - 3)]
# super complicated method for adding searing crags to starting portals if it wasn't chosen
# TODO add a check for transition shuffle when that gets added back in
if not self.options.shuffle_portals and "Searing Crags Portal" not in self.starting_portals:
self.starting_portals.append("Searing Crags Portal")
portals_to_strip = [portal for portal in ["Riviere Turquoise Portal", "Sunken Shrine Portal"]
@@ -181,7 +182,7 @@ class MessengerWorld(World):
region_name = region.name.removeprefix(f"{region.parent} - ")
connection_data = CONNECTIONS[region.parent][region_name]
for exit_region in connection_data:
region.connect(self.multiworld.get_region(exit_region, self.player))
region.connect(self.get_region(exit_region))
# all regions need to be created before i can do these connections so we create and connect the complex first
for region in [level for level in simple_regions if level.name in REGION_CONNECTIONS]:
@@ -256,6 +257,7 @@ class MessengerWorld(World):
f" {logic} for {self.multiworld.get_player_name(self.player)}")
# MessengerOOBRules(self).set_messenger_rules()
def connect_entrances(self) -> None:
add_closed_portal_reqs(self)
# i need portal shuffle to happen after rules exist so i can validate it
attempts = 5
@@ -271,6 +273,9 @@ class MessengerWorld(World):
else:
raise RuntimeError("Unable to generate valid portal output.")
if self.options.shuffle_transitions:
shuffle_transitions(self)
def write_spoiler_header(self, spoiler_handle: TextIO) -> None:
if self.options.available_portals < 6:
spoiler_handle.write(f"\nStarting Portals:\n\n")
@@ -286,9 +291,54 @@ class MessengerWorld(World):
key=lambda portal:
["Autumn Hills", "Riviere Turquoise",
"Howling Grotto", "Sunken Shrine",
"Searing Crags", "Glacial Peak"].index(portal[0]))
"Searing Crags", "Glacial Peak"].index(portal[0])
)
for portal, output in portal_info:
spoiler.set_entrance(f"{portal} Portal", output, "I can write anything I want here lmao", self.player)
spoiler.set_entrance(f"{portal} Portal", output, "", self.player)
if self.options.shuffle_transitions:
for transition in self.transitions:
if (transition.randomization_type == EntranceType.TWO_WAY
and (transition.connected_region.name, "both", self.player) in spoiler.entrances):
continue
spoiler.set_entrance(
transition.name if "->" not in transition.name else transition.parent_region.name,
transition.connected_region.name,
"both" if transition.randomization_type == EntranceType.TWO_WAY
and self.options.shuffle_transitions == ShuffleTransitions.option_coupled else "",
self.player
)
def extend_hint_information(self, hint_data: dict[int, dict[int, str]]) -> None:
if not self.options.shuffle_transitions:
return
hint_data.update({self.player: {}})
all_state = self.multiworld.get_all_state(True)
# sometimes some of my regions aren't in path for some reason?
all_state.update_reachable_regions(self.player)
paths = all_state.path
start = self.get_region("Tower HQ")
start_connections = [entrance.name for entrance in start.exits if entrance not in {"Home", "Shrink Down"}]
transition_names = [transition.name for transition in self.transitions] + start_connections
for loc in self.get_locations():
if (loc.parent_region.name in {"Tower HQ", "The Shop", "Music Box", "The Craftsman's Corner"}
or loc.address is None):
continue
path_to_loc: list[str] = []
name, connection = paths.get(loc.parent_region, (None, None))
while connection != ("Menu", None) and name is not None:
name, connection = connection
if name in transition_names:
if name in start_connections:
name = f"{name} -> {self.get_entrance(name).connected_region.name}"
path_to_loc.append(name)
text = " => ".join(reversed(path_to_loc))
if not text:
continue
hint_data[self.player][loc.address] = text
def fill_slot_data(self) -> dict[str, Any]:
slot_data = {
@@ -308,11 +358,13 @@ class MessengerWorld(World):
def get_filler_item_name(self) -> str:
if not getattr(self, "_filler_items", None):
self._filler_items = [name for name in self.random.choices(
list(self.filler),
weights=list(self.filler.values()),
k=20
)]
self._filler_items = [
name for name in self.random.choices(
list(self.filler),
weights=list(self.filler.values()),
k=20
)
]
return self._filler_items.pop(0)
def create_item(self, name: str) -> MessengerItem:
@@ -331,7 +383,7 @@ class MessengerWorld(World):
self.total_shards += count
return ItemClassification.progression_skip_balancing if count else ItemClassification.filler
if name == "Windmill Shuriken" and getattr(self, "multiworld", None) is not None:
if name == "Windmill Shuriken":
return ItemClassification.progression if self.options.logic_level else ItemClassification.filler
if name == "Power Seal":
@@ -344,7 +396,7 @@ class MessengerWorld(World):
if name in {*USEFUL_ITEMS, *USEFUL_SHOP_ITEMS}:
return ItemClassification.useful
if name in TRAPS:
return ItemClassification.trap
@@ -354,7 +406,7 @@ class MessengerWorld(World):
def create_group(cls, multiworld: "MultiWorld", new_player_id: int, players: set[int]) -> World:
group = super().create_group(multiworld, new_player_id, players)
assert isinstance(group, MessengerWorld)
group.filler = FILLER.copy()
group.options.traps.value = all(multiworld.worlds[player].options.traps for player in players)
if group.options.traps: