The Witness: Item loading refactor. (#1953)
Co-authored-by: el-u <109771707+el-u@users.noreply.github.com> Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
This commit is contained in:
parent
18c9779815
commit
1f6db12797
|
@ -1,6 +1,6 @@
|
|||
from typing import Dict, Union
|
||||
from BaseClasses import MultiWorld
|
||||
from Options import Toggle, DefaultOnToggle, Option, Range, Choice
|
||||
from Options import Toggle, DefaultOnToggle, Range, Choice
|
||||
|
||||
|
||||
# class HardMode(Toggle):
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
Progression:
|
||||
Symbols:
|
||||
0 - Dots
|
||||
1 - Colored Dots
|
||||
2 - Full Dots
|
||||
|
@ -18,16 +18,22 @@ Progression:
|
|||
200 - Progressive Dots - Dots,Full Dots
|
||||
260 - Progressive Stars - Stars,Stars + Same Colored Symbol
|
||||
|
||||
Usefuls:
|
||||
101 - Functioning Brain - False
|
||||
510 - Puzzle Skip - True
|
||||
Useful:
|
||||
510 - Puzzle Skip
|
||||
#511 - Energy Capacity
|
||||
|
||||
Boosts:
|
||||
500 - Speed Boost
|
||||
Filler:
|
||||
500 - Speed Boost - 1
|
||||
#501 - Energy Fill (Small) - 6
|
||||
#502 - Energy Fill - 3
|
||||
#503 - Energy Fill (Max) - 1
|
||||
|
||||
Traps:
|
||||
600 - Slowness
|
||||
610 - Power Surge
|
||||
600 - Slowness - 8
|
||||
610 - Power Surge - 2
|
||||
|
||||
Jokes:
|
||||
650 - Functioning Brain
|
||||
|
||||
Doors:
|
||||
1100 - Glass Factory Entry (Panel) - 0x01A54
|
||||
|
@ -63,18 +69,6 @@ Doors:
|
|||
|
||||
1400 - Caves Mountain Shortcut (Door) - 0x2D73F
|
||||
|
||||
1500 - Symmetry Laser - 0x00509
|
||||
1501 - Desert Laser - 0x012FB,0x01317
|
||||
1502 - Quarry Laser - 0x01539
|
||||
1503 - Shadows Laser - 0x181B3
|
||||
1504 - Keep Laser - 0x014BB
|
||||
1505 - Monastery Laser - 0x17C65
|
||||
1506 - Town Laser - 0x032F9
|
||||
1507 - Jungle Laser - 0x00274
|
||||
1508 - Bunker Laser - 0x0C2B2
|
||||
1509 - Swamp Laser - 0x00BF6
|
||||
1510 - Treehouse Laser - 0x028A4
|
||||
|
||||
1600 - Outside Tutorial Outpost Path (Door) - 0x03BA2
|
||||
1603 - Outside Tutorial Outpost Entry (Door) - 0x0A170
|
||||
1606 - Outside Tutorial Outpost Exit (Door) - 0x04CA3
|
||||
|
@ -198,3 +192,16 @@ Doors:
|
|||
1981 - Caves Doors to Challenge - 0x019A5,0x0A19A
|
||||
1984 - Caves Exits to Main Island - 0x2D859,0x2D73F
|
||||
1987 - Tunnels Doors - 0x27739,0x27263,0x09E87
|
||||
|
||||
Lasers:
|
||||
1500 - Symmetry Laser - 0x00509
|
||||
1501 - Desert Laser - 0x012FB,0x01317
|
||||
1502 - Quarry Laser - 0x01539
|
||||
1503 - Shadows Laser - 0x181B3
|
||||
1504 - Keep Laser - 0x014BB
|
||||
1505 - Monastery Laser - 0x17C65
|
||||
1506 - Town Laser - 0x032F9
|
||||
1507 - Jungle Laser - 0x00274
|
||||
1508 - Bunker Laser - 0x0C2B2
|
||||
1509 - Swamp Laser - 0x00BF6
|
||||
1510 - Treehouse Laser - 0x028A4
|
|
@ -3,19 +3,19 @@ Archipelago init file for The Witness
|
|||
"""
|
||||
import typing
|
||||
|
||||
from BaseClasses import Region, Location, MultiWorld, Item, Entrance, Tutorial, ItemClassification
|
||||
from BaseClasses import Region, Location, MultiWorld, Item, Entrance, Tutorial
|
||||
from .hints import get_always_hint_locations, get_always_hint_items, get_priority_hint_locations, \
|
||||
get_priority_hint_items, make_hints, generate_joke_hints
|
||||
from worlds.AutoWorld import World, WebWorld
|
||||
from .player_logic import WitnessPlayerLogic
|
||||
from .static_logic import StaticWitnessLogic
|
||||
from .locations import WitnessPlayerLocations, StaticWitnessLocations
|
||||
from .items import WitnessItem, StaticWitnessItems, WitnessPlayerItems
|
||||
from .items import WitnessItem, StaticWitnessItems, WitnessPlayerItems, ItemData
|
||||
from .rules import set_rules
|
||||
from .regions import WitnessRegions
|
||||
from .Options import is_option_enabled, the_witness_options, get_option_value
|
||||
from .utils import best_junk_to_add_based_on_weights, get_audio_logs, make_warning_string
|
||||
from logging import warning
|
||||
from .utils import get_audio_logs
|
||||
from logging import warning, error
|
||||
|
||||
|
||||
class WitnessWebWorld(WebWorld):
|
||||
|
@ -40,39 +40,59 @@ class WitnessWorld(World):
|
|||
topology_present = False
|
||||
data_version = 13
|
||||
|
||||
static_logic = StaticWitnessLogic()
|
||||
static_locat = StaticWitnessLocations()
|
||||
static_items = StaticWitnessItems()
|
||||
StaticWitnessLogic()
|
||||
StaticWitnessLocations()
|
||||
StaticWitnessItems()
|
||||
web = WitnessWebWorld()
|
||||
option_definitions = the_witness_options
|
||||
|
||||
item_name_to_id = {
|
||||
name: data.code for name, data in static_items.ALL_ITEM_TABLE.items()
|
||||
name: data.ap_code for name, data in StaticWitnessItems.item_data.items()
|
||||
}
|
||||
location_name_to_id = StaticWitnessLocations.ALL_LOCATIONS_TO_ID
|
||||
item_name_groups = StaticWitnessItems.ITEM_NAME_GROUPS
|
||||
item_name_groups = StaticWitnessItems.item_groups
|
||||
|
||||
required_client_version = (0, 3, 9)
|
||||
|
||||
def __init__(self, multiworld: "MultiWorld", player: int):
|
||||
super().__init__(multiworld, player)
|
||||
|
||||
self.player_logic = None
|
||||
self.locat = None
|
||||
self.items = None
|
||||
self.regio = None
|
||||
|
||||
self.log_ids_to_hints = None
|
||||
|
||||
def _get_slot_data(self):
|
||||
return {
|
||||
'seed': self.multiworld.per_slot_randoms[self.player].randint(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': self.static_items.ITEM_ID_TO_DOOR_HEX_ALL,
|
||||
'door_hexes_in_the_pool': self.items.DOORS,
|
||||
'symbols_not_in_the_game': self.items.SYMBOLS_NOT_IN_THE_GAME,
|
||||
'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_panels': list(self.player_logic.COMPLETELY_DISABLED_CHECKS),
|
||||
'log_ids_to_hints': self.log_ids_to_hints,
|
||||
'progressive_item_lists': self.items.MULTI_LISTS_BY_CODE,
|
||||
'obelisk_side_id_to_EPs': self.static_logic.OBELISK_SIDE_ID_TO_EP_HEXES,
|
||||
'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 | self.player_logic.PRECOMPLETED_LOCATIONS],
|
||||
'entity_to_name': self.static_logic.ENTITY_ID_TO_NAME,
|
||||
}
|
||||
|
||||
def generate_early(self):
|
||||
self.items_by_name = dict()
|
||||
disabled_locations = self.multiworld.exclude_locations[self.player].value
|
||||
|
||||
self.player_logic = WitnessPlayerLogic(
|
||||
self.multiworld, self.player, disabled_locations, self.multiworld.start_inventory[self.player].value
|
||||
)
|
||||
|
||||
self.locat: WitnessPlayerLocations = WitnessPlayerLocations(self.multiworld, self.player, self.player_logic)
|
||||
self.items: WitnessPlayerItems = WitnessPlayerItems(self.multiworld, self.player, self.player_logic, self.locat)
|
||||
self.regio: WitnessRegions = WitnessRegions(self.locat)
|
||||
|
||||
self.log_ids_to_hints = dict()
|
||||
|
||||
if not (is_option_enabled(self.multiworld, self.player, "shuffle_symbols")
|
||||
or get_option_value(self.multiworld, self.player, "shuffle_doors")
|
||||
|
@ -84,49 +104,52 @@ class WitnessWorld(World):
|
|||
raise Exception("This Witness world doesn't have any progression items. Please turn on Symbol Shuffle,"
|
||||
" Door Shuffle or Laser Shuffle.")
|
||||
|
||||
disabled_locations = self.multiworld.exclude_locations[self.player].value
|
||||
|
||||
self.player_logic = WitnessPlayerLogic(
|
||||
self.multiworld, self.player, disabled_locations, self.multiworld.start_inventory[self.player].value
|
||||
)
|
||||
|
||||
self.locat = WitnessPlayerLocations(self.multiworld, self.player, self.player_logic)
|
||||
self.items = WitnessPlayerItems(self.locat, self.multiworld, self.player, self.player_logic)
|
||||
self.regio = WitnessRegions(self.locat)
|
||||
|
||||
self.log_ids_to_hints = dict()
|
||||
self.junk_items_created = {key: 0 for key in self.items.JUNK_WEIGHTS.keys()}
|
||||
|
||||
def create_regions(self):
|
||||
self.regio.create_regions(self.multiworld, self.player, self.player_logic)
|
||||
|
||||
def create_items(self):
|
||||
# Generate item pool
|
||||
pool = []
|
||||
for item in self.items.ITEM_TABLE:
|
||||
for i in range(0, self.items.PROG_ITEM_AMOUNTS[item]):
|
||||
if item in self.items.PROGRESSION_TABLE:
|
||||
witness_item = self.create_item(item)
|
||||
pool.append(witness_item)
|
||||
self.items_by_name[item] = witness_item
|
||||
|
||||
for precol_item in self.multiworld.precollected_items[self.player]:
|
||||
if precol_item.name in self.items_by_name: # if item is in the pool, remove 1 instance.
|
||||
item_obj = self.items_by_name[precol_item.name]
|
||||
# Determine pool size. Note that the dog location is included in the location list, so this needs to be -1.
|
||||
pool_size: int = len(self.locat.CHECK_LOCATION_TABLE) - len(self.locat.EVENT_LOCATION_TABLE) - 1
|
||||
|
||||
if item_obj in pool:
|
||||
pool.remove(item_obj) # remove one instance of this pre-collected item if it exists
|
||||
# Fill mandatory items and remove precollected and/or starting items from the pool.
|
||||
item_pool: dict[str, int] = self.items.get_mandatory_items()
|
||||
|
||||
for item in self.player_logic.STARTING_INVENTORY:
|
||||
self.multiworld.push_precollected(self.items_by_name[item])
|
||||
pool.remove(self.items_by_name[item])
|
||||
for precollected_item_name in [item.name for item in self.multiworld.precollected_items[self.player]]:
|
||||
if precollected_item_name in item_pool:
|
||||
if item_pool[precollected_item_name] == 1:
|
||||
item_pool.pop(precollected_item_name)
|
||||
else:
|
||||
item_pool[precollected_item_name] -= 1
|
||||
|
||||
for item in self.items.EXTRA_AMOUNTS:
|
||||
for i in range(0, self.items.EXTRA_AMOUNTS[item]):
|
||||
witness_item = self.create_item(item)
|
||||
pool.append(witness_item)
|
||||
for inventory_item_name in self.player_logic.STARTING_INVENTORY:
|
||||
if inventory_item_name in item_pool:
|
||||
if item_pool[inventory_item_name] == 1:
|
||||
item_pool.pop(inventory_item_name)
|
||||
else:
|
||||
item_pool[inventory_item_name] -= 1
|
||||
|
||||
# Tie Event Items to Event Locations (e.g. Laser Activations)
|
||||
if len(item_pool) > pool_size:
|
||||
error_string = "The Witness world has too few locations ({num_loc}) to place its necessary items " \
|
||||
"({num_item})."
|
||||
error(error_string.format(num_loc=pool_size, num_item=len(item_pool)))
|
||||
return
|
||||
|
||||
remaining_item_slots = pool_size - sum(item_pool.values())
|
||||
|
||||
# Add puzzle skips.
|
||||
num_puzzle_skips = get_option_value(self.multiworld, self.player, "puzzle_skip_amount")
|
||||
if num_puzzle_skips > remaining_item_slots:
|
||||
warning(f"The Witness world has insufficient locations to place all requested puzzle skips.")
|
||||
num_puzzle_skips = remaining_item_slots
|
||||
item_pool["Puzzle Skip"] = num_puzzle_skips
|
||||
remaining_item_slots -= num_puzzle_skips
|
||||
|
||||
# Add junk items.
|
||||
if remaining_item_slots > 0:
|
||||
item_pool.update(self.items.get_filler_items(remaining_item_slots))
|
||||
|
||||
# Add event items and tie them to event locations (e.g. laser activations).
|
||||
for event_location in self.locat.EVENT_LOCATION_TABLE:
|
||||
item_obj = self.create_item(
|
||||
self.player_logic.EVENT_ITEM_PAIRS[event_location]
|
||||
|
@ -134,113 +157,34 @@ class WitnessWorld(World):
|
|||
location_obj = self.multiworld.get_location(event_location, self.player)
|
||||
location_obj.place_locked_item(item_obj)
|
||||
|
||||
# Find out how much empty space there is for junk items. -1 for the "Town Pet the Dog" check
|
||||
itempool_difference = len(self.locat.CHECK_LOCATION_TABLE) - len(self.locat.EVENT_LOCATION_TABLE) - 1
|
||||
itempool_difference -= len(pool)
|
||||
|
||||
# Place two locked items: Good symbol on Tutorial Gate Open, and a Puzzle Skip on "Town Pet the Dog"
|
||||
good_items_in_the_game = []
|
||||
plandoed_items = set()
|
||||
|
||||
for v in self.multiworld.plando_items[self.player]:
|
||||
if v.get("from_pool", True):
|
||||
for item_key in {"item", "items"}:
|
||||
if item_key in v:
|
||||
if type(v[item_key]) is str:
|
||||
plandoed_items.add(v[item_key])
|
||||
elif type(v[item_key]) is dict:
|
||||
plandoed_items.update(item for item, weight in v[item_key].items() if weight)
|
||||
else:
|
||||
# Other type of iterable
|
||||
plandoed_items.update(v[item_key])
|
||||
|
||||
for symbol in self.items.GOOD_ITEMS:
|
||||
item = self.items_by_name[symbol]
|
||||
if item in pool and symbol not in plandoed_items:
|
||||
# for now, any item that is mentioned in any plando option, even if it's a list of items, is ineligible.
|
||||
# Hopefully, in the future, plando gets resolved before create_items.
|
||||
# I could also partially resolve lists myself, but this could introduce errors if not done carefully.
|
||||
good_items_in_the_game.append(symbol)
|
||||
|
||||
if good_items_in_the_game:
|
||||
random_good_item = self.multiworld.random.choice(good_items_in_the_game)
|
||||
|
||||
item = self.items_by_name[random_good_item]
|
||||
# BAD DOG GET BACK HERE WITH THAT PUZZLE SKIP YOU'RE POLLUTING THE ITEM POOL
|
||||
self.multiworld.get_location("Town Pet the Dog", self.player)\
|
||||
.place_locked_item(self.create_item("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 item_pool]
|
||||
if early_items:
|
||||
random_early_item = self.multiworld.random.choice(early_items)
|
||||
if get_option_value(self.multiworld, self.player, "puzzle_randomization") == 1:
|
||||
self.multiworld.local_early_items[self.player][random_good_item] = 1
|
||||
# In Expert, only tag the item as early, rather than forcing it onto the gate.
|
||||
self.multiworld.local_early_items[self.player][random_early_item] = 1
|
||||
else:
|
||||
first_check = self.multiworld.get_location(
|
||||
"Tutorial Gate Open", self.player
|
||||
)
|
||||
# Force the item onto the tutorial gate check and remove it from our random pool.
|
||||
self.multiworld.get_location("Tutorial Gate Open", self.player)\
|
||||
.place_locked_item(self.create_item(random_early_item))
|
||||
if item_pool[random_early_item] == 1:
|
||||
item_pool.pop(random_early_item)
|
||||
else:
|
||||
item_pool[random_early_item] -= 1
|
||||
|
||||
first_check.place_locked_item(item)
|
||||
pool.remove(item)
|
||||
# Generate the actual items.
|
||||
for item_name, quantity in item_pool.items():
|
||||
self.multiworld.itempool += [self.create_item(item_name) for _ in range(0, quantity)]
|
||||
if self.items.item_data[item_name].local_only:
|
||||
self.multiworld.local_items[self.player].value.add(item_name)
|
||||
|
||||
dog_check = self.multiworld.get_location(
|
||||
"Town Pet the Dog", self.player
|
||||
)
|
||||
|
||||
dog_check.place_locked_item(self.create_item("Puzzle Skip"))
|
||||
|
||||
# Fill rest of item pool with junk if there is room
|
||||
if itempool_difference > 0:
|
||||
for i in range(0, itempool_difference):
|
||||
self.multiworld.itempool.append(self.create_item(self.get_filler_item_name()))
|
||||
|
||||
# Remove junk, Functioning Brain, useful items (non-door), useful door items in that order until there is room
|
||||
if itempool_difference < 0:
|
||||
junk = [
|
||||
item for item in pool
|
||||
if item.classification in {ItemClassification.filler, ItemClassification.trap}
|
||||
and item.name != "Functioning Brain"
|
||||
]
|
||||
|
||||
f_brain = [item for item in pool if item.name == "Functioning Brain"]
|
||||
|
||||
usefuls = [
|
||||
item for item in pool
|
||||
if item.classification == ItemClassification.useful
|
||||
and item.name not in StaticWitnessLogic.ALL_DOOR_ITEMS_AS_DICT
|
||||
]
|
||||
|
||||
removable_doors = [
|
||||
item for item in pool
|
||||
if item.classification == ItemClassification.useful
|
||||
and item.name in StaticWitnessLogic.ALL_DOOR_ITEMS_AS_DICT
|
||||
]
|
||||
|
||||
self.multiworld.per_slot_randoms[self.player].shuffle(junk)
|
||||
self.multiworld.per_slot_randoms[self.player].shuffle(usefuls)
|
||||
self.multiworld.per_slot_randoms[self.player].shuffle(removable_doors)
|
||||
|
||||
removed_junk = False
|
||||
removed_usefuls = False
|
||||
removed_doors = False
|
||||
|
||||
for i in range(itempool_difference, 0):
|
||||
if junk:
|
||||
pool.remove(junk.pop())
|
||||
removed_junk = True
|
||||
elif f_brain:
|
||||
pool.remove(f_brain.pop())
|
||||
elif usefuls:
|
||||
pool.remove(usefuls.pop())
|
||||
removed_usefuls = True
|
||||
elif removable_doors:
|
||||
pool.remove(removable_doors.pop())
|
||||
removed_doors = True
|
||||
|
||||
warn = make_warning_string(
|
||||
removed_junk, removed_usefuls, removed_doors, not junk, not usefuls, not removable_doors
|
||||
)
|
||||
|
||||
if warn:
|
||||
warning(f"This Witness world has too few locations to place all its items."
|
||||
f" In order to make space, {warn} had to be removed.")
|
||||
|
||||
# Finally, add the generated pool to the overall itempool
|
||||
self.multiworld.itempool += pool
|
||||
# Sort the output for consistency across versions if the implementation changes but the logic does not.
|
||||
self.multiworld.itempool = sorted(self.multiworld.itempool, key=lambda item: item.name)
|
||||
|
||||
def set_rules(self):
|
||||
set_rules(self.multiworld, self.player, self.player_logic, self.locat)
|
||||
|
@ -291,33 +235,15 @@ class WitnessWorld(World):
|
|||
|
||||
return slot_data
|
||||
|
||||
def create_item(self, name: str) -> Item:
|
||||
def create_item(self, item_name: str) -> Item:
|
||||
# this conditional is purely for unit tests, which need to be able to create an item before generate_early
|
||||
if hasattr(self, 'items') and name in self.items.ITEM_TABLE:
|
||||
item = self.items.ITEM_TABLE[name]
|
||||
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]
|
||||
else:
|
||||
item = StaticWitnessItems.ALL_ITEM_TABLE[name]
|
||||
item_data = StaticWitnessItems.item_data[item_name]
|
||||
|
||||
if item.trap:
|
||||
classification = ItemClassification.trap
|
||||
elif item.progression:
|
||||
classification = ItemClassification.progression
|
||||
elif item.never_exclude:
|
||||
classification = ItemClassification.useful
|
||||
else:
|
||||
classification = ItemClassification.filler
|
||||
|
||||
new_item = WitnessItem(
|
||||
name, classification, item.code, player=self.player
|
||||
)
|
||||
return new_item
|
||||
|
||||
def get_filler_item_name(self) -> str: # Used by itemlinks
|
||||
item = best_junk_to_add_based_on_weights(self.items.JUNK_WEIGHTS, self.junk_items_created)
|
||||
|
||||
self.junk_items_created[item] += 1
|
||||
|
||||
return item
|
||||
return WitnessItem(item_name, item_data.classification, item_data.ap_code, player=self.player)
|
||||
|
||||
|
||||
class WitnessLocation(Location):
|
||||
|
|
|
@ -2,24 +2,30 @@
|
|||
Defines progression, junk and event items for The Witness
|
||||
"""
|
||||
import copy
|
||||
from collections import defaultdict
|
||||
from typing import Dict, NamedTuple, Optional, Set
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional
|
||||
|
||||
from BaseClasses import Item, MultiWorld
|
||||
from . import StaticWitnessLogic, WitnessPlayerLocations, WitnessPlayerLogic
|
||||
from BaseClasses import Item, MultiWorld, ItemClassification
|
||||
from .Options import get_option_value, is_option_enabled, the_witness_options
|
||||
from fractions import Fraction
|
||||
|
||||
from .locations import ID_START, WitnessPlayerLocations
|
||||
from .player_logic import WitnessPlayerLogic
|
||||
from .static_logic import ItemDefinition, DoorItemDefinition, ProgressiveItemDefinition, ItemCategory, \
|
||||
StaticWitnessLogic, WeightedItemDefinition
|
||||
from .utils import build_weighted_int_list
|
||||
|
||||
NUM_ENERGY_UPGRADES = 4
|
||||
|
||||
|
||||
class ItemData(NamedTuple):
|
||||
@dataclass()
|
||||
class ItemData:
|
||||
"""
|
||||
ItemData for an item in The Witness
|
||||
"""
|
||||
code: Optional[int]
|
||||
progression: bool
|
||||
event: bool = False
|
||||
trap: bool = False
|
||||
never_exclude: bool = False
|
||||
ap_code: Optional[int]
|
||||
definition: ItemDefinition
|
||||
classification: ItemClassification
|
||||
local_only: bool = False
|
||||
|
||||
|
||||
class WitnessItem(Item):
|
||||
|
@ -33,75 +39,50 @@ class StaticWitnessItems:
|
|||
"""
|
||||
Class that handles Witness items independent of world settings
|
||||
"""
|
||||
item_data: dict[str, ItemData] = {}
|
||||
item_groups: dict[str, list[str]] = {}
|
||||
|
||||
ALL_ITEM_TABLE: Dict[str, ItemData] = {}
|
||||
|
||||
ITEM_NAME_GROUPS: Dict[str, Set[str]] = dict()
|
||||
|
||||
# These should always add up to 1!!!
|
||||
BONUS_WEIGHTS = {
|
||||
"Speed Boost": Fraction(1, 1),
|
||||
}
|
||||
|
||||
# These should always add up to 1!!!
|
||||
TRAP_WEIGHTS = {
|
||||
"Slowness": Fraction(8, 10),
|
||||
"Power Surge": Fraction(2, 10),
|
||||
}
|
||||
|
||||
ALL_JUNK_ITEMS = set(BONUS_WEIGHTS.keys()) | set(TRAP_WEIGHTS.keys())
|
||||
|
||||
ITEM_ID_TO_DOOR_HEX_ALL = dict()
|
||||
# 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):
|
||||
item_tab = dict()
|
||||
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
|
||||
|
||||
for item in StaticWitnessLogic.ALL_SYMBOL_ITEMS:
|
||||
if item[0] == "11 Lasers" or item == "7 Lasers":
|
||||
continue
|
||||
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
|
||||
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
|
||||
|
||||
item_tab[item[0]] = ItemData(158000 + item[1], True, False)
|
||||
StaticWitnessItems.item_data[item_name] = ItemData(ap_item_code, definition,
|
||||
classification, local_only)
|
||||
|
||||
self.ITEM_NAME_GROUPS.setdefault("Symbols", set()).add(item[0])
|
||||
|
||||
for progressive, item_list in StaticWitnessLogic.PROGRESSIVE_TO_ITEMS.items():
|
||||
if not item_list:
|
||||
continue
|
||||
|
||||
if item_list[0] in self.ITEM_NAME_GROUPS.setdefault("Symbols", set()):
|
||||
self.ITEM_NAME_GROUPS.setdefault("Symbols", set()).add(progressive)
|
||||
|
||||
for item in StaticWitnessLogic.ALL_DOOR_ITEMS:
|
||||
item_tab[item[0]] = ItemData(158000 + item[1], True, False)
|
||||
|
||||
# 1500 - 1510 are the laser items, which are handled like doors but should be their own separate group.
|
||||
if item[1] in range(1500, 1511):
|
||||
self.ITEM_NAME_GROUPS.setdefault("Lasers", set()).add(item[0])
|
||||
else:
|
||||
self.ITEM_NAME_GROUPS.setdefault("Doors", set()).add(item[0])
|
||||
|
||||
for item in StaticWitnessLogic.ALL_TRAPS:
|
||||
item_tab[item[0]] = ItemData(
|
||||
158000 + item[1], False, False, True
|
||||
)
|
||||
|
||||
for item in StaticWitnessLogic.ALL_BOOSTS:
|
||||
item_tab[item[0]] = ItemData(158000 + item[1], False, False)
|
||||
|
||||
for item in StaticWitnessLogic.ALL_USEFULS:
|
||||
item_tab[item[0]] = ItemData(158000 + item[1], False, False, False, item[2])
|
||||
|
||||
item_tab = dict(sorted(
|
||||
item_tab.items(),
|
||||
key=lambda single_item: single_item[1].code
|
||||
if isinstance(single_item[1].code, int) else 0)
|
||||
)
|
||||
|
||||
for key, item in item_tab.items():
|
||||
self.ALL_ITEM_TABLE[key] = item
|
||||
|
||||
for door in StaticWitnessLogic.ALL_DOOR_ITEMS:
|
||||
self.ITEM_ID_TO_DOOR_HEX_ALL[door[1] + 158000] = {int(door_hex, 16) for door_hex in door[2]}
|
||||
@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:
|
||||
|
@ -109,138 +90,171 @@ class WitnessPlayerItems:
|
|||
Class that defines Items for a single world
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def code(item_name: str):
|
||||
return StaticWitnessItems.ALL_ITEM_TABLE[item_name].code
|
||||
|
||||
@staticmethod
|
||||
def is_progression(item_name: str, multiworld: MultiWorld, player: int):
|
||||
useless_doors = {
|
||||
"River Monastery Shortcut (Door)",
|
||||
"Jungle & River Shortcuts",
|
||||
"Monastery Shortcut (Door)",
|
||||
"Orchard Second Gate (Door)",
|
||||
}
|
||||
|
||||
if item_name in useless_doors:
|
||||
return False
|
||||
|
||||
ep_doors = {
|
||||
"Monastery Garden Entry (Door)",
|
||||
"Monastery Shortcuts",
|
||||
}
|
||||
|
||||
if item_name in ep_doors:
|
||||
return get_option_value(multiworld, player, "shuffle_EPs") != 0
|
||||
|
||||
return True
|
||||
|
||||
def __init__(self, locat: WitnessPlayerLocations, multiworld: MultiWorld, player: int, logic: WitnessPlayerLogic):
|
||||
def __init__(self, multiworld: MultiWorld, player: int, logic: WitnessPlayerLogic, locat: WitnessPlayerLocations):
|
||||
"""Adds event items after logic changes due to options"""
|
||||
self.EVENT_ITEM_TABLE = dict()
|
||||
self.ITEM_TABLE = copy.copy(StaticWitnessItems.ALL_ITEM_TABLE)
|
||||
|
||||
self.PROGRESSION_TABLE = dict()
|
||||
self._world: MultiWorld = multiworld
|
||||
self._player_id: int = player
|
||||
self._logic: WitnessPlayerLogic = logic
|
||||
self._locations: WitnessPlayerLocations = locat
|
||||
|
||||
self.ITEM_ID_TO_DOOR_HEX = dict()
|
||||
self.DOORS = list()
|
||||
# Duplicate the static item data, then make any player-specific adjustments to classification.
|
||||
self.item_data: dict[str, ItemData] = copy.copy(StaticWitnessItems.item_data)
|
||||
|
||||
self.PROG_ITEM_AMOUNTS = defaultdict(lambda: 1)
|
||||
# Remove all progression items that aren't actually in the game.
|
||||
self.item_data = {name: data for (name, data) in self.item_data.items()
|
||||
if data.classification is not ItemClassification.progression or
|
||||
name in logic.PROG_ITEMS_ACTUALLY_IN_THE_GAME}
|
||||
|
||||
self.SYMBOLS_NOT_IN_THE_GAME = list()
|
||||
# Adjust item classifications based on game settings.
|
||||
eps_shuffled = get_option_value(self._world, self._player_id, "shuffle_EPs") != 0
|
||||
for item_name, item_data in self.item_data.items():
|
||||
if not eps_shuffled and item_name in ["Monastery Garden Entry (Door)", "Monastery Shortcuts"]:
|
||||
# Downgrade doors that only gate progress in EP shuffle.
|
||||
item_data.classification = ItemClassification.useful
|
||||
elif item_name in ["River Monastery Shortcut (Door)", "Jungle & River Shortcuts",
|
||||
"Monastery Shortcut (Door)",
|
||||
"Orchard Second Gate (Door)"]:
|
||||
# Downgrade doors that don't gate progress.
|
||||
item_data.classification = ItemClassification.useful
|
||||
|
||||
self.EXTRA_AMOUNTS = {
|
||||
"Functioning Brain": 1,
|
||||
"Puzzle Skip": get_option_value(multiworld, player, "puzzle_skip_amount")
|
||||
}
|
||||
# Build the mandatory item list.
|
||||
self._mandatory_items: dict[str, int] = {}
|
||||
|
||||
for k, v in self.ITEM_TABLE.items():
|
||||
if v.progression and not self.is_progression(k, multiworld, player):
|
||||
self.ITEM_TABLE[k] = ItemData(v.code, False, False, never_exclude=True)
|
||||
|
||||
for item in StaticWitnessLogic.ALL_SYMBOL_ITEMS.union(StaticWitnessLogic.ALL_DOOR_ITEMS):
|
||||
if item[0] not in logic.PROG_ITEMS_ACTUALLY_IN_THE_GAME:
|
||||
del self.ITEM_TABLE[item[0]]
|
||||
if item in StaticWitnessLogic.ALL_SYMBOL_ITEMS:
|
||||
self.SYMBOLS_NOT_IN_THE_GAME.append(StaticWitnessItems.ALL_ITEM_TABLE[item[0]].code)
|
||||
# Add progression items to the mandatory item list.
|
||||
for item_name, item_data in {name: data for (name, data) in self.item_data.items()
|
||||
if data.classification == ItemClassification.progression}.items():
|
||||
if isinstance(item_data.definition, ProgressiveItemDefinition):
|
||||
num_progression = len(self._logic.MULTI_LISTS[item_name])
|
||||
self._mandatory_items[item_name] = num_progression
|
||||
else:
|
||||
if item[0] in StaticWitnessLogic.PROGRESSIVE_TO_ITEMS:
|
||||
self.PROG_ITEM_AMOUNTS[item[0]] = len(logic.MULTI_LISTS[item[0]])
|
||||
self._mandatory_items[item_name] = 1
|
||||
|
||||
self.PROGRESSION_TABLE[item[0]] = self.ITEM_TABLE[item[0]]
|
||||
# Add setting-specific useful items to the mandatory item list.
|
||||
for item_name, item_data in {name: data for (name, data) in self.item_data.items()
|
||||
if data.classification == ItemClassification.useful}.items():
|
||||
if item_name in StaticWitnessItems.special_usefuls:
|
||||
continue
|
||||
elif item_name == "Energy Capacity":
|
||||
self._mandatory_items[item_name] = NUM_ENERGY_UPGRADES
|
||||
elif isinstance(item_data.classification, ProgressiveItemDefinition):
|
||||
self._mandatory_items[item_name] = len(item_data.mappings)
|
||||
else:
|
||||
self._mandatory_items[item_name] = 1
|
||||
|
||||
self.MULTI_LISTS_BY_CODE = dict()
|
||||
# Add event items to the item definition list for later lookup.
|
||||
for event_location in self._locations.EVENT_LOCATION_TABLE:
|
||||
location_name = logic.EVENT_ITEM_PAIRS[event_location]
|
||||
self.item_data[location_name] = ItemData(None, ItemDefinition(0, ItemCategory.EVENT),
|
||||
ItemClassification.progression, False)
|
||||
|
||||
for item in self.PROG_ITEM_AMOUNTS:
|
||||
multi_list = logic.MULTI_LISTS[item]
|
||||
self.MULTI_LISTS_BY_CODE[self.code(item)] = [self.code(single_item) for single_item in multi_list]
|
||||
def get_mandatory_items(self) -> dict[str, int]:
|
||||
"""
|
||||
Returns the list of items that must be in the pool for the game to successfully generate.
|
||||
"""
|
||||
return self._mandatory_items
|
||||
|
||||
for entity_hex, items in logic.DOOR_ITEMS_BY_ID.items():
|
||||
entity_hex_int = int(entity_hex, 16)
|
||||
def get_filler_items(self, quantity: int) -> dict[str, int]:
|
||||
"""
|
||||
Generates a list of filler items of the given length.
|
||||
"""
|
||||
if quantity <= 0:
|
||||
return {}
|
||||
|
||||
self.DOORS.append(entity_hex_int)
|
||||
output: dict[str, int] = {}
|
||||
remaining_quantity = quantity
|
||||
|
||||
for item in items:
|
||||
item_id = StaticWitnessItems.ALL_ITEM_TABLE[item].code
|
||||
self.ITEM_ID_TO_DOOR_HEX.setdefault(item_id, set()).add(entity_hex_int)
|
||||
# Add joke items.
|
||||
output.update({name: 1 for (name, data) in self.item_data.items()
|
||||
if data.definition.category is ItemCategory.JOKE})
|
||||
remaining_quantity -= len(output)
|
||||
|
||||
symbols = is_option_enabled(multiworld, player, "shuffle_symbols")
|
||||
# Read trap configuration data.
|
||||
trap_weight = get_option_value(self._world, self._player_id, "trap_percentage") / 100
|
||||
filler_weight = 1 - trap_weight
|
||||
|
||||
if "shuffle_symbols" not in the_witness_options.keys():
|
||||
symbols = True
|
||||
# Add filler items to the list.
|
||||
filler_items: dict[str, float]
|
||||
filler_items = {name: data.definition.weight if isinstance(data.definition, WeightedItemDefinition) else 1
|
||||
for (name, data) in self.item_data.items() if data.definition.category is ItemCategory.FILLER}
|
||||
filler_items = {name: base_weight * filler_weight / sum(filler_items.values())
|
||||
for name, base_weight in filler_items.items() if base_weight > 0}
|
||||
|
||||
doors = get_option_value(multiworld, player, "shuffle_doors")
|
||||
# Add trap items.
|
||||
if trap_weight > 0:
|
||||
trap_items = {name: data.definition.weight if isinstance(data.definition, WeightedItemDefinition) else 1
|
||||
for (name, data) in self.item_data.items() if data.definition.category is ItemCategory.TRAP}
|
||||
filler_items.update({name: base_weight * trap_weight / sum(trap_items.values())
|
||||
for name, base_weight in trap_items.items() if base_weight > 0})
|
||||
|
||||
self.GOOD_ITEMS = []
|
||||
# Get the actual number of each item by scaling the float weight values to match the target quantity.
|
||||
int_weights: list[int] = build_weighted_int_list(filler_items.values(), remaining_quantity)
|
||||
output.update(zip(filler_items.keys(), int_weights))
|
||||
|
||||
if symbols:
|
||||
self.GOOD_ITEMS = [
|
||||
"Dots", "Black/White Squares", "Stars",
|
||||
"Shapers", "Symmetry"
|
||||
]
|
||||
return output
|
||||
|
||||
if doors:
|
||||
self.GOOD_ITEMS = [
|
||||
"Dots", "Black/White Squares", "Symmetry"
|
||||
]
|
||||
def get_early_items(self) -> list[str]:
|
||||
"""
|
||||
Returns items that are ideal for placing on extremely early checks, like the tutorial gate.
|
||||
"""
|
||||
output: list[str] = []
|
||||
if "shuffle_symbols" not in the_witness_options.keys() \
|
||||
or is_option_enabled(self._world, self._player_id, "shuffle_symbols"):
|
||||
if get_option_value(self._world, self._player_id, "shuffle_doors") > 0:
|
||||
output = ["Dots", "Black/White Squares", "Symmetry"]
|
||||
else:
|
||||
output = ["Dots", "Black/White Squares", "Symmetry", "Shapers", "Stars"]
|
||||
|
||||
if is_option_enabled(multiworld, player, "shuffle_discarded_panels"):
|
||||
if get_option_value(multiworld, player, "puzzle_randomization") == 1:
|
||||
self.GOOD_ITEMS.append("Arrows")
|
||||
if is_option_enabled(self._world, self._player_id, "shuffle_discarded_panels"):
|
||||
if get_option_value(self._world, self._player_id, "puzzle_randomization") == 1:
|
||||
output.append("Arrows")
|
||||
else:
|
||||
self.GOOD_ITEMS.append("Triangles")
|
||||
output.append("Triangles")
|
||||
|
||||
self.GOOD_ITEMS = [
|
||||
StaticWitnessLogic.ITEMS_TO_PROGRESSIVE.get(item, item) for item in self.GOOD_ITEMS
|
||||
]
|
||||
# Replace progressive items with their parents.
|
||||
output = [StaticWitnessLogic.get_parent_progressive_item(item) for item in output]
|
||||
|
||||
for event_location in locat.EVENT_LOCATION_TABLE:
|
||||
location = logic.EVENT_ITEM_PAIRS[event_location]
|
||||
self.EVENT_ITEM_TABLE[location] = ItemData(None, True, True)
|
||||
self.ITEM_TABLE[location] = ItemData(None, True, True)
|
||||
# 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.plando_items[self._player_id]:
|
||||
if plando_setting.get("from_pool", True):
|
||||
if "item" in plando_setting and type(plando_setting["item"]) is str:
|
||||
output.remove(plando_setting["item"])
|
||||
elif "items" in plando_setting:
|
||||
if type(plando_setting["items"]) is dict:
|
||||
output -= [item for item, weight in plando_setting["items"].items() if weight]
|
||||
else:
|
||||
# Assume this is some other kind of iterable.
|
||||
output -= plando_setting["items"]
|
||||
|
||||
trap_percentage = get_option_value(multiworld, player, "trap_percentage")
|
||||
# Sort the output for consistency across versions if the implementation changes but the logic does not.
|
||||
return sorted(output)
|
||||
|
||||
self.JUNK_WEIGHTS = dict()
|
||||
def get_door_ids_in_pool(self) -> list[int]:
|
||||
"""
|
||||
Returns the total set of all door IDs that are controlled by items in the pool.
|
||||
"""
|
||||
output: list[int] = []
|
||||
for item_name, item_data in {name: data for name, data in self.item_data.items()
|
||||
if isinstance(data.definition, DoorItemDefinition)}.items():
|
||||
output += [int(hex_string, 16) for hex_string in item_data.definition.panel_id_hexes]
|
||||
return output
|
||||
|
||||
if trap_percentage != 0:
|
||||
# I'm sure there must be some super "pythonic" way of doing this :D
|
||||
def get_symbol_ids_not_in_pool(self) -> list[int]:
|
||||
"""
|
||||
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()
|
||||
if name not in self.item_data.keys() and data.definition.category is ItemCategory.SYMBOL]
|
||||
|
||||
for trap_name, trap_weight in StaticWitnessItems.TRAP_WEIGHTS.items():
|
||||
self.JUNK_WEIGHTS[trap_name] = (trap_weight * trap_percentage) / 100
|
||||
|
||||
if trap_percentage != 100:
|
||||
for bonus_name, bonus_weight in StaticWitnessItems.BONUS_WEIGHTS.items():
|
||||
self.JUNK_WEIGHTS[bonus_name] = (bonus_weight * (100 - trap_percentage)) / 100
|
||||
|
||||
self.JUNK_WEIGHTS = {
|
||||
key: value for (key, value)
|
||||
in self.JUNK_WEIGHTS.items()
|
||||
if key in self.ITEM_TABLE.keys()
|
||||
}
|
||||
|
||||
# JUNK_WEIGHTS will add up to 1 if the boosts weights and the trap weights each add up to 1 respectively.
|
||||
|
||||
for junk_item in StaticWitnessItems.ALL_JUNK_ITEMS:
|
||||
if junk_item not in self.JUNK_WEIGHTS.keys():
|
||||
del self.ITEM_TABLE[junk_item]
|
||||
def get_progressive_item_ids_in_pool(self) -> dict[int, list[int]]:
|
||||
output: dict[int, list[int]] = {}
|
||||
for item_name, quantity in {name: quantity for name, quantity in self._mandatory_items.items()}.items():
|
||||
item = self.item_data[item_name]
|
||||
if isinstance(item.definition, ProgressiveItemDefinition):
|
||||
# Note: we need to reference the static table here rather than the player-specific one because the child
|
||||
# items were removed from the pool when we pruned out all progression items not in the settings.
|
||||
output[item.ap_code] = [StaticWitnessItems.item_data[child_item].ap_code
|
||||
for child_item in item.definition.child_item_names]
|
||||
return output
|
||||
|
|
|
@ -7,11 +7,13 @@ from .player_logic import WitnessPlayerLogic
|
|||
from .static_logic import StaticWitnessLogic
|
||||
|
||||
|
||||
ID_START = 158000
|
||||
|
||||
|
||||
class StaticWitnessLocations:
|
||||
"""
|
||||
Witness Location Constants that stay consistent across worlds
|
||||
"""
|
||||
ID_START = 158000
|
||||
|
||||
GENERAL_LOCATIONS = {
|
||||
"Tutorial Front Left",
|
||||
|
@ -468,7 +470,7 @@ class WitnessPlayerLocations:
|
|||
victory = get_option_value(world, player, "victory_condition")
|
||||
mount_lasers = get_option_value(world, player, "mountain_lasers")
|
||||
chal_lasers = get_option_value(world, player, "challenge_lasers")
|
||||
laser_shuffle = get_option_value(world, player, "shuffle_lasers")
|
||||
# laser_shuffle = get_option_value(world, player, "shuffle_lasers")
|
||||
|
||||
postgame = set()
|
||||
postgame = postgame | StaticWitnessLocations.CAVES_LOCATIONS
|
||||
|
|
|
@ -16,11 +16,11 @@ When the world has parsed its options, a second function is called to finalize t
|
|||
"""
|
||||
|
||||
import copy
|
||||
from typing import Set, Dict
|
||||
from typing import Set, Dict, cast
|
||||
from logging import warning
|
||||
|
||||
from BaseClasses import MultiWorld
|
||||
from .static_logic import StaticWitnessLogic
|
||||
from .static_logic import StaticWitnessLogic, DoorItemDefinition, ItemCategory, ProgressiveItemDefinition
|
||||
from .utils import define_new_region, get_disable_unrandomized_list, parse_lambda, get_early_utm_list, \
|
||||
get_symbol_shuffle_list, get_door_panel_shuffle_list, get_doors_complex_list, get_doors_max_list, \
|
||||
get_doors_simple_list, get_laser_shuffle, get_ep_all_individual, get_ep_obelisks, get_ep_easy, get_ep_no_eclipse, \
|
||||
|
@ -124,31 +124,41 @@ class WitnessPlayerLogic:
|
|||
|
||||
if adj_type == "Items":
|
||||
line_split = line.split(" - ")
|
||||
item = line_split[0]
|
||||
item_name = line_split[0]
|
||||
|
||||
if item not in StaticWitnessItems.ALL_ITEM_TABLE:
|
||||
raise RuntimeError("Item \"" + item + "\" does not exit.")
|
||||
if item_name not in StaticWitnessItems.item_data:
|
||||
raise RuntimeError("Item \"" + item_name + "\" does not exist.")
|
||||
|
||||
self.THEORETICAL_ITEMS.add(item)
|
||||
self.THEORETICAL_ITEMS_NO_MULTI.update(StaticWitnessLogic.PROGRESSIVE_TO_ITEMS.get(item, [item]))
|
||||
self.THEORETICAL_ITEMS.add(item_name)
|
||||
if isinstance(StaticWitnessLogic.all_items[item_name], ProgressiveItemDefinition):
|
||||
self.THEORETICAL_ITEMS_NO_MULTI.update(cast(ProgressiveItemDefinition,
|
||||
StaticWitnessLogic.all_items[item_name]).child_item_names)
|
||||
else:
|
||||
self.THEORETICAL_ITEMS_NO_MULTI.add(item_name)
|
||||
|
||||
if item in StaticWitnessLogic.ALL_DOOR_ITEMS_AS_DICT:
|
||||
panel_hexes = StaticWitnessLogic.ALL_DOOR_ITEMS_AS_DICT[item][2]
|
||||
if StaticWitnessLogic.all_items[item_name].category in [ItemCategory.DOOR, ItemCategory.LASER]:
|
||||
panel_hexes = cast(DoorItemDefinition, StaticWitnessLogic.all_items[item_name]).panel_id_hexes
|
||||
for panel_hex in panel_hexes:
|
||||
self.DOOR_ITEMS_BY_ID.setdefault(panel_hex, set()).add(item)
|
||||
self.DOOR_ITEMS_BY_ID.setdefault(panel_hex, []).append(item_name)
|
||||
|
||||
return
|
||||
|
||||
if adj_type == "Remove Items":
|
||||
self.THEORETICAL_ITEMS.discard(line)
|
||||
for i in StaticWitnessLogic.PROGRESSIVE_TO_ITEMS.get(line, [line]):
|
||||
self.THEORETICAL_ITEMS_NO_MULTI.discard(i)
|
||||
item_name = line
|
||||
|
||||
if line in StaticWitnessLogic.ALL_DOOR_ITEMS_AS_DICT:
|
||||
panel_hexes = StaticWitnessLogic.ALL_DOOR_ITEMS_AS_DICT[line][2]
|
||||
self.THEORETICAL_ITEMS.discard(item_name)
|
||||
if isinstance(StaticWitnessLogic.all_items[item_name], ProgressiveItemDefinition):
|
||||
self.THEORETICAL_ITEMS_NO_MULTI\
|
||||
.difference_update(cast(ProgressiveItemDefinition,
|
||||
StaticWitnessLogic.all_items[item_name]).child_item_names)
|
||||
else:
|
||||
self.THEORETICAL_ITEMS_NO_MULTI.discard(item_name)
|
||||
|
||||
if StaticWitnessLogic.all_items[item_name].category in [ItemCategory.DOOR, ItemCategory.LASER]:
|
||||
panel_hexes = cast(DoorItemDefinition, StaticWitnessLogic.all_items[item_name]).panel_id_hexes
|
||||
for panel_hex in panel_hexes:
|
||||
if panel_hex in self.DOOR_ITEMS_BY_ID:
|
||||
self.DOOR_ITEMS_BY_ID[panel_hex].discard(line)
|
||||
self.DOOR_ITEMS_BY_ID[panel_hex].remove(item_name)
|
||||
|
||||
if adj_type == "Starting Inventory":
|
||||
self.STARTING_INVENTORY.add(line)
|
||||
|
@ -186,7 +196,9 @@ class WitnessPlayerLogic:
|
|||
|
||||
if len(line_split) > 2:
|
||||
required_items = parse_lambda(line_split[2])
|
||||
items_actually_in_the_game = {item[0] for item in StaticWitnessLogic.ALL_SYMBOL_ITEMS}
|
||||
items_actually_in_the_game = [item_name for item_name, item_definition
|
||||
in StaticWitnessLogic.all_items.items()
|
||||
if item_definition.category is ItemCategory.SYMBOL]
|
||||
required_items = frozenset(
|
||||
subset.intersection(items_actually_in_the_game)
|
||||
for subset in required_items
|
||||
|
@ -337,12 +349,14 @@ class WitnessPlayerLogic:
|
|||
|
||||
for item in self.PROG_ITEMS_ACTUALLY_IN_THE_GAME_NO_MULTI:
|
||||
if item not in self.THEORETICAL_ITEMS:
|
||||
corresponding_multi = StaticWitnessLogic.ITEMS_TO_PROGRESSIVE[item]
|
||||
self.PROG_ITEMS_ACTUALLY_IN_THE_GAME.add(corresponding_multi)
|
||||
multi_list = StaticWitnessLogic.PROGRESSIVE_TO_ITEMS[StaticWitnessLogic.ITEMS_TO_PROGRESSIVE[item]]
|
||||
multi_list = [item for item in multi_list if item in self.PROG_ITEMS_ACTUALLY_IN_THE_GAME_NO_MULTI]
|
||||
progressive_item_name = StaticWitnessLogic.get_parent_progressive_item(item)
|
||||
self.PROG_ITEMS_ACTUALLY_IN_THE_GAME.add(progressive_item_name)
|
||||
child_items = cast(ProgressiveItemDefinition,
|
||||
StaticWitnessLogic.all_items[progressive_item_name]).child_item_names
|
||||
multi_list = [child_item for child_item in child_items
|
||||
if child_item in self.PROG_ITEMS_ACTUALLY_IN_THE_GAME_NO_MULTI]
|
||||
self.MULTI_AMOUNTS[item] = multi_list.index(item) + 1
|
||||
self.MULTI_LISTS[corresponding_multi] = multi_list
|
||||
self.MULTI_LISTS[progressive_item_name] = multi_list
|
||||
else:
|
||||
self.PROG_ITEMS_ACTUALLY_IN_THE_GAME.add(item)
|
||||
|
||||
|
@ -407,7 +421,7 @@ class WitnessPlayerLogic:
|
|||
self.MULTI_LISTS = dict()
|
||||
self.PROG_ITEMS_ACTUALLY_IN_THE_GAME_NO_MULTI = set()
|
||||
self.PROG_ITEMS_ACTUALLY_IN_THE_GAME = set()
|
||||
self.DOOR_ITEMS_BY_ID = dict()
|
||||
self.DOOR_ITEMS_BY_ID: dict[str, list[int]] = {}
|
||||
self.STARTING_INVENTORY = set()
|
||||
|
||||
self.DIFFICULTY = get_option_value(world, player, "puzzle_randomization")
|
||||
|
|
|
@ -156,15 +156,15 @@ class WitnessLogic(LogicMixin):
|
|||
if not (direct_access or theater_from_town and tunnels_from_town):
|
||||
valid_option = False
|
||||
break
|
||||
|
||||
|
||||
elif item in player_logic.EVENT_PANELS:
|
||||
if not self._witness_can_solve_panel(item, world, player, player_logic, locat):
|
||||
valid_option = False
|
||||
break
|
||||
elif not self.has(item, player):
|
||||
prog_dict = StaticWitnessLogic.ITEMS_TO_PROGRESSIVE
|
||||
if not (item in prog_dict and self.has(prog_dict[item], player, player_logic.MULTI_AMOUNTS[item])):
|
||||
# The player doesn't have the item. Check to see if it's part of a progressive item and, if so, the
|
||||
# player has enough of that.
|
||||
prog_item = StaticWitnessLogic.get_parent_progressive_item(item)
|
||||
if prog_item is item or not self.has(prog_item, player, player_logic.MULTI_AMOUNTS[item]):
|
||||
valid_option = False
|
||||
break
|
||||
|
||||
|
|
|
@ -1,7 +1,53 @@
|
|||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
|
||||
from .utils import define_new_region, parse_lambda, lazy, get_items, get_sigma_normal_logic, get_sigma_expert_logic,\
|
||||
get_vanilla_logic
|
||||
|
||||
|
||||
class ItemCategory(Enum):
|
||||
SYMBOL = 0
|
||||
DOOR = 1
|
||||
LASER = 2
|
||||
USEFUL = 3
|
||||
FILLER = 4
|
||||
TRAP = 5
|
||||
JOKE = 6
|
||||
EVENT = 7
|
||||
|
||||
|
||||
CATEGORY_NAME_MAPPINGS: dict[str, ItemCategory] = {
|
||||
"Symbols:": ItemCategory.SYMBOL,
|
||||
"Doors:": ItemCategory.DOOR,
|
||||
"Lasers:": ItemCategory.LASER,
|
||||
"Useful:": ItemCategory.USEFUL,
|
||||
"Filler:": ItemCategory.FILLER,
|
||||
"Traps:": ItemCategory.TRAP,
|
||||
"Jokes:": ItemCategory.JOKE
|
||||
}
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ItemDefinition:
|
||||
local_code: int
|
||||
category: ItemCategory
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ProgressiveItemDefinition(ItemDefinition):
|
||||
child_item_names: list[str]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class DoorItemDefinition(ItemDefinition):
|
||||
panel_id_hexes: list[str]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class WeightedItemDefinition(ItemDefinition):
|
||||
weight: int
|
||||
|
||||
|
||||
class StaticWitnessLogicObj:
|
||||
def read_logic_file(self, lines):
|
||||
"""
|
||||
|
@ -11,7 +57,7 @@ class StaticWitnessLogicObj:
|
|||
current_region = dict()
|
||||
|
||||
for line in lines:
|
||||
if line == "":
|
||||
if line == "" or line[0] == "#":
|
||||
continue
|
||||
|
||||
if line[-1] == ":":
|
||||
|
@ -131,15 +177,9 @@ class StaticWitnessLogicObj:
|
|||
|
||||
|
||||
class StaticWitnessLogic:
|
||||
ALL_SYMBOL_ITEMS = set()
|
||||
ITEMS_TO_PROGRESSIVE = dict()
|
||||
PROGRESSIVE_TO_ITEMS = dict()
|
||||
ALL_DOOR_ITEMS = set()
|
||||
ALL_DOOR_ITEMS_AS_DICT = dict()
|
||||
ALL_USEFULS = set()
|
||||
ALL_TRAPS = set()
|
||||
ALL_BOOSTS = set()
|
||||
CONNECTIONS_TO_SEVER_BY_DOOR_HEX = dict()
|
||||
# Item data parsed from WitnessItems.txt
|
||||
all_items: dict[str, ItemDefinition] = {}
|
||||
_progressive_lookup: dict[str, str] = {}
|
||||
|
||||
ALL_REGIONS_BY_NAME = dict()
|
||||
STATIC_CONNECTIONS_BY_REGION_NAME = dict()
|
||||
|
@ -154,50 +194,54 @@ class StaticWitnessLogic:
|
|||
|
||||
ENTITY_ID_TO_NAME = dict()
|
||||
|
||||
def parse_items(self):
|
||||
@staticmethod
|
||||
def parse_items():
|
||||
"""
|
||||
Parses currently defined items from WitnessItems.txt
|
||||
"""
|
||||
|
||||
lines = get_items()
|
||||
current_set = self.ALL_SYMBOL_ITEMS
|
||||
lines: list[str] = get_items()
|
||||
current_category: ItemCategory = ItemCategory.SYMBOL
|
||||
|
||||
for line in lines:
|
||||
if line == "Progression:":
|
||||
current_set = self.ALL_SYMBOL_ITEMS
|
||||
# Skip empty lines and comments.
|
||||
if line == "" or line[0] == "#":
|
||||
continue
|
||||
if line == "Boosts:":
|
||||
current_set = self.ALL_BOOSTS
|
||||
continue
|
||||
if line == "Traps:":
|
||||
current_set = self.ALL_TRAPS
|
||||
continue
|
||||
if line == "Usefuls:":
|
||||
current_set = self.ALL_USEFULS
|
||||
continue
|
||||
if line == "Doors:":
|
||||
current_set = self.ALL_DOOR_ITEMS
|
||||
continue
|
||||
if line == "":
|
||||
|
||||
# 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(" - ")
|
||||
|
||||
if current_set is self.ALL_USEFULS:
|
||||
current_set.add((line_split[1], int(line_split[0]), line_split[2] == "True"))
|
||||
elif current_set is self.ALL_DOOR_ITEMS:
|
||||
new_door = (line_split[1], int(line_split[0]), frozenset(line_split[2].split(",")))
|
||||
current_set.add(new_door)
|
||||
self.ALL_DOOR_ITEMS_AS_DICT[line_split[1]] = new_door
|
||||
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:
|
||||
if len(line_split) > 2:
|
||||
progressive_items = line_split[2].split(",")
|
||||
for i, value in enumerate(progressive_items):
|
||||
self.ITEMS_TO_PROGRESSIVE[value] = line_split[1]
|
||||
self.PROGRESSIVE_TO_ITEMS[line_split[1]] = progressive_items
|
||||
current_set.add((line_split[1], int(line_split[0])))
|
||||
continue
|
||||
current_set.add((line_split[1], int(line_split[0])))
|
||||
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:
|
||||
|
@ -225,4 +269,4 @@ class StaticWitnessLogic:
|
|||
|
||||
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)
|
||||
self.ENTITY_ID_TO_NAME.update(self.sigma_normal.ENTITY_ID_TO_NAME)
|
||||
|
|
|
@ -1,80 +1,37 @@
|
|||
from functools import lru_cache
|
||||
from itertools import accumulate
|
||||
from math import floor
|
||||
from typing import *
|
||||
from fractions import Fraction
|
||||
from pkgutil import get_data
|
||||
|
||||
|
||||
def make_warning_string(any_j: bool, any_u: bool, any_d: bool, all_j: bool, all_u: bool, all_d: bool) -> str:
|
||||
warning_string = ""
|
||||
|
||||
if any_j:
|
||||
if all_j:
|
||||
warning_string += "all "
|
||||
else:
|
||||
warning_string += "some "
|
||||
|
||||
warning_string += "junk"
|
||||
|
||||
if any_u or any_d:
|
||||
if warning_string:
|
||||
warning_string += " and "
|
||||
|
||||
if all_u:
|
||||
warning_string += "all "
|
||||
else:
|
||||
warning_string += "some "
|
||||
|
||||
warning_string += "usefuls"
|
||||
|
||||
if any_d:
|
||||
warning_string += ", including "
|
||||
|
||||
if all_d:
|
||||
warning_string += "all "
|
||||
else:
|
||||
warning_string += "some "
|
||||
|
||||
warning_string += "non-essential door items"
|
||||
|
||||
return warning_string
|
||||
|
||||
|
||||
def best_junk_to_add_based_on_weights(weights: Dict[Any, Fraction], created_junk: Dict[Any, int]):
|
||||
min_error = ("", 2)
|
||||
|
||||
for junk_name, instances in created_junk.items():
|
||||
new_dist = created_junk.copy()
|
||||
new_dist[junk_name] += 1
|
||||
new_dist_length = sum(new_dist.values())
|
||||
new_dist = {key: Fraction(value/1)/new_dist_length for key, value in new_dist.items()}
|
||||
|
||||
errors = {key: abs(new_dist[key] - weights[key]) for key in created_junk.keys()}
|
||||
|
||||
new_min_error = max(errors.values())
|
||||
|
||||
if min_error[1] > new_min_error:
|
||||
min_error = (junk_name, new_min_error)
|
||||
|
||||
return min_error[0]
|
||||
|
||||
|
||||
def weighted_list(weights: Dict[Any, Fraction], length):
|
||||
def build_weighted_int_list(inputs: Collection[float], total: int) -> list[int]:
|
||||
"""
|
||||
Example:
|
||||
weights = {A: 0.3, B: 0.3, C: 0.4}
|
||||
length = 10
|
||||
|
||||
returns: [A, A, A, B, B, B, C, C, C, C]
|
||||
|
||||
Makes sure to match length *exactly*, might approximate as a result
|
||||
Converts a list of floats to a list of ints of a given length, using the Largest Remainder Method.
|
||||
"""
|
||||
vals = accumulate(map(lambda x: x * length, weights.values()), lambda x, y: x + y)
|
||||
output_list = []
|
||||
for k, v in zip(weights.keys(), vals):
|
||||
while len(output_list) < v:
|
||||
output_list.append(k)
|
||||
return output_list
|
||||
|
||||
# Scale the inputs to sum to the desired total.
|
||||
scale_factor: float = total / sum(inputs)
|
||||
scaled_input = [x * scale_factor for x in inputs]
|
||||
|
||||
# Generate whole number counts, always rounding down.
|
||||
rounded_output: list[int] = [floor(x) for x in scaled_input]
|
||||
rounded_sum = sum(rounded_output)
|
||||
|
||||
# If the output's total is insufficient, increment the value that has the largest remainder until we meet our goal.
|
||||
remainders: list[float] = [real - rounded for real, rounded in zip(scaled_input, rounded_output)]
|
||||
while rounded_sum < total:
|
||||
max_remainder = max(remainders)
|
||||
if max_remainder == 0:
|
||||
break
|
||||
|
||||
# Consume the remainder and increment the total for the given target.
|
||||
max_remainder_index = remainders.index(max_remainder)
|
||||
remainders[max_remainder_index] = 0
|
||||
rounded_output[max_remainder_index] += 1
|
||||
rounded_sum += 1
|
||||
|
||||
return rounded_output
|
||||
|
||||
|
||||
def define_new_region(region_string):
|
||||
|
|
Loading…
Reference in New Issue