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:
Star Rauchenberger 2023-11-08 18:35:12 -05:00 committed by GitHub
parent 154e17f4ff
commit ea9c31392d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 11546 additions and 0 deletions

View File

@ -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

View File

@ -61,6 +61,9 @@
# Kingdom Hearts 2
/worlds/kh2/ @JaredWeakStrike
# Lingo
/worlds/lingo/ @hatkirby
# Links Awakening DX
/worlds/ladx/ @zig-for

7505
worlds/lingo/LL1.yaml Normal file

File diff suppressed because it is too large Load Diff

112
worlds/lingo/__init__.py Normal file
View File

@ -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

View File

@ -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.

View File

@ -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.

1449
worlds/lingo/ids.yaml Normal file

File diff suppressed because it is too large Load Diff

106
worlds/lingo/items.py Normal file
View File

@ -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()

80
worlds/lingo/locations.py Normal file
View File

@ -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()

126
worlds/lingo/options.py Normal file
View File

@ -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

View File

@ -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

84
worlds/lingo/regions.py Normal file
View File

@ -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()

104
worlds/lingo/rules.py Normal file
View File

@ -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)

View File

@ -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()

View File

@ -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))

View File

@ -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"))

View File

@ -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"
}

View File

@ -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))

View File

@ -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))

View File

@ -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)

2
worlds/lingo/testing.py Normal file
View File

@ -0,0 +1,2 @@
class LingoTestOptions:
disable_forced_good_item: bool = False

View File

@ -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)

View File

@ -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)"