mirror of
https://github.com/MarioSpore/Grinch-AP.git
synced 2025-10-21 20:21:32 -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
|
import logging
|
||||||
from typing import Any, ClassVar, TextIO
|
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 Options import Accessibility
|
||||||
from Utils import output_path
|
from Utils import output_path
|
||||||
from settings import FilePath, Group
|
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 .rules import MessengerHardRules, MessengerOOBRules, MessengerRules
|
||||||
from .shop import FIGURINES, PROG_SHOP_ITEMS, SHOP_ITEMS, USEFUL_SHOP_ITEMS, shuffle_shop_prices
|
from .shop import FIGURINES, PROG_SHOP_ITEMS, SHOP_ITEMS, USEFUL_SHOP_ITEMS, shuffle_shop_prices
|
||||||
from .subclasses import MessengerEntrance, MessengerItem, MessengerRegion, MessengerShopLocation
|
from .subclasses import MessengerEntrance, MessengerItem, MessengerRegion, MessengerShopLocation
|
||||||
|
from .transitions import shuffle_transitions
|
||||||
|
|
||||||
components.append(
|
components.append(
|
||||||
Component("The Messenger", component_type=Type.CLIENT, func=launch_game, game_name="The Messenger", supports_uri=True)
|
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]
|
spoiler_portal_mapping: dict[str, str]
|
||||||
portal_mapping: list[int]
|
portal_mapping: list[int]
|
||||||
transitions: list[Entrance]
|
transitions: list[Entrance]
|
||||||
reachable_locs: int = 0
|
reachable_locs: bool = False
|
||||||
filler: dict[str, int]
|
filler: dict[str, int]
|
||||||
|
|
||||||
def generate_early(self) -> None:
|
def generate_early(self) -> None:
|
||||||
@@ -145,13 +146,13 @@ class MessengerWorld(World):
|
|||||||
|
|
||||||
self.shop_prices, self.figurine_prices = shuffle_shop_prices(self)
|
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"
|
self.starting_portals = [f"{portal} Portal"
|
||||||
for portal in starting_portals[:3] +
|
for portal in starting_portals[:3] +
|
||||||
self.random.sample(starting_portals[3:], k=self.options.available_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
|
# 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:
|
if not self.options.shuffle_portals and "Searing Crags Portal" not in self.starting_portals:
|
||||||
self.starting_portals.append("Searing Crags Portal")
|
self.starting_portals.append("Searing Crags Portal")
|
||||||
portals_to_strip = [portal for portal in ["Riviere Turquoise Portal", "Sunken Shrine 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} - ")
|
region_name = region.name.removeprefix(f"{region.parent} - ")
|
||||||
connection_data = CONNECTIONS[region.parent][region_name]
|
connection_data = CONNECTIONS[region.parent][region_name]
|
||||||
for exit_region in connection_data:
|
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
|
# 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]:
|
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)}")
|
f" {logic} for {self.multiworld.get_player_name(self.player)}")
|
||||||
# MessengerOOBRules(self).set_messenger_rules()
|
# MessengerOOBRules(self).set_messenger_rules()
|
||||||
|
|
||||||
|
def connect_entrances(self) -> None:
|
||||||
add_closed_portal_reqs(self)
|
add_closed_portal_reqs(self)
|
||||||
# i need portal shuffle to happen after rules exist so i can validate it
|
# i need portal shuffle to happen after rules exist so i can validate it
|
||||||
attempts = 5
|
attempts = 5
|
||||||
@@ -271,6 +273,9 @@ class MessengerWorld(World):
|
|||||||
else:
|
else:
|
||||||
raise RuntimeError("Unable to generate valid portal output.")
|
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:
|
def write_spoiler_header(self, spoiler_handle: TextIO) -> None:
|
||||||
if self.options.available_portals < 6:
|
if self.options.available_portals < 6:
|
||||||
spoiler_handle.write(f"\nStarting Portals:\n\n")
|
spoiler_handle.write(f"\nStarting Portals:\n\n")
|
||||||
@@ -286,9 +291,54 @@ class MessengerWorld(World):
|
|||||||
key=lambda portal:
|
key=lambda portal:
|
||||||
["Autumn Hills", "Riviere Turquoise",
|
["Autumn Hills", "Riviere Turquoise",
|
||||||
"Howling Grotto", "Sunken Shrine",
|
"Howling Grotto", "Sunken Shrine",
|
||||||
"Searing Crags", "Glacial Peak"].index(portal[0]))
|
"Searing Crags", "Glacial Peak"].index(portal[0])
|
||||||
|
)
|
||||||
for portal, output in portal_info:
|
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]:
|
def fill_slot_data(self) -> dict[str, Any]:
|
||||||
slot_data = {
|
slot_data = {
|
||||||
@@ -308,11 +358,13 @@ class MessengerWorld(World):
|
|||||||
|
|
||||||
def get_filler_item_name(self) -> str:
|
def get_filler_item_name(self) -> str:
|
||||||
if not getattr(self, "_filler_items", None):
|
if not getattr(self, "_filler_items", None):
|
||||||
self._filler_items = [name for name in self.random.choices(
|
self._filler_items = [
|
||||||
|
name for name in self.random.choices(
|
||||||
list(self.filler),
|
list(self.filler),
|
||||||
weights=list(self.filler.values()),
|
weights=list(self.filler.values()),
|
||||||
k=20
|
k=20
|
||||||
)]
|
)
|
||||||
|
]
|
||||||
return self._filler_items.pop(0)
|
return self._filler_items.pop(0)
|
||||||
|
|
||||||
def create_item(self, name: str) -> MessengerItem:
|
def create_item(self, name: str) -> MessengerItem:
|
||||||
@@ -331,7 +383,7 @@ class MessengerWorld(World):
|
|||||||
self.total_shards += count
|
self.total_shards += count
|
||||||
return ItemClassification.progression_skip_balancing if count else ItemClassification.filler
|
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
|
return ItemClassification.progression if self.options.logic_level else ItemClassification.filler
|
||||||
|
|
||||||
if name == "Power Seal":
|
if name == "Power Seal":
|
||||||
|
@@ -244,14 +244,12 @@ CONNECTIONS: dict[str, dict[str, list[str]]] = {
|
|||||||
"Bottom Left": [
|
"Bottom Left": [
|
||||||
"Howling Grotto - Top",
|
"Howling Grotto - Top",
|
||||||
"Quillshroom Marsh - Sand Trap Shop",
|
"Quillshroom Marsh - Sand Trap Shop",
|
||||||
"Quillshroom Marsh - Bottom Right",
|
|
||||||
],
|
],
|
||||||
"Top Right": [
|
"Top Right": [
|
||||||
"Quillshroom Marsh - Queen of Quills Shop",
|
"Quillshroom Marsh - Queen of Quills Shop",
|
||||||
"Searing Crags - Left",
|
"Searing Crags - Left",
|
||||||
],
|
],
|
||||||
"Bottom Right": [
|
"Bottom Right": [
|
||||||
"Quillshroom Marsh - Bottom Left",
|
|
||||||
"Quillshroom Marsh - Sand Trap Shop",
|
"Quillshroom Marsh - Sand Trap Shop",
|
||||||
"Searing Crags - Bottom",
|
"Searing Crags - Bottom",
|
||||||
],
|
],
|
||||||
|
@@ -3,7 +3,8 @@ from dataclasses import dataclass
|
|||||||
from schema import And, Optional, Or, Schema
|
from schema import And, Optional, Or, Schema
|
||||||
|
|
||||||
from Options import Choice, DeathLinkMixin, DefaultOnToggle, ItemsAccessibility, OptionDict, PerGameCommonOptions, \
|
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
|
from .portals import CHECKPOINTS, PORTALS, SHOP_POINTS
|
||||||
|
|
||||||
|
|
||||||
@@ -30,17 +31,25 @@ class PortalPlando(PlandoConnections):
|
|||||||
portals = [f"{portal} Portal" for portal in PORTALS]
|
portals = [f"{portal} Portal" for portal in PORTALS]
|
||||||
shop_points = [point for points in SHOP_POINTS.values() for point in points]
|
shop_points = [point for points in SHOP_POINTS.values() for point in points]
|
||||||
checkpoints = [point for points in CHECKPOINTS.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 = frozenset(PORTALS)
|
||||||
entrances = portal_entrances
|
exits = frozenset(portals + shop_points + checkpoints)
|
||||||
exits = portal_exits
|
|
||||||
|
|
||||||
|
|
||||||
# for back compatibility. To later be replaced with transition plando
|
class TransitionPlando(PlandoConnections):
|
||||||
class HiddenPortalPlando(PortalPlando):
|
"""
|
||||||
visibility = Visibility.none
|
Plando connections to be used with transition shuffle.
|
||||||
entrances = PortalPlando.entrances
|
List of valid connections can be found at https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/messenger/connections.py#L641.
|
||||||
exits = PortalPlando.exits
|
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):
|
class Logic(Choice):
|
||||||
@@ -226,7 +235,7 @@ class MessengerOptions(DeathLinkMixin, PerGameCommonOptions):
|
|||||||
early_meditation: EarlyMed
|
early_meditation: EarlyMed
|
||||||
available_portals: AvailablePortals
|
available_portals: AvailablePortals
|
||||||
shuffle_portals: ShufflePortals
|
shuffle_portals: ShufflePortals
|
||||||
# shuffle_transitions: ShuffleTransitions
|
shuffle_transitions: ShuffleTransitions
|
||||||
goal: Goal
|
goal: Goal
|
||||||
music_box: MusicBox
|
music_box: MusicBox
|
||||||
notes_needed: NotesNeeded
|
notes_needed: NotesNeeded
|
||||||
@@ -236,4 +245,4 @@ class MessengerOptions(DeathLinkMixin, PerGameCommonOptions):
|
|||||||
shop_price: ShopPrices
|
shop_price: ShopPrices
|
||||||
shop_price_plan: PlannedShopPrices
|
shop_price_plan: PlannedShopPrices
|
||||||
portal_plando: PortalPlando
|
portal_plando: PortalPlando
|
||||||
plando_connections: HiddenPortalPlando
|
plando_connections: TransitionPlando
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from BaseClasses import CollectionState, PlandoOptions
|
from BaseClasses import CollectionState
|
||||||
from Options import PlandoConnection
|
from Options import PlandoConnection
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
@@ -252,9 +252,7 @@ def shuffle_portals(world: "MessengerWorld") -> None:
|
|||||||
world.random.shuffle(available_portals)
|
world.random.shuffle(available_portals)
|
||||||
|
|
||||||
plando = world.options.portal_plando.value
|
plando = world.options.portal_plando.value
|
||||||
if not plando:
|
if plando and not world.plando_portals:
|
||||||
plando = world.options.plando_connections.value
|
|
||||||
if plando and world.multiworld.plando_options & PlandoOptions.connections and not world.plando_portals:
|
|
||||||
try:
|
try:
|
||||||
handle_planned_portals(plando)
|
handle_planned_portals(plando)
|
||||||
# any failure i expect will trigger on available_portals.remove
|
# 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:
|
def validate_portals(world: "MessengerWorld") -> bool:
|
||||||
# if world.options.shuffle_transitions:
|
if world.options.shuffle_transitions:
|
||||||
# return True
|
return True
|
||||||
new_state = CollectionState(world.multiworld)
|
new_state = CollectionState(world.multiworld)
|
||||||
new_state.update_reachable_regions(world.player)
|
new_state.update_reachable_regions(world.player)
|
||||||
reachable_locs = 0
|
reachable_locs = 0
|
||||||
|
@@ -1,7 +1,8 @@
|
|||||||
from functools import cached_property
|
from functools import cached_property
|
||||||
from typing import TYPE_CHECKING
|
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 .regions import LOCATIONS, MEGA_SHARDS
|
||||||
from .shop import FIGURINES, SHOP_ITEMS
|
from .shop import FIGURINES, SHOP_ITEMS
|
||||||
|
|
||||||
@@ -12,9 +13,21 @@ if TYPE_CHECKING:
|
|||||||
class MessengerEntrance(Entrance):
|
class MessengerEntrance(Entrance):
|
||||||
world: "MessengerWorld | None" = None
|
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):
|
class MessengerRegion(Region):
|
||||||
parent: str
|
parent: str | None
|
||||||
entrance_type = MessengerEntrance
|
entrance_type = MessengerEntrance
|
||||||
|
|
||||||
def __init__(self, name: str, world: "MessengerWorld", parent: str | None = None) -> None:
|
def __init__(self, name: str, world: "MessengerWorld", parent: str | None = None) -> None:
|
||||||
@@ -32,7 +45,8 @@ class MessengerRegion(Region):
|
|||||||
for shop_loc in SHOP_ITEMS}
|
for shop_loc in SHOP_ITEMS}
|
||||||
self.add_locations(shop_locations, MessengerShopLocation)
|
self.add_locations(shop_locations, MessengerShopLocation)
|
||||||
elif name == "The Craftsman's Corner":
|
elif name == "The Craftsman's Corner":
|
||||||
self.add_locations({figurine: world.location_name_to_id[figurine] for figurine in FIGURINES},
|
self.add_locations(
|
||||||
|
{figurine: world.location_name_to_id[figurine] for figurine in FIGURINES},
|
||||||
MessengerLocation)
|
MessengerLocation)
|
||||||
elif name == "Tower HQ":
|
elif name == "Tower HQ":
|
||||||
locations.append("Money Wrench")
|
locations.append("Money Wrench")
|
||||||
@@ -57,6 +71,7 @@ class MessengerLocation(Location):
|
|||||||
|
|
||||||
|
|
||||||
class MessengerShopLocation(MessengerLocation):
|
class MessengerShopLocation(MessengerLocation):
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def cost(self) -> int:
|
def cost(self) -> int:
|
||||||
name = self.name.removeprefix("The Shop - ")
|
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