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 BaseClasses import MultiWorld
from Options import Toggle, DefaultOnToggle, Option, Range, Choice
from Options import Toggle, DefaultOnToggle, Range, Choice
# class HardMode(Toggle):

View File

@ -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

View File

@ -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):

View File

@ -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

View File

@ -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

View File

@ -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")

View File

@ -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

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,\
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)

View File

@ -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):