Lingo: New game (#1806)
Co-authored-by: Aaron Wagener <mmmcheese158@gmail.com> Co-authored-by: Fabian Dill <Berserker66@users.noreply.github.com> Co-authored-by: Phar <zach@alliware.com>
This commit is contained in:
parent
154e17f4ff
commit
ea9c31392d
|
@ -51,6 +51,7 @@ Currently, the following games are supported:
|
|||
* Muse Dash
|
||||
* DOOM 1993
|
||||
* Terraria
|
||||
* Lingo
|
||||
|
||||
For setup and instructions check out our [tutorials page](https://archipelago.gg/tutorial/).
|
||||
Downloads can be found at [Releases](https://github.com/ArchipelagoMW/Archipelago/releases), including compiled
|
||||
|
|
|
@ -61,6 +61,9 @@
|
|||
# Kingdom Hearts 2
|
||||
/worlds/kh2/ @JaredWeakStrike
|
||||
|
||||
# Lingo
|
||||
/worlds/lingo/ @hatkirby
|
||||
|
||||
# Links Awakening DX
|
||||
/worlds/ladx/ @zig-for
|
||||
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,112 @@
|
|||
"""
|
||||
Archipelago init file for Lingo
|
||||
"""
|
||||
from BaseClasses import Item, Tutorial
|
||||
from worlds.AutoWorld import WebWorld, World
|
||||
from .items import ALL_ITEM_TABLE, LingoItem
|
||||
from .locations import ALL_LOCATION_TABLE
|
||||
from .options import LingoOptions
|
||||
from .player_logic import LingoPlayerLogic
|
||||
from .regions import create_regions
|
||||
from .static_logic import Room, RoomEntrance
|
||||
from .testing import LingoTestOptions
|
||||
|
||||
|
||||
class LingoWebWorld(WebWorld):
|
||||
theme = "grass"
|
||||
tutorials = [Tutorial(
|
||||
"Multiworld Setup Guide",
|
||||
"A guide to playing Lingo with Archipelago.",
|
||||
"English",
|
||||
"setup_en.md",
|
||||
"setup/en",
|
||||
["hatkirby"]
|
||||
)]
|
||||
|
||||
|
||||
class LingoWorld(World):
|
||||
"""
|
||||
Lingo is a first person indie puzzle game in the vein of The Witness. You find yourself in a mazelike, non-Euclidean
|
||||
world filled with 800 word puzzles that use a variety of different mechanics.
|
||||
"""
|
||||
game = "Lingo"
|
||||
web = LingoWebWorld()
|
||||
|
||||
base_id = 444400
|
||||
topology_present = True
|
||||
data_version = 1
|
||||
|
||||
options_dataclass = LingoOptions
|
||||
options: LingoOptions
|
||||
|
||||
item_name_to_id = {
|
||||
name: data.code for name, data in ALL_ITEM_TABLE.items()
|
||||
}
|
||||
location_name_to_id = {
|
||||
name: data.code for name, data in ALL_LOCATION_TABLE.items()
|
||||
}
|
||||
|
||||
player_logic: LingoPlayerLogic
|
||||
|
||||
def generate_early(self):
|
||||
self.player_logic = LingoPlayerLogic(self)
|
||||
|
||||
def create_regions(self):
|
||||
create_regions(self, self.player_logic)
|
||||
|
||||
def create_items(self):
|
||||
pool = [self.create_item(name) for name in self.player_logic.REAL_ITEMS]
|
||||
|
||||
if self.player_logic.FORCED_GOOD_ITEM != "":
|
||||
new_item = self.create_item(self.player_logic.FORCED_GOOD_ITEM)
|
||||
location_obj = self.multiworld.get_location("Second Room - Good Luck", self.player)
|
||||
location_obj.place_locked_item(new_item)
|
||||
|
||||
item_difference = len(self.player_logic.REAL_LOCATIONS) - len(pool)
|
||||
if item_difference:
|
||||
trap_percentage = self.options.trap_percentage
|
||||
traps = int(item_difference * trap_percentage / 100.0)
|
||||
non_traps = item_difference - traps
|
||||
|
||||
if non_traps:
|
||||
skip_percentage = self.options.puzzle_skip_percentage
|
||||
skips = int(non_traps * skip_percentage / 100.0)
|
||||
non_skips = non_traps - skips
|
||||
|
||||
filler_list = [":)", "The Feeling of Being Lost", "Wanderlust", "Empty White Hallways"]
|
||||
for i in range(0, non_skips):
|
||||
pool.append(self.create_item(filler_list[i % len(filler_list)]))
|
||||
|
||||
for i in range(0, skips):
|
||||
pool.append(self.create_item("Puzzle Skip"))
|
||||
|
||||
if traps:
|
||||
traps_list = ["Slowness Trap", "Iceland Trap", "Atbash Trap"]
|
||||
|
||||
for i in range(0, traps):
|
||||
pool.append(self.create_item(traps_list[i % len(traps_list)]))
|
||||
|
||||
self.multiworld.itempool += pool
|
||||
|
||||
def create_item(self, name: str) -> Item:
|
||||
item = ALL_ITEM_TABLE[name]
|
||||
return LingoItem(name, item.classification, item.code, self.player)
|
||||
|
||||
def set_rules(self):
|
||||
self.multiworld.completion_condition[self.player] = lambda state: state.has("Victory", self.player)
|
||||
|
||||
def fill_slot_data(self):
|
||||
slot_options = [
|
||||
"death_link", "victory_condition", "shuffle_colors", "shuffle_doors", "shuffle_paintings", "shuffle_panels",
|
||||
"mastery_achievements", "level_2_requirement", "location_checks", "early_color_hallways"
|
||||
]
|
||||
|
||||
slot_data = {
|
||||
"seed": self.random.randint(0, 1000000),
|
||||
**self.options.as_dict(*slot_options),
|
||||
}
|
||||
|
||||
if self.options.shuffle_paintings:
|
||||
slot_data["painting_entrance_to_exit"] = self.player_logic.PAINTING_MAPPING
|
||||
|
||||
return slot_data
|
|
@ -0,0 +1,42 @@
|
|||
# Lingo
|
||||
|
||||
## Where is the settings page?
|
||||
|
||||
The [player settings page for this game](../player-settings) contains all the options you need to configure and export a
|
||||
config file.
|
||||
|
||||
## What does randomization do to this game?
|
||||
|
||||
There are a couple of modes of randomization currently available, and you can pick and choose which ones you would like
|
||||
to use.
|
||||
|
||||
* **Door shuffle**: There are many doors in the game, which are opened by completing a set of panels. With door shuffle
|
||||
on, the doors become items and only open up once you receive the corresponding item. The panel sets that would
|
||||
ordinarily open the doors become locations.
|
||||
|
||||
* **Color shuffle**: There are ten different colors of puzzle in the game, each representing a different mechanic. With
|
||||
color shuffle on, you would start with only access to white puzzles. Puzzles of other colors will require you to
|
||||
receive an item in order to solve them (e.g. you can't solve any red puzzles until you receive the "Red" item).
|
||||
|
||||
* **Panel shuffle**: Panel shuffling replaces the puzzles on each panel with different ones. So far, the only mode of
|
||||
panel shuffling is "rearrange" mode, which simply shuffles the already-existing puzzles from the base game onto
|
||||
different panels.
|
||||
|
||||
* **Painting shuffle**: This randomizes the appearance of the paintings in the game, as well as which of them are warps,
|
||||
and the locations that they warp you to. It is the equivalent of an entrance randomizer in another game.
|
||||
|
||||
## What is a "check" in this game?
|
||||
|
||||
Most panels / panel sets that open a door are now location checks, even if door shuffle is not enabled. Various other
|
||||
puzzles are also location checks, including the achievement panels for each area.
|
||||
|
||||
## What about wall snipes?
|
||||
|
||||
"Wall sniping" refers to the fact that you are able to solve puzzles on the other side of opaque walls. This randomizer
|
||||
does not change how wall snipes work, but it will never require the use of them. There are three puzzles from the base
|
||||
game that you would ordinarily be expected to wall snipe. The randomizer moves these panels out of the wall or otherwise
|
||||
reveals them so that a snipe is not necessary.
|
||||
|
||||
Because of this, all wall snipes are considered out of logic. This includes sniping The Bearer's MIDDLE while standing
|
||||
outside The Bold, sniping The Colorful without opening all of the color doors, and sniping WELCOME from next to WELCOME
|
||||
BACK.
|
|
@ -0,0 +1,45 @@
|
|||
# Lingo Randomizer Setup
|
||||
|
||||
## Required Software
|
||||
|
||||
- [Lingo](https://store.steampowered.com/app/1814170/Lingo/)
|
||||
- [Lingo Archipelago Randomizer](https://code.fourisland.com/lingo-archipelago/about/CHANGELOG.md)
|
||||
|
||||
## Optional Software
|
||||
|
||||
- [Archipelago Text Client](https://github.com/ArchipelagoMW/Archipelago/releases)
|
||||
- [Lingo AP Tracker](https://code.fourisland.com/lingo-ap-tracker/about/CHANGELOG.md)
|
||||
|
||||
## Installation
|
||||
|
||||
1. Download the Lingo Archipelago Randomizer from the above link.
|
||||
2. Open up Lingo, go to settings, and click View Game Data. This should open up
|
||||
a folder in Windows Explorer.
|
||||
3. Unzip the contents of the randomizer into the "maps" folder. You may need to
|
||||
create the "maps" folder if you have not played a custom Lingo map before.
|
||||
4. Installation complete! You may have to click Return to go back to the main
|
||||
menu and then click Settings again in order to get the randomizer to show up
|
||||
in the level selection list.
|
||||
|
||||
## Joining a Multiworld game
|
||||
|
||||
1. Launch Lingo
|
||||
2. Click on Settings, and then Level. Choose Archipelago from the list.
|
||||
3. Start a new game. Leave the name field blank (anything you type in will be
|
||||
ignored).
|
||||
4. Enter the Archipelago address, slot name, and password into the fields.
|
||||
5. Press Connect.
|
||||
6. Enjoy!
|
||||
|
||||
To continue an earlier game, you can perform the exact same steps as above. You
|
||||
do not have to re-select Archipelago in the level selection screen if you were
|
||||
using Archipelago the last time you launched the game.
|
||||
|
||||
In order to play the base game again, simply return to the level selection
|
||||
screen and choose Level 1 (or whatever else you want to play). The randomizer
|
||||
will not affect gameplay unless you launch it by starting a new game while it is
|
||||
selected in the level selection screen, so it is safe to play the game normally
|
||||
while the client is installed.
|
||||
|
||||
**Note**: Running the randomizer modifies the game's memory. If you want to play
|
||||
the base game after playing the randomizer, you need to restart Lingo first.
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,106 @@
|
|||
from typing import Dict, List, NamedTuple, Optional, TYPE_CHECKING
|
||||
|
||||
from BaseClasses import Item, ItemClassification
|
||||
from .options import ShuffleDoors
|
||||
from .static_logic import DOORS_BY_ROOM, PROGRESSION_BY_ROOM, PROGRESSIVE_ITEMS, get_door_group_item_id, \
|
||||
get_door_item_id, get_progressive_item_id, get_special_item_id
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import LingoWorld
|
||||
|
||||
|
||||
class ItemData(NamedTuple):
|
||||
"""
|
||||
ItemData for an item in Lingo
|
||||
"""
|
||||
code: int
|
||||
classification: ItemClassification
|
||||
mode: Optional[str]
|
||||
door_ids: List[str]
|
||||
painting_ids: List[str]
|
||||
|
||||
def should_include(self, world: "LingoWorld") -> bool:
|
||||
if self.mode == "colors":
|
||||
return world.options.shuffle_colors > 0
|
||||
elif self.mode == "doors":
|
||||
return world.options.shuffle_doors != ShuffleDoors.option_none
|
||||
elif self.mode == "orange tower":
|
||||
# door shuffle is on and tower isn't progressive
|
||||
return world.options.shuffle_doors != ShuffleDoors.option_none \
|
||||
and not world.options.progressive_orange_tower
|
||||
elif self.mode == "complex door":
|
||||
return world.options.shuffle_doors == ShuffleDoors.option_complex
|
||||
elif self.mode == "door group":
|
||||
return world.options.shuffle_doors == ShuffleDoors.option_simple
|
||||
elif self.mode == "special":
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
|
||||
class LingoItem(Item):
|
||||
"""
|
||||
Item from the game Lingo
|
||||
"""
|
||||
game: str = "Lingo"
|
||||
|
||||
|
||||
ALL_ITEM_TABLE: Dict[str, ItemData] = {}
|
||||
|
||||
|
||||
def load_item_data():
|
||||
global ALL_ITEM_TABLE
|
||||
|
||||
for color in ["Black", "Red", "Blue", "Yellow", "Green", "Orange", "Gray", "Brown", "Purple"]:
|
||||
ALL_ITEM_TABLE[color] = ItemData(get_special_item_id(color), ItemClassification.progression,
|
||||
"colors", [], [])
|
||||
|
||||
door_groups: Dict[str, List[str]] = {}
|
||||
for room_name, doors in DOORS_BY_ROOM.items():
|
||||
for door_name, door in doors.items():
|
||||
if door.skip_item is True or door.event is True:
|
||||
continue
|
||||
|
||||
if door.group is None:
|
||||
door_mode = "doors"
|
||||
else:
|
||||
door_mode = "complex door"
|
||||
door_groups.setdefault(door.group, []).extend(door.door_ids)
|
||||
|
||||
if room_name in PROGRESSION_BY_ROOM and door_name in PROGRESSION_BY_ROOM[room_name]:
|
||||
if room_name == "Orange Tower":
|
||||
door_mode = "orange tower"
|
||||
else:
|
||||
door_mode = "special"
|
||||
|
||||
ALL_ITEM_TABLE[door.item_name] = \
|
||||
ItemData(get_door_item_id(room_name, door_name),
|
||||
ItemClassification.filler if door.junk_item else ItemClassification.progression, door_mode,
|
||||
door.door_ids, door.painting_ids)
|
||||
|
||||
for group, group_door_ids in door_groups.items():
|
||||
ALL_ITEM_TABLE[group] = ItemData(get_door_group_item_id(group),
|
||||
ItemClassification.progression, "door group", group_door_ids, [])
|
||||
|
||||
special_items: Dict[str, ItemClassification] = {
|
||||
":)": ItemClassification.filler,
|
||||
"The Feeling of Being Lost": ItemClassification.filler,
|
||||
"Wanderlust": ItemClassification.filler,
|
||||
"Empty White Hallways": ItemClassification.filler,
|
||||
"Slowness Trap": ItemClassification.trap,
|
||||
"Iceland Trap": ItemClassification.trap,
|
||||
"Atbash Trap": ItemClassification.trap,
|
||||
"Puzzle Skip": ItemClassification.useful,
|
||||
}
|
||||
|
||||
for item_name, classification in special_items.items():
|
||||
ALL_ITEM_TABLE[item_name] = ItemData(get_special_item_id(item_name), classification,
|
||||
"special", [], [])
|
||||
|
||||
for item_name in PROGRESSIVE_ITEMS:
|
||||
ALL_ITEM_TABLE[item_name] = ItemData(get_progressive_item_id(item_name),
|
||||
ItemClassification.progression, "special", [], [])
|
||||
|
||||
|
||||
# Initialize the item data at module scope.
|
||||
load_item_data()
|
|
@ -0,0 +1,80 @@
|
|||
from enum import Flag, auto
|
||||
from typing import Dict, List, NamedTuple
|
||||
|
||||
from BaseClasses import Location
|
||||
from .static_logic import DOORS_BY_ROOM, PANELS_BY_ROOM, RoomAndPanel, get_door_location_id, get_panel_location_id
|
||||
|
||||
|
||||
class LocationClassification(Flag):
|
||||
normal = auto()
|
||||
reduced = auto()
|
||||
insanity = auto()
|
||||
|
||||
|
||||
class LocationData(NamedTuple):
|
||||
"""
|
||||
LocationData for a location in Lingo
|
||||
"""
|
||||
code: int
|
||||
room: str
|
||||
panels: List[RoomAndPanel]
|
||||
classification: LocationClassification
|
||||
|
||||
def panel_ids(self):
|
||||
ids = set()
|
||||
for panel in self.panels:
|
||||
effective_room = self.room if panel.room is None else panel.room
|
||||
panel_data = PANELS_BY_ROOM[effective_room][panel.panel]
|
||||
ids = ids | set(panel_data.internal_ids)
|
||||
return ids
|
||||
|
||||
|
||||
class LingoLocation(Location):
|
||||
"""
|
||||
Location from the game Lingo
|
||||
"""
|
||||
game: str = "Lingo"
|
||||
|
||||
|
||||
ALL_LOCATION_TABLE: Dict[str, LocationData] = {}
|
||||
|
||||
|
||||
def load_location_data():
|
||||
global ALL_LOCATION_TABLE
|
||||
|
||||
for room_name, panels in PANELS_BY_ROOM.items():
|
||||
for panel_name, panel in panels.items():
|
||||
location_name = f"{room_name} - {panel_name}"
|
||||
|
||||
classification = LocationClassification.insanity
|
||||
if panel.check:
|
||||
classification |= LocationClassification.normal
|
||||
|
||||
if not panel.exclude_reduce:
|
||||
classification |= LocationClassification.reduced
|
||||
|
||||
ALL_LOCATION_TABLE[location_name] = \
|
||||
LocationData(get_panel_location_id(room_name, panel_name), room_name,
|
||||
[RoomAndPanel(None, panel_name)], classification)
|
||||
|
||||
for room_name, doors in DOORS_BY_ROOM.items():
|
||||
for door_name, door in doors.items():
|
||||
if door.skip_location or door.event or door.panels is None:
|
||||
continue
|
||||
|
||||
location_name = door.location_name
|
||||
classification = LocationClassification.normal
|
||||
if door.include_reduce:
|
||||
classification |= LocationClassification.reduced
|
||||
|
||||
if location_name in ALL_LOCATION_TABLE:
|
||||
new_id = ALL_LOCATION_TABLE[location_name].code
|
||||
classification |= ALL_LOCATION_TABLE[location_name].classification
|
||||
else:
|
||||
new_id = get_door_location_id(room_name, door_name)
|
||||
|
||||
ALL_LOCATION_TABLE[location_name] = LocationData(new_id, room_name, door.panels, classification)
|
||||
|
||||
|
||||
# Initialize location data on the module scope.
|
||||
load_location_data()
|
|
@ -0,0 +1,126 @@
|
|||
from dataclasses import dataclass
|
||||
|
||||
from Options import Toggle, Choice, DefaultOnToggle, Range, PerGameCommonOptions
|
||||
|
||||
|
||||
class ShuffleDoors(Choice):
|
||||
"""If on, opening doors will require their respective "keys".
|
||||
In "simple", doors are sorted into logical groups, which are all opened by receiving an item.
|
||||
In "complex", the items are much more granular, and will usually only open a single door each."""
|
||||
display_name = "Shuffle Doors"
|
||||
option_none = 0
|
||||
option_simple = 1
|
||||
option_complex = 2
|
||||
|
||||
|
||||
class ProgressiveOrangeTower(DefaultOnToggle):
|
||||
"""When "Shuffle Doors" is on, this setting governs the manner in which the Orange Tower floors open up.
|
||||
If off, there is an item for each floor of the tower, and each floor's item is the only one needed to access that floor.
|
||||
If on, there are six progressive items, which open up the tower from the bottom floor upward.
|
||||
"""
|
||||
display_name = "Progressive Orange Tower"
|
||||
|
||||
|
||||
class LocationChecks(Choice):
|
||||
"""On "normal", there will be a location check for each panel set that would ordinarily open a door, as well as for
|
||||
achievement panels and a small handful of other panels.
|
||||
On "reduced", many of the locations that are associated with opening doors are removed.
|
||||
On "insanity", every individual panel in the game is a location check."""
|
||||
display_name = "Location Checks"
|
||||
option_normal = 0
|
||||
option_reduced = 1
|
||||
option_insanity = 2
|
||||
|
||||
|
||||
class ShuffleColors(Toggle):
|
||||
"""If on, an item is added to the pool for every puzzle color (besides White).
|
||||
You will need to unlock the requisite colors in order to be able to solve puzzles of that color."""
|
||||
display_name = "Shuffle Colors"
|
||||
|
||||
|
||||
class ShufflePanels(Choice):
|
||||
"""If on, the puzzles on each panel are randomized.
|
||||
On "rearrange", the puzzles are the same as the ones in the base game, but are placed in different areas."""
|
||||
display_name = "Shuffle Panels"
|
||||
option_none = 0
|
||||
option_rearrange = 1
|
||||
|
||||
|
||||
class ShufflePaintings(Toggle):
|
||||
"""If on, the destination, location, and appearance of the painting warps in the game will be randomized."""
|
||||
display_name = "Shuffle Paintings"
|
||||
|
||||
|
||||
class VictoryCondition(Choice):
|
||||
"""Change the victory condition."""
|
||||
display_name = "Victory Condition"
|
||||
option_the_end = 0
|
||||
option_the_master = 1
|
||||
option_level_2 = 2
|
||||
|
||||
|
||||
class MasteryAchievements(Range):
|
||||
"""The number of achievements required to unlock THE MASTER.
|
||||
In the base game, 21 achievements are needed.
|
||||
If you include The Scientific and The Unchallenged, which are in the base game but are not counted for mastery, 23 would be required.
|
||||
If you include the custom achievement (The Wanderer), 24 would be required.
|
||||
"""
|
||||
display_name = "Mastery Achievements"
|
||||
range_start = 1
|
||||
range_end = 24
|
||||
default = 21
|
||||
|
||||
|
||||
class Level2Requirement(Range):
|
||||
"""The number of panel solves required to unlock LEVEL 2.
|
||||
In the base game, 223 are needed.
|
||||
Note that this count includes ANOTHER TRY.
|
||||
"""
|
||||
display_name = "Level 2 Requirement"
|
||||
range_start = 2
|
||||
range_end = 800
|
||||
default = 223
|
||||
|
||||
|
||||
class EarlyColorHallways(Toggle):
|
||||
"""When on, a painting warp to the color hallways area will appear in the starting room.
|
||||
This lets you avoid being trapped in the starting room for long periods of time when door shuffle is on."""
|
||||
display_name = "Early Color Hallways"
|
||||
|
||||
|
||||
class TrapPercentage(Range):
|
||||
"""Replaces junk items with traps, at the specified rate."""
|
||||
display_name = "Trap Percentage"
|
||||
range_start = 0
|
||||
range_end = 100
|
||||
default = 20
|
||||
|
||||
|
||||
class PuzzleSkipPercentage(Range):
|
||||
"""Replaces junk items with puzzle skips, at the specified rate."""
|
||||
display_name = "Puzzle Skip Percentage"
|
||||
range_start = 0
|
||||
range_end = 100
|
||||
default = 20
|
||||
|
||||
|
||||
class DeathLink(Toggle):
|
||||
"""If on: Whenever another player on death link dies, you will be returned to the starting room."""
|
||||
display_name = "Death Link"
|
||||
|
||||
|
||||
@dataclass
|
||||
class LingoOptions(PerGameCommonOptions):
|
||||
shuffle_doors: ShuffleDoors
|
||||
progressive_orange_tower: ProgressiveOrangeTower
|
||||
location_checks: LocationChecks
|
||||
shuffle_colors: ShuffleColors
|
||||
shuffle_panels: ShufflePanels
|
||||
shuffle_paintings: ShufflePaintings
|
||||
victory_condition: VictoryCondition
|
||||
mastery_achievements: MasteryAchievements
|
||||
level_2_requirement: Level2Requirement
|
||||
early_color_hallways: EarlyColorHallways
|
||||
trap_percentage: TrapPercentage
|
||||
puzzle_skip_percentage: PuzzleSkipPercentage
|
||||
death_link: DeathLink
|
|
@ -0,0 +1,298 @@
|
|||
from typing import Dict, List, NamedTuple, Optional, TYPE_CHECKING
|
||||
|
||||
from .items import ALL_ITEM_TABLE
|
||||
from .locations import ALL_LOCATION_TABLE, LocationClassification
|
||||
from .options import LocationChecks, ShuffleDoors, VictoryCondition
|
||||
from .static_logic import DOORS_BY_ROOM, Door, PAINTINGS, PAINTINGS_BY_ROOM, PAINTING_ENTRANCES, PAINTING_EXITS, \
|
||||
PANELS_BY_ROOM, PROGRESSION_BY_ROOM, REQUIRED_PAINTING_ROOMS, REQUIRED_PAINTING_WHEN_NO_DOORS_ROOMS, ROOMS, \
|
||||
RoomAndPanel
|
||||
from .testing import LingoTestOptions
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import LingoWorld
|
||||
|
||||
|
||||
class PlayerLocation(NamedTuple):
|
||||
name: str
|
||||
code: Optional[int] = None
|
||||
panels: List[RoomAndPanel] = []
|
||||
|
||||
|
||||
class LingoPlayerLogic:
|
||||
"""
|
||||
Defines logic after a player's options have been applied
|
||||
"""
|
||||
|
||||
ITEM_BY_DOOR: Dict[str, Dict[str, str]]
|
||||
|
||||
LOCATIONS_BY_ROOM: Dict[str, List[PlayerLocation]]
|
||||
REAL_LOCATIONS: List[str]
|
||||
|
||||
EVENT_LOC_TO_ITEM: Dict[str, str]
|
||||
REAL_ITEMS: List[str]
|
||||
|
||||
VICTORY_CONDITION: str
|
||||
MASTERY_LOCATION: str
|
||||
LEVEL_2_LOCATION: str
|
||||
|
||||
PAINTING_MAPPING: Dict[str, str]
|
||||
|
||||
FORCED_GOOD_ITEM: str
|
||||
|
||||
def add_location(self, room: str, loc: PlayerLocation):
|
||||
self.LOCATIONS_BY_ROOM.setdefault(room, []).append(loc)
|
||||
|
||||
def set_door_item(self, room: str, door: str, item: str):
|
||||
self.ITEM_BY_DOOR.setdefault(room, {})[door] = item
|
||||
|
||||
def handle_non_grouped_door(self, room_name: str, door_data: Door, world: "LingoWorld"):
|
||||
if room_name in PROGRESSION_BY_ROOM and door_data.name in PROGRESSION_BY_ROOM[room_name]:
|
||||
if room_name == "Orange Tower" and not world.options.progressive_orange_tower:
|
||||
self.set_door_item(room_name, door_data.name, door_data.item_name)
|
||||
else:
|
||||
progressive_item_name = PROGRESSION_BY_ROOM[room_name][door_data.name].item_name
|
||||
self.set_door_item(room_name, door_data.name, progressive_item_name)
|
||||
self.REAL_ITEMS.append(progressive_item_name)
|
||||
else:
|
||||
self.set_door_item(room_name, door_data.name, door_data.item_name)
|
||||
|
||||
def __init__(self, world: "LingoWorld"):
|
||||
self.ITEM_BY_DOOR = {}
|
||||
self.LOCATIONS_BY_ROOM = {}
|
||||
self.REAL_LOCATIONS = []
|
||||
self.EVENT_LOC_TO_ITEM = {}
|
||||
self.REAL_ITEMS = []
|
||||
self.VICTORY_CONDITION = ""
|
||||
self.MASTERY_LOCATION = ""
|
||||
self.LEVEL_2_LOCATION = ""
|
||||
self.PAINTING_MAPPING = {}
|
||||
self.FORCED_GOOD_ITEM = ""
|
||||
|
||||
door_shuffle = world.options.shuffle_doors
|
||||
color_shuffle = world.options.shuffle_colors
|
||||
painting_shuffle = world.options.shuffle_paintings
|
||||
location_checks = world.options.location_checks
|
||||
victory_condition = world.options.victory_condition
|
||||
early_color_hallways = world.options.early_color_hallways
|
||||
|
||||
if location_checks == LocationChecks.option_reduced and door_shuffle != ShuffleDoors.option_none:
|
||||
raise Exception("You cannot have reduced location checks when door shuffle is on, because there would not "
|
||||
"be enough locations for all of the door items.")
|
||||
|
||||
# Create an event for every room that represents being able to reach that room.
|
||||
for room_name in ROOMS.keys():
|
||||
roomloc_name = f"{room_name} (Reached)"
|
||||
self.add_location(room_name, PlayerLocation(roomloc_name, None, []))
|
||||
self.EVENT_LOC_TO_ITEM[roomloc_name] = roomloc_name
|
||||
|
||||
# Create an event for every door, representing whether that door has been opened. Also create event items for
|
||||
# doors that are event-only.
|
||||
for room_name, room_data in DOORS_BY_ROOM.items():
|
||||
for door_name, door_data in room_data.items():
|
||||
if door_shuffle == ShuffleDoors.option_none:
|
||||
itemloc_name = f"{room_name} - {door_name} (Opened)"
|
||||
self.add_location(room_name, PlayerLocation(itemloc_name, None, door_data.panels))
|
||||
self.EVENT_LOC_TO_ITEM[itemloc_name] = itemloc_name
|
||||
self.set_door_item(room_name, door_name, itemloc_name)
|
||||
else:
|
||||
# This line is duplicated from StaticLingoItems
|
||||
if door_data.skip_item is False and door_data.event is False:
|
||||
if door_data.group is not None and door_shuffle == ShuffleDoors.option_simple:
|
||||
# Grouped doors are handled differently if shuffle doors is on simple.
|
||||
self.set_door_item(room_name, door_name, door_data.group)
|
||||
else:
|
||||
self.handle_non_grouped_door(room_name, door_data, world)
|
||||
|
||||
if door_data.event:
|
||||
self.add_location(room_name, PlayerLocation(door_data.item_name, None, door_data.panels))
|
||||
self.EVENT_LOC_TO_ITEM[door_data.item_name] = door_data.item_name + " (Opened)"
|
||||
self.set_door_item(room_name, door_name, door_data.item_name + " (Opened)")
|
||||
|
||||
# Create events for each achievement panel, so that we can determine when THE MASTER is accessible. We also
|
||||
# create events for each counting panel, so that we can determine when LEVEL 2 is accessible.
|
||||
for room_name, room_data in PANELS_BY_ROOM.items():
|
||||
for panel_name, panel_data in room_data.items():
|
||||
if panel_data.achievement:
|
||||
event_name = room_name + " - " + panel_name + " (Achieved)"
|
||||
self.add_location(room_name, PlayerLocation(event_name, None,
|
||||
[RoomAndPanel(room_name, panel_name)]))
|
||||
self.EVENT_LOC_TO_ITEM[event_name] = "Mastery Achievement"
|
||||
|
||||
if not panel_data.non_counting and victory_condition == VictoryCondition.option_level_2:
|
||||
event_name = room_name + " - " + panel_name + " (Counted)"
|
||||
self.add_location(room_name, PlayerLocation(event_name, None,
|
||||
[RoomAndPanel(room_name, panel_name)]))
|
||||
self.EVENT_LOC_TO_ITEM[event_name] = "Counting Panel Solved"
|
||||
|
||||
# Handle the victory condition. Victory conditions other than the chosen one become regular checks, so we need
|
||||
# to prevent the actual victory condition from becoming a check.
|
||||
self.MASTERY_LOCATION = "Orange Tower Seventh Floor - THE MASTER"
|
||||
self.LEVEL_2_LOCATION = "N/A"
|
||||
|
||||
if victory_condition == VictoryCondition.option_the_end:
|
||||
self.VICTORY_CONDITION = "Orange Tower Seventh Floor - THE END"
|
||||
self.add_location("Orange Tower Seventh Floor", PlayerLocation("The End (Solved)"))
|
||||
self.EVENT_LOC_TO_ITEM["The End (Solved)"] = "Victory"
|
||||
elif victory_condition == VictoryCondition.option_the_master:
|
||||
self.VICTORY_CONDITION = "Orange Tower Seventh Floor - THE MASTER"
|
||||
self.MASTERY_LOCATION = "Orange Tower Seventh Floor - Mastery Achievements"
|
||||
|
||||
self.add_location("Orange Tower Seventh Floor", PlayerLocation(self.MASTERY_LOCATION, None, []))
|
||||
self.EVENT_LOC_TO_ITEM[self.MASTERY_LOCATION] = "Victory"
|
||||
elif victory_condition == VictoryCondition.option_level_2:
|
||||
self.VICTORY_CONDITION = "Second Room - LEVEL 2"
|
||||
self.LEVEL_2_LOCATION = "Second Room - Unlock Level 2"
|
||||
|
||||
self.add_location("Second Room", PlayerLocation(self.LEVEL_2_LOCATION, None,
|
||||
[RoomAndPanel("Second Room", "LEVEL 2")]))
|
||||
self.EVENT_LOC_TO_ITEM[self.LEVEL_2_LOCATION] = "Victory"
|
||||
|
||||
# Instantiate all real locations.
|
||||
location_classification = LocationClassification.normal
|
||||
if location_checks == LocationChecks.option_reduced:
|
||||
location_classification = LocationClassification.reduced
|
||||
elif location_checks == LocationChecks.option_insanity:
|
||||
location_classification = LocationClassification.insanity
|
||||
|
||||
for location_name, location_data in ALL_LOCATION_TABLE.items():
|
||||
if location_name != self.VICTORY_CONDITION:
|
||||
if location_classification not in location_data.classification:
|
||||
continue
|
||||
|
||||
self.add_location(location_data.room, PlayerLocation(location_name, location_data.code,
|
||||
location_data.panels))
|
||||
self.REAL_LOCATIONS.append(location_name)
|
||||
|
||||
# Instantiate all real items.
|
||||
for name, item in ALL_ITEM_TABLE.items():
|
||||
if item.should_include(world):
|
||||
self.REAL_ITEMS.append(name)
|
||||
|
||||
# Create the paintings mapping, if painting shuffle is on.
|
||||
if painting_shuffle:
|
||||
# Shuffle paintings until we get something workable.
|
||||
workable_paintings = False
|
||||
for i in range(0, 20):
|
||||
workable_paintings = self.randomize_paintings(world)
|
||||
if workable_paintings:
|
||||
break
|
||||
|
||||
if not workable_paintings:
|
||||
raise Exception("This Lingo world was unable to generate a workable painting mapping after 20 "
|
||||
"iterations. This is very unlikely to happen on its own, and probably indicates some "
|
||||
"kind of logic error.")
|
||||
|
||||
if door_shuffle != ShuffleDoors.option_none and location_classification != LocationClassification.insanity \
|
||||
and not early_color_hallways and LingoTestOptions.disable_forced_good_item is False:
|
||||
# If shuffle doors is on, force a useful item onto the HI panel. This may not necessarily get you out of BK,
|
||||
# but the goal is to allow you to reach at least one more check. The non-painting ones are hardcoded right
|
||||
# now. We only allow the entrance to the Pilgrim Room if color shuffle is off, because otherwise there are
|
||||
# no extra checks in there. We only include the entrance to the Rhyme Room when color shuffle is off and
|
||||
# door shuffle is on simple, because otherwise there are no extra checks in there.
|
||||
good_item_options: List[str] = ["Starting Room - Back Right Door", "Second Room - Exit Door"]
|
||||
|
||||
if not color_shuffle:
|
||||
good_item_options.append("Pilgrim Room - Sun Painting")
|
||||
|
||||
if door_shuffle == ShuffleDoors.option_simple:
|
||||
good_item_options += ["Welcome Back Doors"]
|
||||
|
||||
if not color_shuffle:
|
||||
good_item_options.append("Rhyme Room Doors")
|
||||
else:
|
||||
good_item_options += ["Welcome Back Area - Shortcut to Starting Room"]
|
||||
|
||||
for painting_obj in PAINTINGS_BY_ROOM["Starting Room"]:
|
||||
if not painting_obj.enter_only or painting_obj.required_door is None:
|
||||
continue
|
||||
|
||||
# If painting shuffle is on, we only want to consider paintings that actually go somewhere.
|
||||
if painting_shuffle and painting_obj.id not in self.PAINTING_MAPPING.keys():
|
||||
continue
|
||||
|
||||
pdoor = DOORS_BY_ROOM[painting_obj.required_door.room][painting_obj.required_door.door]
|
||||
good_item_options.append(pdoor.item_name)
|
||||
|
||||
# Copied from The Witness -- remove any plandoed items from the possible good items set.
|
||||
for v in world.multiworld.plando_items[world.player]:
|
||||
if v.get("from_pool", True):
|
||||
for item_key in {"item", "items"}:
|
||||
if item_key in v:
|
||||
if type(v[item_key]) is str:
|
||||
if v[item_key] in good_item_options:
|
||||
good_item_options.remove(v[item_key])
|
||||
elif type(v[item_key]) is dict:
|
||||
for item, weight in v[item_key].items():
|
||||
if weight and item in good_item_options:
|
||||
good_item_options.remove(item)
|
||||
else:
|
||||
# Other type of iterable
|
||||
for item in v[item_key]:
|
||||
if item in good_item_options:
|
||||
good_item_options.remove(item)
|
||||
|
||||
if len(good_item_options) > 0:
|
||||
self.FORCED_GOOD_ITEM = world.random.choice(good_item_options)
|
||||
self.REAL_ITEMS.remove(self.FORCED_GOOD_ITEM)
|
||||
self.REAL_LOCATIONS.remove("Second Room - Good Luck")
|
||||
|
||||
def randomize_paintings(self, world: "LingoWorld") -> bool:
|
||||
self.PAINTING_MAPPING.clear()
|
||||
|
||||
door_shuffle = world.options.shuffle_doors
|
||||
|
||||
# Determine the set of exit paintings. All required-exit paintings are included, as are all
|
||||
# required-when-no-doors paintings if door shuffle is off. We then fill the set with random other paintings.
|
||||
chosen_exits = []
|
||||
if door_shuffle == ShuffleDoors.option_none:
|
||||
chosen_exits = [painting_id for painting_id, painting in PAINTINGS.items()
|
||||
if painting.required_when_no_doors]
|
||||
chosen_exits += [painting_id for painting_id, painting in PAINTINGS.items()
|
||||
if painting.exit_only and painting.required]
|
||||
exitable = [painting_id for painting_id, painting in PAINTINGS.items()
|
||||
if not painting.enter_only and not painting.disable and not painting.required]
|
||||
chosen_exits += world.random.sample(exitable, PAINTING_EXITS - len(chosen_exits))
|
||||
|
||||
# Determine the set of entrance paintings.
|
||||
enterable = [painting_id for painting_id, painting in PAINTINGS.items()
|
||||
if not painting.exit_only and not painting.disable and painting_id not in chosen_exits]
|
||||
chosen_entrances = world.random.sample(enterable, PAINTING_ENTRANCES)
|
||||
|
||||
# Create a mapping from entrances to exits.
|
||||
for warp_exit in chosen_exits:
|
||||
warp_enter = world.random.choice(chosen_entrances)
|
||||
|
||||
# Check whether this is a warp from a required painting room to another (or the same) required painting
|
||||
# room. This could cause a cycle that would make certain regions inaccessible.
|
||||
warp_exit_room = PAINTINGS[warp_exit].room
|
||||
warp_enter_room = PAINTINGS[warp_enter].room
|
||||
|
||||
required_painting_rooms = REQUIRED_PAINTING_ROOMS
|
||||
if door_shuffle == ShuffleDoors.option_none:
|
||||
required_painting_rooms += REQUIRED_PAINTING_WHEN_NO_DOORS_ROOMS
|
||||
|
||||
if warp_exit_room in required_painting_rooms and warp_enter_room in required_painting_rooms:
|
||||
# This shuffling is non-workable. Start over.
|
||||
return False
|
||||
|
||||
chosen_entrances.remove(warp_enter)
|
||||
self.PAINTING_MAPPING[warp_enter] = warp_exit
|
||||
|
||||
for warp_enter in chosen_entrances:
|
||||
warp_exit = world.random.choice(chosen_exits)
|
||||
self.PAINTING_MAPPING[warp_enter] = warp_exit
|
||||
|
||||
# The Eye Wall painting is unique in that it is both double-sided and also enter only (because it moves).
|
||||
# There is only one eligible double-sided exit painting, which is the vanilla exit for this warp. If the
|
||||
# exit painting is an entrance in the shuffle, we will disable the Eye Wall painting. Otherwise, Eye Wall
|
||||
# is forced to point to the vanilla exit.
|
||||
if "eye_painting_2" not in self.PAINTING_MAPPING.keys():
|
||||
self.PAINTING_MAPPING["eye_painting"] = "eye_painting_2"
|
||||
|
||||
# Just for sanity's sake, ensure that all required painting rooms are accessed.
|
||||
for painting_id, painting in PAINTINGS.items():
|
||||
if painting_id not in self.PAINTING_MAPPING.values() \
|
||||
and (painting.required or (painting.required_when_no_doors and door_shuffle == 0)):
|
||||
return False
|
||||
|
||||
return True
|
|
@ -0,0 +1,84 @@
|
|||
from typing import Dict, TYPE_CHECKING
|
||||
|
||||
from BaseClasses import ItemClassification, Region
|
||||
from .items import LingoItem
|
||||
from .locations import LingoLocation
|
||||
from .player_logic import LingoPlayerLogic
|
||||
from .rules import lingo_can_use_entrance, lingo_can_use_pilgrimage, make_location_lambda
|
||||
from .static_logic import ALL_ROOMS, PAINTINGS, Room
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import LingoWorld
|
||||
|
||||
|
||||
def create_region(room: Room, world: "LingoWorld", player_logic: LingoPlayerLogic) -> Region:
|
||||
new_region = Region(room.name, world.player, world.multiworld)
|
||||
for location in player_logic.LOCATIONS_BY_ROOM.get(room.name, {}):
|
||||
new_location = LingoLocation(world.player, location.name, location.code, new_region)
|
||||
new_location.access_rule = make_location_lambda(location, room.name, world, player_logic)
|
||||
new_region.locations.append(new_location)
|
||||
if location.name in player_logic.EVENT_LOC_TO_ITEM:
|
||||
event_name = player_logic.EVENT_LOC_TO_ITEM[location.name]
|
||||
event_item = LingoItem(event_name, ItemClassification.progression, None, world.player)
|
||||
new_location.place_locked_item(event_item)
|
||||
|
||||
return new_region
|
||||
|
||||
|
||||
def handle_pilgrim_room(regions: Dict[str, Region], world: "LingoWorld", player_logic: LingoPlayerLogic) -> None:
|
||||
target_region = regions["Pilgrim Antechamber"]
|
||||
source_region = regions["Outside The Agreeable"]
|
||||
source_region.connect(
|
||||
target_region,
|
||||
"Pilgrimage",
|
||||
lambda state: lingo_can_use_pilgrimage(state, world.player, player_logic))
|
||||
|
||||
|
||||
def connect_painting(regions: Dict[str, Region], warp_enter: str, warp_exit: str, world: "LingoWorld",
|
||||
player_logic: LingoPlayerLogic) -> None:
|
||||
source_painting = PAINTINGS[warp_enter]
|
||||
target_painting = PAINTINGS[warp_exit]
|
||||
|
||||
target_region = regions[target_painting.room]
|
||||
source_region = regions[source_painting.room]
|
||||
source_region.connect(
|
||||
target_region,
|
||||
f"{source_painting.room} to {target_painting.room} (Painting)",
|
||||
lambda state: lingo_can_use_entrance(state, target_painting.room, source_painting.required_door, world.player,
|
||||
player_logic))
|
||||
|
||||
|
||||
def create_regions(world: "LingoWorld", player_logic: LingoPlayerLogic) -> None:
|
||||
regions = {
|
||||
"Menu": Region("Menu", world.player, world.multiworld)
|
||||
}
|
||||
|
||||
painting_shuffle = world.options.shuffle_paintings
|
||||
early_color_hallways = world.options.early_color_hallways
|
||||
|
||||
# Instantiate all rooms as regions with their locations first.
|
||||
for room in ALL_ROOMS:
|
||||
regions[room.name] = create_region(room, world, player_logic)
|
||||
|
||||
# Connect all created regions now that they exist.
|
||||
for room in ALL_ROOMS:
|
||||
for entrance in room.entrances:
|
||||
# Don't use the vanilla painting connections if we are shuffling paintings.
|
||||
if entrance.painting and painting_shuffle:
|
||||
continue
|
||||
|
||||
regions[entrance.room].connect(
|
||||
regions[room.name],
|
||||
f"{entrance.room} to {room.name}",
|
||||
lambda state, r=room, e=entrance: lingo_can_use_entrance(state, r.name, e.door, world.player, player_logic))
|
||||
|
||||
handle_pilgrim_room(regions, world, player_logic)
|
||||
|
||||
if early_color_hallways:
|
||||
regions["Starting Room"].connect(regions["Outside The Undeterred"], "Early Color Hallways")
|
||||
|
||||
if painting_shuffle:
|
||||
for warp_enter, warp_exit in player_logic.PAINTING_MAPPING.items():
|
||||
connect_painting(regions, warp_enter, warp_exit, world, player_logic)
|
||||
|
||||
world.multiworld.regions += regions.values()
|
|
@ -0,0 +1,104 @@
|
|||
from typing import TYPE_CHECKING
|
||||
|
||||
from BaseClasses import CollectionState
|
||||
from .options import VictoryCondition
|
||||
from .player_logic import LingoPlayerLogic, PlayerLocation
|
||||
from .static_logic import PANELS_BY_ROOM, PROGRESSION_BY_ROOM, PROGRESSIVE_ITEMS, RoomAndDoor
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import LingoWorld
|
||||
|
||||
|
||||
def lingo_can_use_entrance(state: CollectionState, room: str, door: RoomAndDoor, player: int,
|
||||
player_logic: LingoPlayerLogic):
|
||||
if door is None:
|
||||
return True
|
||||
|
||||
return _lingo_can_open_door(state, room, room if door.room is None else door.room, door.door, player, player_logic)
|
||||
|
||||
|
||||
def lingo_can_use_pilgrimage(state: CollectionState, player: int, player_logic: LingoPlayerLogic):
|
||||
fake_pilgrimage = [
|
||||
["Second Room", "Exit Door"], ["Crossroads", "Tower Entrance"],
|
||||
["Orange Tower Fourth Floor", "Hot Crusts Door"], ["Outside The Initiated", "Shortcut to Hub Room"],
|
||||
["Orange Tower First Floor", "Shortcut to Hub Room"], ["Directional Gallery", "Shortcut to The Undeterred"],
|
||||
["Orange Tower First Floor", "Salt Pepper Door"], ["Hub Room", "Crossroads Entrance"],
|
||||
["Champion's Rest", "Shortcut to The Steady"], ["The Bearer", "Shortcut to The Bold"],
|
||||
["Art Gallery", "Exit"], ["The Tenacious", "Shortcut to Hub Room"],
|
||||
["Outside The Agreeable", "Tenacious Entrance"]
|
||||
]
|
||||
for entrance in fake_pilgrimage:
|
||||
if not state.has(player_logic.ITEM_BY_DOOR[entrance[0]][entrance[1]], player):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def lingo_can_use_location(state: CollectionState, location: PlayerLocation, room_name: str, world: "LingoWorld",
|
||||
player_logic: LingoPlayerLogic):
|
||||
for panel in location.panels:
|
||||
panel_room = room_name if panel.room is None else panel.room
|
||||
if not _lingo_can_solve_panel(state, room_name, panel_room, panel.panel, world, player_logic):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def lingo_can_use_mastery_location(state: CollectionState, world: "LingoWorld"):
|
||||
return state.has("Mastery Achievement", world.player, world.options.mastery_achievements.value)
|
||||
|
||||
|
||||
def _lingo_can_open_door(state: CollectionState, start_room: str, room: str, door: str, player: int,
|
||||
player_logic: LingoPlayerLogic):
|
||||
"""
|
||||
Determines whether a door can be opened
|
||||
"""
|
||||
item_name = player_logic.ITEM_BY_DOOR[room][door]
|
||||
if item_name in PROGRESSIVE_ITEMS:
|
||||
progression = PROGRESSION_BY_ROOM[room][door]
|
||||
return state.has(item_name, player, progression.index)
|
||||
|
||||
return state.has(item_name, player)
|
||||
|
||||
|
||||
def _lingo_can_solve_panel(state: CollectionState, start_room: str, room: str, panel: str, world: "LingoWorld",
|
||||
player_logic: LingoPlayerLogic):
|
||||
"""
|
||||
Determines whether a panel can be solved
|
||||
"""
|
||||
if start_room != room and not state.has(f"{room} (Reached)", world.player):
|
||||
return False
|
||||
|
||||
if room == "Second Room" and panel == "ANOTHER TRY" \
|
||||
and world.options.victory_condition == VictoryCondition.option_level_2 \
|
||||
and not state.has("Counting Panel Solved", world.player, world.options.level_2_requirement.value - 1):
|
||||
return False
|
||||
|
||||
panel_object = PANELS_BY_ROOM[room][panel]
|
||||
for req_room in panel_object.required_rooms:
|
||||
if not state.has(f"{req_room} (Reached)", world.player):
|
||||
return False
|
||||
|
||||
for req_door in panel_object.required_doors:
|
||||
if not _lingo_can_open_door(state, start_room, room if req_door.room is None else req_door.room,
|
||||
req_door.door, world.player, player_logic):
|
||||
return False
|
||||
|
||||
for req_panel in panel_object.required_panels:
|
||||
if not _lingo_can_solve_panel(state, start_room, room if req_panel.room is None else req_panel.room,
|
||||
req_panel.panel, world, player_logic):
|
||||
return False
|
||||
|
||||
if len(panel_object.colors) > 0 and world.options.shuffle_colors:
|
||||
for color in panel_object.colors:
|
||||
if not state.has(color.capitalize(), world.player):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def make_location_lambda(location: PlayerLocation, room_name: str, world: "LingoWorld", player_logic: LingoPlayerLogic):
|
||||
if location.name == player_logic.MASTERY_LOCATION:
|
||||
return lambda state: lingo_can_use_mastery_location(state, world)
|
||||
|
||||
return lambda state: lingo_can_use_location(state, location, room_name, world, player_logic)
|
|
@ -0,0 +1,544 @@
|
|||
from typing import Dict, List, NamedTuple, Optional, Set
|
||||
|
||||
import yaml
|
||||
|
||||
|
||||
class RoomAndDoor(NamedTuple):
|
||||
room: Optional[str]
|
||||
door: str
|
||||
|
||||
|
||||
class RoomAndPanel(NamedTuple):
|
||||
room: Optional[str]
|
||||
panel: str
|
||||
|
||||
|
||||
class RoomEntrance(NamedTuple):
|
||||
room: str # source room
|
||||
door: Optional[RoomAndDoor]
|
||||
painting: bool
|
||||
|
||||
|
||||
class Room(NamedTuple):
|
||||
name: str
|
||||
entrances: List[RoomEntrance]
|
||||
|
||||
|
||||
class Door(NamedTuple):
|
||||
name: str
|
||||
item_name: str
|
||||
location_name: Optional[str]
|
||||
panels: Optional[List[RoomAndPanel]]
|
||||
skip_location: bool
|
||||
skip_item: bool
|
||||
door_ids: List[str]
|
||||
painting_ids: List[str]
|
||||
event: bool
|
||||
group: Optional[str]
|
||||
include_reduce: bool
|
||||
junk_item: bool
|
||||
|
||||
|
||||
class Panel(NamedTuple):
|
||||
required_rooms: List[str]
|
||||
required_doors: List[RoomAndDoor]
|
||||
required_panels: List[RoomAndPanel]
|
||||
colors: List[str]
|
||||
check: bool
|
||||
event: bool
|
||||
internal_ids: List[str]
|
||||
exclude_reduce: bool
|
||||
achievement: bool
|
||||
non_counting: bool
|
||||
|
||||
|
||||
class Painting(NamedTuple):
|
||||
id: str
|
||||
room: str
|
||||
enter_only: bool
|
||||
exit_only: bool
|
||||
orientation: str
|
||||
required: bool
|
||||
required_when_no_doors: bool
|
||||
required_door: Optional[RoomAndDoor]
|
||||
disable: bool
|
||||
move: bool
|
||||
|
||||
|
||||
class Progression(NamedTuple):
|
||||
item_name: str
|
||||
index: int
|
||||
|
||||
|
||||
ROOMS: Dict[str, Room] = {}
|
||||
PANELS: Dict[str, Panel] = {}
|
||||
DOORS: Dict[str, Door] = {}
|
||||
PAINTINGS: Dict[str, Painting] = {}
|
||||
|
||||
ALL_ROOMS: List[Room] = []
|
||||
DOORS_BY_ROOM: Dict[str, Dict[str, Door]] = {}
|
||||
PANELS_BY_ROOM: Dict[str, Dict[str, Panel]] = {}
|
||||
PAINTINGS_BY_ROOM: Dict[str, List[Painting]] = {}
|
||||
|
||||
PROGRESSIVE_ITEMS: List[str] = []
|
||||
PROGRESSION_BY_ROOM: Dict[str, Dict[str, Progression]] = {}
|
||||
|
||||
PAINTING_ENTRANCES: int = 0
|
||||
PAINTING_EXIT_ROOMS: Set[str] = set()
|
||||
PAINTING_EXITS: int = 0
|
||||
REQUIRED_PAINTING_ROOMS: List[str] = []
|
||||
REQUIRED_PAINTING_WHEN_NO_DOORS_ROOMS: List[str] = []
|
||||
|
||||
SPECIAL_ITEM_IDS: Dict[str, int] = {}
|
||||
PANEL_LOCATION_IDS: Dict[str, Dict[str, int]] = {}
|
||||
DOOR_LOCATION_IDS: Dict[str, Dict[str, int]] = {}
|
||||
DOOR_ITEM_IDS: Dict[str, Dict[str, int]] = {}
|
||||
DOOR_GROUP_ITEM_IDS: Dict[str, int] = {}
|
||||
PROGRESSIVE_ITEM_IDS: Dict[str, int] = {}
|
||||
|
||||
|
||||
def load_static_data():
|
||||
global PAINTING_EXITS, SPECIAL_ITEM_IDS, PANEL_LOCATION_IDS, DOOR_LOCATION_IDS, DOOR_ITEM_IDS, \
|
||||
DOOR_GROUP_ITEM_IDS, PROGRESSIVE_ITEM_IDS
|
||||
|
||||
try:
|
||||
from importlib.resources import files
|
||||
except ImportError:
|
||||
from importlib_resources import files
|
||||
|
||||
# Load in all item and location IDs. These are broken up into groups based on the type of item/location.
|
||||
with files("worlds.lingo").joinpath("ids.yaml").open() as file:
|
||||
config = yaml.load(file, Loader=yaml.Loader)
|
||||
|
||||
if "special_items" in config:
|
||||
for item_name, item_id in config["special_items"].items():
|
||||
SPECIAL_ITEM_IDS[item_name] = item_id
|
||||
|
||||
if "panels" in config:
|
||||
for room_name in config["panels"].keys():
|
||||
PANEL_LOCATION_IDS[room_name] = {}
|
||||
|
||||
for panel_name, location_id in config["panels"][room_name].items():
|
||||
PANEL_LOCATION_IDS[room_name][panel_name] = location_id
|
||||
|
||||
if "doors" in config:
|
||||
for room_name in config["doors"].keys():
|
||||
DOOR_LOCATION_IDS[room_name] = {}
|
||||
DOOR_ITEM_IDS[room_name] = {}
|
||||
|
||||
for door_name, door_data in config["doors"][room_name].items():
|
||||
if "location" in door_data:
|
||||
DOOR_LOCATION_IDS[room_name][door_name] = door_data["location"]
|
||||
|
||||
if "item" in door_data:
|
||||
DOOR_ITEM_IDS[room_name][door_name] = door_data["item"]
|
||||
|
||||
if "door_groups" in config:
|
||||
for item_name, item_id in config["door_groups"].items():
|
||||
DOOR_GROUP_ITEM_IDS[item_name] = item_id
|
||||
|
||||
if "progression" in config:
|
||||
for item_name, item_id in config["progression"].items():
|
||||
PROGRESSIVE_ITEM_IDS[item_name] = item_id
|
||||
|
||||
# Process the main world file.
|
||||
with files("worlds.lingo").joinpath("LL1.yaml").open() as file:
|
||||
config = yaml.load(file, Loader=yaml.Loader)
|
||||
|
||||
for room_name, room_data in config.items():
|
||||
process_room(room_name, room_data)
|
||||
|
||||
PAINTING_EXITS = len(PAINTING_EXIT_ROOMS)
|
||||
|
||||
|
||||
def get_special_item_id(name: str):
|
||||
if name not in SPECIAL_ITEM_IDS:
|
||||
raise Exception(f"Item ID for special item {name} not found in ids.yaml.")
|
||||
|
||||
return SPECIAL_ITEM_IDS[name]
|
||||
|
||||
|
||||
def get_panel_location_id(room: str, name: str):
|
||||
if room not in PANEL_LOCATION_IDS or name not in PANEL_LOCATION_IDS[room]:
|
||||
raise Exception(f"Location ID for panel {room} - {name} not found in ids.yaml.")
|
||||
|
||||
return PANEL_LOCATION_IDS[room][name]
|
||||
|
||||
|
||||
def get_door_location_id(room: str, name: str):
|
||||
if room not in DOOR_LOCATION_IDS or name not in DOOR_LOCATION_IDS[room]:
|
||||
raise Exception(f"Location ID for door {room} - {name} not found in ids.yaml.")
|
||||
|
||||
return DOOR_LOCATION_IDS[room][name]
|
||||
|
||||
|
||||
def get_door_item_id(room: str, name: str):
|
||||
if room not in DOOR_ITEM_IDS or name not in DOOR_ITEM_IDS[room]:
|
||||
raise Exception(f"Item ID for door {room} - {name} not found in ids.yaml.")
|
||||
|
||||
return DOOR_ITEM_IDS[room][name]
|
||||
|
||||
|
||||
def get_door_group_item_id(name: str):
|
||||
if name not in DOOR_GROUP_ITEM_IDS:
|
||||
raise Exception(f"Item ID for door group {name} not found in ids.yaml.")
|
||||
|
||||
return DOOR_GROUP_ITEM_IDS[name]
|
||||
|
||||
|
||||
def get_progressive_item_id(name: str):
|
||||
if name not in PROGRESSIVE_ITEM_IDS:
|
||||
raise Exception(f"Item ID for progressive item {name} not found in ids.yaml.")
|
||||
|
||||
return PROGRESSIVE_ITEM_IDS[name]
|
||||
|
||||
|
||||
def process_entrance(source_room, doors, room_obj):
|
||||
global PAINTING_ENTRANCES, PAINTING_EXIT_ROOMS
|
||||
|
||||
# If the value of an entrance is just True, that means that the entrance is always accessible.
|
||||
if doors is True:
|
||||
room_obj.entrances.append(RoomEntrance(source_room, None, False))
|
||||
elif isinstance(doors, dict):
|
||||
# If the value of an entrance is a dictionary, that means the entrance requires a door to be accessible, is a
|
||||
# painting-based entrance, or both.
|
||||
if "painting" in doors and "door" not in doors:
|
||||
PAINTING_EXIT_ROOMS.add(room_obj.name)
|
||||
PAINTING_ENTRANCES += 1
|
||||
|
||||
room_obj.entrances.append(RoomEntrance(source_room, None, True))
|
||||
else:
|
||||
if "painting" in doors and doors["painting"]:
|
||||
PAINTING_EXIT_ROOMS.add(room_obj.name)
|
||||
PAINTING_ENTRANCES += 1
|
||||
|
||||
room_obj.entrances.append(RoomEntrance(source_room, RoomAndDoor(
|
||||
doors["room"] if "room" in doors else None,
|
||||
doors["door"]
|
||||
), doors["painting"] if "painting" in doors else False))
|
||||
else:
|
||||
# If the value of an entrance is a list, then there are multiple possible doors that can give access to the
|
||||
# entrance.
|
||||
for door in doors:
|
||||
if "painting" in door and door["painting"]:
|
||||
PAINTING_EXIT_ROOMS.add(room_obj.name)
|
||||
PAINTING_ENTRANCES += 1
|
||||
|
||||
room_obj.entrances.append(RoomEntrance(source_room, RoomAndDoor(
|
||||
door["room"] if "room" in door else None,
|
||||
door["door"]
|
||||
), door["painting"] if "painting" in door else False))
|
||||
|
||||
|
||||
def process_panel(room_name, panel_name, panel_data):
|
||||
global PANELS, PANELS_BY_ROOM
|
||||
|
||||
full_name = f"{room_name} - {panel_name}"
|
||||
|
||||
# required_room can either be a single room or a list of rooms.
|
||||
if "required_room" in panel_data:
|
||||
if isinstance(panel_data["required_room"], list):
|
||||
required_rooms = panel_data["required_room"]
|
||||
else:
|
||||
required_rooms = [panel_data["required_room"]]
|
||||
else:
|
||||
required_rooms = []
|
||||
|
||||
# required_door can either be a single door or a list of doors. For convenience, the room key for each door does not
|
||||
# need to be specified if the door is in this room.
|
||||
required_doors = list()
|
||||
if "required_door" in panel_data:
|
||||
if isinstance(panel_data["required_door"], dict):
|
||||
door = panel_data["required_door"]
|
||||
required_doors.append(RoomAndDoor(
|
||||
door["room"] if "room" in door else None,
|
||||
door["door"]
|
||||
))
|
||||
else:
|
||||
for door in panel_data["required_door"]:
|
||||
required_doors.append(RoomAndDoor(
|
||||
door["room"] if "room" in door else None,
|
||||
door["door"]
|
||||
))
|
||||
|
||||
# required_panel can either be a single panel or a list of panels. For convenience, the room key for each panel does
|
||||
# not need to be specified if the panel is in this room.
|
||||
required_panels = list()
|
||||
if "required_panel" in panel_data:
|
||||
if isinstance(panel_data["required_panel"], dict):
|
||||
other_panel = panel_data["required_panel"]
|
||||
required_panels.append(RoomAndPanel(
|
||||
other_panel["room"] if "room" in other_panel else None,
|
||||
other_panel["panel"]
|
||||
))
|
||||
else:
|
||||
for other_panel in panel_data["required_panel"]:
|
||||
required_panels.append(RoomAndPanel(
|
||||
other_panel["room"] if "room" in other_panel else None,
|
||||
other_panel["panel"]
|
||||
))
|
||||
|
||||
# colors can either be a single color or a list of colors.
|
||||
if "colors" in panel_data:
|
||||
if isinstance(panel_data["colors"], list):
|
||||
colors = panel_data["colors"]
|
||||
else:
|
||||
colors = [panel_data["colors"]]
|
||||
else:
|
||||
colors = []
|
||||
|
||||
if "check" in panel_data:
|
||||
check = panel_data["check"]
|
||||
else:
|
||||
check = False
|
||||
|
||||
if "event" in panel_data:
|
||||
event = panel_data["event"]
|
||||
else:
|
||||
event = False
|
||||
|
||||
if "achievement" in panel_data:
|
||||
achievement = True
|
||||
else:
|
||||
achievement = False
|
||||
|
||||
if "exclude_reduce" in panel_data:
|
||||
exclude_reduce = panel_data["exclude_reduce"]
|
||||
else:
|
||||
exclude_reduce = False
|
||||
|
||||
if "non_counting" in panel_data:
|
||||
non_counting = panel_data["non_counting"]
|
||||
else:
|
||||
non_counting = False
|
||||
|
||||
if "id" in panel_data:
|
||||
if isinstance(panel_data["id"], list):
|
||||
internal_ids = panel_data["id"]
|
||||
else:
|
||||
internal_ids = [panel_data["id"]]
|
||||
else:
|
||||
internal_ids = []
|
||||
|
||||
panel_obj = Panel(required_rooms, required_doors, required_panels, colors, check, event, internal_ids,
|
||||
exclude_reduce, achievement, non_counting)
|
||||
PANELS[full_name] = panel_obj
|
||||
PANELS_BY_ROOM[room_name][panel_name] = panel_obj
|
||||
|
||||
|
||||
def process_door(room_name, door_name, door_data):
|
||||
global DOORS, DOORS_BY_ROOM
|
||||
|
||||
# The item name associated with a door can be explicitly specified in the configuration. If it is not, it is
|
||||
# generated from the room and door name.
|
||||
if "item_name" in door_data:
|
||||
item_name = door_data["item_name"]
|
||||
else:
|
||||
item_name = f"{room_name} - {door_name}"
|
||||
|
||||
if "skip_location" in door_data:
|
||||
skip_location = door_data["skip_location"]
|
||||
else:
|
||||
skip_location = False
|
||||
|
||||
if "skip_item" in door_data:
|
||||
skip_item = door_data["skip_item"]
|
||||
else:
|
||||
skip_item = False
|
||||
|
||||
if "event" in door_data:
|
||||
event = door_data["event"]
|
||||
else:
|
||||
event = False
|
||||
|
||||
if "include_reduce" in door_data:
|
||||
include_reduce = door_data["include_reduce"]
|
||||
else:
|
||||
include_reduce = False
|
||||
|
||||
if "junk_item" in door_data:
|
||||
junk_item = door_data["junk_item"]
|
||||
else:
|
||||
junk_item = False
|
||||
|
||||
if "group" in door_data:
|
||||
group = door_data["group"]
|
||||
else:
|
||||
group = None
|
||||
|
||||
# panels is a list of panels. Each panel can either be a simple string (the name of a panel in the current room) or
|
||||
# a dictionary specifying a panel in a different room.
|
||||
if "panels" in door_data:
|
||||
panels = list()
|
||||
for panel in door_data["panels"]:
|
||||
if isinstance(panel, dict):
|
||||
panels.append(RoomAndPanel(panel["room"], panel["panel"]))
|
||||
else:
|
||||
panels.append(RoomAndPanel(None, panel))
|
||||
else:
|
||||
skip_location = True
|
||||
panels = None
|
||||
|
||||
# The location name associated with a door can be explicitly specified in the configuration. If it is not, then the
|
||||
# name is generated using a combination of all of the panels that would ordinarily open the door. This can get quite
|
||||
# messy if there are a lot of panels, especially if panels from multiple rooms are involved, so in these cases it
|
||||
# would be better to specify a name.
|
||||
if "location_name" in door_data:
|
||||
location_name = door_data["location_name"]
|
||||
elif skip_location is False:
|
||||
panel_per_room = dict()
|
||||
for panel in panels:
|
||||
panel_room_name = room_name if panel.room is None else panel.room
|
||||
panel_per_room.setdefault(panel_room_name, []).append(panel.panel)
|
||||
|
||||
room_strs = list()
|
||||
for door_room_str, door_panels_str in panel_per_room.items():
|
||||
room_strs.append(door_room_str + " - " + ", ".join(door_panels_str))
|
||||
|
||||
location_name = " and ".join(room_strs)
|
||||
else:
|
||||
location_name = None
|
||||
|
||||
# The id field can be a single item, or a list of door IDs, in the event that the item for this logical door should
|
||||
# open more than one actual in-game door.
|
||||
if "id" in door_data:
|
||||
if isinstance(door_data["id"], list):
|
||||
door_ids = door_data["id"]
|
||||
else:
|
||||
door_ids = [door_data["id"]]
|
||||
else:
|
||||
door_ids = []
|
||||
|
||||
# The painting_id field can be a single item, or a list of painting IDs, in the event that the item for this logical
|
||||
# door should move more than one actual in-game painting.
|
||||
if "painting_id" in door_data:
|
||||
if isinstance(door_data["painting_id"], list):
|
||||
painting_ids = door_data["painting_id"]
|
||||
else:
|
||||
painting_ids = [door_data["painting_id"]]
|
||||
else:
|
||||
painting_ids = []
|
||||
|
||||
door_obj = Door(door_name, item_name, location_name, panels, skip_location, skip_item, door_ids,
|
||||
painting_ids, event, group, include_reduce, junk_item)
|
||||
|
||||
DOORS[door_obj.item_name] = door_obj
|
||||
DOORS_BY_ROOM[room_name][door_name] = door_obj
|
||||
|
||||
|
||||
def process_painting(room_name, painting_data):
|
||||
global PAINTINGS, PAINTINGS_BY_ROOM, REQUIRED_PAINTING_ROOMS, REQUIRED_PAINTING_WHEN_NO_DOORS_ROOMS
|
||||
|
||||
# Read in information about this painting and store it in an object.
|
||||
painting_id = painting_data["id"]
|
||||
|
||||
if "orientation" in painting_data:
|
||||
orientation = painting_data["orientation"]
|
||||
else:
|
||||
orientation = ""
|
||||
|
||||
if "disable" in painting_data:
|
||||
disable_painting = painting_data["disable"]
|
||||
else:
|
||||
disable_painting = False
|
||||
|
||||
if "required" in painting_data:
|
||||
required_painting = painting_data["required"]
|
||||
if required_painting:
|
||||
REQUIRED_PAINTING_ROOMS.append(room_name)
|
||||
else:
|
||||
required_painting = False
|
||||
|
||||
if "move" in painting_data:
|
||||
move_painting = painting_data["move"]
|
||||
else:
|
||||
move_painting = False
|
||||
|
||||
if "required_when_no_doors" in painting_data:
|
||||
rwnd = painting_data["required_when_no_doors"]
|
||||
if rwnd:
|
||||
REQUIRED_PAINTING_WHEN_NO_DOORS_ROOMS.append(room_name)
|
||||
else:
|
||||
rwnd = False
|
||||
|
||||
if "exit_only" in painting_data:
|
||||
exit_only = painting_data["exit_only"]
|
||||
else:
|
||||
exit_only = False
|
||||
|
||||
if "enter_only" in painting_data:
|
||||
enter_only = painting_data["enter_only"]
|
||||
else:
|
||||
enter_only = False
|
||||
|
||||
required_door = None
|
||||
if "required_door" in painting_data:
|
||||
door = painting_data["required_door"]
|
||||
required_door = RoomAndDoor(
|
||||
door["room"] if "room" in door else room_name,
|
||||
door["door"]
|
||||
)
|
||||
|
||||
painting_obj = Painting(painting_id, room_name, enter_only, exit_only, orientation,
|
||||
required_painting, rwnd, required_door, disable_painting, move_painting)
|
||||
PAINTINGS[painting_id] = painting_obj
|
||||
PAINTINGS_BY_ROOM[room_name].append(painting_obj)
|
||||
|
||||
|
||||
def process_progression(room_name, progression_name, progression_doors):
|
||||
global PROGRESSIVE_ITEMS, PROGRESSION_BY_ROOM
|
||||
|
||||
# Progressive items are configured as a list of doors.
|
||||
PROGRESSIVE_ITEMS.append(progression_name)
|
||||
|
||||
progression_index = 1
|
||||
for door in progression_doors:
|
||||
if isinstance(door, Dict):
|
||||
door_room = door["room"]
|
||||
door_door = door["door"]
|
||||
else:
|
||||
door_room = room_name
|
||||
door_door = door
|
||||
|
||||
room_progressions = PROGRESSION_BY_ROOM.setdefault(door_room, {})
|
||||
room_progressions[door_door] = Progression(progression_name, progression_index)
|
||||
progression_index += 1
|
||||
|
||||
|
||||
def process_room(room_name, room_data):
|
||||
global ROOMS, ALL_ROOMS
|
||||
|
||||
room_obj = Room(room_name, [])
|
||||
|
||||
if "entrances" in room_data:
|
||||
for source_room, doors in room_data["entrances"].items():
|
||||
process_entrance(source_room, doors, room_obj)
|
||||
|
||||
if "panels" in room_data:
|
||||
PANELS_BY_ROOM[room_name] = dict()
|
||||
|
||||
for panel_name, panel_data in room_data["panels"].items():
|
||||
process_panel(room_name, panel_name, panel_data)
|
||||
|
||||
if "doors" in room_data:
|
||||
DOORS_BY_ROOM[room_name] = dict()
|
||||
|
||||
for door_name, door_data in room_data["doors"].items():
|
||||
process_door(room_name, door_name, door_data)
|
||||
|
||||
if "paintings" in room_data:
|
||||
PAINTINGS_BY_ROOM[room_name] = []
|
||||
|
||||
for painting_data in room_data["paintings"]:
|
||||
process_painting(room_name, painting_data)
|
||||
|
||||
if "progression" in room_data:
|
||||
for progression_name, progression_doors in room_data["progression"].items():
|
||||
process_progression(room_name, progression_name, progression_doors)
|
||||
|
||||
ROOMS[room_name] = room_obj
|
||||
ALL_ROOMS.append(room_obj)
|
||||
|
||||
|
||||
# Initialize the static data at module scope.
|
||||
load_static_data()
|
|
@ -0,0 +1,89 @@
|
|||
from . import LingoTestBase
|
||||
|
||||
|
||||
class TestRequiredRoomLogic(LingoTestBase):
|
||||
options = {
|
||||
"shuffle_doors": "complex"
|
||||
}
|
||||
|
||||
def test_pilgrim_first(self) -> None:
|
||||
self.assertFalse(self.multiworld.state.can_reach("The Seeker", "Region", self.player))
|
||||
self.assertFalse(self.multiworld.state.can_reach("Pilgrim Antechamber", "Region", self.player))
|
||||
self.assertFalse(self.multiworld.state.can_reach("Pilgrim Room", "Region", self.player))
|
||||
self.assertFalse(self.can_reach_location("The Seeker - Achievement"))
|
||||
|
||||
self.collect_by_name("Pilgrim Room - Sun Painting")
|
||||
self.assertFalse(self.multiworld.state.can_reach("The Seeker", "Region", self.player))
|
||||
self.assertTrue(self.multiworld.state.can_reach("Pilgrim Antechamber", "Region", self.player))
|
||||
self.assertTrue(self.multiworld.state.can_reach("Pilgrim Room", "Region", self.player))
|
||||
self.assertFalse(self.can_reach_location("The Seeker - Achievement"))
|
||||
|
||||
self.collect_by_name("Pilgrim Room - Shortcut to The Seeker")
|
||||
self.assertTrue(self.multiworld.state.can_reach("The Seeker", "Region", self.player))
|
||||
self.assertTrue(self.multiworld.state.can_reach("Pilgrim Room", "Region", self.player))
|
||||
self.assertFalse(self.can_reach_location("The Seeker - Achievement"))
|
||||
|
||||
self.collect_by_name("Starting Room - Back Right Door")
|
||||
self.assertTrue(self.can_reach_location("The Seeker - Achievement"))
|
||||
|
||||
def test_hidden_first(self) -> None:
|
||||
self.assertFalse(self.multiworld.state.can_reach("The Seeker", "Region", self.player))
|
||||
self.assertFalse(self.multiworld.state.can_reach("Pilgrim Room", "Region", self.player))
|
||||
self.assertFalse(self.can_reach_location("The Seeker - Achievement"))
|
||||
|
||||
self.collect_by_name("Starting Room - Back Right Door")
|
||||
self.assertFalse(self.multiworld.state.can_reach("The Seeker", "Region", self.player))
|
||||
self.assertFalse(self.multiworld.state.can_reach("Pilgrim Room", "Region", self.player))
|
||||
self.assertFalse(self.can_reach_location("The Seeker - Achievement"))
|
||||
|
||||
self.collect_by_name("Pilgrim Room - Shortcut to The Seeker")
|
||||
self.assertFalse(self.multiworld.state.can_reach("The Seeker", "Region", self.player))
|
||||
self.assertFalse(self.multiworld.state.can_reach("Pilgrim Room", "Region", self.player))
|
||||
self.assertFalse(self.can_reach_location("The Seeker - Achievement"))
|
||||
|
||||
self.collect_by_name("Pilgrim Room - Sun Painting")
|
||||
self.assertTrue(self.multiworld.state.can_reach("The Seeker", "Region", self.player))
|
||||
self.assertTrue(self.multiworld.state.can_reach("Pilgrim Room", "Region", self.player))
|
||||
self.assertTrue(self.can_reach_location("The Seeker - Achievement"))
|
||||
|
||||
|
||||
class TestRequiredDoorLogic(LingoTestBase):
|
||||
options = {
|
||||
"shuffle_doors": "complex"
|
||||
}
|
||||
|
||||
def test_through_rhyme(self) -> None:
|
||||
self.assertFalse(self.can_reach_location("Rhyme Room - Circle/Looped Square Wall"))
|
||||
|
||||
self.collect_by_name("Starting Room - Rhyme Room Entrance")
|
||||
self.assertFalse(self.can_reach_location("Rhyme Room - Circle/Looped Square Wall"))
|
||||
|
||||
self.collect_by_name("Rhyme Room (Looped Square) - Door to Circle")
|
||||
self.assertTrue(self.can_reach_location("Rhyme Room - Circle/Looped Square Wall"))
|
||||
|
||||
def test_through_hidden(self) -> None:
|
||||
self.assertFalse(self.can_reach_location("Rhyme Room - Circle/Looped Square Wall"))
|
||||
|
||||
self.collect_by_name("Starting Room - Rhyme Room Entrance")
|
||||
self.assertFalse(self.can_reach_location("Rhyme Room - Circle/Looped Square Wall"))
|
||||
|
||||
self.collect_by_name("Starting Room - Back Right Door")
|
||||
self.assertFalse(self.can_reach_location("Rhyme Room - Circle/Looped Square Wall"))
|
||||
|
||||
self.collect_by_name("Hidden Room - Rhyme Room Entrance")
|
||||
self.assertTrue(self.can_reach_location("Rhyme Room - Circle/Looped Square Wall"))
|
||||
|
||||
|
||||
class TestSimpleDoors(LingoTestBase):
|
||||
options = {
|
||||
"shuffle_doors": "simple"
|
||||
}
|
||||
|
||||
def test_requirement(self):
|
||||
self.assertFalse(self.multiworld.state.can_reach("Outside The Wanderer", "Region", self.player))
|
||||
self.assertFalse(self.multiworld.state.can_reach("Orange Tower Third Floor", "Region", self.player))
|
||||
|
||||
self.collect_by_name("Rhyme Room Doors")
|
||||
self.assertTrue(self.multiworld.state.can_reach("Outside The Wanderer", "Region", self.player))
|
||||
self.assertTrue(self.multiworld.state.can_reach("Orange Tower Third Floor", "Region", self.player))
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
from . import LingoTestBase
|
||||
|
||||
|
||||
class TestMasteryWhenVictoryIsTheEnd(LingoTestBase):
|
||||
options = {
|
||||
"mastery_achievements": "22",
|
||||
"victory_condition": "the_end",
|
||||
"shuffle_colors": "true"
|
||||
}
|
||||
|
||||
def test_requirement(self):
|
||||
self.assertFalse(self.multiworld.state.can_reach("Orange Tower Seventh Floor", "Region", self.player))
|
||||
|
||||
self.collect_by_name(["Red", "Blue", "Black", "Purple", "Orange"])
|
||||
self.assertTrue(self.multiworld.state.can_reach("Orange Tower Seventh Floor", "Region", self.player))
|
||||
self.assertTrue(self.can_reach_location("The End (Solved)"))
|
||||
self.assertFalse(self.can_reach_location("Orange Tower Seventh Floor - THE MASTER"))
|
||||
|
||||
self.collect_by_name(["Green", "Brown", "Yellow"])
|
||||
self.assertTrue(self.can_reach_location("Orange Tower Seventh Floor - THE MASTER"))
|
||||
|
||||
|
||||
class TestMasteryWhenVictoryIsTheMaster(LingoTestBase):
|
||||
options = {
|
||||
"mastery_achievements": "24",
|
||||
"victory_condition": "the_master",
|
||||
"shuffle_colors": "true"
|
||||
}
|
||||
|
||||
def test_requirement(self):
|
||||
self.assertFalse(self.multiworld.state.can_reach("Orange Tower Seventh Floor", "Region", self.player))
|
||||
|
||||
self.collect_by_name(["Red", "Blue", "Black", "Purple", "Orange"])
|
||||
self.assertTrue(self.multiworld.state.can_reach("Orange Tower Seventh Floor", "Region", self.player))
|
||||
self.assertTrue(self.can_reach_location("Orange Tower Seventh Floor - THE END"))
|
||||
self.assertFalse(self.can_reach_location("Orange Tower Seventh Floor - Mastery Achievements"))
|
||||
|
||||
self.collect_by_name(["Green", "Gray", "Brown", "Yellow"])
|
||||
self.assertTrue(self.can_reach_location("Orange Tower Seventh Floor - Mastery Achievements"))
|
|
@ -0,0 +1,31 @@
|
|||
from . import LingoTestBase
|
||||
|
||||
|
||||
class TestMultiShuffleOptions(LingoTestBase):
|
||||
options = {
|
||||
"shuffle_doors": "complex",
|
||||
"progressive_orange_tower": "true",
|
||||
"shuffle_colors": "true",
|
||||
"shuffle_paintings": "true",
|
||||
"early_color_hallways": "true"
|
||||
}
|
||||
|
||||
|
||||
class TestPanelsanity(LingoTestBase):
|
||||
options = {
|
||||
"shuffle_doors": "complex",
|
||||
"progressive_orange_tower": "true",
|
||||
"location_checks": "insanity",
|
||||
"shuffle_colors": "true"
|
||||
}
|
||||
|
||||
|
||||
class TestAllPanelHunt(LingoTestBase):
|
||||
options = {
|
||||
"shuffle_doors": "complex",
|
||||
"progressive_orange_tower": "true",
|
||||
"shuffle_colors": "true",
|
||||
"victory_condition": "level_2",
|
||||
"level_2_requirement": "800",
|
||||
"early_color_hallways": "true"
|
||||
}
|
|
@ -0,0 +1,175 @@
|
|||
from . import LingoTestBase
|
||||
|
||||
|
||||
class TestProgressiveOrangeTower(LingoTestBase):
|
||||
options = {
|
||||
"shuffle_doors": "complex",
|
||||
"progressive_orange_tower": "true"
|
||||
}
|
||||
|
||||
def test_from_welcome_back(self) -> None:
|
||||
self.assertFalse(self.multiworld.state.can_reach("Orange Tower First Floor", "Region", self.player))
|
||||
self.assertFalse(self.multiworld.state.can_reach("Orange Tower Second Floor", "Region", self.player))
|
||||
self.assertFalse(self.multiworld.state.can_reach("Orange Tower Third Floor", "Region", self.player))
|
||||
self.assertFalse(self.multiworld.state.can_reach("Orange Tower Fourth Floor", "Region", self.player))
|
||||
self.assertFalse(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player))
|
||||
self.assertFalse(self.multiworld.state.can_reach("Orange Tower Sixth Floor", "Region", self.player))
|
||||
self.assertFalse(self.multiworld.state.can_reach("Orange Tower Seventh Floor", "Region", self.player))
|
||||
|
||||
self.collect_by_name("Welcome Back Area - Shortcut to Starting Room")
|
||||
self.collect_by_name("Orange Tower Fifth Floor - Welcome Back")
|
||||
self.assertFalse(self.multiworld.state.can_reach("Orange Tower First Floor", "Region", self.player))
|
||||
self.assertFalse(self.multiworld.state.can_reach("Orange Tower Second Floor", "Region", self.player))
|
||||
self.assertFalse(self.multiworld.state.can_reach("Orange Tower Third Floor", "Region", self.player))
|
||||
self.assertFalse(self.multiworld.state.can_reach("Orange Tower Fourth Floor", "Region", self.player))
|
||||
self.assertTrue(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player))
|
||||
self.assertFalse(self.multiworld.state.can_reach("Orange Tower Sixth Floor", "Region", self.player))
|
||||
self.assertFalse(self.multiworld.state.can_reach("Orange Tower Seventh Floor", "Region", self.player))
|
||||
|
||||
progressive_tower = self.get_items_by_name("Progressive Orange Tower")
|
||||
|
||||
self.collect(progressive_tower[0])
|
||||
self.assertFalse(self.multiworld.state.can_reach("Orange Tower First Floor", "Region", self.player))
|
||||
self.assertFalse(self.multiworld.state.can_reach("Orange Tower Second Floor", "Region", self.player))
|
||||
self.assertFalse(self.multiworld.state.can_reach("Orange Tower Third Floor", "Region", self.player))
|
||||
self.assertFalse(self.multiworld.state.can_reach("Orange Tower Fourth Floor", "Region", self.player))
|
||||
self.assertTrue(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player))
|
||||
self.assertFalse(self.multiworld.state.can_reach("Orange Tower Sixth Floor", "Region", self.player))
|
||||
self.assertFalse(self.multiworld.state.can_reach("Orange Tower Seventh Floor", "Region", self.player))
|
||||
|
||||
self.collect(progressive_tower[1])
|
||||
self.assertFalse(self.multiworld.state.can_reach("Orange Tower First Floor", "Region", self.player))
|
||||
self.assertFalse(self.multiworld.state.can_reach("Orange Tower Second Floor", "Region", self.player))
|
||||
self.assertFalse(self.multiworld.state.can_reach("Orange Tower Third Floor", "Region", self.player))
|
||||
self.assertFalse(self.multiworld.state.can_reach("Orange Tower Fourth Floor", "Region", self.player))
|
||||
self.assertTrue(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player))
|
||||
self.assertFalse(self.multiworld.state.can_reach("Orange Tower Sixth Floor", "Region", self.player))
|
||||
self.assertFalse(self.multiworld.state.can_reach("Orange Tower Seventh Floor", "Region", self.player))
|
||||
|
||||
self.collect(progressive_tower[2])
|
||||
self.assertFalse(self.multiworld.state.can_reach("Orange Tower First Floor", "Region", self.player))
|
||||
self.assertFalse(self.multiworld.state.can_reach("Orange Tower Second Floor", "Region", self.player))
|
||||
self.assertFalse(self.multiworld.state.can_reach("Orange Tower Third Floor", "Region", self.player))
|
||||
self.assertFalse(self.multiworld.state.can_reach("Orange Tower Fourth Floor", "Region", self.player))
|
||||
self.assertTrue(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player))
|
||||
self.assertFalse(self.multiworld.state.can_reach("Orange Tower Sixth Floor", "Region", self.player))
|
||||
self.assertFalse(self.multiworld.state.can_reach("Orange Tower Seventh Floor", "Region", self.player))
|
||||
|
||||
self.collect(progressive_tower[3])
|
||||
self.assertTrue(self.multiworld.state.can_reach("Orange Tower First Floor", "Region", self.player))
|
||||
self.assertTrue(self.multiworld.state.can_reach("Orange Tower Second Floor", "Region", self.player))
|
||||
self.assertTrue(self.multiworld.state.can_reach("Orange Tower Third Floor", "Region", self.player))
|
||||
self.assertTrue(self.multiworld.state.can_reach("Orange Tower Fourth Floor", "Region", self.player))
|
||||
self.assertTrue(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player))
|
||||
self.assertFalse(self.multiworld.state.can_reach("Orange Tower Sixth Floor", "Region", self.player))
|
||||
self.assertFalse(self.multiworld.state.can_reach("Orange Tower Seventh Floor", "Region", self.player))
|
||||
|
||||
self.collect(progressive_tower[4])
|
||||
self.assertTrue(self.multiworld.state.can_reach("Orange Tower First Floor", "Region", self.player))
|
||||
self.assertTrue(self.multiworld.state.can_reach("Orange Tower Second Floor", "Region", self.player))
|
||||
self.assertTrue(self.multiworld.state.can_reach("Orange Tower Third Floor", "Region", self.player))
|
||||
self.assertTrue(self.multiworld.state.can_reach("Orange Tower Fourth Floor", "Region", self.player))
|
||||
self.assertTrue(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player))
|
||||
self.assertTrue(self.multiworld.state.can_reach("Orange Tower Sixth Floor", "Region", self.player))
|
||||
self.assertFalse(self.multiworld.state.can_reach("Orange Tower Seventh Floor", "Region", self.player))
|
||||
|
||||
self.collect(progressive_tower[5])
|
||||
self.assertTrue(self.multiworld.state.can_reach("Orange Tower First Floor", "Region", self.player))
|
||||
self.assertTrue(self.multiworld.state.can_reach("Orange Tower Second Floor", "Region", self.player))
|
||||
self.assertTrue(self.multiworld.state.can_reach("Orange Tower Third Floor", "Region", self.player))
|
||||
self.assertTrue(self.multiworld.state.can_reach("Orange Tower Fourth Floor", "Region", self.player))
|
||||
self.assertTrue(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player))
|
||||
self.assertTrue(self.multiworld.state.can_reach("Orange Tower Sixth Floor", "Region", self.player))
|
||||
self.assertTrue(self.multiworld.state.can_reach("Orange Tower Seventh Floor", "Region", self.player))
|
||||
|
||||
def test_from_hub_room(self) -> None:
|
||||
self.assertFalse(self.multiworld.state.can_reach("Orange Tower First Floor", "Region", self.player))
|
||||
self.assertFalse(self.multiworld.state.can_reach("Orange Tower Second Floor", "Region", self.player))
|
||||
self.assertFalse(self.multiworld.state.can_reach("Orange Tower Third Floor", "Region", self.player))
|
||||
self.assertFalse(self.multiworld.state.can_reach("Orange Tower Fourth Floor", "Region", self.player))
|
||||
self.assertFalse(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player))
|
||||
self.assertFalse(self.multiworld.state.can_reach("Orange Tower Sixth Floor", "Region", self.player))
|
||||
self.assertFalse(self.multiworld.state.can_reach("Orange Tower Seventh Floor", "Region", self.player))
|
||||
|
||||
self.collect_by_name("Second Room - Exit Door")
|
||||
self.assertFalse(self.multiworld.state.can_reach("Orange Tower First Floor", "Region", self.player))
|
||||
self.assertTrue(self.multiworld.state.can_reach("Orange Tower Second Floor", "Region", self.player))
|
||||
self.assertFalse(self.multiworld.state.can_reach("Orange Tower Third Floor", "Region", self.player))
|
||||
self.assertFalse(self.multiworld.state.can_reach("Orange Tower Fourth Floor", "Region", self.player))
|
||||
self.assertFalse(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player))
|
||||
self.assertFalse(self.multiworld.state.can_reach("Orange Tower Sixth Floor", "Region", self.player))
|
||||
self.assertFalse(self.multiworld.state.can_reach("Orange Tower Seventh Floor", "Region", self.player))
|
||||
|
||||
self.collect_by_name("Orange Tower First Floor - Shortcut to Hub Room")
|
||||
self.assertTrue(self.multiworld.state.can_reach("Orange Tower First Floor", "Region", self.player))
|
||||
self.assertTrue(self.multiworld.state.can_reach("Orange Tower Second Floor", "Region", self.player))
|
||||
self.assertFalse(self.multiworld.state.can_reach("Orange Tower Third Floor", "Region", self.player))
|
||||
self.assertFalse(self.multiworld.state.can_reach("Orange Tower Fourth Floor", "Region", self.player))
|
||||
self.assertFalse(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player))
|
||||
self.assertFalse(self.multiworld.state.can_reach("Orange Tower Sixth Floor", "Region", self.player))
|
||||
self.assertFalse(self.multiworld.state.can_reach("Orange Tower Seventh Floor", "Region", self.player))
|
||||
|
||||
progressive_tower = self.get_items_by_name("Progressive Orange Tower")
|
||||
|
||||
self.collect(progressive_tower[0])
|
||||
self.assertTrue(self.multiworld.state.can_reach("Orange Tower First Floor", "Region", self.player))
|
||||
self.assertTrue(self.multiworld.state.can_reach("Orange Tower Second Floor", "Region", self.player))
|
||||
self.assertFalse(self.multiworld.state.can_reach("Orange Tower Third Floor", "Region", self.player))
|
||||
self.assertFalse(self.multiworld.state.can_reach("Orange Tower Fourth Floor", "Region", self.player))
|
||||
self.assertFalse(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player))
|
||||
self.assertFalse(self.multiworld.state.can_reach("Orange Tower Sixth Floor", "Region", self.player))
|
||||
self.assertFalse(self.multiworld.state.can_reach("Orange Tower Seventh Floor", "Region", self.player))
|
||||
|
||||
self.remove(self.get_item_by_name("Orange Tower First Floor - Shortcut to Hub Room"))
|
||||
self.assertTrue(self.multiworld.state.can_reach("Orange Tower First Floor", "Region", self.player))
|
||||
self.assertTrue(self.multiworld.state.can_reach("Orange Tower Second Floor", "Region", self.player))
|
||||
self.assertFalse(self.multiworld.state.can_reach("Orange Tower Third Floor", "Region", self.player))
|
||||
self.assertFalse(self.multiworld.state.can_reach("Orange Tower Fourth Floor", "Region", self.player))
|
||||
self.assertFalse(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player))
|
||||
self.assertFalse(self.multiworld.state.can_reach("Orange Tower Sixth Floor", "Region", self.player))
|
||||
self.assertFalse(self.multiworld.state.can_reach("Orange Tower Seventh Floor", "Region", self.player))
|
||||
|
||||
self.collect(progressive_tower[1])
|
||||
self.assertTrue(self.multiworld.state.can_reach("Orange Tower First Floor", "Region", self.player))
|
||||
self.assertTrue(self.multiworld.state.can_reach("Orange Tower Second Floor", "Region", self.player))
|
||||
self.assertTrue(self.multiworld.state.can_reach("Orange Tower Third Floor", "Region", self.player))
|
||||
self.assertFalse(self.multiworld.state.can_reach("Orange Tower Fourth Floor", "Region", self.player))
|
||||
self.assertFalse(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player))
|
||||
self.assertFalse(self.multiworld.state.can_reach("Orange Tower Sixth Floor", "Region", self.player))
|
||||
self.assertFalse(self.multiworld.state.can_reach("Orange Tower Seventh Floor", "Region", self.player))
|
||||
|
||||
self.collect(progressive_tower[2])
|
||||
self.assertTrue(self.multiworld.state.can_reach("Orange Tower First Floor", "Region", self.player))
|
||||
self.assertTrue(self.multiworld.state.can_reach("Orange Tower Second Floor", "Region", self.player))
|
||||
self.assertTrue(self.multiworld.state.can_reach("Orange Tower Third Floor", "Region", self.player))
|
||||
self.assertTrue(self.multiworld.state.can_reach("Orange Tower Fourth Floor", "Region", self.player))
|
||||
self.assertFalse(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player))
|
||||
self.assertFalse(self.multiworld.state.can_reach("Orange Tower Sixth Floor", "Region", self.player))
|
||||
self.assertFalse(self.multiworld.state.can_reach("Orange Tower Seventh Floor", "Region", self.player))
|
||||
|
||||
self.collect(progressive_tower[3])
|
||||
self.assertTrue(self.multiworld.state.can_reach("Orange Tower First Floor", "Region", self.player))
|
||||
self.assertTrue(self.multiworld.state.can_reach("Orange Tower Second Floor", "Region", self.player))
|
||||
self.assertTrue(self.multiworld.state.can_reach("Orange Tower Third Floor", "Region", self.player))
|
||||
self.assertTrue(self.multiworld.state.can_reach("Orange Tower Fourth Floor", "Region", self.player))
|
||||
self.assertTrue(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player))
|
||||
self.assertFalse(self.multiworld.state.can_reach("Orange Tower Sixth Floor", "Region", self.player))
|
||||
self.assertFalse(self.multiworld.state.can_reach("Orange Tower Seventh Floor", "Region", self.player))
|
||||
|
||||
self.collect(progressive_tower[4])
|
||||
self.assertTrue(self.multiworld.state.can_reach("Orange Tower First Floor", "Region", self.player))
|
||||
self.assertTrue(self.multiworld.state.can_reach("Orange Tower Second Floor", "Region", self.player))
|
||||
self.assertTrue(self.multiworld.state.can_reach("Orange Tower Third Floor", "Region", self.player))
|
||||
self.assertTrue(self.multiworld.state.can_reach("Orange Tower Fourth Floor", "Region", self.player))
|
||||
self.assertTrue(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player))
|
||||
self.assertTrue(self.multiworld.state.can_reach("Orange Tower Sixth Floor", "Region", self.player))
|
||||
self.assertFalse(self.multiworld.state.can_reach("Orange Tower Seventh Floor", "Region", self.player))
|
||||
|
||||
self.collect(progressive_tower[5])
|
||||
self.assertTrue(self.multiworld.state.can_reach("Orange Tower First Floor", "Region", self.player))
|
||||
self.assertTrue(self.multiworld.state.can_reach("Orange Tower Second Floor", "Region", self.player))
|
||||
self.assertTrue(self.multiworld.state.can_reach("Orange Tower Third Floor", "Region", self.player))
|
||||
self.assertTrue(self.multiworld.state.can_reach("Orange Tower Fourth Floor", "Region", self.player))
|
||||
self.assertTrue(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player))
|
||||
self.assertTrue(self.multiworld.state.can_reach("Orange Tower Sixth Floor", "Region", self.player))
|
||||
self.assertTrue(self.multiworld.state.can_reach("Orange Tower Seventh Floor", "Region", self.player))
|
|
@ -0,0 +1,191 @@
|
|||
from . import LingoTestBase
|
||||
|
||||
|
||||
class TestComplexProgressiveHallwayRoom(LingoTestBase):
|
||||
options = {
|
||||
"shuffle_doors": "complex"
|
||||
}
|
||||
|
||||
def test_item(self):
|
||||
self.assertFalse(self.multiworld.state.can_reach("Outside The Agreeable", "Region", self.player))
|
||||
self.assertFalse(self.multiworld.state.can_reach("Hallway Room (2)", "Region", self.player))
|
||||
self.assertFalse(self.multiworld.state.can_reach("Hallway Room (3)", "Region", self.player))
|
||||
self.assertFalse(self.multiworld.state.can_reach("Hallway Room (4)", "Region", self.player))
|
||||
self.assertFalse(self.multiworld.state.can_reach("Elements Area", "Region", self.player))
|
||||
|
||||
self.collect_by_name(["Second Room - Exit Door", "The Tenacious - Shortcut to Hub Room",
|
||||
"Outside The Agreeable - Tenacious Entrance"])
|
||||
self.assertTrue(self.multiworld.state.can_reach("Outside The Agreeable", "Region", self.player))
|
||||
self.assertFalse(self.multiworld.state.can_reach("Hallway Room (2)", "Region", self.player))
|
||||
self.assertFalse(self.multiworld.state.can_reach("Hallway Room (3)", "Region", self.player))
|
||||
self.assertFalse(self.multiworld.state.can_reach("Hallway Room (4)", "Region", self.player))
|
||||
self.assertFalse(self.multiworld.state.can_reach("Elements Area", "Region", self.player))
|
||||
|
||||
progressive_hallway_room = self.get_items_by_name("Progressive Hallway Room")
|
||||
|
||||
self.collect(progressive_hallway_room[0])
|
||||
self.assertTrue(self.multiworld.state.can_reach("Outside The Agreeable", "Region", self.player))
|
||||
self.assertTrue(self.multiworld.state.can_reach("Hallway Room (2)", "Region", self.player))
|
||||
self.assertFalse(self.multiworld.state.can_reach("Hallway Room (3)", "Region", self.player))
|
||||
self.assertFalse(self.multiworld.state.can_reach("Hallway Room (4)", "Region", self.player))
|
||||
self.assertFalse(self.multiworld.state.can_reach("Elements Area", "Region", self.player))
|
||||
|
||||
self.collect(progressive_hallway_room[1])
|
||||
self.assertTrue(self.multiworld.state.can_reach("Outside The Agreeable", "Region", self.player))
|
||||
self.assertTrue(self.multiworld.state.can_reach("Hallway Room (2)", "Region", self.player))
|
||||
self.assertTrue(self.multiworld.state.can_reach("Hallway Room (3)", "Region", self.player))
|
||||
self.assertFalse(self.multiworld.state.can_reach("Hallway Room (4)", "Region", self.player))
|
||||
self.assertFalse(self.multiworld.state.can_reach("Elements Area", "Region", self.player))
|
||||
|
||||
self.collect(progressive_hallway_room[2])
|
||||
self.assertTrue(self.multiworld.state.can_reach("Outside The Agreeable", "Region", self.player))
|
||||
self.assertTrue(self.multiworld.state.can_reach("Hallway Room (2)", "Region", self.player))
|
||||
self.assertTrue(self.multiworld.state.can_reach("Hallway Room (3)", "Region", self.player))
|
||||
self.assertTrue(self.multiworld.state.can_reach("Hallway Room (4)", "Region", self.player))
|
||||
self.assertFalse(self.multiworld.state.can_reach("Elements Area", "Region", self.player))
|
||||
|
||||
self.collect(progressive_hallway_room[3])
|
||||
self.assertTrue(self.multiworld.state.can_reach("Outside The Agreeable", "Region", self.player))
|
||||
self.assertTrue(self.multiworld.state.can_reach("Hallway Room (2)", "Region", self.player))
|
||||
self.assertTrue(self.multiworld.state.can_reach("Hallway Room (3)", "Region", self.player))
|
||||
self.assertTrue(self.multiworld.state.can_reach("Hallway Room (4)", "Region", self.player))
|
||||
self.assertTrue(self.multiworld.state.can_reach("Elements Area", "Region", self.player))
|
||||
|
||||
|
||||
class TestSimpleHallwayRoom(LingoTestBase):
|
||||
options = {
|
||||
"shuffle_doors": "simple"
|
||||
}
|
||||
|
||||
def test_item(self):
|
||||
self.assertFalse(self.multiworld.state.can_reach("Outside The Agreeable", "Region", self.player))
|
||||
self.assertFalse(self.multiworld.state.can_reach("Hallway Room (2)", "Region", self.player))
|
||||
self.assertFalse(self.multiworld.state.can_reach("Hallway Room (3)", "Region", self.player))
|
||||
self.assertFalse(self.multiworld.state.can_reach("Hallway Room (4)", "Region", self.player))
|
||||
self.assertFalse(self.multiworld.state.can_reach("Elements Area", "Region", self.player))
|
||||
|
||||
self.collect_by_name(["Second Room - Exit Door", "Entrances to The Tenacious"])
|
||||
self.assertTrue(self.multiworld.state.can_reach("Outside The Agreeable", "Region", self.player))
|
||||
self.assertFalse(self.multiworld.state.can_reach("Hallway Room (2)", "Region", self.player))
|
||||
self.assertFalse(self.multiworld.state.can_reach("Hallway Room (3)", "Region", self.player))
|
||||
self.assertFalse(self.multiworld.state.can_reach("Hallway Room (4)", "Region", self.player))
|
||||
self.assertFalse(self.multiworld.state.can_reach("Elements Area", "Region", self.player))
|
||||
|
||||
self.collect_by_name("Hallway Room Doors")
|
||||
self.assertTrue(self.multiworld.state.can_reach("Outside The Agreeable", "Region", self.player))
|
||||
self.assertTrue(self.multiworld.state.can_reach("Hallway Room (2)", "Region", self.player))
|
||||
self.assertTrue(self.multiworld.state.can_reach("Hallway Room (3)", "Region", self.player))
|
||||
self.assertTrue(self.multiworld.state.can_reach("Hallway Room (4)", "Region", self.player))
|
||||
self.assertTrue(self.multiworld.state.can_reach("Elements Area", "Region", self.player))
|
||||
|
||||
|
||||
class TestProgressiveArtGallery(LingoTestBase):
|
||||
options = {
|
||||
"shuffle_doors": "complex"
|
||||
}
|
||||
|
||||
def test_item(self):
|
||||
self.assertFalse(self.multiworld.state.can_reach("Art Gallery", "Region", self.player))
|
||||
self.assertFalse(self.multiworld.state.can_reach("Art Gallery (Second Floor)", "Region", self.player))
|
||||
self.assertFalse(self.multiworld.state.can_reach("Art Gallery (Third Floor)", "Region", self.player))
|
||||
self.assertFalse(self.multiworld.state.can_reach("Art Gallery (Fourth Floor)", "Region", self.player))
|
||||
self.assertFalse(self.can_reach_location("Art Gallery - ONE ROAD MANY TURNS"))
|
||||
self.assertFalse(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player))
|
||||
|
||||
self.collect_by_name(["Second Room - Exit Door", "Crossroads - Tower Entrance",
|
||||
"Orange Tower Fourth Floor - Hot Crusts Door"])
|
||||
self.assertTrue(self.multiworld.state.can_reach("Art Gallery", "Region", self.player))
|
||||
self.assertFalse(self.multiworld.state.can_reach("Art Gallery (Second Floor)", "Region", self.player))
|
||||
self.assertFalse(self.multiworld.state.can_reach("Art Gallery (Third Floor)", "Region", self.player))
|
||||
self.assertFalse(self.multiworld.state.can_reach("Art Gallery (Fourth Floor)", "Region", self.player))
|
||||
self.assertFalse(self.can_reach_location("Art Gallery - ONE ROAD MANY TURNS"))
|
||||
self.assertFalse(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player))
|
||||
|
||||
progressive_gallery_room = self.get_items_by_name("Progressive Art Gallery")
|
||||
|
||||
self.collect(progressive_gallery_room[0])
|
||||
self.assertTrue(self.multiworld.state.can_reach("Art Gallery", "Region", self.player))
|
||||
self.assertTrue(self.multiworld.state.can_reach("Art Gallery (Second Floor)", "Region", self.player))
|
||||
self.assertFalse(self.multiworld.state.can_reach("Art Gallery (Third Floor)", "Region", self.player))
|
||||
self.assertFalse(self.multiworld.state.can_reach("Art Gallery (Fourth Floor)", "Region", self.player))
|
||||
self.assertFalse(self.can_reach_location("Art Gallery - ONE ROAD MANY TURNS"))
|
||||
self.assertFalse(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player))
|
||||
|
||||
self.collect(progressive_gallery_room[1])
|
||||
self.assertTrue(self.multiworld.state.can_reach("Art Gallery", "Region", self.player))
|
||||
self.assertTrue(self.multiworld.state.can_reach("Art Gallery (Second Floor)", "Region", self.player))
|
||||
self.assertTrue(self.multiworld.state.can_reach("Art Gallery (Third Floor)", "Region", self.player))
|
||||
self.assertFalse(self.multiworld.state.can_reach("Art Gallery (Fourth Floor)", "Region", self.player))
|
||||
self.assertFalse(self.can_reach_location("Art Gallery - ONE ROAD MANY TURNS"))
|
||||
self.assertFalse(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player))
|
||||
|
||||
self.collect(progressive_gallery_room[2])
|
||||
self.assertTrue(self.multiworld.state.can_reach("Art Gallery", "Region", self.player))
|
||||
self.assertTrue(self.multiworld.state.can_reach("Art Gallery (Second Floor)", "Region", self.player))
|
||||
self.assertTrue(self.multiworld.state.can_reach("Art Gallery (Third Floor)", "Region", self.player))
|
||||
self.assertTrue(self.multiworld.state.can_reach("Art Gallery (Fourth Floor)", "Region", self.player))
|
||||
self.assertFalse(self.can_reach_location("Art Gallery - ONE ROAD MANY TURNS"))
|
||||
self.assertFalse(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player))
|
||||
|
||||
self.collect(progressive_gallery_room[3])
|
||||
self.assertTrue(self.multiworld.state.can_reach("Art Gallery", "Region", self.player))
|
||||
self.assertTrue(self.multiworld.state.can_reach("Art Gallery (Second Floor)", "Region", self.player))
|
||||
self.assertTrue(self.multiworld.state.can_reach("Art Gallery (Third Floor)", "Region", self.player))
|
||||
self.assertTrue(self.multiworld.state.can_reach("Art Gallery (Fourth Floor)", "Region", self.player))
|
||||
self.assertTrue(self.can_reach_location("Art Gallery - ONE ROAD MANY TURNS"))
|
||||
self.assertFalse(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player))
|
||||
|
||||
self.collect(progressive_gallery_room[4])
|
||||
self.assertTrue(self.multiworld.state.can_reach("Art Gallery", "Region", self.player))
|
||||
self.assertTrue(self.multiworld.state.can_reach("Art Gallery (Second Floor)", "Region", self.player))
|
||||
self.assertTrue(self.multiworld.state.can_reach("Art Gallery (Third Floor)", "Region", self.player))
|
||||
self.assertTrue(self.multiworld.state.can_reach("Art Gallery (Fourth Floor)", "Region", self.player))
|
||||
self.assertTrue(self.can_reach_location("Art Gallery - ONE ROAD MANY TURNS"))
|
||||
self.assertTrue(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player))
|
||||
|
||||
|
||||
class TestNoDoorsArtGallery(LingoTestBase):
|
||||
options = {
|
||||
"shuffle_doors": "none",
|
||||
"shuffle_colors": "true"
|
||||
}
|
||||
|
||||
def test_item(self):
|
||||
self.assertFalse(self.multiworld.state.can_reach("Art Gallery", "Region", self.player))
|
||||
self.assertFalse(self.multiworld.state.can_reach("Art Gallery (Second Floor)", "Region", self.player))
|
||||
self.assertFalse(self.multiworld.state.can_reach("Art Gallery (Third Floor)", "Region", self.player))
|
||||
self.assertFalse(self.multiworld.state.can_reach("Art Gallery (Fourth Floor)", "Region", self.player))
|
||||
self.assertFalse(self.can_reach_location("Art Gallery - ONE ROAD MANY TURNS"))
|
||||
self.assertFalse(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player))
|
||||
|
||||
self.collect_by_name("Yellow")
|
||||
self.assertTrue(self.multiworld.state.can_reach("Art Gallery", "Region", self.player))
|
||||
self.assertTrue(self.multiworld.state.can_reach("Art Gallery (Second Floor)", "Region", self.player))
|
||||
self.assertFalse(self.multiworld.state.can_reach("Art Gallery (Third Floor)", "Region", self.player))
|
||||
self.assertFalse(self.multiworld.state.can_reach("Art Gallery (Fourth Floor)", "Region", self.player))
|
||||
self.assertFalse(self.can_reach_location("Art Gallery - ONE ROAD MANY TURNS"))
|
||||
self.assertFalse(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player))
|
||||
|
||||
self.collect_by_name("Brown")
|
||||
self.assertTrue(self.multiworld.state.can_reach("Art Gallery", "Region", self.player))
|
||||
self.assertTrue(self.multiworld.state.can_reach("Art Gallery (Second Floor)", "Region", self.player))
|
||||
self.assertTrue(self.multiworld.state.can_reach("Art Gallery (Third Floor)", "Region", self.player))
|
||||
self.assertFalse(self.multiworld.state.can_reach("Art Gallery (Fourth Floor)", "Region", self.player))
|
||||
self.assertFalse(self.can_reach_location("Art Gallery - ONE ROAD MANY TURNS"))
|
||||
self.assertFalse(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player))
|
||||
|
||||
self.collect_by_name("Blue")
|
||||
self.assertTrue(self.multiworld.state.can_reach("Art Gallery", "Region", self.player))
|
||||
self.assertTrue(self.multiworld.state.can_reach("Art Gallery (Second Floor)", "Region", self.player))
|
||||
self.assertTrue(self.multiworld.state.can_reach("Art Gallery (Third Floor)", "Region", self.player))
|
||||
self.assertTrue(self.multiworld.state.can_reach("Art Gallery (Fourth Floor)", "Region", self.player))
|
||||
self.assertFalse(self.can_reach_location("Art Gallery - ONE ROAD MANY TURNS"))
|
||||
self.assertFalse(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player))
|
||||
|
||||
self.collect_by_name(["Orange", "Gray"])
|
||||
self.assertTrue(self.multiworld.state.can_reach("Art Gallery", "Region", self.player))
|
||||
self.assertTrue(self.multiworld.state.can_reach("Art Gallery (Second Floor)", "Region", self.player))
|
||||
self.assertTrue(self.multiworld.state.can_reach("Art Gallery (Third Floor)", "Region", self.player))
|
||||
self.assertTrue(self.multiworld.state.can_reach("Art Gallery (Fourth Floor)", "Region", self.player))
|
||||
self.assertTrue(self.can_reach_location("Art Gallery - ONE ROAD MANY TURNS"))
|
||||
self.assertTrue(self.multiworld.state.can_reach("Orange Tower Fifth Floor", "Region", self.player))
|
|
@ -0,0 +1,13 @@
|
|||
from typing import ClassVar
|
||||
|
||||
from test.bases import WorldTestBase
|
||||
from .. import LingoTestOptions
|
||||
|
||||
|
||||
class LingoTestBase(WorldTestBase):
|
||||
game = "Lingo"
|
||||
player: ClassVar[int] = 1
|
||||
|
||||
def world_setup(self, *args, **kwargs):
|
||||
LingoTestOptions.disable_forced_good_item = True
|
||||
super().world_setup(*args, **kwargs)
|
|
@ -0,0 +1,2 @@
|
|||
class LingoTestOptions:
|
||||
disable_forced_good_item: bool = False
|
|
@ -0,0 +1,178 @@
|
|||
# This utility goes through the provided Lingo config and assigns item and
|
||||
# location IDs to entities that require them (such as doors and panels). These
|
||||
# IDs are output in a separate yaml file. If the output file already exists,
|
||||
# then it will be updated with any newly assigned IDs rather than overwritten.
|
||||
# In this event, all new IDs will be greater than any already existing IDs,
|
||||
# even if there are gaps in the ID space; this is to prevent collision when IDs
|
||||
# are retired.
|
||||
#
|
||||
# This utility should be run whenever logically new items or locations are
|
||||
# required. If an item or location is created that is logically equivalent to
|
||||
# one that used to exist, this utility should not be used, and instead the ID
|
||||
# file should be manually edited so that the old ID can be reused.
|
||||
|
||||
require 'set'
|
||||
require 'yaml'
|
||||
|
||||
configpath = ARGV[0]
|
||||
outputpath = ARGV[1]
|
||||
|
||||
next_item_id = 444400
|
||||
next_location_id = 444400
|
||||
|
||||
location_id_by_name = {}
|
||||
|
||||
old_generated = YAML.load_file(outputpath)
|
||||
File.write(outputpath + ".old", old_generated.to_yaml)
|
||||
|
||||
if old_generated.include? "special_items" then
|
||||
old_generated["special_items"].each do |name, id|
|
||||
if id >= next_item_id then
|
||||
next_item_id = id + 1
|
||||
end
|
||||
end
|
||||
end
|
||||
if old_generated.include? "special_locations" then
|
||||
old_generated["special_locations"].each do |name, id|
|
||||
if id >= next_location_id then
|
||||
next_location_id = id + 1
|
||||
end
|
||||
end
|
||||
end
|
||||
if old_generated.include? "panels" then
|
||||
old_generated["panels"].each do |room, panels|
|
||||
panels.each do |name, id|
|
||||
if id >= next_location_id then
|
||||
next_location_id = id + 1
|
||||
end
|
||||
location_name = "#{room} - #{name}"
|
||||
location_id_by_name[location_name] = id
|
||||
end
|
||||
end
|
||||
end
|
||||
if old_generated.include? "doors" then
|
||||
old_generated["doors"].each do |room, doors|
|
||||
doors.each do |name, ids|
|
||||
if ids.include? "location" then
|
||||
if ids["location"] >= next_location_id then
|
||||
next_location_id = ids["location"] + 1
|
||||
end
|
||||
end
|
||||
if ids.include? "item" then
|
||||
if ids["item"] >= next_item_id then
|
||||
next_item_id = ids["item"] + 1
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
if old_generated.include? "door_groups" then
|
||||
old_generated["door_groups"].each do |name, id|
|
||||
if id >= next_item_id then
|
||||
next_item_id = id + 1
|
||||
end
|
||||
end
|
||||
end
|
||||
if old_generated.include? "progression" then
|
||||
old_generated["progression"].each do |name, id|
|
||||
if id >= next_item_id then
|
||||
next_item_id = id + 1
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
door_groups = Set[]
|
||||
|
||||
config = YAML.load_file(configpath)
|
||||
config.each do |room_name, room_data|
|
||||
if room_data.include? "panels"
|
||||
room_data["panels"].each do |panel_name, panel|
|
||||
unless old_generated.include? "panels" and old_generated["panels"].include? room_name and old_generated["panels"][room_name].include? panel_name then
|
||||
old_generated["panels"] ||= {}
|
||||
old_generated["panels"][room_name] ||= {}
|
||||
old_generated["panels"][room_name][panel_name] = next_location_id
|
||||
|
||||
location_name = "#{room_name} - #{panel_name}"
|
||||
location_id_by_name[location_name] = next_location_id
|
||||
|
||||
next_location_id += 1
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
config.each do |room_name, room_data|
|
||||
if room_data.include? "doors"
|
||||
room_data["doors"].each do |door_name, door|
|
||||
if door.include? "event" and door["event"] then
|
||||
next
|
||||
end
|
||||
|
||||
unless door.include? "skip_item" and door["skip_item"] then
|
||||
unless old_generated.include? "doors" and old_generated["doors"].include? room_name and old_generated["doors"][room_name].include? door_name and old_generated["doors"][room_name][door_name].include? "item" then
|
||||
old_generated["doors"] ||= {}
|
||||
old_generated["doors"][room_name] ||= {}
|
||||
old_generated["doors"][room_name][door_name] ||= {}
|
||||
old_generated["doors"][room_name][door_name]["item"] = next_item_id
|
||||
|
||||
next_item_id += 1
|
||||
end
|
||||
|
||||
if door.include? "group" and not door_groups.include? door["group"] then
|
||||
door_groups.add(door["group"])
|
||||
|
||||
unless old_generated.include? "door_groups" and old_generated["door_groups"].include? door["group"] then
|
||||
old_generated["door_groups"] ||= {}
|
||||
old_generated["door_groups"][door["group"]] = next_item_id
|
||||
|
||||
next_item_id += 1
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
unless door.include? "skip_location" and door["skip_location"] then
|
||||
location_name = ""
|
||||
if door.include? "location_name" then
|
||||
location_name = door["location_name"]
|
||||
elsif door.include? "panels" then
|
||||
location_name = door["panels"].map do |panel|
|
||||
if panel.kind_of? Hash then
|
||||
panel
|
||||
else
|
||||
{"room" => room_name, "panel" => panel}
|
||||
end
|
||||
end.sort_by {|panel| panel["room"]}.chunk {|panel| panel["room"]}.map do |room_panels|
|
||||
room_panels[0] + " - " + room_panels[1].map{|panel| panel["panel"]}.join(", ")
|
||||
end.join(" and ")
|
||||
end
|
||||
|
||||
if location_id_by_name.has_key? location_name then
|
||||
old_generated["doors"] ||= {}
|
||||
old_generated["doors"][room_name] ||= {}
|
||||
old_generated["doors"][room_name][door_name] ||= {}
|
||||
old_generated["doors"][room_name][door_name]["location"] = location_id_by_name[location_name]
|
||||
elsif not (old_generated.include? "doors" and old_generated["doors"].include? room_name and old_generated["doors"][room_name].include? door_name and old_generated["doors"][room_name][door_name].include? "location") then
|
||||
old_generated["doors"] ||= {}
|
||||
old_generated["doors"][room_name] ||= {}
|
||||
old_generated["doors"][room_name][door_name] ||= {}
|
||||
old_generated["doors"][room_name][door_name]["location"] = next_location_id
|
||||
|
||||
next_location_id += 1
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if room_data.include? "progression"
|
||||
room_data["progression"].each do |progression_name, pdata|
|
||||
unless old_generated.include? "progression" and old_generated["progression"].include? progression_name then
|
||||
old_generated["progression"] ||= {}
|
||||
old_generated["progression"][progression_name] = next_item_id
|
||||
|
||||
next_item_id += 1
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
File.write(outputpath, old_generated.to_yaml)
|
|
@ -0,0 +1,329 @@
|
|||
# Script to validate a level config file. This checks that the names used within
|
||||
# the file are consistent. It also checks that the panel and door IDs mentioned
|
||||
# all exist in the map file.
|
||||
#
|
||||
# Usage: validate_config.rb [config file] [map file]
|
||||
|
||||
require 'set'
|
||||
require 'yaml'
|
||||
|
||||
configpath = ARGV[0]
|
||||
mappath = ARGV[1]
|
||||
|
||||
panels = Set["Countdown Panels/Panel_1234567890_wanderlust"]
|
||||
doors = Set["Naps Room Doors/Door_hider_new1", "Tower Room Area Doors/Door_wanderer_entrance"]
|
||||
paintings = Set[]
|
||||
|
||||
File.readlines(mappath).each do |line|
|
||||
line.match(/node name=\"(.*)\" parent=\"Panels\/(.*)\" instance/) do |m|
|
||||
panels.add(m[2] + "/" + m[1])
|
||||
end
|
||||
line.match(/node name=\"(.*)\" parent=\"Doors\/(.*)\" instance/) do |m|
|
||||
doors.add(m[2] + "/" + m[1])
|
||||
end
|
||||
line.match(/node name=\"(.*)\" parent=\"Decorations\/Paintings\" instance/) do |m|
|
||||
paintings.add(m[1])
|
||||
end
|
||||
line.match(/node name=\"(.*)\" parent=\"Decorations\/EndPanel\" instance/) do |m|
|
||||
panels.add("EndPanel/" + m[1])
|
||||
end
|
||||
end
|
||||
|
||||
configured_rooms = Set["Menu"]
|
||||
configured_doors = Set[]
|
||||
configured_panels = Set[]
|
||||
|
||||
mentioned_rooms = Set[]
|
||||
mentioned_doors = Set[]
|
||||
mentioned_panels = Set[]
|
||||
|
||||
door_groups = {}
|
||||
|
||||
directives = Set["entrances", "panels", "doors", "paintings", "progression"]
|
||||
panel_directives = Set["id", "required_room", "required_door", "required_panel", "colors", "check", "exclude_reduce", "tag", "link", "subtag", "achievement", "copy_to_sign", "non_counting"]
|
||||
door_directives = Set["id", "painting_id", "panels", "item_name", "location_name", "skip_location", "skip_item", "group", "include_reduce", "junk_item", "event"]
|
||||
painting_directives = Set["id", "enter_only", "exit_only", "orientation", "required_door", "required", "required_when_no_doors", "move"]
|
||||
|
||||
non_counting = 0
|
||||
|
||||
config = YAML.load_file(configpath)
|
||||
config.each do |room_name, room|
|
||||
configured_rooms.add(room_name)
|
||||
|
||||
used_directives = Set[]
|
||||
room.each_key do |key|
|
||||
used_directives.add(key)
|
||||
end
|
||||
diff_directives = used_directives - directives
|
||||
unless diff_directives.empty? then
|
||||
puts("#{room_name} has the following invalid top-level directives: #{diff_directives.to_s}")
|
||||
end
|
||||
|
||||
(room["entrances"] || {}).each do |source_room, entrance|
|
||||
mentioned_rooms.add(source_room)
|
||||
|
||||
entrances = []
|
||||
if entrance.kind_of? Hash
|
||||
if entrance.keys() != ["painting"] then
|
||||
entrances = [entrance]
|
||||
end
|
||||
elsif entrance.kind_of? Array
|
||||
entrances = entrance
|
||||
end
|
||||
|
||||
entrances.each do |e|
|
||||
entrance_room = e.include?("room") ? e["room"] : room_name
|
||||
mentioned_rooms.add(entrance_room)
|
||||
mentioned_doors.add(entrance_room + " - " + e["door"])
|
||||
end
|
||||
end
|
||||
|
||||
(room["panels"] || {}).each do |panel_name, panel|
|
||||
unless panel_name.kind_of? String then
|
||||
puts "#{room_name} has an invalid panel name"
|
||||
end
|
||||
|
||||
configured_panels.add(room_name + " - " + panel_name)
|
||||
|
||||
if panel.include?("id")
|
||||
panel_ids = []
|
||||
if panel["id"].kind_of? Array
|
||||
panel_ids = panel["id"]
|
||||
else
|
||||
panel_ids = [panel["id"]]
|
||||
end
|
||||
|
||||
panel_ids.each do |panel_id|
|
||||
unless panels.include? panel_id then
|
||||
puts "#{room_name} - #{panel_name} :::: Invalid Panel ID #{panel_id}"
|
||||
end
|
||||
end
|
||||
else
|
||||
puts "#{room_name} - #{panel_name} :::: Panel is missing an ID"
|
||||
end
|
||||
|
||||
if panel.include?("required_room")
|
||||
required_rooms = []
|
||||
if panel["required_room"].kind_of? Array
|
||||
required_rooms = panel["required_room"]
|
||||
else
|
||||
required_rooms = [panel["required_room"]]
|
||||
end
|
||||
|
||||
required_rooms.each do |required_room|
|
||||
mentioned_rooms.add(required_room)
|
||||
end
|
||||
end
|
||||
|
||||
if panel.include?("required_door")
|
||||
required_doors = []
|
||||
if panel["required_door"].kind_of? Array
|
||||
required_doors = panel["required_door"]
|
||||
else
|
||||
required_doors = [panel["required_door"]]
|
||||
end
|
||||
|
||||
required_doors.each do |required_door|
|
||||
other_room = required_door.include?("room") ? required_door["room"] : room_name
|
||||
mentioned_rooms.add(other_room)
|
||||
mentioned_doors.add("#{other_room} - #{required_door["door"]}")
|
||||
end
|
||||
end
|
||||
|
||||
if panel.include?("required_panel")
|
||||
required_panels = []
|
||||
if panel["required_panel"].kind_of? Array
|
||||
required_panels = panel["required_panel"]
|
||||
else
|
||||
required_panels = [panel["required_panel"]]
|
||||
end
|
||||
|
||||
required_panels.each do |required_panel|
|
||||
other_room = required_panel.include?("room") ? required_panel["room"] : room_name
|
||||
mentioned_rooms.add(other_room)
|
||||
mentioned_panels.add("#{other_room} - #{required_panel["panel"]}")
|
||||
end
|
||||
end
|
||||
|
||||
unless panel.include?("tag") then
|
||||
puts "#{room_name} - #{panel_name} :::: Panel is missing a tag"
|
||||
end
|
||||
|
||||
if panel.include?("non_counting") then
|
||||
non_counting += 1
|
||||
end
|
||||
|
||||
bad_subdirectives = []
|
||||
panel.keys.each do |key|
|
||||
unless panel_directives.include?(key) then
|
||||
bad_subdirectives << key
|
||||
end
|
||||
end
|
||||
unless bad_subdirectives.empty? then
|
||||
puts "#{room_name} - #{panel_name} :::: Panel has the following invalid subdirectives: #{bad_subdirectives.join(", ")}"
|
||||
end
|
||||
end
|
||||
|
||||
(room["doors"] || {}).each do |door_name, door|
|
||||
configured_doors.add("#{room_name} - #{door_name}")
|
||||
|
||||
if door.include?("id")
|
||||
door_ids = []
|
||||
if door["id"].kind_of? Array
|
||||
door_ids = door["id"]
|
||||
else
|
||||
door_ids = [door["id"]]
|
||||
end
|
||||
|
||||
door_ids.each do |door_id|
|
||||
unless doors.include? door_id then
|
||||
puts "#{room_name} - #{door_name} :::: Invalid Door ID #{door_id}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if door.include?("painting_id")
|
||||
painting_ids = []
|
||||
if door["painting_id"].kind_of? Array
|
||||
painting_ids = door["painting_id"]
|
||||
else
|
||||
painting_ids = [door["painting_id"]]
|
||||
end
|
||||
|
||||
painting_ids.each do |painting_id|
|
||||
unless paintings.include? painting_id then
|
||||
puts "#{room_name} - #{door_name} :::: Invalid Painting ID #{painting_id}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if not door.include?("id") and not door.include?("painting_id") and not door["skip_item"] and not door["event"] then
|
||||
puts "#{room_name} - #{door_name} :::: Should be marked skip_item or event if there are no doors or paintings"
|
||||
end
|
||||
|
||||
if door.include?("panels")
|
||||
door["panels"].each do |panel|
|
||||
if panel.kind_of? Hash then
|
||||
other_room = panel.include?("room") ? panel["room"] : room_name
|
||||
mentioned_panels.add("#{other_room} - #{panel["panel"]}")
|
||||
else
|
||||
other_room = panel.include?("room") ? panel["room"] : room_name
|
||||
mentioned_panels.add("#{room_name} - #{panel}")
|
||||
end
|
||||
end
|
||||
elsif not door["skip_location"]
|
||||
puts "#{room_name} - #{door_name} :::: Should be marked skip_location if there are no panels"
|
||||
end
|
||||
|
||||
if door.include?("group")
|
||||
door_groups[door["group"]] ||= 0
|
||||
door_groups[door["group"]] += 1
|
||||
end
|
||||
|
||||
bad_subdirectives = []
|
||||
door.keys.each do |key|
|
||||
unless door_directives.include?(key) then
|
||||
bad_subdirectives << key
|
||||
end
|
||||
end
|
||||
unless bad_subdirectives.empty? then
|
||||
puts "#{room_name} - #{door_name} :::: Door has the following invalid subdirectives: #{bad_subdirectives.join(", ")}"
|
||||
end
|
||||
end
|
||||
|
||||
(room["paintings"] || []).each do |painting|
|
||||
if painting.include?("id") and painting["id"].kind_of? String then
|
||||
unless paintings.include? painting["id"] then
|
||||
puts "#{room_name} :::: Invalid Painting ID #{painting["id"]}"
|
||||
end
|
||||
else
|
||||
puts "#{room_name} :::: Painting is missing an ID"
|
||||
end
|
||||
|
||||
if painting["disable"] then
|
||||
# We're good.
|
||||
next
|
||||
end
|
||||
|
||||
if painting.include?("orientation") then
|
||||
unless ["north", "south", "east", "west"].include? painting["orientation"] then
|
||||
puts "#{room_name} - #{painting["id"] || "painting"} :::: Invalid orientation #{painting["orientation"]}"
|
||||
end
|
||||
else
|
||||
puts "#{room_name} :::: Painting is missing an orientation"
|
||||
end
|
||||
|
||||
if painting.include?("required_door")
|
||||
other_room = painting["required_door"].include?("room") ? painting["required_door"]["room"] : room_name
|
||||
mentioned_doors.add("#{other_room} - #{painting["required_door"]["door"]}")
|
||||
|
||||
unless painting["enter_only"] then
|
||||
puts "#{room_name} - #{painting["id"] || "painting"} :::: Should be marked enter_only if there is a required_door"
|
||||
end
|
||||
end
|
||||
|
||||
bad_subdirectives = []
|
||||
painting.keys.each do |key|
|
||||
unless painting_directives.include?(key) then
|
||||
bad_subdirectives << key
|
||||
end
|
||||
end
|
||||
unless bad_subdirectives.empty? then
|
||||
puts "#{room_name} - #{painting["id"] || "painting"} :::: Painting has the following invalid subdirectives: #{bad_subdirectives.join(", ")}"
|
||||
end
|
||||
end
|
||||
|
||||
(room["progression"] || {}).each do |progression_name, door_list|
|
||||
door_list.each do |door|
|
||||
if door.kind_of? Hash then
|
||||
mentioned_doors.add("#{door["room"]} - #{door["door"]}")
|
||||
else
|
||||
mentioned_doors.add("#{room_name} - #{door}")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
errored_rooms = mentioned_rooms - configured_rooms
|
||||
unless errored_rooms.empty? then
|
||||
puts "The folloring rooms are mentioned but do not exist: " + errored_rooms.to_s
|
||||
end
|
||||
|
||||
errored_panels = mentioned_panels - configured_panels
|
||||
unless errored_panels.empty? then
|
||||
puts "The folloring panels are mentioned but do not exist: " + errored_panels.to_s
|
||||
end
|
||||
|
||||
errored_doors = mentioned_doors - configured_doors
|
||||
unless errored_doors.empty? then
|
||||
puts "The folloring doors are mentioned but do not exist: " + errored_doors.to_s
|
||||
end
|
||||
|
||||
door_groups.each do |group,num|
|
||||
if num == 1 then
|
||||
puts "Door group \"#{group}\" only has one door in it"
|
||||
end
|
||||
end
|
||||
|
||||
slashed_rooms = configured_rooms.select do |room|
|
||||
room.include? "/"
|
||||
end
|
||||
unless slashed_rooms.empty? then
|
||||
puts "The following rooms have slashes in their names: " + slashed_rooms.to_s
|
||||
end
|
||||
|
||||
slashed_panels = configured_panels.select do |panel|
|
||||
panel.include? "/"
|
||||
end
|
||||
unless slashed_panels.empty? then
|
||||
puts "The following panels have slashes in their names: " + slashed_panels.to_s
|
||||
end
|
||||
|
||||
slashed_doors = configured_doors.select do |door|
|
||||
door.include? "/"
|
||||
end
|
||||
unless slashed_doors.empty? then
|
||||
puts "The following doors have slashes in their names: " + slashed_doors.to_s
|
||||
end
|
||||
|
||||
puts "#{configured_panels.size} panels (#{non_counting} non counting)"
|
Loading…
Reference in New Issue