Archipelago/worlds/witness/__init__.py

290 lines
12 KiB
Python
Raw Normal View History

"""
Archipelago init file for The Witness
"""
from typing import Dict, Optional
from BaseClasses import Region, Location, MultiWorld, Item, Entrance, Tutorial
2022-10-09 02:13:52 +00:00
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
2022-10-09 02:13:52 +00:00
from .player_logic import WitnessPlayerLogic
from .static_logic import StaticWitnessLogic
from .locations import WitnessPlayerLocations, StaticWitnessLocations
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 get_audio_logs
from logging import warning, error
class WitnessWebWorld(WebWorld):
theme = "jungle"
tutorials = [Tutorial(
"Multiworld Setup Guide",
"A guide to playing The Witness with Archipelago.",
"English",
"setup_en.md",
"setup/en",
["NewSoupVi", "Jarno"]
)]
class WitnessWorld(World):
"""
The Witness is an open-world puzzle game with dozens of locations
to explore and over 500 puzzles. Play the popular puzzle randomizer
by sigma144, with an added layer of progression randomization!
"""
game = "The Witness"
topology_present = False
Witness: Bugfixes in response to beta tests (#1473) * Make all Keep Pressure Plates logically required for the Laser Panel * Added more Tutorial checks * Added the remaining two Shipwreck Boat EPs to the exclude list for normal * Improved itempool filling system, added warning if usefuls had to be eaten * Moved creation of said warning string to utils * Fixed logic bug causing broken seeds on Mountain Floor 2 * Hints system change * Expert Logic Fix * Fixed typo * Better wording * Added missing games to junk hints * Made sure Entrance names are unique * Fixed missing Obelisk Side * Disable Non Randomized + EP Shuffle fix * Fixed disable_non_randomized precompleted EPs being 'disabled' instead of 'precompleted' * Fixed if/elif error * Tutorial Gate Open local symbol item becomes local_early_item in expert instead * Bump required client version. There is a beta client that sends 0.3.9. * Removed print statement, oops * Fixed itempool manipulation in pre_fill * Replaced string concats with fstrings * Improved make_warning_string function signature Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> * Improved performance on removing multiple items from multiworld itempool * Comment * Fixed errors with the code * Made removal from itempool not fail unit test for multiple references * Moved all item creation to create_items, got rid of itempool modifying system * Colored Squares is no longer a good item, that's outdated * Removed double if * React to from_pool: false by removing a junk item * Fixed warning if only Fnc Brain was removed * Make use of string truthiness instead * Made reading of plandoed items safer --------- Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2023-03-02 23:08:24 +00:00
data_version = 13
StaticWitnessLogic()
StaticWitnessLocations()
StaticWitnessItems()
web = WitnessWebWorld()
option_definitions = the_witness_options
item_name_to_id = {
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_groups
Witness: Bugfixes in response to beta tests (#1473) * Make all Keep Pressure Plates logically required for the Laser Panel * Added more Tutorial checks * Added the remaining two Shipwreck Boat EPs to the exclude list for normal * Improved itempool filling system, added warning if usefuls had to be eaten * Moved creation of said warning string to utils * Fixed logic bug causing broken seeds on Mountain Floor 2 * Hints system change * Expert Logic Fix * Fixed typo * Better wording * Added missing games to junk hints * Made sure Entrance names are unique * Fixed missing Obelisk Side * Disable Non Randomized + EP Shuffle fix * Fixed disable_non_randomized precompleted EPs being 'disabled' instead of 'precompleted' * Fixed if/elif error * Tutorial Gate Open local symbol item becomes local_early_item in expert instead * Bump required client version. There is a beta client that sends 0.3.9. * Removed print statement, oops * Fixed itempool manipulation in pre_fill * Replaced string concats with fstrings * Improved make_warning_string function signature Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> * Improved performance on removing multiple items from multiworld itempool * Comment * Fixed errors with the code * Made removal from itempool not fail unit test for multiple references * Moved all item creation to create_items, got rid of itempool modifying system * Colored Squares is no longer a good item, that's outdated * Removed double if * React to from_pool: false by removing a junk item * Fixed warning if only Fnc Brain was removed * Make use of string truthiness instead * Made reading of plandoed items safer --------- Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2023-03-02 23:08:24 +00:00
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': 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),
2022-10-09 02:13:52 +00:00
'log_ids_to_hints': self.log_ids_to_hints,
'progressive_item_lists': self.items.get_progressive_item_ids_in_pool(),
'obelisk_side_id_to_EPs': StaticWitnessLogic.OBELISK_SIDE_ID_TO_EP_HEXES,
'precompleted_puzzles': [int(h, 16) for h in
self.player_logic.EXCLUDED_LOCATIONS | self.player_logic.PRECOMPLETED_LOCATIONS],
'entity_to_name': StaticWitnessLogic.ENTITY_ID_TO_NAME,
}
def generate_early(self):
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")
or is_option_enabled(self.multiworld, self.player, "shuffle_lasers")):
if self.multiworld.players == 1:
warning("This Witness world doesn't have any progression items. Please turn on Symbol Shuffle, Door"
" Shuffle or Laser Shuffle if that doesn't seem right.")
else:
raise Exception("This Witness world doesn't have any progression items. Please turn on Symbol Shuffle,"
" Door Shuffle or Laser Shuffle.")
Witness: Bugfixes in response to beta tests (#1473) * Make all Keep Pressure Plates logically required for the Laser Panel * Added more Tutorial checks * Added the remaining two Shipwreck Boat EPs to the exclude list for normal * Improved itempool filling system, added warning if usefuls had to be eaten * Moved creation of said warning string to utils * Fixed logic bug causing broken seeds on Mountain Floor 2 * Hints system change * Expert Logic Fix * Fixed typo * Better wording * Added missing games to junk hints * Made sure Entrance names are unique * Fixed missing Obelisk Side * Disable Non Randomized + EP Shuffle fix * Fixed disable_non_randomized precompleted EPs being 'disabled' instead of 'precompleted' * Fixed if/elif error * Tutorial Gate Open local symbol item becomes local_early_item in expert instead * Bump required client version. There is a beta client that sends 0.3.9. * Removed print statement, oops * Fixed itempool manipulation in pre_fill * Replaced string concats with fstrings * Improved make_warning_string function signature Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> * Improved performance on removing multiple items from multiworld itempool * Comment * Fixed errors with the code * Made removal from itempool not fail unit test for multiple references * Moved all item creation to create_items, got rid of itempool modifying system * Colored Squares is no longer a good item, that's outdated * Removed double if * React to from_pool: false by removing a junk item * Fixed warning if only Fnc Brain was removed * Make use of string truthiness instead * Made reading of plandoed items safer --------- Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2023-03-02 23:08:24 +00:00
def create_regions(self):
self.regio.create_regions(self.multiworld, self.player, self.player_logic)
def create_items(self):
# 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
# Fill mandatory items and remove precollected and/or starting items from the pool.
item_pool: Dict[str, int] = self.items.get_mandatory_items()
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 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
self.multiworld.push_precollected(self.create_item(inventory_item_name))
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]
)
location_obj = self.multiworld.get_location(event_location, self.player)
location_obj.place_locked_item(item_obj)
# 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)
Witness: Bugfixes in response to beta tests (#1473) * Make all Keep Pressure Plates logically required for the Laser Panel * Added more Tutorial checks * Added the remaining two Shipwreck Boat EPs to the exclude list for normal * Improved itempool filling system, added warning if usefuls had to be eaten * Moved creation of said warning string to utils * Fixed logic bug causing broken seeds on Mountain Floor 2 * Hints system change * Expert Logic Fix * Fixed typo * Better wording * Added missing games to junk hints * Made sure Entrance names are unique * Fixed missing Obelisk Side * Disable Non Randomized + EP Shuffle fix * Fixed disable_non_randomized precompleted EPs being 'disabled' instead of 'precompleted' * Fixed if/elif error * Tutorial Gate Open local symbol item becomes local_early_item in expert instead * Bump required client version. There is a beta client that sends 0.3.9. * Removed print statement, oops * Fixed itempool manipulation in pre_fill * Replaced string concats with fstrings * Improved make_warning_string function signature Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> * Improved performance on removing multiple items from multiworld itempool * Comment * Fixed errors with the code * Made removal from itempool not fail unit test for multiple references * Moved all item creation to create_items, got rid of itempool modifying system * Colored Squares is no longer a good item, that's outdated * Removed double if * React to from_pool: false by removing a junk item * Fixed warning if only Fnc Brain was removed * Make use of string truthiness instead * Made reading of plandoed items safer --------- Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2023-03-02 23:08:24 +00:00
if get_option_value(self.multiworld, self.player, "puzzle_randomization") == 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
Witness: Bugfixes in response to beta tests (#1473) * Make all Keep Pressure Plates logically required for the Laser Panel * Added more Tutorial checks * Added the remaining two Shipwreck Boat EPs to the exclude list for normal * Improved itempool filling system, added warning if usefuls had to be eaten * Moved creation of said warning string to utils * Fixed logic bug causing broken seeds on Mountain Floor 2 * Hints system change * Expert Logic Fix * Fixed typo * Better wording * Added missing games to junk hints * Made sure Entrance names are unique * Fixed missing Obelisk Side * Disable Non Randomized + EP Shuffle fix * Fixed disable_non_randomized precompleted EPs being 'disabled' instead of 'precompleted' * Fixed if/elif error * Tutorial Gate Open local symbol item becomes local_early_item in expert instead * Bump required client version. There is a beta client that sends 0.3.9. * Removed print statement, oops * Fixed itempool manipulation in pre_fill * Replaced string concats with fstrings * Improved make_warning_string function signature Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> * Improved performance on removing multiple items from multiworld itempool * Comment * Fixed errors with the code * Made removal from itempool not fail unit test for multiple references * Moved all item creation to create_items, got rid of itempool modifying system * Colored Squares is no longer a good item, that's outdated * Removed double if * React to from_pool: false by removing a junk item * Fixed warning if only Fnc Brain was removed * Make use of string truthiness instead * Made reading of plandoed items safer --------- Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2023-03-02 23:08:24 +00:00
else:
# 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
# Generate the actual items.
for item_name, quantity in sorted(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)
def set_rules(self):
set_rules(self.multiworld, self.player, self.player_logic, self.locat)
def fill_slot_data(self) -> dict:
hint_amount = get_option_value(self.multiworld, self.player, "hint_amount")
2022-10-09 02:13:52 +00:00
credits_hint = (
"This Randomizer is brought to you by",
"NewSoupVi, Jarno, blastron,",
"jbzdarkid, sigma144, IHNN, oddGarrett, Exempt-Medic.", -1
)
2022-10-09 02:13:52 +00:00
audio_logs = get_audio_logs().copy()
if hint_amount != 0:
generated_hints = make_hints(self.multiworld, self.player, hint_amount)
2022-10-09 02:13:52 +00:00
self.multiworld.per_slot_randoms[self.player].shuffle(audio_logs)
duplicates = min(3, len(audio_logs) // hint_amount)
2022-10-09 02:13:52 +00:00
for _ in range(0, hint_amount):
hint = generated_hints.pop(0)
2022-10-09 02:13:52 +00:00
for _ in range(0, duplicates):
audio_log = audio_logs.pop()
self.log_ids_to_hints[int(audio_log, 16)] = hint
if audio_logs:
audio_log = audio_logs.pop()
self.log_ids_to_hints[int(audio_log, 16)] = credits_hint
joke_hints = generate_joke_hints(self.multiworld, self.player, len(audio_logs))
2022-10-09 02:13:52 +00:00
while audio_logs:
audio_log = audio_logs.pop()
self.log_ids_to_hints[int(audio_log, 16)] = joke_hints.pop()
# generate hints done
slot_data = self._get_slot_data()
for option_name in the_witness_options:
slot_data[option_name] = get_option_value(
self.multiworld, self.player, option_name
)
return slot_data
def create_item(self, item_name: str) -> Item:
# If the player's plando options are malformed, the item_name parameter could be a dictionary containing the
# name of the item, rather than the item itself. This is a workaround to prevent a crash.
if type(item_name) is dict:
item_name = list(item_name.keys())[0]
# this conditional is purely for unit tests, which need to be able to create an item before generate_early
item_data: ItemData
if hasattr(self, 'items') and self.items and item_name in self.items.item_data:
item_data = self.items.item_data[item_name]
else:
item_data = StaticWitnessItems.item_data[item_name]
return WitnessItem(item_name, item_data.classification, item_data.ap_code, player=self.player)
class WitnessLocation(Location):
"""
Archipelago Location for The Witness
"""
game: str = "The Witness"
check_hex: int = -1
def __init__(self, player: int, name: str, address: Optional[int], parent, ch_hex: int = -1):
super().__init__(player, name, address, parent)
self.check_hex = ch_hex
def create_region(world: MultiWorld, player: int, name: str,
locat: WitnessPlayerLocations, region_locations=None, exits=None):
"""
Create an Archipelago Region for The Witness
"""
ret = Region(name, player, world)
if region_locations:
for location in region_locations:
loc_id = locat.CHECK_LOCATION_TABLE[location]
check_hex = -1
if location in StaticWitnessLogic.CHECKS_BY_NAME:
check_hex = int(
StaticWitnessLogic.CHECKS_BY_NAME[location]["checkHex"], 0
)
location = WitnessLocation(
player, location, loc_id, ret, check_hex
)
ret.locations.append(location)
if exits:
for single_exit in exits:
ret.exits.append(Entrance(player, single_exit, ret))
return ret