Skip to content

Commit

Permalink
The Witness: The big dumb refactor (ArchipelagoMW#3007)
Browse files Browse the repository at this point in the history
  • Loading branch information
NewSoupVi authored and EmilyV99 committed Apr 15, 2024
1 parent bbd8fc1 commit cf91daf
Show file tree
Hide file tree
Showing 48 changed files with 1,060 additions and 1,021 deletions.
166 changes: 88 additions & 78 deletions worlds/witness/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,26 @@
Archipelago init file for The Witness
"""
import dataclasses
from logging import error, warning
from typing import Any, Dict, List, Optional, cast

from BaseClasses import CollectionState, Entrance, Location, Region, Tutorial

from typing import Dict, Optional, cast
from BaseClasses import Region, Location, MultiWorld, Item, Entrance, Tutorial, CollectionState
from Options import PerGameCommonOptions, Toggle
from .presets import witness_option_presets
from worlds.AutoWorld import World, WebWorld
from worlds.AutoWorld import WebWorld, World

from .data import static_items as static_witness_items
from .data import static_logic as static_witness_logic
from .data.item_definition_classes import DoorItemDefinition, ItemData
from .data.utils import get_audio_logs
from .hints import CompactItemData, create_all_hints, generate_joke_hints, make_compact_hint_data, make_laser_hints
from .locations import WitnessPlayerLocations, static_witness_locations
from .options import TheWitnessOptions
from .player_items import WitnessItem, WitnessPlayerItems
from .player_logic import WitnessPlayerLogic
from .static_logic import StaticWitnessLogic, ItemCategory, DoorItemDefinition
from .hints import get_always_hint_locations, get_always_hint_items, get_priority_hint_locations, \
get_priority_hint_items, make_always_and_priority_hints, generate_joke_hints, make_area_hints, get_hintable_areas, \
make_extra_location_hints, create_all_hints, make_laser_hints, make_compact_hint_data, CompactItemData
from .locations import WitnessPlayerLocations, StaticWitnessLocations
from .items import WitnessItem, StaticWitnessItems, WitnessPlayerItems, ItemData
from .regions import WitnessRegions
from .presets import witness_option_presets
from .regions import WitnessPlayerRegions
from .rules import set_rules
from .options import TheWitnessOptions
from .utils import get_audio_logs, get_laser_shuffle
from logging import warning, error


class WitnessWebWorld(WebWorld):
Expand Down Expand Up @@ -50,46 +52,43 @@ class WitnessWorld(World):
options: TheWitnessOptions

item_name_to_id = {
name: data.ap_code for name, data in StaticWitnessItems.item_data.items()
name: data.ap_code for name, data in static_witness_items.ITEM_DATA.items()
}
location_name_to_id = StaticWitnessLocations.ALL_LOCATIONS_TO_ID
item_name_groups = StaticWitnessItems.item_groups
location_name_groups = StaticWitnessLocations.AREA_LOCATION_GROUPS
location_name_to_id = static_witness_locations.ALL_LOCATIONS_TO_ID
item_name_groups = static_witness_items.ITEM_GROUPS
location_name_groups = static_witness_locations.AREA_LOCATION_GROUPS

required_client_version = (0, 4, 5)

def __init__(self, multiworld: "MultiWorld", player: int):
super().__init__(multiworld, player)
player_logic: WitnessPlayerLogic
player_locations: WitnessPlayerLocations
player_items: WitnessPlayerItems
player_regions: WitnessPlayerRegions

self.player_logic = None
self.locat = None
self.items = None
self.regio = None
log_ids_to_hints: Dict[int, CompactItemData]
laser_ids_to_hints: Dict[int, CompactItemData]

self.log_ids_to_hints: Dict[int, CompactItemData] = dict()
self.laser_ids_to_hints: Dict[int, CompactItemData] = dict()

self.items_placed_early = []
self.own_itempool = []
items_placed_early: List[str]
own_itempool: List[WitnessItem]

def _get_slot_data(self):
def _get_slot_data(self) -> Dict[str, Any]:
return {
'seed': self.random.randrange(0, 1000000),
'victory_location': int(self.player_logic.VICTORY_LOCATION, 16),
'panelhex_to_id': self.locat.CHECK_PANELHEX_TO_ID,
'item_id_to_door_hexes': StaticWitnessItems.get_item_to_door_mappings(),
'door_hexes_in_the_pool': self.items.get_door_ids_in_pool(),
'symbols_not_in_the_game': self.items.get_symbol_ids_not_in_pool(),
'disabled_entities': [int(h, 16) for h in self.player_logic.COMPLETELY_DISABLED_ENTITIES],
'log_ids_to_hints': self.log_ids_to_hints,
'laser_ids_to_hints': self.laser_ids_to_hints,
'progressive_item_lists': self.items.get_progressive_item_ids_in_pool(),
'obelisk_side_id_to_EPs': StaticWitnessLogic.OBELISK_SIDE_ID_TO_EP_HEXES,
'precompleted_puzzles': [int(h, 16) for h in self.player_logic.EXCLUDED_LOCATIONS],
'entity_to_name': StaticWitnessLogic.ENTITY_ID_TO_NAME,
"seed": self.random.randrange(0, 1000000),
"victory_location": int(self.player_logic.VICTORY_LOCATION, 16),
"panelhex_to_id": self.player_locations.CHECK_PANELHEX_TO_ID,
"item_id_to_door_hexes": static_witness_items.get_item_to_door_mappings(),
"door_hexes_in_the_pool": self.player_items.get_door_ids_in_pool(),
"symbols_not_in_the_game": self.player_items.get_symbol_ids_not_in_pool(),
"disabled_entities": [int(h, 16) for h in self.player_logic.COMPLETELY_DISABLED_ENTITIES],
"log_ids_to_hints": self.log_ids_to_hints,
"laser_ids_to_hints": self.laser_ids_to_hints,
"progressive_item_lists": self.player_items.get_progressive_item_ids_in_pool(),
"obelisk_side_id_to_EPs": static_witness_logic.OBELISK_SIDE_ID_TO_EP_HEXES,
"precompleted_puzzles": [int(h, 16) for h in self.player_logic.EXCLUDED_LOCATIONS],
"entity_to_name": static_witness_logic.ENTITY_ID_TO_NAME,
}

def determine_sufficient_progression(self):
def determine_sufficient_progression(self) -> None:
"""
Determine whether there are enough progression items in this world to consider it "interactive".
In the case of singleplayer, this just outputs a warning.
Expand Down Expand Up @@ -127,20 +126,20 @@ def determine_sufficient_progression(self):
elif not interacts_sufficiently_with_multiworld and self.multiworld.players > 1:
raise Exception(f"{self.multiworld.get_player_name(self.player)}'s Witness world doesn't have enough"
f" progression items that can be placed in other players' worlds. Please turn on Symbol"
f" Shuffle, Door Shuffle or Obelisk Keys.")
f" Shuffle, Door Shuffle, or Obelisk Keys.")

def generate_early(self):
def generate_early(self) -> None:
disabled_locations = self.options.exclude_locations.value

self.player_logic = WitnessPlayerLogic(
self, disabled_locations, self.options.start_inventory.value
)

self.locat: WitnessPlayerLocations = WitnessPlayerLocations(self, self.player_logic)
self.items: WitnessPlayerItems = WitnessPlayerItems(
self, self.player_logic, self.locat
self.player_locations: WitnessPlayerLocations = WitnessPlayerLocations(self, self.player_logic)
self.player_items: WitnessPlayerItems = WitnessPlayerItems(
self, self.player_logic, self.player_locations
)
self.regio: WitnessRegions = WitnessRegions(self.locat, self)
self.player_regions: WitnessPlayerRegions = WitnessPlayerRegions(self.player_locations, self)

self.log_ids_to_hints = dict()

Expand All @@ -149,37 +148,44 @@ def generate_early(self):
if self.options.shuffle_lasers == "local":
self.options.local_items.value |= self.item_name_groups["Lasers"]

def create_regions(self):
self.regio.create_regions(self, self.player_logic)
def create_regions(self) -> None:
self.player_regions.create_regions(self, self.player_logic)

# Set rules early so extra locations can be created based on the results of exploring collection states

set_rules(self)

# Start creating items

self.items_placed_early = []
self.own_itempool = []

# Add event items and tie them to event locations (e.g. laser activations).

event_locations = []

for event_location in self.locat.EVENT_LOCATION_TABLE:
for event_location in self.player_locations.EVENT_LOCATION_TABLE:
item_obj = self.create_item(
self.player_logic.EVENT_ITEM_PAIRS[event_location]
)
location_obj = self.multiworld.get_location(event_location, self.player)
location_obj = self.get_location(event_location)
location_obj.place_locked_item(item_obj)
self.own_itempool.append(item_obj)

event_locations.append(location_obj)

# Place other locked items
dog_puzzle_skip = self.create_item("Puzzle Skip")
self.multiworld.get_location("Town Pet the Dog", self.player).place_locked_item(dog_puzzle_skip)
self.get_location("Town Pet the Dog").place_locked_item(dog_puzzle_skip)

self.own_itempool.append(dog_puzzle_skip)

self.items_placed_early.append("Puzzle Skip")

# Pick an early item to place on the tutorial gate.
early_items = [item for item in self.items.get_early_items() if item in self.items.get_mandatory_items()]
early_items = [
item for item in self.player_items.get_early_items() if item in self.player_items.get_mandatory_items()
]
if early_items:
random_early_item = self.random.choice(early_items)
if self.options.puzzle_randomization == "sigma_expert":
Expand All @@ -188,7 +194,7 @@ def create_regions(self):
else:
# Force the item onto the tutorial gate check and remove it from our random pool.
gate_item = self.create_item(random_early_item)
self.multiworld.get_location("Tutorial Gate Open", self.player).place_locked_item(gate_item)
self.get_location("Tutorial Gate Open").place_locked_item(gate_item)
self.own_itempool.append(gate_item)
self.items_placed_early.append(random_early_item)

Expand Down Expand Up @@ -223,19 +229,19 @@ def create_regions(self):
break

region, loc = extra_checks.pop(0)
self.locat.add_location_late(loc)
self.multiworld.get_region(region, self.player).add_locations({loc: self.location_name_to_id[loc]})
self.player_locations.add_location_late(loc)
self.get_region(region).add_locations({loc: self.location_name_to_id[loc]})

player = self.multiworld.get_player_name(self.player)

warning(f"""Location "{loc}" had to be added to {player}'s world due to insufficient sphere 1 size.""")

def create_items(self):
def create_items(self) -> None:
# Determine pool size.
pool_size: int = len(self.locat.CHECK_LOCATION_TABLE) - len(self.locat.EVENT_LOCATION_TABLE)
pool_size = len(self.player_locations.CHECK_LOCATION_TABLE) - len(self.player_locations.EVENT_LOCATION_TABLE)

# Fill mandatory items and remove precollected and/or starting items from the pool.
item_pool: Dict[str, int] = self.items.get_mandatory_items()
item_pool = self.player_items.get_mandatory_items()

# Remove one copy of each item that was placed early
for already_placed in self.items_placed_early:
Expand Down Expand Up @@ -283,27 +289,30 @@ def create_items(self):

# Add junk items.
if remaining_item_slots > 0:
item_pool.update(self.items.get_filler_items(remaining_item_slots))
item_pool.update(self.player_items.get_filler_items(remaining_item_slots))

# Generate the actual items.
for item_name, quantity in sorted(item_pool.items()):
new_items = [self.create_item(item_name) for _ in range(0, quantity)]

self.own_itempool += new_items
self.multiworld.itempool += new_items
if self.items.item_data[item_name].local_only:
if self.player_items.item_data[item_name].local_only:
self.options.local_items.value.add(item_name)

def fill_slot_data(self) -> dict:
self.log_ids_to_hints: Dict[int, CompactItemData] = dict()
self.laser_ids_to_hints: Dict[int, CompactItemData] = dict()

already_hinted_locations = set()

# Laser hints

if self.options.laser_hints:
laser_hints = make_laser_hints(self, StaticWitnessItems.item_groups["Lasers"])
laser_hints = make_laser_hints(self, static_witness_items.ITEM_GROUPS["Lasers"])

for item_name, hint in laser_hints.items():
item_def = cast(DoorItemDefinition, StaticWitnessLogic.all_items[item_name])
item_def = cast(DoorItemDefinition, static_witness_logic.ALL_ITEMS[item_name])
self.laser_ids_to_hints[int(item_def.panel_id_hexes[0], 16)] = make_compact_hint_data(hint, self.player)
already_hinted_locations.add(hint.location)

Expand Down Expand Up @@ -356,18 +365,18 @@ def fill_slot_data(self) -> dict:

return slot_data

def create_item(self, item_name: str) -> Item:
def create_item(self, item_name: str) -> WitnessItem:
# If the player's plando options are malformed, the item_name parameter could be a dictionary containing the
# name of the item, rather than the item itself. This is a workaround to prevent a crash.
if type(item_name) is dict:
item_name = list(item_name.keys())[0]
if isinstance(item_name, dict):
item_name = next(iter(item_name))

# this conditional is purely for unit tests, which need to be able to create an item before generate_early
item_data: ItemData
if hasattr(self, 'items') and self.items and item_name in self.items.item_data:
item_data = self.items.item_data[item_name]
if hasattr(self, "player_items") and self.player_items and item_name in self.player_items.item_data:
item_data = self.player_items.item_data[item_name]
else:
item_data = StaticWitnessItems.item_data[item_name]
item_data = static_witness_items.ITEM_DATA[item_name]

return WitnessItem(item_name, item_data.classification, item_data.ap_code, player=self.player)

Expand All @@ -382,25 +391,26 @@ class WitnessLocation(Location):
game: str = "The Witness"
entity_hex: int = -1

def __init__(self, player: int, name: str, address: Optional[int], parent, ch_hex: int = -1):
def __init__(self, player: int, name: str, address: Optional[int], parent, ch_hex: int = -1) -> None:
super().__init__(player, name, address, parent)
self.entity_hex = ch_hex


def create_region(world: WitnessWorld, name: str, locat: WitnessPlayerLocations, region_locations=None, exits=None):
def create_region(world: WitnessWorld, name: str, player_locations: WitnessPlayerLocations,
region_locations=None, exits=None) -> Region:
"""
Create an Archipelago Region for The Witness
"""

ret = Region(name, world.player, world.multiworld)
if region_locations:
for location in region_locations:
loc_id = locat.CHECK_LOCATION_TABLE[location]
loc_id = player_locations.CHECK_LOCATION_TABLE[location]

entity_hex = -1
if location in StaticWitnessLogic.ENTITIES_BY_NAME:
if location in static_witness_logic.ENTITIES_BY_NAME:
entity_hex = int(
StaticWitnessLogic.ENTITIES_BY_NAME[location]["entity_hex"], 0
static_witness_logic.ENTITIES_BY_NAME[location]["entity_hex"], 0
)
location = WitnessLocation(
world.player, location, loc_id, ret, entity_hex
Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
Empty file added worlds/witness/data/__init__.py
Empty file.
59 changes: 59 additions & 0 deletions worlds/witness/data/item_definition_classes.py
Original file line number Diff line number Diff line change
@@ -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
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
Loading

0 comments on commit cf91daf

Please sign in to comment.