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:
blastron 2023-07-18 20:02:57 -07:00 committed by GitHub
parent 18c9779815
commit 1f6db12797
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 482 additions and 518 deletions

View File

@ -1,6 +1,6 @@
from typing import Dict, Union from typing import Dict, Union
from BaseClasses import MultiWorld from BaseClasses import MultiWorld
from Options import Toggle, DefaultOnToggle, Option, Range, Choice from Options import Toggle, DefaultOnToggle, Range, Choice
# class HardMode(Toggle): # class HardMode(Toggle):

View File

@ -1,4 +1,4 @@
Progression: Symbols:
0 - Dots 0 - Dots
1 - Colored Dots 1 - Colored Dots
2 - Full Dots 2 - Full Dots
@ -18,16 +18,22 @@ Progression:
200 - Progressive Dots - Dots,Full Dots 200 - Progressive Dots - Dots,Full Dots
260 - Progressive Stars - Stars,Stars + Same Colored Symbol 260 - Progressive Stars - Stars,Stars + Same Colored Symbol
Usefuls: Useful:
101 - Functioning Brain - False 510 - Puzzle Skip
510 - Puzzle Skip - True #511 - Energy Capacity
Boosts: Filler:
500 - Speed Boost 500 - Speed Boost - 1
#501 - Energy Fill (Small) - 6
#502 - Energy Fill - 3
#503 - Energy Fill (Max) - 1
Traps: Traps:
600 - Slowness 600 - Slowness - 8
610 - Power Surge 610 - Power Surge - 2
Jokes:
650 - Functioning Brain
Doors: Doors:
1100 - Glass Factory Entry (Panel) - 0x01A54 1100 - Glass Factory Entry (Panel) - 0x01A54
@ -63,18 +69,6 @@ Doors:
1400 - Caves Mountain Shortcut (Door) - 0x2D73F 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 1600 - Outside Tutorial Outpost Path (Door) - 0x03BA2
1603 - Outside Tutorial Outpost Entry (Door) - 0x0A170 1603 - Outside Tutorial Outpost Entry (Door) - 0x0A170
1606 - Outside Tutorial Outpost Exit (Door) - 0x04CA3 1606 - Outside Tutorial Outpost Exit (Door) - 0x04CA3
@ -198,3 +192,16 @@ Doors:
1981 - Caves Doors to Challenge - 0x019A5,0x0A19A 1981 - Caves Doors to Challenge - 0x019A5,0x0A19A
1984 - Caves Exits to Main Island - 0x2D859,0x2D73F 1984 - Caves Exits to Main Island - 0x2D859,0x2D73F
1987 - Tunnels Doors - 0x27739,0x27263,0x09E87 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

View File

@ -3,19 +3,19 @@ Archipelago init file for The Witness
""" """
import typing 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, \ from .hints import get_always_hint_locations, get_always_hint_items, get_priority_hint_locations, \
get_priority_hint_items, make_hints, generate_joke_hints get_priority_hint_items, make_hints, generate_joke_hints
from worlds.AutoWorld import World, WebWorld from worlds.AutoWorld import World, WebWorld
from .player_logic import WitnessPlayerLogic from .player_logic import WitnessPlayerLogic
from .static_logic import StaticWitnessLogic from .static_logic import StaticWitnessLogic
from .locations import WitnessPlayerLocations, StaticWitnessLocations from .locations import WitnessPlayerLocations, StaticWitnessLocations
from .items import WitnessItem, StaticWitnessItems, WitnessPlayerItems from .items import WitnessItem, StaticWitnessItems, WitnessPlayerItems, ItemData
from .rules import set_rules from .rules import set_rules
from .regions import WitnessRegions from .regions import WitnessRegions
from .Options import is_option_enabled, the_witness_options, get_option_value 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 .utils import get_audio_logs
from logging import warning from logging import warning, error
class WitnessWebWorld(WebWorld): class WitnessWebWorld(WebWorld):
@ -40,39 +40,59 @@ class WitnessWorld(World):
topology_present = False topology_present = False
data_version = 13 data_version = 13
static_logic = StaticWitnessLogic() StaticWitnessLogic()
static_locat = StaticWitnessLocations() StaticWitnessLocations()
static_items = StaticWitnessItems() StaticWitnessItems()
web = WitnessWebWorld() web = WitnessWebWorld()
option_definitions = the_witness_options option_definitions = the_witness_options
item_name_to_id = { 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 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) 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): def _get_slot_data(self):
return { return {
'seed': self.multiworld.per_slot_randoms[self.player].randint(0, 1000000), 'seed': self.multiworld.per_slot_randoms[self.player].randint(0, 1000000),
'victory_location': int(self.player_logic.VICTORY_LOCATION, 16), 'victory_location': int(self.player_logic.VICTORY_LOCATION, 16),
'panelhex_to_id': self.locat.CHECK_PANELHEX_TO_ID, 'panelhex_to_id': self.locat.CHECK_PANELHEX_TO_ID,
'item_id_to_door_hexes': self.static_items.ITEM_ID_TO_DOOR_HEX_ALL, 'item_id_to_door_hexes': StaticWitnessItems.get_item_to_door_mappings(),
'door_hexes_in_the_pool': self.items.DOORS, 'door_hexes_in_the_pool': self.items.get_door_ids_in_pool(),
'symbols_not_in_the_game': self.items.SYMBOLS_NOT_IN_THE_GAME, 'symbols_not_in_the_game': self.items.get_symbol_ids_not_in_pool(),
'disabled_panels': list(self.player_logic.COMPLETELY_DISABLED_CHECKS), 'disabled_panels': list(self.player_logic.COMPLETELY_DISABLED_CHECKS),
'log_ids_to_hints': self.log_ids_to_hints, 'log_ids_to_hints': self.log_ids_to_hints,
'progressive_item_lists': self.items.MULTI_LISTS_BY_CODE, 'progressive_item_lists': self.items.get_progressive_item_ids_in_pool(),
'obelisk_side_id_to_EPs': self.static_logic.OBELISK_SIDE_ID_TO_EP_HEXES, 'obelisk_side_id_to_EPs': StaticWitnessLogic.OBELISK_SIDE_ID_TO_EP_HEXES,
'precompleted_puzzles': [int(h, 16) for h in 'precompleted_puzzles': [int(h, 16) for h in
self.player_logic.EXCLUDED_LOCATIONS | self.player_logic.PRECOMPLETED_LOCATIONS], self.player_logic.EXCLUDED_LOCATIONS | self.player_logic.PRECOMPLETED_LOCATIONS],
'entity_to_name': self.static_logic.ENTITY_ID_TO_NAME, 'entity_to_name': self.static_logic.ENTITY_ID_TO_NAME,
} }
def generate_early(self): 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") if not (is_option_enabled(self.multiworld, self.player, "shuffle_symbols")
or get_option_value(self.multiworld, self.player, "shuffle_doors") 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," raise Exception("This Witness world doesn't have any progression items. Please turn on Symbol Shuffle,"
" Door Shuffle or Laser 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): def create_regions(self):
self.regio.create_regions(self.multiworld, self.player, self.player_logic) self.regio.create_regions(self.multiworld, self.player, self.player_logic)
def create_items(self): 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]: # Determine pool size. Note that the dog location is included in the location list, so this needs to be -1.
if precol_item.name in self.items_by_name: # if item is in the pool, remove 1 instance. pool_size: int = len(self.locat.CHECK_LOCATION_TABLE) - len(self.locat.EVENT_LOCATION_TABLE) - 1
item_obj = self.items_by_name[precol_item.name]
if item_obj in pool: # Fill mandatory items and remove precollected and/or starting items from the pool.
pool.remove(item_obj) # remove one instance of this pre-collected item if it exists item_pool: dict[str, int] = self.items.get_mandatory_items()
for item in self.player_logic.STARTING_INVENTORY: for precollected_item_name in [item.name for item in self.multiworld.precollected_items[self.player]]:
self.multiworld.push_precollected(self.items_by_name[item]) if precollected_item_name in item_pool:
pool.remove(self.items_by_name[item]) 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 inventory_item_name in self.player_logic.STARTING_INVENTORY:
for i in range(0, self.items.EXTRA_AMOUNTS[item]): if inventory_item_name in item_pool:
witness_item = self.create_item(item) if item_pool[inventory_item_name] == 1:
pool.append(witness_item) 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: for event_location in self.locat.EVENT_LOCATION_TABLE:
item_obj = self.create_item( item_obj = self.create_item(
self.player_logic.EVENT_ITEM_PAIRS[event_location] self.player_logic.EVENT_ITEM_PAIRS[event_location]
@ -134,113 +157,34 @@ class WitnessWorld(World):
location_obj = self.multiworld.get_location(event_location, self.player) location_obj = self.multiworld.get_location(event_location, self.player)
location_obj.place_locked_item(item_obj) 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 # BAD DOG GET BACK HERE WITH THAT PUZZLE SKIP YOU'RE POLLUTING THE ITEM POOL
itempool_difference = len(self.locat.CHECK_LOCATION_TABLE) - len(self.locat.EVENT_LOCATION_TABLE) - 1 self.multiworld.get_location("Town Pet the Dog", self.player)\
itempool_difference -= len(pool) .place_locked_item(self.create_item("Puzzle Skip"))
# 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]
# 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: 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: else:
first_check = self.multiworld.get_location( # Force the item onto the tutorial gate check and remove it from our random pool.
"Tutorial Gate Open", self.player 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) # Generate the actual items.
pool.remove(item) 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( # Sort the output for consistency across versions if the implementation changes but the logic does not.
"Town Pet the Dog", self.player self.multiworld.itempool = sorted(self.multiworld.itempool, key=lambda item: item.name)
)
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
def set_rules(self): def set_rules(self):
set_rules(self.multiworld, self.player, self.player_logic, self.locat) set_rules(self.multiworld, self.player, self.player_logic, self.locat)
@ -291,33 +235,15 @@ class WitnessWorld(World):
return slot_data 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 # 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_data: ItemData
item = self.items.ITEM_TABLE[name] if hasattr(self, 'items') and self.items and item_name in self.items.item_data:
item_data = self.items.item_data[item_name]
else: else:
item = StaticWitnessItems.ALL_ITEM_TABLE[name] item_data = StaticWitnessItems.item_data[item_name]
if item.trap: return WitnessItem(item_name, item_data.classification, item_data.ap_code, player=self.player)
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
class WitnessLocation(Location): class WitnessLocation(Location):

View File

@ -2,24 +2,30 @@
Defines progression, junk and event items for The Witness Defines progression, junk and event items for The Witness
""" """
import copy import copy
from collections import defaultdict from dataclasses import dataclass
from typing import Dict, NamedTuple, Optional, Set from typing import Optional
from BaseClasses import Item, MultiWorld from BaseClasses import Item, MultiWorld, ItemClassification
from . import StaticWitnessLogic, WitnessPlayerLocations, WitnessPlayerLogic
from .Options import get_option_value, is_option_enabled, the_witness_options 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 ItemData for an item in The Witness
""" """
code: Optional[int] ap_code: Optional[int]
progression: bool definition: ItemDefinition
event: bool = False classification: ItemClassification
trap: bool = False local_only: bool = False
never_exclude: bool = False
class WitnessItem(Item): class WitnessItem(Item):
@ -33,75 +39,50 @@ class StaticWitnessItems:
""" """
Class that handles Witness items independent of world settings 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] = {} # 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.
ITEM_NAME_GROUPS: Dict[str, Set[str]] = dict() special_usefuls: list[str] = ["Puzzle Skip"]
# 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()
def __init__(self): 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 definition.category is ItemCategory.SYMBOL:
if item[0] == "11 Lasers" or item == "7 Lasers": classification = ItemClassification.progression
continue 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]) @staticmethod
def get_item_to_door_mappings() -> dict[int, list[int]]:
for progressive, item_list in StaticWitnessLogic.PROGRESSIVE_TO_ITEMS.items(): output: dict[int, list[int]] = {}
if not item_list: for item_name, item_data in {name: data for name, data in StaticWitnessItems.item_data.items()
continue if isinstance(data.definition, DoorItemDefinition)}.items():
item = StaticWitnessItems.item_data[item_name]
if item_list[0] in self.ITEM_NAME_GROUPS.setdefault("Symbols", set()): output[item.ap_code] = [int(hex_string, 16) for hex_string in item_data.definition.panel_id_hexes]
self.ITEM_NAME_GROUPS.setdefault("Symbols", set()).add(progressive) return output
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]}
class WitnessPlayerItems: class WitnessPlayerItems:
@ -109,138 +90,171 @@ class WitnessPlayerItems:
Class that defines Items for a single world Class that defines Items for a single world
""" """
@staticmethod def __init__(self, multiworld: MultiWorld, player: int, logic: WitnessPlayerLogic, locat: WitnessPlayerLocations):
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):
"""Adds event items after logic changes due to options""" """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() # Duplicate the static item data, then make any player-specific adjustments to classification.
self.DOORS = list() 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 = { # Build the mandatory item list.
"Functioning Brain": 1, self._mandatory_items: dict[str, int] = {}
"Puzzle Skip": get_option_value(multiworld, player, "puzzle_skip_amount")
}
for k, v in self.ITEM_TABLE.items(): # Add progression items to the mandatory item list.
if v.progression and not self.is_progression(k, multiworld, player): for item_name, item_data in {name: data for (name, data) in self.item_data.items()
self.ITEM_TABLE[k] = ItemData(v.code, False, False, never_exclude=True) if data.classification == ItemClassification.progression}.items():
if isinstance(item_data.definition, ProgressiveItemDefinition):
for item in StaticWitnessLogic.ALL_SYMBOL_ITEMS.union(StaticWitnessLogic.ALL_DOOR_ITEMS): num_progression = len(self._logic.MULTI_LISTS[item_name])
if item[0] not in logic.PROG_ITEMS_ACTUALLY_IN_THE_GAME: self._mandatory_items[item_name] = num_progression
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)
else: else:
if item[0] in StaticWitnessLogic.PROGRESSIVE_TO_ITEMS: self._mandatory_items[item_name] = 1
self.PROG_ITEM_AMOUNTS[item[0]] = len(logic.MULTI_LISTS[item[0]])
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()
self.MULTI_LISTS_BY_CODE = dict() if data.classification == ItemClassification.useful}.items():
if item_name in StaticWitnessItems.special_usefuls:
for item in self.PROG_ITEM_AMOUNTS: continue
multi_list = logic.MULTI_LISTS[item] elif item_name == "Energy Capacity":
self.MULTI_LISTS_BY_CODE[self.code(item)] = [self.code(single_item) for single_item in multi_list] self._mandatory_items[item_name] = NUM_ENERGY_UPGRADES
elif isinstance(item_data.classification, ProgressiveItemDefinition):
for entity_hex, items in logic.DOOR_ITEMS_BY_ID.items(): self._mandatory_items[item_name] = len(item_data.mappings)
entity_hex_int = int(entity_hex, 16)
self.DOORS.append(entity_hex_int)
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)
symbols = is_option_enabled(multiworld, player, "shuffle_symbols")
if "shuffle_symbols" not in the_witness_options.keys():
symbols = True
doors = get_option_value(multiworld, player, "shuffle_doors")
self.GOOD_ITEMS = []
if symbols:
self.GOOD_ITEMS = [
"Dots", "Black/White Squares", "Stars",
"Shapers", "Symmetry"
]
if doors:
self.GOOD_ITEMS = [
"Dots", "Black/White Squares", "Symmetry"
]
if is_option_enabled(multiworld, player, "shuffle_discarded_panels"):
if get_option_value(multiworld, player, "puzzle_randomization") == 1:
self.GOOD_ITEMS.append("Arrows")
else: else:
self.GOOD_ITEMS.append("Triangles") self._mandatory_items[item_name] = 1
self.GOOD_ITEMS = [ # Add event items to the item definition list for later lookup.
StaticWitnessLogic.ITEMS_TO_PROGRESSIVE.get(item, item) for item in self.GOOD_ITEMS 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 event_location in locat.EVENT_LOCATION_TABLE: def get_mandatory_items(self) -> dict[str, int]:
location = logic.EVENT_ITEM_PAIRS[event_location] """
self.EVENT_ITEM_TABLE[location] = ItemData(None, True, True) Returns the list of items that must be in the pool for the game to successfully generate.
self.ITEM_TABLE[location] = ItemData(None, True, True) """
return self._mandatory_items
trap_percentage = get_option_value(multiworld, player, "trap_percentage") 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.JUNK_WEIGHTS = dict() output: dict[str, int] = {}
remaining_quantity = quantity
if trap_percentage != 0: # Add joke items.
# I'm sure there must be some super "pythonic" way of doing this :D output.update({name: 1 for (name, data) in self.item_data.items()
if data.definition.category is ItemCategory.JOKE})
remaining_quantity -= len(output)
for trap_name, trap_weight in StaticWitnessItems.TRAP_WEIGHTS.items(): # Read trap configuration data.
self.JUNK_WEIGHTS[trap_name] = (trap_weight * trap_percentage) / 100 trap_weight = get_option_value(self._world, self._player_id, "trap_percentage") / 100
filler_weight = 1 - trap_weight
if trap_percentage != 100: # Add filler items to the list.
for bonus_name, bonus_weight in StaticWitnessItems.BONUS_WEIGHTS.items(): filler_items: dict[str, float]
self.JUNK_WEIGHTS[bonus_name] = (bonus_weight * (100 - trap_percentage)) / 100 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}
self.JUNK_WEIGHTS = { # Add trap items.
key: value for (key, value) if trap_weight > 0:
in self.JUNK_WEIGHTS.items() trap_items = {name: data.definition.weight if isinstance(data.definition, WeightedItemDefinition) else 1
if key in self.ITEM_TABLE.keys() 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})
# JUNK_WEIGHTS will add up to 1 if the boosts weights and the trap weights each add up to 1 respectively. # 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))
for junk_item in StaticWitnessItems.ALL_JUNK_ITEMS: return output
if junk_item not in self.JUNK_WEIGHTS.keys():
del self.ITEM_TABLE[junk_item] 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(self._world, self._player_id, "shuffle_discarded_panels"):
if get_option_value(self._world, self._player_id, "puzzle_randomization") == 1:
output.append("Arrows")
else:
output.append("Triangles")
# Replace progressive items with their parents.
output = [StaticWitnessLogic.get_parent_progressive_item(item) for item in output]
# Remove items that are mentioned in any plando options. (Hopefully, in the future, plando will get resolved
# before create_items so that we'll be able to check placed items instead of just removing all items mentioned
# 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"]
# Sort the output for consistency across versions if the implementation changes but the logic does not.
return sorted(output)
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
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]
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

View File

@ -7,11 +7,13 @@ from .player_logic import WitnessPlayerLogic
from .static_logic import StaticWitnessLogic from .static_logic import StaticWitnessLogic
ID_START = 158000
class StaticWitnessLocations: class StaticWitnessLocations:
""" """
Witness Location Constants that stay consistent across worlds Witness Location Constants that stay consistent across worlds
""" """
ID_START = 158000
GENERAL_LOCATIONS = { GENERAL_LOCATIONS = {
"Tutorial Front Left", "Tutorial Front Left",
@ -468,7 +470,7 @@ class WitnessPlayerLocations:
victory = get_option_value(world, player, "victory_condition") victory = get_option_value(world, player, "victory_condition")
mount_lasers = get_option_value(world, player, "mountain_lasers") mount_lasers = get_option_value(world, player, "mountain_lasers")
chal_lasers = get_option_value(world, player, "challenge_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 = set()
postgame = postgame | StaticWitnessLocations.CAVES_LOCATIONS postgame = postgame | StaticWitnessLocations.CAVES_LOCATIONS

View File

@ -16,11 +16,11 @@ When the world has parsed its options, a second function is called to finalize t
""" """
import copy import copy
from typing import Set, Dict from typing import Set, Dict, cast
from logging import warning from logging import warning
from BaseClasses import MultiWorld 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, \ 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_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, \ 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": if adj_type == "Items":
line_split = line.split(" - ") line_split = line.split(" - ")
item = line_split[0] item_name = line_split[0]
if item not in StaticWitnessItems.ALL_ITEM_TABLE: if item_name not in StaticWitnessItems.item_data:
raise RuntimeError("Item \"" + item + "\" does not exit.") raise RuntimeError("Item \"" + item_name + "\" does not exist.")
self.THEORETICAL_ITEMS.add(item) self.THEORETICAL_ITEMS.add(item_name)
self.THEORETICAL_ITEMS_NO_MULTI.update(StaticWitnessLogic.PROGRESSIVE_TO_ITEMS.get(item, [item])) 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: if StaticWitnessLogic.all_items[item_name].category in [ItemCategory.DOOR, ItemCategory.LASER]:
panel_hexes = StaticWitnessLogic.ALL_DOOR_ITEMS_AS_DICT[item][2] panel_hexes = cast(DoorItemDefinition, StaticWitnessLogic.all_items[item_name]).panel_id_hexes
for panel_hex in panel_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 return
if adj_type == "Remove Items": if adj_type == "Remove Items":
self.THEORETICAL_ITEMS.discard(line) item_name = line
for i in StaticWitnessLogic.PROGRESSIVE_TO_ITEMS.get(line, [line]):
self.THEORETICAL_ITEMS_NO_MULTI.discard(i)
if line in StaticWitnessLogic.ALL_DOOR_ITEMS_AS_DICT: self.THEORETICAL_ITEMS.discard(item_name)
panel_hexes = StaticWitnessLogic.ALL_DOOR_ITEMS_AS_DICT[line][2] 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: for panel_hex in panel_hexes:
if panel_hex in self.DOOR_ITEMS_BY_ID: 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": if adj_type == "Starting Inventory":
self.STARTING_INVENTORY.add(line) self.STARTING_INVENTORY.add(line)
@ -186,7 +196,9 @@ class WitnessPlayerLogic:
if len(line_split) > 2: if len(line_split) > 2:
required_items = parse_lambda(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( required_items = frozenset(
subset.intersection(items_actually_in_the_game) subset.intersection(items_actually_in_the_game)
for subset in required_items for subset in required_items
@ -337,12 +349,14 @@ class WitnessPlayerLogic:
for item in self.PROG_ITEMS_ACTUALLY_IN_THE_GAME_NO_MULTI: for item in self.PROG_ITEMS_ACTUALLY_IN_THE_GAME_NO_MULTI:
if item not in self.THEORETICAL_ITEMS: if item not in self.THEORETICAL_ITEMS:
corresponding_multi = StaticWitnessLogic.ITEMS_TO_PROGRESSIVE[item] progressive_item_name = StaticWitnessLogic.get_parent_progressive_item(item)
self.PROG_ITEMS_ACTUALLY_IN_THE_GAME.add(corresponding_multi) self.PROG_ITEMS_ACTUALLY_IN_THE_GAME.add(progressive_item_name)
multi_list = StaticWitnessLogic.PROGRESSIVE_TO_ITEMS[StaticWitnessLogic.ITEMS_TO_PROGRESSIVE[item]] child_items = cast(ProgressiveItemDefinition,
multi_list = [item for item in multi_list if item in self.PROG_ITEMS_ACTUALLY_IN_THE_GAME_NO_MULTI] 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_AMOUNTS[item] = multi_list.index(item) + 1
self.MULTI_LISTS[corresponding_multi] = multi_list self.MULTI_LISTS[progressive_item_name] = multi_list
else: else:
self.PROG_ITEMS_ACTUALLY_IN_THE_GAME.add(item) self.PROG_ITEMS_ACTUALLY_IN_THE_GAME.add(item)
@ -407,7 +421,7 @@ class WitnessPlayerLogic:
self.MULTI_LISTS = dict() self.MULTI_LISTS = dict()
self.PROG_ITEMS_ACTUALLY_IN_THE_GAME_NO_MULTI = set() self.PROG_ITEMS_ACTUALLY_IN_THE_GAME_NO_MULTI = set()
self.PROG_ITEMS_ACTUALLY_IN_THE_GAME = 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.STARTING_INVENTORY = set()
self.DIFFICULTY = get_option_value(world, player, "puzzle_randomization") self.DIFFICULTY = get_option_value(world, player, "puzzle_randomization")

View File

@ -156,15 +156,15 @@ class WitnessLogic(LogicMixin):
if not (direct_access or theater_from_town and tunnels_from_town): if not (direct_access or theater_from_town and tunnels_from_town):
valid_option = False valid_option = False
break break
elif item in player_logic.EVENT_PANELS: elif item in player_logic.EVENT_PANELS:
if not self._witness_can_solve_panel(item, world, player, player_logic, locat): if not self._witness_can_solve_panel(item, world, player, player_logic, locat):
valid_option = False valid_option = False
break break
elif not self.has(item, player): elif not self.has(item, player):
prog_dict = StaticWitnessLogic.ITEMS_TO_PROGRESSIVE # The player doesn't have the item. Check to see if it's part of a progressive item and, if so, the
if not (item in prog_dict and self.has(prog_dict[item], player, player_logic.MULTI_AMOUNTS[item])): # 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 valid_option = False
break break

View File

@ -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,\ from .utils import define_new_region, parse_lambda, lazy, get_items, get_sigma_normal_logic, get_sigma_expert_logic,\
get_vanilla_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: class StaticWitnessLogicObj:
def read_logic_file(self, lines): def read_logic_file(self, lines):
""" """
@ -11,7 +57,7 @@ class StaticWitnessLogicObj:
current_region = dict() current_region = dict()
for line in lines: for line in lines:
if line == "": if line == "" or line[0] == "#":
continue continue
if line[-1] == ":": if line[-1] == ":":
@ -131,15 +177,9 @@ class StaticWitnessLogicObj:
class StaticWitnessLogic: class StaticWitnessLogic:
ALL_SYMBOL_ITEMS = set() # Item data parsed from WitnessItems.txt
ITEMS_TO_PROGRESSIVE = dict() all_items: dict[str, ItemDefinition] = {}
PROGRESSIVE_TO_ITEMS = dict() _progressive_lookup: dict[str, str] = {}
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()
ALL_REGIONS_BY_NAME = dict() ALL_REGIONS_BY_NAME = dict()
STATIC_CONNECTIONS_BY_REGION_NAME = dict() STATIC_CONNECTIONS_BY_REGION_NAME = dict()
@ -154,50 +194,54 @@ class StaticWitnessLogic:
ENTITY_ID_TO_NAME = dict() ENTITY_ID_TO_NAME = dict()
def parse_items(self): @staticmethod
def parse_items():
""" """
Parses currently defined items from WitnessItems.txt Parses currently defined items from WitnessItems.txt
""" """
lines = get_items() lines: list[str] = get_items()
current_set = self.ALL_SYMBOL_ITEMS current_category: ItemCategory = ItemCategory.SYMBOL
for line in lines: for line in lines:
if line == "Progression:": # Skip empty lines and comments.
current_set = self.ALL_SYMBOL_ITEMS if line == "" or line[0] == "#":
continue continue
if line == "Boosts:":
current_set = self.ALL_BOOSTS # If this line is a category header, update our cached category.
continue if line in CATEGORY_NAME_MAPPINGS.keys():
if line == "Traps:": current_category = CATEGORY_NAME_MAPPINGS[line]
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 == "":
continue continue
line_split = line.split(" - ") line_split = line.split(" - ")
if current_set is self.ALL_USEFULS: item_code = int(line_split[0])
current_set.add((line_split[1], int(line_split[0]), line_split[2] == "True")) item_name = line_split[1]
elif current_set is self.ALL_DOOR_ITEMS: arguments: list[str] = line_split[2].split(",") if len(line_split) >= 3 else []
new_door = (line_split[1], int(line_split[0]), frozenset(line_split[2].split(",")))
current_set.add(new_door) if current_category in [ItemCategory.DOOR, ItemCategory.LASER]:
self.ALL_DOOR_ITEMS_AS_DICT[line_split[1]] = new_door # 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: else:
if len(line_split) > 2: StaticWitnessLogic.all_items[item_name] = ItemDefinition(item_code, current_category)
progressive_items = line_split[2].split(",")
for i, value in enumerate(progressive_items): @staticmethod
self.ITEMS_TO_PROGRESSIVE[value] = line_split[1] def get_parent_progressive_item(item_name: str):
self.PROGRESSIVE_TO_ITEMS[line_split[1]] = progressive_items """
current_set.add((line_split[1], int(line_split[0]))) Returns the name of the item's progressive parent, if there is one, or the item's name if not.
continue """
current_set.add((line_split[1], int(line_split[0]))) return StaticWitnessLogic._progressive_lookup.get(item_name, item_name)
@lazy @lazy
def sigma_expert(self) -> StaticWitnessLogicObj: def sigma_expert(self) -> StaticWitnessLogicObj:

View File

@ -1,80 +1,37 @@
from functools import lru_cache from functools import lru_cache
from itertools import accumulate from math import floor
from typing import * from typing import *
from fractions import Fraction from fractions import Fraction
from pkgutil import get_data 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: def build_weighted_int_list(inputs: Collection[float], total: int) -> list[int]:
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):
""" """
Example: Converts a list of floats to a list of ints of a given length, using the Largest Remainder Method.
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
""" """
vals = accumulate(map(lambda x: x * length, weights.values()), lambda x, y: x + y)
output_list = [] # Scale the inputs to sum to the desired total.
for k, v in zip(weights.keys(), vals): scale_factor: float = total / sum(inputs)
while len(output_list) < v: scaled_input = [x * scale_factor for x in inputs]
output_list.append(k)
return output_list # 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): def define_new_region(region_string):