mirror of
https://github.com/ArchipelagoMW/Archipelago.git
synced 2026-04-18 13:13:30 -07:00
The Witness: Make "Early Good Item" an OptionSet allowing Symbols, Doors and Obelisk Keys (#3804)
* New solution to that plando issue * better * Another warning * better comment * Best of both worlds I guess? * oops * Smarter code reuse * better comment * oop * lint * mypy * player_name * add unit test * oh * Rebrand time baby * This fits on one line * oop * Update worlds/witness/__init__.py Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * mypy * Reorganize some doors according to medic's suggestions * This should make it much faster (Thanks Medic) * Town Doors works because of Church being a check always, Church Entry feels really bad tho * Only add Desert Entry if there are no control panels stopping you * No overlap here, so why is it a set * Idk everything's kinda good without symbol shuffle * Just make sure, I guess * This makes way more sense doesn't it * Oh, this is probably important * oop * loc * oops 2 * ruff * that was already in there * Change the door picking a bit further * some renaming * slight wording change * Fix * Move it all to a new file * ruff * mypy * . * Make sure we're only adding as many tutorial checks as necessary * ruff * These checks aren't necessary, as the final list gets culled to only existing items anyway. It saves CPU cycles, but this is nicer for future compatibility * Special handling for caves shortcuts * 120 chars * Update worlds/witness/place_early_item.py Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * Clean up Windmill & Theater cases * Make early_symbol_item removed instead * Add early_good_item to presets * replace double None checks with casts * That doesn't exist anymore * Mypy thing * Update the doors again a bit * Pycharm pls * ruff * forgot one * oop * Is it finally right? * Update options.py * Fix with new Panel Keys * Hopefully fix crash when one of the types runs out when the others haven't yet * oops * Medic suggestion * unused import * Update place_early_item.py * Update __init__.py * Update __init__.py * Update options.py * Add possible types to option desc * Make that include all Tutorial (Inside) checks * Update __init__.py * Update test_early_good_item.py * Update test_early_good_item.py * Slight cleanup * fix no tutorial locations being picked up by tutorial location size check --------- Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
This commit is contained in:
@@ -2,10 +2,10 @@
|
||||
Archipelago init file for The Witness
|
||||
"""
|
||||
import dataclasses
|
||||
from logging import error, warning
|
||||
from logging import error, info, warning
|
||||
from typing import Any, Dict, List, Optional, cast
|
||||
|
||||
from BaseClasses import CollectionState, Entrance, Location, LocationProgressType, Region, Tutorial
|
||||
from BaseClasses import CollectionState, Entrance, Item, Location, LocationProgressType, Region, Tutorial
|
||||
|
||||
from Options import OptionError, PerGameCommonOptions, Toggle
|
||||
from worlds.AutoWorld import WebWorld, World
|
||||
@@ -18,6 +18,7 @@ from .data.utils import cast_not_none, get_audio_logs
|
||||
from .hints import CompactHintData, create_all_hints, make_compact_hint_data, make_laser_hints
|
||||
from .locations import WitnessPlayerLocations
|
||||
from .options import TheWitnessOptions, witness_option_groups
|
||||
from .place_early_item import place_early_items
|
||||
from .player_items import WitnessItem, WitnessPlayerItems
|
||||
from .player_logic import WitnessPlayerLogic
|
||||
from .presets import witness_option_presets
|
||||
@@ -94,6 +95,7 @@ class WitnessWorld(World):
|
||||
items_placed_early: List[str]
|
||||
own_itempool: List[WitnessItem]
|
||||
|
||||
reachable_early_locations: List[str]
|
||||
panel_hunt_required_count: int
|
||||
|
||||
def _get_slot_data(self) -> Dict[str, Any]:
|
||||
@@ -218,25 +220,6 @@ class WitnessWorld(World):
|
||||
self.own_itempool.append(dog_puzzle_skip)
|
||||
self.items_placed_early.append("Puzzle Skip")
|
||||
|
||||
if self.options.early_symbol_item:
|
||||
# Pick an early item to place on the tutorial gate.
|
||||
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)
|
||||
mode = self.options.puzzle_randomization
|
||||
if mode == "sigma_expert" or mode == "umbra_variety" or self.options.victory_condition == "panel_hunt":
|
||||
# In Expert and Variety, only tag the item as early, rather than forcing it onto the gate.
|
||||
# Same with panel hunt, since the Tutorial Gate Open panel is used for something else
|
||||
self.multiworld.local_early_items[self.player][random_early_item] = 1
|
||||
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.get_location("Tutorial Gate Open").place_locked_item(gate_item)
|
||||
self.own_itempool.append(gate_item)
|
||||
self.items_placed_early.append(random_early_item)
|
||||
|
||||
# There are some really restrictive options in The Witness.
|
||||
# They are rarely played, but when they are, we add some extra sphere 1 locations.
|
||||
# This is done both to prevent generation failures, but also to make the early game less linear.
|
||||
@@ -245,20 +228,39 @@ class WitnessWorld(World):
|
||||
state = CollectionState(self.multiworld)
|
||||
state.sweep_for_advancements(locations=event_locations)
|
||||
|
||||
num_early_locs = sum(
|
||||
1 for loc in self.multiworld.get_reachable_locations(state, self.player)
|
||||
if loc.address and not loc.item
|
||||
# Adjust the needed size for sphere 1 based on how restrictive the options are in terms of items
|
||||
|
||||
early_locations = [
|
||||
location for location in self.multiworld.get_reachable_locations(state, self.player)
|
||||
if not location.is_event and not location.item
|
||||
]
|
||||
self.reachable_early_locations = [location.name for location in early_locations]
|
||||
|
||||
num_reachable_tutorial_locations = sum(
|
||||
static_witness_logic.ALL_REGIONS_BY_NAME[
|
||||
cast(Region, location.parent_region).name
|
||||
].area.name == "Tutorial (Inside)"
|
||||
for location in early_locations
|
||||
)
|
||||
|
||||
# Adjust the needed size for sphere 1 based on how restrictive the options are in terms of items
|
||||
|
||||
needed_size = 2
|
||||
needed_size += self.options.puzzle_randomization == "sigma_expert"
|
||||
needed_size += self.options.shuffle_symbols
|
||||
needed_size += self.options.shuffle_doors != "off"
|
||||
needed_size_overall = 2
|
||||
needed_size_overall += self.options.puzzle_randomization == "sigma_expert"
|
||||
needed_size_overall += self.options.shuffle_symbols
|
||||
needed_size_overall += self.options.shuffle_doors != "off"
|
||||
|
||||
needed_size_to_hold_tutorial_items = len(
|
||||
self.player_items.get_early_items(set(self.player_items.get_mandatory_items()))
|
||||
)
|
||||
|
||||
# Then, add checks in order until the required amount of sphere 1 checks is met.
|
||||
|
||||
extra_tutorial_checks = [
|
||||
("Tutorial First Hallway Room", "Tutorial First Hallway Bend"),
|
||||
("Tutorial First Hallway", "Tutorial First Hallway Straight")
|
||||
]
|
||||
|
||||
extra_checks = [
|
||||
("Tutorial First Hallway Room", "Tutorial First Hallway Bend"),
|
||||
("Tutorial First Hallway", "Tutorial First Hallway Straight"),
|
||||
@@ -266,13 +268,30 @@ class WitnessWorld(World):
|
||||
("Desert Outside", "Desert Surface 2"),
|
||||
]
|
||||
|
||||
for i in range(num_early_locs, needed_size):
|
||||
for _ in range(num_reachable_tutorial_locations, needed_size_to_hold_tutorial_items):
|
||||
if not extra_tutorial_checks:
|
||||
break
|
||||
|
||||
region, loc = extra_tutorial_checks.pop(0)
|
||||
extra_checks.pop(0)
|
||||
self.player_locations.add_location_late(loc)
|
||||
self.get_region(region).add_locations({loc: self.location_name_to_id[loc]}, WitnessLocation)
|
||||
self.reachable_early_locations.append(loc)
|
||||
|
||||
player = self.player_name
|
||||
|
||||
info(
|
||||
f"""Location "{loc}" had to be added to {player}'s world to hold the requested early good items."""
|
||||
)
|
||||
|
||||
for _ in range(len(self.reachable_early_locations), needed_size_overall):
|
||||
if not extra_checks:
|
||||
break
|
||||
|
||||
region, loc = extra_checks.pop(0)
|
||||
self.player_locations.add_location_late(loc)
|
||||
self.get_region(region).add_locations({loc: self.location_name_to_id[loc]})
|
||||
self.get_region(region).add_locations({loc: self.location_name_to_id[loc]}, WitnessLocation)
|
||||
self.reachable_early_locations.append(loc)
|
||||
|
||||
warning(
|
||||
f"""Location "{loc}" had to be added to {self.player_name}'s world
|
||||
@@ -341,6 +360,10 @@ class WitnessWorld(World):
|
||||
self.own_itempool += new_items
|
||||
self.multiworld.itempool += new_items
|
||||
|
||||
def fill_hook(self, progitempool: List[Item], _: List[Item], _2: List[Item],
|
||||
fill_locations: List[Location]) -> None:
|
||||
place_early_items(self, progitempool, fill_locations)
|
||||
|
||||
def fill_slot_data(self) -> Dict[str, Any]:
|
||||
already_hinted_locations = set()
|
||||
|
||||
|
||||
@@ -72,5 +72,5 @@ class WitnessPlayerLocations:
|
||||
|
||||
def add_location_late(self, entity_name: str) -> None:
|
||||
entity_hex = static_witness_logic.ENTITIES_BY_NAME[entity_name]["entity_hex"]
|
||||
self.CHECK_LOCATION_TABLE[entity_hex] = static_witness_locations.get_id(entity_hex)
|
||||
self.CHECK_LOCATION_TABLE[entity_name] = static_witness_locations.get_id(entity_hex)
|
||||
self.CHECK_PANELHEX_TO_ID[entity_hex] = static_witness_locations.get_id(entity_hex)
|
||||
|
||||
@@ -13,6 +13,7 @@ from Options import (
|
||||
OptionSet,
|
||||
PerGameCommonOptions,
|
||||
Range,
|
||||
Removed,
|
||||
Toggle,
|
||||
Visibility,
|
||||
)
|
||||
@@ -50,12 +51,19 @@ class EarlyCaves(Choice):
|
||||
alias_on = 2
|
||||
|
||||
|
||||
class EarlySymbolItem(DefaultOnToggle):
|
||||
class EarlyGoodItems(OptionSet):
|
||||
"""
|
||||
Put a random helpful symbol item on an early check, specifically Tutorial Gate Open if it is available early.
|
||||
Put one random helpful item of each of the chosen types on an early check, specifically a sphere 1 Tutorial location.
|
||||
If a type is chosen, but no items of that type exist in the itempool, it is skipped.
|
||||
The possible types are: "Symbol", "Door / Door Panel", "Obelisk Key".
|
||||
|
||||
If there aren't enough sphere 1 Tutorial locations, Tutorial First Hallway Straight and Tutorial First Hallway Bend may be added as locations.
|
||||
If there still aren't enough sphere 1 Tutorial locations, a random local sphere 1 location is picked.
|
||||
If no local sphere 1 locations are available, there are no further attempts to place the item.
|
||||
"""
|
||||
|
||||
visibility = Visibility.none
|
||||
valid_keys = {"Symbol", "Door / Door Panel", "Obelisk Key"}
|
||||
default = frozenset({"Symbol"})
|
||||
|
||||
|
||||
class ShuffleSymbols(DefaultOnToggle):
|
||||
@@ -550,7 +558,7 @@ class TheWitnessOptions(PerGameCommonOptions):
|
||||
panel_hunt_discourage_same_area_factor: PanelHuntDiscourageSameAreaFactor
|
||||
panel_hunt_plando: PanelHuntPlando
|
||||
early_caves: EarlyCaves
|
||||
early_symbol_item: EarlySymbolItem
|
||||
early_good_items: EarlyGoodItems
|
||||
elevators_come_to_you: ElevatorsComeToYou
|
||||
trap_percentage: TrapPercentage
|
||||
trap_weights: TrapWeights
|
||||
@@ -565,6 +573,8 @@ class TheWitnessOptions(PerGameCommonOptions):
|
||||
shuffle_dog: ShuffleDog
|
||||
easter_egg_hunt: EasterEggHunt
|
||||
|
||||
early_symbol_item: Removed
|
||||
|
||||
|
||||
witness_option_groups = [
|
||||
OptionGroup("Puzzles & Goal", [
|
||||
@@ -611,6 +621,7 @@ witness_option_groups = [
|
||||
LaserHints
|
||||
]),
|
||||
OptionGroup("Misc", [
|
||||
EarlyGoodItems,
|
||||
EarlyCaves,
|
||||
ElevatorsComeToYou,
|
||||
DeathLink,
|
||||
|
||||
151
worlds/witness/place_early_item.py
Normal file
151
worlds/witness/place_early_item.py
Normal file
@@ -0,0 +1,151 @@
|
||||
from logging import debug, error
|
||||
from typing import TYPE_CHECKING, Dict, List, Set, Tuple
|
||||
|
||||
from BaseClasses import CollectionState, Item, Location, LocationProgressType
|
||||
|
||||
from .data import static_logic as static_witness_logic
|
||||
from .data.utils import cast_not_none
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import WitnessWorld
|
||||
|
||||
|
||||
def get_available_early_locations(world: "WitnessWorld") -> List[Location]:
|
||||
# Pick an early item to put on Tutorial Gate Open.
|
||||
# Done after plando to avoid conflicting with it.
|
||||
# Done in fill_hook because multiworld itempool manipulation is not allowed in pre_fill.
|
||||
|
||||
# Prioritize Tutorial locations in a specific order
|
||||
tutorial_checks_in_order = [
|
||||
"Tutorial Gate Open",
|
||||
"Tutorial Back Left",
|
||||
"Tutorial Back Right",
|
||||
"Tutorial Front Left",
|
||||
"Tutorial First Hallway Straight",
|
||||
"Tutorial First Hallway Bend",
|
||||
"Tutorial Patio Floor",
|
||||
"Tutorial First Hallway EP",
|
||||
"Tutorial Cloud EP",
|
||||
"Tutorial Patio Flowers EP",
|
||||
]
|
||||
available_locations = [
|
||||
world.get_location(location_name) for location_name in tutorial_checks_in_order
|
||||
if location_name in world.reachable_early_locations # May not actually be sphere 1 (e.g. Obelisk Keys for EPs)
|
||||
]
|
||||
|
||||
# Then, add the rest of sphere 1 in "game order"
|
||||
available_locations += sorted(
|
||||
(
|
||||
world.get_location(location_name) for location_name in world.reachable_early_locations
|
||||
if location_name not in tutorial_checks_in_order
|
||||
),
|
||||
key=lambda location_object: static_witness_logic.ENTITIES_BY_NAME[location_object.name]["order"]
|
||||
)
|
||||
|
||||
return [
|
||||
location for location in available_locations
|
||||
if not location.item and location.progress_type != LocationProgressType.EXCLUDED
|
||||
]
|
||||
|
||||
|
||||
def get_eligible_items_by_type_in_random_order(world: "WitnessWorld") -> Dict[str, List[str]]:
|
||||
eligible_early_items_by_type = world.player_items.get_early_items({item.name for item in world.own_itempool})
|
||||
|
||||
for item_list in eligible_early_items_by_type.values():
|
||||
world.random.shuffle(item_list)
|
||||
|
||||
return eligible_early_items_by_type
|
||||
|
||||
|
||||
def grab_own_items_from_itempool(world: "WitnessWorld", itempool: List[Item], ids_to_find: Set[int]) -> List[Item]:
|
||||
found_early_items = []
|
||||
|
||||
def keep_or_take_out(item: Item) -> bool:
|
||||
if item.code not in ids_to_find:
|
||||
return True # Keep
|
||||
ids_to_find.remove(item.code)
|
||||
found_early_items.append(item)
|
||||
return False # Take out
|
||||
|
||||
local_player = world.player
|
||||
itempool[:] = [item for item in itempool if item.player != local_player or keep_or_take_out(item)]
|
||||
|
||||
return found_early_items
|
||||
|
||||
|
||||
def place_items_onto_locations(world: "WitnessWorld", items: List[Item],
|
||||
locations: List[Location]) -> Tuple[List[Item], List[Item]]:
|
||||
fake_state = CollectionState(world.multiworld)
|
||||
|
||||
placed_items = []
|
||||
unplaced_items = []
|
||||
|
||||
for item in items:
|
||||
location = next(
|
||||
(location for location in locations if location.can_fill(fake_state, item, check_access=False)),
|
||||
None,
|
||||
)
|
||||
if location is not None:
|
||||
location.place_locked_item(item)
|
||||
placed_items.append(item)
|
||||
locations.remove(location)
|
||||
else:
|
||||
unplaced_items.append(item)
|
||||
|
||||
return placed_items, unplaced_items
|
||||
|
||||
|
||||
def place_early_items(world: "WitnessWorld", prog_itempool: List[Item], fill_locations: List[Location]) -> None:
|
||||
if not world.options.early_good_items.value:
|
||||
return
|
||||
|
||||
# Get a list of good early locations in a determinstic order
|
||||
eligible_early_locations = get_available_early_locations(world)
|
||||
# Get a list of good early items of each desired item type
|
||||
eligible_early_items_by_type = get_eligible_items_by_type_in_random_order(world)
|
||||
|
||||
if not eligible_early_items_by_type:
|
||||
return
|
||||
|
||||
while any(eligible_early_items_by_type.values()) and eligible_early_locations:
|
||||
# Get one item of each type
|
||||
next_findable_items_dict = {
|
||||
item_list.pop(): item_type
|
||||
for item_type, item_list in eligible_early_items_by_type.items()
|
||||
if item_list
|
||||
}
|
||||
|
||||
# Get their IDs as a set
|
||||
next_findable_item_ids = {world.item_name_to_id[item_name] for item_name in next_findable_items_dict}
|
||||
|
||||
# Grab items from itempool
|
||||
found_early_items = grab_own_items_from_itempool(world, prog_itempool, next_findable_item_ids)
|
||||
|
||||
# Bring found items back into Symbol -> Door -> Obelisk Key order
|
||||
# The intent is that the Symbol is always on Tutorial Gate Open / generally that the order is predictable
|
||||
correct_order = {item_name: i for i, item_name in enumerate(next_findable_items_dict)}
|
||||
found_early_items.sort(key=lambda item: correct_order[item.name])
|
||||
|
||||
# Place found early items on eligible early locations.
|
||||
placed_items, unplaced_items = place_items_onto_locations(world, found_early_items, eligible_early_locations)
|
||||
|
||||
for item in placed_items:
|
||||
debug(f"Placed early good item {item} on early location {item.location}.")
|
||||
# Item type is satisfied
|
||||
del eligible_early_items_by_type[next_findable_items_dict[item.name]]
|
||||
fill_locations.remove(cast_not_none(item.location))
|
||||
for item in unplaced_items:
|
||||
debug(f"Could not find a suitable placement for item {item}.")
|
||||
|
||||
unfilled_types = list(eligible_early_items_by_type)
|
||||
if unfilled_types:
|
||||
if not eligible_early_locations:
|
||||
error(
|
||||
f'Could not find a suitable location for "early good items" of types {unfilled_types} in '
|
||||
f"{world.player_name}'s world. They are excluded or already contain plandoed items.\n"
|
||||
)
|
||||
else:
|
||||
error(
|
||||
f"Could not find any \"early good item\" of types {unfilled_types} in {world.player_name}'s world, "
|
||||
"they were all plandoed elsewhere."
|
||||
)
|
||||
@@ -198,36 +198,118 @@ class WitnessPlayerItems:
|
||||
|
||||
return output
|
||||
|
||||
def get_early_items(self) -> List[str]:
|
||||
def get_early_items(self, existing_items: Set[str]) -> Dict[str, List[str]]:
|
||||
"""
|
||||
Returns items that are ideal for placing on extremely early checks, like the tutorial gate.
|
||||
"""
|
||||
output: Set[str] = set()
|
||||
if self._world.options.shuffle_symbols:
|
||||
discards_on = self._world.options.shuffle_discarded_panels
|
||||
mode = self._world.options.puzzle_randomization.current_key
|
||||
output: Dict[str, List[str]] = {}
|
||||
|
||||
output = static_witness_items.ALWAYS_GOOD_SYMBOL_ITEMS | static_witness_items.MODE_SPECIFIC_GOOD_ITEMS[mode]
|
||||
if discards_on:
|
||||
output |= static_witness_items.MODE_SPECIFIC_GOOD_DISCARD_ITEMS[mode]
|
||||
if "Symbol" in self._world.options.early_good_items.value:
|
||||
good_symbols = ["Dots", "Black/White Squares", "Symmetry", "Shapers", "Stars"]
|
||||
|
||||
# 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
|
||||
# regardless of whether or not they actually wind up being manually placed.
|
||||
for plando_setting in self._world.options.plando_items:
|
||||
if plando_setting.from_pool:
|
||||
if isinstance(plando_setting.items, dict):
|
||||
output -= {item for item, weight in plando_setting.items.items() if weight}
|
||||
if self._world.options.shuffle_discarded_panels:
|
||||
if self._world.options.puzzle_randomization == "sigma_expert":
|
||||
good_symbols.append("Arrows")
|
||||
else:
|
||||
# Assume this is some other kind of iterable.
|
||||
for inner_item in plando_setting.items:
|
||||
if isinstance(inner_item, str):
|
||||
output -= {inner_item}
|
||||
elif isinstance(inner_item, dict):
|
||||
output -= {item for item, weight in inner_item.items() if weight}
|
||||
good_symbols.append("Triangles")
|
||||
|
||||
# Sort the output for consistency across versions if the implementation changes but the logic does not.
|
||||
return sorted(output)
|
||||
# Replace progressive items with their parents.
|
||||
good_symbols = [
|
||||
static_witness_logic.get_parent_progressive_item(item) for item in good_symbols
|
||||
]
|
||||
|
||||
output["Symbol"] = [symbol for symbol in good_symbols if symbol in existing_items]
|
||||
|
||||
if "Door / Door Panel" in self._world.options.early_good_items.value:
|
||||
good_doors = [
|
||||
"Desert Doors & Elevator", "Keep Hedge Maze Doors", "Keep Pressure Plates Doors",
|
||||
"Shadows Lower Doors", "Tunnels Doors", "Quarry Stoneworks Doors",
|
||||
|
||||
"Keep Tower Shortcut (Door)", "Shadows Timed Door", "Tunnels Town Shortcut (Door)",
|
||||
"Quarry Stoneworks Roof Exit (Door)",
|
||||
|
||||
"Desert Panels", "Keep Hedge Maze Panels",
|
||||
|
||||
"Shadows Door Timer (Panel)",
|
||||
]
|
||||
|
||||
# While Caves Shortcuts don't unlock anything in symbol shuffle, you'd still rather have them early.
|
||||
# But, we need to do some special handling for them.
|
||||
|
||||
if self._world.options.shuffle_doors in ("doors", "mixed"):
|
||||
# Caves Shortcuts might exist in vanilla/panel doors because of "early caves: add to pool".
|
||||
# But if the player wanted them early, they would have chosen "early caves: starting inventory".
|
||||
# This is why we make sure these are "natural" Caves Shortcuts.
|
||||
good_doors.append("Caves Shortcuts")
|
||||
|
||||
# These two doors are logically equivalent, so we choose a random one as to not give them twice the chance.
|
||||
good_doors.append(
|
||||
self._world.random.choice(["Caves Mountain Shortcut (Door)", "Caves Swamp Shortcut (Door)"])
|
||||
)
|
||||
|
||||
if self._world.options.shuffle_vault_boxes and not self._world.options.disable_non_randomized_puzzles:
|
||||
good_doors.append("Windmill & Theater Doors")
|
||||
if not self._world.options.shuffle_symbols:
|
||||
good_doors += [
|
||||
"Windmill & Theater Panels",
|
||||
|
||||
"Windmill & Theater Control Panels",
|
||||
]
|
||||
|
||||
if self._world.options.shuffle_doors == "doors": # It's not as good in mixed doors because of Light Control
|
||||
good_doors.append("Desert Light Room Entry (Door)")
|
||||
|
||||
if not self._world.options.shuffle_symbols:
|
||||
good_doors += [
|
||||
"Bunker Doors", "Swamp Doors", "Glass Factory Doors", "Town Doors",
|
||||
|
||||
"Bunker Entry (Door)", "Glass Factory Entry (Door)", "Symmetry Island Lower (Door)",
|
||||
|
||||
"Bunker Panels", "Swamp Panels", "Quarry Outside Panels", "Glass Factory Panels"
|
||||
|
||||
"Glass Factory Entry (Panel)",
|
||||
]
|
||||
|
||||
existing_doors = [door for door in good_doors if door in existing_items]
|
||||
|
||||
# On some options combinations with doors, there just aren't a lot of doors that unlock much early.
|
||||
# In this case, we add some doors that aren't great, but are at least guaranteed to unlock 1 location.
|
||||
|
||||
fallback_doors = [
|
||||
"Keep Shadows Shortcut (Door)", # Always Keep Shadows Shortcut Panel
|
||||
"Keep Shortcuts", # -"-
|
||||
"Keep Pressure Plates Doors", # -"-
|
||||
"Keep Hedge Maze 1 (Panel)", # Always Hedge 1
|
||||
"Town Maze Stairs (Panel)", # Always Maze Panel
|
||||
"Shadows Laser Room Doors", # Always Shadows Laser Panel
|
||||
"Swamp Laser Shortcut (Door)", # Always Swamp Laser
|
||||
"Town Maze Panels", # Always Town Maze Panel
|
||||
"Town Doors", # Always Town Church Lattice
|
||||
"Town Church Entry (Door)", # -"-
|
||||
"Town Tower Doors", # Always Town Laser
|
||||
]
|
||||
self._world.random.shuffle(fallback_doors)
|
||||
|
||||
while len(existing_doors) < 4 and fallback_doors:
|
||||
fallback_door = fallback_doors.pop()
|
||||
if fallback_door in existing_items and fallback_door not in existing_doors:
|
||||
existing_doors.append(fallback_door)
|
||||
|
||||
output["Door"] = existing_doors
|
||||
|
||||
if "Obelisk Key" in self._world.options.early_good_items.value:
|
||||
obelisk_keys = [
|
||||
"Desert Obelisk Key", "Town Obelisk Key", "Quarry Obelisk Key",
|
||||
"Treehouse Obelisk Key", "Monastery Obelisk Key", "Mountainside Obelisk Key"
|
||||
]
|
||||
output["Obelisk Key"] = [key for key in obelisk_keys if key in existing_items]
|
||||
|
||||
assert all(item in self._world.item_names for sublist in output.values() for item in sublist), (
|
||||
[item for sublist in output.values() for item in sublist if item not in self._world.item_names]
|
||||
)
|
||||
|
||||
# Cull empty lists
|
||||
return {item_type: item_list for item_type, item_list in output.items() if item_list}
|
||||
|
||||
def get_door_item_ids_in_pool(self) -> List[int]:
|
||||
"""
|
||||
|
||||
@@ -35,7 +35,7 @@ witness_option_presets: Dict[str, Dict[str, Any]] = {
|
||||
"challenge_lasers": 11,
|
||||
|
||||
"early_caves": EarlyCaves.option_off,
|
||||
|
||||
"early_good_items": {"Door / Door Panel"},
|
||||
"elevators_come_to_you": ElevatorsComeToYou.default,
|
||||
|
||||
"trap_percentage": TrapPercentage.default,
|
||||
@@ -76,7 +76,7 @@ witness_option_presets: Dict[str, Dict[str, Any]] = {
|
||||
"challenge_lasers": 9,
|
||||
|
||||
"early_caves": EarlyCaves.option_off,
|
||||
|
||||
"early_good_items": {"Symbol", "Door / Door Panel"}, # Not Obelisk Key bc I want EPs to open slowly in this one
|
||||
"elevators_come_to_you": ElevatorsComeToYou.default,
|
||||
|
||||
"trap_percentage": TrapPercentage.default,
|
||||
@@ -117,7 +117,7 @@ witness_option_presets: Dict[str, Dict[str, Any]] = {
|
||||
"challenge_lasers": 9,
|
||||
|
||||
"early_caves": EarlyCaves.option_off,
|
||||
|
||||
"early_good_items": {"Symbol", "Door / Door Panel", "Obelisk Key"},
|
||||
"elevators_come_to_you": ElevatorsComeToYou.valid_keys,
|
||||
|
||||
"trap_percentage": TrapPercentage.default,
|
||||
|
||||
@@ -8,7 +8,7 @@ class TestDisableNonRandomized(WitnessTestBase):
|
||||
options = {
|
||||
"disable_non_randomized_puzzles": True,
|
||||
"shuffle_doors": "panels",
|
||||
"early_symbol_item": False,
|
||||
"early_good_items": {},
|
||||
}
|
||||
|
||||
def test_locations_got_disabled_and_alternate_activation_triggers_work(self) -> None:
|
||||
|
||||
109
worlds/witness/test/test_early_good_item.py
Normal file
109
worlds/witness/test/test_early_good_item.py
Normal file
@@ -0,0 +1,109 @@
|
||||
from BaseClasses import ItemClassification, LocationProgressType
|
||||
from Fill import distribute_items_restrictive
|
||||
|
||||
from ..data.utils import cast_not_none
|
||||
from ..test.bases import WitnessTestBase
|
||||
|
||||
|
||||
class TestEarlySymbolItemFalse(WitnessTestBase):
|
||||
options = {
|
||||
"early_good_items": {},
|
||||
|
||||
"shuffle_symbols": True,
|
||||
"shuffle_doors": "off",
|
||||
"shuffle_boat": False,
|
||||
"shuffle_lasers": False,
|
||||
"obelisk_keys": False,
|
||||
}
|
||||
|
||||
def setUp(self) -> None:
|
||||
if self.auto_construct:
|
||||
self.world_setup(seed=1) # Magic seed to prevent false positive
|
||||
|
||||
def test_early_good_item(self) -> None:
|
||||
distribute_items_restrictive(self.multiworld)
|
||||
|
||||
gate_open = self.multiworld.get_location("Tutorial Gate Open", 1)
|
||||
|
||||
self.assertFalse(
|
||||
cast_not_none(gate_open.item).classification & ItemClassification.progression,
|
||||
"Early Good Item was off, yet a Symbol item ended up on Tutorial Gate Open.",
|
||||
)
|
||||
|
||||
|
||||
class TestEarlySymbolItemTrue(WitnessTestBase):
|
||||
options = {
|
||||
"early_good_items": {"Symbol", "Door / Door Panel", "Obelisk Key"},
|
||||
|
||||
"shuffle_symbols": True,
|
||||
"shuffle_doors": "panels",
|
||||
"shuffle_EPs": "individual",
|
||||
"obelisk_keys": True,
|
||||
}
|
||||
|
||||
def setUp(self) -> None:
|
||||
super().setUp()
|
||||
|
||||
def test_early_good_item(self) -> None:
|
||||
"""
|
||||
The items should be in the order:
|
||||
Symbol item on Tutorial Gate Open
|
||||
Door item on Tutorial Back Left
|
||||
Obelisk Key on Tutorial Back Right
|
||||
"""
|
||||
|
||||
distribute_items_restrictive(self.multiworld)
|
||||
|
||||
gate_open = self.multiworld.get_location("Tutorial Gate Open", 1)
|
||||
|
||||
self.assertTrue(gate_open.item is not None, "Somehow, no item got placed on Tutorial Gate Open.")
|
||||
|
||||
self.assertTrue(
|
||||
cast_not_none(gate_open.item).name in self.world.item_name_groups["Symbols"],
|
||||
"Early Good Item was on, yet no Symbol item ended up on Tutorial Gate Open.",
|
||||
)
|
||||
|
||||
back_left = self.multiworld.get_location("Tutorial Back Left", 1)
|
||||
|
||||
self.assertTrue(back_left.item is not None, "Somehow, no item got placed on Tutorial Back Left.")
|
||||
|
||||
doors_and_panel_keys = self.world.item_name_groups["Doors"] | self.world.item_name_groups["Panel Keys"]
|
||||
self.assertTrue(
|
||||
cast_not_none(back_left.item).name in doors_and_panel_keys,
|
||||
"Early Good Item was on, yet no Door item ended up on Tutorial Back Left.",
|
||||
)
|
||||
|
||||
back_right = self.multiworld.get_location("Tutorial Back Right", 1)
|
||||
|
||||
self.assertTrue(back_right.item is not None, "Somehow, no item got placed on Tutorial Back Right.")
|
||||
|
||||
self.assertTrue(
|
||||
cast_not_none(back_right.item).name in self.world.item_name_groups["Obelisk Keys"],
|
||||
"Early Good Item was on, yet no Obelisk Key item ended up on Tutorial Back Right.",
|
||||
)
|
||||
|
||||
|
||||
class TestEarlySymbolItemTrueButExcluded(WitnessTestBase):
|
||||
options = {
|
||||
"shuffle_symbols": True,
|
||||
"shuffle_doors": "off",
|
||||
"shuffle_boat": False,
|
||||
"shuffle_lasers": False,
|
||||
"obelisk_keys": False,
|
||||
}
|
||||
|
||||
def setUp(self) -> None:
|
||||
super().setUp()
|
||||
self.multiworld.get_location("Tutorial Gate Open", 1).progress_type = LocationProgressType.EXCLUDED
|
||||
|
||||
def test_early_good_item(self) -> None:
|
||||
distribute_items_restrictive(self.multiworld)
|
||||
|
||||
gate_open = self.multiworld.get_location("Tutorial Gate Open", 1)
|
||||
|
||||
self.assertTrue(gate_open.item is not None, "Somehow, no item got placed on Tutorial Gate Open.")
|
||||
|
||||
self.assertFalse(
|
||||
cast_not_none(gate_open.item).classification & ItemClassification.progression,
|
||||
"Tutorial Gate Open was excluded, yet it still received an early Symbol item.",
|
||||
)
|
||||
@@ -7,7 +7,7 @@ class TestSymbolsRequiredToWinElevatorNormal(WitnessTestBase):
|
||||
"puzzle_randomization": "sigma_normal",
|
||||
"mountain_lasers": 1,
|
||||
"victory_condition": "elevator",
|
||||
"early_symbol_item": False,
|
||||
"early_good_items": {},
|
||||
}
|
||||
|
||||
def test_symbols_to_win(self) -> None:
|
||||
@@ -37,7 +37,7 @@ class TestSymbolsRequiredToWinElevatorExpert(WitnessTestBase):
|
||||
"shuffle_lasers": True,
|
||||
"mountain_lasers": 1,
|
||||
"victory_condition": "elevator",
|
||||
"early_symbol_item": False,
|
||||
"early_good_items": {},
|
||||
"puzzle_randomization": "sigma_expert",
|
||||
}
|
||||
|
||||
@@ -70,7 +70,7 @@ class TestSymbolsRequiredToWinElevatorVanilla(WitnessTestBase):
|
||||
"shuffle_lasers": True,
|
||||
"mountain_lasers": 1,
|
||||
"victory_condition": "elevator",
|
||||
"early_symbol_item": False,
|
||||
"early_good_items": {},
|
||||
"puzzle_randomization": "none",
|
||||
}
|
||||
|
||||
@@ -101,7 +101,6 @@ class TestSymbolsRequiredToWinElevatorVariety(WitnessTestBase):
|
||||
"shuffle_lasers": True,
|
||||
"mountain_lasers": 1,
|
||||
"victory_condition": "elevator",
|
||||
"early_symbol_item": False,
|
||||
"puzzle_randomization": "umbra_variety",
|
||||
}
|
||||
|
||||
@@ -134,7 +133,7 @@ class TestPanelsRequiredToWinElevator(WitnessTestBase):
|
||||
"shuffle_lasers": True,
|
||||
"mountain_lasers": 1,
|
||||
"victory_condition": "elevator",
|
||||
"early_symbol_item": False,
|
||||
"early_good_items": {},
|
||||
"shuffle_symbols": False,
|
||||
"shuffle_doors": "panels",
|
||||
"door_groupings": "off",
|
||||
@@ -163,7 +162,7 @@ class TestDoorsRequiredToWinElevator(WitnessTestBase):
|
||||
"shuffle_lasers": True,
|
||||
"mountain_lasers": 1,
|
||||
"victory_condition": "elevator",
|
||||
"early_symbol_item": False,
|
||||
"early_good_items": {},
|
||||
"shuffle_symbols": False,
|
||||
"shuffle_doors": "doors",
|
||||
"door_groupings": "off",
|
||||
|
||||
@@ -3,7 +3,7 @@ from ..test.bases import WitnessMultiworldTestBase, WitnessTestBase
|
||||
|
||||
class TestSymbols(WitnessTestBase):
|
||||
options = {
|
||||
"early_symbol_item": False,
|
||||
"early_good_items": {},
|
||||
}
|
||||
|
||||
def test_progressive_symbols(self) -> None:
|
||||
@@ -53,7 +53,7 @@ class TestSymbolRequirementsMultiworld(WitnessMultiworldTestBase):
|
||||
|
||||
common_options = {
|
||||
"shuffle_discarded_panels": True,
|
||||
"early_symbol_item": False,
|
||||
"early_good_items": {},
|
||||
}
|
||||
|
||||
def test_arrows_exist_and_are_required_in_expert_seeds_only(self) -> None:
|
||||
|
||||
Reference in New Issue
Block a user