mirror of
https://github.com/MarioSpore/Grinch-AP.git
synced 2025-10-21 12:11:33 -06:00
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:
@@ -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:
|
||||
|
@@ -244,14 +244,12 @@ CONNECTIONS: dict[str, dict[str, list[str]]] = {
|
||||
"Bottom Left": [
|
||||
"Howling Grotto - Top",
|
||||
"Quillshroom Marsh - Sand Trap Shop",
|
||||
"Quillshroom Marsh - Bottom Right",
|
||||
],
|
||||
"Top Right": [
|
||||
"Quillshroom Marsh - Queen of Quills Shop",
|
||||
"Searing Crags - Left",
|
||||
],
|
||||
"Bottom Right": [
|
||||
"Quillshroom Marsh - Bottom Left",
|
||||
"Quillshroom Marsh - Sand Trap Shop",
|
||||
"Searing Crags - Bottom",
|
||||
],
|
||||
@@ -639,43 +637,43 @@ CONNECTIONS: dict[str, dict[str, list[str]]] = {
|
||||
}
|
||||
|
||||
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",
|
||||
"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",
|
||||
"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",
|
||||
}
|
||||
|
||||
TRANSITIONS: list[str] = [
|
||||
|
@@ -3,7 +3,8 @@ from dataclasses import dataclass
|
||||
from schema import And, Optional, Or, Schema
|
||||
|
||||
from Options import Choice, DeathLinkMixin, DefaultOnToggle, ItemsAccessibility, OptionDict, PerGameCommonOptions, \
|
||||
PlandoConnections, Range, StartInventoryPool, Toggle, Visibility
|
||||
PlandoConnections, Range, StartInventoryPool, Toggle
|
||||
from . import RANDOMIZED_CONNECTIONS
|
||||
from .portals import CHECKPOINTS, PORTALS, SHOP_POINTS
|
||||
|
||||
|
||||
@@ -30,17 +31,25 @@ class PortalPlando(PlandoConnections):
|
||||
portals = [f"{portal} Portal" for portal in PORTALS]
|
||||
shop_points = [point for points in SHOP_POINTS.values() for point in points]
|
||||
checkpoints = [point for points in CHECKPOINTS.values() for point in points]
|
||||
portal_entrances = PORTALS
|
||||
portal_exits = portals + shop_points + checkpoints
|
||||
entrances = portal_entrances
|
||||
exits = portal_exits
|
||||
|
||||
entrances = frozenset(PORTALS)
|
||||
exits = frozenset(portals + shop_points + checkpoints)
|
||||
|
||||
|
||||
# for back compatibility. To later be replaced with transition plando
|
||||
class HiddenPortalPlando(PortalPlando):
|
||||
visibility = Visibility.none
|
||||
entrances = PortalPlando.entrances
|
||||
exits = PortalPlando.exits
|
||||
class TransitionPlando(PlandoConnections):
|
||||
"""
|
||||
Plando connections to be used with transition shuffle.
|
||||
List of valid connections can be found at https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/messenger/connections.py#L641.
|
||||
Dictionary keys (left) are entrances and values (right) are exits. If transition shuffle is on coupled all plando
|
||||
connections will be coupled. If on decoupled, "entrance" and "exit" will be treated the same, simply making the
|
||||
plando connection one-way from entrance to exit.
|
||||
Example:
|
||||
- entrance: Searing Crags - Top
|
||||
exit: Dark Cave - Right
|
||||
direction: both
|
||||
"""
|
||||
entrances = frozenset(RANDOMIZED_CONNECTIONS.keys())
|
||||
exits = frozenset(RANDOMIZED_CONNECTIONS.values())
|
||||
|
||||
|
||||
class Logic(Choice):
|
||||
@@ -226,7 +235,7 @@ class MessengerOptions(DeathLinkMixin, PerGameCommonOptions):
|
||||
early_meditation: EarlyMed
|
||||
available_portals: AvailablePortals
|
||||
shuffle_portals: ShufflePortals
|
||||
# shuffle_transitions: ShuffleTransitions
|
||||
shuffle_transitions: ShuffleTransitions
|
||||
goal: Goal
|
||||
music_box: MusicBox
|
||||
notes_needed: NotesNeeded
|
||||
@@ -236,4 +245,4 @@ class MessengerOptions(DeathLinkMixin, PerGameCommonOptions):
|
||||
shop_price: ShopPrices
|
||||
shop_price_plan: PlannedShopPrices
|
||||
portal_plando: PortalPlando
|
||||
plando_connections: HiddenPortalPlando
|
||||
plando_connections: TransitionPlando
|
||||
|
@@ -1,7 +1,7 @@
|
||||
from copy import deepcopy
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from BaseClasses import CollectionState, PlandoOptions
|
||||
from BaseClasses import CollectionState
|
||||
from Options import PlandoConnection
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -252,9 +252,7 @@ def shuffle_portals(world: "MessengerWorld") -> None:
|
||||
world.random.shuffle(available_portals)
|
||||
|
||||
plando = world.options.portal_plando.value
|
||||
if not plando:
|
||||
plando = world.options.plando_connections.value
|
||||
if plando and world.multiworld.plando_options & PlandoOptions.connections and not world.plando_portals:
|
||||
if plando and not world.plando_portals:
|
||||
try:
|
||||
handle_planned_portals(plando)
|
||||
# any failure i expect will trigger on available_portals.remove
|
||||
@@ -294,8 +292,8 @@ def disconnect_portals(world: "MessengerWorld") -> None:
|
||||
|
||||
|
||||
def validate_portals(world: "MessengerWorld") -> bool:
|
||||
# if world.options.shuffle_transitions:
|
||||
# return True
|
||||
if world.options.shuffle_transitions:
|
||||
return True
|
||||
new_state = CollectionState(world.multiworld)
|
||||
new_state.update_reachable_regions(world.player)
|
||||
reachable_locs = 0
|
||||
|
@@ -1,7 +1,8 @@
|
||||
from functools import cached_property
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from BaseClasses import CollectionState, Entrance, Item, ItemClassification, Location, Region
|
||||
from BaseClasses import CollectionState, Entrance, EntranceType, Item, ItemClassification, Location, Region
|
||||
from entrance_rando import ERPlacementState
|
||||
from .regions import LOCATIONS, MEGA_SHARDS
|
||||
from .shop import FIGURINES, SHOP_ITEMS
|
||||
|
||||
@@ -12,9 +13,21 @@ if TYPE_CHECKING:
|
||||
class MessengerEntrance(Entrance):
|
||||
world: "MessengerWorld | None" = None
|
||||
|
||||
def can_connect_to(self, other: Entrance, dead_end: bool, state: "ERPlacementState") -> bool:
|
||||
can_connect = super().can_connect_to(other, dead_end, state)
|
||||
world: MessengerWorld = getattr(self, "world", None)
|
||||
if not world or world.reachable_locs or not can_connect:
|
||||
return can_connect
|
||||
empty_state = CollectionState(world.multiworld, True)
|
||||
self.connected_region = other.connected_region
|
||||
empty_state.update_reachable_regions(world.player)
|
||||
world.reachable_locs = any(loc.can_reach(empty_state) and not loc.is_event for loc in world.get_locations())
|
||||
self.connected_region = None
|
||||
return world.reachable_locs and (not state.coupled or self.name != other.name)
|
||||
|
||||
|
||||
class MessengerRegion(Region):
|
||||
parent: str
|
||||
parent: str | None
|
||||
entrance_type = MessengerEntrance
|
||||
|
||||
def __init__(self, name: str, world: "MessengerWorld", parent: str | None = None) -> None:
|
||||
@@ -32,8 +45,9 @@ class MessengerRegion(Region):
|
||||
for shop_loc in SHOP_ITEMS}
|
||||
self.add_locations(shop_locations, MessengerShopLocation)
|
||||
elif name == "The Craftsman's Corner":
|
||||
self.add_locations({figurine: world.location_name_to_id[figurine] for figurine in FIGURINES},
|
||||
MessengerLocation)
|
||||
self.add_locations(
|
||||
{figurine: world.location_name_to_id[figurine] for figurine in FIGURINES},
|
||||
MessengerLocation)
|
||||
elif name == "Tower HQ":
|
||||
locations.append("Money Wrench")
|
||||
|
||||
@@ -57,6 +71,7 @@ class MessengerLocation(Location):
|
||||
|
||||
|
||||
class MessengerShopLocation(MessengerLocation):
|
||||
|
||||
@cached_property
|
||||
def cost(self) -> int:
|
||||
name = self.name.removeprefix("The Shop - ")
|
||||
|
19
worlds/messenger/test/test_entrance_randomization.py
Normal file
19
worlds/messenger/test/test_entrance_randomization.py
Normal file
@@ -0,0 +1,19 @@
|
||||
import unittest
|
||||
|
||||
from . import MessengerTestBase
|
||||
|
||||
|
||||
class StrictEntranceRandoTest(MessengerTestBase):
|
||||
"""Bare-bones world that tests the strictest possible settings to ensure it doesn't crash"""
|
||||
auto_construct = True
|
||||
options = {
|
||||
"limited_movement": 1,
|
||||
"available_portals": 3,
|
||||
"shuffle_portals": 1,
|
||||
"shuffle_transitions": 1,
|
||||
}
|
||||
|
||||
@unittest.skip
|
||||
def test_all_state_can_reach_everything(self) -> None:
|
||||
"""It's not possible to reach everything with these options so skip this test."""
|
||||
pass
|
92
worlds/messenger/transitions.py
Normal file
92
worlds/messenger/transitions.py
Normal file
@@ -0,0 +1,92 @@
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from BaseClasses import Region
|
||||
from entrance_rando import EntranceType, randomize_entrances
|
||||
from .connections import RANDOMIZED_CONNECTIONS, TRANSITIONS
|
||||
from .options import ShuffleTransitions, TransitionPlando
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import MessengerWorld
|
||||
|
||||
|
||||
def connect_plando(world: "MessengerWorld", plando_connections: TransitionPlando) -> None:
|
||||
def remove_dangling_exit(region: Region) -> None:
|
||||
# find the disconnected exit and remove references to it
|
||||
for _exit in region.exits:
|
||||
if not _exit.connected_region:
|
||||
break
|
||||
else:
|
||||
raise ValueError(f"Unable to find randomized transition for {plando_connection}")
|
||||
region.exits.remove(_exit)
|
||||
|
||||
def remove_dangling_entrance(region: Region) -> None:
|
||||
# find the disconnected entrance and remove references to it
|
||||
for _entrance in region.entrances:
|
||||
if not _entrance.parent_region:
|
||||
break
|
||||
else:
|
||||
raise ValueError(f"Invalid target region for {plando_connection}")
|
||||
region.entrances.remove(_entrance)
|
||||
|
||||
for plando_connection in plando_connections:
|
||||
# get the connecting regions
|
||||
reg1 = world.get_region(plando_connection.entrance)
|
||||
reg2 = world.get_region(plando_connection.exit)
|
||||
|
||||
remove_dangling_exit(reg1)
|
||||
remove_dangling_entrance(reg2)
|
||||
# connect the regions
|
||||
reg1.connect(reg2)
|
||||
|
||||
# pretend the user set the plando direction as "both" regardless of what they actually put on coupled
|
||||
if ((world.options.shuffle_transitions == ShuffleTransitions.option_coupled
|
||||
or plando_connection.direction == "both")
|
||||
and plando_connection.exit in RANDOMIZED_CONNECTIONS):
|
||||
remove_dangling_exit(reg2)
|
||||
remove_dangling_entrance(reg1)
|
||||
reg2.connect(reg1)
|
||||
|
||||
|
||||
def shuffle_transitions(world: "MessengerWorld") -> None:
|
||||
coupled = world.options.shuffle_transitions == ShuffleTransitions.option_coupled
|
||||
|
||||
def disconnect_entrance() -> None:
|
||||
child_region.entrances.remove(entrance)
|
||||
entrance.connected_region = None
|
||||
|
||||
er_type = EntranceType.ONE_WAY if child == "Glacial Peak - Left" else \
|
||||
EntranceType.TWO_WAY if child in RANDOMIZED_CONNECTIONS else EntranceType.ONE_WAY
|
||||
if er_type == EntranceType.TWO_WAY:
|
||||
mock_entrance = parent_region.create_er_target(entrance.name)
|
||||
else:
|
||||
mock_entrance = child_region.create_er_target(child)
|
||||
|
||||
entrance.randomization_type = er_type
|
||||
mock_entrance.randomization_type = er_type
|
||||
|
||||
for parent, child in RANDOMIZED_CONNECTIONS.items():
|
||||
if child == "Corrupted Future":
|
||||
entrance = world.get_entrance("Artificer's Portal")
|
||||
elif child == "Tower of Time - Left":
|
||||
entrance = world.get_entrance("Artificer's Challenge")
|
||||
else:
|
||||
entrance = world.get_entrance(f"{parent} -> {child}")
|
||||
parent_region = entrance.parent_region
|
||||
child_region = entrance.connected_region
|
||||
entrance.world = world
|
||||
disconnect_entrance()
|
||||
|
||||
plando = world.options.plando_connections
|
||||
if plando:
|
||||
connect_plando(world, plando)
|
||||
|
||||
result = randomize_entrances(world, coupled, {0: [0]})
|
||||
|
||||
world.transitions = sorted(result.placements, key=lambda entrance: TRANSITIONS.index(entrance.parent_region.name))
|
||||
|
||||
for transition in world.transitions:
|
||||
if "->" not in transition.name:
|
||||
continue
|
||||
transition.parent_region.exits.remove(transition)
|
||||
transition.name = f"{transition.parent_region.name} -> {transition.connected_region.name}"
|
||||
transition.parent_region.exits.append(transition)
|
Reference in New Issue
Block a user