The Witness: The big dumb refactor (#3007)

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 Archipelago init file for The Witness
""" """
import dataclasses 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 Options import PerGameCommonOptions, Toggle
from .presets import witness_option_presets from worlds.AutoWorld import WebWorld, World
from worlds.AutoWorld import World, WebWorld
from .player_logic import WitnessPlayerLogic from .data import static_items as static_witness_items
from .static_logic import StaticWitnessLogic, ItemCategory, DoorItemDefinition from .data import static_logic as static_witness_logic
from .hints import get_always_hint_locations, get_always_hint_items, get_priority_hint_locations, \ from .data.item_definition_classes import DoorItemDefinition, ItemData
get_priority_hint_items, make_always_and_priority_hints, generate_joke_hints, make_area_hints, get_hintable_areas, \ from .data.utils import get_audio_logs
make_extra_location_hints, create_all_hints, make_laser_hints, make_compact_hint_data, CompactItemData from .hints import CompactItemData, create_all_hints, generate_joke_hints, make_compact_hint_data, make_laser_hints
from .locations import WitnessPlayerLocations, StaticWitnessLocations from .locations import WitnessPlayerLocations, static_witness_locations
from .items import WitnessItem, StaticWitnessItems, WitnessPlayerItems, ItemData
from .regions import WitnessRegions
from .rules import set_rules
from .options import TheWitnessOptions from .options import TheWitnessOptions
from .utils import get_audio_logs, get_laser_shuffle from .player_items import WitnessItem, WitnessPlayerItems
from logging import warning, error from .player_logic import WitnessPlayerLogic
from .presets import witness_option_presets
from .regions import WitnessPlayerRegions
from .rules import set_rules
class WitnessWebWorld(WebWorld): class WitnessWebWorld(WebWorld):
@ -50,46 +52,43 @@ class WitnessWorld(World):
options: TheWitnessOptions options: TheWitnessOptions
item_name_to_id = { 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 location_name_to_id = static_witness_locations.ALL_LOCATIONS_TO_ID
item_name_groups = StaticWitnessItems.item_groups item_name_groups = static_witness_items.ITEM_GROUPS
location_name_groups = StaticWitnessLocations.AREA_LOCATION_GROUPS location_name_groups = static_witness_locations.AREA_LOCATION_GROUPS
required_client_version = (0, 4, 5) required_client_version = (0, 4, 5)
def __init__(self, multiworld: "MultiWorld", player: int): player_logic: WitnessPlayerLogic
super().__init__(multiworld, player) player_locations: WitnessPlayerLocations
player_items: WitnessPlayerItems
player_regions: WitnessPlayerRegions
self.player_logic = None log_ids_to_hints: Dict[int, CompactItemData]
self.locat = None laser_ids_to_hints: Dict[int, CompactItemData]
self.items = None
self.regio = None
self.log_ids_to_hints: Dict[int, CompactItemData] = dict() items_placed_early: List[str]
self.laser_ids_to_hints: Dict[int, CompactItemData] = dict() own_itempool: List[WitnessItem]
self.items_placed_early = [] def _get_slot_data(self) -> Dict[str, Any]:
self.own_itempool = []
def _get_slot_data(self):
return { return {
'seed': self.random.randrange(0, 1000000), "seed": self.random.randrange(0, 1000000),
'victory_location': int(self.player_logic.VICTORY_LOCATION, 16), "victory_location": int(self.player_logic.VICTORY_LOCATION, 16),
'panelhex_to_id': self.locat.CHECK_PANELHEX_TO_ID, "panelhex_to_id": self.player_locations.CHECK_PANELHEX_TO_ID,
'item_id_to_door_hexes': StaticWitnessItems.get_item_to_door_mappings(), "item_id_to_door_hexes": static_witness_items.get_item_to_door_mappings(),
'door_hexes_in_the_pool': self.items.get_door_ids_in_pool(), "door_hexes_in_the_pool": self.player_items.get_door_ids_in_pool(),
'symbols_not_in_the_game': self.items.get_symbol_ids_not_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], "disabled_entities": [int(h, 16) for h in self.player_logic.COMPLETELY_DISABLED_ENTITIES],
'log_ids_to_hints': self.log_ids_to_hints, "log_ids_to_hints": self.log_ids_to_hints,
'laser_ids_to_hints': self.laser_ids_to_hints, "laser_ids_to_hints": self.laser_ids_to_hints,
'progressive_item_lists': self.items.get_progressive_item_ids_in_pool(), "progressive_item_lists": self.player_items.get_progressive_item_ids_in_pool(),
'obelisk_side_id_to_EPs': StaticWitnessLogic.OBELISK_SIDE_ID_TO_EP_HEXES, "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], "precompleted_puzzles": [int(h, 16) for h in self.player_logic.EXCLUDED_LOCATIONS],
'entity_to_name': StaticWitnessLogic.ENTITY_ID_TO_NAME, "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". Determine whether there are enough progression items in this world to consider it "interactive".
In the case of singleplayer, this just outputs a warning. 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: 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" 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" 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 disabled_locations = self.options.exclude_locations.value
self.player_logic = WitnessPlayerLogic( self.player_logic = WitnessPlayerLogic(
self, disabled_locations, self.options.start_inventory.value self, disabled_locations, self.options.start_inventory.value
) )
self.locat: WitnessPlayerLocations = WitnessPlayerLocations(self, self.player_logic) self.player_locations: WitnessPlayerLocations = WitnessPlayerLocations(self, self.player_logic)
self.items: WitnessPlayerItems = WitnessPlayerItems( self.player_items: WitnessPlayerItems = WitnessPlayerItems(
self, self.player_logic, self.locat 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() self.log_ids_to_hints = dict()
@ -149,22 +148,27 @@ class WitnessWorld(World):
if self.options.shuffle_lasers == "local": if self.options.shuffle_lasers == "local":
self.options.local_items.value |= self.item_name_groups["Lasers"] self.options.local_items.value |= self.item_name_groups["Lasers"]
def create_regions(self): def create_regions(self) -> None:
self.regio.create_regions(self, self.player_logic) 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 early so extra locations can be created based on the results of exploring collection states
set_rules(self) 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). # Add event items and tie them to event locations (e.g. laser activations).
event_locations = [] 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( item_obj = self.create_item(
self.player_logic.EVENT_ITEM_PAIRS[event_location] 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) location_obj.place_locked_item(item_obj)
self.own_itempool.append(item_obj) self.own_itempool.append(item_obj)
@ -172,14 +176,16 @@ class WitnessWorld(World):
# Place other locked items # Place other locked items
dog_puzzle_skip = self.create_item("Puzzle Skip") 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.own_itempool.append(dog_puzzle_skip)
self.items_placed_early.append("Puzzle Skip") self.items_placed_early.append("Puzzle Skip")
# Pick an early item to place on the tutorial gate. # 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: if early_items:
random_early_item = self.random.choice(early_items) random_early_item = self.random.choice(early_items)
if self.options.puzzle_randomization == "sigma_expert": if self.options.puzzle_randomization == "sigma_expert":
@ -188,7 +194,7 @@ class WitnessWorld(World):
else: else:
# Force the item onto the tutorial gate check and remove it from our random pool. # Force the item onto the tutorial gate check and remove it from our random pool.
gate_item = self.create_item(random_early_item) 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.own_itempool.append(gate_item)
self.items_placed_early.append(random_early_item) self.items_placed_early.append(random_early_item)
@ -223,19 +229,19 @@ class WitnessWorld(World):
break break
region, loc = extra_checks.pop(0) region, loc = extra_checks.pop(0)
self.locat.add_location_late(loc) self.player_locations.add_location_late(loc)
self.multiworld.get_region(region, self.player).add_locations({loc: self.location_name_to_id[loc]}) self.get_region(region).add_locations({loc: self.location_name_to_id[loc]})
player = self.multiworld.get_player_name(self.player) 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.""") 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. # 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. # 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 # Remove one copy of each item that was placed early
for already_placed in self.items_placed_early: for already_placed in self.items_placed_early:
@ -283,7 +289,7 @@ class WitnessWorld(World):
# Add junk items. # Add junk items.
if remaining_item_slots > 0: 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. # Generate the actual items.
for item_name, quantity in sorted(item_pool.items()): for item_name, quantity in sorted(item_pool.items()):
@ -291,19 +297,22 @@ class WitnessWorld(World):
self.own_itempool += new_items self.own_itempool += new_items
self.multiworld.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) self.options.local_items.value.add(item_name)
def fill_slot_data(self) -> dict: 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() already_hinted_locations = set()
# Laser hints # Laser hints
if self.options.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(): 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) 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) already_hinted_locations.add(hint.location)
@ -356,18 +365,18 @@ class WitnessWorld(World):
return slot_data 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 # 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. # name of the item, rather than the item itself. This is a workaround to prevent a crash.
if type(item_name) is dict: if isinstance(item_name, dict):
item_name = list(item_name.keys())[0] 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 # this conditional is purely for unit tests, which need to be able to create an item before generate_early
item_data: ItemData item_data: ItemData
if hasattr(self, 'items') and self.items and item_name in self.items.item_data: if hasattr(self, "player_items") and self.player_items and item_name in self.player_items.item_data:
item_data = self.items.item_data[item_name] item_data = self.player_items.item_data[item_name]
else: 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) 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" game: str = "The Witness"
entity_hex: int = -1 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) super().__init__(player, name, address, parent)
self.entity_hex = ch_hex 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 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) ret = Region(name, world.player, world.multiworld)
if region_locations: if region_locations:
for location in 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 entity_hex = -1
if location in StaticWitnessLogic.ENTITIES_BY_NAME: if location in static_witness_logic.ENTITIES_BY_NAME:
entity_hex = int( entity_hex = int(
StaticWitnessLogic.ENTITIES_BY_NAME[location]["entity_hex"], 0 static_witness_logic.ENTITIES_BY_NAME[location]["entity_hex"], 0
) )
location = WitnessLocation( location = WitnessLocation(
world.player, location, loc_id, ret, entity_hex 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 functools import lru_cache
from enum import Enum
from typing import Dict, List from typing import Dict, List
from .utils import define_new_region, parse_lambda, lazy, get_items, get_sigma_normal_logic, get_sigma_expert_logic,\ from .item_definition_classes import (
get_vanilla_logic CATEGORY_NAME_MAPPINGS,
DoorItemDefinition,
ItemCategory,
class ItemCategory(Enum): ItemDefinition,
SYMBOL = 0 ProgressiveItemDefinition,
DOOR = 1 WeightedItemDefinition,
LASER = 2 )
USEFUL = 3 from .utils import (
FILLER = 4 define_new_region,
TRAP = 5 get_items,
JOKE = 6 get_sigma_expert_logic,
EVENT = 7 get_sigma_normal_logic,
get_vanilla_logic,
parse_lambda,
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
class StaticWitnessLogicObj: 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 Reads the logic file and does the initial population of data structures
""" """
@ -152,7 +122,7 @@ class StaticWitnessLogicObj:
} }
if location_type == "Obelisk Side": if location_type == "Obelisk Side":
eps = set(list(required_panels)[0]) eps = set(next(iter(required_panels)))
eps -= {"Theater to Tunnels"} eps -= {"Theater to Tunnels"}
eps_ints = {int(h, 16) for h in eps} eps_ints = {int(h, 16) for h in eps}
@ -177,7 +147,7 @@ class StaticWitnessLogicObj:
current_region["panels"].append(entity_hex) current_region["panels"].append(entity_hex)
def __init__(self, lines=None): def __init__(self, lines=None) -> None:
if lines is None: if lines is None:
lines = get_sigma_normal_logic() lines = get_sigma_normal_logic()
@ -199,102 +169,95 @@ class StaticWitnessLogicObj:
self.read_logic_file(lines) self.read_logic_file(lines)
class StaticWitnessLogic: # Item data parsed from WitnessItems.txt
# Item data parsed from WitnessItems.txt ALL_ITEMS: Dict[str, ItemDefinition] = {}
all_items: Dict[str, ItemDefinition] = {} _progressive_lookup: Dict[str, str] = {}
_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)
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 functools import lru_cache
from math import floor from math import floor
from typing import List, Collection, FrozenSet, Tuple, Dict, Any, Set
from pkgutil import get_data from pkgutil import get_data
from random import random 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)) positions = range(len(population))
indices = [] indices = []
while True: while True:
@ -95,25 +95,9 @@ def parse_lambda(lambda_string) -> FrozenSet[FrozenSet[str]]:
return lambda_set 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) @lru_cache(maxsize=None)
def get_adjustment_file(adjustment_file: str) -> List[str]: 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")] return [line.strip() for line in data.split("\n")]

View File

@ -1,9 +1,11 @@
import logging import logging
from dataclasses import dataclass from dataclasses import dataclass
from typing import Tuple, List, TYPE_CHECKING, Set, Dict, Optional, Union from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple, Union
from BaseClasses import Item, ItemClassification, Location, LocationProgressType, CollectionState
from . import StaticWitnessLogic from BaseClasses import CollectionState, Item, Location, LocationProgressType
from .utils import weighted_sample
from .data import static_logic as static_witness_logic
from .data.utils import weighted_sample
if TYPE_CHECKING: if TYPE_CHECKING:
from . import WitnessWorld 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 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 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 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?\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 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?", "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 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 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 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 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 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 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.", "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 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 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 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", "Quaternions break my brain",
"Eclipse has nothing, but you should do it anyway.", "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?", "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.", "Droplets: Low, High, Mid.\nAmbience: Mid, Low, Mid, High.",
"Name a better game involving lines. I'll wait.", "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.", "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.", "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 :)", "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?", "\"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?", "Wouldn't you like to know, weather buoy?",
@ -192,10 +194,10 @@ class WitnessLocationHint:
hint_came_from_location: bool 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 # 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) return hash(self.location)
def __eq__(self, other): def __eq__(self, other) -> bool:
return self.location == other.location return self.location == other.location
@ -324,7 +326,7 @@ def get_priority_hint_locations(world: "WitnessWorld") -> List[str]:
"Boat Shipwreck Green EP", "Boat Shipwreck Green EP",
"Quarry Stoneworks Control Room Left", "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 # 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: if "0x33A20" not in world.player_logic.COMPLETELY_DISABLED_ENTITIES:
priority.append("Town Obelisk Side 6") # Theater Flowers EP priority.append("Town Obelisk Side 6") # Theater Flowers EP
@ -338,7 +340,7 @@ def get_priority_hint_locations(world: "WitnessWorld") -> List[str]:
return priority return priority
def word_direct_hint(world: "WitnessWorld", hint: WitnessLocationHint): def word_direct_hint(world: "WitnessWorld", hint: WitnessLocationHint) -> WitnessWordedHint:
location_name = hint.location.name location_name = hint.location.name
if hint.location.player != world.player: if hint.location.player != world.player:
location_name += " (" + world.multiworld.get_player_name(hint.location.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]: def hint_from_location(world: "WitnessWorld", location: str) -> Optional[WitnessLocationHint]:
location_obj = world.multiworld.get_location(location, world.player) location_obj = world.get_location(location)
item_obj = world.multiworld.get_location(location, world.player).item item_obj = location_obj.item
item_name = item_obj.name item_name = item_obj.name
if item_obj.player != world.player: if item_obj.player != world.player:
item_name += " (" + world.multiworld.get_player_name(item_obj.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) 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( prog_items_in_this_world = sorted(
item.name for item in own_itempool item.name for item in own_itempool
if item.advancement and item.code and item.location 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 = [] hints = []
# This is a way to reverse a Dict[a,List[b]] to a Dict[b,a] # 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: while len(hints) < hint_amount:
if not prog_items_in_this_world and not locations_in_this_world and not hints_to_use_first: 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]]]: 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() locations_per_area = dict()
items_per_area = dict() items_per_area = dict()
for area in potential_areas: for area in potential_areas:
regions = [ regions = [
world.regio.created_regions[region] world.player_regions.created_regions[region]
for region in StaticWitnessLogic.ALL_AREAS_BY_NAME[area]["regions"] for region in static_witness_logic.ALL_AREAS_BY_NAME[area]["regions"]
if region in world.regio.created_regions if region in world.player_regions.created_regions
] ]
locations = [location for region in regions for location in region.get_locations() if location.address] 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: if local_lasers == total_progression:
sentence_end = (" for this world." if player_count > 1 else ".") 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: elif player_count > 1:
if local_progression and non_local_progression: 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 |= { already_hinted_locations |= {
loc for loc in world.multiworld.get_reachable_locations(state, world.player) 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 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 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 .player_logic import WitnessPlayerLogic
from .static_logic import StaticWitnessLogic
if TYPE_CHECKING: if TYPE_CHECKING:
from . import WitnessWorld 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 WitnessPlayerLocations:
""" """
Class that defines locations for a single player 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""" """Defines locations AFTER logic changes due to options"""
self.PANEL_TYPES_TO_SHUFFLE = {"General", "Laser"} 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: if world.options.shuffle_discarded_panels:
self.PANEL_TYPES_TO_SHUFFLE.add("Discard") self.PANEL_TYPES_TO_SHUFFLE.add("Discard")
@ -520,28 +33,28 @@ class WitnessPlayerLocations:
elif world.options.shuffle_EPs == "obelisk_sides": elif world.options.shuffle_EPs == "obelisk_sides":
self.PANEL_TYPES_TO_SHUFFLE.add("Obelisk Side") self.PANEL_TYPES_TO_SHUFFLE.add("Obelisk Side")
for obelisk_loc in StaticWitnessLocations.OBELISK_SIDES: for obelisk_loc in static_witness_locations.OBELISK_SIDES:
obelisk_loc_hex = StaticWitnessLogic.ENTITIES_BY_NAME[obelisk_loc]["entity_hex"] obelisk_loc_hex = static_witness_logic.ENTITIES_BY_NAME[obelisk_loc]["entity_hex"]
if player_logic.REQUIREMENTS_BY_HEX[obelisk_loc_hex] == frozenset({frozenset()}): if player_logic.REQUIREMENTS_BY_HEX[obelisk_loc_hex] == frozenset({frozenset()}):
self.CHECK_LOCATIONS.discard(obelisk_loc) self.CHECK_LOCATIONS.discard(obelisk_loc)
self.CHECK_LOCATIONS = self.CHECK_LOCATIONS | player_logic.ADDED_CHECKS 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 - { 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 for entity_hex in player_logic.COMPLETELY_DISABLED_ENTITIES | player_logic.PRECOMPLETED_LOCATIONS
} }
self.CHECK_PANELHEX_TO_ID = { 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 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_hex = static_witness_logic.ENTITIES_BY_NAME["Town Pet the Dog"]["entity_hex"]
dog_id = StaticWitnessLocations.ALL_LOCATIONS_TO_ID["Town Pet the Dog"] 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[dog_hex] = dog_id
self.CHECK_PANELHEX_TO_ID = dict( self.CHECK_PANELHEX_TO_ID = dict(
@ -553,22 +66,19 @@ class WitnessPlayerLocations:
} }
self.EVENT_LOCATION_TABLE = { self.EVENT_LOCATION_TABLE = {
StaticWitnessLocations.get_event_name(panel_hex): None static_witness_locations.get_event_name(entity_hex): None
for panel_hex in event_locations for entity_hex in event_locations
} }
check_dict = { check_dict = {
StaticWitnessLogic.ENTITIES_BY_HEX[location]["checkName"]: static_witness_logic.ENTITIES_BY_HEX[location]["checkName"]:
StaticWitnessLocations.get_id(StaticWitnessLogic.ENTITIES_BY_HEX[location]["entity_hex"]) static_witness_locations.get_id(static_witness_logic.ENTITIES_BY_HEX[location]["entity_hex"])
for location in self.CHECK_PANELHEX_TO_ID for location in self.CHECK_PANELHEX_TO_ID
} }
self.CHECK_LOCATION_TABLE = {**self.EVENT_LOCATION_TABLE, **check_dict} self.CHECK_LOCATION_TABLE = {**self.EVENT_LOCATION_TABLE, **check_dict}
def add_location_late(self, entity_name: str): def add_location_late(self, entity_name: str) -> None:
entity_hex = StaticWitnessLogic.ENTITIES_BY_NAME[entity_name]["entity_hex"] entity_hex = static_witness_logic.ENTITIES_BY_NAME[entity_name]["entity_hex"]
self.CHECK_LOCATION_TABLE[entity_hex] = entity_name self.CHECK_LOCATION_TABLE[entity_hex] = entity_name
self.CHECK_PANELHEX_TO_ID[entity_hex] = StaticWitnessLocations.get_id(entity_hex) self.CHECK_PANELHEX_TO_ID[entity_hex] = static_witness_locations.get_id(entity_hex)
StaticWitnessLocations()

View File

@ -1,10 +1,11 @@
from dataclasses import dataclass 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): class DisableNonRandomizedPuzzles(Toggle):
@ -232,12 +233,12 @@ class TrapWeights(OptionDict):
display_name = "Trap Weights" display_name = "Trap Weights"
schema = Schema({ schema = Schema({
trap_name: And(int, lambda n: n >= 0) 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 if isinstance(item_definition, WeightedItemDefinition) and item_definition.category is ItemCategory.TRAP
}) })
default = { default = {
trap_name: item_definition.weight 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 if isinstance(item_definition, WeightedItemDefinition) and item_definition.category is ItemCategory.TRAP
} }
@ -315,7 +316,7 @@ class TheWitnessOptions(PerGameCommonOptions):
shuffle_discarded_panels: ShuffleDiscardedPanels shuffle_discarded_panels: ShuffleDiscardedPanels
shuffle_vault_boxes: ShuffleVaultBoxes shuffle_vault_boxes: ShuffleVaultBoxes
obelisk_keys: ObeliskKeys obelisk_keys: ObeliskKeys
shuffle_EPs: ShuffleEnvironmentalPuzzles shuffle_EPs: ShuffleEnvironmentalPuzzles # noqa: N815
EP_difficulty: EnvironmentalPuzzlesDifficulty EP_difficulty: EnvironmentalPuzzlesDifficulty
shuffle_postgame: ShufflePostgame shuffle_postgame: ShufflePostgame
victory_condition: VictoryCondition victory_condition: VictoryCondition

View File

@ -2,16 +2,23 @@
Defines progression, junk and event items for The Witness Defines progression, junk and event items for The Witness
""" """
import copy import copy
from typing import TYPE_CHECKING, Dict, List, Set
from dataclasses import dataclass from BaseClasses import Item, ItemClassification, MultiWorld
from typing import Optional, Dict, List, Set, TYPE_CHECKING
from BaseClasses import Item, MultiWorld, ItemClassification from .data import static_items as static_witness_items
from .locations import ID_START, WitnessPlayerLocations 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 .player_logic import WitnessPlayerLogic
from .static_logic import ItemDefinition, DoorItemDefinition, ProgressiveItemDefinition, ItemCategory, \
StaticWitnessLogic, WeightedItemDefinition
from .utils import build_weighted_int_list
if TYPE_CHECKING: if TYPE_CHECKING:
from . import WitnessWorld from . import WitnessWorld
@ -19,17 +26,6 @@ if TYPE_CHECKING:
NUM_ENERGY_UPGRADES = 4 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): class WitnessItem(Item):
""" """
Item from the game The Witness Item from the game The Witness
@ -37,79 +33,30 @@ class WitnessItem(Item):
game: str = "The Witness" 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 WitnessPlayerItems:
""" """
Class that defines Items for a single world 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""" """Adds event items after logic changes due to options"""
self._world: "WitnessWorld" = world self._world: "WitnessWorld" = world
self._multiworld: MultiWorld = world.multiworld self._multiworld: MultiWorld = world.multiworld
self._player_id: int = world.player self._player_id: int = world.player
self._logic: WitnessPlayerLogic = logic self._logic: WitnessPlayerLogic = player_logic
self._locations: WitnessPlayerLocations = locat self._locations: WitnessPlayerLocations = player_locations
# Duplicate the static item data, then make any player-specific adjustments to classification. # 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. # Remove all progression items that aren't actually in the game.
self.item_data = { self.item_data = {
name: data for (name, data) in self.item_data.items() name: data for (name, data) in self.item_data.items()
if data.classification not in if data.classification not in
{ItemClassification.progression, ItemClassification.progression_skip_balancing} {ItemClassification.progression, ItemClassification.progression_skip_balancing}
or name in logic.PROG_ITEMS_ACTUALLY_IN_THE_GAME or name in player_logic.PROG_ITEMS_ACTUALLY_IN_THE_GAME
} }
# Downgrade door items # Downgrade door items
@ -138,7 +85,7 @@ class WitnessPlayerItems:
# Add setting-specific useful items to the mandatory item list. # 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() for item_name, item_data in {name: data for (name, data) in self.item_data.items()
if data.classification == ItemClassification.useful}.items(): if data.classification == ItemClassification.useful}.items():
if item_name in StaticWitnessItems.special_usefuls: if item_name in static_witness_items._special_usefuls:
continue continue
elif item_name == "Energy Capacity": elif item_name == "Energy Capacity":
self._mandatory_items[item_name] = NUM_ENERGY_UPGRADES 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. # Add event items to the item definition list for later lookup.
for event_location in self._locations.EVENT_LOCATION_TABLE: 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), self.item_data[location_name] = ItemData(None, ItemDefinition(0, ItemCategory.EVENT),
ItemClassification.progression, False) ItemClassification.progression, False)
@ -219,7 +166,7 @@ class WitnessPlayerItems:
output.add("Triangles") output.add("Triangles")
# Replace progressive items with their parents. # 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 # 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 # 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]: for plando_setting in self._multiworld.plando_items[self._player_id]:
if plando_setting.get("from_pool", True): if plando_setting.get("from_pool", True):
for item_setting_key in [key for key in ["item", "items"] if key in plando_setting]: 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]} 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} output -= {item for item, weight in plando_setting[item_setting_key].items() if weight}
else: else:
# Assume this is some other kind of iterable. # Assume this is some other kind of iterable.
for inner_item in plando_setting[item_setting_key]: for inner_item in plando_setting[item_setting_key]:
if type(inner_item) is str: if isinstance(inner_item, str):
output -= {inner_item} 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} 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. # 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. 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] 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]]: def get_progressive_item_ids_in_pool(self) -> Dict[int, List[int]]:
@ -267,9 +214,8 @@ class WitnessPlayerItems:
if isinstance(item.definition, ProgressiveItemDefinition): if isinstance(item.definition, ProgressiveItemDefinition):
# Note: we need to reference the static table here rather than the player-specific one because the child # 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. # 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] for child_item in item.definition.child_item_names]
return output 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 import copy
from collections import defaultdict from collections import defaultdict
from typing import cast, TYPE_CHECKING from functools import lru_cache
from logging import warning from logging import warning
from typing import TYPE_CHECKING, Dict, FrozenSet, List, Set, Tuple, cast
from .static_logic import StaticWitnessLogic, DoorItemDefinition, ItemCategory, ProgressiveItemDefinition from .data import static_logic as static_witness_logic
from .utils import * from .data import utils
from .data.item_definition_classes import DoorItemDefinition, ItemCategory, ProgressiveItemDefinition
if TYPE_CHECKING: if TYPE_CHECKING:
from . import WitnessWorld from . import WitnessWorld
@ -31,7 +33,7 @@ class WitnessPlayerLogic:
"""WITNESS LOGIC CLASS""" """WITNESS LOGIC CLASS"""
@lru_cache(maxsize=None) @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. Panels in this game often only turn on when other panels are solved.
Those other panels may have different item requirements. 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. 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() 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()}) these_items = frozenset({frozenset()})
if entity_obj["id"]: 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({ these_items = frozenset({
subset.intersection(self.THEORETICAL_ITEMS_NO_MULTI) subset.intersection(self.THEORETICAL_ITEMS_NO_MULTI)
@ -58,28 +60,28 @@ class WitnessPlayerLogic:
for subset in these_items: for subset in these_items:
self.PROG_ITEMS_ACTUALLY_IN_THE_GAME_NO_MULTI.update(subset) 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: if entity_hex in self.DOOR_ITEMS_BY_ID:
door_items = frozenset({frozenset([item]) for item in self.DOOR_ITEMS_BY_ID[panel_hex]}) door_items = frozenset({frozenset([item]) for item in self.DOOR_ITEMS_BY_ID[entity_hex]})
all_options: Set[FrozenSet[str]] = set() all_options: Set[FrozenSet[str]] = set()
for dependentItem in door_items: for dependent_item in door_items:
self.PROG_ITEMS_ACTUALLY_IN_THE_GAME_NO_MULTI.update(dependentItem) self.PROG_ITEMS_ACTUALLY_IN_THE_GAME_NO_MULTI.update(dependent_item)
for items_option in these_items: 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 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, # 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. # 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. # 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 these_items = all_options
# Another dependency that is not power-based: The Symmetry Island Upper Panel latches # 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 these_items = all_options
else: else:
@ -107,9 +109,9 @@ class WitnessPlayerLogic:
if option_entity in self.ALWAYS_EVENT_NAMES_BY_HEX: if option_entity in self.ALWAYS_EVENT_NAMES_BY_HEX:
new_items = frozenset({frozenset([option_entity])}) 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])}) 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", elif option_entity in {"7 Lasers", "11 Lasers", "7 Lasers + Redirect", "11 Lasers + Redirect",
"PP2 Weirdness", "Theater to Tunnels"}: "PP2 Weirdness", "Theater to Tunnels"}:
new_items = frozenset({frozenset([option_entity])}) new_items = frozenset({frozenset([option_entity])})
@ -121,36 +123,36 @@ class WitnessPlayerLogic:
for possibility in new_items 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 items_option in these_items:
for dependentItem in dependent_items_for_option: for dependent_item in dependent_items_for_option:
all_options.add(items_option.union(dependentItem)) 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): def make_single_adjustment(self, adj_type: str, line: str) -> None:
from . import StaticWitnessItems from .data import static_items as static_witness_items
"""Makes a single logic adjustment based on additional logic file""" """Makes a single logic adjustment based on additional logic file"""
if adj_type == "Items": if adj_type == "Items":
line_split = line.split(" - ") line_split = line.split(" - ")
item_name = line_split[0] item_name = line_split[0]
if item_name not in StaticWitnessItems.item_data: if item_name not in static_witness_items.ITEM_DATA:
raise RuntimeError("Item \"" + item_name + "\" does not exist.") raise RuntimeError(f'Item "{item_name}" does not exist.')
self.THEORETICAL_ITEMS.add(item_name) 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, 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: else:
self.THEORETICAL_ITEMS_NO_MULTI.add(item_name) self.THEORETICAL_ITEMS_NO_MULTI.add(item_name)
if StaticWitnessLogic.all_items[item_name].category in [ItemCategory.DOOR, ItemCategory.LASER]: if static_witness_logic.ALL_ITEMS[item_name].category in [ItemCategory.DOOR, ItemCategory.LASER]:
panel_hexes = cast(DoorItemDefinition, StaticWitnessLogic.all_items[item_name]).panel_id_hexes entity_hexes = cast(DoorItemDefinition, static_witness_logic.ALL_ITEMS[item_name]).panel_id_hexes
for panel_hex in panel_hexes: for entity_hex in entity_hexes:
self.DOOR_ITEMS_BY_ID.setdefault(panel_hex, []).append(item_name) self.DOOR_ITEMS_BY_ID.setdefault(entity_hex, []).append(item_name)
return return
@ -158,18 +160,18 @@ class WitnessPlayerLogic:
item_name = line item_name = line
self.THEORETICAL_ITEMS.discard(item_name) 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( 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: else:
self.THEORETICAL_ITEMS_NO_MULTI.discard(item_name) self.THEORETICAL_ITEMS_NO_MULTI.discard(item_name)
if StaticWitnessLogic.all_items[item_name].category in [ItemCategory.DOOR, ItemCategory.LASER]: if static_witness_logic.ALL_ITEMS[item_name].category in [ItemCategory.DOOR, ItemCategory.LASER]:
panel_hexes = cast(DoorItemDefinition, StaticWitnessLogic.all_items[item_name]).panel_id_hexes entity_hexes = cast(DoorItemDefinition, static_witness_logic.ALL_ITEMS[item_name]).panel_id_hexes
for panel_hex in panel_hexes: for entity_hex in entity_hexes:
if panel_hex in self.DOOR_ITEMS_BY_ID and item_name in self.DOOR_ITEMS_BY_ID[panel_hex]: 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[panel_hex].remove(item_name) self.DOOR_ITEMS_BY_ID[entity_hex].remove(item_name)
if adj_type == "Starting Inventory": if adj_type == "Starting Inventory":
self.STARTING_INVENTORY.add(line) self.STARTING_INVENTORY.add(line)
@ -189,13 +191,13 @@ class WitnessPlayerLogic:
line_split = line.split(" - ") line_split = line.split(" - ")
requirement = { requirement = {
"panels": parse_lambda(line_split[1]), "panels": utils.parse_lambda(line_split[1]),
} }
if len(line_split) > 2: 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 = [ 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 if item_definition.category is ItemCategory.SYMBOL
] ]
required_items = frozenset( required_items = frozenset(
@ -210,21 +212,21 @@ class WitnessPlayerLogic:
return return
if adj_type == "Disabled Locations": 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 return
if adj_type == "Irrelevant Locations": 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 return
if adj_type == "Region Changes": 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] 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"])})) (target_region, frozenset({frozenset(["TrueOneWay"])}))
) )
else: 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)) self.CONNECTIONS_BY_REGION_NAME[source_region].add((target_region, new_lambda))
break break
else: # Execute if loop did not break. TIL this is a thing you can do! 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) self.CONNECTIONS_BY_REGION_NAME[source_region].add(new_conn)
if adj_type == "Added Locations": if adj_type == "Added Locations":
@ -258,7 +260,7 @@ class WitnessPlayerLogic:
self.ADDED_CHECKS.add(line) self.ADDED_CHECKS.add(line)
@staticmethod @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. # 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. # This has a lot of complicated considerations, which I'll try my best to explain.
postgame_adjustments = [] postgame_adjustments = []
@ -285,29 +287,29 @@ class WitnessPlayerLogic:
# Caves & Challenge should never have anything if doors are vanilla - definitionally "post-game" # Caves & Challenge should never have anything if doors are vanilla - definitionally "post-game"
# This is technically imprecise, but it matches player expectations better. # This is technically imprecise, but it matches player expectations better.
if not (early_caves or doors): if not (early_caves or doors):
postgame_adjustments.append(get_caves_exclusion_list()) postgame_adjustments.append(utils.get_caves_exclusion_list())
postgame_adjustments.append(get_beyond_challenge_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 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": if not victory == "challenge":
postgame_adjustments.append(get_path_to_challenge_exclusion_list()) postgame_adjustments.append(utils.get_path_to_challenge_exclusion_list())
postgame_adjustments.append(get_challenge_vault_box_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. # 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 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. # 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): 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": 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") # Mountain can't be reached if the goal is shortbox (or "reverse long box")
if not mountain_enterable_from_top: 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. # Same goes for lower mountain, but that one *can* be reached in remote doors modes.
if not doors: 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) # 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. # 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. # 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 are vanilla, Bottom Floor Discard locks a door to an area, which has to be disabled as well.
if doors: if doors:
postgame_adjustments.append(get_bottom_floor_discard_exclusion_list()) postgame_adjustments.append(utils.get_bottom_floor_discard_exclusion_list())
else: 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, # 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". # 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. # 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: 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 we have a proper short box goal, long box will never be activated first.
if proper_shortbox_goal: if proper_shortbox_goal:
@ -335,7 +337,7 @@ class WitnessPlayerLogic:
return postgame_adjustments return postgame_adjustments
def make_options_adjustments(self, world: "WitnessWorld"): def make_options_adjustments(self, world: "WitnessWorld") -> None:
"""Makes logic adjustments based on options""" """Makes logic adjustments based on options"""
adjustment_linesets_in_order = [] adjustment_linesets_in_order = []
@ -356,15 +358,15 @@ class WitnessPlayerLogic:
# In disable_non_randomized, the discards are needed for alternate activation triggers, UNLESS both # In disable_non_randomized, the discards are needed for alternate activation triggers, UNLESS both
# (remote) doors and lasers are shuffled. # (remote) doors and lasers are shuffled.
if not world.options.disable_non_randomized_puzzles or (doors and lasers): 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: 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: 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": 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 # Victory Condition
@ -387,54 +389,54 @@ class WitnessPlayerLogic:
]) ])
if world.options.disable_non_randomized_puzzles: 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: 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": 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": 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.door_groupings == "regional":
if world.options.shuffle_doors == "panels": 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": 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": elif world.options.shuffle_doors == "mixed":
adjustment_linesets_in_order.append(get_simple_doors()) adjustment_linesets_in_order.append(utils.get_simple_doors())
adjustment_linesets_in_order.append(get_simple_additional_panels()) adjustment_linesets_in_order.append(utils.get_simple_additional_panels())
else: else:
if world.options.shuffle_doors == "panels": if world.options.shuffle_doors == "panels":
adjustment_linesets_in_order.append(get_complex_door_panels()) adjustment_linesets_in_order.append(utils.get_complex_door_panels())
adjustment_linesets_in_order.append(get_complex_additional_panels()) adjustment_linesets_in_order.append(utils.get_complex_additional_panels())
elif world.options.shuffle_doors == "doors": 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": elif world.options.shuffle_doors == "mixed":
adjustment_linesets_in_order.append(get_complex_doors()) adjustment_linesets_in_order.append(utils.get_complex_doors())
adjustment_linesets_in_order.append(get_complex_additional_panels()) adjustment_linesets_in_order.append(utils.get_complex_additional_panels())
if world.options.shuffle_boat: 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": 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: 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: 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: for item in self.YAML_ADDED_ITEMS:
adjustment_linesets_in_order.append(["Items:", item]) adjustment_linesets_in_order.append(["Items:", item])
if lasers: 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: 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": 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() 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"] ep_name = self.REFERENCE_LOGIC.ENTITIES_BY_HEX[ep_hex]["checkName"]
self.ALWAYS_EVENT_NAMES_BY_HEX[ep_hex] = f"{obelisk_name} - {ep_name}" self.ALWAYS_EVENT_NAMES_BY_HEX[ep_hex] = f"{obelisk_name} - {ep_name}"
else: 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: 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: for yaml_disabled_location in self.YAML_DISABLED_LOCATIONS:
if yaml_disabled_location not in self.REFERENCE_LOGIC.ENTITIES_BY_NAME: 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: if entity_id in self.DOOR_ITEMS_BY_ID:
del self.DOOR_ITEMS_BY_ID[entity_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 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: for item in self.PROG_ITEMS_ACTUALLY_IN_THE_GAME_NO_MULTI:
if item not in self.THEORETICAL_ITEMS: 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) self.PROG_ITEMS_ACTUALLY_IN_THE_GAME.add(progressive_item_name)
child_items = cast(ProgressiveItemDefinition, 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 multi_list = [child_item for child_item in child_items
if child_item in self.PROG_ITEMS_ACTUALLY_IN_THE_GAME_NO_MULTI] if child_item in self.PROG_ITEMS_ACTUALLY_IN_THE_GAME_NO_MULTI]
self.MULTI_AMOUNTS[item] = multi_list.index(item) + 1 self.MULTI_AMOUNTS[item] = multi_list.index(item) + 1
@ -520,24 +522,24 @@ class WitnessPlayerLogic:
if self.REFERENCE_LOGIC.ENTITIES_BY_HEX[entity]["region"]: if self.REFERENCE_LOGIC.ENTITIES_BY_HEX[entity]["region"]:
region_name = self.REFERENCE_LOGIC.ENTITIES_BY_HEX[entity]["region"]["name"] 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) 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)) new_connections.append((connection[0], overall_requirement))
self.CONNECTIONS_BY_REGION_NAME[region] = new_connections 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 ( return not (
entity_hex in self.ENTITIES_WITHOUT_ENSURED_SOLVABILITY entity_hex in self.ENTITIES_WITHOUT_ENSURED_SOLVABILITY
or entity_hex in self.COMPLETELY_DISABLED_ENTITIES or entity_hex in self.COMPLETELY_DISABLED_ENTITIES
or entity_hex in self.IRRELEVANT_BUT_NOT_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""" """Figure out which major items are actually useless in this world's settings"""
# Gather quick references to relevant options # 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 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 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 name = self.REFERENCE_LOGIC.ENTITIES_BY_HEX[panel]["checkName"] + action
if panel not in self.USED_EVENT_NAMES_BY_HEX: 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" self.USED_EVENT_NAMES_BY_HEX[panel] = name + " Event"
pair = (name, self.USED_EVENT_NAMES_BY_HEX[panel]) pair = (name, self.USED_EVENT_NAMES_BY_HEX[panel])
return pair 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.ALWAYS_EVENT_NAMES_BY_HEX[self.VICTORY_LOCATION] = "Victory"
self.USED_EVENT_NAMES_BY_HEX.update(self.ALWAYS_EVENT_NAMES_BY_HEX) 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) pair = self.make_event_item_pair(panel)
self.EVENT_ITEM_PAIRS[pair[0]] = pair[1] 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_DISABLED_LOCATIONS = disabled_locations
self.YAML_ADDED_ITEMS = start_inv self.YAML_ADDED_ITEMS = start_inv
@ -646,11 +648,11 @@ class WitnessPlayerLogic:
self.DIFFICULTY = world.options.puzzle_randomization self.DIFFICULTY = world.options.puzzle_randomization
if self.DIFFICULTY == "sigma_normal": if self.DIFFICULTY == "sigma_normal":
self.REFERENCE_LOGIC = StaticWitnessLogic.sigma_normal self.REFERENCE_LOGIC = static_witness_logic.sigma_normal
elif self.DIFFICULTY == "sigma_expert": elif self.DIFFICULTY == "sigma_expert":
self.REFERENCE_LOGIC = StaticWitnessLogic.sigma_expert self.REFERENCE_LOGIC = static_witness_logic.sigma_expert
elif self.DIFFICULTY == "none": 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.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) 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, Defines Region for The Witness, assigns locations to them,
and connects them with the proper requirements 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 BaseClasses import Entrance, Region
from Utils import KeyedDefaultDict
from .static_logic import StaticWitnessLogic from worlds.generic.Rules import CollectionRule
from .locations import WitnessPlayerLocations, StaticWitnessLocations
from .data import static_logic as static_witness_logic
from .locations import WitnessPlayerLocations, static_witness_locations
from .player_logic import WitnessPlayerLogic from .player_logic import WitnessPlayerLogic
if TYPE_CHECKING: if TYPE_CHECKING:
from . import WitnessWorld from . import WitnessWorld
class WitnessRegions: class WitnessPlayerRegions:
"""Class that defines Witness Regions""" """Class that defines Witness Regions"""
locat = None player_locations = None
logic = None logic = None
@staticmethod @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 from .rules import _meets_item_requirements
""" """
@ -82,7 +85,7 @@ class WitnessRegions:
for dependent_region in mentioned_regions: for dependent_region in mentioned_regions:
world.multiworld.register_indirect_condition(regions_by_name[dependent_region], connection) 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 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(): for region_name, region in self.reference_logic.ALL_REGIONS_BY_NAME.items():
locations_for_this_region = [ locations_for_this_region = [
self.reference_logic.ENTITIES_BY_HEX[panel]["checkName"] for panel in region["panels"] 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 += [ locations_for_this_region += [
StaticWitnessLocations.get_event_name(panel) for panel in region["panels"] static_witness_locations.get_event_name(panel) for panel in region["panels"]
if StaticWitnessLocations.get_event_name(panel) in self.locat.EVENT_LOCATION_TABLE if static_witness_locations.get_event_name(panel) in self.player_locations.EVENT_LOCATION_TABLE
] ]
all_locations = all_locations | set(locations_for_this_region) 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 regions_by_name[region_name] = new_region
@ -133,16 +137,16 @@ class WitnessRegions:
world.multiworld.regions += self.created_regions.values() 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 difficulty = world.options.puzzle_randomization
if difficulty == "sigma_normal": if difficulty == "sigma_normal":
self.reference_logic = StaticWitnessLogic.sigma_normal self.reference_logic = static_witness_logic.sigma_normal
elif difficulty == "sigma_expert": elif difficulty == "sigma_expert":
self.reference_logic = StaticWitnessLogic.sigma_expert self.reference_logic = static_witness_logic.sigma_expert
elif difficulty == "none": elif difficulty == "none":
self.reference_logic = StaticWitnessLogic.vanilla self.reference_logic = static_witness_logic.vanilla
self.locat = locat self.player_locations = player_locations
self.created_entrances: Dict[Tuple[str, str], List[Entrance]] = KeyedDefaultDict(lambda _: []) self.created_entrances: Dict[Tuple[str, str], List[Entrance]] = defaultdict(lambda: [])
self.created_regions: Dict[str, Region] = dict() 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 depending on the items received
""" """
from typing import TYPE_CHECKING, Callable, FrozenSet from typing import TYPE_CHECKING, FrozenSet
from BaseClasses import CollectionState 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 .locations import WitnessPlayerLocations
from . import StaticWitnessLogic, WitnessRegions from .player_logic import WitnessPlayerLogic
from worlds.generic.Rules import set_rule
if TYPE_CHECKING: if TYPE_CHECKING:
from . import WitnessWorld from . import WitnessWorld
@ -30,17 +33,17 @@ laser_hexes = [
def _has_laser(laser_hex: str, world: "WitnessWorld", player: int, 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: if laser_hex == "0x012FB" and redirect_required:
return lambda state: ( 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) and state.has("Desert Laser Redirection", player)
) )
else: 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 = [] laser_lambdas = []
for laser_hex in laser_hexes: 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, 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 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] panel_obj = player_logic.REFERENCE_LOGIC.ENTITIES_BY_HEX[panel]
entity_name = panel_obj["checkName"] 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) return lambda state: state.has(player_logic.EVENT_ITEM_PAIRS[entity_name + " Solved"], player)
else: else:
return make_lambda(panel, world) return make_lambda(panel, world)
def _can_move_either_direction(state: CollectionState, source: str, target: str, regio: WitnessRegions) -> bool: def _can_move_either_direction(state: CollectionState, source: str, target: str,
entrance_forward = regio.created_entrances[source, target] player_regions: WitnessPlayerRegions) -> bool:
entrance_backward = regio.created_entrances[target, source] entrance_forward = player_regions.created_entrances[source, target]
entrance_backward = player_regions.created_entrances[target, source]
return ( return (
any(entrance.can_reach(state) for entrance in entrance_forward) 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 player = world.player
hedge_2_access = ( 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 = ( hedge_3_access = (
_can_move_either_direction(state, "Keep 3rd Maze", "Keep", world.regio) _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.regio) or _can_move_either_direction(state, "Keep 3rd Maze", "Keep 2nd Maze", world.player_regions)
and hedge_2_access and hedge_2_access
) )
hedge_4_access = ( hedge_4_access = (
_can_move_either_direction(state, "Keep 4th Maze", "Keep", world.regio) _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.regio) or _can_move_either_direction(state, "Keep 4th Maze", "Keep 3rd Maze", world.player_regions)
and hedge_3_access and hedge_3_access
) )
hedge_access = ( hedge_access = (
_can_move_either_direction(state, "Keep 4th Maze", "Keep Tower", world.regio) _can_move_either_direction(state, "Keep 4th Maze", "Keep Tower", world.player_regions)
and state.can_reach("Keep", "Region", player) and state.can_reach("Keep", "Region", player)
and hedge_4_access and hedge_4_access
) )
backwards_to_fourth = ( backwards_to_fourth = (
state.can_reach("Keep", "Region", player) 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 4th Pressure Plate", "Keep Tower", world.player_regions)
and ( and (
_can_move_either_direction(state, "Keep", "Keep Tower", world.regio) _can_move_either_direction(state, "Keep", "Keep Tower", world.player_regions)
or hedge_access or hedge_access
) )
) )
shadows_shortcut = ( shadows_shortcut = (
state.can_reach("Main Island", "Region", player) state.can_reach("Main Island", "Region", player)
and _can_move_either_direction(state, "Keep 4th Pressure Plate", "Shadows", world.regio) and _can_move_either_direction(state, "Keep 4th Pressure Plate", "Shadows", world.player_regions)
) )
backwards_access = ( backwards_access = (
_can_move_either_direction(state, "Keep 3rd Pressure Plate", "Keep 4th Pressure Plate", world.regio) _can_move_either_direction(state, "Keep 3rd Pressure Plate", "Keep 4th Pressure Plate", world.player_regions)
and (backwards_to_fourth or shadows_shortcut) and (backwards_to_fourth or shadows_shortcut)
) )
front_access = ( front_access = (
_can_move_either_direction(state, "Keep 2nd Pressure Plate", "Keep", world.regio) _can_move_either_direction(state, "Keep 2nd Pressure Plate", "Keep", world.player_regions)
and state.can_reach("Keep", "Region", player) and state.can_reach("Keep", "Region", player)
) )
return front_access and backwards_access 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: def _can_do_theater_to_tunnels(state: CollectionState, world: "WitnessWorld") -> bool:
direct_access = ( direct_access = (
_can_move_either_direction(state, "Tunnels", "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.regio) and _can_move_either_direction(state, "Theater", "Windmill Interior", world.player_regions)
) )
theater_from_town = ( theater_from_town = (
_can_move_either_direction(state, "Town", "Windmill Interior", world.regio) _can_move_either_direction(state, "Town", "Windmill Interior", world.player_regions)
and _can_move_either_direction(state, "Theater", "Windmill Interior", world.regio) and _can_move_either_direction(state, "Theater", "Windmill Interior", world.player_regions)
or _can_move_either_direction(state, "Town", "Theater", world.regio) or _can_move_either_direction(state, "Town", "Theater", world.player_regions)
) )
tunnels_from_town = ( tunnels_from_town = (
_can_move_either_direction(state, "Tunnels", "Windmill Interior", world.regio) _can_move_either_direction(state, "Tunnels", "Windmill Interior", world.player_regions)
and _can_move_either_direction(state, "Town", "Windmill Interior", world.regio) and _can_move_either_direction(state, "Town", "Windmill Interior", world.player_regions)
or _can_move_either_direction(state, "Tunnels", "Town", world.regio) or _can_move_either_direction(state, "Tunnels", "Town", world.player_regions)
) )
return direct_access or theater_from_town and tunnels_from_town return direct_access or theater_from_town and tunnels_from_town
def _has_item(item: str, world: "WitnessWorld", player: int, 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: if item in player_logic.REFERENCE_LOGIC.ALL_REGIONS_BY_NAME:
return lambda state: state.can_reach(item, "Region", player) return lambda state: state.can_reach(item, "Region", player)
if item == "7 Lasers": if item == "7 Lasers":
@ -171,21 +175,21 @@ def _has_item(item: str, world: "WitnessWorld", player: int,
elif item == "Theater to Tunnels": elif item == "Theater to Tunnels":
return lambda state: _can_do_theater_to_tunnels(state, world) return lambda state: _can_do_theater_to_tunnels(state, world)
if item in player_logic.USED_EVENT_NAMES_BY_HEX: 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]) return lambda state: state.has(prog_item, player, player_logic.MULTI_AMOUNTS[item])
def _meets_item_requirements(requirements: FrozenSet[FrozenSet[str]], 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 Checks whether item and panel requirements are met for
a panel a panel
""" """
lambda_conversion = [ 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 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 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) return _meets_item_requirements(entity_req, world)
def set_rules(world: "WitnessWorld"): def set_rules(world: "WitnessWorld") -> None:
""" """
Sets all rules for all locations 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 real_location = location
if location in world.locat.EVENT_LOCATION_TABLE: if location in world.player_locations.EVENT_LOCATION_TABLE:
real_location = location[:-7] real_location = location[:-7]
associated_entity = world.player_logic.REFERENCE_LOGIC.ENTITIES_BY_NAME[real_location] 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) rule = make_lambda(entity_hex, world)
location = world.multiworld.get_location(location, world.player) location = world.get_location(location)
set_rule(location, rule) 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)