The Witness: The big dumb refactor ()

This commit is contained in:
NewSoupVi 2024-04-12 00:27:42 +02:00 committed by GitHub
parent 5d4ed00452
commit 401a6d9a42
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
48 changed files with 1080 additions and 1041 deletions

View File

@ -2,24 +2,26 @@
Archipelago init file for The Witness
"""
import dataclasses
from logging import error, warning
from typing import Any, Dict, List, Optional, cast
from BaseClasses import CollectionState, Entrance, Location, Region, Tutorial
from typing import Dict, Optional, cast
from BaseClasses import Region, Location, MultiWorld, Item, Entrance, Tutorial, CollectionState
from Options import PerGameCommonOptions, Toggle
from .presets import witness_option_presets
from worlds.AutoWorld import World, WebWorld
from .player_logic import WitnessPlayerLogic
from .static_logic import StaticWitnessLogic, ItemCategory, DoorItemDefinition
from .hints import get_always_hint_locations, get_always_hint_items, get_priority_hint_locations, \
get_priority_hint_items, make_always_and_priority_hints, generate_joke_hints, make_area_hints, get_hintable_areas, \
make_extra_location_hints, create_all_hints, make_laser_hints, make_compact_hint_data, CompactItemData
from .locations import WitnessPlayerLocations, StaticWitnessLocations
from .items import WitnessItem, StaticWitnessItems, WitnessPlayerItems, ItemData
from .regions import WitnessRegions
from .rules import set_rules
from worlds.AutoWorld import WebWorld, World
from .data import static_items as static_witness_items
from .data import static_logic as static_witness_logic
from .data.item_definition_classes import DoorItemDefinition, ItemData
from .data.utils import get_audio_logs
from .hints import CompactItemData, create_all_hints, generate_joke_hints, make_compact_hint_data, make_laser_hints
from .locations import WitnessPlayerLocations, static_witness_locations
from .options import TheWitnessOptions
from .utils import get_audio_logs, get_laser_shuffle
from logging import warning, error
from .player_items import WitnessItem, WitnessPlayerItems
from .player_logic import WitnessPlayerLogic
from .presets import witness_option_presets
from .regions import WitnessPlayerRegions
from .rules import set_rules
class WitnessWebWorld(WebWorld):
@ -50,46 +52,43 @@ class WitnessWorld(World):
options: TheWitnessOptions
item_name_to_id = {
name: data.ap_code for name, data in StaticWitnessItems.item_data.items()
name: data.ap_code for name, data in static_witness_items.ITEM_DATA.items()
}
location_name_to_id = StaticWitnessLocations.ALL_LOCATIONS_TO_ID
item_name_groups = StaticWitnessItems.item_groups
location_name_groups = StaticWitnessLocations.AREA_LOCATION_GROUPS
location_name_to_id = static_witness_locations.ALL_LOCATIONS_TO_ID
item_name_groups = static_witness_items.ITEM_GROUPS
location_name_groups = static_witness_locations.AREA_LOCATION_GROUPS
required_client_version = (0, 4, 5)
def __init__(self, multiworld: "MultiWorld", player: int):
super().__init__(multiworld, player)
player_logic: WitnessPlayerLogic
player_locations: WitnessPlayerLocations
player_items: WitnessPlayerItems
player_regions: WitnessPlayerRegions
self.player_logic = None
self.locat = None
self.items = None
self.regio = None
log_ids_to_hints: Dict[int, CompactItemData]
laser_ids_to_hints: Dict[int, CompactItemData]
self.log_ids_to_hints: Dict[int, CompactItemData] = dict()
self.laser_ids_to_hints: Dict[int, CompactItemData] = dict()
items_placed_early: List[str]
own_itempool: List[WitnessItem]
self.items_placed_early = []
self.own_itempool = []
def _get_slot_data(self):
def _get_slot_data(self) -> Dict[str, Any]:
return {
'seed': self.random.randrange(0, 1000000),
'victory_location': int(self.player_logic.VICTORY_LOCATION, 16),
'panelhex_to_id': self.locat.CHECK_PANELHEX_TO_ID,
'item_id_to_door_hexes': StaticWitnessItems.get_item_to_door_mappings(),
'door_hexes_in_the_pool': self.items.get_door_ids_in_pool(),
'symbols_not_in_the_game': self.items.get_symbol_ids_not_in_pool(),
'disabled_entities': [int(h, 16) for h in self.player_logic.COMPLETELY_DISABLED_ENTITIES],
'log_ids_to_hints': self.log_ids_to_hints,
'laser_ids_to_hints': self.laser_ids_to_hints,
'progressive_item_lists': self.items.get_progressive_item_ids_in_pool(),
'obelisk_side_id_to_EPs': StaticWitnessLogic.OBELISK_SIDE_ID_TO_EP_HEXES,
'precompleted_puzzles': [int(h, 16) for h in self.player_logic.EXCLUDED_LOCATIONS],
'entity_to_name': StaticWitnessLogic.ENTITY_ID_TO_NAME,
"seed": self.random.randrange(0, 1000000),
"victory_location": int(self.player_logic.VICTORY_LOCATION, 16),
"panelhex_to_id": self.player_locations.CHECK_PANELHEX_TO_ID,
"item_id_to_door_hexes": static_witness_items.get_item_to_door_mappings(),
"door_hexes_in_the_pool": self.player_items.get_door_ids_in_pool(),
"symbols_not_in_the_game": self.player_items.get_symbol_ids_not_in_pool(),
"disabled_entities": [int(h, 16) for h in self.player_logic.COMPLETELY_DISABLED_ENTITIES],
"log_ids_to_hints": self.log_ids_to_hints,
"laser_ids_to_hints": self.laser_ids_to_hints,
"progressive_item_lists": self.player_items.get_progressive_item_ids_in_pool(),
"obelisk_side_id_to_EPs": static_witness_logic.OBELISK_SIDE_ID_TO_EP_HEXES,
"precompleted_puzzles": [int(h, 16) for h in self.player_logic.EXCLUDED_LOCATIONS],
"entity_to_name": static_witness_logic.ENTITY_ID_TO_NAME,
}
def determine_sufficient_progression(self):
def determine_sufficient_progression(self) -> None:
"""
Determine whether there are enough progression items in this world to consider it "interactive".
In the case of singleplayer, this just outputs a warning.
@ -127,20 +126,20 @@ class WitnessWorld(World):
elif not interacts_sufficiently_with_multiworld and self.multiworld.players > 1:
raise Exception(f"{self.multiworld.get_player_name(self.player)}'s Witness world doesn't have enough"
f" progression items that can be placed in other players' worlds. Please turn on Symbol"
f" Shuffle, Door Shuffle or Obelisk Keys.")
f" Shuffle, Door Shuffle, or Obelisk Keys.")
def generate_early(self):
def generate_early(self) -> None:
disabled_locations = self.options.exclude_locations.value
self.player_logic = WitnessPlayerLogic(
self, disabled_locations, self.options.start_inventory.value
)
self.locat: WitnessPlayerLocations = WitnessPlayerLocations(self, self.player_logic)
self.items: WitnessPlayerItems = WitnessPlayerItems(
self, self.player_logic, self.locat
self.player_locations: WitnessPlayerLocations = WitnessPlayerLocations(self, self.player_logic)
self.player_items: WitnessPlayerItems = WitnessPlayerItems(
self, self.player_logic, self.player_locations
)
self.regio: WitnessRegions = WitnessRegions(self.locat, self)
self.player_regions: WitnessPlayerRegions = WitnessPlayerRegions(self.player_locations, self)
self.log_ids_to_hints = dict()
@ -149,22 +148,27 @@ class WitnessWorld(World):
if self.options.shuffle_lasers == "local":
self.options.local_items.value |= self.item_name_groups["Lasers"]
def create_regions(self):
self.regio.create_regions(self, self.player_logic)
def create_regions(self) -> None:
self.player_regions.create_regions(self, self.player_logic)
# Set rules early so extra locations can be created based on the results of exploring collection states
set_rules(self)
# Start creating items
self.items_placed_early = []
self.own_itempool = []
# Add event items and tie them to event locations (e.g. laser activations).
event_locations = []
for event_location in self.locat.EVENT_LOCATION_TABLE:
for event_location in self.player_locations.EVENT_LOCATION_TABLE:
item_obj = self.create_item(
self.player_logic.EVENT_ITEM_PAIRS[event_location]
)
location_obj = self.multiworld.get_location(event_location, self.player)
location_obj = self.get_location(event_location)
location_obj.place_locked_item(item_obj)
self.own_itempool.append(item_obj)
@ -172,14 +176,16 @@ class WitnessWorld(World):
# Place other locked items
dog_puzzle_skip = self.create_item("Puzzle Skip")
self.multiworld.get_location("Town Pet the Dog", self.player).place_locked_item(dog_puzzle_skip)
self.get_location("Town Pet the Dog").place_locked_item(dog_puzzle_skip)
self.own_itempool.append(dog_puzzle_skip)
self.items_placed_early.append("Puzzle Skip")
# Pick an early item to place on the tutorial gate.
early_items = [item for item in self.items.get_early_items() if item in self.items.get_mandatory_items()]
early_items = [
item for item in self.player_items.get_early_items() if item in self.player_items.get_mandatory_items()
]
if early_items:
random_early_item = self.random.choice(early_items)
if self.options.puzzle_randomization == "sigma_expert":
@ -188,7 +194,7 @@ class WitnessWorld(World):
else:
# Force the item onto the tutorial gate check and remove it from our random pool.
gate_item = self.create_item(random_early_item)
self.multiworld.get_location("Tutorial Gate Open", self.player).place_locked_item(gate_item)
self.get_location("Tutorial Gate Open").place_locked_item(gate_item)
self.own_itempool.append(gate_item)
self.items_placed_early.append(random_early_item)
@ -223,19 +229,19 @@ class WitnessWorld(World):
break
region, loc = extra_checks.pop(0)
self.locat.add_location_late(loc)
self.multiworld.get_region(region, self.player).add_locations({loc: self.location_name_to_id[loc]})
self.player_locations.add_location_late(loc)
self.get_region(region).add_locations({loc: self.location_name_to_id[loc]})
player = self.multiworld.get_player_name(self.player)
warning(f"""Location "{loc}" had to be added to {player}'s world due to insufficient sphere 1 size.""")
def create_items(self):
def create_items(self) -> None:
# Determine pool size.
pool_size: int = len(self.locat.CHECK_LOCATION_TABLE) - len(self.locat.EVENT_LOCATION_TABLE)
pool_size = len(self.player_locations.CHECK_LOCATION_TABLE) - len(self.player_locations.EVENT_LOCATION_TABLE)
# Fill mandatory items and remove precollected and/or starting items from the pool.
item_pool: Dict[str, int] = self.items.get_mandatory_items()
item_pool = self.player_items.get_mandatory_items()
# Remove one copy of each item that was placed early
for already_placed in self.items_placed_early:
@ -283,7 +289,7 @@ class WitnessWorld(World):
# Add junk items.
if remaining_item_slots > 0:
item_pool.update(self.items.get_filler_items(remaining_item_slots))
item_pool.update(self.player_items.get_filler_items(remaining_item_slots))
# Generate the actual items.
for item_name, quantity in sorted(item_pool.items()):
@ -291,19 +297,22 @@ class WitnessWorld(World):
self.own_itempool += new_items
self.multiworld.itempool += new_items
if self.items.item_data[item_name].local_only:
if self.player_items.item_data[item_name].local_only:
self.options.local_items.value.add(item_name)
def fill_slot_data(self) -> dict:
self.log_ids_to_hints: Dict[int, CompactItemData] = dict()
self.laser_ids_to_hints: Dict[int, CompactItemData] = dict()
already_hinted_locations = set()
# Laser hints
if self.options.laser_hints:
laser_hints = make_laser_hints(self, StaticWitnessItems.item_groups["Lasers"])
laser_hints = make_laser_hints(self, static_witness_items.ITEM_GROUPS["Lasers"])
for item_name, hint in laser_hints.items():
item_def = cast(DoorItemDefinition, StaticWitnessLogic.all_items[item_name])
item_def = cast(DoorItemDefinition, static_witness_logic.ALL_ITEMS[item_name])
self.laser_ids_to_hints[int(item_def.panel_id_hexes[0], 16)] = make_compact_hint_data(hint, self.player)
already_hinted_locations.add(hint.location)
@ -356,18 +365,18 @@ class WitnessWorld(World):
return slot_data
def create_item(self, item_name: str) -> Item:
def create_item(self, item_name: str) -> WitnessItem:
# If the player's plando options are malformed, the item_name parameter could be a dictionary containing the
# name of the item, rather than the item itself. This is a workaround to prevent a crash.
if type(item_name) is dict:
item_name = list(item_name.keys())[0]
if isinstance(item_name, dict):
item_name = next(iter(item_name))
# this conditional is purely for unit tests, which need to be able to create an item before generate_early
item_data: ItemData
if hasattr(self, 'items') and self.items and item_name in self.items.item_data:
item_data = self.items.item_data[item_name]
if hasattr(self, "player_items") and self.player_items and item_name in self.player_items.item_data:
item_data = self.player_items.item_data[item_name]
else:
item_data = StaticWitnessItems.item_data[item_name]
item_data = static_witness_items.ITEM_DATA[item_name]
return WitnessItem(item_name, item_data.classification, item_data.ap_code, player=self.player)
@ -382,12 +391,13 @@ class WitnessLocation(Location):
game: str = "The Witness"
entity_hex: int = -1
def __init__(self, player: int, name: str, address: Optional[int], parent, ch_hex: int = -1):
def __init__(self, player: int, name: str, address: Optional[int], parent, ch_hex: int = -1) -> None:
super().__init__(player, name, address, parent)
self.entity_hex = ch_hex
def create_region(world: WitnessWorld, name: str, locat: WitnessPlayerLocations, region_locations=None, exits=None):
def create_region(world: WitnessWorld, name: str, player_locations: WitnessPlayerLocations,
region_locations=None, exits=None) -> Region:
"""
Create an Archipelago Region for The Witness
"""
@ -395,12 +405,12 @@ def create_region(world: WitnessWorld, name: str, locat: WitnessPlayerLocations,
ret = Region(name, world.player, world.multiworld)
if region_locations:
for location in region_locations:
loc_id = locat.CHECK_LOCATION_TABLE[location]
loc_id = player_locations.CHECK_LOCATION_TABLE[location]
entity_hex = -1
if location in StaticWitnessLogic.ENTITIES_BY_NAME:
if location in static_witness_logic.ENTITIES_BY_NAME:
entity_hex = int(
StaticWitnessLogic.ENTITIES_BY_NAME[location]["entity_hex"], 0
static_witness_logic.ENTITIES_BY_NAME[location]["entity_hex"], 0
)
location = WitnessLocation(
world.player, location, loc_id, ret, entity_hex

View File

View File

@ -0,0 +1,59 @@
from dataclasses import dataclass
from enum import Enum
from typing import Dict, List, Optional
from BaseClasses import ItemClassification
class ItemCategory(Enum):
SYMBOL = 0
DOOR = 1
LASER = 2
USEFUL = 3
FILLER = 4
TRAP = 5
JOKE = 6
EVENT = 7
CATEGORY_NAME_MAPPINGS: Dict[str, ItemCategory] = {
"Symbols:": ItemCategory.SYMBOL,
"Doors:": ItemCategory.DOOR,
"Lasers:": ItemCategory.LASER,
"Useful:": ItemCategory.USEFUL,
"Filler:": ItemCategory.FILLER,
"Traps:": ItemCategory.TRAP,
"Jokes:": ItemCategory.JOKE
}
@dataclass(frozen=True)
class ItemDefinition:
local_code: int
category: ItemCategory
@dataclass(frozen=True)
class ProgressiveItemDefinition(ItemDefinition):
child_item_names: List[str]
@dataclass(frozen=True)
class DoorItemDefinition(ItemDefinition):
panel_id_hexes: List[str]
@dataclass(frozen=True)
class WeightedItemDefinition(ItemDefinition):
weight: int
@dataclass()
class ItemData:
"""
ItemData for an item in The Witness
"""
ap_code: Optional[int]
definition: ItemDefinition
classification: ItemClassification
local_only: bool = False

View File

@ -0,0 +1,56 @@
from typing import Dict, List
from BaseClasses import ItemClassification
from . import static_logic as static_witness_logic
from .item_definition_classes import DoorItemDefinition, ItemCategory, ItemData
from .static_locations import ID_START
ITEM_DATA: Dict[str, ItemData] = {}
ITEM_GROUPS: Dict[str, List[str]] = {}
# Useful items that are treated specially at generation time and should not be automatically added to the player's
# item list during get_progression_items.
_special_usefuls: List[str] = ["Puzzle Skip"]
def populate_items() -> None:
for item_name, definition in static_witness_logic.ALL_ITEMS.items():
ap_item_code = definition.local_code + ID_START
classification: ItemClassification = ItemClassification.filler
local_only: bool = False
if definition.category is ItemCategory.SYMBOL:
classification = ItemClassification.progression
ITEM_GROUPS.setdefault("Symbols", []).append(item_name)
elif definition.category is ItemCategory.DOOR:
classification = ItemClassification.progression
ITEM_GROUPS.setdefault("Doors", []).append(item_name)
elif definition.category is ItemCategory.LASER:
classification = ItemClassification.progression_skip_balancing
ITEM_GROUPS.setdefault("Lasers", []).append(item_name)
elif definition.category is ItemCategory.USEFUL:
classification = ItemClassification.useful
elif definition.category is ItemCategory.FILLER:
if item_name in ["Energy Fill (Small)"]:
local_only = True
classification = ItemClassification.filler
elif definition.category is ItemCategory.TRAP:
classification = ItemClassification.trap
elif definition.category is ItemCategory.JOKE:
classification = ItemClassification.filler
ITEM_DATA[item_name] = ItemData(ap_item_code, definition,
classification, local_only)
def get_item_to_door_mappings() -> Dict[int, List[int]]:
output: Dict[int, List[int]] = {}
for item_name, item_data in ITEM_DATA.items():
if not isinstance(item_data.definition, DoorItemDefinition):
continue
output[item_data.ap_code] = [int(hex_string, 16) for hex_string in item_data.definition.panel_id_hexes]
return output
populate_items()

View File

@ -0,0 +1,482 @@
from . import static_logic as static_witness_logic
ID_START = 158000
GENERAL_LOCATIONS = {
"Tutorial Front Left",
"Tutorial Back Left",
"Tutorial Back Right",
"Tutorial Patio Floor",
"Tutorial Gate Open",
"Outside Tutorial Vault Box",
"Outside Tutorial Discard",
"Outside Tutorial Shed Row 5",
"Outside Tutorial Tree Row 9",
"Outside Tutorial Outpost Entry Panel",
"Outside Tutorial Outpost Exit Panel",
"Glass Factory Discard",
"Glass Factory Back Wall 5",
"Glass Factory Front 3",
"Glass Factory Melting 3",
"Symmetry Island Lower Panel",
"Symmetry Island Right 5",
"Symmetry Island Back 6",
"Symmetry Island Left 7",
"Symmetry Island Upper Panel",
"Symmetry Island Scenery Outlines 5",
"Symmetry Island Laser Yellow 3",
"Symmetry Island Laser Blue 3",
"Symmetry Island Laser Panel",
"Orchard Apple Tree 5",
"Desert Vault Box",
"Desert Discard",
"Desert Surface 8",
"Desert Light Room 3",
"Desert Pond Room 5",
"Desert Flood Room 6",
"Desert Elevator Room Hexagonal",
"Desert Elevator Room Bent 3",
"Desert Laser Panel",
"Quarry Entry 1 Panel",
"Quarry Entry 2 Panel",
"Quarry Stoneworks Entry Left Panel",
"Quarry Stoneworks Entry Right Panel",
"Quarry Stoneworks Lower Row 6",
"Quarry Stoneworks Upper Row 8",
"Quarry Stoneworks Control Room Left",
"Quarry Stoneworks Control Room Right",
"Quarry Stoneworks Stairs Panel",
"Quarry Boathouse Intro Right",
"Quarry Boathouse Intro Left",
"Quarry Boathouse Front Row 5",
"Quarry Boathouse Back First Row 9",
"Quarry Boathouse Back Second Row 3",
"Quarry Discard",
"Quarry Laser Panel",
"Shadows Intro 8",
"Shadows Far 8",
"Shadows Near 5",
"Shadows Laser Panel",
"Keep Hedge Maze 1",
"Keep Hedge Maze 2",
"Keep Hedge Maze 3",
"Keep Hedge Maze 4",
"Keep Pressure Plates 1",
"Keep Pressure Plates 2",
"Keep Pressure Plates 3",
"Keep Pressure Plates 4",
"Keep Discard",
"Keep Laser Panel Hedges",
"Keep Laser Panel Pressure Plates",
"Shipwreck Vault Box",
"Shipwreck Discard",
"Monastery Outside 3",
"Monastery Inside 4",
"Monastery Laser Panel",
"Town Cargo Box Entry Panel",
"Town Cargo Box Discard",
"Town Tall Hexagonal",
"Town Church Entry Panel",
"Town Church Lattice",
"Town Maze Panel",
"Town Rooftop Discard",
"Town Red Rooftop 5",
"Town Wooden Roof Lower Row 5",
"Town Wooden Rooftop",
"Windmill Entry Panel",
"Town RGB House Entry Panel",
"Town Laser Panel",
"Town RGB House Upstairs Left",
"Town RGB House Upstairs Right",
"Town RGB House Sound Room Right",
"Windmill Theater Entry Panel",
"Theater Exit Left Panel",
"Theater Exit Right Panel",
"Theater Tutorial Video",
"Theater Desert Video",
"Theater Jungle Video",
"Theater Shipwreck Video",
"Theater Mountain Video",
"Theater Discard",
"Jungle Discard",
"Jungle First Row 3",
"Jungle Second Row 4",
"Jungle Popup Wall 6",
"Jungle Laser Panel",
"Jungle Vault Box",
"Jungle Monastery Garden Shortcut Panel",
"Bunker Entry Panel",
"Bunker Intro Left 5",
"Bunker Intro Back 4",
"Bunker Glass Room 3",
"Bunker UV Room 2",
"Bunker Laser Panel",
"Swamp Entry Panel",
"Swamp Intro Front 6",
"Swamp Intro Back 8",
"Swamp Between Bridges Near Row 4",
"Swamp Cyan Underwater 5",
"Swamp Platform Row 4",
"Swamp Platform Shortcut Right Panel",
"Swamp Between Bridges Far Row 4",
"Swamp Red Underwater 4",
"Swamp Purple Underwater",
"Swamp Beyond Rotating Bridge 4",
"Swamp Blue Underwater 5",
"Swamp Laser Panel",
"Swamp Laser Shortcut Right Panel",
"Treehouse First Door Panel",
"Treehouse Second Door Panel",
"Treehouse Third Door Panel",
"Treehouse Yellow Bridge 9",
"Treehouse First Purple Bridge 5",
"Treehouse Second Purple Bridge 7",
"Treehouse Green Bridge 7",
"Treehouse Green Bridge Discard",
"Treehouse Left Orange Bridge 15",
"Treehouse Laser Discard",
"Treehouse Right Orange Bridge 12",
"Treehouse Laser Panel",
"Treehouse Drawbridge Panel",
"Mountainside Discard",
"Mountainside Vault Box",
"Mountaintop River Shape",
"Tutorial First Hallway EP",
"Tutorial Cloud EP",
"Tutorial Patio Flowers EP",
"Tutorial Gate EP",
"Outside Tutorial Garden EP",
"Outside Tutorial Town Sewer EP",
"Outside Tutorial Path EP",
"Outside Tutorial Tractor EP",
"Mountainside Thundercloud EP",
"Glass Factory Vase EP",
"Symmetry Island Glass Factory Black Line Reflection EP",
"Symmetry Island Glass Factory Black Line EP",
"Desert Sand Snake EP",
"Desert Facade Right EP",
"Desert Facade Left EP",
"Desert Stairs Left EP",
"Desert Stairs Right EP",
"Desert Broken Wall Straight EP",
"Desert Broken Wall Bend EP",
"Desert Shore EP",
"Desert Island EP",
"Desert Pond Room Near Reflection EP",
"Desert Pond Room Far Reflection EP",
"Desert Flood Room EP",
"Desert Elevator EP",
"Quarry Shore EP",
"Quarry Entrance Pipe EP",
"Quarry Sand Pile EP",
"Quarry Rock Line EP",
"Quarry Rock Line Reflection EP",
"Quarry Railroad EP",
"Quarry Stoneworks Ramp EP",
"Quarry Stoneworks Lift EP",
"Quarry Boathouse Moving Ramp EP",
"Quarry Boathouse Hook EP",
"Shadows Quarry Stoneworks Rooftop Vent EP",
"Treehouse Beach Rock Shadow EP",
"Treehouse Beach Sand Shadow EP",
"Treehouse Beach Both Orange Bridges EP",
"Keep Red Flowers EP",
"Keep Purple Flowers EP",
"Shipwreck Circle Near EP",
"Shipwreck Circle Left EP",
"Shipwreck Circle Far EP",
"Shipwreck Stern EP",
"Shipwreck Rope Inner EP",
"Shipwreck Rope Outer EP",
"Shipwreck Couch EP",
"Keep Pressure Plates 1 EP",
"Keep Pressure Plates 2 EP",
"Keep Pressure Plates 3 EP",
"Keep Pressure Plates 4 Left Exit EP",
"Keep Pressure Plates 4 Right Exit EP",
"Keep Path EP",
"Keep Hedges EP",
"Monastery Facade Left Near EP",
"Monastery Facade Left Far Short EP",
"Monastery Facade Left Far Long EP",
"Monastery Facade Right Near EP",
"Monastery Facade Left Stairs EP",
"Monastery Facade Right Stairs EP",
"Monastery Grass Stairs EP",
"Monastery Left Shutter EP",
"Monastery Middle Shutter EP",
"Monastery Right Shutter EP",
"Windmill First Blade EP",
"Windmill Second Blade EP",
"Windmill Third Blade EP",
"Town Tower Underside Third EP",
"Town Tower Underside Fourth EP",
"Town Tower Underside First EP",
"Town Tower Underside Second EP",
"Town RGB House Red EP",
"Town RGB House Green EP",
"Town Maze Bridge Underside EP",
"Town Black Line Redirect EP",
"Town Black Line Church EP",
"Town Brown Bridge EP",
"Town Black Line Tower EP",
"Theater Eclipse EP",
"Theater Window EP",
"Theater Door EP",
"Theater Church EP",
"Jungle Long Arch Moss EP",
"Jungle Straight Left Moss EP",
"Jungle Pop-up Wall Moss EP",
"Jungle Short Arch Moss EP",
"Jungle Entrance EP",
"Jungle Tree Halo EP",
"Jungle Bamboo CCW EP",
"Jungle Bamboo CW EP",
"Jungle Green Leaf Moss EP",
"Monastery Garden Left EP",
"Monastery Garden Right EP",
"Monastery Wall EP",
"Bunker Tinted Door EP",
"Bunker Green Room Flowers EP",
"Swamp Purple Sand Middle EP",
"Swamp Purple Sand Top EP",
"Swamp Purple Sand Bottom EP",
"Swamp Sliding Bridge Left EP",
"Swamp Sliding Bridge Right EP",
"Swamp Cyan Underwater Sliding Bridge EP",
"Swamp Rotating Bridge CCW EP",
"Swamp Rotating Bridge CW EP",
"Swamp Boat EP",
"Swamp Long Bridge Side EP",
"Swamp Purple Underwater Right EP",
"Swamp Purple Underwater Left EP",
"Treehouse Buoy EP",
"Treehouse Right Orange Bridge EP",
"Treehouse Burned House Beach EP",
"Mountainside Cloud Cycle EP",
"Mountainside Bush EP",
"Mountainside Apparent River EP",
"Mountaintop River Shape EP",
"Mountaintop Arch Black EP",
"Mountaintop Arch White Right EP",
"Mountaintop Arch White Left EP",
"Mountain Bottom Floor Yellow Bridge EP",
"Mountain Bottom Floor Blue Bridge EP",
"Mountain Floor 2 Pink Bridge EP",
"Caves Skylight EP",
"Challenge Water EP",
"Tunnels Theater Flowers EP",
"Boat Desert EP",
"Boat Shipwreck CCW Underside EP",
"Boat Shipwreck Green EP",
"Boat Shipwreck CW Underside EP",
"Boat Bunker Yellow Line EP",
"Boat Town Long Sewer EP",
"Boat Tutorial EP",
"Boat Tutorial Reflection EP",
"Boat Tutorial Moss EP",
"Boat Cargo Box EP",
"Desert Obelisk Side 1",
"Desert Obelisk Side 2",
"Desert Obelisk Side 3",
"Desert Obelisk Side 4",
"Desert Obelisk Side 5",
"Monastery Obelisk Side 1",
"Monastery Obelisk Side 2",
"Monastery Obelisk Side 3",
"Monastery Obelisk Side 4",
"Monastery Obelisk Side 5",
"Monastery Obelisk Side 6",
"Treehouse Obelisk Side 1",
"Treehouse Obelisk Side 2",
"Treehouse Obelisk Side 3",
"Treehouse Obelisk Side 4",
"Treehouse Obelisk Side 5",
"Treehouse Obelisk Side 6",
"Mountainside Obelisk Side 1",
"Mountainside Obelisk Side 2",
"Mountainside Obelisk Side 3",
"Mountainside Obelisk Side 4",
"Mountainside Obelisk Side 5",
"Mountainside Obelisk Side 6",
"Quarry Obelisk Side 1",
"Quarry Obelisk Side 2",
"Quarry Obelisk Side 3",
"Quarry Obelisk Side 4",
"Quarry Obelisk Side 5",
"Town Obelisk Side 1",
"Town Obelisk Side 2",
"Town Obelisk Side 3",
"Town Obelisk Side 4",
"Town Obelisk Side 5",
"Town Obelisk Side 6",
"Caves Mountain Shortcut Panel",
"Caves Swamp Shortcut Panel",
"Caves Blue Tunnel Right First 4",
"Caves Blue Tunnel Left First 1",
"Caves Blue Tunnel Left Second 5",
"Caves Blue Tunnel Right Second 5",
"Caves Blue Tunnel Right Third 1",
"Caves Blue Tunnel Left Fourth 1",
"Caves Blue Tunnel Left Third 1",
"Caves First Floor Middle",
"Caves First Floor Right",
"Caves First Floor Left",
"Caves First Floor Grounded",
"Caves Lone Pillar",
"Caves First Wooden Beam",
"Caves Second Wooden Beam",
"Caves Third Wooden Beam",
"Caves Fourth Wooden Beam",
"Caves Right Upstairs Left Row 8",
"Caves Right Upstairs Right Row 3",
"Caves Left Upstairs Single",
"Caves Left Upstairs Left Row 5",
"Caves Challenge Entry Panel",
"Challenge Tunnels Entry Panel",
"Tunnels Vault Box",
"Theater Challenge Video",
"Tunnels Town Shortcut Panel",
"Caves Skylight EP",
"Challenge Water EP",
"Tunnels Theater Flowers EP",
"Tutorial Gate EP",
"Mountaintop Mountain Entry Panel",
"Mountain Floor 1 Light Bridge Controller",
"Mountain Floor 1 Right Row 5",
"Mountain Floor 1 Left Row 7",
"Mountain Floor 1 Back Row 3",
"Mountain Floor 1 Trash Pillar 2",
"Mountain Floor 2 Near Row 5",
"Mountain Floor 2 Far Row 6",
"Mountain Floor 2 Light Bridge Controller Near",
"Mountain Floor 2 Light Bridge Controller Far",
"Mountain Bottom Floor Yellow Bridge EP",
"Mountain Bottom Floor Blue Bridge EP",
"Mountain Floor 2 Pink Bridge EP",
"Mountain Floor 2 Elevator Discard",
"Mountain Bottom Floor Giant Puzzle",
"Mountain Bottom Floor Pillars Room Entry Left",
"Mountain Bottom Floor Pillars Room Entry Right",
"Mountain Bottom Floor Caves Entry Panel",
"Mountain Bottom Floor Left Pillar 4",
"Mountain Bottom Floor Right Pillar 4",
"Challenge Vault Box",
"Theater Challenge Video",
"Mountain Bottom Floor Discard",
}
OBELISK_SIDES = {
"Desert Obelisk Side 1",
"Desert Obelisk Side 2",
"Desert Obelisk Side 3",
"Desert Obelisk Side 4",
"Desert Obelisk Side 5",
"Monastery Obelisk Side 1",
"Monastery Obelisk Side 2",
"Monastery Obelisk Side 3",
"Monastery Obelisk Side 4",
"Monastery Obelisk Side 5",
"Monastery Obelisk Side 6",
"Treehouse Obelisk Side 1",
"Treehouse Obelisk Side 2",
"Treehouse Obelisk Side 3",
"Treehouse Obelisk Side 4",
"Treehouse Obelisk Side 5",
"Treehouse Obelisk Side 6",
"Mountainside Obelisk Side 1",
"Mountainside Obelisk Side 2",
"Mountainside Obelisk Side 3",
"Mountainside Obelisk Side 4",
"Mountainside Obelisk Side 5",
"Mountainside Obelisk Side 6",
"Quarry Obelisk Side 1",
"Quarry Obelisk Side 2",
"Quarry Obelisk Side 3",
"Quarry Obelisk Side 4",
"Quarry Obelisk Side 5",
"Town Obelisk Side 1",
"Town Obelisk Side 2",
"Town Obelisk Side 3",
"Town Obelisk Side 4",
"Town Obelisk Side 5",
"Town Obelisk Side 6",
}
ALL_LOCATIONS_TO_ID = dict()
AREA_LOCATION_GROUPS = dict()
def get_id(entity_hex: str) -> str:
"""
Calculates the location ID for any given location
"""
return static_witness_logic.ENTITIES_BY_HEX[entity_hex]["id"]
def get_event_name(entity_hex: str) -> str:
"""
Returns the event name of any given panel.
"""
action = " Opened" if static_witness_logic.ENTITIES_BY_HEX[entity_hex]["entityType"] == "Door" else " Solved"
return static_witness_logic.ENTITIES_BY_HEX[entity_hex]["checkName"] + action
ALL_LOCATIONS_TO_IDS = {
panel_obj["checkName"]: get_id(chex)
for chex, panel_obj in static_witness_logic.ENTITIES_BY_HEX.items()
if panel_obj["id"]
}
ALL_LOCATIONS_TO_IDS = dict(
sorted(ALL_LOCATIONS_TO_IDS.items(), key=lambda loc: loc[1])
)
for key, item in ALL_LOCATIONS_TO_IDS.items():
ALL_LOCATIONS_TO_ID[key] = item
for loc in ALL_LOCATIONS_TO_IDS:
area = static_witness_logic.ENTITIES_BY_NAME[loc]["area"]["name"]
AREA_LOCATION_GROUPS.setdefault(area, []).append(loc)

View File

@ -1,56 +1,26 @@
from dataclasses import dataclass
from enum import Enum
from functools import lru_cache
from typing import Dict, List
from .utils import define_new_region, parse_lambda, lazy, get_items, get_sigma_normal_logic, get_sigma_expert_logic,\
get_vanilla_logic
class ItemCategory(Enum):
SYMBOL = 0
DOOR = 1
LASER = 2
USEFUL = 3
FILLER = 4
TRAP = 5
JOKE = 6
EVENT = 7
CATEGORY_NAME_MAPPINGS: Dict[str, ItemCategory] = {
"Symbols:": ItemCategory.SYMBOL,
"Doors:": ItemCategory.DOOR,
"Lasers:": ItemCategory.LASER,
"Useful:": ItemCategory.USEFUL,
"Filler:": ItemCategory.FILLER,
"Traps:": ItemCategory.TRAP,
"Jokes:": ItemCategory.JOKE
}
@dataclass(frozen=True)
class ItemDefinition:
local_code: int
category: ItemCategory
@dataclass(frozen=True)
class ProgressiveItemDefinition(ItemDefinition):
child_item_names: List[str]
@dataclass(frozen=True)
class DoorItemDefinition(ItemDefinition):
panel_id_hexes: List[str]
@dataclass(frozen=True)
class WeightedItemDefinition(ItemDefinition):
weight: int
from .item_definition_classes import (
CATEGORY_NAME_MAPPINGS,
DoorItemDefinition,
ItemCategory,
ItemDefinition,
ProgressiveItemDefinition,
WeightedItemDefinition,
)
from .utils import (
define_new_region,
get_items,
get_sigma_expert_logic,
get_sigma_normal_logic,
get_vanilla_logic,
parse_lambda,
)
class StaticWitnessLogicObj:
def read_logic_file(self, lines):
def read_logic_file(self, lines) -> None:
"""
Reads the logic file and does the initial population of data structures
"""
@ -152,7 +122,7 @@ class StaticWitnessLogicObj:
}
if location_type == "Obelisk Side":
eps = set(list(required_panels)[0])
eps = set(next(iter(required_panels)))
eps -= {"Theater to Tunnels"}
eps_ints = {int(h, 16) for h in eps}
@ -177,7 +147,7 @@ class StaticWitnessLogicObj:
current_region["panels"].append(entity_hex)
def __init__(self, lines=None):
def __init__(self, lines=None) -> None:
if lines is None:
lines = get_sigma_normal_logic()
@ -199,102 +169,95 @@ class StaticWitnessLogicObj:
self.read_logic_file(lines)
class StaticWitnessLogic:
# Item data parsed from WitnessItems.txt
all_items: Dict[str, ItemDefinition] = {}
_progressive_lookup: Dict[str, str] = {}
ALL_REGIONS_BY_NAME = dict()
ALL_AREAS_BY_NAME = dict()
STATIC_CONNECTIONS_BY_REGION_NAME = dict()
OBELISK_SIDE_ID_TO_EP_HEXES = dict()
ENTITIES_BY_HEX = dict()
ENTITIES_BY_NAME = dict()
STATIC_DEPENDENT_REQUIREMENTS_BY_HEX = dict()
EP_TO_OBELISK_SIDE = dict()
ENTITY_ID_TO_NAME = dict()
@staticmethod
def parse_items():
"""
Parses currently defined items from WitnessItems.txt
"""
lines: List[str] = get_items()
current_category: ItemCategory = ItemCategory.SYMBOL
for line in lines:
# Skip empty lines and comments.
if line == "" or line[0] == "#":
continue
# If this line is a category header, update our cached category.
if line in CATEGORY_NAME_MAPPINGS.keys():
current_category = CATEGORY_NAME_MAPPINGS[line]
continue
line_split = line.split(" - ")
item_code = int(line_split[0])
item_name = line_split[1]
arguments: List[str] = line_split[2].split(",") if len(line_split) >= 3 else []
if current_category in [ItemCategory.DOOR, ItemCategory.LASER]:
# Map doors to IDs.
StaticWitnessLogic.all_items[item_name] = DoorItemDefinition(item_code, current_category,
arguments)
elif current_category == ItemCategory.TRAP or current_category == ItemCategory.FILLER:
# Read filler weights.
weight = int(arguments[0]) if len(arguments) >= 1 else 1
StaticWitnessLogic.all_items[item_name] = WeightedItemDefinition(item_code, current_category, weight)
elif arguments:
# Progressive items.
StaticWitnessLogic.all_items[item_name] = ProgressiveItemDefinition(item_code, current_category,
arguments)
for child_item in arguments:
StaticWitnessLogic._progressive_lookup[child_item] = item_name
else:
StaticWitnessLogic.all_items[item_name] = ItemDefinition(item_code, current_category)
@staticmethod
def get_parent_progressive_item(item_name: str):
"""
Returns the name of the item's progressive parent, if there is one, or the item's name if not.
"""
return StaticWitnessLogic._progressive_lookup.get(item_name, item_name)
@lazy
def sigma_expert(self) -> StaticWitnessLogicObj:
return StaticWitnessLogicObj(get_sigma_expert_logic())
@lazy
def sigma_normal(self) -> StaticWitnessLogicObj:
return StaticWitnessLogicObj(get_sigma_normal_logic())
@lazy
def vanilla(self) -> StaticWitnessLogicObj:
return StaticWitnessLogicObj(get_vanilla_logic())
def __init__(self):
self.parse_items()
self.ALL_REGIONS_BY_NAME.update(self.sigma_normal.ALL_REGIONS_BY_NAME)
self.ALL_AREAS_BY_NAME.update(self.sigma_normal.ALL_AREAS_BY_NAME)
self.STATIC_CONNECTIONS_BY_REGION_NAME.update(self.sigma_normal.STATIC_CONNECTIONS_BY_REGION_NAME)
self.ENTITIES_BY_HEX.update(self.sigma_normal.ENTITIES_BY_HEX)
self.ENTITIES_BY_NAME.update(self.sigma_normal.ENTITIES_BY_NAME)
self.STATIC_DEPENDENT_REQUIREMENTS_BY_HEX.update(self.sigma_normal.STATIC_DEPENDENT_REQUIREMENTS_BY_HEX)
self.OBELISK_SIDE_ID_TO_EP_HEXES.update(self.sigma_normal.OBELISK_SIDE_ID_TO_EP_HEXES)
self.EP_TO_OBELISK_SIDE.update(self.sigma_normal.EP_TO_OBELISK_SIDE)
self.ENTITY_ID_TO_NAME.update(self.sigma_normal.ENTITY_ID_TO_NAME)
# Item data parsed from WitnessItems.txt
ALL_ITEMS: Dict[str, ItemDefinition] = {}
_progressive_lookup: Dict[str, str] = {}
StaticWitnessLogic()
def parse_items() -> None:
"""
Parses currently defined items from WitnessItems.txt
"""
lines: List[str] = get_items()
current_category: ItemCategory = ItemCategory.SYMBOL
for line in lines:
# Skip empty lines and comments.
if line == "" or line[0] == "#":
continue
# If this line is a category header, update our cached category.
if line in CATEGORY_NAME_MAPPINGS.keys():
current_category = CATEGORY_NAME_MAPPINGS[line]
continue
line_split = line.split(" - ")
item_code = int(line_split[0])
item_name = line_split[1]
arguments: List[str] = line_split[2].split(",") if len(line_split) >= 3 else []
if current_category in [ItemCategory.DOOR, ItemCategory.LASER]:
# Map doors to IDs.
ALL_ITEMS[item_name] = DoorItemDefinition(item_code, current_category, arguments)
elif current_category == ItemCategory.TRAP or current_category == ItemCategory.FILLER:
# Read filler weights.
weight = int(arguments[0]) if len(arguments) >= 1 else 1
ALL_ITEMS[item_name] = WeightedItemDefinition(item_code, current_category, weight)
elif arguments:
# Progressive items.
ALL_ITEMS[item_name] = ProgressiveItemDefinition(item_code, current_category, arguments)
for child_item in arguments:
_progressive_lookup[child_item] = item_name
else:
ALL_ITEMS[item_name] = ItemDefinition(item_code, current_category)
def get_parent_progressive_item(item_name: str) -> str:
"""
Returns the name of the item's progressive parent, if there is one, or the item's name if not.
"""
return _progressive_lookup.get(item_name, item_name)
@lru_cache
def get_vanilla() -> StaticWitnessLogicObj:
return StaticWitnessLogicObj(get_vanilla_logic())
@lru_cache
def get_sigma_normal() -> StaticWitnessLogicObj:
return StaticWitnessLogicObj(get_sigma_normal_logic())
@lru_cache
def get_sigma_expert() -> StaticWitnessLogicObj:
return StaticWitnessLogicObj(get_sigma_expert_logic())
def __getattr__(name):
if name == "vanilla":
return get_vanilla()
elif name == "sigma_normal":
return get_sigma_normal()
elif name == "sigma_expert":
return get_sigma_expert()
raise AttributeError(f"module '{__name__}' has no attribute '{name}'")
parse_items()
ALL_REGIONS_BY_NAME = get_sigma_normal().ALL_REGIONS_BY_NAME
ALL_AREAS_BY_NAME = get_sigma_normal().ALL_AREAS_BY_NAME
STATIC_CONNECTIONS_BY_REGION_NAME = get_sigma_normal().STATIC_CONNECTIONS_BY_REGION_NAME
ENTITIES_BY_HEX = get_sigma_normal().ENTITIES_BY_HEX
ENTITIES_BY_NAME = get_sigma_normal().ENTITIES_BY_NAME
STATIC_DEPENDENT_REQUIREMENTS_BY_HEX = get_sigma_normal().STATIC_DEPENDENT_REQUIREMENTS_BY_HEX
OBELISK_SIDE_ID_TO_EP_HEXES = get_sigma_normal().OBELISK_SIDE_ID_TO_EP_HEXES
EP_TO_OBELISK_SIDE = get_sigma_normal().EP_TO_OBELISK_SIDE
ENTITY_ID_TO_NAME = get_sigma_normal().ENTITY_ID_TO_NAME

View File

@ -1,11 +1,11 @@
from functools import lru_cache
from math import floor
from typing import List, Collection, FrozenSet, Tuple, Dict, Any, Set
from pkgutil import get_data
from random import random
from typing import Any, Collection, Dict, FrozenSet, List, Set, Tuple
def weighted_sample(world_random: random, population: List, weights: List[float], k: int):
def weighted_sample(world_random: random, population: List, weights: List[float], k: int) -> List:
positions = range(len(population))
indices = []
while True:
@ -95,25 +95,9 @@ def parse_lambda(lambda_string) -> FrozenSet[FrozenSet[str]]:
return lambda_set
class lazy(object):
def __init__(self, func, name=None):
self.func = func
self.name = name if name is not None else func.__name__
self.__doc__ = func.__doc__
def __get__(self, instance, class_):
if instance is None:
res = self.func(class_)
setattr(class_, self.name, res)
return res
res = self.func(instance)
setattr(instance, self.name, res)
return res
@lru_cache(maxsize=None)
def get_adjustment_file(adjustment_file: str) -> List[str]:
data = get_data(__name__, adjustment_file).decode('utf-8')
data = get_data(__name__, adjustment_file).decode("utf-8")
return [line.strip() for line in data.split("\n")]

View File

@ -1,9 +1,11 @@
import logging
from dataclasses import dataclass
from typing import Tuple, List, TYPE_CHECKING, Set, Dict, Optional, Union
from BaseClasses import Item, ItemClassification, Location, LocationProgressType, CollectionState
from . import StaticWitnessLogic
from .utils import weighted_sample
from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple, Union
from BaseClasses import CollectionState, Item, Location, LocationProgressType
from .data import static_logic as static_witness_logic
from .data.utils import weighted_sample
if TYPE_CHECKING:
from . import WitnessWorld
@ -22,7 +24,7 @@ joke_hints = [
"Have you tried Clique?\nIt's certainly a lot less complicated than this game!",
"Have you tried Dark Souls III?\nA tough game like this feels better when friends are helping you!",
"Have you tried Donkey Kong Country 3?\nA legendary game from a golden age of platformers!",
"Have you tried DLC Quest?\nI know you all like parody games.\nI got way too many requests to make a randomizer for \"The Looker\".",
'Have you tried DLC Quest?\nI know you all like parody games.\nI got way too many requests to make a randomizer for "The Looker".',
"Have you tried Doom?\nI wonder if a smart fridge can connect to Archipelago.",
"Have you tried Doom II?\nGot a good game on your hands? Just make it bigger and better.",
"Have you tried Factorio?\nAlone in an unknown multiworld. Sound familiar?",
@ -62,9 +64,9 @@ joke_hints = [
"Have you tried Slay the Spire?\nExperience the thrill of combat without needing fast fingers!",
"Have you tried Stardew Valley?\nThe Farming game that gave a damn. It's so easy to lose hours and days to it...",
"Have you tried Subnautica?\nIf you like this game's lonely atmosphere, I would suggest you try it.",
"Have you tried Terraria?\nA prime example of a survival sandbox game that beats the \"Wide as an ocean, deep as a puddle\" allegations.",
'Have you tried Terraria?\nA prime example of a survival sandbox game that beats the "Wide as an ocean, deep as a puddle" allegations.',
"Have you tried Timespinner?\nEveryone who plays it ends up loving it!",
"Have you tried The Legend of Zelda?\nIn some sense, it was the starting point of \"adventure\" in video games.",
'Have you tried The Legend of Zelda?\nIn some sense, it was the starting point of "adventure" in video games.',
"Have you tried TUNC?\nWhat? No, I'm pretty sure I spelled that right.",
"Have you tried TUNIC?\nRemember what discovering your first Environmental Puzzle was like?\nTUNIC will make you feel like that at least 5 times over.",
"Have you tried Undertale?\nI hope I'm not the 10th person to ask you that. But it's, like, really good.",
@ -72,7 +74,7 @@ joke_hints = [
"Have you tried Wargroove?\nI'm glad that for every abandoned series, enough people are yearning for its return that one of them will know how to code.",
"Have you tried The Witness?\nOh. I guess you already have. Thanks for playing!",
"Have you tried Zillion?\nMe neither. But it looks fun. So, let's try something new together?",
"Have you tried Zork: Grand Inquisitor?\nThis 1997 game uses Z-Vision technology to simulate 3D environments.\nCome on, I know you wanna find out what \"Z-Vision\" is.",
'Have you tried Zork: Grand Inquisitor?\nThis 1997 game uses Z-Vision technology to simulate 3D environments.\nCome on, I know you wanna find out what "Z-Vision" is.',
"Quaternions break my brain",
"Eclipse has nothing, but you should do it anyway.",
@ -136,10 +138,10 @@ joke_hints = [
"In the future, war will break out between obelisk_sides and individual EP players.\nWhich side are you on?",
"Droplets: Low, High, Mid.\nAmbience: Mid, Low, Mid, High.",
"Name a better game involving lines. I'll wait.",
"\"You have to draw a line in the sand.\"\n- Arin \"Egoraptor\" Hanson",
'"You have to draw a line in the sand."\n- Arin "Egoraptor" Hanson',
"Have you tried?\nThe puzzles tend to get easier if you do.",
"Sorry, I accidentally left my phone in the Jungle.\nAnd also all my fragile dishes.",
"Winner of the \"Most Irrelevant PR in AP History\" award!",
'Winner of the "Most Irrelevant PR in AP History" award!',
"I bet you wish this was a real hint :)",
"\"This hint is an impostor.\"- Junk hint submitted by T1mshady.\n...wait, I'm not supposed to say that part?",
"Wouldn't you like to know, weather buoy?",
@ -192,10 +194,10 @@ class WitnessLocationHint:
hint_came_from_location: bool
# If a hint gets added to a set twice, but once as an item hint and once as a location hint, those are the same
def __hash__(self):
def __hash__(self) -> int:
return hash(self.location)
def __eq__(self, other):
def __eq__(self, other) -> bool:
return self.location == other.location
@ -324,7 +326,7 @@ def get_priority_hint_locations(world: "WitnessWorld") -> List[str]:
"Boat Shipwreck Green EP",
"Quarry Stoneworks Control Room Left",
]
# Add Obelisk Sides that contain EPs that are meant to be hinted, if they are necessary to complete the Obelisk Side
if "0x33A20" not in world.player_logic.COMPLETELY_DISABLED_ENTITIES:
priority.append("Town Obelisk Side 6") # Theater Flowers EP
@ -338,7 +340,7 @@ def get_priority_hint_locations(world: "WitnessWorld") -> List[str]:
return priority
def word_direct_hint(world: "WitnessWorld", hint: WitnessLocationHint):
def word_direct_hint(world: "WitnessWorld", hint: WitnessLocationHint) -> WitnessWordedHint:
location_name = hint.location.name
if hint.location.player != world.player:
location_name += " (" + world.multiworld.get_player_name(hint.location.player) + ")"
@ -373,8 +375,8 @@ def hint_from_item(world: "WitnessWorld", item_name: str, own_itempool: List[Ite
def hint_from_location(world: "WitnessWorld", location: str) -> Optional[WitnessLocationHint]:
location_obj = world.multiworld.get_location(location, world.player)
item_obj = world.multiworld.get_location(location, world.player).item
location_obj = world.get_location(location)
item_obj = location_obj.item
item_name = item_obj.name
if item_obj.player != world.player:
item_name += " (" + world.multiworld.get_player_name(item_obj.player) + ")"
@ -382,7 +384,8 @@ def hint_from_location(world: "WitnessWorld", location: str) -> Optional[Witness
return WitnessLocationHint(location_obj, True)
def get_items_and_locations_in_random_order(world: "WitnessWorld", own_itempool: List[Item]):
def get_items_and_locations_in_random_order(world: "WitnessWorld",
own_itempool: List[Item]) -> Tuple[List[str], List[str]]:
prog_items_in_this_world = sorted(
item.name for item in own_itempool
if item.advancement and item.code and item.location
@ -455,7 +458,11 @@ def make_extra_location_hints(world: "WitnessWorld", hint_amount: int, own_itemp
hints = []
# This is a way to reverse a Dict[a,List[b]] to a Dict[b,a]
area_reverse_lookup = {v: k for k, l in unhinted_locations_for_hinted_areas.items() for v in l}
area_reverse_lookup = {
unhinted_location: hinted_area
for hinted_area, unhinted_locations in unhinted_locations_for_hinted_areas.items()
for unhinted_location in unhinted_locations
}
while len(hints) < hint_amount:
if not prog_items_in_this_world and not locations_in_this_world and not hints_to_use_first:
@ -529,16 +536,16 @@ def choose_areas(world: "WitnessWorld", amount: int, locations_per_area: Dict[st
def get_hintable_areas(world: "WitnessWorld") -> Tuple[Dict[str, List[Location]], Dict[str, List[Item]]]:
potential_areas = list(StaticWitnessLogic.ALL_AREAS_BY_NAME.keys())
potential_areas = list(static_witness_logic.ALL_AREAS_BY_NAME.keys())
locations_per_area = dict()
items_per_area = dict()
for area in potential_areas:
regions = [
world.regio.created_regions[region]
for region in StaticWitnessLogic.ALL_AREAS_BY_NAME[area]["regions"]
if region in world.regio.created_regions
world.player_regions.created_regions[region]
for region in static_witness_logic.ALL_AREAS_BY_NAME[area]["regions"]
if region in world.player_regions.created_regions
]
locations = [location for region in regions for location in region.get_locations() if location.address]
@ -596,7 +603,7 @@ def word_area_hint(world: "WitnessWorld", hinted_area: str, corresponding_items:
if local_lasers == total_progression:
sentence_end = (" for this world." if player_count > 1 else ".")
hint_string += f"\nAll of them are lasers" + sentence_end
hint_string += "\nAll of them are lasers" + sentence_end
elif player_count > 1:
if local_progression and non_local_progression:
@ -663,7 +670,7 @@ def create_all_hints(world: "WitnessWorld", hint_amount: int, area_hints: int,
already_hinted_locations |= {
loc for loc in world.multiworld.get_reachable_locations(state, world.player)
if loc.address and StaticWitnessLogic.ENTITIES_BY_NAME[loc.name]["area"]["name"] == "Tutorial (Inside)"
if loc.address and static_witness_logic.ENTITIES_BY_NAME[loc.name]["area"]["name"] == "Tutorial (Inside)"
}
intended_location_hints = hint_amount - area_hints

View File

@ -3,511 +3,24 @@ Defines constants for different types of locations in the game
"""
from typing import TYPE_CHECKING
from .data import static_locations as static_witness_locations
from .data import static_logic as static_witness_logic
from .player_logic import WitnessPlayerLogic
from .static_logic import StaticWitnessLogic
if TYPE_CHECKING:
from . import WitnessWorld
ID_START = 158000
class StaticWitnessLocations:
"""
Witness Location Constants that stay consistent across worlds
"""
GENERAL_LOCATIONS = {
"Tutorial Front Left",
"Tutorial Back Left",
"Tutorial Back Right",
"Tutorial Patio Floor",
"Tutorial Gate Open",
"Outside Tutorial Vault Box",
"Outside Tutorial Discard",
"Outside Tutorial Shed Row 5",
"Outside Tutorial Tree Row 9",
"Outside Tutorial Outpost Entry Panel",
"Outside Tutorial Outpost Exit Panel",
"Glass Factory Discard",
"Glass Factory Back Wall 5",
"Glass Factory Front 3",
"Glass Factory Melting 3",
"Symmetry Island Lower Panel",
"Symmetry Island Right 5",
"Symmetry Island Back 6",
"Symmetry Island Left 7",
"Symmetry Island Upper Panel",
"Symmetry Island Scenery Outlines 5",
"Symmetry Island Laser Yellow 3",
"Symmetry Island Laser Blue 3",
"Symmetry Island Laser Panel",
"Orchard Apple Tree 5",
"Desert Vault Box",
"Desert Discard",
"Desert Surface 8",
"Desert Light Room 3",
"Desert Pond Room 5",
"Desert Flood Room 6",
"Desert Elevator Room Hexagonal",
"Desert Elevator Room Bent 3",
"Desert Laser Panel",
"Quarry Entry 1 Panel",
"Quarry Entry 2 Panel",
"Quarry Stoneworks Entry Left Panel",
"Quarry Stoneworks Entry Right Panel",
"Quarry Stoneworks Lower Row 6",
"Quarry Stoneworks Upper Row 8",
"Quarry Stoneworks Control Room Left",
"Quarry Stoneworks Control Room Right",
"Quarry Stoneworks Stairs Panel",
"Quarry Boathouse Intro Right",
"Quarry Boathouse Intro Left",
"Quarry Boathouse Front Row 5",
"Quarry Boathouse Back First Row 9",
"Quarry Boathouse Back Second Row 3",
"Quarry Discard",
"Quarry Laser Panel",
"Shadows Intro 8",
"Shadows Far 8",
"Shadows Near 5",
"Shadows Laser Panel",
"Keep Hedge Maze 1",
"Keep Hedge Maze 2",
"Keep Hedge Maze 3",
"Keep Hedge Maze 4",
"Keep Pressure Plates 1",
"Keep Pressure Plates 2",
"Keep Pressure Plates 3",
"Keep Pressure Plates 4",
"Keep Discard",
"Keep Laser Panel Hedges",
"Keep Laser Panel Pressure Plates",
"Shipwreck Vault Box",
"Shipwreck Discard",
"Monastery Outside 3",
"Monastery Inside 4",
"Monastery Laser Panel",
"Town Cargo Box Entry Panel",
"Town Cargo Box Discard",
"Town Tall Hexagonal",
"Town Church Entry Panel",
"Town Church Lattice",
"Town Maze Panel",
"Town Rooftop Discard",
"Town Red Rooftop 5",
"Town Wooden Roof Lower Row 5",
"Town Wooden Rooftop",
"Windmill Entry Panel",
"Town RGB House Entry Panel",
"Town Laser Panel",
"Town RGB House Upstairs Left",
"Town RGB House Upstairs Right",
"Town RGB House Sound Room Right",
"Windmill Theater Entry Panel",
"Theater Exit Left Panel",
"Theater Exit Right Panel",
"Theater Tutorial Video",
"Theater Desert Video",
"Theater Jungle Video",
"Theater Shipwreck Video",
"Theater Mountain Video",
"Theater Discard",
"Jungle Discard",
"Jungle First Row 3",
"Jungle Second Row 4",
"Jungle Popup Wall 6",
"Jungle Laser Panel",
"Jungle Vault Box",
"Jungle Monastery Garden Shortcut Panel",
"Bunker Entry Panel",
"Bunker Intro Left 5",
"Bunker Intro Back 4",
"Bunker Glass Room 3",
"Bunker UV Room 2",
"Bunker Laser Panel",
"Swamp Entry Panel",
"Swamp Intro Front 6",
"Swamp Intro Back 8",
"Swamp Between Bridges Near Row 4",
"Swamp Cyan Underwater 5",
"Swamp Platform Row 4",
"Swamp Platform Shortcut Right Panel",
"Swamp Between Bridges Far Row 4",
"Swamp Red Underwater 4",
"Swamp Purple Underwater",
"Swamp Beyond Rotating Bridge 4",
"Swamp Blue Underwater 5",
"Swamp Laser Panel",
"Swamp Laser Shortcut Right Panel",
"Treehouse First Door Panel",
"Treehouse Second Door Panel",
"Treehouse Third Door Panel",
"Treehouse Yellow Bridge 9",
"Treehouse First Purple Bridge 5",
"Treehouse Second Purple Bridge 7",
"Treehouse Green Bridge 7",
"Treehouse Green Bridge Discard",
"Treehouse Left Orange Bridge 15",
"Treehouse Laser Discard",
"Treehouse Right Orange Bridge 12",
"Treehouse Laser Panel",
"Treehouse Drawbridge Panel",
"Mountainside Discard",
"Mountainside Vault Box",
"Mountaintop River Shape",
"Tutorial First Hallway EP",
"Tutorial Cloud EP",
"Tutorial Patio Flowers EP",
"Tutorial Gate EP",
"Outside Tutorial Garden EP",
"Outside Tutorial Town Sewer EP",
"Outside Tutorial Path EP",
"Outside Tutorial Tractor EP",
"Mountainside Thundercloud EP",
"Glass Factory Vase EP",
"Symmetry Island Glass Factory Black Line Reflection EP",
"Symmetry Island Glass Factory Black Line EP",
"Desert Sand Snake EP",
"Desert Facade Right EP",
"Desert Facade Left EP",
"Desert Stairs Left EP",
"Desert Stairs Right EP",
"Desert Broken Wall Straight EP",
"Desert Broken Wall Bend EP",
"Desert Shore EP",
"Desert Island EP",
"Desert Pond Room Near Reflection EP",
"Desert Pond Room Far Reflection EP",
"Desert Flood Room EP",
"Desert Elevator EP",
"Quarry Shore EP",
"Quarry Entrance Pipe EP",
"Quarry Sand Pile EP",
"Quarry Rock Line EP",
"Quarry Rock Line Reflection EP",
"Quarry Railroad EP",
"Quarry Stoneworks Ramp EP",
"Quarry Stoneworks Lift EP",
"Quarry Boathouse Moving Ramp EP",
"Quarry Boathouse Hook EP",
"Shadows Quarry Stoneworks Rooftop Vent EP",
"Treehouse Beach Rock Shadow EP",
"Treehouse Beach Sand Shadow EP",
"Treehouse Beach Both Orange Bridges EP",
"Keep Red Flowers EP",
"Keep Purple Flowers EP",
"Shipwreck Circle Near EP",
"Shipwreck Circle Left EP",
"Shipwreck Circle Far EP",
"Shipwreck Stern EP",
"Shipwreck Rope Inner EP",
"Shipwreck Rope Outer EP",
"Shipwreck Couch EP",
"Keep Pressure Plates 1 EP",
"Keep Pressure Plates 2 EP",
"Keep Pressure Plates 3 EP",
"Keep Pressure Plates 4 Left Exit EP",
"Keep Pressure Plates 4 Right Exit EP",
"Keep Path EP",
"Keep Hedges EP",
"Monastery Facade Left Near EP",
"Monastery Facade Left Far Short EP",
"Monastery Facade Left Far Long EP",
"Monastery Facade Right Near EP",
"Monastery Facade Left Stairs EP",
"Monastery Facade Right Stairs EP",
"Monastery Grass Stairs EP",
"Monastery Left Shutter EP",
"Monastery Middle Shutter EP",
"Monastery Right Shutter EP",
"Windmill First Blade EP",
"Windmill Second Blade EP",
"Windmill Third Blade EP",
"Town Tower Underside Third EP",
"Town Tower Underside Fourth EP",
"Town Tower Underside First EP",
"Town Tower Underside Second EP",
"Town RGB House Red EP",
"Town RGB House Green EP",
"Town Maze Bridge Underside EP",
"Town Black Line Redirect EP",
"Town Black Line Church EP",
"Town Brown Bridge EP",
"Town Black Line Tower EP",
"Theater Eclipse EP",
"Theater Window EP",
"Theater Door EP",
"Theater Church EP",
"Jungle Long Arch Moss EP",
"Jungle Straight Left Moss EP",
"Jungle Pop-up Wall Moss EP",
"Jungle Short Arch Moss EP",
"Jungle Entrance EP",
"Jungle Tree Halo EP",
"Jungle Bamboo CCW EP",
"Jungle Bamboo CW EP",
"Jungle Green Leaf Moss EP",
"Monastery Garden Left EP",
"Monastery Garden Right EP",
"Monastery Wall EP",
"Bunker Tinted Door EP",
"Bunker Green Room Flowers EP",
"Swamp Purple Sand Middle EP",
"Swamp Purple Sand Top EP",
"Swamp Purple Sand Bottom EP",
"Swamp Sliding Bridge Left EP",
"Swamp Sliding Bridge Right EP",
"Swamp Cyan Underwater Sliding Bridge EP",
"Swamp Rotating Bridge CCW EP",
"Swamp Rotating Bridge CW EP",
"Swamp Boat EP",
"Swamp Long Bridge Side EP",
"Swamp Purple Underwater Right EP",
"Swamp Purple Underwater Left EP",
"Treehouse Buoy EP",
"Treehouse Right Orange Bridge EP",
"Treehouse Burned House Beach EP",
"Mountainside Cloud Cycle EP",
"Mountainside Bush EP",
"Mountainside Apparent River EP",
"Mountaintop River Shape EP",
"Mountaintop Arch Black EP",
"Mountaintop Arch White Right EP",
"Mountaintop Arch White Left EP",
"Mountain Bottom Floor Yellow Bridge EP",
"Mountain Bottom Floor Blue Bridge EP",
"Mountain Floor 2 Pink Bridge EP",
"Caves Skylight EP",
"Challenge Water EP",
"Tunnels Theater Flowers EP",
"Boat Desert EP",
"Boat Shipwreck CCW Underside EP",
"Boat Shipwreck Green EP",
"Boat Shipwreck CW Underside EP",
"Boat Bunker Yellow Line EP",
"Boat Town Long Sewer EP",
"Boat Tutorial EP",
"Boat Tutorial Reflection EP",
"Boat Tutorial Moss EP",
"Boat Cargo Box EP",
"Desert Obelisk Side 1",
"Desert Obelisk Side 2",
"Desert Obelisk Side 3",
"Desert Obelisk Side 4",
"Desert Obelisk Side 5",
"Monastery Obelisk Side 1",
"Monastery Obelisk Side 2",
"Monastery Obelisk Side 3",
"Monastery Obelisk Side 4",
"Monastery Obelisk Side 5",
"Monastery Obelisk Side 6",
"Treehouse Obelisk Side 1",
"Treehouse Obelisk Side 2",
"Treehouse Obelisk Side 3",
"Treehouse Obelisk Side 4",
"Treehouse Obelisk Side 5",
"Treehouse Obelisk Side 6",
"Mountainside Obelisk Side 1",
"Mountainside Obelisk Side 2",
"Mountainside Obelisk Side 3",
"Mountainside Obelisk Side 4",
"Mountainside Obelisk Side 5",
"Mountainside Obelisk Side 6",
"Quarry Obelisk Side 1",
"Quarry Obelisk Side 2",
"Quarry Obelisk Side 3",
"Quarry Obelisk Side 4",
"Quarry Obelisk Side 5",
"Town Obelisk Side 1",
"Town Obelisk Side 2",
"Town Obelisk Side 3",
"Town Obelisk Side 4",
"Town Obelisk Side 5",
"Town Obelisk Side 6",
"Caves Mountain Shortcut Panel",
"Caves Swamp Shortcut Panel",
"Caves Blue Tunnel Right First 4",
"Caves Blue Tunnel Left First 1",
"Caves Blue Tunnel Left Second 5",
"Caves Blue Tunnel Right Second 5",
"Caves Blue Tunnel Right Third 1",
"Caves Blue Tunnel Left Fourth 1",
"Caves Blue Tunnel Left Third 1",
"Caves First Floor Middle",
"Caves First Floor Right",
"Caves First Floor Left",
"Caves First Floor Grounded",
"Caves Lone Pillar",
"Caves First Wooden Beam",
"Caves Second Wooden Beam",
"Caves Third Wooden Beam",
"Caves Fourth Wooden Beam",
"Caves Right Upstairs Left Row 8",
"Caves Right Upstairs Right Row 3",
"Caves Left Upstairs Single",
"Caves Left Upstairs Left Row 5",
"Caves Challenge Entry Panel",
"Challenge Tunnels Entry Panel",
"Tunnels Vault Box",
"Theater Challenge Video",
"Tunnels Town Shortcut Panel",
"Caves Skylight EP",
"Challenge Water EP",
"Tunnels Theater Flowers EP",
"Tutorial Gate EP",
"Mountaintop Mountain Entry Panel",
"Mountain Floor 1 Light Bridge Controller",
"Mountain Floor 1 Right Row 5",
"Mountain Floor 1 Left Row 7",
"Mountain Floor 1 Back Row 3",
"Mountain Floor 1 Trash Pillar 2",
"Mountain Floor 2 Near Row 5",
"Mountain Floor 2 Far Row 6",
"Mountain Floor 2 Light Bridge Controller Near",
"Mountain Floor 2 Light Bridge Controller Far",
"Mountain Bottom Floor Yellow Bridge EP",
"Mountain Bottom Floor Blue Bridge EP",
"Mountain Floor 2 Pink Bridge EP",
"Mountain Floor 2 Elevator Discard",
"Mountain Bottom Floor Giant Puzzle",
"Mountain Bottom Floor Pillars Room Entry Left",
"Mountain Bottom Floor Pillars Room Entry Right",
"Mountain Bottom Floor Caves Entry Panel",
"Mountain Bottom Floor Left Pillar 4",
"Mountain Bottom Floor Right Pillar 4",
"Challenge Vault Box",
"Theater Challenge Video",
"Mountain Bottom Floor Discard",
}
OBELISK_SIDES = {
"Desert Obelisk Side 1",
"Desert Obelisk Side 2",
"Desert Obelisk Side 3",
"Desert Obelisk Side 4",
"Desert Obelisk Side 5",
"Monastery Obelisk Side 1",
"Monastery Obelisk Side 2",
"Monastery Obelisk Side 3",
"Monastery Obelisk Side 4",
"Monastery Obelisk Side 5",
"Monastery Obelisk Side 6",
"Treehouse Obelisk Side 1",
"Treehouse Obelisk Side 2",
"Treehouse Obelisk Side 3",
"Treehouse Obelisk Side 4",
"Treehouse Obelisk Side 5",
"Treehouse Obelisk Side 6",
"Mountainside Obelisk Side 1",
"Mountainside Obelisk Side 2",
"Mountainside Obelisk Side 3",
"Mountainside Obelisk Side 4",
"Mountainside Obelisk Side 5",
"Mountainside Obelisk Side 6",
"Quarry Obelisk Side 1",
"Quarry Obelisk Side 2",
"Quarry Obelisk Side 3",
"Quarry Obelisk Side 4",
"Quarry Obelisk Side 5",
"Town Obelisk Side 1",
"Town Obelisk Side 2",
"Town Obelisk Side 3",
"Town Obelisk Side 4",
"Town Obelisk Side 5",
"Town Obelisk Side 6",
}
ALL_LOCATIONS_TO_ID = dict()
AREA_LOCATION_GROUPS = dict()
@staticmethod
def get_id(chex: str):
"""
Calculates the location ID for any given location
"""
return StaticWitnessLogic.ENTITIES_BY_HEX[chex]["id"]
@staticmethod
def get_event_name(panel_hex: str):
"""
Returns the event name of any given panel.
"""
action = " Opened" if StaticWitnessLogic.ENTITIES_BY_HEX[panel_hex]["entityType"] == "Door" else " Solved"
return StaticWitnessLogic.ENTITIES_BY_HEX[panel_hex]["checkName"] + action
def __init__(self):
all_loc_to_id = {
panel_obj["checkName"]: self.get_id(chex)
for chex, panel_obj in StaticWitnessLogic.ENTITIES_BY_HEX.items()
if panel_obj["id"]
}
all_loc_to_id = dict(
sorted(all_loc_to_id.items(), key=lambda loc: loc[1])
)
for key, item in all_loc_to_id.items():
self.ALL_LOCATIONS_TO_ID[key] = item
for loc in all_loc_to_id:
area = StaticWitnessLogic.ENTITIES_BY_NAME[loc]["area"]["name"]
self.AREA_LOCATION_GROUPS.setdefault(area, []).append(loc)
class WitnessPlayerLocations:
"""
Class that defines locations for a single player
"""
def __init__(self, world: "WitnessWorld", player_logic: WitnessPlayerLogic):
def __init__(self, world: "WitnessWorld", player_logic: WitnessPlayerLogic) -> None:
"""Defines locations AFTER logic changes due to options"""
self.PANEL_TYPES_TO_SHUFFLE = {"General", "Laser"}
self.CHECK_LOCATIONS = StaticWitnessLocations.GENERAL_LOCATIONS.copy()
self.CHECK_LOCATIONS = static_witness_locations.GENERAL_LOCATIONS.copy()
if world.options.shuffle_discarded_panels:
self.PANEL_TYPES_TO_SHUFFLE.add("Discard")
@ -520,28 +33,28 @@ class WitnessPlayerLocations:
elif world.options.shuffle_EPs == "obelisk_sides":
self.PANEL_TYPES_TO_SHUFFLE.add("Obelisk Side")
for obelisk_loc in StaticWitnessLocations.OBELISK_SIDES:
obelisk_loc_hex = StaticWitnessLogic.ENTITIES_BY_NAME[obelisk_loc]["entity_hex"]
for obelisk_loc in static_witness_locations.OBELISK_SIDES:
obelisk_loc_hex = static_witness_logic.ENTITIES_BY_NAME[obelisk_loc]["entity_hex"]
if player_logic.REQUIREMENTS_BY_HEX[obelisk_loc_hex] == frozenset({frozenset()}):
self.CHECK_LOCATIONS.discard(obelisk_loc)
self.CHECK_LOCATIONS = self.CHECK_LOCATIONS | player_logic.ADDED_CHECKS
self.CHECK_LOCATIONS.discard(StaticWitnessLogic.ENTITIES_BY_HEX[player_logic.VICTORY_LOCATION]["checkName"])
self.CHECK_LOCATIONS.discard(static_witness_logic.ENTITIES_BY_HEX[player_logic.VICTORY_LOCATION]["checkName"])
self.CHECK_LOCATIONS = self.CHECK_LOCATIONS - {
StaticWitnessLogic.ENTITIES_BY_HEX[entity_hex]["checkName"]
static_witness_logic.ENTITIES_BY_HEX[entity_hex]["checkName"]
for entity_hex in player_logic.COMPLETELY_DISABLED_ENTITIES | player_logic.PRECOMPLETED_LOCATIONS
}
self.CHECK_PANELHEX_TO_ID = {
StaticWitnessLogic.ENTITIES_BY_NAME[ch]["entity_hex"]: StaticWitnessLocations.ALL_LOCATIONS_TO_ID[ch]
static_witness_logic.ENTITIES_BY_NAME[ch]["entity_hex"]: static_witness_locations.ALL_LOCATIONS_TO_ID[ch]
for ch in self.CHECK_LOCATIONS
if StaticWitnessLogic.ENTITIES_BY_NAME[ch]["entityType"] in self.PANEL_TYPES_TO_SHUFFLE
if static_witness_logic.ENTITIES_BY_NAME[ch]["entityType"] in self.PANEL_TYPES_TO_SHUFFLE
}
dog_hex = StaticWitnessLogic.ENTITIES_BY_NAME["Town Pet the Dog"]["entity_hex"]
dog_id = StaticWitnessLocations.ALL_LOCATIONS_TO_ID["Town Pet the Dog"]
dog_hex = static_witness_logic.ENTITIES_BY_NAME["Town Pet the Dog"]["entity_hex"]
dog_id = static_witness_locations.ALL_LOCATIONS_TO_ID["Town Pet the Dog"]
self.CHECK_PANELHEX_TO_ID[dog_hex] = dog_id
self.CHECK_PANELHEX_TO_ID = dict(
@ -553,22 +66,19 @@ class WitnessPlayerLocations:
}
self.EVENT_LOCATION_TABLE = {
StaticWitnessLocations.get_event_name(panel_hex): None
for panel_hex in event_locations
static_witness_locations.get_event_name(entity_hex): None
for entity_hex in event_locations
}
check_dict = {
StaticWitnessLogic.ENTITIES_BY_HEX[location]["checkName"]:
StaticWitnessLocations.get_id(StaticWitnessLogic.ENTITIES_BY_HEX[location]["entity_hex"])
static_witness_logic.ENTITIES_BY_HEX[location]["checkName"]:
static_witness_locations.get_id(static_witness_logic.ENTITIES_BY_HEX[location]["entity_hex"])
for location in self.CHECK_PANELHEX_TO_ID
}
self.CHECK_LOCATION_TABLE = {**self.EVENT_LOCATION_TABLE, **check_dict}
def add_location_late(self, entity_name: str):
entity_hex = StaticWitnessLogic.ENTITIES_BY_NAME[entity_name]["entity_hex"]
def add_location_late(self, entity_name: str) -> None:
entity_hex = static_witness_logic.ENTITIES_BY_NAME[entity_name]["entity_hex"]
self.CHECK_LOCATION_TABLE[entity_hex] = entity_name
self.CHECK_PANELHEX_TO_ID[entity_hex] = StaticWitnessLocations.get_id(entity_hex)
StaticWitnessLocations()
self.CHECK_PANELHEX_TO_ID[entity_hex] = static_witness_locations.get_id(entity_hex)

View File

@ -1,10 +1,11 @@
from dataclasses import dataclass
from schema import Schema, And, Optional
from schema import And, Schema
from Options import Toggle, DefaultOnToggle, Range, Choice, PerGameCommonOptions, OptionDict
from Options import Choice, DefaultOnToggle, OptionDict, PerGameCommonOptions, Range, Toggle
from .static_logic import WeightedItemDefinition, ItemCategory, StaticWitnessLogic
from .data import static_logic as static_witness_logic
from .data.item_definition_classes import ItemCategory, WeightedItemDefinition
class DisableNonRandomizedPuzzles(Toggle):
@ -232,12 +233,12 @@ class TrapWeights(OptionDict):
display_name = "Trap Weights"
schema = Schema({
trap_name: And(int, lambda n: n >= 0)
for trap_name, item_definition in StaticWitnessLogic.all_items.items()
for trap_name, item_definition in static_witness_logic.ALL_ITEMS.items()
if isinstance(item_definition, WeightedItemDefinition) and item_definition.category is ItemCategory.TRAP
})
default = {
trap_name: item_definition.weight
for trap_name, item_definition in StaticWitnessLogic.all_items.items()
for trap_name, item_definition in static_witness_logic.ALL_ITEMS.items()
if isinstance(item_definition, WeightedItemDefinition) and item_definition.category is ItemCategory.TRAP
}
@ -315,7 +316,7 @@ class TheWitnessOptions(PerGameCommonOptions):
shuffle_discarded_panels: ShuffleDiscardedPanels
shuffle_vault_boxes: ShuffleVaultBoxes
obelisk_keys: ObeliskKeys
shuffle_EPs: ShuffleEnvironmentalPuzzles
shuffle_EPs: ShuffleEnvironmentalPuzzles # noqa: N815
EP_difficulty: EnvironmentalPuzzlesDifficulty
shuffle_postgame: ShufflePostgame
victory_condition: VictoryCondition

View File

@ -2,16 +2,23 @@
Defines progression, junk and event items for The Witness
"""
import copy
from typing import TYPE_CHECKING, Dict, List, Set
from dataclasses import dataclass
from typing import Optional, Dict, List, Set, TYPE_CHECKING
from BaseClasses import Item, ItemClassification, MultiWorld
from BaseClasses import Item, MultiWorld, ItemClassification
from .locations import ID_START, WitnessPlayerLocations
from .data import static_items as static_witness_items
from .data import static_logic as static_witness_logic
from .data.item_definition_classes import (
DoorItemDefinition,
ItemCategory,
ItemData,
ItemDefinition,
ProgressiveItemDefinition,
WeightedItemDefinition,
)
from .data.utils import build_weighted_int_list
from .locations import WitnessPlayerLocations
from .player_logic import WitnessPlayerLogic
from .static_logic import ItemDefinition, DoorItemDefinition, ProgressiveItemDefinition, ItemCategory, \
StaticWitnessLogic, WeightedItemDefinition
from .utils import build_weighted_int_list
if TYPE_CHECKING:
from . import WitnessWorld
@ -19,17 +26,6 @@ if TYPE_CHECKING:
NUM_ENERGY_UPGRADES = 4
@dataclass()
class ItemData:
"""
ItemData for an item in The Witness
"""
ap_code: Optional[int]
definition: ItemDefinition
classification: ItemClassification
local_only: bool = False
class WitnessItem(Item):
"""
Item from the game The Witness
@ -37,79 +33,30 @@ class WitnessItem(Item):
game: str = "The Witness"
class StaticWitnessItems:
"""
Class that handles Witness items independent of world settings
"""
item_data: Dict[str, ItemData] = {}
item_groups: Dict[str, List[str]] = {}
# Useful items that are treated specially at generation time and should not be automatically added to the player's
# item list during get_progression_items.
special_usefuls: List[str] = ["Puzzle Skip"]
def __init__(self):
for item_name, definition in StaticWitnessLogic.all_items.items():
ap_item_code = definition.local_code + ID_START
classification: ItemClassification = ItemClassification.filler
local_only: bool = False
if definition.category is ItemCategory.SYMBOL:
classification = ItemClassification.progression
StaticWitnessItems.item_groups.setdefault("Symbols", []).append(item_name)
elif definition.category is ItemCategory.DOOR:
classification = ItemClassification.progression
StaticWitnessItems.item_groups.setdefault("Doors", []).append(item_name)
elif definition.category is ItemCategory.LASER:
classification = ItemClassification.progression_skip_balancing
StaticWitnessItems.item_groups.setdefault("Lasers", []).append(item_name)
elif definition.category is ItemCategory.USEFUL:
classification = ItemClassification.useful
elif definition.category is ItemCategory.FILLER:
if item_name in ["Energy Fill (Small)"]:
local_only = True
classification = ItemClassification.filler
elif definition.category is ItemCategory.TRAP:
classification = ItemClassification.trap
elif definition.category is ItemCategory.JOKE:
classification = ItemClassification.filler
StaticWitnessItems.item_data[item_name] = ItemData(ap_item_code, definition,
classification, local_only)
@staticmethod
def get_item_to_door_mappings() -> Dict[int, List[int]]:
output: Dict[int, List[int]] = {}
for item_name, item_data in {name: data for name, data in StaticWitnessItems.item_data.items()
if isinstance(data.definition, DoorItemDefinition)}.items():
item = StaticWitnessItems.item_data[item_name]
output[item.ap_code] = [int(hex_string, 16) for hex_string in item_data.definition.panel_id_hexes]
return output
class WitnessPlayerItems:
"""
Class that defines Items for a single world
"""
def __init__(self, world: "WitnessWorld", logic: WitnessPlayerLogic, locat: WitnessPlayerLocations):
def __init__(self, world: "WitnessWorld", player_logic: WitnessPlayerLogic,
player_locations: WitnessPlayerLocations) -> None:
"""Adds event items after logic changes due to options"""
self._world: "WitnessWorld" = world
self._multiworld: MultiWorld = world.multiworld
self._player_id: int = world.player
self._logic: WitnessPlayerLogic = logic
self._locations: WitnessPlayerLocations = locat
self._logic: WitnessPlayerLogic = player_logic
self._locations: WitnessPlayerLocations = player_locations
# Duplicate the static item data, then make any player-specific adjustments to classification.
self.item_data: Dict[str, ItemData] = copy.deepcopy(StaticWitnessItems.item_data)
self.item_data: Dict[str, ItemData] = copy.deepcopy(static_witness_items.ITEM_DATA)
# Remove all progression items that aren't actually in the game.
self.item_data = {
name: data for (name, data) in self.item_data.items()
if data.classification not in
{ItemClassification.progression, ItemClassification.progression_skip_balancing}
or name in logic.PROG_ITEMS_ACTUALLY_IN_THE_GAME
{ItemClassification.progression, ItemClassification.progression_skip_balancing}
or name in player_logic.PROG_ITEMS_ACTUALLY_IN_THE_GAME
}
# Downgrade door items
@ -138,7 +85,7 @@ class WitnessPlayerItems:
# Add setting-specific useful items to the mandatory item list.
for item_name, item_data in {name: data for (name, data) in self.item_data.items()
if data.classification == ItemClassification.useful}.items():
if item_name in StaticWitnessItems.special_usefuls:
if item_name in static_witness_items._special_usefuls:
continue
elif item_name == "Energy Capacity":
self._mandatory_items[item_name] = NUM_ENERGY_UPGRADES
@ -149,7 +96,7 @@ class WitnessPlayerItems:
# Add event items to the item definition list for later lookup.
for event_location in self._locations.EVENT_LOCATION_TABLE:
location_name = logic.EVENT_ITEM_PAIRS[event_location]
location_name = player_logic.EVENT_ITEM_PAIRS[event_location]
self.item_data[location_name] = ItemData(None, ItemDefinition(0, ItemCategory.EVENT),
ItemClassification.progression, False)
@ -219,7 +166,7 @@ class WitnessPlayerItems:
output.add("Triangles")
# Replace progressive items with their parents.
output = {StaticWitnessLogic.get_parent_progressive_item(item) for item in output}
output = {static_witness_logic.get_parent_progressive_item(item) for item in output}
# Remove items that are mentioned in any plando options. (Hopefully, in the future, plando will get resolved
# before create_items so that we'll be able to check placed items instead of just removing all items mentioned
@ -227,16 +174,16 @@ class WitnessPlayerItems:
for plando_setting in self._multiworld.plando_items[self._player_id]:
if plando_setting.get("from_pool", True):
for item_setting_key in [key for key in ["item", "items"] if key in plando_setting]:
if type(plando_setting[item_setting_key]) is str:
if isinstance(plando_setting[item_setting_key], str):
output -= {plando_setting[item_setting_key]}
elif type(plando_setting[item_setting_key]) is dict:
elif isinstance(plando_setting[item_setting_key], dict):
output -= {item for item, weight in plando_setting[item_setting_key].items() if weight}
else:
# Assume this is some other kind of iterable.
for inner_item in plando_setting[item_setting_key]:
if type(inner_item) is str:
if isinstance(inner_item, str):
output -= {inner_item}
elif type(inner_item) is dict:
elif isinstance(inner_item, dict):
output -= {item for item, weight in inner_item.items() if weight}
# Sort the output for consistency across versions if the implementation changes but the logic does not.
@ -257,7 +204,7 @@ class WitnessPlayerItems:
"""
Returns the item IDs of symbol items that were defined in the configuration file but are not in the pool.
"""
return [data.ap_code for name, data in StaticWitnessItems.item_data.items()
return [data.ap_code for name, data in static_witness_items.ITEM_DATA.items()
if name not in self.item_data.keys() and data.definition.category is ItemCategory.SYMBOL]
def get_progressive_item_ids_in_pool(self) -> Dict[int, List[int]]:
@ -267,9 +214,8 @@ class WitnessPlayerItems:
if isinstance(item.definition, ProgressiveItemDefinition):
# Note: we need to reference the static table here rather than the player-specific one because the child
# items were removed from the pool when we pruned out all progression items not in the settings.
output[item.ap_code] = [StaticWitnessItems.item_data[child_item].ap_code
output[item.ap_code] = [static_witness_items.ITEM_DATA[child_item].ap_code
for child_item in item.definition.child_item_names]
return output
StaticWitnessItems()

View File

@ -17,11 +17,13 @@ When the world has parsed its options, a second function is called to finalize t
import copy
from collections import defaultdict
from typing import cast, TYPE_CHECKING
from functools import lru_cache
from logging import warning
from typing import TYPE_CHECKING, Dict, FrozenSet, List, Set, Tuple, cast
from .static_logic import StaticWitnessLogic, DoorItemDefinition, ItemCategory, ProgressiveItemDefinition
from .utils import *
from .data import static_logic as static_witness_logic
from .data import utils
from .data.item_definition_classes import DoorItemDefinition, ItemCategory, ProgressiveItemDefinition
if TYPE_CHECKING:
from . import WitnessWorld
@ -31,7 +33,7 @@ class WitnessPlayerLogic:
"""WITNESS LOGIC CLASS"""
@lru_cache(maxsize=None)
def reduce_req_within_region(self, panel_hex: str) -> FrozenSet[FrozenSet[str]]:
def reduce_req_within_region(self, entity_hex: str) -> FrozenSet[FrozenSet[str]]:
"""
Panels in this game often only turn on when other panels are solved.
Those other panels may have different item requirements.
@ -40,15 +42,15 @@ class WitnessPlayerLogic:
Panels outside of the same region will still be checked manually.
"""
if panel_hex in self.COMPLETELY_DISABLED_ENTITIES or panel_hex in self.IRRELEVANT_BUT_NOT_DISABLED_ENTITIES:
if entity_hex in self.COMPLETELY_DISABLED_ENTITIES or entity_hex in self.IRRELEVANT_BUT_NOT_DISABLED_ENTITIES:
return frozenset()
entity_obj = self.REFERENCE_LOGIC.ENTITIES_BY_HEX[panel_hex]
entity_obj = self.REFERENCE_LOGIC.ENTITIES_BY_HEX[entity_hex]
these_items = frozenset({frozenset()})
if entity_obj["id"]:
these_items = self.DEPENDENT_REQUIREMENTS_BY_HEX[panel_hex]["items"]
these_items = self.DEPENDENT_REQUIREMENTS_BY_HEX[entity_hex]["items"]
these_items = frozenset({
subset.intersection(self.THEORETICAL_ITEMS_NO_MULTI)
@ -58,28 +60,28 @@ class WitnessPlayerLogic:
for subset in these_items:
self.PROG_ITEMS_ACTUALLY_IN_THE_GAME_NO_MULTI.update(subset)
these_panels = self.DEPENDENT_REQUIREMENTS_BY_HEX[panel_hex]["panels"]
these_panels = self.DEPENDENT_REQUIREMENTS_BY_HEX[entity_hex]["panels"]
if panel_hex in self.DOOR_ITEMS_BY_ID:
door_items = frozenset({frozenset([item]) for item in self.DOOR_ITEMS_BY_ID[panel_hex]})
if entity_hex in self.DOOR_ITEMS_BY_ID:
door_items = frozenset({frozenset([item]) for item in self.DOOR_ITEMS_BY_ID[entity_hex]})
all_options: Set[FrozenSet[str]] = set()
for dependentItem in door_items:
self.PROG_ITEMS_ACTUALLY_IN_THE_GAME_NO_MULTI.update(dependentItem)
for dependent_item in door_items:
self.PROG_ITEMS_ACTUALLY_IN_THE_GAME_NO_MULTI.update(dependent_item)
for items_option in these_items:
all_options.add(items_option.union(dependentItem))
all_options.add(items_option.union(dependent_item))
# If this entity is not an EP, and it has an associated door item, ignore the original power dependencies
if StaticWitnessLogic.ENTITIES_BY_HEX[panel_hex]["entityType"] != "EP":
if static_witness_logic.ENTITIES_BY_HEX[entity_hex]["entityType"] != "EP":
# 0x28A0D depends on another entity for *non-power* reasons -> This dependency needs to be preserved,
# except in Expert, where that dependency doesn't exist, but now there *is* a power dependency.
# In the future, it'd be wise to make a distinction between "power dependencies" and other dependencies.
if panel_hex == "0x28A0D" and not any("0x28998" in option for option in these_panels):
if entity_hex == "0x28A0D" and not any("0x28998" in option for option in these_panels):
these_items = all_options
# Another dependency that is not power-based: The Symmetry Island Upper Panel latches
elif panel_hex == "0x1C349":
elif entity_hex == "0x1C349":
these_items = all_options
else:
@ -107,9 +109,9 @@ class WitnessPlayerLogic:
if option_entity in self.ALWAYS_EVENT_NAMES_BY_HEX:
new_items = frozenset({frozenset([option_entity])})
elif (panel_hex, option_entity) in self.CONDITIONAL_EVENTS:
elif (entity_hex, option_entity) in self.CONDITIONAL_EVENTS:
new_items = frozenset({frozenset([option_entity])})
self.USED_EVENT_NAMES_BY_HEX[option_entity] = self.CONDITIONAL_EVENTS[(panel_hex, option_entity)]
self.USED_EVENT_NAMES_BY_HEX[option_entity] = self.CONDITIONAL_EVENTS[(entity_hex, option_entity)]
elif option_entity in {"7 Lasers", "11 Lasers", "7 Lasers + Redirect", "11 Lasers + Redirect",
"PP2 Weirdness", "Theater to Tunnels"}:
new_items = frozenset({frozenset([option_entity])})
@ -121,36 +123,36 @@ class WitnessPlayerLogic:
for possibility in new_items
)
dependent_items_for_option = dnf_and([dependent_items_for_option, new_items])
dependent_items_for_option = utils.dnf_and([dependent_items_for_option, new_items])
for items_option in these_items:
for dependentItem in dependent_items_for_option:
all_options.add(items_option.union(dependentItem))
for dependent_item in dependent_items_for_option:
all_options.add(items_option.union(dependent_item))
return dnf_remove_redundancies(frozenset(all_options))
return utils.dnf_remove_redundancies(frozenset(all_options))
def make_single_adjustment(self, adj_type: str, line: str):
from . import StaticWitnessItems
def make_single_adjustment(self, adj_type: str, line: str) -> None:
from .data import static_items as static_witness_items
"""Makes a single logic adjustment based on additional logic file"""
if adj_type == "Items":
line_split = line.split(" - ")
item_name = line_split[0]
if item_name not in StaticWitnessItems.item_data:
raise RuntimeError("Item \"" + item_name + "\" does not exist.")
if item_name not in static_witness_items.ITEM_DATA:
raise RuntimeError(f'Item "{item_name}" does not exist.')
self.THEORETICAL_ITEMS.add(item_name)
if isinstance(StaticWitnessLogic.all_items[item_name], ProgressiveItemDefinition):
if isinstance(static_witness_logic.ALL_ITEMS[item_name], ProgressiveItemDefinition):
self.THEORETICAL_ITEMS_NO_MULTI.update(cast(ProgressiveItemDefinition,
StaticWitnessLogic.all_items[item_name]).child_item_names)
static_witness_logic.ALL_ITEMS[item_name]).child_item_names)
else:
self.THEORETICAL_ITEMS_NO_MULTI.add(item_name)
if StaticWitnessLogic.all_items[item_name].category in [ItemCategory.DOOR, ItemCategory.LASER]:
panel_hexes = cast(DoorItemDefinition, StaticWitnessLogic.all_items[item_name]).panel_id_hexes
for panel_hex in panel_hexes:
self.DOOR_ITEMS_BY_ID.setdefault(panel_hex, []).append(item_name)
if static_witness_logic.ALL_ITEMS[item_name].category in [ItemCategory.DOOR, ItemCategory.LASER]:
entity_hexes = cast(DoorItemDefinition, static_witness_logic.ALL_ITEMS[item_name]).panel_id_hexes
for entity_hex in entity_hexes:
self.DOOR_ITEMS_BY_ID.setdefault(entity_hex, []).append(item_name)
return
@ -158,18 +160,18 @@ class WitnessPlayerLogic:
item_name = line
self.THEORETICAL_ITEMS.discard(item_name)
if isinstance(StaticWitnessLogic.all_items[item_name], ProgressiveItemDefinition):
if isinstance(static_witness_logic.ALL_ITEMS[item_name], ProgressiveItemDefinition):
self.THEORETICAL_ITEMS_NO_MULTI.difference_update(
cast(ProgressiveItemDefinition, StaticWitnessLogic.all_items[item_name]).child_item_names
cast(ProgressiveItemDefinition, static_witness_logic.ALL_ITEMS[item_name]).child_item_names
)
else:
self.THEORETICAL_ITEMS_NO_MULTI.discard(item_name)
if StaticWitnessLogic.all_items[item_name].category in [ItemCategory.DOOR, ItemCategory.LASER]:
panel_hexes = cast(DoorItemDefinition, StaticWitnessLogic.all_items[item_name]).panel_id_hexes
for panel_hex in panel_hexes:
if panel_hex in self.DOOR_ITEMS_BY_ID and item_name in self.DOOR_ITEMS_BY_ID[panel_hex]:
self.DOOR_ITEMS_BY_ID[panel_hex].remove(item_name)
if static_witness_logic.ALL_ITEMS[item_name].category in [ItemCategory.DOOR, ItemCategory.LASER]:
entity_hexes = cast(DoorItemDefinition, static_witness_logic.ALL_ITEMS[item_name]).panel_id_hexes
for entity_hex in entity_hexes:
if entity_hex in self.DOOR_ITEMS_BY_ID and item_name in self.DOOR_ITEMS_BY_ID[entity_hex]:
self.DOOR_ITEMS_BY_ID[entity_hex].remove(item_name)
if adj_type == "Starting Inventory":
self.STARTING_INVENTORY.add(line)
@ -189,13 +191,13 @@ class WitnessPlayerLogic:
line_split = line.split(" - ")
requirement = {
"panels": parse_lambda(line_split[1]),
"panels": utils.parse_lambda(line_split[1]),
}
if len(line_split) > 2:
required_items = parse_lambda(line_split[2])
required_items = utils.parse_lambda(line_split[2])
items_actually_in_the_game = [
item_name for item_name, item_definition in StaticWitnessLogic.all_items.items()
item_name for item_name, item_definition in static_witness_logic.ALL_ITEMS.items()
if item_definition.category is ItemCategory.SYMBOL
]
required_items = frozenset(
@ -210,21 +212,21 @@ class WitnessPlayerLogic:
return
if adj_type == "Disabled Locations":
panel_hex = line[:7]
entity_hex = line[:7]
self.COMPLETELY_DISABLED_ENTITIES.add(panel_hex)
self.COMPLETELY_DISABLED_ENTITIES.add(entity_hex)
return
if adj_type == "Irrelevant Locations":
panel_hex = line[:7]
entity_hex = line[:7]
self.IRRELEVANT_BUT_NOT_DISABLED_ENTITIES.add(panel_hex)
self.IRRELEVANT_BUT_NOT_DISABLED_ENTITIES.add(entity_hex)
return
if adj_type == "Region Changes":
new_region_and_options = define_new_region(line + ":")
new_region_and_options = utils.define_new_region(line + ":")
self.CONNECTIONS_BY_REGION_NAME[new_region_and_options[0]["name"]] = new_region_and_options[1]
@ -245,11 +247,11 @@ class WitnessPlayerLogic:
(target_region, frozenset({frozenset(["TrueOneWay"])}))
)
else:
new_lambda = connection[1] | parse_lambda(panel_set_string)
new_lambda = connection[1] | utils.parse_lambda(panel_set_string)
self.CONNECTIONS_BY_REGION_NAME[source_region].add((target_region, new_lambda))
break
else: # Execute if loop did not break. TIL this is a thing you can do!
new_conn = (target_region, parse_lambda(panel_set_string))
new_conn = (target_region, utils.parse_lambda(panel_set_string))
self.CONNECTIONS_BY_REGION_NAME[source_region].add(new_conn)
if adj_type == "Added Locations":
@ -258,7 +260,7 @@ class WitnessPlayerLogic:
self.ADDED_CHECKS.add(line)
@staticmethod
def handle_postgame(world: "WitnessWorld"):
def handle_postgame(world: "WitnessWorld") -> List[List[str]]:
# In shuffle_postgame, panels that become accessible "after or at the same time as the goal" are disabled.
# This has a lot of complicated considerations, which I'll try my best to explain.
postgame_adjustments = []
@ -285,29 +287,29 @@ class WitnessPlayerLogic:
# Caves & Challenge should never have anything if doors are vanilla - definitionally "post-game"
# This is technically imprecise, but it matches player expectations better.
if not (early_caves or doors):
postgame_adjustments.append(get_caves_exclusion_list())
postgame_adjustments.append(get_beyond_challenge_exclusion_list())
postgame_adjustments.append(utils.get_caves_exclusion_list())
postgame_adjustments.append(utils.get_beyond_challenge_exclusion_list())
# If Challenge is the goal, some panels on the way need to be left on, as well as Challenge Vault box itself
if not victory == "challenge":
postgame_adjustments.append(get_path_to_challenge_exclusion_list())
postgame_adjustments.append(get_challenge_vault_box_exclusion_list())
postgame_adjustments.append(utils.get_path_to_challenge_exclusion_list())
postgame_adjustments.append(utils.get_challenge_vault_box_exclusion_list())
# Challenge can only have something if the goal is not challenge or longbox itself.
# In case of shortbox, it'd have to be a "reverse shortbox" situation where shortbox requires *more* lasers.
# In that case, it'd also have to be a doors mode, but that's already covered by the previous block.
if not (victory == "elevator" or reverse_shortbox_goal):
postgame_adjustments.append(get_beyond_challenge_exclusion_list())
postgame_adjustments.append(utils.get_beyond_challenge_exclusion_list())
if not victory == "challenge":
postgame_adjustments.append(get_challenge_vault_box_exclusion_list())
postgame_adjustments.append(utils.get_challenge_vault_box_exclusion_list())
# Mountain can't be reached if the goal is shortbox (or "reverse long box")
if not mountain_enterable_from_top:
postgame_adjustments.append(get_mountain_upper_exclusion_list())
postgame_adjustments.append(utils.get_mountain_upper_exclusion_list())
# Same goes for lower mountain, but that one *can* be reached in remote doors modes.
if not doors:
postgame_adjustments.append(get_mountain_lower_exclusion_list())
postgame_adjustments.append(utils.get_mountain_lower_exclusion_list())
# The Mountain Bottom Floor Discard is a bit complicated, so we handle it separately. ("it" == the Discard)
# In Elevator Goal, it is definitionally in the post-game, unless remote doors is played.
@ -319,15 +321,15 @@ class WitnessPlayerLogic:
# This has different consequences depending on whether remote doors is being played.
# If doors are vanilla, Bottom Floor Discard locks a door to an area, which has to be disabled as well.
if doors:
postgame_adjustments.append(get_bottom_floor_discard_exclusion_list())
postgame_adjustments.append(utils.get_bottom_floor_discard_exclusion_list())
else:
postgame_adjustments.append(get_bottom_floor_discard_nondoors_exclusion_list())
postgame_adjustments.append(utils.get_bottom_floor_discard_nondoors_exclusion_list())
# In Challenge goal + early_caves + vanilla doors, you could find something important on Bottom Floor Discard,
# including the Caves Shortcuts themselves if playing "early_caves: start_inventory".
# This is another thing that was deemed "unfun" more than fitting the actual definition of post-game.
if victory == "challenge" and early_caves and not doors:
postgame_adjustments.append(get_bottom_floor_discard_nondoors_exclusion_list())
postgame_adjustments.append(utils.get_bottom_floor_discard_nondoors_exclusion_list())
# If we have a proper short box goal, long box will never be activated first.
if proper_shortbox_goal:
@ -335,7 +337,7 @@ class WitnessPlayerLogic:
return postgame_adjustments
def make_options_adjustments(self, world: "WitnessWorld"):
def make_options_adjustments(self, world: "WitnessWorld") -> None:
"""Makes logic adjustments based on options"""
adjustment_linesets_in_order = []
@ -356,15 +358,15 @@ class WitnessPlayerLogic:
# In disable_non_randomized, the discards are needed for alternate activation triggers, UNLESS both
# (remote) doors and lasers are shuffled.
if not world.options.disable_non_randomized_puzzles or (doors and lasers):
adjustment_linesets_in_order.append(get_discard_exclusion_list())
adjustment_linesets_in_order.append(utils.get_discard_exclusion_list())
if doors:
adjustment_linesets_in_order.append(get_bottom_floor_discard_exclusion_list())
adjustment_linesets_in_order.append(utils.get_bottom_floor_discard_exclusion_list())
if not world.options.shuffle_vault_boxes:
adjustment_linesets_in_order.append(get_vault_exclusion_list())
adjustment_linesets_in_order.append(utils.get_vault_exclusion_list())
if not victory == "challenge":
adjustment_linesets_in_order.append(get_challenge_vault_box_exclusion_list())
adjustment_linesets_in_order.append(utils.get_challenge_vault_box_exclusion_list())
# Victory Condition
@ -387,54 +389,54 @@ class WitnessPlayerLogic:
])
if world.options.disable_non_randomized_puzzles:
adjustment_linesets_in_order.append(get_disable_unrandomized_list())
adjustment_linesets_in_order.append(utils.get_disable_unrandomized_list())
if world.options.shuffle_symbols:
adjustment_linesets_in_order.append(get_symbol_shuffle_list())
adjustment_linesets_in_order.append(utils.get_symbol_shuffle_list())
if world.options.EP_difficulty == "normal":
adjustment_linesets_in_order.append(get_ep_easy())
adjustment_linesets_in_order.append(utils.get_ep_easy())
elif world.options.EP_difficulty == "tedious":
adjustment_linesets_in_order.append(get_ep_no_eclipse())
adjustment_linesets_in_order.append(utils.get_ep_no_eclipse())
if world.options.door_groupings == "regional":
if world.options.shuffle_doors == "panels":
adjustment_linesets_in_order.append(get_simple_panels())
adjustment_linesets_in_order.append(utils.get_simple_panels())
elif world.options.shuffle_doors == "doors":
adjustment_linesets_in_order.append(get_simple_doors())
adjustment_linesets_in_order.append(utils.get_simple_doors())
elif world.options.shuffle_doors == "mixed":
adjustment_linesets_in_order.append(get_simple_doors())
adjustment_linesets_in_order.append(get_simple_additional_panels())
adjustment_linesets_in_order.append(utils.get_simple_doors())
adjustment_linesets_in_order.append(utils.get_simple_additional_panels())
else:
if world.options.shuffle_doors == "panels":
adjustment_linesets_in_order.append(get_complex_door_panels())
adjustment_linesets_in_order.append(get_complex_additional_panels())
adjustment_linesets_in_order.append(utils.get_complex_door_panels())
adjustment_linesets_in_order.append(utils.get_complex_additional_panels())
elif world.options.shuffle_doors == "doors":
adjustment_linesets_in_order.append(get_complex_doors())
adjustment_linesets_in_order.append(utils.get_complex_doors())
elif world.options.shuffle_doors == "mixed":
adjustment_linesets_in_order.append(get_complex_doors())
adjustment_linesets_in_order.append(get_complex_additional_panels())
adjustment_linesets_in_order.append(utils.get_complex_doors())
adjustment_linesets_in_order.append(utils.get_complex_additional_panels())
if world.options.shuffle_boat:
adjustment_linesets_in_order.append(get_boat())
adjustment_linesets_in_order.append(utils.get_boat())
if world.options.early_caves == "starting_inventory":
adjustment_linesets_in_order.append(get_early_caves_start_list())
adjustment_linesets_in_order.append(utils.get_early_caves_start_list())
if world.options.early_caves == "add_to_pool" and not doors:
adjustment_linesets_in_order.append(get_early_caves_list())
adjustment_linesets_in_order.append(utils.get_early_caves_list())
if world.options.elevators_come_to_you:
adjustment_linesets_in_order.append(get_elevators_come_to_you())
adjustment_linesets_in_order.append(utils.get_elevators_come_to_you())
for item in self.YAML_ADDED_ITEMS:
adjustment_linesets_in_order.append(["Items:", item])
if lasers:
adjustment_linesets_in_order.append(get_laser_shuffle())
adjustment_linesets_in_order.append(utils.get_laser_shuffle())
if world.options.shuffle_EPs and world.options.obelisk_keys:
adjustment_linesets_in_order.append(get_obelisk_keys())
adjustment_linesets_in_order.append(utils.get_obelisk_keys())
if world.options.shuffle_EPs == "obelisk_sides":
ep_gen = ((ep_hex, ep_obj) for (ep_hex, ep_obj) in self.REFERENCE_LOGIC.ENTITIES_BY_HEX.items()
@ -446,10 +448,10 @@ class WitnessPlayerLogic:
ep_name = self.REFERENCE_LOGIC.ENTITIES_BY_HEX[ep_hex]["checkName"]
self.ALWAYS_EVENT_NAMES_BY_HEX[ep_hex] = f"{obelisk_name} - {ep_name}"
else:
adjustment_linesets_in_order.append(["Disabled Locations:"] + get_ep_obelisks()[1:])
adjustment_linesets_in_order.append(["Disabled Locations:"] + utils.get_ep_obelisks()[1:])
if not world.options.shuffle_EPs:
adjustment_linesets_in_order.append(["Disabled Locations:"] + get_ep_all_individual()[1:])
adjustment_linesets_in_order.append(["Disabled Locations:"] + utils.get_ep_all_individual()[1:])
for yaml_disabled_location in self.YAML_DISABLED_LOCATIONS:
if yaml_disabled_location not in self.REFERENCE_LOGIC.ENTITIES_BY_NAME:
@ -480,7 +482,7 @@ class WitnessPlayerLogic:
if entity_id in self.DOOR_ITEMS_BY_ID:
del self.DOOR_ITEMS_BY_ID[entity_id]
def make_dependency_reduced_checklist(self):
def make_dependency_reduced_checklist(self) -> None:
"""
Turns dependent check set into semi-independent check set
"""
@ -492,10 +494,10 @@ class WitnessPlayerLogic:
for item in self.PROG_ITEMS_ACTUALLY_IN_THE_GAME_NO_MULTI:
if item not in self.THEORETICAL_ITEMS:
progressive_item_name = StaticWitnessLogic.get_parent_progressive_item(item)
progressive_item_name = static_witness_logic.get_parent_progressive_item(item)
self.PROG_ITEMS_ACTUALLY_IN_THE_GAME.add(progressive_item_name)
child_items = cast(ProgressiveItemDefinition,
StaticWitnessLogic.all_items[progressive_item_name]).child_item_names
static_witness_logic.ALL_ITEMS[progressive_item_name]).child_item_names
multi_list = [child_item for child_item in child_items
if child_item in self.PROG_ITEMS_ACTUALLY_IN_THE_GAME_NO_MULTI]
self.MULTI_AMOUNTS[item] = multi_list.index(item) + 1
@ -520,24 +522,24 @@ class WitnessPlayerLogic:
if self.REFERENCE_LOGIC.ENTITIES_BY_HEX[entity]["region"]:
region_name = self.REFERENCE_LOGIC.ENTITIES_BY_HEX[entity]["region"]["name"]
entity_req = dnf_and([entity_req, frozenset({frozenset({region_name})})])
entity_req = utils.dnf_and([entity_req, frozenset({frozenset({region_name})})])
individual_entity_requirements.append(entity_req)
overall_requirement |= dnf_and(individual_entity_requirements)
overall_requirement |= utils.dnf_and(individual_entity_requirements)
new_connections.append((connection[0], overall_requirement))
self.CONNECTIONS_BY_REGION_NAME[region] = new_connections
def solvability_guaranteed(self, entity_hex: str):
def solvability_guaranteed(self, entity_hex: str) -> bool:
return not (
entity_hex in self.ENTITIES_WITHOUT_ENSURED_SOLVABILITY
or entity_hex in self.COMPLETELY_DISABLED_ENTITIES
or entity_hex in self.IRRELEVANT_BUT_NOT_DISABLED_ENTITIES
)
def determine_unrequired_entities(self, world: "WitnessWorld"):
def determine_unrequired_entities(self, world: "WitnessWorld") -> None:
"""Figure out which major items are actually useless in this world's settings"""
# Gather quick references to relevant options
@ -596,7 +598,7 @@ class WitnessPlayerLogic:
item_name for item_name, is_required in is_item_required_dict.items() if not is_required
}
def make_event_item_pair(self, panel: str):
def make_event_item_pair(self, panel: str) -> Tuple[str, str]:
"""
Makes a pair of an event panel and its event item
"""
@ -604,12 +606,12 @@ class WitnessPlayerLogic:
name = self.REFERENCE_LOGIC.ENTITIES_BY_HEX[panel]["checkName"] + action
if panel not in self.USED_EVENT_NAMES_BY_HEX:
warning("Panel \"" + name + "\" does not have an associated event name.")
warning(f'Panel "{name}" does not have an associated event name.')
self.USED_EVENT_NAMES_BY_HEX[panel] = name + " Event"
pair = (name, self.USED_EVENT_NAMES_BY_HEX[panel])
return pair
def make_event_panel_lists(self):
def make_event_panel_lists(self) -> None:
self.ALWAYS_EVENT_NAMES_BY_HEX[self.VICTORY_LOCATION] = "Victory"
self.USED_EVENT_NAMES_BY_HEX.update(self.ALWAYS_EVENT_NAMES_BY_HEX)
@ -623,7 +625,7 @@ class WitnessPlayerLogic:
pair = self.make_event_item_pair(panel)
self.EVENT_ITEM_PAIRS[pair[0]] = pair[1]
def __init__(self, world: "WitnessWorld", disabled_locations: Set[str], start_inv: Dict[str, int]):
def __init__(self, world: "WitnessWorld", disabled_locations: Set[str], start_inv: Dict[str, int]) -> None:
self.YAML_DISABLED_LOCATIONS = disabled_locations
self.YAML_ADDED_ITEMS = start_inv
@ -646,11 +648,11 @@ class WitnessPlayerLogic:
self.DIFFICULTY = world.options.puzzle_randomization
if self.DIFFICULTY == "sigma_normal":
self.REFERENCE_LOGIC = StaticWitnessLogic.sigma_normal
self.REFERENCE_LOGIC = static_witness_logic.sigma_normal
elif self.DIFFICULTY == "sigma_expert":
self.REFERENCE_LOGIC = StaticWitnessLogic.sigma_expert
self.REFERENCE_LOGIC = static_witness_logic.sigma_expert
elif self.DIFFICULTY == "none":
self.REFERENCE_LOGIC = StaticWitnessLogic.vanilla
self.REFERENCE_LOGIC = static_witness_logic.vanilla
self.CONNECTIONS_BY_REGION_NAME = copy.deepcopy(self.REFERENCE_LOGIC.STATIC_CONNECTIONS_BY_REGION_NAME)
self.DEPENDENT_REQUIREMENTS_BY_HEX = copy.deepcopy(self.REFERENCE_LOGIC.STATIC_DEPENDENT_REQUIREMENTS_BY_HEX)

View File

@ -2,26 +2,29 @@
Defines Region for The Witness, assigns locations to them,
and connects them with the proper requirements
"""
from typing import FrozenSet, TYPE_CHECKING, Dict, Tuple, List
from collections import defaultdict
from typing import TYPE_CHECKING, Dict, FrozenSet, List, Tuple
from BaseClasses import Entrance, Region
from Utils import KeyedDefaultDict
from .static_logic import StaticWitnessLogic
from .locations import WitnessPlayerLocations, StaticWitnessLocations
from worlds.generic.Rules import CollectionRule
from .data import static_logic as static_witness_logic
from .locations import WitnessPlayerLocations, static_witness_locations
from .player_logic import WitnessPlayerLogic
if TYPE_CHECKING:
from . import WitnessWorld
class WitnessRegions:
class WitnessPlayerRegions:
"""Class that defines Witness Regions"""
locat = None
player_locations = None
logic = None
@staticmethod
def make_lambda(item_requirement: FrozenSet[FrozenSet[str]], world: "WitnessWorld"):
def make_lambda(item_requirement: FrozenSet[FrozenSet[str]], world: "WitnessWorld") -> CollectionRule:
from .rules import _meets_item_requirements
"""
@ -82,7 +85,7 @@ class WitnessRegions:
for dependent_region in mentioned_regions:
world.multiworld.register_indirect_condition(regions_by_name[dependent_region], connection)
def create_regions(self, world: "WitnessWorld", player_logic: WitnessPlayerLogic):
def create_regions(self, world: "WitnessWorld", player_logic: WitnessPlayerLogic) -> None:
"""
Creates all the regions for The Witness
"""
@ -94,16 +97,17 @@ class WitnessRegions:
for region_name, region in self.reference_logic.ALL_REGIONS_BY_NAME.items():
locations_for_this_region = [
self.reference_logic.ENTITIES_BY_HEX[panel]["checkName"] for panel in region["panels"]
if self.reference_logic.ENTITIES_BY_HEX[panel]["checkName"] in self.locat.CHECK_LOCATION_TABLE
if self.reference_logic.ENTITIES_BY_HEX[panel]["checkName"]
in self.player_locations.CHECK_LOCATION_TABLE
]
locations_for_this_region += [
StaticWitnessLocations.get_event_name(panel) for panel in region["panels"]
if StaticWitnessLocations.get_event_name(panel) in self.locat.EVENT_LOCATION_TABLE
static_witness_locations.get_event_name(panel) for panel in region["panels"]
if static_witness_locations.get_event_name(panel) in self.player_locations.EVENT_LOCATION_TABLE
]
all_locations = all_locations | set(locations_for_this_region)
new_region = create_region(world, region_name, self.locat, locations_for_this_region)
new_region = create_region(world, region_name, self.player_locations, locations_for_this_region)
regions_by_name[region_name] = new_region
@ -133,16 +137,16 @@ class WitnessRegions:
world.multiworld.regions += self.created_regions.values()
def __init__(self, locat: WitnessPlayerLocations, world: "WitnessWorld"):
def __init__(self, player_locations: WitnessPlayerLocations, world: "WitnessWorld") -> None:
difficulty = world.options.puzzle_randomization
if difficulty == "sigma_normal":
self.reference_logic = StaticWitnessLogic.sigma_normal
self.reference_logic = static_witness_logic.sigma_normal
elif difficulty == "sigma_expert":
self.reference_logic = StaticWitnessLogic.sigma_expert
self.reference_logic = static_witness_logic.sigma_expert
elif difficulty == "none":
self.reference_logic = StaticWitnessLogic.vanilla
self.reference_logic = static_witness_logic.vanilla
self.locat = locat
self.created_entrances: Dict[Tuple[str, str], List[Entrance]] = KeyedDefaultDict(lambda _: [])
self.player_locations = player_locations
self.created_entrances: Dict[Tuple[str, str], List[Entrance]] = defaultdict(lambda: [])
self.created_regions: Dict[str, Region] = dict()

11
worlds/witness/ruff.toml Normal file
View File

@ -0,0 +1,11 @@
line-length = 120
[lint]
select = ["E", "F", "W", "I", "N", "Q", "UP", "RUF", "ISC", "T20"]
ignore = ["RUF012", "RUF100"]
[per-file-ignores]
# The way options definitions work right now, I am forced to break line length requirements.
"options.py" = ["E501"]
# The import list would just be so big if I imported every option individually in presets.py
"presets.py" = ["F403", "F405"]

View File

@ -3,13 +3,16 @@ Defines the rules by which locations can be accessed,
depending on the items received
"""
from typing import TYPE_CHECKING, Callable, FrozenSet
from typing import TYPE_CHECKING, FrozenSet
from BaseClasses import CollectionState
from .player_logic import WitnessPlayerLogic
from worlds.generic.Rules import CollectionRule, set_rule
from . import WitnessPlayerRegions
from .data import static_logic as static_witness_logic
from .locations import WitnessPlayerLocations
from . import StaticWitnessLogic, WitnessRegions
from worlds.generic.Rules import set_rule
from .player_logic import WitnessPlayerLogic
if TYPE_CHECKING:
from . import WitnessWorld
@ -30,17 +33,17 @@ laser_hexes = [
def _has_laser(laser_hex: str, world: "WitnessWorld", player: int,
redirect_required: bool) -> Callable[[CollectionState], bool]:
redirect_required: bool) -> CollectionRule:
if laser_hex == "0x012FB" and redirect_required:
return lambda state: (
_can_solve_panel(laser_hex, world, world.player, world.player_logic, world.locat)(state)
_can_solve_panel(laser_hex, world, world.player, world.player_logic, world.player_locations)(state)
and state.has("Desert Laser Redirection", player)
)
else:
return _can_solve_panel(laser_hex, world, world.player, world.player_logic, world.locat)
return _can_solve_panel(laser_hex, world, world.player, world.player_logic, world.player_locations)
def _has_lasers(amount: int, world: "WitnessWorld", redirect_required: bool) -> Callable[[CollectionState], bool]:
def _has_lasers(amount: int, world: "WitnessWorld", redirect_required: bool) -> CollectionRule:
laser_lambdas = []
for laser_hex in laser_hexes:
@ -52,7 +55,7 @@ def _has_lasers(amount: int, world: "WitnessWorld", redirect_required: bool) ->
def _can_solve_panel(panel: str, world: "WitnessWorld", player: int, player_logic: WitnessPlayerLogic,
locat: WitnessPlayerLocations) -> Callable[[CollectionState], bool]:
player_locations: WitnessPlayerLocations) -> CollectionRule:
"""
Determines whether a panel can be solved
"""
@ -60,15 +63,16 @@ def _can_solve_panel(panel: str, world: "WitnessWorld", player: int, player_logi
panel_obj = player_logic.REFERENCE_LOGIC.ENTITIES_BY_HEX[panel]
entity_name = panel_obj["checkName"]
if entity_name + " Solved" in locat.EVENT_LOCATION_TABLE:
if entity_name + " Solved" in player_locations.EVENT_LOCATION_TABLE:
return lambda state: state.has(player_logic.EVENT_ITEM_PAIRS[entity_name + " Solved"], player)
else:
return make_lambda(panel, world)
def _can_move_either_direction(state: CollectionState, source: str, target: str, regio: WitnessRegions) -> bool:
entrance_forward = regio.created_entrances[source, target]
entrance_backward = regio.created_entrances[target, source]
def _can_move_either_direction(state: CollectionState, source: str, target: str,
player_regions: WitnessPlayerRegions) -> bool:
entrance_forward = player_regions.created_entrances[source, target]
entrance_backward = player_regions.created_entrances[target, source]
return (
any(entrance.can_reach(state) for entrance in entrance_forward)
@ -81,49 +85,49 @@ def _can_do_expert_pp2(state: CollectionState, world: "WitnessWorld") -> bool:
player = world.player
hedge_2_access = (
_can_move_either_direction(state, "Keep 2nd Maze", "Keep", world.regio)
_can_move_either_direction(state, "Keep 2nd Maze", "Keep", world.player_regions)
)
hedge_3_access = (
_can_move_either_direction(state, "Keep 3rd Maze", "Keep", world.regio)
or _can_move_either_direction(state, "Keep 3rd Maze", "Keep 2nd Maze", world.regio)
and hedge_2_access
_can_move_either_direction(state, "Keep 3rd Maze", "Keep", world.player_regions)
or _can_move_either_direction(state, "Keep 3rd Maze", "Keep 2nd Maze", world.player_regions)
and hedge_2_access
)
hedge_4_access = (
_can_move_either_direction(state, "Keep 4th Maze", "Keep", world.regio)
or _can_move_either_direction(state, "Keep 4th Maze", "Keep 3rd Maze", world.regio)
and hedge_3_access
_can_move_either_direction(state, "Keep 4th Maze", "Keep", world.player_regions)
or _can_move_either_direction(state, "Keep 4th Maze", "Keep 3rd Maze", world.player_regions)
and hedge_3_access
)
hedge_access = (
_can_move_either_direction(state, "Keep 4th Maze", "Keep Tower", world.regio)
and state.can_reach("Keep", "Region", player)
and hedge_4_access
_can_move_either_direction(state, "Keep 4th Maze", "Keep Tower", world.player_regions)
and state.can_reach("Keep", "Region", player)
and hedge_4_access
)
backwards_to_fourth = (
state.can_reach("Keep", "Region", player)
and _can_move_either_direction(state, "Keep 4th Pressure Plate", "Keep Tower", world.regio)
and (
_can_move_either_direction(state, "Keep", "Keep Tower", world.regio)
or hedge_access
)
state.can_reach("Keep", "Region", player)
and _can_move_either_direction(state, "Keep 4th Pressure Plate", "Keep Tower", world.player_regions)
and (
_can_move_either_direction(state, "Keep", "Keep Tower", world.player_regions)
or hedge_access
)
)
shadows_shortcut = (
state.can_reach("Main Island", "Region", player)
and _can_move_either_direction(state, "Keep 4th Pressure Plate", "Shadows", world.regio)
state.can_reach("Main Island", "Region", player)
and _can_move_either_direction(state, "Keep 4th Pressure Plate", "Shadows", world.player_regions)
)
backwards_access = (
_can_move_either_direction(state, "Keep 3rd Pressure Plate", "Keep 4th Pressure Plate", world.regio)
and (backwards_to_fourth or shadows_shortcut)
_can_move_either_direction(state, "Keep 3rd Pressure Plate", "Keep 4th Pressure Plate", world.player_regions)
and (backwards_to_fourth or shadows_shortcut)
)
front_access = (
_can_move_either_direction(state, "Keep 2nd Pressure Plate", "Keep", world.regio)
and state.can_reach("Keep", "Region", player)
_can_move_either_direction(state, "Keep 2nd Pressure Plate", "Keep", world.player_regions)
and state.can_reach("Keep", "Region", player)
)
return front_access and backwards_access
@ -131,27 +135,27 @@ def _can_do_expert_pp2(state: CollectionState, world: "WitnessWorld") -> bool:
def _can_do_theater_to_tunnels(state: CollectionState, world: "WitnessWorld") -> bool:
direct_access = (
_can_move_either_direction(state, "Tunnels", "Windmill Interior", world.regio)
and _can_move_either_direction(state, "Theater", "Windmill Interior", world.regio)
_can_move_either_direction(state, "Tunnels", "Windmill Interior", world.player_regions)
and _can_move_either_direction(state, "Theater", "Windmill Interior", world.player_regions)
)
theater_from_town = (
_can_move_either_direction(state, "Town", "Windmill Interior", world.regio)
and _can_move_either_direction(state, "Theater", "Windmill Interior", world.regio)
or _can_move_either_direction(state, "Town", "Theater", world.regio)
_can_move_either_direction(state, "Town", "Windmill Interior", world.player_regions)
and _can_move_either_direction(state, "Theater", "Windmill Interior", world.player_regions)
or _can_move_either_direction(state, "Town", "Theater", world.player_regions)
)
tunnels_from_town = (
_can_move_either_direction(state, "Tunnels", "Windmill Interior", world.regio)
and _can_move_either_direction(state, "Town", "Windmill Interior", world.regio)
or _can_move_either_direction(state, "Tunnels", "Town", world.regio)
_can_move_either_direction(state, "Tunnels", "Windmill Interior", world.player_regions)
and _can_move_either_direction(state, "Town", "Windmill Interior", world.player_regions)
or _can_move_either_direction(state, "Tunnels", "Town", world.player_regions)
)
return direct_access or theater_from_town and tunnels_from_town
def _has_item(item: str, world: "WitnessWorld", player: int,
player_logic: WitnessPlayerLogic, locat: WitnessPlayerLocations) -> Callable[[CollectionState], bool]:
player_logic: WitnessPlayerLogic, player_locations: WitnessPlayerLocations) -> CollectionRule:
if item in player_logic.REFERENCE_LOGIC.ALL_REGIONS_BY_NAME:
return lambda state: state.can_reach(item, "Region", player)
if item == "7 Lasers":
@ -171,21 +175,21 @@ def _has_item(item: str, world: "WitnessWorld", player: int,
elif item == "Theater to Tunnels":
return lambda state: _can_do_theater_to_tunnels(state, world)
if item in player_logic.USED_EVENT_NAMES_BY_HEX:
return _can_solve_panel(item, world, player, player_logic, locat)
return _can_solve_panel(item, world, player, player_logic, player_locations)
prog_item = StaticWitnessLogic.get_parent_progressive_item(item)
prog_item = static_witness_logic.get_parent_progressive_item(item)
return lambda state: state.has(prog_item, player, player_logic.MULTI_AMOUNTS[item])
def _meets_item_requirements(requirements: FrozenSet[FrozenSet[str]],
world: "WitnessWorld") -> Callable[[CollectionState], bool]:
world: "WitnessWorld") -> CollectionRule:
"""
Checks whether item and panel requirements are met for
a panel
"""
lambda_conversion = [
[_has_item(item, world, world.player, world.player_logic, world.locat) for item in subset]
[_has_item(item, world, world.player, world.player_logic, world.player_locations) for item in subset]
for subset in requirements
]
@ -195,7 +199,7 @@ def _meets_item_requirements(requirements: FrozenSet[FrozenSet[str]],
)
def make_lambda(entity_hex: str, world: "WitnessWorld") -> Callable[[CollectionState], bool]:
def make_lambda(entity_hex: str, world: "WitnessWorld") -> CollectionRule:
"""
Lambdas are created in a for loop so values need to be captured
"""
@ -204,15 +208,15 @@ def make_lambda(entity_hex: str, world: "WitnessWorld") -> Callable[[CollectionS
return _meets_item_requirements(entity_req, world)
def set_rules(world: "WitnessWorld"):
def set_rules(world: "WitnessWorld") -> None:
"""
Sets all rules for all locations
"""
for location in world.locat.CHECK_LOCATION_TABLE:
for location in world.player_locations.CHECK_LOCATION_TABLE:
real_location = location
if location in world.locat.EVENT_LOCATION_TABLE:
if location in world.player_locations.EVENT_LOCATION_TABLE:
real_location = location[:-7]
associated_entity = world.player_logic.REFERENCE_LOGIC.ENTITIES_BY_NAME[real_location]
@ -220,8 +224,8 @@ def set_rules(world: "WitnessWorld"):
rule = make_lambda(entity_hex, world)
location = world.multiworld.get_location(location, world.player)
location = world.get_location(location)
set_rule(location, rule)
world.multiworld.completion_condition[world.player] = lambda state: state.has('Victory', world.player)
world.multiworld.completion_condition[world.player] = lambda state: state.has("Victory", world.player)