Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 10 additions & 10 deletions worlds/messenger/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from .connections import CONNECTIONS, RANDOMIZED_CONNECTIONS, TRANSITIONS
from .constants import ALL_ITEMS, ALWAYS_LOCATIONS, BOSS_LOCATIONS, FILLER, NOTES, PHOBEKINS, PROG_ITEMS, TRAPS, \
USEFUL_ITEMS
from .options import AvailablePortals, Goal, Logic, MessengerOptions, NotesNeeded, option_groups, ShuffleTransitions
from .options import AvailablePortals, Goal, Logic, MessengerOptions, NotesNeeded, option_groups, ShuffleTransitions, ShufflePortals
from .portals import PORTALS, add_closed_portal_reqs, disconnect_portals, shuffle_portals, validate_portals
from .regions import LEVELS, MEGA_SHARDS, LOCATIONS, REGION_CONNECTIONS
from .rules import MessengerHardRules, MessengerOOBRules, MessengerRules
Expand Down Expand Up @@ -213,10 +213,12 @@ def create_regions(self) -> None:
for reg_name in sub_region]

for region in complex_regions:
region_name = region.name.removeprefix(f"{region.parent} - ")
connection_data = CONNECTIONS[region.parent][region_name]
parent_name = region.parent
region_name = region.name.removeprefix(f"{parent_name} - ")
connection_data: list[str] = CONNECTIONS[parent_name][region_name]
for exit_region in connection_data:
region.connect(self.get_region(exit_region))
connection_name = region.name + " exit" if not exit_region.startswith(parent_name) else None
region.connect(self.get_region(exit_region), name=connection_name)

# 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]:
Expand Down Expand Up @@ -298,15 +300,14 @@ def set_rules(self) -> None:
def connect_entrances(self) -> None:
if self.options.shuffle_transitions:
disconnect_entrances(self)
keep_entrance_logic = False

if hasattr(self.multiworld, "re_gen_passthrough"):
slot_data = self.multiworld.re_gen_passthrough.get(self.game)
if slot_data:
self.multiworld.plando_options |= PlandoOptions.connections
self.options.portal_plando.value = reverse_portal_exits_into_portal_plando(slot_data["portal_exits"])
self.options.plando_connections.value = reverse_transitions_into_plando_connections(slot_data["transitions"])
keep_entrance_logic = True


add_closed_portal_reqs(self)
# i need portal shuffle to happen after rules exist so i can validate it
Expand All @@ -324,7 +325,7 @@ def connect_entrances(self) -> None:
raise RuntimeError("Unable to generate valid portal output.")

if self.options.shuffle_transitions:
shuffle_transitions(self, keep_entrance_logic)
shuffle_transitions(self)

def write_spoiler_header(self, spoiler_handle: TextIO) -> None:
if self.options.available_portals < 6:
Expand Down Expand Up @@ -352,7 +353,7 @@ def write_spoiler_header(self, spoiler_handle: TextIO) -> None:
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.name if " exit" 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 "",
Expand Down Expand Up @@ -398,8 +399,7 @@ def fill_slot_data(self) -> dict[str, Any]:
"required_seals": self.required_seals,
"starting_portals": self.starting_portals,
"portal_exits": self.portal_mapping,
"transitions": [[TRANSITIONS.index("Corrupted Future") if transition.name == "Artificer's Portal"
else TRANSITIONS.index(RANDOMIZED_CONNECTIONS[transition.parent_region.name]),
"transitions": [[TRANSITIONS.index(RANDOMIZED_CONNECTIONS[transition.name]),
TRANSITIONS.index(transition.connected_region.name)]
for transition in self.transitions],
**self.options.as_dict("music_box", "death_link", "logic_level"),
Expand Down
93 changes: 55 additions & 38 deletions worlds/messenger/connections.py
Original file line number Diff line number Diff line change
Expand Up @@ -642,43 +642,43 @@

# fmt: off
RANDOMIZED_CONNECTIONS: dict[str, str] = {
"Ninja Village - Right": "Autumn Hills - Left",
"Autumn Hills - Left": "Ninja Village - Right",
"Autumn Hills - Right": "Forlorn Temple - Left",
"Autumn Hills - Bottom": "Catacombs - Bottom Left",
"Forlorn Temple - Left": "Autumn Hills - Right",
"Forlorn Temple - Right": "Bamboo Creek - Top Left",
"Forlorn Temple - Bottom": "Catacombs - Top Left",
"Catacombs - Top Left": "Forlorn Temple - Bottom",
"Catacombs - Bottom Left": "Autumn Hills - Bottom",
"Catacombs - Bottom": "Dark Cave - Right",
"Catacombs - Right": "Bamboo Creek - Bottom Left",
"Bamboo Creek - Bottom Left": "Catacombs - Right",
"Bamboo Creek - Right": "Howling Grotto - Left",
"Bamboo Creek - Top Left": "Forlorn Temple - Right",
"Howling Grotto - Left": "Bamboo Creek - Right",
"Howling Grotto - Top": "Quillshroom Marsh - Bottom Left",
"Howling Grotto - Right": "Quillshroom Marsh - Top Left",
"Howling Grotto - Bottom": "Sunken Shrine - Left",
"Quillshroom Marsh - Top Left": "Howling Grotto - Right",
"Quillshroom Marsh - Bottom Left": "Howling Grotto - Top",
"Quillshroom Marsh - Top Right": "Searing Crags - Left",
"Quillshroom Marsh - Bottom Right": "Searing Crags - Bottom",
"Searing Crags - Left": "Quillshroom Marsh - Top Right",
"Searing Crags - Top": "Glacial Peak - Bottom",
"Searing Crags - Bottom": "Quillshroom Marsh - Bottom Right",
"Searing Crags - Right": "Underworld - Left",
"Glacial Peak - Bottom": "Searing Crags - Top",
"Glacial Peak - Top": "Cloud Ruins - Left",
"Glacial Peak - Left": "Elemental Skylands - Air Shmup",
"Cloud Ruins - Left": "Glacial Peak - Top",
"Elemental Skylands - Right": "Glacial Peak - Left",
"Tower HQ": "Tower of Time - Left",
"Artificer": "Corrupted Future",
"Underworld - Left": "Searing Crags - Right",
"Dark Cave - Right": "Catacombs - Bottom",
"Dark Cave - Left": "Riviere Turquoise - Right",
"Sunken Shrine - Left": "Howling Grotto - Bottom",
"Ninja Village - Right exit": "Autumn Hills - Left",
"Autumn Hills - Left exit": "Ninja Village - Right",
"Autumn Hills - Right exit": "Forlorn Temple - Left",
"Autumn Hills - Bottom exit": "Catacombs - Bottom Left",
"Forlorn Temple - Left exit": "Autumn Hills - Right",
"Forlorn Temple - Right exit": "Bamboo Creek - Top Left",
"Forlorn Temple - Bottom exit": "Catacombs - Top Left",
"Catacombs - Top Left exit": "Forlorn Temple - Bottom",
"Catacombs - Bottom Left exit": "Autumn Hills - Bottom",
"Catacombs - Bottom exit": "Dark Cave - Right",
"Catacombs - Right exit": "Bamboo Creek - Bottom Left",
"Bamboo Creek - Bottom Left exit": "Catacombs - Right",
"Bamboo Creek - Right exit": "Howling Grotto - Left",
"Bamboo Creek - Top Left exit": "Forlorn Temple - Right",
"Howling Grotto - Left exit": "Bamboo Creek - Right",
"Howling Grotto - Top exit": "Quillshroom Marsh - Bottom Left",
"Howling Grotto - Right exit": "Quillshroom Marsh - Top Left",
"Howling Grotto - Bottom exit": "Sunken Shrine - Left",
"Quillshroom Marsh - Top Left exit": "Howling Grotto - Right",
"Quillshroom Marsh - Bottom Left exit": "Howling Grotto - Top",
"Quillshroom Marsh - Top Right exit": "Searing Crags - Left",
"Quillshroom Marsh - Bottom Right exit": "Searing Crags - Bottom",
"Searing Crags - Left exit": "Quillshroom Marsh - Top Right",
"Searing Crags - Top exit": "Glacial Peak - Bottom",
"Searing Crags - Bottom exit": "Quillshroom Marsh - Bottom Right",
"Searing Crags - Right exit": "Underworld - Left",
"Glacial Peak - Bottom exit": "Searing Crags - Top",
"Glacial Peak - Top exit": "Cloud Ruins - Left",
"Glacial Peak - Left exit": "Elemental Skylands - Air Shmup",
"Cloud Ruins - Left exit": "Glacial Peak - Top",
"Elemental Skylands - Right exit": "Glacial Peak - Left",
"Artificer's Challenge": "Tower of Time - Left",
"Artificer's Portal": "Corrupted Future",
"Underworld - Left exit": "Searing Crags - Right",
"Dark Cave - Right exit": "Catacombs - Bottom",
"Dark Cave - Left exit": "Riviere Turquoise - Right",
"Sunken Shrine - Left exit": "Howling Grotto - Bottom",
}
# fmt: on

Expand Down Expand Up @@ -718,9 +718,26 @@
"Glacial Peak - Left",
"Elemental Skylands - Air Shmup",
"Elemental Skylands - Right",
"Tower HQ",
"Artificer's Challenge",
"Tower of Time - Left",
"Corrupted Future",
"Cloud Ruins - Left",
"Underworld - Left",
"Artificer's Portal",
]

ONE_WAY_EXITS = {
"Glacial Peak - Left exit",
"Artificer's Portal",
"Artificer's Challenge",
"Dark Cave - Left exit",
"Elemental Skylands - Right exit"
}

ONE_WAY_ENTRANCES = {
"Elemental Skylands - Air Shmup",
"Corrupted Future",
"Tower of Time - Left",
"Riviere Turquoise - Right",
"Glacial Peak - Left"
}
16 changes: 8 additions & 8 deletions worlds/messenger/docs/plando_en.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,22 +64,21 @@ will exit to the Sunny Day checkpoint, and the Searing Crags Portal will exit to
## Transition Plando

This option allows you to specify certain connections when using transition shuffle. This will only work if
transition shuffle and the `connections` plando host setting are enabled.
transition shuffle and the `connections` plando host setting are enabled.

Each transition connection is plandoed by specifying its attributes:

* `entrance` is where you will enter this transition from.
* `entrance` is the name of the transition (level exit).
* `exit` is where the transition will lead.
* `percentage` is the chance this connection will happen at all.
* `direction` is used to specify whether this connection will also go in reverse. This entry will be ignored if the
transition shuffle is set to `coupled` or if the specified connection can only occur in one direction, such as exiting
to Riviere Turquoise. The default direction is "both", which will make it so that returning through the exit
transition will return you to where you entered it from. "entrance" and "exit" are treated the same, with them both
making this transition only one-way.
transition will return you to where you entered it from.

Valid connections can be found in the [`RANDOMIZED_CONNECTIONS` dictionary](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/messenger/connections.py#L640).
The keys (left) are entrances, and values (right) are exits. Whether you want the connection to go both ways or not,
both sides must either be two-way or one-way; E.g. connecting Artificer (Corrupted Future Portal) to one of the
both sides must either be two-way or one-way; E.g. connecting Artificer's Portal (Corrupted Future Portal) to one of the
Quillshroom Marsh entrances is not a valid pairing. A pairing can be determined to be two-way if both the entrance and
exit of that pair are an exit and entrance of another pairing, respectively.

Expand All @@ -88,14 +87,15 @@ exit of that pair are an exit and entrance of another pairing, respectively.
```yaml
The Messenger:
plando_connections:
- entrance: Searing Crags - Top
- entrance: Searing Crags - Top exit
exit: Dark Cave - Right
- entrance: Glacial Peak - Left
- entrance: Glacial Peak - Left exit
exit: Corrupted Future
```

This block will create the following connections:

1. Leaving Searing Crags towards Glacial Peak will take you to the beginning of Dark Cave, and leaving the Dark Cave
door will return you to the top of Searing Crags.
2. Taking Manfred to leave Glacial Peak, will take you to Corrupted Future. There is no reverse connection here so it
will always be one-way.
will always be one-way. Taking the Corrupted Future portals sends you to the HQ.
10 changes: 5 additions & 5 deletions worlds/messenger/rules.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,11 +129,11 @@ def __init__(self, world: "MessengerWorld") -> None:
# Glacial Peak
"Glacial Peak - Portal -> Glacial Peak - Tower Entrance Shop":
self.has_vertical,
"Glacial Peak - Left -> Elemental Skylands - Air Shmup":
"Glacial Peak - Left exit":
lambda state: state.has("Magic Firefly", self.player)
and state.multiworld.get_location("Quillshroom Marsh - Queen of Quills", self.player)
.can_reach(state),
"Glacial Peak - Top -> Cloud Ruins - Left":
"Glacial Peak - Top exit":
lambda state: state.has("Ruxxtin's Amulet", self.player),
"Glacial Peak - Projectile Spike Pit Checkpoint -> Glacial Peak - Left":
lambda state: self.has_dart(state) or (self.can_dboost(state) and self.has_wingsuit(state)),
Expand Down Expand Up @@ -200,7 +200,7 @@ def __init__(self, world: "MessengerWorld") -> None:
# Riviere Turquoise
"Riviere Turquoise - Waterfall Shop -> Riviere Turquoise - Flower Flight Checkpoint":
lambda state: self.has_dart(state) or (
self.has_wingsuit(state) and self.can_destroy_projectiles(state)),
self.has_wingsuit(state) and self.can_destroy_projectiles(state)),
"Riviere Turquoise - Launch of Faith Shop -> Riviere Turquoise - Flower Flight Checkpoint":
lambda state: self.has_dart(state) and self.can_dboost(state),
"Riviere Turquoise - Flower Flight Checkpoint -> Riviere Turquoise - Waterfall Shop":
Expand Down Expand Up @@ -233,7 +233,7 @@ def __init__(self, world: "MessengerWorld") -> None:
"Howling Grotto - Breezy Crushers Checkpoint -> Howling Grotto - Crushing Pits Shop": [
self.world.get_region("Howling Grotto - Emerald Golem Shop")
],
"Glacial Peak - Left -> Elemental Skylands - Air Shmup": [
"Glacial Peak - Left exit": [
self.world.get_location("Quillshroom Marsh - Queen of Quills").parent_region
],
}
Expand Down Expand Up @@ -432,7 +432,7 @@ def __init__(self, world: "MessengerWorld") -> None:
"Searing Crags - Before Final Climb Shop -> Searing Crags - Colossuses Shop":
self.true,
# Glacial Peak
"Glacial Peak - Left -> Elemental Skylands - Air Shmup":
"Glacial Peak - Left exit":
lambda state: self.has_windmill(state) or
(state.has("Magic Firefly", self.player) and
state.multiworld.get_location(
Expand Down
49 changes: 49 additions & 0 deletions worlds/messenger/test/test_entrance_randomization.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import unittest
from typing import ClassVar

from test.param import classvar_matrix
from . import MessengerTestBase
from ..connections import RANDOMIZED_CONNECTIONS, TRANSITIONS, ONE_WAY_EXITS, ONE_WAY_ENTRANCES


class StrictEntranceRandoTest(MessengerTestBase):
Expand All @@ -17,3 +20,49 @@ class StrictEntranceRandoTest(MessengerTestBase):
def test_all_state_can_reach_everything(self) -> None:
"""It's not possible to reach everything with these options so skip this test."""
pass


@classvar_matrix(entrance=ONE_WAY_EXITS, exit=ONE_WAY_ENTRANCES)
class OneWayTransitionPlandoTest(MessengerTestBase):
entrance: ClassVar[str]
exit: ClassVar[str]

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.options = {
"shuffle_transitions": "coupled",
"plando_connections": [
{"entrance": self.entrance, "exit": self.exit, }
],
}

@property
def run_default_tests(self) -> bool:
return False

def test_validate_plando(self) -> None:
entrance = self.world.get_entrance(self.entrance)
self.assertIsNotNone(entrance.connected_region)
self.assertEqual(self.exit, entrance.connected_region.name)


class ConnectionsConstantTest(MessengerTestBase):

@property
def run_default_tests(self) -> bool:
return False

def test_all_transitions_exist(self) -> None:
for transition in sorted(RANDOMIZED_CONNECTIONS.keys()):
with self.subTest(transition=transition):
self.assertIn(transition.replace(" exit", ""), TRANSITIONS)
entrance = self.world.get_entrance(transition)
self.assertIsNotNone(entrance)

for transition in sorted(RANDOMIZED_CONNECTIONS.values()):
with self.subTest(transition=transition):
self.assertIn(transition, TRANSITIONS)

if transition not in ONE_WAY_ENTRANCES:
entrance = self.world.get_entrance(transition + " exit")
self.assertIsNotNone(entrance)
Loading
Loading