diff --git a/.github/pyright-config.json b/.github/pyright-config.json index 6ad7fa5f19b5..7d981778905f 100644 --- a/.github/pyright-config.json +++ b/.github/pyright-config.json @@ -16,7 +16,7 @@ "reportMissingImports": true, "reportMissingTypeStubs": true, - "pythonVersion": "3.8", + "pythonVersion": "3.10", "pythonPlatform": "Windows", "executionEnvironments": [ diff --git a/.github/workflows/analyze-modified-files.yml b/.github/workflows/analyze-modified-files.yml index c9995fa2d043..b59336fafe9b 100644 --- a/.github/workflows/analyze-modified-files.yml +++ b/.github/workflows/analyze-modified-files.yml @@ -53,7 +53,7 @@ jobs: - uses: actions/setup-python@v5 if: env.diff != '' with: - python-version: 3.8 + python-version: '3.10' - name: "Install dependencies" if: env.diff != '' diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 23c463fb947a..c013172ea034 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -24,14 +24,14 @@ env: jobs: # build-release-macos: # LF volunteer - build-win-py38: # RCs will still be built and signed by hand + build-win-py310: # RCs will still be built and signed by hand runs-on: windows-latest steps: - uses: actions/checkout@v4 - name: Install python uses: actions/setup-python@v5 with: - python-version: '3.8' + python-version: '3.10' - name: Download run-time dependencies run: | Invoke-WebRequest -Uri https://github.com/Ijwu/Enemizer/releases/download/${Env:ENEMIZER_VERSION}/win-x64.zip -OutFile enemizer.zip diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index b0cfe35d2bc5..3abbb5f6449f 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -47,7 +47,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v2 + uses: github/codeql-action/init@v3 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -58,7 +58,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@v2 + uses: github/codeql-action/autobuild@v3 # ℹī¸ Command-line programs to run using the OS shell. # 📚 https://git.io/JvXDl @@ -72,4 +72,4 @@ jobs: # make release - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 + uses: github/codeql-action/analyze@v3 diff --git a/.github/workflows/unittests.yml b/.github/workflows/unittests.yml index 9a3a6d11217f..88b5d12987ad 100644 --- a/.github/workflows/unittests.yml +++ b/.github/workflows/unittests.yml @@ -33,13 +33,11 @@ jobs: matrix: os: [ubuntu-latest] python: - - {version: '3.8'} - - {version: '3.9'} - {version: '3.10'} - {version: '3.11'} - {version: '3.12'} include: - - python: {version: '3.8'} # win7 compat + - python: {version: '3.10'} # old compat os: windows-latest - python: {version: '3.12'} # current os: windows-latest @@ -89,4 +87,4 @@ jobs: run: | source venv/bin/activate export PYTHONPATH=$(pwd) - python test/hosting/__main__.py + timeout 600 python test/hosting/__main__.py diff --git a/BaseClasses.py b/BaseClasses.py index 0d4f34e51445..2e4efd606df9 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -1,18 +1,16 @@ from __future__ import annotations import collections -import itertools import functools import logging import random import secrets -import typing # this can go away when Python 3.8 support is dropped from argparse import Namespace from collections import Counter, deque from collections.abc import Collection, MutableSequence from enum import IntEnum, IntFlag from typing import (AbstractSet, Any, Callable, ClassVar, Dict, Iterable, Iterator, List, Mapping, NamedTuple, - Optional, Protocol, Set, Tuple, Union, Type) + Optional, Protocol, Set, Tuple, Union, TYPE_CHECKING) from typing_extensions import NotRequired, TypedDict @@ -20,7 +18,7 @@ import Options import Utils -if typing.TYPE_CHECKING: +if TYPE_CHECKING: from worlds import AutoWorld @@ -231,7 +229,7 @@ def set_options(self, args: Namespace) -> None: for player in self.player_ids: world_type = AutoWorld.AutoWorldRegister.world_types[self.game[player]] self.worlds[player] = world_type(self, player) - options_dataclass: typing.Type[Options.PerGameCommonOptions] = world_type.options_dataclass + options_dataclass: type[Options.PerGameCommonOptions] = world_type.options_dataclass self.worlds[player].options = options_dataclass(**{option_key: getattr(args, option_key)[player] for option_key in options_dataclass.type_hints}) @@ -341,7 +339,7 @@ def find_common_pool(players: Set[int], shared_pool: Set[str]) -> Tuple[ new_item.classification |= classifications[item_name] new_itempool.append(new_item) - region = Region("Menu", group_id, self, "ItemLink") + region = Region(group["world"].origin_region_name, group_id, self, "ItemLink") self.regions.append(region) locations = region.locations # ensure that progression items are linked first, then non-progression @@ -975,7 +973,7 @@ class Region: entrances: List[Entrance] exits: List[Entrance] locations: List[Location] - entrance_type: ClassVar[Type[Entrance]] = Entrance + entrance_type: ClassVar[type[Entrance]] = Entrance class Register(MutableSequence): region_manager: MultiWorld.RegionManager @@ -1075,7 +1073,7 @@ def get_connecting_entrance(self, is_main_entrance: Callable[[Entrance], bool]) return entrance.parent_region.get_connecting_entrance(is_main_entrance) def add_locations(self, locations: Dict[str, Optional[int]], - location_type: Optional[Type[Location]] = None) -> None: + location_type: Optional[type[Location]] = None) -> None: """ Adds locations to the Region object, where location_type is your Location class and locations is a dict of location names to address. @@ -1264,6 +1262,10 @@ def useful(self) -> bool: def trap(self) -> bool: return ItemClassification.trap in self.classification + @property + def excludable(self) -> bool: + return not (self.advancement or self.useful) + @property def flags(self) -> int: return self.classification.as_flag() diff --git a/CommonClient.py b/CommonClient.py index 296c10ed4b4e..47100a7383ab 100644 --- a/CommonClient.py +++ b/CommonClient.py @@ -355,6 +355,8 @@ def __init__(self, server_address: typing.Optional[str] = None, password: typing self.item_names = self.NameLookupDict(self, "item") self.location_names = self.NameLookupDict(self, "location") + self.versions = {} + self.checksums = {} self.jsontotextparser = JSONtoTextParser(self) self.rawjsontotextparser = RawJSONtoTextParser(self) @@ -571,26 +573,34 @@ async def prepare_data_package(self, relevant_games: typing.Set[str], needed_updates.add(game) continue - local_version: int = network_data_package["games"].get(game, {}).get("version", 0) - local_checksum: typing.Optional[str] = network_data_package["games"].get(game, {}).get("checksum") - # no action required if local version is new enough - if (not remote_checksum and (remote_version > local_version or remote_version == 0)) \ - or remote_checksum != local_checksum: - cached_game = Utils.load_data_package_for_checksum(game, remote_checksum) - cache_version: int = cached_game.get("version", 0) - cache_checksum: typing.Optional[str] = cached_game.get("checksum") - # download remote version if cache is not new enough - if (not remote_checksum and (remote_version > cache_version or remote_version == 0)) \ - or remote_checksum != cache_checksum: - needed_updates.add(game) + cached_version: int = self.versions.get(game, 0) + cached_checksum: typing.Optional[str] = self.checksums.get(game) + # no action required if cached version is new enough + if (not remote_checksum and (remote_version > cached_version or remote_version == 0)) \ + or remote_checksum != cached_checksum: + local_version: int = network_data_package["games"].get(game, {}).get("version", 0) + local_checksum: typing.Optional[str] = network_data_package["games"].get(game, {}).get("checksum") + if ((remote_checksum or remote_version <= local_version and remote_version != 0) + and remote_checksum == local_checksum): + self.update_game(network_data_package["games"][game], game) else: - self.update_game(cached_game, game) + cached_game = Utils.load_data_package_for_checksum(game, remote_checksum) + cache_version: int = cached_game.get("version", 0) + cache_checksum: typing.Optional[str] = cached_game.get("checksum") + # download remote version if cache is not new enough + if (not remote_checksum and (remote_version > cache_version or remote_version == 0)) \ + or remote_checksum != cache_checksum: + needed_updates.add(game) + else: + self.update_game(cached_game, game) if needed_updates: await self.send_msgs([{"cmd": "GetDataPackage", "games": [game_name]} for game_name in needed_updates]) def update_game(self, game_package: dict, game: str): self.item_names.update_game(game, game_package["item_name_to_id"]) self.location_names.update_game(game, game_package["location_name_to_id"]) + self.versions[game] = game_package.get("version", 0) + self.checksums[game] = game_package.get("checksum") def update_data_package(self, data_package: dict): for game, game_data in data_package["games"].items(): @@ -700,6 +710,11 @@ def run_gui(self): def run_cli(self): if sys.stdin: + if sys.stdin.fileno() != 0: + from multiprocessing import parent_process + if parent_process(): + return # ignore MultiProcessing pipe + # steam overlay breaks when starting console_loop if 'gameoverlayrenderer' in os.environ.get('LD_PRELOAD', ''): logger.info("Skipping terminal input, due to conflicting Steam Overlay detected. Please use GUI only.") diff --git a/Generate.py b/Generate.py index 52babdf18839..8aba72abafe9 100644 --- a/Generate.py +++ b/Generate.py @@ -110,7 +110,7 @@ def main(args=None) -> Tuple[argparse.Namespace, int]: player_files = {} for file in os.scandir(args.player_files_path): fname = file.name - if file.is_file() and not fname.startswith(".") and \ + if file.is_file() and not fname.startswith(".") and not fname.lower().endswith(".ini") and \ os.path.join(args.player_files_path, fname) not in {args.meta_file_path, args.weights_file_path}: path = os.path.join(args.player_files_path, fname) try: @@ -453,6 +453,10 @@ def roll_settings(weights: dict, plando_options: PlandoOptions = PlandoOptions.b raise Exception(f"Option {option_key} has to be in a game's section, not on its own.") ret.game = get_choice("game", weights) + if not isinstance(ret.game, str): + if ret.game is None: + raise Exception('"game" not specified') + raise Exception(f"Invalid game: {ret.game}") if ret.game not in AutoWorldRegister.world_types: from worlds import failed_world_loads picks = Utils.get_fuzzy_results(ret.game, list(AutoWorldRegister.world_types) + failed_world_loads, limit=1)[0] diff --git a/Launcher.py b/Launcher.py index 42f93547cc9d..f04d67a5aa0d 100644 --- a/Launcher.py +++ b/Launcher.py @@ -22,20 +22,21 @@ from shutil import which from typing import Callable, Optional, Sequence, Tuple, Union -import Utils -import settings -from worlds.LauncherComponents import Component, components, Type, SuffixIdentifier, icon_paths - if __name__ == "__main__": import ModuleUpdate ModuleUpdate.update() -from Utils import is_frozen, user_path, local_path, init_logging, open_filename, messagebox, \ - is_windows, is_macos, is_linux +import settings +import Utils +from Utils import (init_logging, is_frozen, is_linux, is_macos, is_windows, local_path, messagebox, open_filename, + user_path) +from worlds.LauncherComponents import Component, components, icon_paths, SuffixIdentifier, Type def open_host_yaml(): - file = settings.get_settings().filename + s = settings.get_settings() + file = s.filename + s.save() assert file, "host.yaml missing" if is_linux: exe = which('sensible-editor') or which('gedit') or \ @@ -102,6 +103,7 @@ def update_settings(): Component("Open host.yaml", func=open_host_yaml), Component("Open Patch", func=open_patch), Component("Generate Template Options", func=generate_yamls), + Component("Archipelago Website", func=lambda: webbrowser.open("https://archipelago.gg/")), Component("Discord Server", icon="discord", func=lambda: webbrowser.open("https://discord.gg/8Z65BR2")), Component("Unrated/18+ Discord Server", icon="discord", func=lambda: webbrowser.open("https://discord.gg/fqvNCCRsu4")), Component("Browse Files", func=browse_files), @@ -179,6 +181,11 @@ def update_label(self, dt): App.get_running_app().stop() Window.close() + def _stop(self, *largs): + # see run_gui Launcher _stop comment for details + self.root_window.close() + super()._stop(*largs) + Popup().run() @@ -252,7 +259,7 @@ class Launcher(App): _client_layout: Optional[ScrollBox] = None def __init__(self, ctx=None): - self.title = self.base_title + self.title = self.base_title + " " + Utils.__version__ self.ctx = ctx self.icon = r"data/icon.png" super().__init__() diff --git a/ModuleUpdate.py b/ModuleUpdate.py index f49182bb7863..dada16cefcaf 100644 --- a/ModuleUpdate.py +++ b/ModuleUpdate.py @@ -5,8 +5,8 @@ import warnings -if sys.version_info < (3, 8, 6): - raise RuntimeError("Incompatible Python Version. 3.8.7+ is supported.") +if sys.version_info < (3, 10, 11): + raise RuntimeError(f"Incompatible Python Version found: {sys.version_info}. 3.10.11+ is supported.") # don't run update if environment is frozen/compiled or if not the parent process (skip in subprocess) _skip_update = bool(getattr(sys, "frozen", False) or multiprocessing.parent_process()) diff --git a/MultiServer.py b/MultiServer.py index 0fe950b5e4f3..847a0b281c40 100644 --- a/MultiServer.py +++ b/MultiServer.py @@ -185,11 +185,9 @@ class Context: slot_info: typing.Dict[int, NetworkSlot] generator_version = Version(0, 0, 0) checksums: typing.Dict[str, str] - item_names: typing.Dict[str, typing.Dict[int, str]] = ( - collections.defaultdict(lambda: Utils.KeyedDefaultDict(lambda code: f'Unknown item (ID:{code})'))) + item_names: typing.Dict[str, typing.Dict[int, str]] item_name_groups: typing.Dict[str, typing.Dict[str, typing.Set[str]]] - location_names: typing.Dict[str, typing.Dict[int, str]] = ( - collections.defaultdict(lambda: Utils.KeyedDefaultDict(lambda code: f'Unknown location (ID:{code})'))) + location_names: typing.Dict[str, typing.Dict[int, str]] location_name_groups: typing.Dict[str, typing.Dict[str, typing.Set[str]]] all_item_and_group_names: typing.Dict[str, typing.Set[str]] all_location_and_group_names: typing.Dict[str, typing.Set[str]] @@ -198,7 +196,6 @@ class Context: """ each sphere is { player: { location_id, ... } } """ logger: logging.Logger - def __init__(self, host: str, port: int, server_password: str, password: str, location_check_points: int, hint_cost: int, item_cheat: bool, release_mode: str = "disabled", collect_mode="disabled", remaining_mode: str = "disabled", auto_shutdown: typing.SupportsFloat = 0, compatibility: int = 2, @@ -269,6 +266,10 @@ def __init__(self, host: str, port: int, server_password: str, password: str, lo self.location_name_groups = {} self.all_item_and_group_names = {} self.all_location_and_group_names = {} + self.item_names = collections.defaultdict( + lambda: Utils.KeyedDefaultDict(lambda code: f'Unknown item (ID:{code})')) + self.location_names = collections.defaultdict( + lambda: Utils.KeyedDefaultDict(lambda code: f'Unknown location (ID:{code})')) self.non_hintable_names = collections.defaultdict(frozenset) self._load_game_data() @@ -726,15 +727,15 @@ def notify_hints(self, team: int, hints: typing.List[NetUtils.Hint], only_new: b if not hint.local and data not in concerns[hint.finding_player]: concerns[hint.finding_player].append(data) # remember hints in all cases - if not hint.found: - # since hints are bidirectional, finding player and receiving player, - # we can check once if hint already exists - if hint not in self.hints[team, hint.finding_player]: - self.hints[team, hint.finding_player].add(hint) - new_hint_events.add(hint.finding_player) - for player in self.slot_set(hint.receiving_player): - self.hints[team, player].add(hint) - new_hint_events.add(player) + + # since hints are bidirectional, finding player and receiving player, + # we can check once if hint already exists + if hint not in self.hints[team, hint.finding_player]: + self.hints[team, hint.finding_player].add(hint) + new_hint_events.add(hint.finding_player) + for player in self.slot_set(hint.receiving_player): + self.hints[team, player].add(hint) + new_hint_events.add(player) self.logger.info("Notice (Team #%d): %s" % (team + 1, format_hint(self, team, hint))) for slot in new_hint_events: @@ -1153,7 +1154,10 @@ def __call__(self, raw: str) -> typing.Optional[bool]: if not raw: return try: - command = shlex.split(raw, comments=False) + try: + command = shlex.split(raw, comments=False) + except ValueError: # most likely: "ValueError: No closing quotation" + command = raw.split() basecommand = command[0] if basecommand[0] == self.marker: method = self.commands.get(basecommand[1:].lower(), None) @@ -1956,8 +1960,10 @@ def _cmd_status(self, tag: str = "") -> bool: def _cmd_exit(self) -> bool: """Shutdown the server""" - self.ctx.server.ws_server.close() - self.ctx.exit_event.set() + try: + self.ctx.server.ws_server.close() + finally: + self.ctx.exit_event.set() return True @mark_raw diff --git a/Options.py b/Options.py index aa6f175fa58d..992348cb546d 100644 --- a/Options.py +++ b/Options.py @@ -15,7 +15,7 @@ from schema import And, Optional, Or, Schema from typing_extensions import Self -from Utils import get_fuzzy_results, is_iterable_except_str, output_path +from Utils import get_file_safe_name, get_fuzzy_results, is_iterable_except_str, output_path if typing.TYPE_CHECKING: from BaseClasses import MultiWorld, PlandoOptions @@ -1531,7 +1531,7 @@ def yaml_dump_scalar(scalar) -> str: del file_data - with open(os.path.join(target_folder, game_name + ".yaml"), "w", encoding="utf-8-sig") as f: + with open(os.path.join(target_folder, get_file_safe_name(game_name) + ".yaml"), "w", encoding="utf-8-sig") as f: f.write(res) diff --git a/SNIClient.py b/SNIClient.py index 222ed54f5cc5..19440e1dc5be 100644 --- a/SNIClient.py +++ b/SNIClient.py @@ -633,7 +633,13 @@ async def game_watcher(ctx: SNIContext) -> None: if not ctx.client_handler: continue - rom_validated = await ctx.client_handler.validate_rom(ctx) + try: + rom_validated = await ctx.client_handler.validate_rom(ctx) + except Exception as e: + snes_logger.error(f"An error occurred, see logs for details: {e}") + text_file_logger = logging.getLogger() + text_file_logger.exception(e) + rom_validated = False if not rom_validated or (ctx.auth and ctx.auth != ctx.rom): snes_logger.warning("ROM change detected, please reconnect to the multiworld server") @@ -649,7 +655,13 @@ async def game_watcher(ctx: SNIContext) -> None: perf_counter = time.perf_counter() - await ctx.client_handler.game_watcher(ctx) + try: + await ctx.client_handler.game_watcher(ctx) + except Exception as e: + snes_logger.error(f"An error occurred, see logs for details: {e}") + text_file_logger = logging.getLogger() + text_file_logger.exception(e) + await snes_disconnect(ctx) async def run_game(romfile: str) -> None: diff --git a/Utils.py b/Utils.py index d6709431d32c..535933d815b1 100644 --- a/Utils.py +++ b/Utils.py @@ -18,8 +18,8 @@ from argparse import Namespace from settings import Settings, get_settings -from typing import BinaryIO, Coroutine, Optional, Set, Dict, Any, Union -from typing_extensions import TypeGuard +from time import sleep +from typing import BinaryIO, Coroutine, Optional, Set, Dict, Any, Union, TypeGuard from yaml import load, load_all, dump try: @@ -31,6 +31,7 @@ import tkinter import pathlib from BaseClasses import Region + import multiprocessing def tuplize_version(version: str) -> Version: @@ -46,7 +47,7 @@ def as_simple_string(self) -> str: return ".".join(str(item) for item in self) -__version__ = "0.5.1" +__version__ = "0.6.0" version_tuple = tuplize_version(__version__) is_linux = sys.platform.startswith("linux") @@ -423,7 +424,7 @@ def find_class(self, module: str, name: str) -> type: if module == "NetUtils" and name in {"NetworkItem", "ClientStatus", "Hint", "SlotType", "NetworkSlot"}: return getattr(self.net_utils_module, name) # Options and Plando are unpickled by WebHost -> Generate - if module == "worlds.generic" and name in {"PlandoItem", "PlandoConnection"}: + if module == "worlds.generic" and name == "PlandoItem": if not self.generic_properties_module: self.generic_properties_module = importlib.import_module("worlds.generic") return getattr(self.generic_properties_module, name) @@ -434,7 +435,7 @@ def find_class(self, module: str, name: str) -> type: else: mod = importlib.import_module(module) obj = getattr(mod, name) - if issubclass(obj, self.options_module.Option): + if issubclass(obj, (self.options_module.Option, self.options_module.PlandoConnection)): return obj # Forbid everything else. raise pickle.UnpicklingError(f"global '{module}.{name}' is forbidden") @@ -567,6 +568,8 @@ def queuer(): else: if text: queue.put_nowait(text) + else: + sleep(0.01) # non-blocking stream from threading import Thread thread = Thread(target=queuer, name=f"Stream handler for {stream.name}", daemon=True) @@ -664,6 +667,19 @@ def get_input_text_from_response(text: str, command: str) -> typing.Optional[str return None +def is_kivy_running() -> bool: + if "kivy" in sys.modules: + from kivy.app import App + return App.get_running_app() is not None + return False + + +def _mp_open_filename(res: "multiprocessing.Queue[typing.Optional[str]]", *args: Any) -> None: + if is_kivy_running(): + raise RuntimeError("kivy should not be running in multiprocess") + res.put(open_filename(*args)) + + def open_filename(title: str, filetypes: typing.Iterable[typing.Tuple[str, typing.Iterable[str]]], suggest: str = "") \ -> typing.Optional[str]: logging.info(f"Opening file input dialog for {title}.") @@ -693,6 +709,13 @@ def run(*args: str): f'This attempt was made because open_filename was used for "{title}".') raise e else: + if is_macos and is_kivy_running(): + # on macOS, mixing kivy and tk does not work, so spawn a new process + # FIXME: performance of this is pretty bad, and we should (also) look into alternatives + from multiprocessing import Process, Queue + res: "Queue[typing.Optional[str]]" = Queue() + Process(target=_mp_open_filename, args=(res, title, filetypes, suggest)).start() + return res.get() try: root = tkinter.Tk() except tkinter.TclError: @@ -702,6 +725,12 @@ def run(*args: str): initialfile=suggest or None) +def _mp_open_directory(res: "multiprocessing.Queue[typing.Optional[str]]", *args: Any) -> None: + if is_kivy_running(): + raise RuntimeError("kivy should not be running in multiprocess") + res.put(open_directory(*args)) + + def open_directory(title: str, suggest: str = "") -> typing.Optional[str]: def run(*args: str): return subprocess.run(args, capture_output=True, text=True).stdout.split("\n", 1)[0] or None @@ -725,9 +754,16 @@ def run(*args: str): import tkinter.filedialog except Exception as e: logging.error('Could not load tkinter, which is likely not installed. ' - f'This attempt was made because open_filename was used for "{title}".') + f'This attempt was made because open_directory was used for "{title}".') raise e else: + if is_macos and is_kivy_running(): + # on macOS, mixing kivy and tk does not work, so spawn a new process + # FIXME: performance of this is pretty bad, and we should (also) look into alternatives + from multiprocessing import Process, Queue + res: "Queue[typing.Optional[str]]" = Queue() + Process(target=_mp_open_directory, args=(res, title, suggest)).start() + return res.get() try: root = tkinter.Tk() except tkinter.TclError: @@ -740,12 +776,6 @@ def messagebox(title: str, text: str, error: bool = False) -> None: def run(*args: str): return subprocess.run(args, capture_output=True, text=True).stdout.split("\n", 1)[0] or None - def is_kivy_running(): - if "kivy" in sys.modules: - from kivy.app import App - return App.get_running_app() is not None - return False - if is_kivy_running(): from kvui import MessageBox MessageBox(title, text, error).open() diff --git a/WebHost.py b/WebHost.py index e597de24763d..3790a5f6f4d2 100644 --- a/WebHost.py +++ b/WebHost.py @@ -12,11 +12,12 @@ # in case app gets imported by something like gunicorn import Utils import settings +from Utils import get_file_safe_name if typing.TYPE_CHECKING: from flask import Flask -Utils.local_path.cached_path = os.path.dirname(__file__) or "." # py3.8 is not abs. remove "." when dropping 3.8 +Utils.local_path.cached_path = os.path.dirname(__file__) settings.no_gui = True configpath = os.path.abspath("config.yaml") if not os.path.exists(configpath): # fall back to config.yaml in home @@ -71,7 +72,7 @@ def create_ordered_tutorials_file() -> typing.List[typing.Dict[str, typing.Any]] shutil.rmtree(base_target_path, ignore_errors=True) for game, world in worlds.items(): # copy files from world's docs folder to the generated folder - target_path = os.path.join(base_target_path, game) + target_path = os.path.join(base_target_path, get_file_safe_name(game)) os.makedirs(target_path, exist_ok=True) if world.zip_path: diff --git a/WebHostLib/__init__.py b/WebHostLib/__init__.py index fdf3037fe015..dbe2182b0747 100644 --- a/WebHostLib/__init__.py +++ b/WebHostLib/__init__.py @@ -9,7 +9,7 @@ from pony.flask import Pony from werkzeug.routing import BaseConverter -from Utils import title_sorted +from Utils import title_sorted, get_file_safe_name UPLOAD_FOLDER = os.path.relpath('uploads') LOGS_FOLDER = os.path.relpath('logs') @@ -20,6 +20,7 @@ app.jinja_env.filters['any'] = any app.jinja_env.filters['all'] = all +app.jinja_env.filters['get_file_safe_name'] = get_file_safe_name app.config["SELFHOST"] = True # application process is in charge of running the websites app.config["GENERATORS"] = 8 # maximum concurrent world gens diff --git a/WebHostLib/generate.py b/WebHostLib/generate.py index dbe7dd958910..b19f3d483515 100644 --- a/WebHostLib/generate.py +++ b/WebHostLib/generate.py @@ -81,6 +81,7 @@ def start_generation(options: Dict[str, Union[dict, str]], meta: Dict[str, Any]) elif len(gen_options) > app.config["MAX_ROLL"]: flash(f"Sorry, generating of multiworlds is limited to {app.config['MAX_ROLL']} players. " f"If you have a larger group, please generate it yourself and upload it.") + return redirect(url_for(request.endpoint, **(request.view_args or {}))) elif len(gen_options) >= app.config["JOB_THRESHOLD"]: gen = Generation( options=pickle.dumps({name: vars(options) for name, options in gen_options.items()}), diff --git a/WebHostLib/misc.py b/WebHostLib/misc.py index 4784fcd9da63..c49b1ae17801 100644 --- a/WebHostLib/misc.py +++ b/WebHostLib/misc.py @@ -5,6 +5,7 @@ import jinja2.exceptions from flask import request, redirect, url_for, render_template, Response, session, abort, send_from_directory from pony.orm import count, commit, db_session +from werkzeug.utils import secure_filename from worlds.AutoWorld import AutoWorldRegister from . import app, cache @@ -69,14 +70,40 @@ def tutorial_landing(): @app.route('/faq//') @cache.cached() -def faq(lang): - return render_template("faq.html", lang=lang) +def faq(lang: str): + import markdown + with open(os.path.join(app.static_folder, "assets", "faq", secure_filename(lang)+".md")) as f: + document = f.read() + return render_template( + "markdown_document.html", + title="Frequently Asked Questions", + html_from_markdown=markdown.markdown( + document, + extensions=["toc", "mdx_breakless_lists"], + extension_configs={ + "toc": {"anchorlink": True} + } + ), + ) @app.route('/glossary//') @cache.cached() -def terms(lang): - return render_template("glossary.html", lang=lang) +def glossary(lang: str): + import markdown + with open(os.path.join(app.static_folder, "assets", "glossary", secure_filename(lang)+".md")) as f: + document = f.read() + return render_template( + "markdown_document.html", + title="Glossary", + html_from_markdown=markdown.markdown( + document, + extensions=["toc", "mdx_breakless_lists"], + extension_configs={ + "toc": {"anchorlink": True} + } + ), + ) @app.route('/seed/') diff --git a/WebHostLib/requirements.txt b/WebHostLib/requirements.txt index c593cd63df7e..b7b14dea1e6f 100644 --- a/WebHostLib/requirements.txt +++ b/WebHostLib/requirements.txt @@ -1,11 +1,11 @@ flask>=3.0.3 -werkzeug>=3.0.4 +werkzeug>=3.0.6 pony>=0.7.19 waitress>=3.0.0 Flask-Caching>=2.3.0 Flask-Compress>=1.15 Flask-Limiter>=3.8.0 -bokeh>=3.1.1; python_version <= '3.8' -bokeh>=3.4.3; python_version == '3.9' -bokeh>=3.5.2; python_version >= '3.10' +bokeh>=3.5.2 markupsafe>=2.1.5 +Markdown>=3.7 +mdx-breakless-lists>=1.0.1 diff --git a/WebHostLib/static/assets/faq.js b/WebHostLib/static/assets/faq.js deleted file mode 100644 index 1bf5e5a65995..000000000000 --- a/WebHostLib/static/assets/faq.js +++ /dev/null @@ -1,51 +0,0 @@ -window.addEventListener('load', () => { - const tutorialWrapper = document.getElementById('faq-wrapper'); - new Promise((resolve, reject) => { - const ajax = new XMLHttpRequest(); - ajax.onreadystatechange = () => { - if (ajax.readyState !== 4) { return; } - if (ajax.status === 404) { - reject("Sorry, the tutorial is not available in that language yet."); - return; - } - if (ajax.status !== 200) { - reject("Something went wrong while loading the tutorial."); - return; - } - resolve(ajax.responseText); - }; - ajax.open('GET', `${window.location.origin}/static/assets/faq/` + - `faq_${tutorialWrapper.getAttribute('data-lang')}.md`, true); - ajax.send(); - }).then((results) => { - // Populate page with HTML generated from markdown - showdown.setOption('tables', true); - showdown.setOption('strikethrough', true); - showdown.setOption('literalMidWordUnderscores', true); - tutorialWrapper.innerHTML += (new showdown.Converter()).makeHtml(results); - adjustHeaderWidth(); - - // Reset the id of all header divs to something nicer - for (const header of document.querySelectorAll('h1, h2, h3, h4, h5, h6')) { - const headerId = header.innerText.replace(/\s+/g, '-').toLowerCase(); - header.setAttribute('id', headerId); - header.addEventListener('click', () => { - window.location.hash = `#${headerId}`; - header.scrollIntoView(); - }); - } - - // Manually scroll the user to the appropriate header if anchor navigation is used - document.fonts.ready.finally(() => { - if (window.location.hash) { - const scrollTarget = document.getElementById(window.location.hash.substring(1)); - scrollTarget?.scrollIntoView(); - } - }); - }).catch((error) => { - console.error(error); - tutorialWrapper.innerHTML = - `

This page is out of logic!

-

Click here to return to safety.

`; - }); -}); diff --git a/WebHostLib/static/assets/faq/faq_en.md b/WebHostLib/static/assets/faq/en.md similarity index 100% rename from WebHostLib/static/assets/faq/faq_en.md rename to WebHostLib/static/assets/faq/en.md diff --git a/WebHostLib/static/assets/glossary.js b/WebHostLib/static/assets/glossary.js deleted file mode 100644 index 04a292008655..000000000000 --- a/WebHostLib/static/assets/glossary.js +++ /dev/null @@ -1,51 +0,0 @@ -window.addEventListener('load', () => { - const tutorialWrapper = document.getElementById('glossary-wrapper'); - new Promise((resolve, reject) => { - const ajax = new XMLHttpRequest(); - ajax.onreadystatechange = () => { - if (ajax.readyState !== 4) { return; } - if (ajax.status === 404) { - reject("Sorry, the glossary page is not available in that language yet."); - return; - } - if (ajax.status !== 200) { - reject("Something went wrong while loading the glossary."); - return; - } - resolve(ajax.responseText); - }; - ajax.open('GET', `${window.location.origin}/static/assets/faq/` + - `glossary_${tutorialWrapper.getAttribute('data-lang')}.md`, true); - ajax.send(); - }).then((results) => { - // Populate page with HTML generated from markdown - showdown.setOption('tables', true); - showdown.setOption('strikethrough', true); - showdown.setOption('literalMidWordUnderscores', true); - tutorialWrapper.innerHTML += (new showdown.Converter()).makeHtml(results); - adjustHeaderWidth(); - - // Reset the id of all header divs to something nicer - for (const header of document.querySelectorAll('h1, h2, h3, h4, h5, h6')) { - const headerId = header.innerText.replace(/\s+/g, '-').toLowerCase(); - header.setAttribute('id', headerId); - header.addEventListener('click', () => { - window.location.hash = `#${headerId}`; - header.scrollIntoView(); - }); - } - - // Manually scroll the user to the appropriate header if anchor navigation is used - document.fonts.ready.finally(() => { - if (window.location.hash) { - const scrollTarget = document.getElementById(window.location.hash.substring(1)); - scrollTarget?.scrollIntoView(); - } - }); - }).catch((error) => { - console.error(error); - tutorialWrapper.innerHTML = - `

This page is out of logic!

-

Click here to return to safety.

`; - }); -}); diff --git a/WebHostLib/static/assets/faq/glossary_en.md b/WebHostLib/static/assets/glossary/en.md similarity index 100% rename from WebHostLib/static/assets/faq/glossary_en.md rename to WebHostLib/static/assets/glossary/en.md diff --git a/WebHostLib/static/assets/playerOptions.js b/WebHostLib/static/assets/playerOptions.js index d0f2e388c2a6..fbf96a3a71c2 100644 --- a/WebHostLib/static/assets/playerOptions.js +++ b/WebHostLib/static/assets/playerOptions.js @@ -288,6 +288,11 @@ const applyPresets = (presetName) => { } }); namedRangeSelect.value = trueValue; + // It is also possible for a preset to use an unnamed value. If this happens, set the dropdown to "Custom" + if (namedRangeSelect.selectedIndex == -1) + { + namedRangeSelect.value = "custom"; + } } // Handle options whose presets are "random" diff --git a/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-atlas.png b/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-atlas.png new file mode 100644 index 000000000000..537e27979180 Binary files /dev/null and b/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-atlas.png differ diff --git a/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-atlas.webp b/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-atlas.webp new file mode 100644 index 000000000000..f34cd5ff2ec1 Binary files /dev/null and b/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-atlas.webp differ diff --git a/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-bottom-left-corner.png b/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-bottom-left-corner.png index 326670b7ebc4..a0b41b0f8cac 100644 Binary files a/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-bottom-left-corner.png and b/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-bottom-left-corner.png differ diff --git a/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-bottom-left-corner.webp b/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-bottom-left-corner.webp new file mode 100644 index 000000000000..4a5f2d75a0d4 Binary files /dev/null and b/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-bottom-left-corner.webp differ diff --git a/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-bottom-right-corner.png b/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-bottom-right-corner.png index c8297d34578c..6e1608d82b7f 100644 Binary files a/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-bottom-right-corner.png and b/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-bottom-right-corner.png differ diff --git a/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-bottom-right-corner.webp b/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-bottom-right-corner.webp new file mode 100644 index 000000000000..30bd2d047a76 Binary files /dev/null and b/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-bottom-right-corner.webp differ diff --git a/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-bottom.png b/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-bottom.png index 2a28958e0931..3d3e089ef79f 100644 Binary files a/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-bottom.png and b/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-bottom.png differ diff --git a/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-bottom.webp b/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-bottom.webp new file mode 100644 index 000000000000..f575ac5d9d48 Binary files /dev/null and b/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-bottom.webp differ diff --git a/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-left.png b/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-left.png index 9bc84ff603ec..08730d98489c 100644 Binary files a/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-left.png and b/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-left.png differ diff --git a/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-left.webp b/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-left.webp new file mode 100644 index 000000000000..f9227e8f2286 Binary files /dev/null and b/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-left.webp differ diff --git a/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-right.png b/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-right.png index a1e9c7c8b6f5..0bc82fa70e9b 100644 Binary files a/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-right.png and b/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-right.png differ diff --git a/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-right.webp b/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-right.webp new file mode 100644 index 000000000000..3c0a57740263 Binary files /dev/null and b/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-right.webp differ diff --git a/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-top-left-corner.png b/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-top-left-corner.png index a40bca60f080..05e675d6a97c 100644 Binary files a/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-top-left-corner.png and b/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-top-left-corner.png differ diff --git a/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-top-left-corner.webp b/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-top-left-corner.webp new file mode 100644 index 000000000000..4283cd42b16a Binary files /dev/null and b/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-top-left-corner.webp differ diff --git a/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-top-right-corner.png b/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-top-right-corner.png index b8a8c6a7265e..e0683a74bba5 100644 Binary files a/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-top-right-corner.png and b/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-top-right-corner.png differ diff --git a/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-top-right-corner.webp b/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-top-right-corner.webp new file mode 100644 index 000000000000..3075cec96add Binary files /dev/null and b/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-top-right-corner.webp differ diff --git a/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-top.png b/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-top.png index bb6ccec3d583..cded7ad108d3 100644 Binary files a/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-top.png and b/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-top.png differ diff --git a/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-top.webp b/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-top.webp new file mode 100644 index 000000000000..781b8e4df0d0 Binary files /dev/null and b/WebHostLib/static/static/backgrounds/cliffs/grass/cliff-top.webp differ diff --git a/WebHostLib/static/static/backgrounds/clouds/cloud-0001.png b/WebHostLib/static/static/backgrounds/clouds/cloud-0001.png index dba338f58552..1015819bc8f6 100644 Binary files a/WebHostLib/static/static/backgrounds/clouds/cloud-0001.png and b/WebHostLib/static/static/backgrounds/clouds/cloud-0001.png differ diff --git a/WebHostLib/static/static/backgrounds/clouds/cloud-0001.webp b/WebHostLib/static/static/backgrounds/clouds/cloud-0001.webp new file mode 100644 index 000000000000..73e249f6e530 Binary files /dev/null and b/WebHostLib/static/static/backgrounds/clouds/cloud-0001.webp differ diff --git a/WebHostLib/static/static/backgrounds/clouds/cloud-0002.png b/WebHostLib/static/static/backgrounds/clouds/cloud-0002.png index 33f09b19ce86..7b479bfe7b0b 100644 Binary files a/WebHostLib/static/static/backgrounds/clouds/cloud-0002.png and b/WebHostLib/static/static/backgrounds/clouds/cloud-0002.png differ diff --git a/WebHostLib/static/static/backgrounds/clouds/cloud-0002.webp b/WebHostLib/static/static/backgrounds/clouds/cloud-0002.webp new file mode 100644 index 000000000000..e4ac19bef687 Binary files /dev/null and b/WebHostLib/static/static/backgrounds/clouds/cloud-0002.webp differ diff --git a/WebHostLib/static/static/backgrounds/clouds/cloud-0003.png b/WebHostLib/static/static/backgrounds/clouds/cloud-0003.png index f665015b0d01..59844e31ac42 100644 Binary files a/WebHostLib/static/static/backgrounds/clouds/cloud-0003.png and b/WebHostLib/static/static/backgrounds/clouds/cloud-0003.png differ diff --git a/WebHostLib/static/static/backgrounds/clouds/cloud-0003.webp b/WebHostLib/static/static/backgrounds/clouds/cloud-0003.webp new file mode 100644 index 000000000000..36abe6e552a3 Binary files /dev/null and b/WebHostLib/static/static/backgrounds/clouds/cloud-0003.webp differ diff --git a/WebHostLib/static/static/backgrounds/dirt.png b/WebHostLib/static/static/backgrounds/dirt.png index 4ac930edc698..db6bc34635e3 100644 Binary files a/WebHostLib/static/static/backgrounds/dirt.png and b/WebHostLib/static/static/backgrounds/dirt.png differ diff --git a/WebHostLib/static/static/backgrounds/dirt.webp b/WebHostLib/static/static/backgrounds/dirt.webp new file mode 100644 index 000000000000..5a8635506f9f Binary files /dev/null and b/WebHostLib/static/static/backgrounds/dirt.webp differ diff --git a/WebHostLib/static/static/backgrounds/footer/footer-0001.png b/WebHostLib/static/static/backgrounds/footer/footer-0001.png index b863a3d42952..6752ab4e3279 100644 Binary files a/WebHostLib/static/static/backgrounds/footer/footer-0001.png and b/WebHostLib/static/static/backgrounds/footer/footer-0001.png differ diff --git a/WebHostLib/static/static/backgrounds/footer/footer-0001.webp b/WebHostLib/static/static/backgrounds/footer/footer-0001.webp new file mode 100644 index 000000000000..fb278c3b1643 Binary files /dev/null and b/WebHostLib/static/static/backgrounds/footer/footer-0001.webp differ diff --git a/WebHostLib/static/static/backgrounds/footer/footer-0002.png b/WebHostLib/static/static/backgrounds/footer/footer-0002.png index 90fdfe95d015..3bacab4134e2 100644 Binary files a/WebHostLib/static/static/backgrounds/footer/footer-0002.png and b/WebHostLib/static/static/backgrounds/footer/footer-0002.png differ diff --git a/WebHostLib/static/static/backgrounds/footer/footer-0002.webp b/WebHostLib/static/static/backgrounds/footer/footer-0002.webp new file mode 100644 index 000000000000..9b8e457c52a9 Binary files /dev/null and b/WebHostLib/static/static/backgrounds/footer/footer-0002.webp differ diff --git a/WebHostLib/static/static/backgrounds/footer/footer-0003.png b/WebHostLib/static/static/backgrounds/footer/footer-0003.png index 5fc31d1ee970..f8223e690171 100644 Binary files a/WebHostLib/static/static/backgrounds/footer/footer-0003.png and b/WebHostLib/static/static/backgrounds/footer/footer-0003.png differ diff --git a/WebHostLib/static/static/backgrounds/footer/footer-0003.webp b/WebHostLib/static/static/backgrounds/footer/footer-0003.webp new file mode 100644 index 000000000000..c2ded77536d6 Binary files /dev/null and b/WebHostLib/static/static/backgrounds/footer/footer-0003.webp differ diff --git a/WebHostLib/static/static/backgrounds/footer/footer-0004.png b/WebHostLib/static/static/backgrounds/footer/footer-0004.png index 4a95ce9a3aaf..d4476e53f759 100644 Binary files a/WebHostLib/static/static/backgrounds/footer/footer-0004.png and b/WebHostLib/static/static/backgrounds/footer/footer-0004.png differ diff --git a/WebHostLib/static/static/backgrounds/footer/footer-0004.webp b/WebHostLib/static/static/backgrounds/footer/footer-0004.webp new file mode 100644 index 000000000000..a2100817461a Binary files /dev/null and b/WebHostLib/static/static/backgrounds/footer/footer-0004.webp differ diff --git a/WebHostLib/static/static/backgrounds/footer/footer-0005.png b/WebHostLib/static/static/backgrounds/footer/footer-0005.png index 7b7cd502f36c..794615962454 100644 Binary files a/WebHostLib/static/static/backgrounds/footer/footer-0005.png and b/WebHostLib/static/static/backgrounds/footer/footer-0005.png differ diff --git a/WebHostLib/static/static/backgrounds/footer/footer-0005.webp b/WebHostLib/static/static/backgrounds/footer/footer-0005.webp new file mode 100644 index 000000000000..c0ee5205ca22 Binary files /dev/null and b/WebHostLib/static/static/backgrounds/footer/footer-0005.webp differ diff --git a/WebHostLib/static/static/backgrounds/grass-flowers.png b/WebHostLib/static/static/backgrounds/grass-flowers.png index 464fdbe58155..ea39c5419004 100644 Binary files a/WebHostLib/static/static/backgrounds/grass-flowers.png and b/WebHostLib/static/static/backgrounds/grass-flowers.png differ diff --git a/WebHostLib/static/static/backgrounds/grass-flowers.webp b/WebHostLib/static/static/backgrounds/grass-flowers.webp new file mode 100644 index 000000000000..1b8ebd7706ad Binary files /dev/null and b/WebHostLib/static/static/backgrounds/grass-flowers.webp differ diff --git a/WebHostLib/static/static/backgrounds/grass.png b/WebHostLib/static/static/backgrounds/grass.png index b88c33dec44b..6a99c4d94310 100644 Binary files a/WebHostLib/static/static/backgrounds/grass.png and b/WebHostLib/static/static/backgrounds/grass.png differ diff --git a/WebHostLib/static/static/backgrounds/grass.webp b/WebHostLib/static/static/backgrounds/grass.webp new file mode 100644 index 000000000000..212ab377a624 Binary files /dev/null and b/WebHostLib/static/static/backgrounds/grass.webp differ diff --git a/WebHostLib/static/static/backgrounds/header/dirt-header.png b/WebHostLib/static/static/backgrounds/header/dirt-header.png index 7c9e298e228b..8a9c0963e72f 100644 Binary files a/WebHostLib/static/static/backgrounds/header/dirt-header.png and b/WebHostLib/static/static/backgrounds/header/dirt-header.png differ diff --git a/WebHostLib/static/static/backgrounds/header/dirt-header.webp b/WebHostLib/static/static/backgrounds/header/dirt-header.webp new file mode 100644 index 000000000000..6c2b0bd8bf9b Binary files /dev/null and b/WebHostLib/static/static/backgrounds/header/dirt-header.webp differ diff --git a/WebHostLib/static/static/backgrounds/header/grass-header.png b/WebHostLib/static/static/backgrounds/header/grass-header.png index c2acc588071c..6d620e5033a2 100644 Binary files a/WebHostLib/static/static/backgrounds/header/grass-header.png and b/WebHostLib/static/static/backgrounds/header/grass-header.png differ diff --git a/WebHostLib/static/static/backgrounds/header/grass-header.webp b/WebHostLib/static/static/backgrounds/header/grass-header.webp new file mode 100644 index 000000000000..ca5d1e23bc2b Binary files /dev/null and b/WebHostLib/static/static/backgrounds/header/grass-header.webp differ diff --git a/WebHostLib/static/static/backgrounds/header/ocean-header.png b/WebHostLib/static/static/backgrounds/header/ocean-header.png index a0ff51f924f8..1e1c18e93c65 100644 Binary files a/WebHostLib/static/static/backgrounds/header/ocean-header.png and b/WebHostLib/static/static/backgrounds/header/ocean-header.png differ diff --git a/WebHostLib/static/static/backgrounds/header/ocean-header.webp b/WebHostLib/static/static/backgrounds/header/ocean-header.webp new file mode 100644 index 000000000000..fc1803ca0e4b Binary files /dev/null and b/WebHostLib/static/static/backgrounds/header/ocean-header.webp differ diff --git a/WebHostLib/static/static/backgrounds/header/party-time-header.png b/WebHostLib/static/static/backgrounds/header/party-time-header.png index 799f32f2282e..601ad829f1fa 100644 Binary files a/WebHostLib/static/static/backgrounds/header/party-time-header.png and b/WebHostLib/static/static/backgrounds/header/party-time-header.png differ diff --git a/WebHostLib/static/static/backgrounds/header/party-time-header.webp b/WebHostLib/static/static/backgrounds/header/party-time-header.webp new file mode 100644 index 000000000000..0b3c70871ada Binary files /dev/null and b/WebHostLib/static/static/backgrounds/header/party-time-header.webp differ diff --git a/WebHostLib/static/static/backgrounds/header/stone-header.png b/WebHostLib/static/static/backgrounds/header/stone-header.png index e0c9787e5735..f0d2f2fee56e 100644 Binary files a/WebHostLib/static/static/backgrounds/header/stone-header.png and b/WebHostLib/static/static/backgrounds/header/stone-header.png differ diff --git a/WebHostLib/static/static/backgrounds/header/stone-header.webp b/WebHostLib/static/static/backgrounds/header/stone-header.webp new file mode 100644 index 000000000000..9f26d1a505d4 Binary files /dev/null and b/WebHostLib/static/static/backgrounds/header/stone-header.webp differ diff --git a/WebHostLib/static/static/backgrounds/ice.png b/WebHostLib/static/static/backgrounds/ice.png index fcf7299b3582..c64f1b20f3b0 100644 Binary files a/WebHostLib/static/static/backgrounds/ice.png and b/WebHostLib/static/static/backgrounds/ice.png differ diff --git a/WebHostLib/static/static/backgrounds/ice.webp b/WebHostLib/static/static/backgrounds/ice.webp new file mode 100644 index 000000000000..a129d5f439c6 Binary files /dev/null and b/WebHostLib/static/static/backgrounds/ice.webp differ diff --git a/WebHostLib/static/static/backgrounds/jungle.png b/WebHostLib/static/static/backgrounds/jungle.png index e27d7e992086..c4ec5b964847 100644 Binary files a/WebHostLib/static/static/backgrounds/jungle.png and b/WebHostLib/static/static/backgrounds/jungle.png differ diff --git a/WebHostLib/static/static/backgrounds/jungle.webp b/WebHostLib/static/static/backgrounds/jungle.webp new file mode 100644 index 000000000000..d21edc8e55f2 Binary files /dev/null and b/WebHostLib/static/static/backgrounds/jungle.webp differ diff --git a/WebHostLib/static/static/backgrounds/ocean.png b/WebHostLib/static/static/backgrounds/ocean.png index 5c22c0b92aa1..d6c9d285c963 100644 Binary files a/WebHostLib/static/static/backgrounds/ocean.png and b/WebHostLib/static/static/backgrounds/ocean.png differ diff --git a/WebHostLib/static/static/backgrounds/ocean.webp b/WebHostLib/static/static/backgrounds/ocean.webp new file mode 100644 index 000000000000..a50b7b27f743 Binary files /dev/null and b/WebHostLib/static/static/backgrounds/ocean.webp differ diff --git a/WebHostLib/static/static/backgrounds/party-time.png b/WebHostLib/static/static/backgrounds/party-time.png index ad00851ba4dc..3fcea8a46eef 100644 Binary files a/WebHostLib/static/static/backgrounds/party-time.png and b/WebHostLib/static/static/backgrounds/party-time.png differ diff --git a/WebHostLib/static/static/backgrounds/party-time.webp b/WebHostLib/static/static/backgrounds/party-time.webp new file mode 100644 index 000000000000..7cd547329a40 Binary files /dev/null and b/WebHostLib/static/static/backgrounds/party-time.webp differ diff --git a/WebHostLib/static/static/backgrounds/stone.png b/WebHostLib/static/static/backgrounds/stone.png index 9e15a34375e4..2956beaaa80b 100644 Binary files a/WebHostLib/static/static/backgrounds/stone.png and b/WebHostLib/static/static/backgrounds/stone.png differ diff --git a/WebHostLib/static/static/backgrounds/stone.webp b/WebHostLib/static/static/backgrounds/stone.webp new file mode 100644 index 000000000000..96303c816227 Binary files /dev/null and b/WebHostLib/static/static/backgrounds/stone.webp differ diff --git a/WebHostLib/static/static/branding/header-logo-full.svg b/WebHostLib/static/static/branding/header-logo-full.svg new file mode 100644 index 000000000000..3e22500905f3 --- /dev/null +++ b/WebHostLib/static/static/branding/header-logo-full.svg @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/WebHostLib/static/static/branding/header-logo.png b/WebHostLib/static/static/branding/header-logo.png index e5d7f9b4a0c0..5a3dbe7dafc5 100644 Binary files a/WebHostLib/static/static/branding/header-logo.png and b/WebHostLib/static/static/branding/header-logo.png differ diff --git a/WebHostLib/static/static/branding/header-logo.svg b/WebHostLib/static/static/branding/header-logo.svg index 3e22500905f3..ceedba43385a 100644 --- a/WebHostLib/static/static/branding/header-logo.svg +++ b/WebHostLib/static/static/branding/header-logo.svg @@ -1,66 +1 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/WebHostLib/static/static/branding/header-logo.webp b/WebHostLib/static/static/branding/header-logo.webp new file mode 100644 index 000000000000..c8088e826266 Binary files /dev/null and b/WebHostLib/static/static/branding/header-logo.webp differ diff --git a/WebHostLib/static/static/branding/landing-logo.png b/WebHostLib/static/static/branding/landing-logo.png index 1f2b967a9844..d4845a475daa 100644 Binary files a/WebHostLib/static/static/branding/landing-logo.png and b/WebHostLib/static/static/branding/landing-logo.png differ diff --git a/WebHostLib/static/static/branding/landing-logo.webp b/WebHostLib/static/static/branding/landing-logo.webp new file mode 100644 index 000000000000..7bd4673e99e0 Binary files /dev/null and b/WebHostLib/static/static/branding/landing-logo.webp differ diff --git a/WebHostLib/static/static/button-images/hamburger-menu-icon.png b/WebHostLib/static/static/button-images/hamburger-menu-icon.png index f1c96316358d..c834501453ab 100644 Binary files a/WebHostLib/static/static/button-images/hamburger-menu-icon.png and b/WebHostLib/static/static/button-images/hamburger-menu-icon.png differ diff --git a/WebHostLib/static/static/button-images/hamburger-menu-icon.webp b/WebHostLib/static/static/button-images/hamburger-menu-icon.webp new file mode 100644 index 000000000000..970754d7bfc8 Binary files /dev/null and b/WebHostLib/static/static/button-images/hamburger-menu-icon.webp differ diff --git a/WebHostLib/static/static/button-images/island-button-a.png b/WebHostLib/static/static/button-images/island-button-a.png index f3872dfd6cdf..552e4d8f6d34 100644 Binary files a/WebHostLib/static/static/button-images/island-button-a.png and b/WebHostLib/static/static/button-images/island-button-a.png differ diff --git a/WebHostLib/static/static/button-images/island-button-a.webp b/WebHostLib/static/static/button-images/island-button-a.webp new file mode 100644 index 000000000000..6da0c1720030 Binary files /dev/null and b/WebHostLib/static/static/button-images/island-button-a.webp differ diff --git a/WebHostLib/static/static/button-images/island-button-b.png b/WebHostLib/static/static/button-images/island-button-b.png index 65008eaf59ef..fd4a256c7c9f 100644 Binary files a/WebHostLib/static/static/button-images/island-button-b.png and b/WebHostLib/static/static/button-images/island-button-b.png differ diff --git a/WebHostLib/static/static/button-images/island-button-b.webp b/WebHostLib/static/static/button-images/island-button-b.webp new file mode 100644 index 000000000000..6b7c3a279ed0 Binary files /dev/null and b/WebHostLib/static/static/button-images/island-button-b.webp differ diff --git a/WebHostLib/static/static/button-images/island-button-c.png b/WebHostLib/static/static/button-images/island-button-c.png index 9e5f9f50d2be..2f10f45828c4 100644 Binary files a/WebHostLib/static/static/button-images/island-button-c.png and b/WebHostLib/static/static/button-images/island-button-c.png differ diff --git a/WebHostLib/static/static/button-images/island-button-c.webp b/WebHostLib/static/static/button-images/island-button-c.webp new file mode 100644 index 000000000000..83ce413da807 Binary files /dev/null and b/WebHostLib/static/static/button-images/island-button-c.webp differ diff --git a/WebHostLib/static/static/button-images/popover.png b/WebHostLib/static/static/button-images/popover.png index cbc863410489..e3247194b06b 100644 Binary files a/WebHostLib/static/static/button-images/popover.png and b/WebHostLib/static/static/button-images/popover.png differ diff --git a/WebHostLib/static/static/button-images/popover.webp b/WebHostLib/static/static/button-images/popover.webp new file mode 100644 index 000000000000..cd1c006221b0 Binary files /dev/null and b/WebHostLib/static/static/button-images/popover.webp differ diff --git a/WebHostLib/static/static/decorations/island-a.png b/WebHostLib/static/static/decorations/island-a.png index d931aed0bdc7..4f5d7c264198 100644 Binary files a/WebHostLib/static/static/decorations/island-a.png and b/WebHostLib/static/static/decorations/island-a.png differ diff --git a/WebHostLib/static/static/decorations/island-a.webp b/WebHostLib/static/static/decorations/island-a.webp new file mode 100644 index 000000000000..32c9cc8f6bd6 Binary files /dev/null and b/WebHostLib/static/static/decorations/island-a.webp differ diff --git a/WebHostLib/static/static/decorations/island-b.png b/WebHostLib/static/static/decorations/island-b.png index d6902281922c..cceb79af33b0 100644 Binary files a/WebHostLib/static/static/decorations/island-b.png and b/WebHostLib/static/static/decorations/island-b.png differ diff --git a/WebHostLib/static/static/decorations/island-b.webp b/WebHostLib/static/static/decorations/island-b.webp new file mode 100644 index 000000000000..3ec6aae438ba Binary files /dev/null and b/WebHostLib/static/static/decorations/island-b.webp differ diff --git a/WebHostLib/static/static/decorations/island-c.png b/WebHostLib/static/static/decorations/island-c.png index 790c7b01d53c..2beedce19d26 100644 Binary files a/WebHostLib/static/static/decorations/island-c.png and b/WebHostLib/static/static/decorations/island-c.png differ diff --git a/WebHostLib/static/static/decorations/island-c.webp b/WebHostLib/static/static/decorations/island-c.webp new file mode 100644 index 000000000000..98e1add91ee3 Binary files /dev/null and b/WebHostLib/static/static/decorations/island-c.webp differ diff --git a/WebHostLib/static/static/decorations/rock-in-water.png b/WebHostLib/static/static/decorations/rock-in-water.png index 25c62acd24fb..1320bef7cee1 100644 Binary files a/WebHostLib/static/static/decorations/rock-in-water.png and b/WebHostLib/static/static/decorations/rock-in-water.png differ diff --git a/WebHostLib/static/static/decorations/rock-in-water.webp b/WebHostLib/static/static/decorations/rock-in-water.webp new file mode 100644 index 000000000000..2c8af460d5e2 Binary files /dev/null and b/WebHostLib/static/static/decorations/rock-in-water.webp differ diff --git a/WebHostLib/static/static/decorations/rock-single.png b/WebHostLib/static/static/decorations/rock-single.png index cc237d132ef4..c003abe0d173 100644 Binary files a/WebHostLib/static/static/decorations/rock-single.png and b/WebHostLib/static/static/decorations/rock-single.png differ diff --git a/WebHostLib/static/static/decorations/rock-single.webp b/WebHostLib/static/static/decorations/rock-single.webp new file mode 100644 index 000000000000..e53a2fb5c480 Binary files /dev/null and b/WebHostLib/static/static/decorations/rock-single.webp differ diff --git a/WebHostLib/static/styles/markdown.css b/WebHostLib/static/styles/markdown.css index e0165b7489ef..5ead2c60f791 100644 --- a/WebHostLib/static/styles/markdown.css +++ b/WebHostLib/static/styles/markdown.css @@ -28,7 +28,7 @@ font-weight: normal; font-family: LondrinaSolid-Regular, sans-serif; text-transform: uppercase; - cursor: pointer; + cursor: pointer; /* TODO: remove once we drop showdown.js */ width: 100%; text-shadow: 1px 1px 4px #000000; } @@ -37,7 +37,7 @@ font-size: 38px; font-weight: normal; font-family: LondrinaSolid-Light, sans-serif; - cursor: pointer; + cursor: pointer; /* TODO: remove once we drop showdown.js */ width: 100%; margin-top: 20px; margin-bottom: 0.5rem; @@ -50,7 +50,7 @@ font-family: LexendDeca-Regular, sans-serif; text-transform: none; text-align: left; - cursor: pointer; + cursor: pointer; /* TODO: remove once we drop showdown.js */ width: 100%; margin-bottom: 0.5rem; } @@ -59,7 +59,7 @@ font-family: LexendDeca-Regular, sans-serif; text-transform: none; font-size: 24px; - cursor: pointer; + cursor: pointer; /* TODO: remove once we drop showdown.js */ margin-bottom: 24px; } @@ -67,20 +67,29 @@ font-family: LexendDeca-Regular, sans-serif; text-transform: none; font-size: 22px; - cursor: pointer; + cursor: pointer; /* TODO: remove once we drop showdown.js */ } .markdown h6, .markdown details summary.h6{ font-family: LexendDeca-Regular, sans-serif; text-transform: none; font-size: 20px; - cursor: pointer;; + cursor: pointer; /* TODO: remove once we drop showdown.js */ } .markdown h4, .markdown h5, .markdown h6{ margin-bottom: 0.5rem; } +.markdown h1 > a, +.markdown h2 > a, +.markdown h3 > a, +.markdown h4 > a, +.markdown h5 > a, +.markdown h6 > a { + color: inherit; +} + .markdown ul{ margin-top: 0.5rem; margin-bottom: 0.5rem; diff --git a/WebHostLib/templates/faq.html b/WebHostLib/templates/faq.html deleted file mode 100644 index 76bdb96d2ef8..000000000000 --- a/WebHostLib/templates/faq.html +++ /dev/null @@ -1,17 +0,0 @@ -{% extends 'pageWrapper.html' %} - -{% block head %} - {% include 'header/grassHeader.html' %} - Frequently Asked Questions - - - -{% endblock %} - -{% block body %} -
- -
-{% endblock %} diff --git a/WebHostLib/templates/gameInfo.html b/WebHostLib/templates/gameInfo.html index c5ebba82848d..3b908004b1be 100644 --- a/WebHostLib/templates/gameInfo.html +++ b/WebHostLib/templates/gameInfo.html @@ -11,7 +11,7 @@ {% block body %} {% include 'header/'+theme+'Header.html' %} -
+
{% endblock %} diff --git a/WebHostLib/templates/genericTracker.html b/WebHostLib/templates/genericTracker.html index 947cf2837278..b92097ceea08 100644 --- a/WebHostLib/templates/genericTracker.html +++ b/WebHostLib/templates/genericTracker.html @@ -98,6 +98,8 @@ {% if hint.finding_player == player %} {{ player_names_with_alias[(team, hint.finding_player)] }} + {% elif get_slot_info(team, hint.finding_player).type == 2 %} + {{ player_names_with_alias[(team, hint.finding_player)] }} {% else %} {{ player_names_with_alias[(team, hint.finding_player)] }} @@ -107,6 +109,8 @@ {% if hint.receiving_player == player %} {{ player_names_with_alias[(team, hint.receiving_player)] }} + {% elif get_slot_info(team, hint.receiving_player).type == 2 %} + {{ player_names_with_alias[(team, hint.receiving_player)] }} {% else %} {{ player_names_with_alias[(team, hint.receiving_player)] }} diff --git a/WebHostLib/templates/glossary.html b/WebHostLib/templates/glossary.html deleted file mode 100644 index 921f678157fc..000000000000 --- a/WebHostLib/templates/glossary.html +++ /dev/null @@ -1,17 +0,0 @@ -{% extends 'pageWrapper.html' %} - -{% block head %} - {% include 'header/grassHeader.html' %} - Glossary - - - -{% endblock %} - -{% block body %} -
- -
-{% endblock %} diff --git a/WebHostLib/templates/markdown_document.html b/WebHostLib/templates/markdown_document.html new file mode 100644 index 000000000000..07b3c8354d0d --- /dev/null +++ b/WebHostLib/templates/markdown_document.html @@ -0,0 +1,13 @@ +{% extends 'pageWrapper.html' %} + +{% block head %} + {% include 'header/grassHeader.html' %} + {{ title }} + +{% endblock %} + +{% block body %} +
+ {{ html_from_markdown | safe}} +
+{% endblock %} diff --git a/WebHostLib/templates/multitrackerHintTable.html b/WebHostLib/templates/multitrackerHintTable.html index a931e9b04845..fcc15fb37a9f 100644 --- a/WebHostLib/templates/multitrackerHintTable.html +++ b/WebHostLib/templates/multitrackerHintTable.html @@ -21,8 +21,20 @@ ) -%} - {{ player_names_with_alias[(team, hint.finding_player)] }} - {{ player_names_with_alias[(team, hint.receiving_player)] }} + + {% if get_slot_info(team, hint.finding_player).type == 2 %} + {{ player_names_with_alias[(team, hint.finding_player)] }} + {% else %} + {{ player_names_with_alias[(team, hint.finding_player)] }} + {% endif %} + + + {% if get_slot_info(team, hint.receiving_player).type == 2 %} + {{ player_names_with_alias[(team, hint.receiving_player)] }} + {% else %} + {{ player_names_with_alias[(team, hint.receiving_player)] }} + {% endif %} + {{ item_id_to_name[games[(team, hint.receiving_player)]][hint.item] }} {{ location_id_to_name[games[(team, hint.finding_player)]][hint.location] }} {{ games[(team, hint.finding_player)] }} diff --git a/WebHostLib/templates/playerOptions/macros.html b/WebHostLib/templates/playerOptions/macros.html index 30a4fc78dff3..64f0f140de95 100644 --- a/WebHostLib/templates/playerOptions/macros.html +++ b/WebHostLib/templates/playerOptions/macros.html @@ -196,13 +196,14 @@ {% macro OptionTitle(option_name, option) %}
User Content Page.
You may also download the - template file for this game. + template file for this game.

diff --git a/WebHostLib/templates/tutorial.html b/WebHostLib/templates/tutorial.html index d3a7e0a05ecc..4b6622c31336 100644 --- a/WebHostLib/templates/tutorial.html +++ b/WebHostLib/templates/tutorial.html @@ -11,7 +11,7 @@ {% endblock %} {% block body %} -
+
{% endblock %} diff --git a/WebHostLib/templates/weightedOptions/macros.html b/WebHostLib/templates/weightedOptions/macros.html index 68d3968a178a..d18d0f0b8957 100644 --- a/WebHostLib/templates/weightedOptions/macros.html +++ b/WebHostLib/templates/weightedOptions/macros.html @@ -53,7 +53,7 @@ {{ RangeRow(option_name, option, option.range_start, option.range_start, True) }} - {% if option.range_start < option.default < option.range_end %} + {% if option.default is number and option.range_start < option.default < option.range_end %} {{ RangeRow(option_name, option, option.default, option.default, True) }} {% endif %} {{ RangeRow(option_name, option, option.range_end, option.range_end, True) }} diff --git a/WebHostLib/tracker.py b/WebHostLib/tracker.py index 75b5fb0202d9..043764a53b08 100644 --- a/WebHostLib/tracker.py +++ b/WebHostLib/tracker.py @@ -5,7 +5,7 @@ from uuid import UUID from email.utils import parsedate_to_datetime -from flask import render_template, make_response, Response, request +from flask import make_response, render_template, request, Request, Response from werkzeug.exceptions import abort from MultiServer import Context, get_saving_second @@ -298,17 +298,25 @@ def get_spheres(self) -> List[List[int]]: return self._multidata.get("spheres", []) -def _process_if_request_valid(incoming_request, room: Optional[Room]) -> Optional[Response]: +def _process_if_request_valid(incoming_request: Request, room: Optional[Room]) -> Optional[Response]: if not room: abort(404) - if_modified = incoming_request.headers.get("If-Modified-Since", None) - if if_modified: - if_modified = parsedate_to_datetime(if_modified) + if_modified_str: Optional[str] = incoming_request.headers.get("If-Modified-Since", None) + if if_modified_str: + if_modified = parsedate_to_datetime(if_modified_str) + if if_modified.tzinfo is None: + abort(400) # standard requires "GMT" timezone + # database may use datetime.utcnow(), which is timezone-naive. convert to timezone-aware. + last_activity = room.last_activity + if last_activity.tzinfo is None: + last_activity = room.last_activity.replace(tzinfo=datetime.timezone.utc) # if_modified has less precision than last_activity, so we bring them to same precision - if if_modified >= room.last_activity.replace(microsecond=0): + if if_modified >= last_activity.replace(microsecond=0): return make_response("", 304) + return None + @app.route("/tracker///") def get_player_tracker(tracker: UUID, tracked_team: int, tracked_player: int, generic: bool = False) -> Response: @@ -415,6 +423,7 @@ def render_generic_tracker(tracker_data: TrackerData, team: int, player: int) -> template_name_or_list="genericTracker.html", game_specific_tracker=game in _player_trackers, room=tracker_data.room, + get_slot_info=tracker_data.get_slot_info, team=team, player=player, player_name=tracker_data.get_room_long_player_names()[team, player], @@ -438,6 +447,7 @@ def render_generic_multiworld_tracker(tracker_data: TrackerData, enabled_tracker enabled_trackers=enabled_trackers, current_tracker="Generic", room=tracker_data.room, + get_slot_info=tracker_data.get_slot_info, all_slots=tracker_data.get_all_slots(), room_players=tracker_data.get_all_players(), locations=tracker_data.get_room_locations(), @@ -489,7 +499,7 @@ def render_Factorio_multiworld_tracker(tracker_data: TrackerData, enabled_tracke (team, player): collections.Counter({ tracker_data.item_id_to_name["Factorio"][item_id]: count for item_id, count in tracker_data.get_player_inventory_counts(team, player).items() - }) for team, players in tracker_data.get_all_slots().items() for player in players + }) for team, players in tracker_data.get_all_players().items() for player in players if tracker_data.get_player_game(team, player) == "Factorio" } @@ -498,6 +508,7 @@ def render_Factorio_multiworld_tracker(tracker_data: TrackerData, enabled_tracke enabled_trackers=enabled_trackers, current_tracker="Factorio", room=tracker_data.room, + get_slot_info=tracker_data.get_slot_info, all_slots=tracker_data.get_all_slots(), room_players=tracker_data.get_all_players(), locations=tracker_data.get_room_locations(), @@ -630,6 +641,7 @@ def render_ALinkToThePast_multiworld_tracker(tracker_data: TrackerData, enabled_ enabled_trackers=enabled_trackers, current_tracker="A Link to the Past", room=tracker_data.room, + get_slot_info=tracker_data.get_slot_info, all_slots=tracker_data.get_all_slots(), room_players=tracker_data.get_all_players(), locations=tracker_data.get_room_locations(), diff --git a/data/options.yaml b/data/options.yaml index ee8866627d52..09bfcdcec1f6 100644 --- a/data/options.yaml +++ b/data/options.yaml @@ -28,9 +28,9 @@ name: Player{number} # Used to describe your yaml. Useful if you have multiple files. -description: Default {{ game }} Template +description: {{ yaml_dump("Default %s Template" % game) }} -game: {{ game }} +game: {{ yaml_dump(game) }} requires: version: {{ __version__ }} # Version of Archipelago required for this yaml to work as expected. @@ -44,7 +44,7 @@ requires: {%- endfor -%} {% endmacro %} -{{ game }}: +{{ yaml_dump(game) }}: {%- for group_name, group_options in option_groups.items() %} # {{ group_name }} diff --git a/docs/CODEOWNERS b/docs/CODEOWNERS index ee7fd7ed863b..a51cac37026b 100644 --- a/docs/CODEOWNERS +++ b/docs/CODEOWNERS @@ -143,7 +143,7 @@ /worlds/shivers/ @GodlFire # A Short Hike -/worlds/shorthike/ @chandler05 +/worlds/shorthike/ @chandler05 @BrandenEK # Sonic Adventure 2 Battle /worlds/sa2b/ @PoryGone @RaspberrySpace diff --git a/docs/contributing.md b/docs/contributing.md index 9fd21408eb7b..96fc316be82c 100644 --- a/docs/contributing.md +++ b/docs/contributing.md @@ -16,7 +16,7 @@ game contributions: * **Do not introduce unit test failures/regressions.** Archipelago supports multiple versions of Python. You may need to download older Python versions to fully test your changes. Currently, the oldest supported version - is [Python 3.8](https://www.python.org/downloads/release/python-380/). + is [Python 3.10](https://www.python.org/downloads/release/python-31015/). It is recommended that automated github actions are turned on in your fork to have github run unit tests after pushing. You can turn them on here: diff --git a/docs/network protocol.md b/docs/network protocol.md index 1c4579c4066f..4a96a43f818f 100644 --- a/docs/network protocol.md +++ b/docs/network protocol.md @@ -268,6 +268,7 @@ Additional arguments added to the [Set](#Set) package that triggered this [SetRe These packets are sent purely from client to server. They are not accepted by clients. * [Connect](#Connect) +* [ConnectUpdate](#ConnectUpdate) * [Sync](#Sync) * [LocationChecks](#LocationChecks) * [LocationScouts](#LocationScouts) diff --git a/docs/running from source.md b/docs/running from source.md index a161265fcb74..66dd1925c897 100644 --- a/docs/running from source.md +++ b/docs/running from source.md @@ -7,7 +7,7 @@ use that version. These steps are for developers or platforms without compiled r ## General What you'll need: - * [Python 3.8.7 or newer](https://www.python.org/downloads/), not the Windows Store version + * [Python 3.10.15 or newer](https://www.python.org/downloads/), not the Windows Store version * Python 3.12.x is currently the newest supported version * pip: included in downloads from python.org, separate in many Linux distributions * Matching C compiler @@ -85,4 +85,4 @@ PyCharm has a built-in version control integration that supports Git. ## Running tests -Run `pip install pytest pytest-subtests`, then use your IDE to run tests or run `pytest` from the source folder. +Information about running tests can be found in [tests.md](https://github.com/ArchipelagoMW/Archipelago/blob/main/docs/tests.md#running-tests) diff --git a/docs/tests.md b/docs/tests.md index 7a3531f0f84f..c8655ccf3f4d 100644 --- a/docs/tests.md +++ b/docs/tests.md @@ -84,7 +84,19 @@ testing portions of your code that can be tested without relying on a multiworld ## Running Tests -In PyCharm, running all tests can be done by right-clicking the root `test` directory and selecting `run Python tests`. -If you do not have pytest installed, you may get import failures. To solve this, edit the run configuration, and set the -working directory of the run to the Archipelago directory. If you only want to run your world's defined tests, repeat -the steps for the test directory within your world. +#### Using Pycharm + +In PyCharm, running all tests can be done by right-clicking the root test directory and selecting Run 'Archipelago Unittests'. +Unless you configured PyCharm to use pytest as a test runner, you may get import failures. To solve this, edit the run configuration, +and set the working directory to the Archipelago directory which contains all the project files. + +If you only want to run your world's defined tests, repeat the steps for the test directory within your world. +Your working directory should be the directory of your world in the worlds directory and the script should be the +tests folder within your world. + +You can also find the 'Archipelago Unittests' as an option in the dropdown at the top of the window +next to the run and debug buttons. + +#### Running Tests without Pycharm + +Run `pip install pytest pytest-subtests`, then use your IDE to run tests or run `pytest` from the source folder. diff --git a/kvui.py b/kvui.py index 74d8ad06734a..2723654214c1 100644 --- a/kvui.py +++ b/kvui.py @@ -12,10 +12,7 @@ # kivy 2.2.0 introduced DPI awareness on Windows, but it makes the UI enter an infinitely recursive re-layout # by setting the application to not DPI Aware, Windows handles scaling the entire window on its own, ignoring kivy's - try: - ctypes.windll.shcore.SetProcessDpiAwareness(0) - except FileNotFoundError: # shcore may not be found on <= Windows 7 - pass # TODO: remove silent except when Python 3.8 is phased out. + ctypes.windll.shcore.SetProcessDpiAwareness(0) os.environ["KIVY_NO_CONSOLELOG"] = "1" os.environ["KIVY_NO_FILELOG"] = "1" diff --git a/requirements.txt b/requirements.txt index 6fe14c9f32ce..946546cb6961 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ colorama>=0.4.6 -websockets>=13.0.1 +websockets>=13.0.1,<14 PyYAML>=6.0.2 jellyfish>=1.1.0 jinja2>=3.1.4 diff --git a/setup.py b/setup.py index 0c9ee2c29302..f075551d58b0 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,6 @@ import shutil import sys import sysconfig -import typing import warnings import zipfile import urllib.request @@ -14,14 +13,14 @@ import threading import subprocess -from collections.abc import Iterable from hashlib import sha3_512 from pathlib import Path +from typing import Dict, Iterable, List, Optional, Sequence, Set, Tuple, Union # This is a bit jank. We need cx-Freeze to be able to run anything from this script, so install it +requirement = 'cx-Freeze==7.2.0' try: - requirement = 'cx-Freeze==7.2.0' import pkg_resources try: pkg_resources.require(requirement) @@ -30,7 +29,7 @@ install_cx_freeze = True except ImportError: install_cx_freeze = True - pkg_resources = None # type: ignore [assignment] + pkg_resources = None # type: ignore[assignment] if install_cx_freeze: # check if pip is available @@ -61,7 +60,7 @@ # On Python < 3.10 LogicMixin is not currently supported. -non_apworlds: set = { +non_apworlds: Set[str] = { "A Link to the Past", "Adventure", "ArchipIDLE", @@ -84,7 +83,7 @@ if sys.version_info < (3,10): non_apworlds.add("Hollow Knight") -def download_SNI(): +def download_SNI() -> None: print("Updating SNI") machine_to_go = { "x86_64": "amd64", @@ -94,7 +93,7 @@ def download_SNI(): platform_name = platform.system().lower() machine_name = platform.machine().lower() # force amd64 on macos until we have universal2 sni, otherwise resolve to GOARCH - machine_name = "amd64" if platform_name == "darwin" else machine_to_go.get(machine_name, machine_name) + machine_name = "universal" if platform_name == "darwin" else machine_to_go.get(machine_name, machine_name) with urllib.request.urlopen("https://api.github.com/repos/alttpo/sni/releases/latest") as request: data = json.load(request) files = data["assets"] @@ -105,17 +104,19 @@ def download_SNI(): download_url: str = file["browser_download_url"] machine_match = download_url.rsplit("-", 1)[1].split(".", 1)[0] == machine_name if platform_name in download_url and machine_match: + source_url = download_url # prefer "many" builds if "many" in download_url: - source_url = download_url break - source_url = download_url + # prefer the correct windows or windows7 build + if platform_name == "windows" and ("windows7" in download_url) == (sys.version_info < (3, 9)): + break if source_url and source_url.endswith(".zip"): with urllib.request.urlopen(source_url) as download: with zipfile.ZipFile(io.BytesIO(download.read()), "r") as zf: - for member in zf.infolist(): - zf.extract(member, path="SNI") + for zf_member in zf.infolist(): + zf.extract(zf_member, path="SNI") print(f"Downloaded SNI from {source_url}") elif source_url and (source_url.endswith(".tar.xz") or source_url.endswith(".tar.gz")): @@ -129,11 +130,13 @@ def download_SNI(): raise ValueError(f"Unexpected file '{member.name}' in {source_url}") elif member.isdir() and not sni_dir: sni_dir = member.name - elif member.isfile() and not sni_dir or not member.name.startswith(sni_dir): + elif member.isfile() and not sni_dir or sni_dir and not member.name.startswith(sni_dir): raise ValueError(f"Expected folder before '{member.name}' in {source_url}") elif member.isfile() and sni_dir: tf.extract(member) # sadly SNI is in its own folder on non-windows, so we need to rename + if not sni_dir: + raise ValueError("Did not find SNI in archive") shutil.rmtree("SNI", True) os.rename(sni_dir, "SNI") print(f"Downloaded SNI from {source_url}") @@ -145,7 +148,7 @@ def download_SNI(): print(f"No SNI found for system spec {platform_name} {machine_name}") -signtool: typing.Optional[str] +signtool: Optional[str] if os.path.exists("X:/pw.txt"): print("Using signtool") with open("X:/pw.txt", encoding="utf-8-sig") as f: @@ -197,13 +200,13 @@ def resolve_icon(icon_name: str): extra_libs = ["libssl.so", "libcrypto.so"] if is_linux else [] -def remove_sprites_from_folder(folder): +def remove_sprites_from_folder(folder: Path) -> None: for file in os.listdir(folder): if file != ".gitignore": os.remove(folder / file) -def _threaded_hash(filepath): +def _threaded_hash(filepath: Union[str, Path]) -> str: hasher = sha3_512() hasher.update(open(filepath, "rb").read()) return base64.b85encode(hasher.digest()).decode() @@ -217,11 +220,11 @@ class BuildCommand(setuptools.command.build.build): yes: bool last_yes: bool = False # used by sub commands of build - def initialize_options(self): + def initialize_options(self) -> None: super().initialize_options() type(self).last_yes = self.yes = False - def finalize_options(self): + def finalize_options(self) -> None: super().finalize_options() type(self).last_yes = self.yes @@ -233,27 +236,27 @@ class BuildExeCommand(cx_Freeze.command.build_exe.build_exe): ('extra-data=', None, 'Additional files to add.'), ] yes: bool - extra_data: Iterable # [any] not available in 3.8 - extra_libs: Iterable # work around broken include_files + extra_data: Iterable[str] + extra_libs: Iterable[str] # work around broken include_files buildfolder: Path libfolder: Path library: Path buildtime: datetime.datetime - def initialize_options(self): + def initialize_options(self) -> None: super().initialize_options() self.yes = BuildCommand.last_yes self.extra_data = [] self.extra_libs = [] - def finalize_options(self): + def finalize_options(self) -> None: super().finalize_options() self.buildfolder = self.build_exe self.libfolder = Path(self.buildfolder, "lib") self.library = Path(self.libfolder, "library.zip") - def installfile(self, path, subpath=None, keep_content: bool = False): + def installfile(self, path: Path, subpath: Optional[Union[str, Path]] = None, keep_content: bool = False) -> None: folder = self.buildfolder if subpath: folder /= subpath @@ -268,7 +271,7 @@ def installfile(self, path, subpath=None, keep_content: bool = False): else: print('Warning,', path, 'not found') - def create_manifest(self, create_hashes=False): + def create_manifest(self, create_hashes: bool = False) -> None: # Since the setup is now split into components and the manifest is not, # it makes most sense to just remove the hashes for now. Not aware of anyone using them. hashes = {} @@ -290,7 +293,7 @@ def create_manifest(self, create_hashes=False): json.dump(manifest, open(manifestpath, "wt"), indent=4) print("Created Manifest") - def run(self): + def run(self) -> None: # start downloading sni asap sni_thread = threading.Thread(target=download_SNI, name="SNI Downloader") sni_thread.start() @@ -341,7 +344,7 @@ def run(self): # post build steps if is_windows: # kivy_deps is win32 only, linux picks them up automatically - from kivy_deps import sdl2, glew + from kivy_deps import sdl2, glew # type: ignore for folder in sdl2.dep_bins + glew.dep_bins: shutil.copytree(folder, self.libfolder, dirs_exist_ok=True) print(f"copying {folder} -> {self.libfolder}") @@ -362,7 +365,7 @@ def run(self): self.installfile(Path(data)) # kivi data files - import kivy + import kivy # type: ignore[import-untyped] shutil.copytree(os.path.join(os.path.dirname(kivy.__file__), "data"), self.buildfolder / "data", dirs_exist_ok=True) @@ -372,7 +375,7 @@ def run(self): from worlds.AutoWorld import AutoWorldRegister assert not non_apworlds - set(AutoWorldRegister.world_types), \ f"Unknown world {non_apworlds - set(AutoWorldRegister.world_types)} designated for .apworld" - folders_to_remove: typing.List[str] = [] + folders_to_remove: List[str] = [] disabled_worlds_folder = "worlds_disabled" for entry in os.listdir(disabled_worlds_folder): if os.path.isdir(os.path.join(disabled_worlds_folder, entry)): @@ -393,7 +396,7 @@ def run(self): shutil.rmtree(world_directory) shutil.copyfile("meta.yaml", self.buildfolder / "Players" / "Templates" / "meta.yaml") try: - from maseya import z3pr + from maseya import z3pr # type: ignore[import-untyped] except ImportError: print("Maseya Palette Shuffle not found, skipping data files.") else: @@ -444,16 +447,16 @@ class AppImageCommand(setuptools.Command): ("app-exec=", None, "The application to run inside the image."), ("yes", "y", 'Answer "yes" to all questions.'), ] - build_folder: typing.Optional[Path] - dist_file: typing.Optional[Path] - app_dir: typing.Optional[Path] + build_folder: Optional[Path] + dist_file: Optional[Path] + app_dir: Optional[Path] app_name: str - app_exec: typing.Optional[Path] - app_icon: typing.Optional[Path] # source file + app_exec: Optional[Path] + app_icon: Optional[Path] # source file app_id: str # lower case name, used for icon and .desktop yes: bool - def write_desktop(self): + def write_desktop(self) -> None: assert self.app_dir, "Invalid app_dir" desktop_filename = self.app_dir / f"{self.app_id}.desktop" with open(desktop_filename, 'w', encoding="utf-8") as f: @@ -468,7 +471,7 @@ def write_desktop(self): ))) desktop_filename.chmod(0o755) - def write_launcher(self, default_exe: Path): + def write_launcher(self, default_exe: Path) -> None: assert self.app_dir, "Invalid app_dir" launcher_filename = self.app_dir / "AppRun" with open(launcher_filename, 'w', encoding="utf-8") as f: @@ -491,7 +494,7 @@ def write_launcher(self, default_exe: Path): """) launcher_filename.chmod(0o755) - def install_icon(self, src: Path, name: typing.Optional[str] = None, symlink: typing.Optional[Path] = None): + def install_icon(self, src: Path, name: Optional[str] = None, symlink: Optional[Path] = None) -> None: assert self.app_dir, "Invalid app_dir" try: from PIL import Image @@ -513,7 +516,8 @@ def install_icon(self, src: Path, name: typing.Optional[str] = None, symlink: ty if symlink: symlink.symlink_to(dest_file.relative_to(symlink.parent)) - def initialize_options(self): + def initialize_options(self) -> None: + assert self.distribution.metadata.name self.build_folder = None self.app_dir = None self.app_name = self.distribution.metadata.name @@ -527,17 +531,22 @@ def initialize_options(self): )) self.yes = False - def finalize_options(self): + def finalize_options(self) -> None: + assert self.build_folder if not self.app_dir: self.app_dir = self.build_folder.parent / "AppDir" self.app_id = self.app_name.lower() - def run(self): + def run(self) -> None: + assert self.build_folder and self.dist_file, "Command not properly set up" + assert ( + self.app_icon and self.app_id and self.app_dir and self.app_exec and self.app_name + ), "AppImageCommand not properly set up" self.dist_file.parent.mkdir(parents=True, exist_ok=True) if self.app_dir.is_dir(): shutil.rmtree(self.app_dir) self.app_dir.mkdir(parents=True) - opt_dir = self.app_dir / "opt" / self.distribution.metadata.name + opt_dir = self.app_dir / "opt" / self.app_name shutil.copytree(self.build_folder, opt_dir) root_icon = self.app_dir / f'{self.app_id}{self.app_icon.suffix}' self.install_icon(self.app_icon, self.app_id, symlink=root_icon) @@ -548,7 +557,7 @@ def run(self): subprocess.call(f'ARCH={build_arch} ./appimagetool -n "{self.app_dir}" "{self.dist_file}"', shell=True) -def find_libs(*args: str) -> typing.Sequence[typing.Tuple[str, str]]: +def find_libs(*args: str) -> Sequence[Tuple[str, str]]: """Try to find system libraries to be included.""" if not args: return [] @@ -556,7 +565,7 @@ def find_libs(*args: str) -> typing.Sequence[typing.Tuple[str, str]]: arch = build_arch.replace('_', '-') libc = 'libc6' # we currently don't support musl - def parse(line): + def parse(line: str) -> Tuple[Tuple[str, str, str], str]: lib, path = line.strip().split(' => ') lib, typ = lib.split(' ', 1) for test_arch in ('x86-64', 'i386', 'aarch64'): @@ -577,26 +586,29 @@ def parse(line): ldconfig = shutil.which("ldconfig") assert ldconfig, "Make sure ldconfig is in PATH" data = subprocess.run([ldconfig, "-p"], capture_output=True, text=True).stdout.split("\n")[1:] - find_libs.cache = { # type: ignore [attr-defined] + find_libs.cache = { # type: ignore[attr-defined] k: v for k, v in (parse(line) for line in data if "=>" in line) } - def find_lib(lib, arch, libc): - for k, v in find_libs.cache.items(): + def find_lib(lib: str, arch: str, libc: str) -> Optional[str]: + cache: Dict[Tuple[str, str, str], str] = getattr(find_libs, "cache") + for k, v in cache.items(): if k == (lib, arch, libc): return v - for k, v, in find_libs.cache.items(): + for k, v, in cache.items(): if k[0].startswith(lib) and k[1] == arch and k[2] == libc: return v return None - res = [] + res: List[Tuple[str, str]] = [] for arg in args: # try exact match, empty libc, empty arch, empty arch and libc file = find_lib(arg, arch, libc) file = file or find_lib(arg, arch, '') file = file or find_lib(arg, '', libc) file = file or find_lib(arg, '', '') + if not file: + raise ValueError(f"Could not find lib {arg}") # resolve symlinks for n in range(0, 5): res.append((file, os.path.join('lib', os.path.basename(file)))) @@ -620,9 +632,9 @@ def find_lib(lib, arch, libc): "packages": ["worlds", "kivy", "cymem", "websockets"], "includes": [], "excludes": ["numpy", "Cython", "PySide2", "PIL", - "pandas"], + "pandas", "zstandard"], "zip_include_packages": ["*"], - "zip_exclude_packages": ["worlds", "sc2", "orjson"], # TODO: remove orjson here once we drop py3.8 support + "zip_exclude_packages": ["worlds", "sc2"], "include_files": [], # broken in cx 6.14.0, we use more special sauce now "include_msvcr": False, "replace_paths": ["*."], diff --git a/test/__init__.py b/test/__init__.py index 37ebe3f62743..ab9383b3cd90 100644 --- a/test/__init__.py +++ b/test/__init__.py @@ -4,6 +4,7 @@ import settings warnings.simplefilter("always") +warnings.filterwarnings(action="ignore", category=DeprecationWarning, module="s2clientprotocol") settings.no_gui = True settings.skip_autosave = True diff --git a/test/general/test_fill.py b/test/general/test_fill.py index 2dba147aca84..c8bcec9581ac 100644 --- a/test/general/test_fill.py +++ b/test/general/test_fill.py @@ -688,8 +688,8 @@ def test_non_excluded_local_items(self): for item in multiworld.get_items(): item.classification = ItemClassification.useful - multiworld.local_items[player1.id].value = set(names(player1.basic_items)) - multiworld.local_items[player2.id].value = set(names(player2.basic_items)) + multiworld.worlds[player1.id].options.local_items.value = set(names(player1.basic_items)) + multiworld.worlds[player2.id].options.local_items.value = set(names(player2.basic_items)) locality_rules(multiworld) distribute_items_restrictive(multiworld) @@ -795,8 +795,8 @@ def setUp(self) -> None: def test_balances_progression(self) -> None: """Tests that progression balancing moves progression items earlier""" - self.multiworld.progression_balancing[self.player1.id].value = 50 - self.multiworld.progression_balancing[self.player2.id].value = 50 + self.multiworld.worlds[self.player1.id].options.progression_balancing.value = 50 + self.multiworld.worlds[self.player2.id].options.progression_balancing.value = 50 self.assertRegionContains( self.player1.regions[2], self.player2.prog_items[0]) @@ -808,8 +808,8 @@ def test_balances_progression(self) -> None: def test_balances_progression_light(self) -> None: """Test that progression balancing still moves items earlier on minimum value""" - self.multiworld.progression_balancing[self.player1.id].value = 1 - self.multiworld.progression_balancing[self.player2.id].value = 1 + self.multiworld.worlds[self.player1.id].options.progression_balancing.value = 1 + self.multiworld.worlds[self.player2.id].options.progression_balancing.value = 1 self.assertRegionContains( self.player1.regions[2], self.player2.prog_items[0]) @@ -822,8 +822,8 @@ def test_balances_progression_light(self) -> None: def test_balances_progression_heavy(self) -> None: """Test that progression balancing moves items earlier on maximum value""" - self.multiworld.progression_balancing[self.player1.id].value = 99 - self.multiworld.progression_balancing[self.player2.id].value = 99 + self.multiworld.worlds[self.player1.id].options.progression_balancing.value = 99 + self.multiworld.worlds[self.player2.id].options.progression_balancing.value = 99 self.assertRegionContains( self.player1.regions[2], self.player2.prog_items[0]) @@ -836,8 +836,8 @@ def test_balances_progression_heavy(self) -> None: def test_skips_balancing_progression(self) -> None: """Test that progression balancing is skipped when players have it disabled""" - self.multiworld.progression_balancing[self.player1.id].value = 0 - self.multiworld.progression_balancing[self.player2.id].value = 0 + self.multiworld.worlds[self.player1.id].options.progression_balancing.value = 0 + self.multiworld.worlds[self.player2.id].options.progression_balancing.value = 0 self.assertRegionContains( self.player1.regions[2], self.player2.prog_items[0]) @@ -849,8 +849,8 @@ def test_skips_balancing_progression(self) -> None: def test_ignores_priority_locations(self) -> None: """Test that progression items on priority locations don't get moved by balancing""" - self.multiworld.progression_balancing[self.player1.id].value = 50 - self.multiworld.progression_balancing[self.player2.id].value = 50 + self.multiworld.worlds[self.player1.id].options.progression_balancing.value = 50 + self.multiworld.worlds[self.player2.id].options.progression_balancing.value = 50 self.player2.prog_items[0].location.progress_type = LocationProgressType.PRIORITY diff --git a/test/general/test_options.py b/test/general/test_options.py index 2229b7ea7e66..7a3743e5a4e7 100644 --- a/test/general/test_options.py +++ b/test/general/test_options.py @@ -21,6 +21,17 @@ def test_options_are_not_set_by_world(self): self.assertFalse(hasattr(world_type, "options"), f"Unexpected assignment to {world_type.__name__}.options!") + def test_duplicate_options(self) -> None: + """Tests that a world doesn't reuse the same option class.""" + for game_name, world_type in AutoWorldRegister.world_types.items(): + with self.subTest(game=game_name): + seen_options = set() + for option in world_type.options_dataclass.type_hints.values(): + if not option.visibility: + continue + self.assertFalse(option in seen_options, f"{option} found in assigned options multiple times.") + seen_options.add(option) + def test_item_links_name_groups(self): """Tests that item links successfully unfold item_name_groups""" item_link_groups = [ @@ -59,3 +70,12 @@ def test_item_links_resolve(self): item_links = {1: ItemLinks.from_any(item_link_group), 2: ItemLinks.from_any(item_link_group)} for link in item_links.values(): self.assertEqual(link.value[0], item_link_group[0]) + + def test_pickle_dumps(self): + """Test options can be pickled into database for WebHost generation""" + import pickle + for gamename, world_type in AutoWorldRegister.world_types.items(): + if not world_type.hidden: + for option_key, option in world_type.options_dataclass.type_hints.items(): + with self.subTest(game=gamename, option=option_key): + pickle.dumps(option.from_any(option.default)) diff --git a/test/options/test_generate_templates.py b/test/options/test_generate_templates.py new file mode 100644 index 000000000000..cab97c54b129 --- /dev/null +++ b/test/options/test_generate_templates.py @@ -0,0 +1,55 @@ +import unittest + +from pathlib import Path +from tempfile import TemporaryDirectory +from typing import TYPE_CHECKING, Dict, Type +from Utils import parse_yaml + +if TYPE_CHECKING: + from worlds.AutoWorld import World + + +class TestGenerateYamlTemplates(unittest.TestCase): + old_world_types: Dict[str, Type["World"]] + + def setUp(self) -> None: + import worlds.AutoWorld + + self.old_world_types = worlds.AutoWorld.AutoWorldRegister.world_types + + def tearDown(self) -> None: + import worlds.AutoWorld + + worlds.AutoWorld.AutoWorldRegister.world_types = self.old_world_types + + if "World: with colon" in worlds.AutoWorld.AutoWorldRegister.world_types: + del worlds.AutoWorld.AutoWorldRegister.world_types["World: with colon"] + + def test_name_with_colon(self) -> None: + from Options import generate_yaml_templates + from worlds.AutoWorld import AutoWorldRegister + from worlds.AutoWorld import World + + class WorldWithColon(World): + game = "World: with colon" + item_name_to_id = {} + location_name_to_id = {} + + AutoWorldRegister.world_types = {WorldWithColon.game: WorldWithColon} + with TemporaryDirectory(f"archipelago_{__name__}") as temp_dir: + generate_yaml_templates(temp_dir) + path: Path + for path in Path(temp_dir).iterdir(): + self.assertTrue(path.is_file()) + self.assertTrue(path.suffix == ".yaml") + with path.open(encoding="utf-8") as f: + try: + data = parse_yaml(f) + except: + f.seek(0) + print(f"Error in {path.name}:\n{f.read()}") + raise + self.assertIn("game", data) + self.assertIn(":", data["game"]) + self.assertIn(data["game"], data) + self.assertIsInstance(data[data["game"]], dict) diff --git a/test/programs/data/weights/weights.yaml b/test/programs/data/weights/weights.yaml new file mode 100644 index 000000000000..1e3c65d8f9c1 --- /dev/null +++ b/test/programs/data/weights/weights.yaml @@ -0,0 +1,10 @@ +name: Player{number} +game: Archipelago # we only need to test options work and this "supports" all the base options +Archipelago: + progression_balancing: + 0: 50 + 50: 50 + 99: 50 + accessibility: + 0: 50 + 2: 50 diff --git a/test/programs/test_generate.py b/test/programs/test_generate.py index 9281c9c753cd..51800a0ec5c2 100644 --- a/test/programs/test_generate.py +++ b/test/programs/test_generate.py @@ -92,3 +92,48 @@ def test_generate_yaml(self): user_path.cached_path = user_path_backup self.assertOutput(self.output_tempdir.name) + + +class TestGenerateWeights(TestGenerateMain): + """Tests Generate.py using a weighted file to generate for multiple players.""" + + # this test will probably break if something in generation is changed that affects the seed before the weights get processed + # can be fixed by changing the expected_results dict + generate_dir = TestGenerateMain.generate_dir + run_dir = TestGenerateMain.run_dir + abs_input_dir = Path(__file__).parent / "data" / "weights" + rel_input_dir = abs_input_dir.relative_to(run_dir) # directly supplied relative paths are relative to cwd + yaml_input_dir = abs_input_dir.relative_to(generate_dir) # yaml paths are relative to user_path + + # don't need to run these tests + test_generate_absolute = None + test_generate_relative = None + + def test_generate_yaml(self): + from settings import get_settings + from Utils import user_path, local_path + settings = get_settings() + settings.generator.player_files_path = settings.generator.PlayerFilesPath(self.yaml_input_dir) + settings.generator.players = 5 # arbitrary number, should be enough + settings._filename = None + user_path_backup = user_path.cached_path + user_path.cached_path = local_path() + try: + sys.argv = [sys.argv[0], "--seed", "1"] + namespace, seed = Generate.main() + finally: + user_path.cached_path = user_path_backup + + # there's likely a better way to do this, but hardcode the results from seed 1 to ensure they're always this + expected_results = { + "accessibility": [0, 2, 0, 2, 2], + "progression_balancing": [0, 50, 99, 0, 50], + } + + self.assertEqual(seed, 1) + for option_name, results in expected_results.items(): + for player, result in enumerate(results, 1): + self.assertEqual( + result, getattr(namespace, option_name)[player].value, + "Generated results from weights file did not match expected value." + ) diff --git a/test/webhost/data/One_Archipelago.archipelago b/test/webhost/data/One_Archipelago.archipelago new file mode 100644 index 000000000000..8b7a8ce0a8a1 Binary files /dev/null and b/test/webhost/data/One_Archipelago.archipelago differ diff --git a/test/webhost/test_docs.py b/test/webhost/test_docs.py index 68aba05f9dcc..1e6c1b88f42c 100644 --- a/test/webhost/test_docs.py +++ b/test/webhost/test_docs.py @@ -30,10 +30,16 @@ def test_has_tutorial(self): def test_has_game_info(self): for game_name, world_type in AutoWorldRegister.world_types.items(): if not world_type.hidden: - target_path = Utils.local_path("WebHostLib", "static", "generated", "docs", game_name) + safe_name = Utils.get_file_safe_name(game_name) + target_path = Utils.local_path("WebHostLib", "static", "generated", "docs", safe_name) for game_info_lang in world_type.web.game_info_languages: with self.subTest(game_name): self.assertTrue( - os.path.isfile(Utils.local_path(target_path, f'{game_info_lang}_{game_name}.md')), + safe_name == game_name or + not os.path.isfile(Utils.local_path(target_path, f'{game_info_lang}_{game_name}.md')), + f'Info docs have be named _{safe_name}.md for {game_name}.' + ) + self.assertTrue( + os.path.isfile(Utils.local_path(target_path, f'{game_info_lang}_{safe_name}.md')), f'{game_name} missing game info file for "{game_info_lang}" language.' ) diff --git a/test/webhost/test_generate.py b/test/webhost/test_generate.py new file mode 100644 index 000000000000..5440f6e02bec --- /dev/null +++ b/test/webhost/test_generate.py @@ -0,0 +1,73 @@ +import zipfile +from io import BytesIO + +from flask import url_for + +from . import TestBase + + +class TestGenerate(TestBase): + def test_valid_yaml(self) -> None: + """ + Verify that posting a valid yaml will start generating a game. + """ + with self.app.app_context(), self.app.test_request_context(): + yaml_data = """ + name: Player1 + game: Archipelago + Archipelago: {} + """ + response = self.client.post(url_for("generate"), + data={"file": (BytesIO(yaml_data.encode("utf-8")), "test.yaml")}, + follow_redirects=True) + self.assertEqual(response.status_code, 200) + self.assertTrue("/seed/" in response.request.path or + "/wait/" in response.request.path, + f"Response did not properly redirect ({response.request.path})") + + def test_empty_zip(self) -> None: + """ + Verify that posting an empty zip will give an error. + """ + with self.app.app_context(), self.app.test_request_context(): + zip_data = BytesIO() + zipfile.ZipFile(zip_data, "w").close() + zip_data.seek(0) + self.assertGreater(len(zip_data.read()), 0) + zip_data.seek(0) + response = self.client.post(url_for("generate"), + data={"file": (zip_data, "test.zip")}, + follow_redirects=True) + self.assertIn("user-message", response.text, + "Request did not call flash()") + self.assertIn("not find any valid files", response.text, + "Response shows unexpected error") + self.assertIn("generate-game-form", response.text, + "Response did not get user back to the form") + + def test_too_many_players(self) -> None: + """ + Verify that posting too many players will give an error. + """ + max_roll = self.app.config["MAX_ROLL"] + # validate that max roll has a sensible value, otherwise we probably changed how it works + self.assertIsInstance(max_roll, int) + self.assertGreater(max_roll, 1) + self.assertLess(max_roll, 100) + # create a yaml with max_roll+1 players and watch it fail + with self.app.app_context(), self.app.test_request_context(): + yaml_data = "---\n".join([ + f"name: Player{n}\n" + "game: Archipelago\n" + "Archipelago: {}\n" + for n in range(1, max_roll + 2) + ]) + response = self.client.post(url_for("generate"), + data={"file": (BytesIO(yaml_data.encode("utf-8")), "test.yaml")}, + follow_redirects=True) + self.assertIn("user-message", response.text, + "Request did not call flash()") + self.assertIn("limited to", response.text, + "Response shows unexpected error") + self.assertIn("generate-game-form", response.text, + "Response did not get user back to the form") diff --git a/test/webhost/test_option_presets.py b/test/webhost/test_option_presets.py index b0af8a871183..7105c7f80593 100644 --- a/test/webhost/test_option_presets.py +++ b/test/webhost/test_option_presets.py @@ -1,5 +1,6 @@ import unittest +from BaseClasses import PlandoOptions from worlds import AutoWorldRegister from Options import ItemDict, NamedRange, NumericOption, OptionList, OptionSet @@ -14,6 +15,10 @@ def test_option_presets_have_valid_options(self): with self.subTest(game=game_name, preset=preset_name, option=option_name): try: option = world_type.options_dataclass.type_hints[option_name].from_any(option_value) + # some options may need verification to ensure the provided option is actually valid + # pass in all plando options in case a preset wants to require certain plando options + # for some reason + option.verify(world_type, "Test Player", PlandoOptions(sum(PlandoOptions))) supported_types = [NumericOption, OptionSet, OptionList, ItemDict] if not any([issubclass(option.__class__, t) for t in supported_types]): self.fail(f"'{option_name}' in preset '{preset_name}' for game '{game_name}' " diff --git a/test/webhost/test_tracker.py b/test/webhost/test_tracker.py new file mode 100644 index 000000000000..58145d77f3bc --- /dev/null +++ b/test/webhost/test_tracker.py @@ -0,0 +1,95 @@ +import os +import pickle +from pathlib import Path +from typing import ClassVar +from uuid import UUID, uuid4 + +from flask import url_for + +from . import TestBase + + +class TestTracker(TestBase): + room_id: UUID + tracker_uuid: UUID + log_filename: str + data: ClassVar[bytes] + + @classmethod + def setUpClass(cls) -> None: + super().setUpClass() + with (Path(__file__).parent / "data" / "One_Archipelago.archipelago").open("rb") as f: + cls.data = f.read() + + def setUp(self) -> None: + from pony.orm import db_session + from MultiServer import Context as MultiServerContext + from Utils import user_path + from WebHostLib.models import GameDataPackage, Room, Seed + + super().setUp() + + multidata = MultiServerContext.decompress(self.data) + + with self.client.session_transaction() as session: + session["_id"] = uuid4() + self.tracker_uuid = uuid4() + with db_session: + # store game datapackage(s) + for game, game_data in multidata["datapackage"].items(): + if not GameDataPackage.get(checksum=game_data["checksum"]): + GameDataPackage(checksum=game_data["checksum"], + data=pickle.dumps(game_data)) + # create an empty seed and a room from it + seed = Seed(multidata=self.data, owner=session["_id"]) + room = Room(seed=seed, owner=session["_id"], tracker=self.tracker_uuid) + self.room_id = room.id + self.log_filename = user_path("logs", f"{self.room_id}.txt") + + def tearDown(self) -> None: + from pony.orm import db_session, select + from WebHostLib.models import Command, Room + + with db_session: + for command in select(command for command in Command if command.room.id == self.room_id): # type: ignore + command.delete() + room: Room = Room.get(id=self.room_id) + room.seed.delete() + room.delete() + + try: + os.unlink(self.log_filename) + except FileNotFoundError: + pass + + def test_valid_if_modified_since(self) -> None: + """ + Verify that we get a 200 response for valid If-Modified-Since + """ + with self.app.app_context(), self.app.test_request_context(): + response = self.client.get( + url_for( + "get_player_tracker", + tracker=self.tracker_uuid, + tracked_team=0, + tracked_player=1, + ), + headers={"If-Modified-Since": "Wed, 21 Oct 2015 07:28:00 GMT"}, + ) + self.assertEqual(response.status_code, 200) + + def test_invalid_if_modified_since(self) -> None: + """ + Verify that we get a 400 response for invalid If-Modified-Since + """ + with self.app.app_context(), self.app.test_request_context(): + response = self.client.get( + url_for( + "get_player_tracker", + tracker=self.tracker_uuid, + tracked_team=1, + tracked_player=0, + ), + headers={"If-Modified-Since": "Wed, 21 Oct 2015 07:28:00"}, # missing timezone + ) + self.assertEqual(response.status_code, 400) diff --git a/worlds/AutoSNIClient.py b/worlds/AutoSNIClient.py index 2b984d9c8846..f9444eee73c6 100644 --- a/worlds/AutoSNIClient.py +++ b/worlds/AutoSNIClient.py @@ -1,9 +1,8 @@ from __future__ import annotations import abc -from typing import TYPE_CHECKING, ClassVar, Dict, Iterable, Tuple, Any, Optional, Union - -from typing_extensions import TypeGuard +import logging +from typing import TYPE_CHECKING, ClassVar, Dict, Iterable, Tuple, Any, Optional, Union, TypeGuard from worlds.LauncherComponents import Component, SuffixIdentifier, Type, components @@ -60,8 +59,12 @@ def __new__(cls, name: str, bases: Tuple[type, ...], dct: Dict[str, Any]) -> Aut @staticmethod async def get_handler(ctx: SNIContext) -> Optional[SNIClient]: for _game, handler in AutoSNIClientRegister.game_handlers.items(): - if await handler.validate_rom(ctx): - return handler + try: + if await handler.validate_rom(ctx): + return handler + except Exception as e: + text_file_logger = logging.getLogger() + text_file_logger.exception(e) return None diff --git a/worlds/AutoWorld.py b/worlds/AutoWorld.py index f7dae2b92750..3c4edc1b0c3b 100644 --- a/worlds/AutoWorld.py +++ b/worlds/AutoWorld.py @@ -10,7 +10,7 @@ from typing import (Any, Callable, ClassVar, Dict, FrozenSet, List, Mapping, Optional, Set, TextIO, Tuple, TYPE_CHECKING, Type, Union) -from Options import item_and_loc_options, OptionGroup, PerGameCommonOptions +from Options import item_and_loc_options, ItemsAccessibility, OptionGroup, PerGameCommonOptions from BaseClasses import CollectionState if TYPE_CHECKING: @@ -480,6 +480,7 @@ def create_group(cls, multiworld: "MultiWorld", new_player_id: int, players: Set group = cls(multiworld, new_player_id) group.options = cls.options_dataclass(**{option_key: option.from_any(option.default) for option_key, option in cls.options_dataclass.type_hints.items()}) + group.options.accessibility = ItemsAccessibility(ItemsAccessibility.option_items) return group diff --git a/worlds/LauncherComponents.py b/worlds/LauncherComponents.py index fe6e44bb308e..3c4c4477ef09 100644 --- a/worlds/LauncherComponents.py +++ b/worlds/LauncherComponents.py @@ -100,10 +100,16 @@ def _install_apworld(apworld_src: str = "") -> Optional[Tuple[pathlib.Path, path apworld_path = pathlib.Path(apworld_src) - module_name = pathlib.Path(apworld_path.name).stem try: import zipfile - zipfile.ZipFile(apworld_path).open(module_name + "/__init__.py") + zip = zipfile.ZipFile(apworld_path) + directories = [f.filename.strip('/') for f in zip.filelist if f.CRC == 0 and f.file_size == 0 and f.filename.count('/') == 1] + if len(directories) == 1 and directories[0] in apworld_path.stem: + module_name = directories[0] + apworld_name = module_name + ".apworld" + else: + raise Exception("APWorld appears to be invalid or damaged. (expected a single directory)") + zip.open(module_name + "/__init__.py") except ValueError as e: raise Exception("Archive appears invalid or damaged.") from e except KeyError as e: @@ -122,7 +128,7 @@ def _install_apworld(apworld_src: str = "") -> Optional[Tuple[pathlib.Path, path # TODO: run generic test suite over the apworld. # TODO: have some kind of version system to tell from metadata if the apworld should be compatible. - target = pathlib.Path(worlds.user_folder) / apworld_path.name + target = pathlib.Path(worlds.user_folder) / apworld_name import shutil shutil.copyfile(apworld_path, target) diff --git a/worlds/__init__.py b/worlds/__init__.py index c277ac9ca1de..7db651bdd9e3 100644 --- a/worlds/__init__.py +++ b/worlds/__init__.py @@ -66,19 +66,12 @@ def load(self) -> bool: start = time.perf_counter() if self.is_zip: importer = zipimport.zipimporter(self.resolved_path) - if hasattr(importer, "find_spec"): # new in Python 3.10 - spec = importer.find_spec(os.path.basename(self.path).rsplit(".", 1)[0]) - assert spec, f"{self.path} is not a loadable module" - mod = importlib.util.module_from_spec(spec) - else: # TODO: remove with 3.8 support - mod = importer.load_module(os.path.basename(self.path).rsplit(".", 1)[0]) - - if mod.__package__ is not None: - mod.__package__ = f"worlds.{mod.__package__}" - else: - # load_module does not populate package, we'll have to assume mod.__name__ is correct here - # probably safe to remove with 3.8 support - mod.__package__ = f"worlds.{mod.__name__}" + spec = importer.find_spec(os.path.basename(self.path).rsplit(".", 1)[0]) + assert spec, f"{self.path} is not a loadable module" + mod = importlib.util.module_from_spec(spec) + + mod.__package__ = f"worlds.{mod.__package__}" + mod.__name__ = f"worlds.{mod.__name__}" sys.modules[mod.__name__] = mod with warnings.catch_warnings(): diff --git a/worlds/ahit/Regions.py b/worlds/ahit/Regions.py index c70f08b475eb..31edf1d0b057 100644 --- a/worlds/ahit/Regions.py +++ b/worlds/ahit/Regions.py @@ -740,17 +740,20 @@ def is_valid_first_act(world: "HatInTimeWorld", act: Region) -> bool: def connect_time_rift(world: "HatInTimeWorld", time_rift: Region, exit_region: Region): - i = 1 - while i <= len(rift_access_regions[time_rift.name]): + for i, access_region in enumerate(rift_access_regions[time_rift.name], start=1): + # Matches the naming convention and iteration order in `create_rift_connections()`. name = f"{time_rift.name} Portal - Entrance {i}" entrance: Entrance try: - entrance = world.multiworld.get_entrance(name, world.player) + entrance = world.get_entrance(name) + # Reconnect the rift access region to the new exit region. reconnect_regions(entrance, entrance.parent_region, exit_region) except KeyError: - time_rift.connect(exit_region, name) - - i += 1 + # The original entrance to the time rift has been deleted by already reconnecting a telescope act to the + # time rift, so create a new entrance from the original rift access region to the new exit region. + # Normally, acts and time rifts are sorted such that time rifts are reconnected to acts/rifts first, but + # starting acts/rifts and act-plando can reconnect acts to time rifts before this happens. + world.get_region(access_region).connect(exit_region, name) def get_shuffleable_act_regions(world: "HatInTimeWorld") -> List[Region]: diff --git a/worlds/alttp/EntranceShuffle.py b/worlds/alttp/EntranceShuffle.py index f759b6309a0e..d0487494aa64 100644 --- a/worlds/alttp/EntranceShuffle.py +++ b/worlds/alttp/EntranceShuffle.py @@ -3338,25 +3338,6 @@ def plando_connect(world, player: int): ('Turtle Rock Exit (Front)', 'Dark Death Mountain'), ('Ice Palace Exit', 'Dark Lake Hylia')] -# Regions that can be required to access entrances through rules, not paths -indirect_connections = { - "Turtle Rock (Top)": "Turtle Rock", - "East Dark World": "Pyramid Fairy", - "Dark Desert": "Pyramid Fairy", - "West Dark World": "Pyramid Fairy", - "South Dark World": "Pyramid Fairy", - "Light World": "Pyramid Fairy", - "Old Man Cave": "Old Man S&Q" -} - -indirect_connections_inverted = { - "Inverted Big Bomb Shop": "Pyramid Fairy", -} - -indirect_connections_not_inverted = { - "Big Bomb Shop": "Pyramid Fairy", -} - # format: # Key=Name # addr = (door_index, exitdata) # multiexit diff --git a/worlds/alttp/__init__.py b/worlds/alttp/__init__.py index 3cdbb1cb458a..f897d3762929 100644 --- a/worlds/alttp/__init__.py +++ b/worlds/alttp/__init__.py @@ -8,8 +8,7 @@ import Utils from BaseClasses import Item, CollectionState, Tutorial, MultiWorld from .Dungeons import create_dungeons, Dungeon -from .EntranceShuffle import link_entrances, link_inverted_entrances, plando_connect, \ - indirect_connections, indirect_connections_inverted, indirect_connections_not_inverted +from .EntranceShuffle import link_entrances, link_inverted_entrances, plando_connect from .InvertedRegions import create_inverted_regions, mark_dark_world_regions from .ItemPool import generate_itempool, difficulties from .Items import item_init_table, item_name_groups, item_table, GetBeemizerItem @@ -137,6 +136,7 @@ class ALTTPWorld(World): settings_key = "lttp_options" settings: typing.ClassVar[ALTTPSettings] topology_present = True + explicit_indirect_conditions = False item_name_groups = item_name_groups location_name_groups = { "Blind's Hideout": {"Blind's Hideout - Top", "Blind's Hideout - Left", "Blind's Hideout - Right", @@ -394,23 +394,13 @@ def create_regions(self): if multiworld.mode[player] != 'inverted': link_entrances(multiworld, player) mark_light_world_regions(multiworld, player) - for region_name, entrance_name in indirect_connections_not_inverted.items(): - multiworld.register_indirect_condition(multiworld.get_region(region_name, player), - multiworld.get_entrance(entrance_name, player)) else: link_inverted_entrances(multiworld, player) mark_dark_world_regions(multiworld, player) - for region_name, entrance_name in indirect_connections_inverted.items(): - multiworld.register_indirect_condition(multiworld.get_region(region_name, player), - multiworld.get_entrance(entrance_name, player)) multiworld.random = old_random plando_connect(multiworld, player) - for region_name, entrance_name in indirect_connections.items(): - multiworld.register_indirect_condition(multiworld.get_region(region_name, player), - multiworld.get_entrance(entrance_name, player)) - def collect_item(self, state: CollectionState, item: Item, remove=False): item_name = item.name if item_name.startswith('Progressive '): diff --git a/worlds/alttp/test/items/TestDifficulty.py b/worlds/alttp/test/items/TestDifficulty.py index 8fee56f393c7..69dd8a4dc6ba 100644 --- a/worlds/alttp/test/items/TestDifficulty.py +++ b/worlds/alttp/test/items/TestDifficulty.py @@ -1,5 +1,5 @@ from worlds.alttp.ItemPool import difficulties -from test.TestBase import TestBase +from test.bases import TestBase base_items = 41 extra_counts = (15, 15, 10, 5, 25) diff --git a/worlds/alttp/test/items/TestPrizes.py b/worlds/alttp/test/items/TestPrizes.py index 5e729093f9b3..5a9f6aa9c9ae 100644 --- a/worlds/alttp/test/items/TestPrizes.py +++ b/worlds/alttp/test/items/TestPrizes.py @@ -1,7 +1,7 @@ from typing import List from BaseClasses import Item, Location -from test.TestBase import WorldTestBase +from test.bases import WorldTestBase class TestPrizes(WorldTestBase): diff --git a/worlds/alttp/test/minor_glitches/TestMinor.py b/worlds/alttp/test/minor_glitches/TestMinor.py index 8432028bf007..7663c20a2943 100644 --- a/worlds/alttp/test/minor_glitches/TestMinor.py +++ b/worlds/alttp/test/minor_glitches/TestMinor.py @@ -2,7 +2,7 @@ from worlds.alttp.InvertedRegions import mark_dark_world_regions from worlds.alttp.ItemPool import difficulties from worlds.alttp.Items import item_factory -from test.TestBase import TestBase +from test.bases import TestBase from worlds.alttp.Options import GlitchesRequired from worlds.alttp.test import LTTPTestBase diff --git a/worlds/alttp/test/owg/TestVanillaOWG.py b/worlds/alttp/test/owg/TestVanillaOWG.py index 67156eb97275..e51970bc50b6 100644 --- a/worlds/alttp/test/owg/TestVanillaOWG.py +++ b/worlds/alttp/test/owg/TestVanillaOWG.py @@ -2,7 +2,7 @@ from worlds.alttp.InvertedRegions import mark_dark_world_regions from worlds.alttp.ItemPool import difficulties from worlds.alttp.Items import item_factory -from test.TestBase import TestBase +from test.bases import TestBase from worlds.alttp.Options import GlitchesRequired from worlds.alttp.test import LTTPTestBase diff --git a/worlds/alttp/test/shops/TestSram.py b/worlds/alttp/test/shops/TestSram.py index f5feedfb373e..74a41a628988 100644 --- a/worlds/alttp/test/shops/TestSram.py +++ b/worlds/alttp/test/shops/TestSram.py @@ -1,5 +1,5 @@ from worlds.alttp.Shops import shop_table -from test.TestBase import TestBase +from test.bases import TestBase class TestSram(TestBase): diff --git a/worlds/alttp/test/vanilla/TestVanilla.py b/worlds/alttp/test/vanilla/TestVanilla.py index 7eebc349d43f..9b5db7b12291 100644 --- a/worlds/alttp/test/vanilla/TestVanilla.py +++ b/worlds/alttp/test/vanilla/TestVanilla.py @@ -2,7 +2,7 @@ from worlds.alttp.InvertedRegions import mark_dark_world_regions from worlds.alttp.ItemPool import difficulties from worlds.alttp.Items import item_factory -from test.TestBase import TestBase +from test.bases import TestBase from worlds.alttp.Options import GlitchesRequired from worlds.alttp.test import LTTPTestBase diff --git a/worlds/aquaria/Regions.py b/worlds/aquaria/Regions.py index 3ec1fb880e13..7a41e0d0c864 100755 --- a/worlds/aquaria/Regions.py +++ b/worlds/aquaria/Regions.py @@ -738,9 +738,7 @@ def __connect_veil_regions(self) -> None: self.__connect_regions("Sun Temple left area", "Veil left of sun temple", self.sun_temple_l, self.veil_tr_l) self.__connect_regions("Sun Temple left area", "Sun Temple before boss area", - self.sun_temple_l, self.sun_temple_boss_path, - lambda state: _has_light(state, self.player) or - _has_sun_crystal(state, self.player)) + self.sun_temple_l, self.sun_temple_boss_path) self.__connect_regions("Sun Temple before boss area", "Sun Temple boss area", self.sun_temple_boss_path, self.sun_temple_boss, lambda state: _has_energy_attack_item(state, self.player)) @@ -775,14 +773,11 @@ def __connect_abyss_regions(self) -> None: self.abyss_l, self.king_jellyfish_cave, lambda state: (_has_energy_form(state, self.player) and _has_beast_form(state, self.player)) or - _has_dual_form(state, self.player)) + _has_dual_form(state, self.player)) self.__connect_regions("Abyss left area", "Abyss right area", self.abyss_l, self.abyss_r) - self.__connect_one_way_regions("Abyss right area", "Abyss right area, transturtle", + self.__connect_regions("Abyss right area", "Abyss right area, transturtle", self.abyss_r, self.abyss_r_transturtle) - self.__connect_one_way_regions("Abyss right area, transturtle", "Abyss right area", - self.abyss_r_transturtle, self.abyss_r, - lambda state: _has_light(state, self.player)) self.__connect_regions("Abyss right area", "Inside the whale", self.abyss_r, self.whale, lambda state: _has_spirit_form(state, self.player) and @@ -1092,12 +1087,10 @@ def __adjusting_light_in_dark_place_rules(self) -> None: lambda state: _has_light(state, self.player)) add_rule(self.multiworld.get_entrance("Open Water bottom left area to Abyss left area", self.player), lambda state: _has_light(state, self.player)) - add_rule(self.multiworld.get_entrance("Sun Temple left area to Sun Temple right area", self.player), - lambda state: _has_light(state, self.player) or _has_sun_crystal(state, self.player)) - add_rule(self.multiworld.get_entrance("Sun Temple right area to Sun Temple left area", self.player), - lambda state: _has_light(state, self.player) or _has_sun_crystal(state, self.player)) add_rule(self.multiworld.get_entrance("Veil left of sun temple to Sun Temple left area", self.player), lambda state: _has_light(state, self.player) or _has_sun_crystal(state, self.player)) + add_rule(self.multiworld.get_entrance("Abyss right area, transturtle to Abyss right area", self.player), + lambda state: _has_light(state, self.player)) def __adjusting_manual_rules(self) -> None: add_rule(self.multiworld.get_location("Mithalas Cathedral, Mithalan Dress", self.player), @@ -1151,83 +1144,87 @@ def __adjusting_manual_rules(self) -> None: lambda state: state.has("Sun God beated", self.player)) add_rule(self.multiworld.get_location("The Body center area, breaking Li's cage", self.player), lambda state: _has_tongue_cleared(state, self.player)) + add_rule(self.multiworld.get_location( + "Open Water top right area, bulb in the small path before Mithalas", + self.player), lambda state: _has_bind_song(state, self.player) + ) def __no_progression_hard_or_hidden_location(self) -> None: self.multiworld.get_location("Energy Temple boss area, Fallen God Tooth", self.player).item_rule = \ - lambda item: item.classification != ItemClassification.progression + lambda item: not item.advancement self.multiworld.get_location("Mithalas boss area, beating Mithalan God", self.player).item_rule = \ - lambda item: item.classification != ItemClassification.progression + lambda item: not item.advancement self.multiworld.get_location("Kelp Forest boss area, beating Drunian God", self.player).item_rule = \ - lambda item: item.classification != ItemClassification.progression + lambda item: not item.advancement self.multiworld.get_location("Sun Temple boss area, beating Sun God", self.player).item_rule = \ - lambda item: item.classification != ItemClassification.progression + lambda item: not item.advancement self.multiworld.get_location("Sunken City, bulb on top of the boss area", self.player).item_rule = \ - lambda item: item.classification != ItemClassification.progression + lambda item: not item.advancement self.multiworld.get_location("Home Water, Nautilus Egg", self.player).item_rule = \ - lambda item: item.classification != ItemClassification.progression + lambda item: not item.advancement self.multiworld.get_location("Energy Temple blaster room, Blaster Egg", self.player).item_rule = \ - lambda item: item.classification != ItemClassification.progression + lambda item: not item.advancement self.multiworld.get_location("Mithalas City Castle, beating the Priests", self.player).item_rule = \ - lambda item: item.classification != ItemClassification.progression + lambda item: not item.advancement self.multiworld.get_location("Mermog cave, Piranha Egg", self.player).item_rule = \ - lambda item: item.classification != ItemClassification.progression + lambda item: not item.advancement self.multiworld.get_location("Octopus Cave, Dumbo Egg", self.player).item_rule = \ - lambda item: item.classification != ItemClassification.progression + lambda item: not item.advancement self.multiworld.get_location("King Jellyfish Cave, bulb in the right path from King Jelly", self.player).item_rule = \ - lambda item: item.classification != ItemClassification.progression + lambda item: not item.advancement self.multiworld.get_location("King Jellyfish Cave, Jellyfish Costume", self.player).item_rule = \ - lambda item: item.classification != ItemClassification.progression + lambda item: not item.advancement self.multiworld.get_location("Final Boss area, bulb in the boss third form room", self.player).item_rule = \ - lambda item: item.classification != ItemClassification.progression + lambda item: not item.advancement self.multiworld.get_location("Sun Worm path, first cliff bulb", self.player).item_rule = \ - lambda item: item.classification != ItemClassification.progression + lambda item: not item.advancement self.multiworld.get_location("Sun Worm path, second cliff bulb", self.player).item_rule = \ - lambda item: item.classification != ItemClassification.progression + lambda item: not item.advancement self.multiworld.get_location("The Veil top right area, bulb at the top of the waterfall", self.player).item_rule = \ - lambda item: item.classification != ItemClassification.progression + lambda item: not item.advancement self.multiworld.get_location("Bubble Cave, bulb in the left cave wall", self.player).item_rule = \ - lambda item: item.classification != ItemClassification.progression + lambda item: not item.advancement self.multiworld.get_location("Bubble Cave, bulb in the right cave wall (behind the ice crystal)", self.player).item_rule = \ - lambda item: item.classification != ItemClassification.progression + lambda item: not item.advancement self.multiworld.get_location("Bubble Cave, Verse Egg", self.player).item_rule = \ - lambda item: item.classification != ItemClassification.progression + lambda item: not item.advancement self.multiworld.get_location("Kelp Forest bottom left area, bulb close to the spirit crystals", self.player).item_rule = \ - lambda item: item.classification != ItemClassification.progression + lambda item: not item.advancement self.multiworld.get_location("Kelp Forest bottom left area, Walker Baby", self.player).item_rule = \ - lambda item: item.classification != ItemClassification.progression + lambda item: not item.advancement self.multiworld.get_location("Sun Temple, Sun Key", self.player).item_rule = \ - lambda item: item.classification != ItemClassification.progression + lambda item: not item.advancement self.multiworld.get_location("The Body bottom area, Mutant Costume", self.player).item_rule = \ - lambda item: item.classification != ItemClassification.progression + lambda item: not item.advancement self.multiworld.get_location("Sun Temple, bulb in the hidden room of the right part", self.player).item_rule = \ - lambda item: item.classification != ItemClassification.progression + lambda item: not item.advancement self.multiworld.get_location("Arnassi Ruins, Arnassi Armor", self.player).item_rule = \ - lambda item: item.classification != ItemClassification.progression + lambda item: not item.advancement def adjusting_rules(self, options: AquariaOptions) -> None: """ diff --git a/worlds/aquaria/__init__.py b/worlds/aquaria/__init__.py index 1fb04036d81b..f620bf6d7306 100644 --- a/worlds/aquaria/__init__.py +++ b/worlds/aquaria/__init__.py @@ -117,25 +117,23 @@ def create_item(self, name: str) -> AquariaItem: Create an AquariaItem using 'name' as item name. """ result: AquariaItem - try: - data = item_table[name] - classification: ItemClassification = ItemClassification.useful - if data.type == ItemType.JUNK: - classification = ItemClassification.filler - elif data.type == ItemType.PROGRESSION: - classification = ItemClassification.progression - result = AquariaItem(name, classification, data.id, self.player) - except BaseException: - raise Exception('The item ' + name + ' is not valid.') + data = item_table[name] + classification: ItemClassification = ItemClassification.useful + if data.type == ItemType.JUNK: + classification = ItemClassification.filler + elif data.type == ItemType.PROGRESSION: + classification = ItemClassification.progression + result = AquariaItem(name, classification, data.id, self.player) return result - def __pre_fill_item(self, item_name: str, location_name: str, precollected) -> None: + def __pre_fill_item(self, item_name: str, location_name: str, precollected, + itemClassification: ItemClassification = ItemClassification.useful) -> None: """Pre-assign an item to a location""" if item_name not in precollected: self.exclude.append(item_name) data = item_table[item_name] - item = AquariaItem(item_name, ItemClassification.useful, data.id, self.player) + item = AquariaItem(item_name, itemClassification, data.id, self.player) self.multiworld.get_location(location_name, self.player).place_locked_item(item) def get_filler_item_name(self): @@ -164,7 +162,8 @@ def create_items(self) -> None: self.__pre_fill_item("Transturtle Abyss right", "Abyss right area, Transturtle", precollected) self.__pre_fill_item("Transturtle Final Boss", "Final Boss area, Transturtle", precollected) # The last two are inverted because in the original game, they are special turtle that communicate directly - self.__pre_fill_item("Transturtle Simon Says", "Arnassi Ruins, Transturtle", precollected) + self.__pre_fill_item("Transturtle Simon Says", "Arnassi Ruins, Transturtle", precollected, + ItemClassification.progression) self.__pre_fill_item("Transturtle Arnassi Ruins", "Simon Says area, Transturtle", precollected) for name, data in item_table.items(): if name not in self.exclude: @@ -212,4 +211,8 @@ def fill_slot_data(self) -> Dict[str, Any]: "skip_first_vision": bool(self.options.skip_first_vision.value), "unconfine_home_water_energy_door": self.options.unconfine_home_water.value in [1, 3], "unconfine_home_water_transturtle": self.options.unconfine_home_water.value in [2, 3], + "bind_song_needed_to_get_under_rock_bulb": bool(self.options.bind_song_needed_to_get_under_rock_bulb), + "no_progression_hard_or_hidden_locations": bool(self.options.no_progression_hard_or_hidden_locations), + "light_needed_to_get_to_dark_places": bool(self.options.light_needed_to_get_to_dark_places), + "turtle_randomizer": self.options.turtle_randomizer.value, } diff --git a/worlds/aquaria/docs/setup_en.md b/worlds/aquaria/docs/setup_en.md index 34196757a31c..8177725ded64 100644 --- a/worlds/aquaria/docs/setup_en.md +++ b/worlds/aquaria/docs/setup_en.md @@ -8,6 +8,8 @@ ## Optional Software - For sending [commands](/tutorial/Archipelago/commands/en) like `!hint`: the TextClient from [the most recent Archipelago release](https://github.com/ArchipelagoMW/Archipelago/releases) +- [Aquaria AP Tracker](https://github.com/palex00/aquaria-ap-tracker/releases/latest), for use with +[PopTracker](https://github.com/black-sliver/PopTracker/releases/latest) ## Installation and execution Procedures @@ -113,3 +115,16 @@ sure that your executable has executable permission: ```bash chmod +x aquaria_randomizer ``` + +## Auto-Tracking + +Aquaria has a fully functional map tracker that supports auto-tracking. + +1. Download [Aquaria AP Tracker](https://github.com/palex00/aquaria-ap-tracker/releases/latest) and +[PopTracker](https://github.com/black-sliver/PopTracker/releases/latest). +2. Put the tracker pack into /packs/ in your PopTracker install. +3. Open PopTracker, and load the Aquaria pack. +4. For autotracking, click on the "AP" symbol at the top. +5. Enter the Archipelago server address (the one you connected your client to), slot name, and password. + +This pack will automatically prompt you to update if one is available. diff --git a/worlds/aquaria/docs/setup_fr.md b/worlds/aquaria/docs/setup_fr.md index 2c34f1e6a50f..66b6d6119708 100644 --- a/worlds/aquaria/docs/setup_fr.md +++ b/worlds/aquaria/docs/setup_fr.md @@ -2,9 +2,14 @@ ## Logiciels nÊcessaires -- Le jeu Aquaria original (trouvable sur la majoritÊ des sites de ventes de jeux vidÊo en ligne) -- Le client Randomizer d'Aquaria [Aquaria randomizer](https://github.com/tioui/Aquaria_Randomizer/releases) +- Une copie du jeu Aquaria non-modifiÊe (disponible sur la majoritÊ des sites de ventes de jeux vidÊos en ligne) +- Le client du Randomizer d'Aquaria [Aquaria randomizer] +(https://github.com/tioui/Aquaria_Randomizer/releases) + +## Logiciels optionnels + - De manière optionnel, pour pouvoir envoyer des [commandes](/tutorial/Archipelago/commands/en) comme `!hint`: utilisez le client texte de [la version la plus rÊcente d'Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases) +- [Aquaria AP Tracker](https://github.com/palex00/aquaria-ap-tracker/releases/latest), pour utiliser avec [PopTracker](https://github.com/black-sliver/PopTracker/releases/latest) ## ProcÊdures d'installation et d'exÊcution @@ -116,3 +121,15 @@ pour vous assurer que votre fichier est exÊcutable: ```bash chmod +x aquaria_randomizer ``` + +## Tracking automatique + +Aquaria a un tracker complet qui supporte le tracking automatique. + +1. TÊlÊchargez [Aquaria AP Tracker](https://github.com/palex00/aquaria-ap-tracker/releases/latest) et [PopTracker](https://github.com/black-sliver/PopTracker/releases/latest). +2. Mettre le fichier compressÊ du tracker dans le sous-rÊpertoire /packs/ du rÊpertoire d'installation de PopTracker. +3. Lancez PopTracker, et ouvrez le pack d'Aquaria. +4. Pour activer le tracking automatique, cliquez sur le symbole "AP" dans le haut de la fenÃĒtre. +5. Entrez l'adresse du serveur Archipelago (le serveur auquel vous avez connectÊ le client), le nom de votre slot, et le mot de passe (si un mot de passe est nÊcessaire). + +Le logiciel vous indiquera si une mise à jour du pack est disponible. diff --git a/worlds/aquaria/test/test_no_progression_hard_hidden_locations.py b/worlds/aquaria/test/test_no_progression_hard_hidden_locations.py index f015b26de10b..517af3028dd2 100644 --- a/worlds/aquaria/test/test_no_progression_hard_hidden_locations.py +++ b/worlds/aquaria/test/test_no_progression_hard_hidden_locations.py @@ -49,7 +49,7 @@ def test_unconfine_home_water_both_location_fillable(self) -> None: for location in self.unfillable_locations: for item_name in self.world.item_names: item = self.get_item_by_name(item_name) - if item.classification == ItemClassification.progression: + if item.advancement: self.assertFalse( self.world.get_location(location).can_fill(self.multiworld.state, item, False), "The location \"" + location + "\" can be filled with \"" + item_name + "\"") diff --git a/worlds/bumpstik/test/__init__.py b/worlds/bumpstik/test/__init__.py index 1199d7b8e506..5f40426e5b10 100644 --- a/worlds/bumpstik/test/__init__.py +++ b/worlds/bumpstik/test/__init__.py @@ -1,4 +1,4 @@ -from test.TestBase import WorldTestBase +from test.bases import WorldTestBase class BumpStikTestBase(WorldTestBase): diff --git a/worlds/checksfinder/__init__.py b/worlds/checksfinder/__init__.py index 9ba57b059185..0b9b7105bfc4 100644 --- a/worlds/checksfinder/__init__.py +++ b/worlds/checksfinder/__init__.py @@ -11,19 +11,18 @@ class ChecksFinderWeb(WebWorld): tutorials = [Tutorial( "Multiworld Setup Guide", - "A guide to setting up the Archipelago ChecksFinder software on your computer. This guide covers " - "single-player, multiworld, and related software.", + "A guide to playing Archipelago ChecksFinder.", "English", "setup_en.md", "setup/en", - ["Mewlif"] + ["SunCat"] )] class ChecksFinderWorld(World): """ - ChecksFinder is a game where you avoid mines and find checks inside the board - with the mines! You win when you get all your items and beat the board! + ChecksFinder is a game where you avoid mines and collect checks by beating boards! + You win when you get all your items and beat the last board! """ game = "ChecksFinder" options_dataclass = PerGameCommonOptions diff --git a/worlds/cv64/__init__.py b/worlds/cv64/__init__.py index 0d384acc8f3d..1bd069a2cea8 100644 --- a/worlds/cv64/__init__.py +++ b/worlds/cv64/__init__.py @@ -89,7 +89,7 @@ class CV64World(World): def generate_early(self) -> None: # Generate the player's unique authentication - self.auth = bytearray(self.multiworld.random.getrandbits(8) for _ in range(16)) + self.auth = bytearray(self.random.getrandbits(8) for _ in range(16)) self.total_s1s = self.options.total_special1s.value self.s1s_per_warp = self.options.special1s_per_warp.value diff --git a/worlds/cv64/client.py b/worlds/cv64/client.py index 2430cc5ffc67..cec5f551b9e5 100644 --- a/worlds/cv64/client.py +++ b/worlds/cv64/client.py @@ -66,8 +66,9 @@ def on_package(self, ctx: "BizHawkClientContext", cmd: str, args: dict) -> None: self.received_deathlinks += 1 if "cause" in args["data"]: cause = args["data"]["cause"] - if len(cause) > 88: - cause = cause[0x00:0x89] + # Truncate the death cause message at 120 characters. + if len(cause) > 120: + cause = cause[0:120] else: cause = f"{args['data']['source']} killed you!" self.death_causes.append(cause) @@ -146,8 +147,18 @@ async def game_watcher(self, ctx: "BizHawkClientContext") -> None: text_color = bytearray([0xA2, 0x0B]) else: text_color = bytearray([0xA2, 0x02]) + + # Get the item's player's name. If it's longer than 40 characters, truncate it at 40. + # 35 should be the max number of characters in a server player name right now (16 for the original + # name + 16 for the alias + 3 for the added parenthesis and space), but if it ever goes higher it + # should be future-proofed now. No need to truncate CV64 items names because its longest item name + # gets nowhere near the limit. + player_name = ctx.player_names[next_item.player] + if len(player_name) > 40: + player_name = player_name[0:40] + received_text, num_lines = cv64_text_wrap(f"{ctx.item_names.lookup_in_game(next_item.item)}\n" - f"from {ctx.player_names[next_item.player]}", 96) + f"from {player_name}", 96) await bizhawk.guarded_write(ctx.bizhawk_ctx, [(0x389BE1, [next_item.item & 0xFF], "RDRAM"), (0x18C0A8, text_color + cv64_string_to_bytearray(received_text, False), diff --git a/worlds/cv64/data/patches.py b/worlds/cv64/data/patches.py index 938b615b3213..6ef4eafb67d3 100644 --- a/worlds/cv64/data/patches.py +++ b/worlds/cv64/data/patches.py @@ -197,6 +197,23 @@ 0xA168FFFD, # SB T0, 0xFFFD (T3) ] +deathlink_nitro_state_checker = [ + # Checks to see if the player is in an alright state before exploding them. If not, then the Nitro explosion spawn + # code will be aborted, and they should eventually explode after getting out of that state. + # + # Invalid states so far include: interacting/going through a door, being grabbed by a vampire. + 0x90880009, # LBU T0, 0x0009 (A0) + 0x24090005, # ADDIU T1, R0, 0x0005 + 0x11090005, # BEQ T0, T1, [forward 0x05] + 0x24090002, # ADDIU T1, R0, 0x0002 + 0x11090003, # BEQ T0, T1, [forward 0x03] + 0x00000000, # NOP + 0x08000660, # J 0x80001980 + 0x00000000, # NOP + 0x03E00008, # JR RA + 0xAC400048 # SW R0, 0x0048 (V0) +] + launch_fall_killer = [ # Custom code to force the instant fall death if at a high enough falling speed after getting killed by something # that launches you (whether it be the Nitro explosion or a Big Toss hit). The game doesn't normally run the check diff --git a/worlds/cv64/rom.py b/worlds/cv64/rom.py index ab4371b0ac12..db621c7101d6 100644 --- a/worlds/cv64/rom.py +++ b/worlds/cv64/rom.py @@ -357,8 +357,12 @@ def apply_patches(caller: APProcedurePatch, rom: bytes, options_file: str) -> by # Make received DeathLinks blow you to smithereens instead of kill you normally. if options["death_link"] == DeathLink.option_explosive: - rom_data.write_int32(0x27A70, 0x10000008) # B [forward 0x08] rom_data.write_int32s(0xBFC0D0, patches.deathlink_nitro_edition) + rom_data.write_int32(0x27A70, 0x10000008) # B [forward 0x08] + rom_data.write_int32(0x27AA0, 0x0C0FFA78) # JAL 0x803FE9E0 + rom_data.write_int32s(0xBFE9E0, patches.deathlink_nitro_state_checker) + # NOP the function call to subtract Nitro from the inventory after exploding, just in case. + rom_data.write_int32(0x32DBC, 0x00000000) # Set the DeathLink ROM flag if it's on at all. if options["death_link"] != DeathLink.option_off: @@ -944,13 +948,19 @@ def write_patch(world: "CV64World", patch: CV64ProcedurePatch, offset_data: Dict for loc in active_locations: if loc.address is None or get_location_info(loc.name, "type") == "shop" or loc.item.player == world.player: continue - if len(loc.item.name) > 67: - item_name = loc.item.name[0x00:0x68] + # If the Item's name is longer than 104 characters, truncate the name to inject at 104. + if len(loc.item.name) > 104: + item_name = loc.item.name[0:104] else: item_name = loc.item.name + # Get the item's player's name. If it's longer than 16 characters (which can happen if it's an ItemLinked item), + # truncate it at 16. + player_name = world.multiworld.get_player_name(loc.item.player) + if len(player_name) > 16: + player_name = player_name[0:16] + inject_address = 0xBB7164 + (256 * (loc.address & 0xFFF)) - wrapped_name, num_lines = cv64_text_wrap(item_name + "\nfor " + - world.multiworld.get_player_name(loc.item.player), 96) + wrapped_name, num_lines = cv64_text_wrap(item_name + "\nfor " + player_name, 96) patch.write_token(APTokenTypes.WRITE, inject_address, bytes(get_item_text_color(loc) + cv64_string_to_bytearray(wrapped_name))) patch.write_token(APTokenTypes.WRITE, inject_address + 255, bytes([num_lines])) diff --git a/worlds/dark_souls_3/__init__.py b/worlds/dark_souls_3/__init__.py index b51668539be2..765ffb1fc544 100644 --- a/worlds/dark_souls_3/__init__.py +++ b/worlds/dark_souls_3/__init__.py @@ -89,6 +89,7 @@ def __init__(self, multiworld: MultiWorld, player: int): self.all_excluded_locations = set() def generate_early(self) -> None: + self.created_regions = set() self.all_excluded_locations.update(self.options.exclude_locations.value) # Inform Universal Tracker where Yhorm is being randomized to. @@ -294,6 +295,7 @@ def create_region(self, region_name, location_table) -> Region: new_region.locations.append(new_location) self.multiworld.regions.append(new_region) + self.created_regions.add(region_name) return new_region def create_items(self) -> None: @@ -1305,7 +1307,7 @@ def _add_location_rule(self, location: Union[str, List[str]], rule: Union[Collec def _add_entrance_rule(self, region: str, rule: Union[CollectionRule, str]) -> None: """Sets a rule for the entrance to the given region.""" assert region in location_tables - if not any(region == reg for reg in self.multiworld.regions.region_cache[self.player]): return + if region not in self.created_regions: return if isinstance(rule, str): if " -> " not in rule: assert item_dictionary[rule].classification == ItemClassification.progression @@ -1566,6 +1568,16 @@ def fill_slot_data(self) -> Dict[str, object]: "apIdsToItemIds": ap_ids_to_ds3_ids, "itemCounts": item_counts, "locationIdsToKeys": location_ids_to_keys, + # The range of versions of the static randomizer that are compatible + # with this slot data. Incompatible versions should have at least a + # minor version bump. Pre-release versions should generally only be + # compatible with a single version, except very close to a stable + # release when no changes are expected. + # + # This is checked by the static randomizer, which will surface an + # error to the user if its version doesn't fall into the allowed + # range. + "versions": ">=3.0.0-beta.24 <3.1.0", } return slot_data diff --git a/worlds/dark_souls_3/docs/setup_en.md b/worlds/dark_souls_3/docs/setup_en.md index 9755cce1c6a8..484afdce3fcb 100644 --- a/worlds/dark_souls_3/docs/setup_en.md +++ b/worlds/dark_souls_3/docs/setup_en.md @@ -18,7 +18,8 @@ installation folder. Version 3.0.0 of the randomizer _only_ supports the latest version of _Dark Souls III_, 1.15.2. This is the latest version, so you don't need to do any downpatching! However, if you've already downpatched your game to use an older version of the randomizer, you'll need to reinstall the latest -version before using this version. +version before using this version. You should also delete the `dinput8.dll` file if you still have +one from an older randomizer version. ### One-Time Setup diff --git a/worlds/dark_souls_3/test/TestDarkSouls3.py b/worlds/dark_souls_3/test/TestDarkSouls3.py index e590cd732b41..7acdad465da7 100644 --- a/worlds/dark_souls_3/test/TestDarkSouls3.py +++ b/worlds/dark_souls_3/test/TestDarkSouls3.py @@ -1,4 +1,4 @@ -from test.TestBase import WorldTestBase +from test.bases import WorldTestBase from worlds.dark_souls_3.Items import item_dictionary from worlds.dark_souls_3.Locations import location_tables diff --git a/worlds/dlcquest/test/TestOptionsLong.py b/worlds/dlcquest/test/TestOptionsLong.py index 3e9acac7e791..c6c594b6a004 100644 --- a/worlds/dlcquest/test/TestOptionsLong.py +++ b/worlds/dlcquest/test/TestOptionsLong.py @@ -5,7 +5,6 @@ from .option_names import options_to_include from .checks.world_checks import assert_can_win, assert_same_number_items_locations from . import DLCQuestTestBase, setup_dlc_quest_solo_multiworld -from ... import AutoWorldRegister def basic_checks(tester: DLCQuestTestBase, multiworld: MultiWorld): diff --git a/worlds/dlcquest/test/__init__.py b/worlds/dlcquest/test/__init__.py index 8a39b43a2cfd..0432ae8b60ba 100644 --- a/worlds/dlcquest/test/__init__.py +++ b/worlds/dlcquest/test/__init__.py @@ -4,7 +4,7 @@ from argparse import Namespace from BaseClasses import MultiWorld -from test.TestBase import WorldTestBase +from test.bases import WorldTestBase from .. import DLCqworld from test.general import gen_steps, setup_solo_multiworld as setup_base_solo_multiworld from worlds.AutoWorld import call_all diff --git a/worlds/dlcquest/test/checks/world_checks.py b/worlds/dlcquest/test/checks/world_checks.py index cc2fa7f51ad2..48c919c0f62b 100644 --- a/worlds/dlcquest/test/checks/world_checks.py +++ b/worlds/dlcquest/test/checks/world_checks.py @@ -1,6 +1,6 @@ from typing import List -from BaseClasses import MultiWorld, ItemClassification +from BaseClasses import MultiWorld from .. import DLCQuestTestBase from ... import Options @@ -14,7 +14,7 @@ def get_all_location_names(multiworld: MultiWorld) -> List[str]: def assert_victory_exists(tester: DLCQuestTestBase, multiworld: MultiWorld): - campaign = multiworld.campaign[1] + campaign = multiworld.worlds[1].options.campaign all_items = [item.name for item in multiworld.get_items()] if campaign == Options.Campaign.option_basic or campaign == Options.Campaign.option_both: tester.assertIn("Victory Basic", all_items) @@ -25,7 +25,7 @@ def assert_victory_exists(tester: DLCQuestTestBase, multiworld: MultiWorld): def collect_all_then_assert_can_win(tester: DLCQuestTestBase, multiworld: MultiWorld): for item in multiworld.get_items(): multiworld.state.collect(item) - campaign = multiworld.campaign[1] + campaign = multiworld.worlds[1].options.campaign if campaign == Options.Campaign.option_basic or campaign == Options.Campaign.option_both: tester.assertTrue(multiworld.find_item("Victory Basic", 1).can_reach(multiworld.state)) if campaign == Options.Campaign.option_live_freemium_or_die or campaign == Options.Campaign.option_both: @@ -39,4 +39,4 @@ def assert_can_win(tester: DLCQuestTestBase, multiworld: MultiWorld): def assert_same_number_items_locations(tester: DLCQuestTestBase, multiworld: MultiWorld): non_event_locations = [location for location in multiworld.get_locations() if not location.advancement] - tester.assertEqual(len(multiworld.itempool), len(non_event_locations)) \ No newline at end of file + tester.assertEqual(len(multiworld.itempool), len(non_event_locations)) diff --git a/worlds/factorio/Client.py b/worlds/factorio/Client.py index 23dfa0633eb4..3c35c4cb0986 100644 --- a/worlds/factorio/Client.py +++ b/worlds/factorio/Client.py @@ -304,13 +304,13 @@ def queuer(): async def factorio_server_watcher(ctx: FactorioContext): - savegame_name = os.path.abspath(ctx.savegame_name) + savegame_name = os.path.abspath(os.path.join(ctx.write_data_path, "saves", "Archipelago", ctx.savegame_name)) if not os.path.exists(savegame_name): logger.info(f"Creating savegame {savegame_name}") subprocess.run(( executable, "--create", savegame_name, "--preset", "archipelago" )) - factorio_process = subprocess.Popen((executable, "--start-server", ctx.savegame_name, + factorio_process = subprocess.Popen((executable, "--start-server", savegame_name, *(str(elem) for elem in server_args)), stderr=subprocess.PIPE, stdout=subprocess.PIPE, @@ -331,7 +331,8 @@ async def factorio_server_watcher(ctx: FactorioContext): factorio_queue.task_done() if not ctx.rcon_client and "Starting RCON interface at IP ADDR:" in msg: - ctx.rcon_client = factorio_rcon.RCONClient("localhost", rcon_port, rcon_password) + ctx.rcon_client = factorio_rcon.RCONClient("localhost", rcon_port, rcon_password, + timeout=5) if not ctx.server: logger.info("Established bridge to Factorio Server. " "Ready to connect to Archipelago via /connect") @@ -405,8 +406,7 @@ async def get_info(ctx: FactorioContext, rcon_client: factorio_rcon.RCONClient): info = json.loads(rcon_client.send_command("/ap-rcon-info")) ctx.auth = info["slot_name"] ctx.seed_name = info["seed_name"] - # 0.2.0 addition, not present earlier - death_link = bool(info.get("death_link", False)) + death_link = info["death_link"] ctx.energy_link_increment = info.get("energy_link", 0) logger.debug(f"Energy Link Increment: {ctx.energy_link_increment}") if ctx.energy_link_increment and ctx.ui: diff --git a/worlds/factorio/Mod.py b/worlds/factorio/Mod.py index 7eec71875829..7dee04afbee3 100644 --- a/worlds/factorio/Mod.py +++ b/worlds/factorio/Mod.py @@ -35,9 +35,11 @@ "author": "Berserker", "homepage": "https://archipelago.gg", "description": "Integration client for the Archipelago Randomizer", - "factorio_version": "1.1", + "factorio_version": "2.0", "dependencies": [ - "base >= 1.1.0", + "base >= 2.0.15", + "? quality >= 2.0.15", + "! space-age", "? science-not-invited", "? factory-levels" ] @@ -133,21 +135,21 @@ def flop_random(low, high, base=None): "allowed_science_packs": world.options.max_science_pack.get_allowed_packs(), "custom_technologies": world.custom_technologies, "tech_tree_layout_prerequisites": world.tech_tree_layout_prerequisites, - "slot_name": world.player_name, "seed_name": multiworld.seed_name, + "slot_name": world.player_name, + "seed_name": multiworld.seed_name, "slot_player": player, - "starting_items": world.options.starting_items, "recipes": recipes, - "random": random, "flop_random": flop_random, + "recipes": recipes, + "random": random, + "flop_random": flop_random, "recipe_time_scale": recipe_time_scales.get(world.options.recipe_time.value, None), "recipe_time_range": recipe_time_ranges.get(world.options.recipe_time.value, None), "free_sample_blacklist": {item: 1 for item in free_sample_exclusions}, + "free_sample_quality_name": world.options.free_samples_quality.current_key, "progressive_technology_table": {tech.name: tech.progressive for tech in progressive_technology_table.values()}, "custom_recipes": world.custom_recipes, - "max_science_pack": world.options.max_science_pack.value, "liquids": fluids, - "goal": world.options.goal.value, - "energy_link": world.options.energy_link.value, - "useless_technologies": useless_technologies, + "removed_technologies": world.removed_technologies, "chunk_shuffle": 0, } diff --git a/worlds/factorio/Options.py b/worlds/factorio/Options.py index 788d1f9e1d92..5a41250fa760 100644 --- a/worlds/factorio/Options.py +++ b/worlds/factorio/Options.py @@ -1,12 +1,11 @@ from __future__ import annotations from dataclasses import dataclass -import datetime import typing from schema import Schema, Optional, And, Or -from Options import Choice, OptionDict, OptionSet, Option, DefaultOnToggle, Range, DeathLink, Toggle, \ +from Options import Choice, OptionDict, OptionSet, DefaultOnToggle, Range, DeathLink, Toggle, \ StartInventoryPool, PerGameCommonOptions # schema helpers @@ -122,6 +121,18 @@ class FreeSamples(Choice): default = 3 +class FreeSamplesQuality(Choice): + """If free samples are on, determine the quality of the granted items. + Requires the quality mod, which is part of the Space Age DLC. Without it, normal quality is given.""" + display_name = "Free Samples Quality" + option_normal = 0 + option_uncommon = 1 + option_rare = 2 + option_epic = 3 + option_legendary = 4 + default = 0 + + class TechTreeLayout(Choice): """Selects how the tech tree nodes are interwoven. Single: No dependencies @@ -284,17 +295,21 @@ class FactorioWorldGen(OptionDict): # FIXME: do we want default be a rando-optimized default or in-game DS? value: typing.Dict[str, typing.Dict[str, typing.Any]] default = { - "terrain_segmentation": 0.5, - "water": 1.5, "autoplace_controls": { + # terrain + "water": {"frequency": 1, "size": 1, "richness": 1}, + "nauvis_cliff": {"frequency": 1, "size": 1, "richness": 1}, + "starting_area_moisture": {"frequency": 1, "size": 1, "richness": 1}, + # resources "coal": {"frequency": 1, "size": 3, "richness": 6}, "copper-ore": {"frequency": 1, "size": 3, "richness": 6}, "crude-oil": {"frequency": 1, "size": 3, "richness": 6}, - "enemy-base": {"frequency": 1, "size": 1, "richness": 1}, "iron-ore": {"frequency": 1, "size": 3, "richness": 6}, "stone": {"frequency": 1, "size": 3, "richness": 6}, + "uranium-ore": {"frequency": 1, "size": 3, "richness": 6}, + # misc "trees": {"frequency": 1, "size": 1, "richness": 1}, - "uranium-ore": {"frequency": 1, "size": 3, "richness": 6} + "enemy-base": {"frequency": 1, "size": 1, "richness": 1}, }, "seed": None, "starting_area": 1, @@ -336,8 +351,6 @@ class FactorioWorldGen(OptionDict): } schema = Schema({ "basic": { - Optional("terrain_segmentation"): FloatRange(0.166, 6), - Optional("water"): FloatRange(0.166, 6), Optional("autoplace_controls"): { str: { "frequency": FloatRange(0, 6), @@ -438,6 +451,7 @@ class FactorioOptions(PerGameCommonOptions): silo: Silo satellite: Satellite free_samples: FreeSamples + free_samples_quality: FreeSamplesQuality tech_tree_information: TechTreeInformation starting_items: FactorioStartItems free_sample_blacklist: FactorioFreeSampleBlacklist diff --git a/worlds/factorio/Technologies.py b/worlds/factorio/Technologies.py index 112cc49f0920..6111462e8ca9 100644 --- a/worlds/factorio/Technologies.py +++ b/worlds/factorio/Technologies.py @@ -1,13 +1,13 @@ from __future__ import annotations -import orjson -import logging -import os -import string +import functools import pkgutil +import string from collections import Counter from concurrent.futures import ThreadPoolExecutor -from typing import Dict, Set, FrozenSet, Tuple, Union, List, Any +from typing import Dict, Set, FrozenSet, Tuple, Union, List, Any, Optional + +import orjson import Utils from . import Options @@ -32,8 +32,23 @@ def load_json_data(data_name: str) -> Union[List[str], Dict[str, Any]]: tech_table: Dict[str, int] = {} technology_table: Dict[str, Technology] = {} +start_unlocked_recipes = { + "offshore-pump", + "boiler", + "steam-engine", + "automation-science-pack", + "inserter", + "small-electric-pole", + "copper-cable", + "lab", + "electronic-circuit", + "electric-mining-drill", + "pipe", + "pipe-to-ground", +} + -def always(state): +def always(state) -> bool: return True @@ -50,15 +65,13 @@ def __hash__(self): class Technology(FactorioElement): # maybe make subclass of Location? has_modifier: bool factorio_id: int - ingredients: Set[str] progressive: Tuple[str] unlocks: Union[Set[str], bool] # bool case is for progressive technologies - def __init__(self, name: str, ingredients: Set[str], factorio_id: int, progressive: Tuple[str] = (), + def __init__(self, technology_name: str, factorio_id: int, progressive: Tuple[str] = (), has_modifier: bool = False, unlocks: Union[Set[str], bool] = None): - self.name = name + self.name = technology_name self.factorio_id = factorio_id - self.ingredients = ingredients self.progressive = progressive self.has_modifier = has_modifier if unlocks: @@ -66,19 +79,6 @@ def __init__(self, name: str, ingredients: Set[str], factorio_id: int, progressi else: self.unlocks = set() - def build_rule(self, player: int): - logging.debug(f"Building rules for {self.name}") - - return lambda state: all(state.has(f"Automated {ingredient}", player) - for ingredient in self.ingredients) - - def get_prior_technologies(self) -> Set[Technology]: - """Get Technologies that have to precede this one to resolve tree connections.""" - technologies = set() - for ingredient in self.ingredients: - technologies |= required_technologies[ingredient] # technologies that unlock the recipes - return technologies - def __hash__(self): return self.factorio_id @@ -91,22 +91,22 @@ def useful(self) -> bool: class CustomTechnology(Technology): """A particularly configured Technology for a world.""" + ingredients: Set[str] def __init__(self, origin: Technology, world, allowed_packs: Set[str], player: int): - ingredients = origin.ingredients & allowed_packs - military_allowed = "military-science-pack" in allowed_packs \ - and ((ingredients & {"chemical-science-pack", "production-science-pack", "utility-science-pack"}) - or origin.name == "rocket-silo") + ingredients = allowed_packs self.player = player if origin.name not in world.special_nodes: - if military_allowed: - ingredients.add("military-science-pack") - ingredients = list(ingredients) - ingredients.sort() # deterministic sample - ingredients = world.random.sample(ingredients, world.random.randint(1, len(ingredients))) - elif origin.name == "rocket-silo" and military_allowed: - ingredients.add("military-science-pack") - super(CustomTechnology, self).__init__(origin.name, ingredients, origin.factorio_id) + ingredients = set(world.random.sample(list(ingredients), world.random.randint(1, len(ingredients)))) + self.ingredients = ingredients + super(CustomTechnology, self).__init__(origin.name, origin.factorio_id) + + def get_prior_technologies(self) -> Set[Technology]: + """Get Technologies that have to precede this one to resolve tree connections.""" + technologies = set() + for ingredient in self.ingredients: + technologies |= required_technologies[ingredient] # technologies that unlock the recipes + return technologies class Recipe(FactorioElement): @@ -149,19 +149,22 @@ def rel_cost(self) -> float: ingredients = sum(self.ingredients.values()) return min(ingredients / amount for product, amount in self.products.items()) - @property + @functools.cached_property def base_cost(self) -> Dict[str, int]: ingredients = Counter() - for ingredient, cost in self.ingredients.items(): - if ingredient in all_product_sources: - for recipe in all_product_sources[ingredient]: - if recipe.ingredients: - ingredients.update({name: amount * cost / recipe.products[ingredient] for name, amount in - recipe.base_cost.items()}) - else: - ingredients[ingredient] += recipe.energy * cost / recipe.products[ingredient] - else: - ingredients[ingredient] += cost + try: + for ingredient, cost in self.ingredients.items(): + if ingredient in all_product_sources: + for recipe in all_product_sources[ingredient]: + if recipe.ingredients: + ingredients.update({name: amount * cost / recipe.products[ingredient] for name, amount in + recipe.base_cost.items()}) + else: + ingredients[ingredient] += recipe.energy * cost / recipe.products[ingredient] + else: + ingredients[ingredient] += cost + except RecursionError as e: + raise Exception(f"Infinite recursion in ingredients of {self}.") from e return ingredients @property @@ -191,9 +194,12 @@ def __init__(self, name, categories): # recipes and technologies can share names in Factorio for technology_name, data in sorted(techs_future.result().items()): - current_ingredients = set(data["ingredients"]) - technology = Technology(technology_name, current_ingredients, factorio_tech_id, - has_modifier=data["has_modifier"], unlocks=set(data["unlocks"])) + technology = Technology( + technology_name, + factorio_tech_id, + has_modifier=data["has_modifier"], + unlocks=set(data["unlocks"]) - start_unlocked_recipes, + ) factorio_tech_id += 1 tech_table[technology_name] = technology.factorio_id technology_table[technology_name] = technology @@ -226,11 +232,12 @@ def __init__(self, name, categories): recipes[recipe_name] = recipe if set(recipe.products).isdisjoint( # prevents loop recipes like uranium centrifuging - set(recipe.ingredients)) and ("empty-barrel" not in recipe.products or recipe.name == "empty-barrel") and \ + set(recipe.ingredients)) and ("barrel" not in recipe.products or recipe.name == "barrel") and \ not recipe_name.endswith("-reprocessing"): for product_name in recipe.products: all_product_sources.setdefault(product_name, set()).add(recipe) +assert all(recipe_name in raw_recipes for recipe_name in start_unlocked_recipes), "Unknown Recipe defined." machines: Dict[str, Machine] = {} @@ -248,9 +255,7 @@ def __init__(self, name, categories): # build requirements graph for all technology ingredients -all_ingredient_names: Set[str] = set() -for technology in technology_table.values(): - all_ingredient_names |= technology.ingredients +all_ingredient_names: Set[str] = set(Options.MaxSciencePack.get_ordered_science_packs()) def unlock_just_tech(recipe: Recipe, _done) -> Set[Technology]: @@ -319,13 +324,17 @@ def recursively_get_unlocking_technologies(ingredient_name, _done=None, unlock_f recursively_get_unlocking_technologies(ingredient_name, unlock_func=unlock))) -def get_rocket_requirements(silo_recipe: Recipe, part_recipe: Recipe, satellite_recipe: Recipe) -> Set[str]: +def get_rocket_requirements(silo_recipe: Optional[Recipe], part_recipe: Recipe, + satellite_recipe: Optional[Recipe], cargo_landing_pad_recipe: Optional[Recipe]) -> Set[str]: techs = set() if silo_recipe: for ingredient in silo_recipe.ingredients: techs |= recursively_get_unlocking_technologies(ingredient) for ingredient in part_recipe.ingredients: techs |= recursively_get_unlocking_technologies(ingredient) + if cargo_landing_pad_recipe: + for ingredient in cargo_landing_pad_recipe.ingredients: + techs |= recursively_get_unlocking_technologies(ingredient) if satellite_recipe: techs |= satellite_recipe.unlocking_technologies for ingredient in satellite_recipe.ingredients: @@ -382,15 +391,15 @@ def get_rocket_requirements(silo_recipe: Recipe, part_recipe: Recipe, satellite_ "uranium-processing", "kovarex-enrichment-process", "nuclear-fuel-reprocessing") progressive_rows["progressive-rocketry"] = ("rocketry", "explosive-rocketry", "atomic-bomb") progressive_rows["progressive-vehicle"] = ("automobilism", "tank", "spidertron") -progressive_rows["progressive-train-network"] = ("railway", "fluid-wagon", - "automated-rail-transportation", "rail-signals") +progressive_rows["progressive-fluid-handling"] = ("fluid-handling", "fluid-wagon") +progressive_rows["progressive-train-network"] = ("railway", "automated-rail-transportation") progressive_rows["progressive-engine"] = ("engine", "electric-engine") progressive_rows["progressive-armor"] = ("heavy-armor", "modular-armor", "power-armor", "power-armor-mk2") progressive_rows["progressive-personal-battery"] = ("battery-equipment", "battery-mk2-equipment") progressive_rows["progressive-energy-shield"] = ("energy-shield-equipment", "energy-shield-mk2-equipment") progressive_rows["progressive-wall"] = ("stone-wall", "gate") progressive_rows["progressive-follower"] = ("defender", "distractor", "destroyer") -progressive_rows["progressive-inserter"] = ("fast-inserter", "stack-inserter") +progressive_rows["progressive-inserter"] = ("fast-inserter", "bulk-inserter") progressive_rows["progressive-turret"] = ("gun-turret", "laser-turret") progressive_rows["progressive-flamethrower"] = ("flamethrower",) # leaving out flammables, as they do nothing progressive_rows["progressive-personal-roboport-equipment"] = ("personal-roboport-equipment", @@ -402,7 +411,7 @@ def get_rocket_requirements(silo_recipe: Recipe, part_recipe: Recipe, satellite_ source_target_mapping: Dict[str, str] = { "progressive-braking-force": "progressive-train-network", "progressive-inserter-capacity-bonus": "progressive-inserter", - "progressive-refined-flammables": "progressive-flamethrower" + "progressive-refined-flammables": "progressive-flamethrower", } for source, target in source_target_mapping.items(): @@ -416,12 +425,14 @@ def get_rocket_requirements(silo_recipe: Recipe, part_recipe: Recipe, satellite_ for root in sorted_rows: progressive = progressive_rows[root] - assert all(tech in tech_table for tech in progressive), "declared a progressive technology without base technology" + assert all(tech in tech_table for tech in progressive), \ + (f"Declared a progressive technology ({root}) without base technology. " + f"Missing: f{tuple(tech for tech in progressive if tech not in tech_table)}") factorio_tech_id += 1 - progressive_technology = Technology(root, technology_table[progressive[0]].ingredients, factorio_tech_id, - progressive, + progressive_technology = Technology(root, factorio_tech_id, + tuple(progressive), has_modifier=any(technology_table[tech].has_modifier for tech in progressive), - unlocks=any(technology_table[tech].unlocks for tech in progressive)) + unlocks=any(technology_table[tech].unlocks for tech in progressive),) progressive_tech_table[root] = progressive_technology.factorio_id progressive_technology_table[root] = progressive_technology diff --git a/worlds/factorio/__init__.py b/worlds/factorio/__init__.py index 925327655a24..9f1f3cb573f9 100644 --- a/worlds/factorio/__init__.py +++ b/worlds/factorio/__init__.py @@ -2,10 +2,11 @@ import collections import logging -import settings import typing -from BaseClasses import Region, Entrance, Location, Item, Tutorial, ItemClassification +import Utils +import settings +from BaseClasses import Region, Location, Item, Tutorial, ItemClassification from worlds.AutoWorld import World, WebWorld from worlds.LauncherComponents import Component, components, Type, launch_subprocess from worlds.generic import Rules @@ -14,7 +15,7 @@ from .Options import FactorioOptions, MaxSciencePack, Silo, Satellite, TechTreeInformation, Goal, TechCostDistribution from .Shapes import get_shapes from .Technologies import base_tech_table, recipe_sources, base_technology_table, \ - all_ingredient_names, all_product_sources, required_technologies, get_rocket_requirements, \ + all_product_sources, required_technologies, get_rocket_requirements, \ progressive_technology_table, common_tech_table, tech_to_progressive_lookup, progressive_tech_table, \ get_science_pack_pools, Recipe, recipes, technology_table, tech_table, factorio_base_id, useless_technologies, \ fluids, stacking_items, valid_ingredients, progressive_rows @@ -97,19 +98,21 @@ class Factorio(World): item_name_groups = { "Progressive": set(progressive_tech_table.keys()), } - required_client_version = (0, 5, 0) - + required_client_version = (0, 5, 1) + if Utils.version_tuple < required_client_version: + raise Exception(f"Update Archipelago to use this world ({game}).") ordered_science_packs: typing.List[str] = MaxSciencePack.get_ordered_science_packs() tech_tree_layout_prerequisites: typing.Dict[FactorioScienceLocation, typing.Set[FactorioScienceLocation]] tech_mix: int = 0 skip_silo: bool = False origin_region_name = "Nauvis" science_locations: typing.List[FactorioScienceLocation] - + removed_technologies: typing.Set[str] settings: typing.ClassVar[FactorioSettings] def __init__(self, world, player: int): super(Factorio, self).__init__(world, player) + self.removed_technologies = useless_technologies.copy() self.advancement_technologies = set() self.custom_recipes = {} self.science_locations = [] @@ -208,11 +211,9 @@ def create_items(self) -> None: for loc in self.science_locations: loc.revealed = True if self.skip_silo: - removed = useless_technologies | {"rocket-silo"} - else: - removed = useless_technologies + self.removed_technologies |= {"rocket-silo"} for tech_name in base_tech_table: - if tech_name not in removed: + if tech_name not in self.removed_technologies: progressive_item_name = tech_to_progressive_lookup.get(tech_name, tech_name) want_progressive = want_progressives[progressive_item_name] item_name = progressive_item_name if want_progressive else tech_name @@ -240,40 +241,49 @@ def set_rules(self): custom_recipe = self.custom_recipes[ingredient] location.access_rule = lambda state, ingredient=ingredient, custom_recipe=custom_recipe: \ - (ingredient not in technology_table or state.has(ingredient, player)) and \ + (not technology_table[ingredient].unlocks or state.has(ingredient, player)) and \ all(state.has(technology.name, player) for sub_ingredient in custom_recipe.ingredients for technology in required_technologies[sub_ingredient]) and \ all(state.has(technology.name, player) for technology in required_technologies[custom_recipe.crafting_machine]) + else: location.access_rule = lambda state, ingredient=ingredient: \ all(state.has(technology.name, player) for technology in required_technologies[ingredient]) for location in self.science_locations: - Rules.set_rule(location, lambda state, ingredients=location.ingredients: + Rules.set_rule(location, lambda state, ingredients=frozenset(location.ingredients): all(state.has(f"Automated {ingredient}", player) for ingredient in ingredients)) prerequisites = shapes.get(location) if prerequisites: - Rules.add_rule(location, lambda state, locations= - prerequisites: all(state.can_reach(loc) for loc in locations)) + Rules.add_rule(location, lambda state, locations=frozenset(prerequisites): + all(state.can_reach(loc) for loc in locations)) silo_recipe = None + cargo_pad_recipe = None if self.options.silo == Silo.option_spawn: - silo_recipe = self.custom_recipes["rocket-silo"] if "rocket-silo" in self.custom_recipes \ - else next(iter(all_product_sources.get("rocket-silo"))) + silo_recipe = self.get_recipe("rocket-silo") + cargo_pad_recipe = self.get_recipe("cargo-landing-pad") part_recipe = self.custom_recipes["rocket-part"] satellite_recipe = None if self.options.goal == Goal.option_satellite: - satellite_recipe = self.custom_recipes["satellite"] if "satellite" in self.custom_recipes \ - else next(iter(all_product_sources.get("satellite"))) - victory_tech_names = get_rocket_requirements(silo_recipe, part_recipe, satellite_recipe) - if self.options.silo != Silo.option_spawn: - victory_tech_names.add("rocket-silo") + satellite_recipe = self.get_recipe("satellite") + victory_tech_names = get_rocket_requirements(silo_recipe, part_recipe, satellite_recipe, cargo_pad_recipe) + if self.options.silo == Silo.option_spawn: + victory_tech_names -= {"rocket-silo"} + else: + victory_tech_names |= {"rocket-silo"} self.get_location("Rocket Launch").access_rule = lambda state: all(state.has(technology, player) for technology in victory_tech_names) - + for tech_name in victory_tech_names: + if not self.multiworld.get_all_state(True).has(tech_name, player): + print(tech_name) self.multiworld.completion_condition[player] = lambda state: state.has('Victory', player) + def get_recipe(self, name: str) -> Recipe: + return self.custom_recipes[name] if name in self.custom_recipes \ + else next(iter(all_product_sources.get(name))) + def generate_basic(self): map_basic_settings = self.options.world_gen.value["basic"] if map_basic_settings.get("seed", None) is None: # allow seed 0 @@ -321,9 +331,11 @@ def get_category(category: str, liquids: int) -> str: def make_quick_recipe(self, original: Recipe, pool: list, allow_liquids: int = 2, ingredients_offset: int = 0) -> Recipe: + count: int = len(original.ingredients) + ingredients_offset + assert len(pool) >= count, f"Can't pick {count} many items from pool {pool}." new_ingredients = {} liquids_used = 0 - for _ in range(len(original.ingredients) + ingredients_offset): + for _ in range(count): new_ingredient = pool.pop() if new_ingredient in fluids: while liquids_used == allow_liquids and new_ingredient in fluids: @@ -440,7 +452,8 @@ def set_custom_recipes(self): ingredients_offset = self.options.recipe_ingredients_offset original_rocket_part = recipes["rocket-part"] science_pack_pools = get_science_pack_pools() - valid_pool = sorted(science_pack_pools[self.options.max_science_pack.get_max_pack()] & valid_ingredients) + valid_pool = sorted(science_pack_pools[self.options.max_science_pack.get_max_pack()] + & valid_ingredients) self.random.shuffle(valid_pool) self.custom_recipes = {"rocket-part": Recipe("rocket-part", original_rocket_part.category, {valid_pool[x]: 10 for x in range(3 + ingredients_offset)}, @@ -489,7 +502,7 @@ def set_custom_recipes(self): needed_recipes = self.options.max_science_pack.get_allowed_packs() | {"rocket-part"} if self.options.silo != Silo.option_spawn: - needed_recipes |= {"rocket-silo"} + needed_recipes |= {"rocket-silo", "cargo-landing-pad"} if self.options.goal.value == Goal.option_satellite: needed_recipes |= {"satellite"} diff --git a/worlds/factorio/data/fluids.json b/worlds/factorio/data/fluids.json index 448ccf4e4921..6972690f5355 100644 --- a/worlds/factorio/data/fluids.json +++ b/worlds/factorio/data/fluids.json @@ -1 +1 @@ -["fluid-unknown","water","crude-oil","steam","heavy-oil","light-oil","petroleum-gas","sulfuric-acid","lubricant"] \ No newline at end of file +["water","steam","crude-oil","petroleum-gas","light-oil","heavy-oil","lubricant","sulfuric-acid","parameter-0","parameter-1","parameter-2","parameter-3","parameter-4","parameter-5","parameter-6","parameter-7","parameter-8","parameter-9","fluid-unknown"] \ No newline at end of file diff --git a/worlds/factorio/data/items.json b/worlds/factorio/data/items.json index fa34430f40c4..d9ec7befba90 100644 --- a/worlds/factorio/data/items.json +++ b/worlds/factorio/data/items.json @@ -1 +1 @@ -{"wooden-chest":50,"iron-chest":50,"steel-chest":50,"storage-tank":50,"transport-belt":100,"fast-transport-belt":100,"express-transport-belt":100,"underground-belt":50,"fast-underground-belt":50,"express-underground-belt":50,"splitter":50,"fast-splitter":50,"express-splitter":50,"loader":50,"fast-loader":50,"express-loader":50,"burner-inserter":50,"inserter":50,"long-handed-inserter":50,"fast-inserter":50,"filter-inserter":50,"stack-inserter":50,"stack-filter-inserter":50,"small-electric-pole":50,"medium-electric-pole":50,"big-electric-pole":50,"substation":50,"pipe":100,"pipe-to-ground":50,"pump":50,"rail":100,"train-stop":10,"rail-signal":50,"rail-chain-signal":50,"locomotive":5,"cargo-wagon":5,"fluid-wagon":5,"artillery-wagon":5,"car":1,"tank":1,"spidertron":1,"spidertron-remote":1,"logistic-robot":50,"construction-robot":50,"logistic-chest-active-provider":50,"logistic-chest-passive-provider":50,"logistic-chest-storage":50,"logistic-chest-buffer":50,"logistic-chest-requester":50,"roboport":10,"small-lamp":50,"red-wire":200,"green-wire":200,"arithmetic-combinator":50,"decider-combinator":50,"constant-combinator":50,"power-switch":50,"programmable-speaker":50,"stone-brick":100,"concrete":100,"hazard-concrete":100,"refined-concrete":100,"refined-hazard-concrete":100,"landfill":100,"cliff-explosives":20,"dummy-steel-axe":1,"repair-pack":100,"blueprint":1,"deconstruction-planner":1,"upgrade-planner":1,"blueprint-book":1,"copy-paste-tool":1,"cut-paste-tool":1,"boiler":50,"steam-engine":10,"solar-panel":50,"accumulator":50,"nuclear-reactor":10,"heat-pipe":50,"heat-exchanger":50,"steam-turbine":10,"burner-mining-drill":50,"electric-mining-drill":50,"offshore-pump":20,"pumpjack":20,"stone-furnace":50,"steel-furnace":50,"electric-furnace":50,"assembling-machine-1":50,"assembling-machine-2":50,"assembling-machine-3":50,"oil-refinery":10,"chemical-plant":10,"centrifuge":50,"lab":10,"beacon":10,"speed-module":50,"speed-module-2":50,"speed-module-3":50,"effectivity-module":50,"effectivity-module-2":50,"effectivity-module-3":50,"productivity-module":50,"productivity-module-2":50,"productivity-module-3":50,"rocket-silo":1,"satellite":1,"wood":100,"coal":50,"stone":50,"iron-ore":50,"copper-ore":50,"uranium-ore":50,"raw-fish":100,"iron-plate":100,"copper-plate":100,"solid-fuel":50,"steel-plate":100,"plastic-bar":100,"sulfur":50,"battery":200,"explosives":50,"crude-oil-barrel":10,"heavy-oil-barrel":10,"light-oil-barrel":10,"lubricant-barrel":10,"petroleum-gas-barrel":10,"sulfuric-acid-barrel":10,"water-barrel":10,"copper-cable":200,"iron-stick":100,"iron-gear-wheel":100,"empty-barrel":10,"electronic-circuit":200,"advanced-circuit":200,"processing-unit":100,"engine-unit":50,"electric-engine-unit":50,"flying-robot-frame":50,"rocket-control-unit":10,"low-density-structure":10,"rocket-fuel":10,"rocket-part":5,"nuclear-fuel":1,"uranium-235":100,"uranium-238":100,"uranium-fuel-cell":50,"used-up-uranium-fuel-cell":50,"automation-science-pack":200,"logistic-science-pack":200,"military-science-pack":200,"chemical-science-pack":200,"production-science-pack":200,"utility-science-pack":200,"space-science-pack":2000,"coin":100000,"pistol":5,"submachine-gun":5,"tank-machine-gun":1,"vehicle-machine-gun":1,"tank-flamethrower":1,"shotgun":5,"combat-shotgun":5,"rocket-launcher":5,"flamethrower":5,"land-mine":100,"artillery-wagon-cannon":1,"spidertron-rocket-launcher-1":1,"spidertron-rocket-launcher-2":1,"spidertron-rocket-launcher-3":1,"spidertron-rocket-launcher-4":1,"tank-cannon":1,"firearm-magazine":200,"piercing-rounds-magazine":200,"uranium-rounds-magazine":200,"shotgun-shell":200,"piercing-shotgun-shell":200,"cannon-shell":200,"explosive-cannon-shell":200,"uranium-cannon-shell":200,"explosive-uranium-cannon-shell":200,"artillery-shell":1,"rocket":200,"explosive-rocket":200,"atomic-bomb":10,"flamethrower-ammo":100,"grenade":100,"cluster-grenade":100,"poison-capsule":100,"slowdown-capsule":100,"defender-capsule":100,"distractor-capsule":100,"destroyer-capsule":100,"light-armor":1,"heavy-armor":1,"modular-armor":1,"power-armor":1,"power-armor-mk2":1,"solar-panel-equipment":20,"fusion-reactor-equipment":20,"battery-equipment":20,"battery-mk2-equipment":20,"belt-immunity-equipment":20,"exoskeleton-equipment":20,"personal-roboport-equipment":20,"personal-roboport-mk2-equipment":20,"night-vision-equipment":20,"energy-shield-equipment":20,"energy-shield-mk2-equipment":20,"personal-laser-defense-equipment":20,"discharge-defense-equipment":20,"discharge-defense-remote":1,"stone-wall":100,"gate":50,"gun-turret":50,"laser-turret":50,"flamethrower-turret":50,"artillery-turret":10,"artillery-targeting-remote":1,"radar":50,"player-port":50,"item-unknown":1,"electric-energy-interface":50,"linked-chest":10,"heat-interface":20,"linked-belt":10,"infinity-chest":10,"infinity-pipe":10,"selection-tool":1,"item-with-inventory":1,"item-with-label":1,"item-with-tags":1,"simple-entity-with-force":50,"simple-entity-with-owner":50,"burner-generator":10} \ No newline at end of file +{"wooden-chest":50,"iron-chest":50,"steel-chest":50,"storage-tank":50,"transport-belt":100,"fast-transport-belt":100,"express-transport-belt":100,"underground-belt":50,"fast-underground-belt":50,"express-underground-belt":50,"splitter":50,"fast-splitter":50,"express-splitter":50,"loader":50,"fast-loader":50,"express-loader":50,"burner-inserter":50,"inserter":50,"long-handed-inserter":50,"fast-inserter":50,"bulk-inserter":50,"small-electric-pole":50,"medium-electric-pole":50,"big-electric-pole":50,"substation":50,"pipe":100,"pipe-to-ground":50,"pump":50,"rail":100,"train-stop":10,"rail-signal":50,"rail-chain-signal":50,"locomotive":5,"cargo-wagon":5,"fluid-wagon":5,"artillery-wagon":5,"car":1,"tank":1,"spidertron":1,"logistic-robot":50,"construction-robot":50,"active-provider-chest":50,"passive-provider-chest":50,"storage-chest":50,"buffer-chest":50,"requester-chest":50,"roboport":10,"small-lamp":50,"arithmetic-combinator":50,"decider-combinator":50,"selector-combinator":50,"constant-combinator":50,"power-switch":10,"programmable-speaker":10,"display-panel":10,"stone-brick":100,"concrete":100,"hazard-concrete":100,"refined-concrete":100,"refined-hazard-concrete":100,"landfill":100,"cliff-explosives":20,"repair-pack":100,"blueprint":1,"deconstruction-planner":1,"upgrade-planner":1,"blueprint-book":1,"copy-paste-tool":1,"cut-paste-tool":1,"boiler":50,"steam-engine":10,"solar-panel":50,"accumulator":50,"nuclear-reactor":10,"heat-pipe":50,"heat-exchanger":50,"steam-turbine":10,"burner-mining-drill":50,"electric-mining-drill":50,"offshore-pump":20,"pumpjack":20,"stone-furnace":50,"steel-furnace":50,"electric-furnace":50,"assembling-machine-1":50,"assembling-machine-2":50,"assembling-machine-3":50,"oil-refinery":10,"chemical-plant":10,"centrifuge":50,"lab":10,"beacon":20,"speed-module":50,"speed-module-2":50,"speed-module-3":50,"efficiency-module":50,"efficiency-module-2":50,"efficiency-module-3":50,"productivity-module":50,"productivity-module-2":50,"productivity-module-3":50,"empty-module-slot":1,"rocket-silo":1,"cargo-landing-pad":1,"satellite":1,"wood":100,"coal":50,"stone":50,"iron-ore":50,"copper-ore":50,"uranium-ore":50,"raw-fish":100,"iron-plate":100,"copper-plate":100,"steel-plate":100,"solid-fuel":50,"plastic-bar":100,"sulfur":50,"battery":200,"explosives":50,"water-barrel":10,"crude-oil-barrel":10,"petroleum-gas-barrel":10,"light-oil-barrel":10,"heavy-oil-barrel":10,"lubricant-barrel":10,"sulfuric-acid-barrel":10,"iron-gear-wheel":100,"iron-stick":100,"copper-cable":200,"barrel":10,"electronic-circuit":200,"advanced-circuit":200,"processing-unit":100,"engine-unit":50,"electric-engine-unit":50,"flying-robot-frame":50,"low-density-structure":50,"rocket-fuel":20,"rocket-part":5,"uranium-235":100,"uranium-238":100,"uranium-fuel-cell":50,"depleted-uranium-fuel-cell":50,"nuclear-fuel":1,"automation-science-pack":200,"logistic-science-pack":200,"military-science-pack":200,"chemical-science-pack":200,"production-science-pack":200,"utility-science-pack":200,"space-science-pack":2000,"coin":100000,"science":1,"pistol":5,"submachine-gun":5,"tank-machine-gun":1,"vehicle-machine-gun":1,"tank-flamethrower":1,"shotgun":5,"combat-shotgun":5,"rocket-launcher":5,"flamethrower":5,"artillery-wagon-cannon":1,"spidertron-rocket-launcher-1":1,"spidertron-rocket-launcher-2":1,"spidertron-rocket-launcher-3":1,"spidertron-rocket-launcher-4":1,"tank-cannon":1,"firearm-magazine":100,"piercing-rounds-magazine":100,"uranium-rounds-magazine":100,"shotgun-shell":100,"piercing-shotgun-shell":100,"cannon-shell":100,"explosive-cannon-shell":100,"uranium-cannon-shell":100,"explosive-uranium-cannon-shell":100,"artillery-shell":1,"rocket":100,"explosive-rocket":100,"atomic-bomb":10,"flamethrower-ammo":100,"grenade":100,"cluster-grenade":100,"poison-capsule":100,"slowdown-capsule":100,"defender-capsule":100,"distractor-capsule":100,"destroyer-capsule":100,"light-armor":1,"heavy-armor":1,"modular-armor":1,"power-armor":1,"power-armor-mk2":1,"solar-panel-equipment":20,"fission-reactor-equipment":20,"battery-equipment":20,"battery-mk2-equipment":20,"belt-immunity-equipment":20,"exoskeleton-equipment":20,"personal-roboport-equipment":20,"personal-roboport-mk2-equipment":20,"night-vision-equipment":20,"energy-shield-equipment":20,"energy-shield-mk2-equipment":20,"personal-laser-defense-equipment":20,"discharge-defense-equipment":20,"stone-wall":100,"gate":50,"radar":50,"land-mine":100,"gun-turret":50,"laser-turret":50,"flamethrower-turret":50,"artillery-turret":10,"parameter-0":1,"parameter-1":1,"parameter-2":1,"parameter-3":1,"parameter-4":1,"parameter-5":1,"parameter-6":1,"parameter-7":1,"parameter-8":1,"parameter-9":1,"copper-wire":1,"green-wire":1,"red-wire":1,"spidertron-remote":1,"discharge-defense-remote":1,"artillery-targeting-remote":1,"item-unknown":1,"electric-energy-interface":50,"linked-chest":10,"heat-interface":20,"lane-splitter":50,"linked-belt":10,"infinity-chest":10,"infinity-pipe":10,"selection-tool":1,"simple-entity-with-force":50,"simple-entity-with-owner":50,"burner-generator":10} \ No newline at end of file diff --git a/worlds/factorio/data/machines.json b/worlds/factorio/data/machines.json index 15a79580d060..c8629ab8bef0 100644 --- a/worlds/factorio/data/machines.json +++ b/worlds/factorio/data/machines.json @@ -1 +1 @@ -{"stone-furnace":{"smelting":true},"steel-furnace":{"smelting":true},"electric-furnace":{"smelting":true},"assembling-machine-1":{"crafting":true,"basic-crafting":true,"advanced-crafting":true},"assembling-machine-2":{"basic-crafting":true,"crafting":true,"advanced-crafting":true,"crafting-with-fluid":true},"assembling-machine-3":{"basic-crafting":true,"crafting":true,"advanced-crafting":true,"crafting-with-fluid":true},"oil-refinery":{"oil-processing":true},"chemical-plant":{"chemistry":true},"centrifuge":{"centrifuging":true},"rocket-silo":{"rocket-building":true},"character":{"crafting":true}} \ No newline at end of file +{"stone-furnace":{"smelting":true},"steel-furnace":{"smelting":true},"electric-furnace":{"smelting":true},"assembling-machine-1":{"crafting":true,"basic-crafting":true,"advanced-crafting":true,"parameters":true},"assembling-machine-2":{"basic-crafting":true,"crafting":true,"advanced-crafting":true,"crafting-with-fluid":true,"parameters":true},"assembling-machine-3":{"basic-crafting":true,"crafting":true,"advanced-crafting":true,"crafting-with-fluid":true,"parameters":true},"oil-refinery":{"oil-processing":true,"parameters":true},"chemical-plant":{"chemistry":true,"parameters":true},"centrifuge":{"centrifuging":true,"parameters":true},"rocket-silo":{"rocket-building":true,"parameters":true},"character":{"crafting":true}} \ No newline at end of file diff --git a/worlds/factorio/data/mod/lib.lua b/worlds/factorio/data/mod/lib.lua index 2b18f119a427..7be7403e48f1 100644 --- a/worlds/factorio/data/mod/lib.lua +++ b/worlds/factorio/data/mod/lib.lua @@ -1,9 +1,9 @@ function get_any_stack_size(name) - local item = game.item_prototypes[name] + local item = prototypes.item[name] if item ~= nil then return item.stack_size end - item = game.equipment_prototypes[name] + item = prototypes.equipment[name] if item ~= nil then return item.stack_size end @@ -24,7 +24,7 @@ function split(s, sep) end function random_offset_position(position, offset) - return {x=position.x+math.random(-offset, offset), y=position.y+math.random(-1024, 1024)} + return {x=position.x+math.random(-offset, offset), y=position.y+math.random(-offset, offset)} end function fire_entity_at_players(entity_name, speed) @@ -36,4 +36,4 @@ function fire_entity_at_players(entity_name, speed) target=current_character, speed=speed} end end -end \ No newline at end of file +end diff --git a/worlds/factorio/data/mod_template/control.lua b/worlds/factorio/data/mod_template/control.lua index ace231e12b4b..b08608a60ae9 100644 --- a/worlds/factorio/data/mod_template/control.lua +++ b/worlds/factorio/data/mod_template/control.lua @@ -105,8 +105,8 @@ function on_player_changed_position(event) end local target_direction = exit_table[outbound_direction] - local target_position = {(CHUNK_OFFSET[target_direction][1] + last_x_chunk) * 32 + 16, - (CHUNK_OFFSET[target_direction][2] + last_y_chunk) * 32 + 16} + local target_position = {(CHUNK_OFFSET[target_direction][1] + last_x_chunk) * 32 + 16, + (CHUNK_OFFSET[target_direction][2] + last_y_chunk) * 32 + 16} target_position = character.surface.find_non_colliding_position(character.prototype.name, target_position, 32, 0.5) if target_position ~= nil then @@ -134,40 +134,96 @@ end script.on_event(defines.events.on_player_changed_position, on_player_changed_position) {% endif %} - +function count_energy_bridges() + local count = 0 + for i, bridge in pairs(storage.energy_link_bridges) do + if validate_energy_link_bridge(i, bridge) then + count = count + 1 + (bridge.quality.level * 0.3) + end + end + return count +end +function get_energy_increment(bridge) + return ENERGY_INCREMENT + (ENERGY_INCREMENT * 0.3 * bridge.quality.level) +end function on_check_energy_link(event) --- assuming 1 MJ increment and 5MJ battery: --- first 2 MJ request fill, last 2 MJ push energy, middle 1 MJ does nothing if event.tick % 60 == 30 then - local surface = game.get_surface(1) local force = "player" - local bridges = surface.find_entities_filtered({name="ap-energy-bridge", force=force}) - local bridgecount = table_size(bridges) - global.forcedata[force].energy_bridges = bridgecount - if global.forcedata[force].energy == nil then - global.forcedata[force].energy = 0 + local bridges = storage.energy_link_bridges + local bridgecount = count_energy_bridges() + storage.forcedata[force].energy_bridges = bridgecount + if storage.forcedata[force].energy == nil then + storage.forcedata[force].energy = 0 end - if global.forcedata[force].energy < ENERGY_INCREMENT * bridgecount * 5 then - for i, bridge in ipairs(bridges) do - if bridge.energy > ENERGY_INCREMENT*3 then - global.forcedata[force].energy = global.forcedata[force].energy + (ENERGY_INCREMENT * ENERGY_LINK_EFFICIENCY) - bridge.energy = bridge.energy - ENERGY_INCREMENT + if storage.forcedata[force].energy < ENERGY_INCREMENT * bridgecount * 5 then + for i, bridge in pairs(bridges) do + if validate_energy_link_bridge(i, bridge) then + energy_increment = get_energy_increment(bridge) + if bridge.energy > energy_increment*3 then + storage.forcedata[force].energy = storage.forcedata[force].energy + (energy_increment * ENERGY_LINK_EFFICIENCY) + bridge.energy = bridge.energy - energy_increment + end end end end - for i, bridge in ipairs(bridges) do - if global.forcedata[force].energy < ENERGY_INCREMENT then - break - end - if bridge.energy < ENERGY_INCREMENT*2 and global.forcedata[force].energy > ENERGY_INCREMENT then - global.forcedata[force].energy = global.forcedata[force].energy - ENERGY_INCREMENT - bridge.energy = bridge.energy + ENERGY_INCREMENT + for i, bridge in pairs(bridges) do + if validate_energy_link_bridge(i, bridge) then + energy_increment = get_energy_increment(bridge) + if storage.forcedata[force].energy < energy_increment and bridge.quality.level == 0 then + break + end + if bridge.energy < energy_increment*2 and storage.forcedata[force].energy > energy_increment then + storage.forcedata[force].energy = storage.forcedata[force].energy - energy_increment + bridge.energy = bridge.energy + energy_increment + end end end end end +function string_starts_with(str, start) + return str:sub(1, #start) == start +end +function validate_energy_link_bridge(unit_number, entity) + if not entity then + if storage.energy_link_bridges[unit_number] == nil then return false end + storage.energy_link_bridges[unit_number] = nil + return false + end + if not entity.valid then + if storage.energy_link_bridges[unit_number] == nil then return false end + storage.energy_link_bridges[unit_number] = nil + return false + end + return true +end +function on_energy_bridge_constructed(entity) + if entity and entity.valid then + if string_starts_with(entity.prototype.name, "ap-energy-bridge") then + storage.energy_link_bridges[entity.unit_number] = entity + end + end +end +function on_energy_bridge_removed(entity) + if string_starts_with(entity.prototype.name, "ap-energy-bridge") then + if storage.energy_link_bridges[entity.unit_number] == nil then return end + storage.energy_link_bridges[entity.unit_number] = nil + end +end if (ENERGY_INCREMENT) then script.on_event(defines.events.on_tick, on_check_energy_link) + + script.on_event({defines.events.on_built_entity}, function(event) on_energy_bridge_constructed(event.entity) end) + script.on_event({defines.events.on_robot_built_entity}, function(event) on_energy_bridge_constructed(event.entity) end) + script.on_event({defines.events.on_entity_cloned}, function(event) on_energy_bridge_constructed(event.destination) end) + + script.on_event({defines.events.script_raised_revive}, function(event) on_energy_bridge_constructed(event.entity) end) + script.on_event({defines.events.script_raised_built}, function(event) on_energy_bridge_constructed(event.entity) end) + + script.on_event({defines.events.on_entity_died}, function(event) on_energy_bridge_removed(event.entity) end) + script.on_event({defines.events.on_player_mined_entity}, function(event) on_energy_bridge_removed(event.entity) end) + script.on_event({defines.events.on_robot_mined_entity}, function(event) on_energy_bridge_removed(event.entity) end) end {% if not imported_blueprints -%} @@ -186,23 +242,41 @@ function check_spawn_silo(force) local surface = game.get_surface(1) local spawn_position = force.get_spawn_position(surface) spawn_entity(surface, force, "rocket-silo", spawn_position.x, spawn_position.y, 80, true, true) + spawn_entity(surface, force, "cargo-landing-pad", spawn_position.x, spawn_position.y, 80, true, true) end end function check_despawn_silo(force) - if not force.players or #force.players < 1 and force.get_entity_count("rocket-silo") > 0 then - local surface = game.get_surface(1) - local spawn_position = force.get_spawn_position(surface) - local x1 = spawn_position.x - 41 - local x2 = spawn_position.x + 41 - local y1 = spawn_position.y - 41 - local y2 = spawn_position.y + 41 - local silos = surface.find_entities_filtered{area = { {x1, y1}, {x2, y2} }, - name = "rocket-silo", - force = force} - for i,silo in ipairs(silos) do - silo.destructible = true - silo.destroy() + if not force.players or #force.players < 1 then + if force.get_entity_count("rocket-silo") > 0 then + local surface = game.get_surface(1) + local spawn_position = force.get_spawn_position(surface) + local x1 = spawn_position.x - 41 + local x2 = spawn_position.x + 41 + local y1 = spawn_position.y - 41 + local y2 = spawn_position.y + 41 + local silos = surface.find_entities_filtered{area = { {x1, y1}, {x2, y2} }, + name = "rocket-silo", + force = force} + for i, silo in ipairs(silos) do + silo.destructible = true + silo.destroy() + end + end + if force.get_entity_count("cargo-landing-pad") > 0 then + local surface = game.get_surface(1) + local spawn_position = force.get_spawn_position(surface) + local x1 = spawn_position.x - 41 + local x2 = spawn_position.x + 41 + local y1 = spawn_position.y - 41 + local y2 = spawn_position.y + 41 + local pads = surface.find_entities_filtered{area = { {x1, y1}, {x2, y2} }, + name = "cargo-landing-pad", + force = force} + for i, pad in ipairs(pads) do + pad.destructible = true + pad.destroy() + end end end end @@ -214,19 +288,18 @@ function on_force_created(event) if type(event.force) == "string" then -- should be of type LuaForce force = game.forces[force] end - force.research_queue_enabled = true local data = {} data['earned_samples'] = {{ dict_to_lua(starting_items) }} data["victory"] = 0 data["death_link_tick"] = 0 data["energy"] = 0 data["energy_bridges"] = 0 - global.forcedata[event.force] = data + storage.forcedata[event.force] = data {%- if silo == 2 %} check_spawn_silo(force) {%- endif %} -{%- for tech_name in useless_technologies %} - force.technologies.{{ tech_name }}.researched = true +{%- for tech_name in removed_technologies %} + force.technologies["{{ tech_name }}"].researched = true {%- endfor %} end script.on_event(defines.events.on_force_created, on_force_created) @@ -236,7 +309,7 @@ function on_force_destroyed(event) {%- if silo == 2 %} check_despawn_silo(event.force) {%- endif %} - global.forcedata[event.force.name] = nil + storage.forcedata[event.force.name] = nil end function on_runtime_mod_setting_changed(event) @@ -267,8 +340,8 @@ function on_player_created(event) -- FIXME: This (probably) fires before any other mod has a chance to change the player's force -- For now, they will (probably) always be on the 'player' force when this event fires. local data = {} - data['pending_samples'] = table.deepcopy(global.forcedata[player.force.name]['earned_samples']) - global.playerdata[player.index] = data + data['pending_samples'] = table.deepcopy(storage.forcedata[player.force.name]['earned_samples']) + storage.playerdata[player.index] = data update_player(player.index) -- Attempt to send pending free samples, if relevant. {%- if silo == 2 %} check_spawn_silo(game.players[event.player_index].force) @@ -287,14 +360,19 @@ end script.on_event(defines.events.on_player_changed_force, on_player_changed_force) function on_player_removed(event) - global.playerdata[event.player_index] = nil + storage.playerdata[event.player_index] = nil end script.on_event(defines.events.on_player_removed, on_player_removed) function on_rocket_launched(event) - if event.rocket and event.rocket.valid and global.forcedata[event.rocket.force.name]['victory'] == 0 then - if event.rocket.get_item_count("satellite") > 0 or GOAL == 0 then - global.forcedata[event.rocket.force.name]['victory'] = 1 + if event.rocket and event.rocket.valid and storage.forcedata[event.rocket.force.name]['victory'] == 0 then + satellite_count = 0 + cargo_pod = event.rocket.cargo_pod + if cargo_pod then + satellite_count = cargo_pod.get_item_count("satellite") + end + if satellite_count > 0 or GOAL == 0 then + storage.forcedata[event.rocket.force.name]['victory'] = 1 dumpInfo(event.rocket.force) game.set_game_state { @@ -318,7 +396,7 @@ function update_player(index) if not character or not character.valid then return end - local data = global.playerdata[index] + local data = storage.playerdata[index] local samples = data['pending_samples'] local sent --player.print(serpent.block(data['pending_samples'])) @@ -327,14 +405,17 @@ function update_player(index) for name, count in pairs(samples) do stack.name = name stack.count = count - if game.item_prototypes[name] then + if script.active_mods["quality"] then + stack.quality = "{{ free_sample_quality_name }}" + end + if prototypes.item[name] then if character.can_insert(stack) then sent = character.insert(stack) else sent = 0 end if sent > 0 then - player.print("Received " .. sent .. "x [item=" .. name .. "]") + player.print("Received " .. sent .. "x [item=" .. name .. ",quality={{ free_sample_quality_name }}]") data.suppress_full_inventory_message = false end if sent ~= count then -- Couldn't full send. @@ -372,19 +453,20 @@ function add_samples(force, name, count) end t[name] = (t[name] or 0) + count end - -- Add to global table of earned samples for future new players - add_to_table(global.forcedata[force.name]['earned_samples']) + -- Add to storage table of earned samples for future new players + add_to_table(storage.forcedata[force.name]['earned_samples']) -- Add to existing players for _, player in pairs(force.players) do - add_to_table(global.playerdata[player.index]['pending_samples']) + add_to_table(storage.playerdata[player.index]['pending_samples']) update_player(player.index) end end script.on_init(function() {% if not imported_blueprints %}set_permissions(){% endif %} - global.forcedata = {} - global.playerdata = {} + storage.forcedata = {} + storage.playerdata = {} + storage.energy_link_bridges = {} -- Fire dummy events for all currently existing forces. local e = {} for name, _ in pairs(game.forces) do @@ -420,12 +502,12 @@ script.on_event(defines.events.on_research_finished, function(event) if FREE_SAMPLES == 0 then return -- Nothing else to do end - if not technology.effects then + if not technology.prototype.effects then return -- No technology effects, so nothing to do. end - for _, effect in pairs(technology.effects) do + for _, effect in pairs(technology.prototype.effects) do if effect.type == "unlock-recipe" then - local recipe = game.recipe_prototypes[effect.recipe] + local recipe = prototypes.recipe[effect.recipe] for _, result in pairs(recipe.products) do if result.type == "item" and result.amount then local name = result.name @@ -477,7 +559,7 @@ function kill_players(force) end function spawn_entity(surface, force, name, x, y, radius, randomize, avoid_ores) - local prototype = game.entity_prototypes[name] + local prototype = prototypes.entity[name] local args = { -- For can_place_entity and place_entity name = prototype.name, position = {x = x, y = y}, @@ -537,7 +619,7 @@ function spawn_entity(surface, force, name, x, y, radius, randomize, avoid_ores) } local entities = surface.find_entities_filtered { area = collision_area, - collision_mask = prototype.collision_mask + collision_mask = prototype.collision_mask.layers } local can_place = true for _, entity in pairs(entities) do @@ -560,6 +642,9 @@ function spawn_entity(surface, force, name, x, y, radius, randomize, avoid_ores) end args.build_check_type = defines.build_check_type.script args.create_build_effect_smoke = false + if script.active_mods["quality"] then + args.quality = "{{ free_sample_quality_name }}" + end new_entity = surface.create_entity(args) if new_entity then new_entity.destructible = false @@ -585,7 +670,7 @@ script.on_event(defines.events.on_entity_died, function(event) end local force = event.entity.force - global.forcedata[force.name].death_link_tick = game.tick + storage.forcedata[force.name].death_link_tick = game.tick dumpInfo(force) kill_players(force) end, {LuaEntityDiedEventFilter = {["filter"] = "name", ["name"] = "character"}}) @@ -600,7 +685,7 @@ commands.add_command("ap-sync", "Used by the Archipelago client to get progress force = game.players[call.player_index].force end local research_done = {} - local forcedata = chain_lookup(global, "forcedata", force.name) + local forcedata = chain_lookup(storage, "forcedata", force.name) local data_collection = { ["research_done"] = research_done, ["victory"] = chain_lookup(forcedata, "victory"), @@ -616,7 +701,7 @@ commands.add_command("ap-sync", "Used by the Archipelago client to get progress research_done[tech_name] = tech.researched end end - rcon.print(game.table_to_json({["slot_name"] = SLOT_NAME, ["seed_name"] = SEED_NAME, ["info"] = data_collection})) + rcon.print(helpers.table_to_json({["slot_name"] = SLOT_NAME, ["seed_name"] = SEED_NAME, ["info"] = data_collection})) end) commands.add_command("ap-print", "Used by the Archipelago client to print messages", function (call) @@ -655,8 +740,8 @@ end, } commands.add_command("ap-get-technology", "Grant a technology, used by the Archipelago Client.", function(call) - if global.index_sync == nil then - global.index_sync = {} + if storage.index_sync == nil then + storage.index_sync = {} end local tech local force = game.forces["player"] @@ -680,8 +765,8 @@ commands.add_command("ap-get-technology", "Grant a technology, used by the Archi end return elseif progressive_technologies[item_name] ~= nil then - if global.index_sync[index] ~= item_name then -- not yet received prog item - global.index_sync[index] = item_name + if storage.index_sync[index] ~= item_name then -- not yet received prog item + storage.index_sync[index] = item_name local tech_stack = progressive_technologies[item_name] for _, item_name in ipairs(tech_stack) do tech = force.technologies[item_name] @@ -696,7 +781,7 @@ commands.add_command("ap-get-technology", "Grant a technology, used by the Archi elseif force.technologies[item_name] ~= nil then tech = force.technologies[item_name] if tech ~= nil then - global.index_sync[index] = tech + storage.index_sync[index] = tech if tech.researched ~= true then game.print({"", "Received [technology=" .. tech.name .. "] from ", source}) game.play_sound({path="utility/research_completed"}) @@ -704,8 +789,8 @@ commands.add_command("ap-get-technology", "Grant a technology, used by the Archi end end elseif TRAP_TABLE[item_name] ~= nil then - if global.index_sync[index] ~= item_name then -- not yet received trap - global.index_sync[index] = item_name + if storage.index_sync[index] ~= item_name then -- not yet received trap + storage.index_sync[index] = item_name game.print({"", "Received ", item_name, " from ", source}) TRAP_TABLE[item_name]() end @@ -716,7 +801,7 @@ end) commands.add_command("ap-rcon-info", "Used by the Archipelago client to get information", function(call) - rcon.print(game.table_to_json({ + rcon.print(helpers.table_to_json({ ["slot_name"] = SLOT_NAME, ["seed_name"] = SEED_NAME, ["death_link"] = DEATH_LINK, @@ -726,7 +811,7 @@ end) {% if allow_cheats -%} -commands.add_command("ap-spawn-silo", "Attempts to spawn a silo around 0,0", function(call) +commands.add_command("ap-spawn-silo", "Attempts to spawn a silo and cargo landing pad around 0,0", function(call) spawn_entity(game.player.surface, game.player.force, "rocket-silo", 0, 0, 80, true, true) end) {% endif -%} @@ -742,7 +827,7 @@ end) commands.add_command("ap-energylink", "Used by the Archipelago client to manage Energy Link", function(call) local change = tonumber(call.parameter or "0") local force = "player" - global.forcedata[force].energy = global.forcedata[force].energy + change + storage.forcedata[force].energy = storage.forcedata[force].energy + change end) commands.add_command("energy-link", "Print the status of the Archipelago energy link.", function(call) diff --git a/worlds/factorio/data/mod_template/data-final-fixes.lua b/worlds/factorio/data/mod_template/data-final-fixes.lua index 3021fd5dadca..dc068c4f62aa 100644 --- a/worlds/factorio/data/mod_template/data-final-fixes.lua +++ b/worlds/factorio/data/mod_template/data-final-fixes.lua @@ -6,43 +6,46 @@ data.raw["rocket-silo"]["rocket-silo"].fluid_boxes = { production_type = "input", pipe_picture = assembler2pipepictures(), pipe_covers = pipecoverspictures(), + volume = 1000, base_area = 10, base_level = -1, pipe_connections = { - { type = "input", position = { 0, 5 } }, - { type = "input", position = { 0, -5 } }, - { type = "input", position = { 5, 0 } }, - { type = "input", position = { -5, 0 } } + { flow_direction = "input", direction = defines.direction.south, position = { 0, 4.2 } }, + { flow_direction = "input", direction = defines.direction.north, position = { 0, -4.2 } }, + { flow_direction = "input", direction = defines.direction.east, position = { 4.2, 0 } }, + { flow_direction = "input", direction = defines.direction.west, position = { -4.2, 0 } } } }, { production_type = "input", pipe_picture = assembler2pipepictures(), pipe_covers = pipecoverspictures(), + volume = 1000, base_area = 10, base_level = -1, pipe_connections = { - { type = "input", position = { -3, 5 } }, - { type = "input", position = { -3, -5 } }, - { type = "input", position = { 5, -3 } }, - { type = "input", position = { -5, -3 } } + { flow_direction = "input", direction = defines.direction.south, position = { -3, 4.2 } }, + { flow_direction = "input", direction = defines.direction.north, position = { -3, -4.2 } }, + { flow_direction = "input", direction = defines.direction.east, position = { 4.2, -3 } }, + { flow_direction = "input", direction = defines.direction.west, position = { -4.2, -3 } } } }, { production_type = "input", pipe_picture = assembler2pipepictures(), pipe_covers = pipecoverspictures(), + volume = 1000, base_area = 10, base_level = -1, pipe_connections = { - { type = "input", position = { 3, 5 } }, - { type = "input", position = { 3, -5 } }, - { type = "input", position = { 5, 3 } }, - { type = "input", position = { -5, 3 } } + { flow_direction = "input", direction = defines.direction.south, position = { 3, 4.2 } }, + { flow_direction = "input", direction = defines.direction.north, position = { 3, -4.2 } }, + { flow_direction = "input", direction = defines.direction.east, position = { 4.2, 3 } }, + { flow_direction = "input", direction = defines.direction.west, position = { -4.2, 3 } } } - }, - off_when_no_fluid_recipe = true + } } +data.raw["rocket-silo"]["rocket-silo"].fluid_boxes_off_when_no_fluid_recipe = true {%- for recipe_name, recipe in custom_recipes.items() %} data.raw["recipe"]["{{recipe_name}}"].category = "{{recipe.category}}" diff --git a/worlds/factorio/data/mod_template/data.lua b/worlds/factorio/data/mod_template/data.lua index 82053453eadf..43151ff00840 100644 --- a/worlds/factorio/data/mod_template/data.lua +++ b/worlds/factorio/data/mod_template/data.lua @@ -18,12 +18,9 @@ energy_bridge.energy_source.buffer_capacity = "50MJ" energy_bridge.energy_source.input_flow_limit = "10MW" energy_bridge.energy_source.output_flow_limit = "10MW" tint_icon(energy_bridge, energy_bridge_tint()) -energy_bridge.picture.layers[1].tint = energy_bridge_tint() -energy_bridge.picture.layers[1].hr_version.tint = energy_bridge_tint() -energy_bridge.charge_animation.layers[1].layers[1].tint = energy_bridge_tint() -energy_bridge.charge_animation.layers[1].layers[1].hr_version.tint = energy_bridge_tint() -energy_bridge.discharge_animation.layers[1].layers[1].tint = energy_bridge_tint() -energy_bridge.discharge_animation.layers[1].layers[1].hr_version.tint = energy_bridge_tint() +energy_bridge.chargable_graphics.picture.layers[1].tint = energy_bridge_tint() +energy_bridge.chargable_graphics.charge_animation.layers[1].layers[1].tint = energy_bridge_tint() +energy_bridge.chargable_graphics.discharge_animation.layers[1].layers[1].tint = energy_bridge_tint() data.raw["accumulator"]["ap-energy-bridge"] = energy_bridge local energy_bridge_item = table.deepcopy(data.raw["item"]["accumulator"]) @@ -35,9 +32,9 @@ data.raw["item"]["ap-energy-bridge"] = energy_bridge_item local energy_bridge_recipe = table.deepcopy(data.raw["recipe"]["accumulator"]) energy_bridge_recipe.name = "ap-energy-bridge" -energy_bridge_recipe.result = energy_bridge_item.name +energy_bridge_recipe.results = { {type = "item", name = energy_bridge_item.name, amount = 1} } energy_bridge_recipe.energy_required = 1 -energy_bridge_recipe.enabled = {{ energy_link }} +energy_bridge_recipe.enabled = {% if energy_link %}true{% else %}false{% endif %} energy_bridge_recipe.localised_name = "Archipelago EnergyLink Bridge" data.raw["recipe"]["ap-energy-bridge"] = energy_bridge_recipe diff --git a/worlds/factorio/data/mod_template/macros.lua b/worlds/factorio/data/mod_template/macros.lua index 1b271031a393..f1530359c823 100644 --- a/worlds/factorio/data/mod_template/macros.lua +++ b/worlds/factorio/data/mod_template/macros.lua @@ -26,4 +26,4 @@ {type = {% if key in liquids %}"fluid"{% else %}"item"{% endif %}, name = "{{ key }}", amount = {{ value | safe }}}{% if not loop.last %},{% endif %} {% endfor -%} } -{%- endmacro %} \ No newline at end of file +{%- endmacro %} diff --git a/worlds/factorio/data/mod_template/settings.lua b/worlds/factorio/data/mod_template/settings.lua index 73e131a60e7c..41d30e58d552 100644 --- a/worlds/factorio/data/mod_template/settings.lua +++ b/worlds/factorio/data/mod_template/settings.lua @@ -27,4 +27,4 @@ data:extend({ default_value = false {% endif %} } -}) \ No newline at end of file +}) diff --git a/worlds/factorio/data/recipes.json b/worlds/factorio/data/recipes.json index 4c4ab81526af..b0633b493d79 100644 --- a/worlds/factorio/data/recipes.json +++ b/worlds/factorio/data/recipes.json @@ -1 +1 @@ -{"accumulator":{"ingredients":{"iron-plate":2,"battery":5},"products":{"accumulator":1},"category":"crafting","energy":10},"advanced-circuit":{"ingredients":{"plastic-bar":2,"copper-cable":4,"electronic-circuit":2},"products":{"advanced-circuit":1},"category":"crafting","energy":6},"arithmetic-combinator":{"ingredients":{"copper-cable":5,"electronic-circuit":5},"products":{"arithmetic-combinator":1},"category":"crafting","energy":0.5},"artillery-shell":{"ingredients":{"explosives":8,"explosive-cannon-shell":4,"radar":1},"products":{"artillery-shell":1},"category":"crafting","energy":15},"artillery-targeting-remote":{"ingredients":{"processing-unit":1,"radar":1},"products":{"artillery-targeting-remote":1},"category":"crafting","energy":0.5},"artillery-turret":{"ingredients":{"steel-plate":60,"iron-gear-wheel":40,"advanced-circuit":20,"concrete":60},"products":{"artillery-turret":1},"category":"crafting","energy":40},"artillery-wagon":{"ingredients":{"steel-plate":40,"iron-gear-wheel":10,"advanced-circuit":20,"engine-unit":64,"pipe":16},"products":{"artillery-wagon":1},"category":"crafting","energy":4},"assembling-machine-1":{"ingredients":{"iron-plate":9,"iron-gear-wheel":5,"electronic-circuit":3},"products":{"assembling-machine-1":1},"category":"crafting","energy":0.5},"assembling-machine-2":{"ingredients":{"steel-plate":2,"iron-gear-wheel":5,"electronic-circuit":3,"assembling-machine-1":1},"products":{"assembling-machine-2":1},"category":"crafting","energy":0.5},"assembling-machine-3":{"ingredients":{"assembling-machine-2":2,"speed-module":4},"products":{"assembling-machine-3":1},"category":"crafting","energy":0.5},"atomic-bomb":{"ingredients":{"explosives":10,"rocket-control-unit":10,"uranium-235":30},"products":{"atomic-bomb":1},"category":"crafting","energy":50},"automation-science-pack":{"ingredients":{"copper-plate":1,"iron-gear-wheel":1},"products":{"automation-science-pack":1},"category":"crafting","energy":5},"battery":{"ingredients":{"iron-plate":1,"copper-plate":1,"sulfuric-acid":20},"products":{"battery":1},"category":"chemistry","energy":4},"battery-equipment":{"ingredients":{"steel-plate":10,"battery":5},"products":{"battery-equipment":1},"category":"crafting","energy":10},"battery-mk2-equipment":{"ingredients":{"processing-unit":15,"low-density-structure":5,"battery-equipment":10},"products":{"battery-mk2-equipment":1},"category":"crafting","energy":10},"beacon":{"ingredients":{"steel-plate":10,"copper-cable":10,"electronic-circuit":20,"advanced-circuit":20},"products":{"beacon":1},"category":"crafting","energy":15},"belt-immunity-equipment":{"ingredients":{"steel-plate":10,"advanced-circuit":5},"products":{"belt-immunity-equipment":1},"category":"crafting","energy":10},"big-electric-pole":{"ingredients":{"copper-plate":5,"steel-plate":5,"iron-stick":8},"products":{"big-electric-pole":1},"category":"crafting","energy":0.5},"boiler":{"ingredients":{"pipe":4,"stone-furnace":1},"products":{"boiler":1},"category":"crafting","energy":0.5},"burner-inserter":{"ingredients":{"iron-plate":1,"iron-gear-wheel":1},"products":{"burner-inserter":1},"category":"crafting","energy":0.5},"burner-mining-drill":{"ingredients":{"iron-plate":3,"iron-gear-wheel":3,"stone-furnace":1},"products":{"burner-mining-drill":1},"category":"crafting","energy":2},"cannon-shell":{"ingredients":{"steel-plate":2,"plastic-bar":2,"explosives":1},"products":{"cannon-shell":1},"category":"crafting","energy":8},"car":{"ingredients":{"iron-plate":20,"steel-plate":5,"engine-unit":8},"products":{"car":1},"category":"crafting","energy":2},"cargo-wagon":{"ingredients":{"iron-plate":20,"steel-plate":20,"iron-gear-wheel":10},"products":{"cargo-wagon":1},"category":"crafting","energy":1},"centrifuge":{"ingredients":{"steel-plate":50,"iron-gear-wheel":100,"advanced-circuit":100,"concrete":100},"products":{"centrifuge":1},"category":"crafting","energy":4},"chemical-plant":{"ingredients":{"steel-plate":5,"iron-gear-wheel":5,"electronic-circuit":5,"pipe":5},"products":{"chemical-plant":1},"category":"crafting","energy":5},"chemical-science-pack":{"ingredients":{"sulfur":1,"advanced-circuit":3,"engine-unit":2},"products":{"chemical-science-pack":2},"category":"crafting","energy":24},"cliff-explosives":{"ingredients":{"explosives":10,"empty-barrel":1,"grenade":1},"products":{"cliff-explosives":1},"category":"crafting","energy":8},"cluster-grenade":{"ingredients":{"steel-plate":5,"explosives":5,"grenade":7},"products":{"cluster-grenade":1},"category":"crafting","energy":8},"combat-shotgun":{"ingredients":{"wood":10,"copper-plate":10,"steel-plate":15,"iron-gear-wheel":5},"products":{"combat-shotgun":1},"category":"crafting","energy":10},"concrete":{"ingredients":{"iron-ore":1,"stone-brick":5,"water":100},"products":{"concrete":10},"category":"crafting-with-fluid","energy":10},"constant-combinator":{"ingredients":{"copper-cable":5,"electronic-circuit":2},"products":{"constant-combinator":1},"category":"crafting","energy":0.5},"construction-robot":{"ingredients":{"electronic-circuit":2,"flying-robot-frame":1},"products":{"construction-robot":1},"category":"crafting","energy":0.5},"copper-cable":{"ingredients":{"copper-plate":1},"products":{"copper-cable":2},"category":"crafting","energy":0.5},"copper-plate":{"ingredients":{"copper-ore":1},"products":{"copper-plate":1},"category":"smelting","energy":3.20000000000000017763568394002504646778106689453125},"decider-combinator":{"ingredients":{"copper-cable":5,"electronic-circuit":5},"products":{"decider-combinator":1},"category":"crafting","energy":0.5},"defender-capsule":{"ingredients":{"iron-gear-wheel":3,"electronic-circuit":3,"piercing-rounds-magazine":3},"products":{"defender-capsule":1},"category":"crafting","energy":8},"destroyer-capsule":{"ingredients":{"speed-module":1,"distractor-capsule":4},"products":{"destroyer-capsule":1},"category":"crafting","energy":15},"discharge-defense-equipment":{"ingredients":{"steel-plate":20,"processing-unit":5,"laser-turret":10},"products":{"discharge-defense-equipment":1},"category":"crafting","energy":10},"discharge-defense-remote":{"ingredients":{"electronic-circuit":1},"products":{"discharge-defense-remote":1},"category":"crafting","energy":0.5},"distractor-capsule":{"ingredients":{"advanced-circuit":3,"defender-capsule":4},"products":{"distractor-capsule":1},"category":"crafting","energy":15},"effectivity-module":{"ingredients":{"electronic-circuit":5,"advanced-circuit":5},"products":{"effectivity-module":1},"category":"crafting","energy":15},"effectivity-module-2":{"ingredients":{"advanced-circuit":5,"processing-unit":5,"effectivity-module":4},"products":{"effectivity-module-2":1},"category":"crafting","energy":30},"effectivity-module-3":{"ingredients":{"advanced-circuit":5,"processing-unit":5,"effectivity-module-2":5},"products":{"effectivity-module-3":1},"category":"crafting","energy":60},"electric-engine-unit":{"ingredients":{"electronic-circuit":2,"engine-unit":1,"lubricant":15},"products":{"electric-engine-unit":1},"category":"crafting-with-fluid","energy":10},"electric-furnace":{"ingredients":{"steel-plate":10,"advanced-circuit":5,"stone-brick":10},"products":{"electric-furnace":1},"category":"crafting","energy":5},"electric-mining-drill":{"ingredients":{"iron-plate":10,"iron-gear-wheel":5,"electronic-circuit":3},"products":{"electric-mining-drill":1},"category":"crafting","energy":2},"electronic-circuit":{"ingredients":{"iron-plate":1,"copper-cable":3},"products":{"electronic-circuit":1},"category":"crafting","energy":0.5},"empty-barrel":{"ingredients":{"steel-plate":1},"products":{"empty-barrel":1},"category":"crafting","energy":1},"energy-shield-equipment":{"ingredients":{"steel-plate":10,"advanced-circuit":5},"products":{"energy-shield-equipment":1},"category":"crafting","energy":10},"energy-shield-mk2-equipment":{"ingredients":{"processing-unit":5,"low-density-structure":5,"energy-shield-equipment":10},"products":{"energy-shield-mk2-equipment":1},"category":"crafting","energy":10},"engine-unit":{"ingredients":{"steel-plate":1,"iron-gear-wheel":1,"pipe":2},"products":{"engine-unit":1},"category":"advanced-crafting","energy":10},"exoskeleton-equipment":{"ingredients":{"steel-plate":20,"processing-unit":10,"electric-engine-unit":30},"products":{"exoskeleton-equipment":1},"category":"crafting","energy":10},"explosive-cannon-shell":{"ingredients":{"steel-plate":2,"plastic-bar":2,"explosives":2},"products":{"explosive-cannon-shell":1},"category":"crafting","energy":8},"explosive-rocket":{"ingredients":{"explosives":2,"rocket":1},"products":{"explosive-rocket":1},"category":"crafting","energy":8},"explosive-uranium-cannon-shell":{"ingredients":{"uranium-238":1,"explosive-cannon-shell":1},"products":{"explosive-uranium-cannon-shell":1},"category":"crafting","energy":12},"explosives":{"ingredients":{"coal":1,"sulfur":1,"water":10},"products":{"explosives":2},"category":"chemistry","energy":4},"express-splitter":{"ingredients":{"iron-gear-wheel":10,"advanced-circuit":10,"fast-splitter":1,"lubricant":80},"products":{"express-splitter":1},"category":"crafting-with-fluid","energy":2},"express-transport-belt":{"ingredients":{"iron-gear-wheel":10,"fast-transport-belt":1,"lubricant":20},"products":{"express-transport-belt":1},"category":"crafting-with-fluid","energy":0.5},"express-underground-belt":{"ingredients":{"iron-gear-wheel":80,"fast-underground-belt":2,"lubricant":40},"products":{"express-underground-belt":2},"category":"crafting-with-fluid","energy":2},"fast-inserter":{"ingredients":{"iron-plate":2,"electronic-circuit":2,"inserter":1},"products":{"fast-inserter":1},"category":"crafting","energy":0.5},"fast-splitter":{"ingredients":{"iron-gear-wheel":10,"electronic-circuit":10,"splitter":1},"products":{"fast-splitter":1},"category":"crafting","energy":2},"fast-transport-belt":{"ingredients":{"iron-gear-wheel":5,"transport-belt":1},"products":{"fast-transport-belt":1},"category":"crafting","energy":0.5},"fast-underground-belt":{"ingredients":{"iron-gear-wheel":40,"underground-belt":2},"products":{"fast-underground-belt":2},"category":"crafting","energy":2},"filter-inserter":{"ingredients":{"electronic-circuit":4,"fast-inserter":1},"products":{"filter-inserter":1},"category":"crafting","energy":0.5},"firearm-magazine":{"ingredients":{"iron-plate":4},"products":{"firearm-magazine":1},"category":"crafting","energy":1},"flamethrower":{"ingredients":{"steel-plate":5,"iron-gear-wheel":10},"products":{"flamethrower":1},"category":"crafting","energy":10},"flamethrower-ammo":{"ingredients":{"steel-plate":5,"crude-oil":100},"products":{"flamethrower-ammo":1},"category":"chemistry","energy":6},"flamethrower-turret":{"ingredients":{"steel-plate":30,"iron-gear-wheel":15,"engine-unit":5,"pipe":10},"products":{"flamethrower-turret":1},"category":"crafting","energy":20},"fluid-wagon":{"ingredients":{"steel-plate":16,"iron-gear-wheel":10,"storage-tank":1,"pipe":8},"products":{"fluid-wagon":1},"category":"crafting","energy":1.5},"flying-robot-frame":{"ingredients":{"steel-plate":1,"battery":2,"electronic-circuit":3,"electric-engine-unit":1},"products":{"flying-robot-frame":1},"category":"crafting","energy":20},"fusion-reactor-equipment":{"ingredients":{"processing-unit":200,"low-density-structure":50},"products":{"fusion-reactor-equipment":1},"category":"crafting","energy":10},"gate":{"ingredients":{"steel-plate":2,"electronic-circuit":2,"stone-wall":1},"products":{"gate":1},"category":"crafting","energy":0.5},"green-wire":{"ingredients":{"copper-cable":1,"electronic-circuit":1},"products":{"green-wire":1},"category":"crafting","energy":0.5},"grenade":{"ingredients":{"coal":10,"iron-plate":5},"products":{"grenade":1},"category":"crafting","energy":8},"gun-turret":{"ingredients":{"iron-plate":20,"copper-plate":10,"iron-gear-wheel":10},"products":{"gun-turret":1},"category":"crafting","energy":8},"hazard-concrete":{"ingredients":{"concrete":10},"products":{"hazard-concrete":10},"category":"crafting","energy":0.25},"heat-exchanger":{"ingredients":{"copper-plate":100,"steel-plate":10,"pipe":10},"products":{"heat-exchanger":1},"category":"crafting","energy":3},"heat-pipe":{"ingredients":{"copper-plate":20,"steel-plate":10},"products":{"heat-pipe":1},"category":"crafting","energy":1},"heavy-armor":{"ingredients":{"copper-plate":100,"steel-plate":50},"products":{"heavy-armor":1},"category":"crafting","energy":8},"inserter":{"ingredients":{"iron-plate":1,"iron-gear-wheel":1,"electronic-circuit":1},"products":{"inserter":1},"category":"crafting","energy":0.5},"iron-chest":{"ingredients":{"iron-plate":8},"products":{"iron-chest":1},"category":"crafting","energy":0.5},"iron-gear-wheel":{"ingredients":{"iron-plate":2},"products":{"iron-gear-wheel":1},"category":"crafting","energy":0.5},"iron-plate":{"ingredients":{"iron-ore":1},"products":{"iron-plate":1},"category":"smelting","energy":3.20000000000000017763568394002504646778106689453125},"iron-stick":{"ingredients":{"iron-plate":1},"products":{"iron-stick":2},"category":"crafting","energy":0.5},"lab":{"ingredients":{"iron-gear-wheel":10,"electronic-circuit":10,"transport-belt":4},"products":{"lab":1},"category":"crafting","energy":2},"land-mine":{"ingredients":{"steel-plate":1,"explosives":2},"products":{"land-mine":4},"category":"crafting","energy":5},"landfill":{"ingredients":{"stone":20},"products":{"landfill":1},"category":"crafting","energy":0.5},"laser-turret":{"ingredients":{"steel-plate":20,"battery":12,"electronic-circuit":20},"products":{"laser-turret":1},"category":"crafting","energy":20},"light-armor":{"ingredients":{"iron-plate":40},"products":{"light-armor":1},"category":"crafting","energy":3},"locomotive":{"ingredients":{"steel-plate":30,"electronic-circuit":10,"engine-unit":20},"products":{"locomotive":1},"category":"crafting","energy":4},"logistic-chest-active-provider":{"ingredients":{"electronic-circuit":3,"advanced-circuit":1,"steel-chest":1},"products":{"logistic-chest-active-provider":1},"category":"crafting","energy":0.5},"logistic-chest-buffer":{"ingredients":{"electronic-circuit":3,"advanced-circuit":1,"steel-chest":1},"products":{"logistic-chest-buffer":1},"category":"crafting","energy":0.5},"logistic-chest-passive-provider":{"ingredients":{"electronic-circuit":3,"advanced-circuit":1,"steel-chest":1},"products":{"logistic-chest-passive-provider":1},"category":"crafting","energy":0.5},"logistic-chest-requester":{"ingredients":{"electronic-circuit":3,"advanced-circuit":1,"steel-chest":1},"products":{"logistic-chest-requester":1},"category":"crafting","energy":0.5},"logistic-chest-storage":{"ingredients":{"electronic-circuit":3,"advanced-circuit":1,"steel-chest":1},"products":{"logistic-chest-storage":1},"category":"crafting","energy":0.5},"logistic-robot":{"ingredients":{"advanced-circuit":2,"flying-robot-frame":1},"products":{"logistic-robot":1},"category":"crafting","energy":0.5},"logistic-science-pack":{"ingredients":{"transport-belt":1,"inserter":1},"products":{"logistic-science-pack":1},"category":"crafting","energy":6},"long-handed-inserter":{"ingredients":{"iron-plate":1,"iron-gear-wheel":1,"inserter":1},"products":{"long-handed-inserter":1},"category":"crafting","energy":0.5},"low-density-structure":{"ingredients":{"copper-plate":20,"steel-plate":2,"plastic-bar":5},"products":{"low-density-structure":1},"category":"crafting","energy":20},"lubricant":{"ingredients":{"heavy-oil":10},"products":{"lubricant":10},"category":"chemistry","energy":1},"medium-electric-pole":{"ingredients":{"copper-plate":2,"steel-plate":2,"iron-stick":4},"products":{"medium-electric-pole":1},"category":"crafting","energy":0.5},"military-science-pack":{"ingredients":{"piercing-rounds-magazine":1,"grenade":1,"stone-wall":2},"products":{"military-science-pack":2},"category":"crafting","energy":10},"modular-armor":{"ingredients":{"steel-plate":50,"advanced-circuit":30},"products":{"modular-armor":1},"category":"crafting","energy":15},"night-vision-equipment":{"ingredients":{"steel-plate":10,"advanced-circuit":5},"products":{"night-vision-equipment":1},"category":"crafting","energy":10},"nuclear-fuel":{"ingredients":{"rocket-fuel":1,"uranium-235":1},"products":{"nuclear-fuel":1},"category":"centrifuging","energy":90},"nuclear-reactor":{"ingredients":{"copper-plate":500,"steel-plate":500,"advanced-circuit":500,"concrete":500},"products":{"nuclear-reactor":1},"category":"crafting","energy":8},"offshore-pump":{"ingredients":{"iron-gear-wheel":1,"electronic-circuit":2,"pipe":1},"products":{"offshore-pump":1},"category":"crafting","energy":0.5},"oil-refinery":{"ingredients":{"steel-plate":15,"iron-gear-wheel":10,"electronic-circuit":10,"pipe":10,"stone-brick":10},"products":{"oil-refinery":1},"category":"crafting","energy":8},"personal-laser-defense-equipment":{"ingredients":{"processing-unit":20,"low-density-structure":5,"laser-turret":5},"products":{"personal-laser-defense-equipment":1},"category":"crafting","energy":10},"personal-roboport-equipment":{"ingredients":{"steel-plate":20,"battery":45,"iron-gear-wheel":40,"advanced-circuit":10},"products":{"personal-roboport-equipment":1},"category":"crafting","energy":10},"personal-roboport-mk2-equipment":{"ingredients":{"processing-unit":100,"low-density-structure":20,"personal-roboport-equipment":5},"products":{"personal-roboport-mk2-equipment":1},"category":"crafting","energy":20},"piercing-rounds-magazine":{"ingredients":{"copper-plate":5,"steel-plate":1,"firearm-magazine":1},"products":{"piercing-rounds-magazine":1},"category":"crafting","energy":3},"piercing-shotgun-shell":{"ingredients":{"copper-plate":5,"steel-plate":2,"shotgun-shell":2},"products":{"piercing-shotgun-shell":1},"category":"crafting","energy":8},"pipe":{"ingredients":{"iron-plate":1},"products":{"pipe":1},"category":"crafting","energy":0.5},"pipe-to-ground":{"ingredients":{"iron-plate":5,"pipe":10},"products":{"pipe-to-ground":2},"category":"crafting","energy":0.5},"pistol":{"ingredients":{"iron-plate":5,"copper-plate":5},"products":{"pistol":1},"category":"crafting","energy":5},"plastic-bar":{"ingredients":{"coal":1,"petroleum-gas":20},"products":{"plastic-bar":2},"category":"chemistry","energy":1},"poison-capsule":{"ingredients":{"coal":10,"steel-plate":3,"electronic-circuit":3},"products":{"poison-capsule":1},"category":"crafting","energy":8},"power-armor":{"ingredients":{"steel-plate":40,"processing-unit":40,"electric-engine-unit":20},"products":{"power-armor":1},"category":"crafting","energy":20},"power-armor-mk2":{"ingredients":{"processing-unit":60,"electric-engine-unit":40,"low-density-structure":30,"speed-module-2":25,"effectivity-module-2":25},"products":{"power-armor-mk2":1},"category":"crafting","energy":25},"power-switch":{"ingredients":{"iron-plate":5,"copper-cable":5,"electronic-circuit":2},"products":{"power-switch":1},"category":"crafting","energy":2},"processing-unit":{"ingredients":{"electronic-circuit":20,"advanced-circuit":2,"sulfuric-acid":5},"products":{"processing-unit":1},"category":"crafting-with-fluid","energy":10},"production-science-pack":{"ingredients":{"rail":30,"electric-furnace":1,"productivity-module":1},"products":{"production-science-pack":3},"category":"crafting","energy":21},"productivity-module":{"ingredients":{"electronic-circuit":5,"advanced-circuit":5},"products":{"productivity-module":1},"category":"crafting","energy":15},"productivity-module-2":{"ingredients":{"advanced-circuit":5,"processing-unit":5,"productivity-module":4},"products":{"productivity-module-2":1},"category":"crafting","energy":30},"productivity-module-3":{"ingredients":{"advanced-circuit":5,"processing-unit":5,"productivity-module-2":5},"products":{"productivity-module-3":1},"category":"crafting","energy":60},"programmable-speaker":{"ingredients":{"iron-plate":3,"copper-cable":5,"iron-stick":4,"electronic-circuit":4},"products":{"programmable-speaker":1},"category":"crafting","energy":2},"pump":{"ingredients":{"steel-plate":1,"engine-unit":1,"pipe":1},"products":{"pump":1},"category":"crafting","energy":2},"pumpjack":{"ingredients":{"steel-plate":5,"iron-gear-wheel":10,"electronic-circuit":5,"pipe":10},"products":{"pumpjack":1},"category":"crafting","energy":5},"radar":{"ingredients":{"iron-plate":10,"iron-gear-wheel":5,"electronic-circuit":5},"products":{"radar":1},"category":"crafting","energy":0.5},"rail":{"ingredients":{"stone":1,"steel-plate":1,"iron-stick":1},"products":{"rail":2},"category":"crafting","energy":0.5},"rail-chain-signal":{"ingredients":{"iron-plate":5,"electronic-circuit":1},"products":{"rail-chain-signal":1},"category":"crafting","energy":0.5},"rail-signal":{"ingredients":{"iron-plate":5,"electronic-circuit":1},"products":{"rail-signal":1},"category":"crafting","energy":0.5},"red-wire":{"ingredients":{"copper-cable":1,"electronic-circuit":1},"products":{"red-wire":1},"category":"crafting","energy":0.5},"refined-concrete":{"ingredients":{"steel-plate":1,"iron-stick":8,"concrete":20,"water":100},"products":{"refined-concrete":10},"category":"crafting-with-fluid","energy":15},"refined-hazard-concrete":{"ingredients":{"refined-concrete":10},"products":{"refined-hazard-concrete":10},"category":"crafting","energy":0.25},"repair-pack":{"ingredients":{"iron-gear-wheel":2,"electronic-circuit":2},"products":{"repair-pack":1},"category":"crafting","energy":0.5},"roboport":{"ingredients":{"steel-plate":45,"iron-gear-wheel":45,"advanced-circuit":45},"products":{"roboport":1},"category":"crafting","energy":5},"rocket":{"ingredients":{"iron-plate":2,"explosives":1,"electronic-circuit":1},"products":{"rocket":1},"category":"crafting","energy":8},"rocket-control-unit":{"ingredients":{"processing-unit":1,"speed-module":1},"products":{"rocket-control-unit":1},"category":"crafting","energy":30},"rocket-fuel":{"ingredients":{"solid-fuel":10,"light-oil":10},"products":{"rocket-fuel":1},"category":"crafting-with-fluid","energy":30},"rocket-launcher":{"ingredients":{"iron-plate":5,"iron-gear-wheel":5,"electronic-circuit":5},"products":{"rocket-launcher":1},"category":"crafting","energy":10},"rocket-part":{"ingredients":{"rocket-control-unit":10,"low-density-structure":10,"rocket-fuel":10},"products":{"rocket-part":1},"category":"rocket-building","energy":3},"rocket-silo":{"ingredients":{"steel-plate":1000,"processing-unit":200,"electric-engine-unit":200,"pipe":100,"concrete":1000},"products":{"rocket-silo":1},"category":"crafting","energy":30},"satellite":{"ingredients":{"processing-unit":100,"low-density-structure":100,"rocket-fuel":50,"solar-panel":100,"accumulator":100,"radar":5},"products":{"satellite":1},"category":"crafting","energy":5},"shotgun":{"ingredients":{"wood":5,"iron-plate":15,"copper-plate":10,"iron-gear-wheel":5},"products":{"shotgun":1},"category":"crafting","energy":10},"shotgun-shell":{"ingredients":{"iron-plate":2,"copper-plate":2},"products":{"shotgun-shell":1},"category":"crafting","energy":3},"slowdown-capsule":{"ingredients":{"coal":5,"steel-plate":2,"electronic-circuit":2},"products":{"slowdown-capsule":1},"category":"crafting","energy":8},"small-electric-pole":{"ingredients":{"wood":1,"copper-cable":2},"products":{"small-electric-pole":2},"category":"crafting","energy":0.5},"small-lamp":{"ingredients":{"iron-plate":1,"copper-cable":3,"electronic-circuit":1},"products":{"small-lamp":1},"category":"crafting","energy":0.5},"solar-panel":{"ingredients":{"copper-plate":5,"steel-plate":5,"electronic-circuit":15},"products":{"solar-panel":1},"category":"crafting","energy":10},"solar-panel-equipment":{"ingredients":{"steel-plate":5,"advanced-circuit":2,"solar-panel":1},"products":{"solar-panel-equipment":1},"category":"crafting","energy":10},"speed-module":{"ingredients":{"electronic-circuit":5,"advanced-circuit":5},"products":{"speed-module":1},"category":"crafting","energy":15},"speed-module-2":{"ingredients":{"advanced-circuit":5,"processing-unit":5,"speed-module":4},"products":{"speed-module-2":1},"category":"crafting","energy":30},"speed-module-3":{"ingredients":{"advanced-circuit":5,"processing-unit":5,"speed-module-2":5},"products":{"speed-module-3":1},"category":"crafting","energy":60},"spidertron":{"ingredients":{"raw-fish":1,"rocket-control-unit":16,"low-density-structure":150,"effectivity-module-3":2,"rocket-launcher":4,"fusion-reactor-equipment":2,"exoskeleton-equipment":4,"radar":2},"products":{"spidertron":1},"category":"crafting","energy":10},"spidertron-remote":{"ingredients":{"rocket-control-unit":1,"radar":1},"products":{"spidertron-remote":1},"category":"crafting","energy":0.5},"splitter":{"ingredients":{"iron-plate":5,"electronic-circuit":5,"transport-belt":4},"products":{"splitter":1},"category":"crafting","energy":1},"stack-filter-inserter":{"ingredients":{"electronic-circuit":5,"stack-inserter":1},"products":{"stack-filter-inserter":1},"category":"crafting","energy":0.5},"stack-inserter":{"ingredients":{"iron-gear-wheel":15,"electronic-circuit":15,"advanced-circuit":1,"fast-inserter":1},"products":{"stack-inserter":1},"category":"crafting","energy":0.5},"steam-engine":{"ingredients":{"iron-plate":10,"iron-gear-wheel":8,"pipe":5},"products":{"steam-engine":1},"category":"crafting","energy":0.5},"steam-turbine":{"ingredients":{"copper-plate":50,"iron-gear-wheel":50,"pipe":20},"products":{"steam-turbine":1},"category":"crafting","energy":3},"steel-chest":{"ingredients":{"steel-plate":8},"products":{"steel-chest":1},"category":"crafting","energy":0.5},"steel-furnace":{"ingredients":{"steel-plate":6,"stone-brick":10},"products":{"steel-furnace":1},"category":"crafting","energy":3},"steel-plate":{"ingredients":{"iron-plate":5},"products":{"steel-plate":1},"category":"smelting","energy":16},"stone-brick":{"ingredients":{"stone":2},"products":{"stone-brick":1},"category":"smelting","energy":3.20000000000000017763568394002504646778106689453125},"stone-furnace":{"ingredients":{"stone":5},"products":{"stone-furnace":1},"category":"crafting","energy":0.5},"stone-wall":{"ingredients":{"stone-brick":5},"products":{"stone-wall":1},"category":"crafting","energy":0.5},"storage-tank":{"ingredients":{"iron-plate":20,"steel-plate":5},"products":{"storage-tank":1},"category":"crafting","energy":3},"submachine-gun":{"ingredients":{"iron-plate":10,"copper-plate":5,"iron-gear-wheel":10},"products":{"submachine-gun":1},"category":"crafting","energy":10},"substation":{"ingredients":{"copper-plate":5,"steel-plate":10,"advanced-circuit":5},"products":{"substation":1},"category":"crafting","energy":0.5},"sulfur":{"ingredients":{"water":30,"petroleum-gas":30},"products":{"sulfur":2},"category":"chemistry","energy":1},"sulfuric-acid":{"ingredients":{"iron-plate":1,"sulfur":5,"water":100},"products":{"sulfuric-acid":50},"category":"chemistry","energy":1},"tank":{"ingredients":{"steel-plate":50,"iron-gear-wheel":15,"advanced-circuit":10,"engine-unit":32},"products":{"tank":1},"category":"crafting","energy":5},"train-stop":{"ingredients":{"iron-plate":6,"steel-plate":3,"iron-stick":6,"electronic-circuit":5},"products":{"train-stop":1},"category":"crafting","energy":0.5},"transport-belt":{"ingredients":{"iron-plate":1,"iron-gear-wheel":1},"products":{"transport-belt":2},"category":"crafting","energy":0.5},"underground-belt":{"ingredients":{"iron-plate":10,"transport-belt":5},"products":{"underground-belt":2},"category":"crafting","energy":1},"uranium-cannon-shell":{"ingredients":{"uranium-238":1,"cannon-shell":1},"products":{"uranium-cannon-shell":1},"category":"crafting","energy":12},"uranium-fuel-cell":{"ingredients":{"iron-plate":10,"uranium-235":1,"uranium-238":19},"products":{"uranium-fuel-cell":10},"category":"crafting","energy":10},"uranium-rounds-magazine":{"ingredients":{"uranium-238":1,"piercing-rounds-magazine":1},"products":{"uranium-rounds-magazine":1},"category":"crafting","energy":10},"utility-science-pack":{"ingredients":{"processing-unit":2,"flying-robot-frame":1,"low-density-structure":3},"products":{"utility-science-pack":3},"category":"crafting","energy":21},"wooden-chest":{"ingredients":{"wood":2},"products":{"wooden-chest":1},"category":"crafting","energy":0.5},"basic-oil-processing":{"ingredients":{"crude-oil":100},"products":{"petroleum-gas":45},"category":"oil-processing","energy":5},"advanced-oil-processing":{"ingredients":{"water":50,"crude-oil":100},"products":{"heavy-oil":25,"light-oil":45,"petroleum-gas":55},"category":"oil-processing","energy":5},"coal-liquefaction":{"ingredients":{"coal":10,"heavy-oil":25,"steam":50},"products":{"heavy-oil":90,"light-oil":20,"petroleum-gas":10},"category":"oil-processing","energy":5},"fill-crude-oil-barrel":{"ingredients":{"empty-barrel":1,"crude-oil":50},"products":{"crude-oil-barrel":1},"category":"crafting-with-fluid","energy":0.2},"fill-heavy-oil-barrel":{"ingredients":{"empty-barrel":1,"heavy-oil":50},"products":{"heavy-oil-barrel":1},"category":"crafting-with-fluid","energy":0.2},"fill-light-oil-barrel":{"ingredients":{"empty-barrel":1,"light-oil":50},"products":{"light-oil-barrel":1},"category":"crafting-with-fluid","energy":0.2},"fill-lubricant-barrel":{"ingredients":{"empty-barrel":1,"lubricant":50},"products":{"lubricant-barrel":1},"category":"crafting-with-fluid","energy":0.2},"fill-petroleum-gas-barrel":{"ingredients":{"empty-barrel":1,"petroleum-gas":50},"products":{"petroleum-gas-barrel":1},"category":"crafting-with-fluid","energy":0.2},"fill-sulfuric-acid-barrel":{"ingredients":{"empty-barrel":1,"sulfuric-acid":50},"products":{"sulfuric-acid-barrel":1},"category":"crafting-with-fluid","energy":0.2},"fill-water-barrel":{"ingredients":{"empty-barrel":1,"water":50},"products":{"water-barrel":1},"category":"crafting-with-fluid","energy":0.2},"heavy-oil-cracking":{"ingredients":{"water":30,"heavy-oil":40},"products":{"light-oil":30},"category":"chemistry","energy":2},"light-oil-cracking":{"ingredients":{"water":30,"light-oil":30},"products":{"petroleum-gas":20},"category":"chemistry","energy":2},"solid-fuel-from-light-oil":{"ingredients":{"light-oil":10},"products":{"solid-fuel":1},"category":"chemistry","energy":2},"solid-fuel-from-petroleum-gas":{"ingredients":{"petroleum-gas":20},"products":{"solid-fuel":1},"category":"chemistry","energy":2},"solid-fuel-from-heavy-oil":{"ingredients":{"heavy-oil":20},"products":{"solid-fuel":1},"category":"chemistry","energy":2},"empty-crude-oil-barrel":{"ingredients":{"crude-oil-barrel":1},"products":{"empty-barrel":1,"crude-oil":50},"category":"crafting-with-fluid","energy":0.2},"empty-heavy-oil-barrel":{"ingredients":{"heavy-oil-barrel":1},"products":{"empty-barrel":1,"heavy-oil":50},"category":"crafting-with-fluid","energy":0.2},"empty-light-oil-barrel":{"ingredients":{"light-oil-barrel":1},"products":{"empty-barrel":1,"light-oil":50},"category":"crafting-with-fluid","energy":0.2},"empty-lubricant-barrel":{"ingredients":{"lubricant-barrel":1},"products":{"empty-barrel":1,"lubricant":50},"category":"crafting-with-fluid","energy":0.2},"empty-petroleum-gas-barrel":{"ingredients":{"petroleum-gas-barrel":1},"products":{"empty-barrel":1,"petroleum-gas":50},"category":"crafting-with-fluid","energy":0.2},"empty-sulfuric-acid-barrel":{"ingredients":{"sulfuric-acid-barrel":1},"products":{"empty-barrel":1,"sulfuric-acid":50},"category":"crafting-with-fluid","energy":0.2},"empty-water-barrel":{"ingredients":{"water-barrel":1},"products":{"empty-barrel":1,"water":50},"category":"crafting-with-fluid","energy":0.2},"uranium-processing":{"ingredients":{"uranium-ore":10},"products":{"uranium-235":1,"uranium-238":1},"category":"centrifuging","energy":12},"nuclear-fuel-reprocessing":{"ingredients":{"used-up-uranium-fuel-cell":5},"products":{"uranium-238":3},"category":"centrifuging","energy":60},"kovarex-enrichment-process":{"ingredients":{"uranium-235":40,"uranium-238":5},"products":{"uranium-235":41,"uranium-238":2},"category":"centrifuging","energy":60}} \ No newline at end of file +{"wooden-chest":{"ingredients":{"wood":2},"products":{"wooden-chest":1},"category":"crafting","energy":0.5},"iron-chest":{"ingredients":{"iron-plate":8},"products":{"iron-chest":1},"category":"crafting","energy":0.5},"steel-chest":{"ingredients":{"steel-plate":8},"products":{"steel-chest":1},"category":"crafting","energy":0.5},"storage-tank":{"ingredients":{"iron-plate":20,"steel-plate":5},"products":{"storage-tank":1},"category":"crafting","energy":3},"transport-belt":{"ingredients":{"iron-plate":1,"iron-gear-wheel":1},"products":{"transport-belt":2},"category":"crafting","energy":0.5},"fast-transport-belt":{"ingredients":{"iron-gear-wheel":5,"transport-belt":1},"products":{"fast-transport-belt":1},"category":"crafting","energy":0.5},"express-transport-belt":{"ingredients":{"iron-gear-wheel":10,"fast-transport-belt":1,"lubricant":20},"products":{"express-transport-belt":1},"category":"crafting-with-fluid","energy":0.5},"underground-belt":{"ingredients":{"iron-plate":10,"transport-belt":5},"products":{"underground-belt":2},"category":"crafting","energy":1},"fast-underground-belt":{"ingredients":{"iron-gear-wheel":40,"underground-belt":2},"products":{"fast-underground-belt":2},"category":"crafting","energy":2},"express-underground-belt":{"ingredients":{"iron-gear-wheel":80,"fast-underground-belt":2,"lubricant":40},"products":{"express-underground-belt":2},"category":"crafting-with-fluid","energy":2},"splitter":{"ingredients":{"iron-plate":5,"electronic-circuit":5,"transport-belt":4},"products":{"splitter":1},"category":"crafting","energy":1},"fast-splitter":{"ingredients":{"iron-gear-wheel":10,"electronic-circuit":10,"splitter":1},"products":{"fast-splitter":1},"category":"crafting","energy":2},"express-splitter":{"ingredients":{"iron-gear-wheel":10,"advanced-circuit":10,"fast-splitter":1,"lubricant":80},"products":{"express-splitter":1},"category":"crafting-with-fluid","energy":2},"burner-inserter":{"ingredients":{"iron-plate":1,"iron-gear-wheel":1},"products":{"burner-inserter":1},"category":"crafting","energy":0.5},"inserter":{"ingredients":{"iron-plate":1,"iron-gear-wheel":1,"electronic-circuit":1},"products":{"inserter":1},"category":"crafting","energy":0.5},"long-handed-inserter":{"ingredients":{"iron-plate":1,"iron-gear-wheel":1,"inserter":1},"products":{"long-handed-inserter":1},"category":"crafting","energy":0.5},"fast-inserter":{"ingredients":{"iron-plate":2,"electronic-circuit":2,"inserter":1},"products":{"fast-inserter":1},"category":"crafting","energy":0.5},"bulk-inserter":{"ingredients":{"iron-gear-wheel":15,"electronic-circuit":15,"advanced-circuit":1,"fast-inserter":1},"products":{"bulk-inserter":1},"category":"crafting","energy":0.5},"small-electric-pole":{"ingredients":{"wood":1,"copper-cable":2},"products":{"small-electric-pole":2},"category":"crafting","energy":0.5},"medium-electric-pole":{"ingredients":{"steel-plate":2,"iron-stick":4,"copper-cable":2},"products":{"medium-electric-pole":1},"category":"crafting","energy":0.5},"big-electric-pole":{"ingredients":{"steel-plate":5,"iron-stick":8,"copper-cable":4},"products":{"big-electric-pole":1},"category":"crafting","energy":0.5},"substation":{"ingredients":{"steel-plate":10,"copper-cable":6,"advanced-circuit":5},"products":{"substation":1},"category":"crafting","energy":0.5},"pipe":{"ingredients":{"iron-plate":1},"products":{"pipe":1},"category":"crafting","energy":0.5},"pipe-to-ground":{"ingredients":{"iron-plate":5,"pipe":10},"products":{"pipe-to-ground":2},"category":"crafting","energy":0.5},"pump":{"ingredients":{"steel-plate":1,"engine-unit":1,"pipe":1},"products":{"pump":1},"category":"crafting","energy":2},"rail":{"ingredients":{"stone":1,"steel-plate":1,"iron-stick":1},"products":{"rail":2},"category":"crafting","energy":0.5},"train-stop":{"ingredients":{"iron-plate":6,"steel-plate":3,"iron-stick":6,"electronic-circuit":5},"products":{"train-stop":1},"category":"crafting","energy":0.5},"rail-signal":{"ingredients":{"iron-plate":5,"electronic-circuit":1},"products":{"rail-signal":1},"category":"crafting","energy":0.5},"rail-chain-signal":{"ingredients":{"iron-plate":5,"electronic-circuit":1},"products":{"rail-chain-signal":1},"category":"crafting","energy":0.5},"locomotive":{"ingredients":{"steel-plate":30,"electronic-circuit":10,"engine-unit":20},"products":{"locomotive":1},"category":"crafting","energy":4},"cargo-wagon":{"ingredients":{"iron-plate":20,"steel-plate":20,"iron-gear-wheel":10},"products":{"cargo-wagon":1},"category":"crafting","energy":1},"fluid-wagon":{"ingredients":{"steel-plate":16,"iron-gear-wheel":10,"storage-tank":1,"pipe":8},"products":{"fluid-wagon":1},"category":"crafting","energy":1.5},"artillery-wagon":{"ingredients":{"steel-plate":40,"iron-gear-wheel":10,"advanced-circuit":20,"engine-unit":64,"pipe":16},"products":{"artillery-wagon":1},"category":"crafting","energy":4},"car":{"ingredients":{"iron-plate":20,"steel-plate":5,"engine-unit":8},"products":{"car":1},"category":"crafting","energy":2},"tank":{"ingredients":{"steel-plate":50,"iron-gear-wheel":15,"advanced-circuit":10,"engine-unit":32},"products":{"tank":1},"category":"crafting","energy":5},"spidertron":{"ingredients":{"raw-fish":1,"processing-unit":16,"low-density-structure":150,"efficiency-module-3":2,"rocket-launcher":4,"fission-reactor-equipment":2,"exoskeleton-equipment":4,"radar":2},"products":{"spidertron":1},"category":"crafting","energy":10},"logistic-robot":{"ingredients":{"advanced-circuit":2,"flying-robot-frame":1},"products":{"logistic-robot":1},"category":"crafting","energy":0.5},"construction-robot":{"ingredients":{"electronic-circuit":2,"flying-robot-frame":1},"products":{"construction-robot":1},"category":"crafting","energy":0.5},"active-provider-chest":{"ingredients":{"electronic-circuit":3,"advanced-circuit":1,"steel-chest":1},"products":{"active-provider-chest":1},"category":"crafting","energy":0.5},"passive-provider-chest":{"ingredients":{"electronic-circuit":3,"advanced-circuit":1,"steel-chest":1},"products":{"passive-provider-chest":1},"category":"crafting","energy":0.5},"storage-chest":{"ingredients":{"electronic-circuit":3,"advanced-circuit":1,"steel-chest":1},"products":{"storage-chest":1},"category":"crafting","energy":0.5},"buffer-chest":{"ingredients":{"electronic-circuit":3,"advanced-circuit":1,"steel-chest":1},"products":{"buffer-chest":1},"category":"crafting","energy":0.5},"requester-chest":{"ingredients":{"electronic-circuit":3,"advanced-circuit":1,"steel-chest":1},"products":{"requester-chest":1},"category":"crafting","energy":0.5},"roboport":{"ingredients":{"steel-plate":45,"iron-gear-wheel":45,"advanced-circuit":45},"products":{"roboport":1},"category":"crafting","energy":5},"small-lamp":{"ingredients":{"iron-plate":1,"copper-cable":3,"electronic-circuit":1},"products":{"small-lamp":1},"category":"crafting","energy":0.5},"arithmetic-combinator":{"ingredients":{"copper-cable":5,"electronic-circuit":5},"products":{"arithmetic-combinator":1},"category":"crafting","energy":0.5},"decider-combinator":{"ingredients":{"copper-cable":5,"electronic-circuit":5},"products":{"decider-combinator":1},"category":"crafting","energy":0.5},"selector-combinator":{"ingredients":{"advanced-circuit":2,"decider-combinator":5},"products":{"selector-combinator":1},"category":"crafting","energy":0.5},"constant-combinator":{"ingredients":{"copper-cable":5,"electronic-circuit":2},"products":{"constant-combinator":1},"category":"crafting","energy":0.5},"power-switch":{"ingredients":{"iron-plate":5,"copper-cable":5,"electronic-circuit":2},"products":{"power-switch":1},"category":"crafting","energy":2},"programmable-speaker":{"ingredients":{"iron-plate":3,"iron-stick":4,"copper-cable":5,"electronic-circuit":4},"products":{"programmable-speaker":1},"category":"crafting","energy":2},"display-panel":{"ingredients":{"iron-plate":1,"electronic-circuit":1},"products":{"display-panel":1},"category":"crafting","energy":0.5},"stone-brick":{"ingredients":{"stone":2},"products":{"stone-brick":1},"category":"smelting","energy":3.20000000000000017763568394002504646778106689453125},"concrete":{"ingredients":{"iron-ore":1,"stone-brick":5,"water":100},"products":{"concrete":10},"category":"crafting-with-fluid","energy":10},"hazard-concrete":{"ingredients":{"concrete":10},"products":{"hazard-concrete":10},"category":"crafting","energy":0.25},"refined-concrete":{"ingredients":{"steel-plate":1,"iron-stick":8,"concrete":20,"water":100},"products":{"refined-concrete":10},"category":"crafting-with-fluid","energy":15},"refined-hazard-concrete":{"ingredients":{"refined-concrete":10},"products":{"refined-hazard-concrete":10},"category":"crafting","energy":0.25},"landfill":{"ingredients":{"stone":50},"products":{"landfill":1},"category":"crafting","energy":0.5},"cliff-explosives":{"ingredients":{"explosives":10,"barrel":1,"grenade":1},"products":{"cliff-explosives":1},"category":"crafting","energy":8},"repair-pack":{"ingredients":{"iron-gear-wheel":2,"electronic-circuit":2},"products":{"repair-pack":1},"category":"crafting","energy":0.5},"boiler":{"ingredients":{"pipe":4,"stone-furnace":1},"products":{"boiler":1},"category":"crafting","energy":0.5},"steam-engine":{"ingredients":{"iron-plate":10,"iron-gear-wheel":8,"pipe":5},"products":{"steam-engine":1},"category":"crafting","energy":0.5},"solar-panel":{"ingredients":{"copper-plate":5,"steel-plate":5,"electronic-circuit":15},"products":{"solar-panel":1},"category":"crafting","energy":10},"accumulator":{"ingredients":{"iron-plate":2,"battery":5},"products":{"accumulator":1},"category":"crafting","energy":10},"nuclear-reactor":{"ingredients":{"copper-plate":500,"steel-plate":500,"advanced-circuit":500,"concrete":500},"products":{"nuclear-reactor":1},"category":"crafting","energy":8},"heat-pipe":{"ingredients":{"copper-plate":20,"steel-plate":10},"products":{"heat-pipe":1},"category":"crafting","energy":1},"heat-exchanger":{"ingredients":{"copper-plate":100,"steel-plate":10,"pipe":10},"products":{"heat-exchanger":1},"category":"crafting","energy":3},"steam-turbine":{"ingredients":{"copper-plate":50,"iron-gear-wheel":50,"pipe":20},"products":{"steam-turbine":1},"category":"crafting","energy":3},"burner-mining-drill":{"ingredients":{"iron-plate":3,"iron-gear-wheel":3,"stone-furnace":1},"products":{"burner-mining-drill":1},"category":"crafting","energy":2},"electric-mining-drill":{"ingredients":{"iron-plate":10,"iron-gear-wheel":5,"electronic-circuit":3},"products":{"electric-mining-drill":1},"category":"crafting","energy":2},"offshore-pump":{"ingredients":{"iron-gear-wheel":2,"pipe":3},"products":{"offshore-pump":1},"category":"crafting","energy":0.5},"pumpjack":{"ingredients":{"steel-plate":5,"iron-gear-wheel":10,"electronic-circuit":5,"pipe":10},"products":{"pumpjack":1},"category":"crafting","energy":5},"stone-furnace":{"ingredients":{"stone":5},"products":{"stone-furnace":1},"category":"crafting","energy":0.5},"steel-furnace":{"ingredients":{"steel-plate":6,"stone-brick":10},"products":{"steel-furnace":1},"category":"crafting","energy":3},"electric-furnace":{"ingredients":{"steel-plate":10,"advanced-circuit":5,"stone-brick":10},"products":{"electric-furnace":1},"category":"crafting","energy":5},"assembling-machine-1":{"ingredients":{"iron-plate":9,"iron-gear-wheel":5,"electronic-circuit":3},"products":{"assembling-machine-1":1},"category":"crafting","energy":0.5},"assembling-machine-2":{"ingredients":{"steel-plate":2,"iron-gear-wheel":5,"electronic-circuit":3,"assembling-machine-1":1},"products":{"assembling-machine-2":1},"category":"crafting","energy":0.5},"assembling-machine-3":{"ingredients":{"assembling-machine-2":2,"speed-module":4},"products":{"assembling-machine-3":1},"category":"crafting","energy":0.5},"oil-refinery":{"ingredients":{"steel-plate":15,"iron-gear-wheel":10,"electronic-circuit":10,"pipe":10,"stone-brick":10},"products":{"oil-refinery":1},"category":"crafting","energy":8},"chemical-plant":{"ingredients":{"steel-plate":5,"iron-gear-wheel":5,"electronic-circuit":5,"pipe":5},"products":{"chemical-plant":1},"category":"crafting","energy":5},"centrifuge":{"ingredients":{"steel-plate":50,"iron-gear-wheel":100,"advanced-circuit":100,"concrete":100},"products":{"centrifuge":1},"category":"crafting","energy":4},"lab":{"ingredients":{"iron-gear-wheel":10,"electronic-circuit":10,"transport-belt":4},"products":{"lab":1},"category":"crafting","energy":2},"beacon":{"ingredients":{"steel-plate":10,"copper-cable":10,"electronic-circuit":20,"advanced-circuit":20},"products":{"beacon":1},"category":"crafting","energy":15},"speed-module":{"ingredients":{"electronic-circuit":5,"advanced-circuit":5},"products":{"speed-module":1},"category":"crafting","energy":15},"speed-module-2":{"ingredients":{"advanced-circuit":5,"processing-unit":5,"speed-module":4},"products":{"speed-module-2":1},"category":"crafting","energy":30},"speed-module-3":{"ingredients":{"advanced-circuit":5,"processing-unit":5,"speed-module-2":4},"products":{"speed-module-3":1},"category":"crafting","energy":60},"efficiency-module":{"ingredients":{"electronic-circuit":5,"advanced-circuit":5},"products":{"efficiency-module":1},"category":"crafting","energy":15},"efficiency-module-2":{"ingredients":{"advanced-circuit":5,"processing-unit":5,"efficiency-module":4},"products":{"efficiency-module-2":1},"category":"crafting","energy":30},"efficiency-module-3":{"ingredients":{"advanced-circuit":5,"processing-unit":5,"efficiency-module-2":4},"products":{"efficiency-module-3":1},"category":"crafting","energy":60},"productivity-module":{"ingredients":{"electronic-circuit":5,"advanced-circuit":5},"products":{"productivity-module":1},"category":"crafting","energy":15},"productivity-module-2":{"ingredients":{"advanced-circuit":5,"processing-unit":5,"productivity-module":4},"products":{"productivity-module-2":1},"category":"crafting","energy":30},"productivity-module-3":{"ingredients":{"advanced-circuit":5,"processing-unit":5,"productivity-module-2":4},"products":{"productivity-module-3":1},"category":"crafting","energy":60},"rocket-silo":{"ingredients":{"steel-plate":1000,"processing-unit":200,"electric-engine-unit":200,"pipe":100,"concrete":1000},"products":{"rocket-silo":1},"category":"crafting","energy":30},"cargo-landing-pad":{"ingredients":{"steel-plate":25,"processing-unit":10,"concrete":200},"products":{"cargo-landing-pad":1},"category":"crafting","energy":30},"satellite":{"ingredients":{"processing-unit":100,"low-density-structure":100,"rocket-fuel":50,"solar-panel":100,"accumulator":100,"radar":5},"products":{"satellite":1},"category":"crafting","energy":5},"basic-oil-processing":{"ingredients":{"crude-oil":100},"products":{"petroleum-gas":45},"category":"oil-processing","energy":5},"advanced-oil-processing":{"ingredients":{"water":50,"crude-oil":100},"products":{"heavy-oil":25,"light-oil":45,"petroleum-gas":55},"category":"oil-processing","energy":5},"coal-liquefaction":{"ingredients":{"coal":10,"heavy-oil":25,"steam":50},"products":{"heavy-oil":90,"light-oil":20,"petroleum-gas":10},"category":"oil-processing","energy":5},"heavy-oil-cracking":{"ingredients":{"water":30,"heavy-oil":40},"products":{"light-oil":30},"category":"chemistry","energy":2},"light-oil-cracking":{"ingredients":{"water":30,"light-oil":30},"products":{"petroleum-gas":20},"category":"chemistry","energy":2},"solid-fuel-from-petroleum-gas":{"ingredients":{"petroleum-gas":20},"products":{"solid-fuel":1},"category":"chemistry","energy":1},"solid-fuel-from-light-oil":{"ingredients":{"light-oil":10},"products":{"solid-fuel":1},"category":"chemistry","energy":1},"solid-fuel-from-heavy-oil":{"ingredients":{"heavy-oil":20},"products":{"solid-fuel":1},"category":"chemistry","energy":1},"lubricant":{"ingredients":{"heavy-oil":10},"products":{"lubricant":10},"category":"chemistry","energy":1},"sulfuric-acid":{"ingredients":{"iron-plate":1,"sulfur":5,"water":100},"products":{"sulfuric-acid":50},"category":"chemistry","energy":1},"iron-plate":{"ingredients":{"iron-ore":1},"products":{"iron-plate":1},"category":"smelting","energy":3.20000000000000017763568394002504646778106689453125},"copper-plate":{"ingredients":{"copper-ore":1},"products":{"copper-plate":1},"category":"smelting","energy":3.20000000000000017763568394002504646778106689453125},"steel-plate":{"ingredients":{"iron-plate":5},"products":{"steel-plate":1},"category":"smelting","energy":16},"plastic-bar":{"ingredients":{"coal":1,"petroleum-gas":20},"products":{"plastic-bar":2},"category":"chemistry","energy":1},"sulfur":{"ingredients":{"water":30,"petroleum-gas":30},"products":{"sulfur":2},"category":"chemistry","energy":1},"battery":{"ingredients":{"iron-plate":1,"copper-plate":1,"sulfuric-acid":20},"products":{"battery":1},"category":"chemistry","energy":4},"explosives":{"ingredients":{"coal":1,"sulfur":1,"water":10},"products":{"explosives":2},"category":"chemistry","energy":4},"water-barrel":{"ingredients":{"barrel":1,"water":50},"products":{"water-barrel":1},"category":"crafting-with-fluid","energy":0.2},"crude-oil-barrel":{"ingredients":{"barrel":1,"crude-oil":50},"products":{"crude-oil-barrel":1},"category":"crafting-with-fluid","energy":0.2},"petroleum-gas-barrel":{"ingredients":{"barrel":1,"petroleum-gas":50},"products":{"petroleum-gas-barrel":1},"category":"crafting-with-fluid","energy":0.2},"light-oil-barrel":{"ingredients":{"barrel":1,"light-oil":50},"products":{"light-oil-barrel":1},"category":"crafting-with-fluid","energy":0.2},"heavy-oil-barrel":{"ingredients":{"barrel":1,"heavy-oil":50},"products":{"heavy-oil-barrel":1},"category":"crafting-with-fluid","energy":0.2},"lubricant-barrel":{"ingredients":{"barrel":1,"lubricant":50},"products":{"lubricant-barrel":1},"category":"crafting-with-fluid","energy":0.2},"sulfuric-acid-barrel":{"ingredients":{"barrel":1,"sulfuric-acid":50},"products":{"sulfuric-acid-barrel":1},"category":"crafting-with-fluid","energy":0.2},"empty-water-barrel":{"ingredients":{"water-barrel":1},"products":{"barrel":1,"water":50},"category":"crafting-with-fluid","energy":0.2},"empty-crude-oil-barrel":{"ingredients":{"crude-oil-barrel":1},"products":{"barrel":1,"crude-oil":50},"category":"crafting-with-fluid","energy":0.2},"empty-petroleum-gas-barrel":{"ingredients":{"petroleum-gas-barrel":1},"products":{"barrel":1,"petroleum-gas":50},"category":"crafting-with-fluid","energy":0.2},"empty-light-oil-barrel":{"ingredients":{"light-oil-barrel":1},"products":{"barrel":1,"light-oil":50},"category":"crafting-with-fluid","energy":0.2},"empty-heavy-oil-barrel":{"ingredients":{"heavy-oil-barrel":1},"products":{"barrel":1,"heavy-oil":50},"category":"crafting-with-fluid","energy":0.2},"empty-lubricant-barrel":{"ingredients":{"lubricant-barrel":1},"products":{"barrel":1,"lubricant":50},"category":"crafting-with-fluid","energy":0.2},"empty-sulfuric-acid-barrel":{"ingredients":{"sulfuric-acid-barrel":1},"products":{"barrel":1,"sulfuric-acid":50},"category":"crafting-with-fluid","energy":0.2},"iron-gear-wheel":{"ingredients":{"iron-plate":2},"products":{"iron-gear-wheel":1},"category":"crafting","energy":0.5},"iron-stick":{"ingredients":{"iron-plate":1},"products":{"iron-stick":2},"category":"crafting","energy":0.5},"copper-cable":{"ingredients":{"copper-plate":1},"products":{"copper-cable":2},"category":"crafting","energy":0.5},"barrel":{"ingredients":{"steel-plate":1},"products":{"barrel":1},"category":"crafting","energy":1},"electronic-circuit":{"ingredients":{"iron-plate":1,"copper-cable":3},"products":{"electronic-circuit":1},"category":"crafting","energy":0.5},"advanced-circuit":{"ingredients":{"plastic-bar":2,"copper-cable":4,"electronic-circuit":2},"products":{"advanced-circuit":1},"category":"crafting","energy":6},"processing-unit":{"ingredients":{"electronic-circuit":20,"advanced-circuit":2,"sulfuric-acid":5},"products":{"processing-unit":1},"category":"crafting-with-fluid","energy":10},"engine-unit":{"ingredients":{"steel-plate":1,"iron-gear-wheel":1,"pipe":2},"products":{"engine-unit":1},"category":"advanced-crafting","energy":10},"electric-engine-unit":{"ingredients":{"electronic-circuit":2,"engine-unit":1,"lubricant":15},"products":{"electric-engine-unit":1},"category":"crafting-with-fluid","energy":10},"flying-robot-frame":{"ingredients":{"steel-plate":1,"battery":2,"electronic-circuit":3,"electric-engine-unit":1},"products":{"flying-robot-frame":1},"category":"crafting","energy":20},"low-density-structure":{"ingredients":{"copper-plate":20,"steel-plate":2,"plastic-bar":5},"products":{"low-density-structure":1},"category":"crafting","energy":15},"rocket-fuel":{"ingredients":{"solid-fuel":10,"light-oil":10},"products":{"rocket-fuel":1},"category":"crafting-with-fluid","energy":15},"rocket-part":{"ingredients":{"processing-unit":10,"low-density-structure":10,"rocket-fuel":10},"products":{"rocket-part":1},"category":"rocket-building","energy":3},"uranium-processing":{"ingredients":{"uranium-ore":10},"products":{"uranium-235":1,"uranium-238":1},"category":"centrifuging","energy":12},"uranium-fuel-cell":{"ingredients":{"iron-plate":10,"uranium-235":1,"uranium-238":19},"products":{"uranium-fuel-cell":10},"category":"crafting","energy":10},"nuclear-fuel-reprocessing":{"ingredients":{"depleted-uranium-fuel-cell":5},"products":{"uranium-238":3},"category":"centrifuging","energy":60},"kovarex-enrichment-process":{"ingredients":{"uranium-235":40,"uranium-238":5},"products":{"uranium-235":41,"uranium-238":2},"category":"centrifuging","energy":60},"nuclear-fuel":{"ingredients":{"rocket-fuel":1,"uranium-235":1},"products":{"nuclear-fuel":1},"category":"centrifuging","energy":90},"automation-science-pack":{"ingredients":{"copper-plate":1,"iron-gear-wheel":1},"products":{"automation-science-pack":1},"category":"crafting","energy":5},"logistic-science-pack":{"ingredients":{"transport-belt":1,"inserter":1},"products":{"logistic-science-pack":1},"category":"crafting","energy":6},"military-science-pack":{"ingredients":{"piercing-rounds-magazine":1,"grenade":1,"stone-wall":2},"products":{"military-science-pack":2},"category":"crafting","energy":10},"chemical-science-pack":{"ingredients":{"sulfur":1,"advanced-circuit":3,"engine-unit":2},"products":{"chemical-science-pack":2},"category":"crafting","energy":24},"production-science-pack":{"ingredients":{"rail":30,"electric-furnace":1,"productivity-module":1},"products":{"production-science-pack":3},"category":"crafting","energy":21},"utility-science-pack":{"ingredients":{"processing-unit":2,"flying-robot-frame":1,"low-density-structure":3},"products":{"utility-science-pack":3},"category":"crafting","energy":21},"submachine-gun":{"ingredients":{"iron-plate":10,"copper-plate":5,"iron-gear-wheel":10},"products":{"submachine-gun":1},"category":"crafting","energy":10},"shotgun":{"ingredients":{"wood":5,"iron-plate":15,"copper-plate":10,"iron-gear-wheel":5},"products":{"shotgun":1},"category":"crafting","energy":10},"combat-shotgun":{"ingredients":{"wood":10,"copper-plate":10,"steel-plate":15,"iron-gear-wheel":5},"products":{"combat-shotgun":1},"category":"crafting","energy":10},"rocket-launcher":{"ingredients":{"iron-plate":5,"iron-gear-wheel":5,"electronic-circuit":5},"products":{"rocket-launcher":1},"category":"crafting","energy":10},"flamethrower":{"ingredients":{"steel-plate":5,"iron-gear-wheel":10},"products":{"flamethrower":1},"category":"crafting","energy":10},"firearm-magazine":{"ingredients":{"iron-plate":4},"products":{"firearm-magazine":1},"category":"crafting","energy":1},"piercing-rounds-magazine":{"ingredients":{"copper-plate":5,"steel-plate":1,"firearm-magazine":1},"products":{"piercing-rounds-magazine":1},"category":"crafting","energy":3},"uranium-rounds-magazine":{"ingredients":{"uranium-238":1,"piercing-rounds-magazine":1},"products":{"uranium-rounds-magazine":1},"category":"crafting","energy":10},"shotgun-shell":{"ingredients":{"iron-plate":2,"copper-plate":2},"products":{"shotgun-shell":1},"category":"crafting","energy":3},"piercing-shotgun-shell":{"ingredients":{"copper-plate":5,"steel-plate":2,"shotgun-shell":2},"products":{"piercing-shotgun-shell":1},"category":"crafting","energy":8},"cannon-shell":{"ingredients":{"steel-plate":2,"plastic-bar":2,"explosives":1},"products":{"cannon-shell":1},"category":"crafting","energy":8},"explosive-cannon-shell":{"ingredients":{"steel-plate":2,"plastic-bar":2,"explosives":2},"products":{"explosive-cannon-shell":1},"category":"crafting","energy":8},"uranium-cannon-shell":{"ingredients":{"uranium-238":1,"cannon-shell":1},"products":{"uranium-cannon-shell":1},"category":"crafting","energy":12},"explosive-uranium-cannon-shell":{"ingredients":{"uranium-238":1,"explosive-cannon-shell":1},"products":{"explosive-uranium-cannon-shell":1},"category":"crafting","energy":12},"artillery-shell":{"ingredients":{"explosives":8,"explosive-cannon-shell":4,"radar":1},"products":{"artillery-shell":1},"category":"crafting","energy":15},"rocket":{"ingredients":{"iron-plate":2,"explosives":1},"products":{"rocket":1},"category":"crafting","energy":4},"explosive-rocket":{"ingredients":{"explosives":2,"rocket":1},"products":{"explosive-rocket":1},"category":"crafting","energy":8},"atomic-bomb":{"ingredients":{"explosives":10,"processing-unit":10,"uranium-235":30},"products":{"atomic-bomb":1},"category":"crafting","energy":50},"flamethrower-ammo":{"ingredients":{"steel-plate":5,"crude-oil":100},"products":{"flamethrower-ammo":1},"category":"chemistry","energy":6},"grenade":{"ingredients":{"coal":10,"iron-plate":5},"products":{"grenade":1},"category":"crafting","energy":8},"cluster-grenade":{"ingredients":{"steel-plate":5,"explosives":5,"grenade":7},"products":{"cluster-grenade":1},"category":"crafting","energy":8},"poison-capsule":{"ingredients":{"coal":10,"steel-plate":3,"electronic-circuit":3},"products":{"poison-capsule":1},"category":"crafting","energy":8},"slowdown-capsule":{"ingredients":{"coal":5,"steel-plate":2,"electronic-circuit":2},"products":{"slowdown-capsule":1},"category":"crafting","energy":8},"defender-capsule":{"ingredients":{"iron-gear-wheel":3,"electronic-circuit":3,"piercing-rounds-magazine":3},"products":{"defender-capsule":1},"category":"crafting","energy":8},"distractor-capsule":{"ingredients":{"advanced-circuit":3,"defender-capsule":4},"products":{"distractor-capsule":1},"category":"crafting","energy":15},"destroyer-capsule":{"ingredients":{"speed-module":1,"distractor-capsule":4},"products":{"destroyer-capsule":1},"category":"crafting","energy":15},"light-armor":{"ingredients":{"iron-plate":40},"products":{"light-armor":1},"category":"crafting","energy":3},"heavy-armor":{"ingredients":{"copper-plate":100,"steel-plate":50},"products":{"heavy-armor":1},"category":"crafting","energy":8},"modular-armor":{"ingredients":{"steel-plate":50,"advanced-circuit":30},"products":{"modular-armor":1},"category":"crafting","energy":15},"power-armor":{"ingredients":{"steel-plate":40,"processing-unit":40,"electric-engine-unit":20},"products":{"power-armor":1},"category":"crafting","energy":20},"power-armor-mk2":{"ingredients":{"processing-unit":60,"electric-engine-unit":40,"low-density-structure":30,"speed-module-2":25,"efficiency-module-2":25},"products":{"power-armor-mk2":1},"category":"crafting","energy":25},"solar-panel-equipment":{"ingredients":{"steel-plate":5,"advanced-circuit":2,"solar-panel":1},"products":{"solar-panel-equipment":1},"category":"crafting","energy":10},"fission-reactor-equipment":{"ingredients":{"processing-unit":200,"low-density-structure":50,"uranium-fuel-cell":4},"products":{"fission-reactor-equipment":1},"category":"crafting","energy":10},"battery-equipment":{"ingredients":{"steel-plate":10,"battery":5},"products":{"battery-equipment":1},"category":"crafting","energy":10},"battery-mk2-equipment":{"ingredients":{"processing-unit":15,"low-density-structure":5,"battery-equipment":10},"products":{"battery-mk2-equipment":1},"category":"crafting","energy":10},"belt-immunity-equipment":{"ingredients":{"steel-plate":10,"advanced-circuit":5},"products":{"belt-immunity-equipment":1},"category":"crafting","energy":10},"exoskeleton-equipment":{"ingredients":{"steel-plate":20,"processing-unit":10,"electric-engine-unit":30},"products":{"exoskeleton-equipment":1},"category":"crafting","energy":10},"personal-roboport-equipment":{"ingredients":{"steel-plate":20,"battery":45,"iron-gear-wheel":40,"advanced-circuit":10},"products":{"personal-roboport-equipment":1},"category":"crafting","energy":10},"personal-roboport-mk2-equipment":{"ingredients":{"processing-unit":100,"low-density-structure":20,"personal-roboport-equipment":5},"products":{"personal-roboport-mk2-equipment":1},"category":"crafting","energy":20},"night-vision-equipment":{"ingredients":{"steel-plate":10,"advanced-circuit":5},"products":{"night-vision-equipment":1},"category":"crafting","energy":10},"energy-shield-equipment":{"ingredients":{"steel-plate":10,"advanced-circuit":5},"products":{"energy-shield-equipment":1},"category":"crafting","energy":10},"energy-shield-mk2-equipment":{"ingredients":{"processing-unit":5,"low-density-structure":5,"energy-shield-equipment":10},"products":{"energy-shield-mk2-equipment":1},"category":"crafting","energy":10},"personal-laser-defense-equipment":{"ingredients":{"processing-unit":20,"low-density-structure":5,"laser-turret":5},"products":{"personal-laser-defense-equipment":1},"category":"crafting","energy":10},"discharge-defense-equipment":{"ingredients":{"steel-plate":20,"processing-unit":5,"laser-turret":10},"products":{"discharge-defense-equipment":1},"category":"crafting","energy":10},"stone-wall":{"ingredients":{"stone-brick":5},"products":{"stone-wall":1},"category":"crafting","energy":0.5},"gate":{"ingredients":{"steel-plate":2,"electronic-circuit":2,"stone-wall":1},"products":{"gate":1},"category":"crafting","energy":0.5},"radar":{"ingredients":{"iron-plate":10,"iron-gear-wheel":5,"electronic-circuit":5},"products":{"radar":1},"category":"crafting","energy":0.5},"land-mine":{"ingredients":{"steel-plate":1,"explosives":2},"products":{"land-mine":4},"category":"crafting","energy":5},"gun-turret":{"ingredients":{"iron-plate":20,"copper-plate":10,"iron-gear-wheel":10},"products":{"gun-turret":1},"category":"crafting","energy":8},"laser-turret":{"ingredients":{"steel-plate":20,"battery":12,"electronic-circuit":20},"products":{"laser-turret":1},"category":"crafting","energy":20},"flamethrower-turret":{"ingredients":{"steel-plate":30,"iron-gear-wheel":15,"engine-unit":5,"pipe":10},"products":{"flamethrower-turret":1},"category":"crafting","energy":20},"artillery-turret":{"ingredients":{"steel-plate":60,"iron-gear-wheel":40,"advanced-circuit":20,"concrete":60},"products":{"artillery-turret":1},"category":"crafting","energy":40},"parameter-0":{"ingredients":{},"products":{},"category":"parameters","energy":0.5},"parameter-1":{"ingredients":{},"products":{},"category":"parameters","energy":0.5},"parameter-2":{"ingredients":{},"products":{},"category":"parameters","energy":0.5},"parameter-3":{"ingredients":{},"products":{},"category":"parameters","energy":0.5},"parameter-4":{"ingredients":{},"products":{},"category":"parameters","energy":0.5},"parameter-5":{"ingredients":{},"products":{},"category":"parameters","energy":0.5},"parameter-6":{"ingredients":{},"products":{},"category":"parameters","energy":0.5},"parameter-7":{"ingredients":{},"products":{},"category":"parameters","energy":0.5},"parameter-8":{"ingredients":{},"products":{},"category":"parameters","energy":0.5},"parameter-9":{"ingredients":{},"products":{},"category":"parameters","energy":0.5},"recipe-unknown":{"ingredients":{},"products":{},"category":"crafting","energy":0.5}} \ No newline at end of file diff --git a/worlds/factorio/data/resources.json b/worlds/factorio/data/resources.json index 10279db37955..80c00fe3df42 100644 --- a/worlds/factorio/data/resources.json +++ b/worlds/factorio/data/resources.json @@ -1 +1 @@ -{"iron-ore":{"minable":true,"infinite":false,"category":"basic-solid","mining_time":1,"products":{"iron-ore":{"name":"iron-ore","amount":1}}},"copper-ore":{"minable":true,"infinite":false,"category":"basic-solid","mining_time":1,"products":{"copper-ore":{"name":"copper-ore","amount":1}}},"stone":{"minable":true,"infinite":false,"category":"basic-solid","mining_time":1,"products":{"stone":{"name":"stone","amount":1}}},"coal":{"minable":true,"infinite":false,"category":"basic-solid","mining_time":1,"products":{"coal":{"name":"coal","amount":1}}},"uranium-ore":{"minable":true,"infinite":false,"category":"basic-solid","mining_time":2,"required_fluid":"sulfuric-acid","fluid_amount":10,"products":{"uranium-ore":{"name":"uranium-ore","amount":1}}},"crude-oil":{"minable":true,"infinite":true,"infinite_depletion":10,"category":"basic-fluid","mining_time":1,"products":{"crude-oil":{"name":"crude-oil","amount":10}}}} \ No newline at end of file +{"iron-ore":{"minable":true,"infinite":false,"category":"basic-solid","mining_time":1,"products":{"iron-ore":{"name":"iron-ore","amount":1}}},"copper-ore":{"minable":true,"infinite":false,"category":"basic-solid","mining_time":1,"products":{"copper-ore":{"name":"copper-ore","amount":1}}},"stone":{"minable":true,"infinite":false,"category":"basic-solid","mining_time":1,"products":{"stone":{"name":"stone","amount":1}}},"coal":{"minable":true,"infinite":false,"category":"basic-solid","mining_time":1,"products":{"coal":{"name":"coal","amount":1}}},"crude-oil":{"minable":true,"infinite":true,"infinite_depletion":10,"category":"basic-fluid","mining_time":1,"products":{"crude-oil":{"name":"crude-oil","amount":10}}},"uranium-ore":{"minable":true,"infinite":false,"category":"basic-solid","mining_time":2,"required_fluid":"sulfuric-acid","fluid_amount":10,"products":{"uranium-ore":{"name":"uranium-ore","amount":1}}}} \ No newline at end of file diff --git a/worlds/factorio/data/techs.json b/worlds/factorio/data/techs.json index d9977f2986d6..ecb31126e1dc 100644 --- a/worlds/factorio/data/techs.json +++ b/worlds/factorio/data/techs.json @@ -1 +1 @@ -{"automation":{"unlocks":["assembling-machine-1","long-handed-inserter"],"requires":{},"ingredients":["automation-science-pack"],"has_modifier":false},"automation-2":{"unlocks":["assembling-machine-2"],"requires":["electronics","steel-processing","logistic-science-pack"],"ingredients":["automation-science-pack","logistic-science-pack"],"has_modifier":false},"automation-3":{"unlocks":["assembling-machine-3"],"requires":["speed-module","production-science-pack"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack","production-science-pack"],"has_modifier":false},"electronics":{"unlocks":{},"requires":["automation"],"ingredients":["automation-science-pack"],"has_modifier":false},"fast-inserter":{"unlocks":["fast-inserter","filter-inserter"],"requires":["electronics"],"ingredients":["automation-science-pack"],"has_modifier":false},"advanced-electronics":{"unlocks":["advanced-circuit"],"requires":["plastics"],"ingredients":["automation-science-pack","logistic-science-pack"],"has_modifier":false},"advanced-electronics-2":{"unlocks":["processing-unit"],"requires":["chemical-science-pack"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack"],"has_modifier":false},"circuit-network":{"unlocks":["red-wire","green-wire","arithmetic-combinator","decider-combinator","constant-combinator","power-switch","programmable-speaker"],"requires":["electronics","logistic-science-pack"],"ingredients":["automation-science-pack","logistic-science-pack"],"has_modifier":false},"explosives":{"unlocks":["explosives"],"requires":["sulfur-processing"],"ingredients":["automation-science-pack","logistic-science-pack"],"has_modifier":false},"logistics":{"unlocks":["underground-belt","splitter"],"requires":{},"ingredients":["automation-science-pack"],"has_modifier":false},"logistics-2":{"unlocks":["fast-transport-belt","fast-underground-belt","fast-splitter"],"requires":["logistics","logistic-science-pack"],"ingredients":["automation-science-pack","logistic-science-pack"],"has_modifier":false},"logistics-3":{"unlocks":["express-transport-belt","express-underground-belt","express-splitter"],"requires":["production-science-pack","lubricant"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack","production-science-pack"],"has_modifier":false},"optics":{"unlocks":["small-lamp"],"requires":{},"ingredients":["automation-science-pack"],"has_modifier":false},"laser":{"unlocks":{},"requires":["optics","battery","chemical-science-pack"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack"],"has_modifier":false},"solar-energy":{"unlocks":["solar-panel"],"requires":["optics","electronics","steel-processing","logistic-science-pack"],"ingredients":["automation-science-pack","logistic-science-pack"],"has_modifier":false},"gun-turret":{"unlocks":["gun-turret"],"requires":{},"ingredients":["automation-science-pack"],"has_modifier":false},"laser-turret":{"unlocks":["laser-turret"],"requires":["laser","military-science-pack"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack","chemical-science-pack"],"has_modifier":false},"stone-wall":{"unlocks":["stone-wall"],"requires":{},"ingredients":["automation-science-pack"],"has_modifier":false},"gate":{"unlocks":["gate"],"requires":["stone-wall","military-2"],"ingredients":["automation-science-pack","logistic-science-pack"],"has_modifier":false},"engine":{"unlocks":["engine-unit"],"requires":["steel-processing","logistic-science-pack"],"ingredients":["automation-science-pack","logistic-science-pack"],"has_modifier":false},"electric-engine":{"unlocks":["electric-engine-unit"],"requires":["lubricant"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack"],"has_modifier":false},"lubricant":{"unlocks":["lubricant"],"requires":["advanced-oil-processing"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack"],"has_modifier":false},"battery":{"unlocks":["battery"],"requires":["sulfur-processing"],"ingredients":["automation-science-pack","logistic-science-pack"],"has_modifier":false},"landfill":{"unlocks":["landfill"],"requires":["logistic-science-pack"],"ingredients":["automation-science-pack","logistic-science-pack"],"has_modifier":false},"braking-force-1":{"unlocks":{},"requires":["railway","chemical-science-pack"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack"],"has_modifier":true},"braking-force-2":{"unlocks":{},"requires":["braking-force-1"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack"],"has_modifier":true},"braking-force-3":{"unlocks":{},"requires":["braking-force-2"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack","production-science-pack"],"has_modifier":true},"braking-force-4":{"unlocks":{},"requires":["braking-force-3"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack","production-science-pack"],"has_modifier":true},"braking-force-5":{"unlocks":{},"requires":["braking-force-4"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack","production-science-pack"],"has_modifier":true},"braking-force-6":{"unlocks":{},"requires":["braking-force-5"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack","production-science-pack","utility-science-pack"],"has_modifier":true},"braking-force-7":{"unlocks":{},"requires":["braking-force-6"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack","production-science-pack","utility-science-pack"],"has_modifier":true},"chemical-science-pack":{"unlocks":["chemical-science-pack"],"requires":["advanced-electronics","sulfur-processing"],"ingredients":["automation-science-pack","logistic-science-pack"],"has_modifier":false},"logistic-science-pack":{"unlocks":["logistic-science-pack"],"requires":{},"ingredients":["automation-science-pack"],"has_modifier":false},"military-science-pack":{"unlocks":["military-science-pack"],"requires":["military-2","stone-wall"],"ingredients":["automation-science-pack","logistic-science-pack"],"has_modifier":false},"production-science-pack":{"unlocks":["production-science-pack"],"requires":["productivity-module","advanced-material-processing-2","railway"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack"],"has_modifier":false},"space-science-pack":{"unlocks":["satellite"],"requires":["rocket-silo","electric-energy-accumulators","solar-energy"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack","production-science-pack","utility-science-pack"],"has_modifier":false},"steel-processing":{"unlocks":["steel-plate","steel-chest"],"requires":{},"ingredients":["automation-science-pack"],"has_modifier":false},"utility-science-pack":{"unlocks":["utility-science-pack"],"requires":["robotics","advanced-electronics-2","low-density-structure"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack"],"has_modifier":false},"advanced-material-processing":{"unlocks":["steel-furnace"],"requires":["steel-processing","logistic-science-pack"],"ingredients":["automation-science-pack","logistic-science-pack"],"has_modifier":false},"steel-axe":{"unlocks":{},"requires":["steel-processing"],"ingredients":["automation-science-pack"],"has_modifier":true},"advanced-material-processing-2":{"unlocks":["electric-furnace"],"requires":["advanced-material-processing","chemical-science-pack"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack"],"has_modifier":false},"concrete":{"unlocks":["concrete","hazard-concrete","refined-concrete","refined-hazard-concrete"],"requires":["advanced-material-processing","automation-2"],"ingredients":["automation-science-pack","logistic-science-pack"],"has_modifier":false},"electric-energy-accumulators":{"unlocks":["accumulator"],"requires":["electric-energy-distribution-1","battery"],"ingredients":["automation-science-pack","logistic-science-pack"],"has_modifier":false},"electric-energy-distribution-1":{"unlocks":["medium-electric-pole","big-electric-pole"],"requires":["electronics","steel-processing","logistic-science-pack"],"ingredients":["automation-science-pack","logistic-science-pack"],"has_modifier":false},"electric-energy-distribution-2":{"unlocks":["substation"],"requires":["electric-energy-distribution-1","chemical-science-pack"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack"],"has_modifier":false},"railway":{"unlocks":["rail","locomotive","cargo-wagon"],"requires":["logistics-2","engine"],"ingredients":["automation-science-pack","logistic-science-pack"],"has_modifier":false},"fluid-wagon":{"unlocks":["fluid-wagon"],"requires":["railway","fluid-handling"],"ingredients":["automation-science-pack","logistic-science-pack"],"has_modifier":false},"automated-rail-transportation":{"unlocks":["train-stop"],"requires":["railway"],"ingredients":["automation-science-pack","logistic-science-pack"],"has_modifier":false},"rail-signals":{"unlocks":["rail-signal","rail-chain-signal"],"requires":["automated-rail-transportation"],"ingredients":["automation-science-pack","logistic-science-pack"],"has_modifier":false},"robotics":{"unlocks":["flying-robot-frame"],"requires":["electric-engine","battery"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack"],"has_modifier":false},"construction-robotics":{"unlocks":["roboport","logistic-chest-passive-provider","logistic-chest-storage","construction-robot"],"requires":["robotics"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack"],"has_modifier":true},"logistic-robotics":{"unlocks":["roboport","logistic-chest-passive-provider","logistic-chest-storage","logistic-robot"],"requires":["robotics"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack"],"has_modifier":true},"logistic-system":{"unlocks":["logistic-chest-active-provider","logistic-chest-requester","logistic-chest-buffer"],"requires":["utility-science-pack","logistic-robotics"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack","utility-science-pack"],"has_modifier":false},"personal-roboport-equipment":{"unlocks":["personal-roboport-equipment"],"requires":["construction-robotics","solar-panel-equipment"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack"],"has_modifier":false},"personal-roboport-mk2-equipment":{"unlocks":["personal-roboport-mk2-equipment"],"requires":["personal-roboport-equipment","utility-science-pack"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack","utility-science-pack"],"has_modifier":false},"worker-robots-speed-1":{"unlocks":{},"requires":["robotics"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack"],"has_modifier":true},"worker-robots-speed-2":{"unlocks":{},"requires":["worker-robots-speed-1"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack"],"has_modifier":true},"worker-robots-speed-3":{"unlocks":{},"requires":["worker-robots-speed-2"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack","utility-science-pack"],"has_modifier":true},"worker-robots-speed-4":{"unlocks":{},"requires":["worker-robots-speed-3"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack","utility-science-pack"],"has_modifier":true},"mining-productivity-1":{"unlocks":{},"requires":["advanced-electronics"],"ingredients":["automation-science-pack","logistic-science-pack"],"has_modifier":true},"mining-productivity-2":{"unlocks":{},"requires":["mining-productivity-1"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack"],"has_modifier":true},"mining-productivity-3":{"unlocks":{},"requires":["mining-productivity-2"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack","production-science-pack","utility-science-pack"],"has_modifier":true},"worker-robots-speed-5":{"unlocks":{},"requires":["worker-robots-speed-4"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack","production-science-pack","utility-science-pack"],"has_modifier":true},"worker-robots-storage-1":{"unlocks":{},"requires":["robotics"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack"],"has_modifier":true},"worker-robots-storage-2":{"unlocks":{},"requires":["worker-robots-storage-1"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack","production-science-pack"],"has_modifier":true},"worker-robots-storage-3":{"unlocks":{},"requires":["worker-robots-storage-2"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack","production-science-pack","utility-science-pack"],"has_modifier":true},"toolbelt":{"unlocks":{},"requires":["logistic-science-pack"],"ingredients":["automation-science-pack","logistic-science-pack"],"has_modifier":true},"research-speed-1":{"unlocks":{},"requires":["automation-2"],"ingredients":["automation-science-pack","logistic-science-pack"],"has_modifier":true},"research-speed-2":{"unlocks":{},"requires":["research-speed-1"],"ingredients":["automation-science-pack","logistic-science-pack"],"has_modifier":true},"research-speed-3":{"unlocks":{},"requires":["research-speed-2"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack"],"has_modifier":true},"research-speed-4":{"unlocks":{},"requires":["research-speed-3"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack"],"has_modifier":true},"research-speed-5":{"unlocks":{},"requires":["research-speed-4"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack","production-science-pack"],"has_modifier":true},"research-speed-6":{"unlocks":{},"requires":["research-speed-5"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack","production-science-pack","utility-science-pack"],"has_modifier":true},"stack-inserter":{"unlocks":["stack-inserter","stack-filter-inserter"],"requires":["fast-inserter","logistics-2","advanced-electronics"],"ingredients":["automation-science-pack","logistic-science-pack"],"has_modifier":true},"inserter-capacity-bonus-1":{"unlocks":{},"requires":["stack-inserter"],"ingredients":["automation-science-pack","logistic-science-pack"],"has_modifier":true},"inserter-capacity-bonus-2":{"unlocks":{},"requires":["inserter-capacity-bonus-1"],"ingredients":["automation-science-pack","logistic-science-pack"],"has_modifier":true},"inserter-capacity-bonus-3":{"unlocks":{},"requires":["inserter-capacity-bonus-2"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack"],"has_modifier":true},"inserter-capacity-bonus-4":{"unlocks":{},"requires":["inserter-capacity-bonus-3"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack","production-science-pack"],"has_modifier":true},"inserter-capacity-bonus-5":{"unlocks":{},"requires":["inserter-capacity-bonus-4"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack","production-science-pack"],"has_modifier":true},"inserter-capacity-bonus-6":{"unlocks":{},"requires":["inserter-capacity-bonus-5"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack","production-science-pack"],"has_modifier":true},"inserter-capacity-bonus-7":{"unlocks":{},"requires":["inserter-capacity-bonus-6"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack","production-science-pack","utility-science-pack"],"has_modifier":true},"oil-processing":{"unlocks":["pumpjack","oil-refinery","chemical-plant","basic-oil-processing","solid-fuel-from-petroleum-gas"],"requires":["fluid-handling"],"ingredients":["automation-science-pack","logistic-science-pack"],"has_modifier":false},"fluid-handling":{"unlocks":["storage-tank","pump","empty-barrel","fill-water-barrel","empty-water-barrel","fill-sulfuric-acid-barrel","empty-sulfuric-acid-barrel","fill-crude-oil-barrel","empty-crude-oil-barrel","fill-heavy-oil-barrel","empty-heavy-oil-barrel","fill-light-oil-barrel","empty-light-oil-barrel","fill-petroleum-gas-barrel","empty-petroleum-gas-barrel","fill-lubricant-barrel","empty-lubricant-barrel"],"requires":["automation-2","engine"],"ingredients":["automation-science-pack","logistic-science-pack"],"has_modifier":false},"advanced-oil-processing":{"unlocks":["advanced-oil-processing","heavy-oil-cracking","light-oil-cracking","solid-fuel-from-heavy-oil","solid-fuel-from-light-oil"],"requires":["chemical-science-pack"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack"],"has_modifier":false},"coal-liquefaction":{"unlocks":["coal-liquefaction"],"requires":["advanced-oil-processing","production-science-pack"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack","production-science-pack"],"has_modifier":false},"sulfur-processing":{"unlocks":["sulfuric-acid","sulfur"],"requires":["oil-processing"],"ingredients":["automation-science-pack","logistic-science-pack"],"has_modifier":false},"plastics":{"unlocks":["plastic-bar"],"requires":["oil-processing"],"ingredients":["automation-science-pack","logistic-science-pack"],"has_modifier":false},"artillery":{"unlocks":["artillery-wagon","artillery-turret","artillery-shell","artillery-targeting-remote"],"requires":["military-4","tank"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack","chemical-science-pack","utility-science-pack"],"has_modifier":false},"spidertron":{"unlocks":["spidertron","spidertron-remote"],"requires":["military-4","exoskeleton-equipment","fusion-reactor-equipment","rocketry","rocket-control-unit","effectivity-module-3"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack","chemical-science-pack","production-science-pack","utility-science-pack"],"has_modifier":false},"military":{"unlocks":["submachine-gun","shotgun","shotgun-shell"],"requires":{},"ingredients":["automation-science-pack"],"has_modifier":false},"atomic-bomb":{"unlocks":["atomic-bomb"],"requires":["military-4","kovarex-enrichment-process","rocket-control-unit","rocketry"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack","chemical-science-pack","production-science-pack","utility-science-pack"],"has_modifier":false},"military-2":{"unlocks":["piercing-rounds-magazine","grenade"],"requires":["military","steel-processing","logistic-science-pack"],"ingredients":["automation-science-pack","logistic-science-pack"],"has_modifier":false},"uranium-ammo":{"unlocks":["uranium-rounds-magazine","uranium-cannon-shell","explosive-uranium-cannon-shell"],"requires":["uranium-processing","military-4","tank"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack","chemical-science-pack","utility-science-pack"],"has_modifier":false},"military-3":{"unlocks":["poison-capsule","slowdown-capsule","combat-shotgun"],"requires":["chemical-science-pack","military-science-pack"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack","chemical-science-pack"],"has_modifier":false},"military-4":{"unlocks":["piercing-shotgun-shell","cluster-grenade"],"requires":["military-3","utility-science-pack","explosives"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack","chemical-science-pack","utility-science-pack"],"has_modifier":false},"automobilism":{"unlocks":["car"],"requires":["logistics-2","engine"],"ingredients":["automation-science-pack","logistic-science-pack"],"has_modifier":false},"flammables":{"unlocks":{},"requires":["oil-processing"],"ingredients":["automation-science-pack","logistic-science-pack"],"has_modifier":false},"flamethrower":{"unlocks":["flamethrower","flamethrower-ammo","flamethrower-turret"],"requires":["flammables","military-science-pack"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack"],"has_modifier":false},"tank":{"unlocks":["tank","cannon-shell","explosive-cannon-shell"],"requires":["automobilism","military-3","explosives"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack","chemical-science-pack"],"has_modifier":false},"land-mine":{"unlocks":["land-mine"],"requires":["explosives","military-science-pack"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack"],"has_modifier":false},"rocketry":{"unlocks":["rocket-launcher","rocket"],"requires":["explosives","flammables","military-science-pack"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack"],"has_modifier":false},"explosive-rocketry":{"unlocks":["explosive-rocket"],"requires":["rocketry","military-3"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack","chemical-science-pack"],"has_modifier":false},"energy-weapons-damage-1":{"unlocks":{},"requires":["laser","military-science-pack"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack","chemical-science-pack"],"has_modifier":true},"refined-flammables-1":{"unlocks":{},"requires":["flamethrower"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack"],"has_modifier":true},"stronger-explosives-1":{"unlocks":{},"requires":["military-2"],"ingredients":["automation-science-pack","logistic-science-pack"],"has_modifier":true},"weapon-shooting-speed-1":{"unlocks":{},"requires":["military"],"ingredients":["automation-science-pack"],"has_modifier":true},"physical-projectile-damage-1":{"unlocks":{},"requires":["military"],"ingredients":["automation-science-pack"],"has_modifier":true},"energy-weapons-damage-2":{"unlocks":{},"requires":["energy-weapons-damage-1"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack","chemical-science-pack"],"has_modifier":true},"physical-projectile-damage-2":{"unlocks":{},"requires":["physical-projectile-damage-1"],"ingredients":["automation-science-pack","logistic-science-pack"],"has_modifier":true},"refined-flammables-2":{"unlocks":{},"requires":["refined-flammables-1"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack"],"has_modifier":true},"stronger-explosives-2":{"unlocks":{},"requires":["stronger-explosives-1"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack"],"has_modifier":true},"weapon-shooting-speed-2":{"unlocks":{},"requires":["weapon-shooting-speed-1"],"ingredients":["automation-science-pack","logistic-science-pack"],"has_modifier":true},"energy-weapons-damage-3":{"unlocks":{},"requires":["energy-weapons-damage-2"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack","chemical-science-pack"],"has_modifier":true},"physical-projectile-damage-3":{"unlocks":{},"requires":["physical-projectile-damage-2"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack"],"has_modifier":true},"refined-flammables-3":{"unlocks":{},"requires":["refined-flammables-2"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack","chemical-science-pack"],"has_modifier":true},"stronger-explosives-3":{"unlocks":{},"requires":["stronger-explosives-2"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack","chemical-science-pack"],"has_modifier":true},"weapon-shooting-speed-3":{"unlocks":{},"requires":["weapon-shooting-speed-2"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack"],"has_modifier":true},"energy-weapons-damage-4":{"unlocks":{},"requires":["energy-weapons-damage-3"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack","chemical-science-pack"],"has_modifier":true},"physical-projectile-damage-4":{"unlocks":{},"requires":["physical-projectile-damage-3"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack"],"has_modifier":true},"refined-flammables-4":{"unlocks":{},"requires":["refined-flammables-3"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack","chemical-science-pack","utility-science-pack"],"has_modifier":true},"stronger-explosives-4":{"unlocks":{},"requires":["stronger-explosives-3"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack","chemical-science-pack","utility-science-pack"],"has_modifier":true},"weapon-shooting-speed-4":{"unlocks":{},"requires":["weapon-shooting-speed-3"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack"],"has_modifier":true},"energy-weapons-damage-5":{"unlocks":{},"requires":["energy-weapons-damage-4"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack","chemical-science-pack","utility-science-pack"],"has_modifier":true},"physical-projectile-damage-5":{"unlocks":{},"requires":["physical-projectile-damage-4"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack","chemical-science-pack"],"has_modifier":true},"refined-flammables-5":{"unlocks":{},"requires":["refined-flammables-4"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack","chemical-science-pack","utility-science-pack"],"has_modifier":true},"stronger-explosives-5":{"unlocks":{},"requires":["stronger-explosives-4"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack","chemical-science-pack","utility-science-pack"],"has_modifier":true},"weapon-shooting-speed-5":{"unlocks":{},"requires":["weapon-shooting-speed-4"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack","chemical-science-pack"],"has_modifier":true},"energy-weapons-damage-6":{"unlocks":{},"requires":["energy-weapons-damage-5"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack","chemical-science-pack","utility-science-pack"],"has_modifier":true},"physical-projectile-damage-6":{"unlocks":{},"requires":["physical-projectile-damage-5"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack","chemical-science-pack","utility-science-pack"],"has_modifier":true},"refined-flammables-6":{"unlocks":{},"requires":["refined-flammables-5"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack","chemical-science-pack","utility-science-pack"],"has_modifier":true},"stronger-explosives-6":{"unlocks":{},"requires":["stronger-explosives-5"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack","chemical-science-pack","utility-science-pack"],"has_modifier":true},"weapon-shooting-speed-6":{"unlocks":{},"requires":["weapon-shooting-speed-5"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack","chemical-science-pack","utility-science-pack"],"has_modifier":true},"laser-shooting-speed-1":{"unlocks":{},"requires":["laser","military-science-pack"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack","chemical-science-pack"],"has_modifier":true},"laser-shooting-speed-2":{"unlocks":{},"requires":["laser-shooting-speed-1"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack","chemical-science-pack"],"has_modifier":true},"laser-shooting-speed-3":{"unlocks":{},"requires":["laser-shooting-speed-2"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack","chemical-science-pack"],"has_modifier":true},"laser-shooting-speed-4":{"unlocks":{},"requires":["laser-shooting-speed-3"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack","chemical-science-pack"],"has_modifier":true},"laser-shooting-speed-5":{"unlocks":{},"requires":["laser-shooting-speed-4"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack","chemical-science-pack","utility-science-pack"],"has_modifier":true},"laser-shooting-speed-6":{"unlocks":{},"requires":["laser-shooting-speed-5"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack","chemical-science-pack","utility-science-pack"],"has_modifier":true},"laser-shooting-speed-7":{"unlocks":{},"requires":["laser-shooting-speed-6"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack","chemical-science-pack","utility-science-pack"],"has_modifier":true},"defender":{"unlocks":["defender-capsule"],"requires":["military-science-pack"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack"],"has_modifier":true},"distractor":{"unlocks":["distractor-capsule"],"requires":["defender","military-3","laser"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack","chemical-science-pack"],"has_modifier":false},"destroyer":{"unlocks":["destroyer-capsule"],"requires":["military-4","distractor","speed-module"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack","chemical-science-pack","utility-science-pack"],"has_modifier":false},"follower-robot-count-1":{"unlocks":{},"requires":["defender"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack"],"has_modifier":true},"follower-robot-count-2":{"unlocks":{},"requires":["follower-robot-count-1"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack"],"has_modifier":true},"follower-robot-count-3":{"unlocks":{},"requires":["follower-robot-count-2"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack","chemical-science-pack"],"has_modifier":true},"follower-robot-count-4":{"unlocks":{},"requires":["follower-robot-count-3"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack","chemical-science-pack"],"has_modifier":true},"follower-robot-count-5":{"unlocks":{},"requires":["follower-robot-count-4","destroyer"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack","chemical-science-pack","utility-science-pack"],"has_modifier":true},"follower-robot-count-6":{"unlocks":{},"requires":["follower-robot-count-5"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack","chemical-science-pack","utility-science-pack"],"has_modifier":true},"kovarex-enrichment-process":{"unlocks":["kovarex-enrichment-process","nuclear-fuel"],"requires":["production-science-pack","uranium-processing","rocket-fuel"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack","production-science-pack"],"has_modifier":false},"nuclear-fuel-reprocessing":{"unlocks":["nuclear-fuel-reprocessing"],"requires":["nuclear-power","production-science-pack"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack","production-science-pack"],"has_modifier":false},"nuclear-power":{"unlocks":["nuclear-reactor","heat-exchanger","heat-pipe","steam-turbine"],"requires":["uranium-processing"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack"],"has_modifier":false},"uranium-processing":{"unlocks":["centrifuge","uranium-processing","uranium-fuel-cell"],"requires":["chemical-science-pack","concrete"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack"],"has_modifier":false},"heavy-armor":{"unlocks":["heavy-armor"],"requires":["military","steel-processing"],"ingredients":["automation-science-pack"],"has_modifier":false},"modular-armor":{"unlocks":["modular-armor"],"requires":["heavy-armor","advanced-electronics"],"ingredients":["automation-science-pack","logistic-science-pack"],"has_modifier":false},"power-armor":{"unlocks":["power-armor"],"requires":["modular-armor","electric-engine","advanced-electronics-2"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack"],"has_modifier":false},"power-armor-mk2":{"unlocks":["power-armor-mk2"],"requires":["power-armor","military-4","speed-module-2","effectivity-module-2"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack","chemical-science-pack","utility-science-pack"],"has_modifier":false},"energy-shield-equipment":{"unlocks":["energy-shield-equipment"],"requires":["solar-panel-equipment","military-science-pack"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack"],"has_modifier":false},"energy-shield-mk2-equipment":{"unlocks":["energy-shield-mk2-equipment"],"requires":["energy-shield-equipment","military-3","low-density-structure","power-armor"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack","chemical-science-pack"],"has_modifier":false},"night-vision-equipment":{"unlocks":["night-vision-equipment"],"requires":["solar-panel-equipment"],"ingredients":["automation-science-pack","logistic-science-pack"],"has_modifier":false},"belt-immunity-equipment":{"unlocks":["belt-immunity-equipment"],"requires":["solar-panel-equipment"],"ingredients":["automation-science-pack","logistic-science-pack"],"has_modifier":false},"exoskeleton-equipment":{"unlocks":["exoskeleton-equipment"],"requires":["advanced-electronics-2","electric-engine","solar-panel-equipment"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack"],"has_modifier":false},"battery-equipment":{"unlocks":["battery-equipment"],"requires":["battery","solar-panel-equipment"],"ingredients":["automation-science-pack","logistic-science-pack"],"has_modifier":false},"battery-mk2-equipment":{"unlocks":["battery-mk2-equipment"],"requires":["battery-equipment","low-density-structure","power-armor"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack"],"has_modifier":false},"solar-panel-equipment":{"unlocks":["solar-panel-equipment"],"requires":["modular-armor","solar-energy"],"ingredients":["automation-science-pack","logistic-science-pack"],"has_modifier":false},"fusion-reactor-equipment":{"unlocks":["fusion-reactor-equipment"],"requires":["utility-science-pack","power-armor","military-science-pack"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack","chemical-science-pack","utility-science-pack"],"has_modifier":false},"personal-laser-defense-equipment":{"unlocks":["personal-laser-defense-equipment"],"requires":["laser-turret","military-3","low-density-structure","power-armor","solar-panel-equipment"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack","chemical-science-pack"],"has_modifier":false},"discharge-defense-equipment":{"unlocks":["discharge-defense-equipment","discharge-defense-remote"],"requires":["laser-turret","military-3","power-armor","solar-panel-equipment"],"ingredients":["automation-science-pack","logistic-science-pack","military-science-pack","chemical-science-pack"],"has_modifier":false},"modules":{"unlocks":{},"requires":["advanced-electronics"],"ingredients":["automation-science-pack","logistic-science-pack"],"has_modifier":false},"speed-module":{"unlocks":["speed-module"],"requires":["modules"],"ingredients":["automation-science-pack","logistic-science-pack"],"has_modifier":false},"speed-module-2":{"unlocks":["speed-module-2"],"requires":["speed-module","advanced-electronics-2"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack"],"has_modifier":false},"speed-module-3":{"unlocks":["speed-module-3"],"requires":["speed-module-2","production-science-pack"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack","production-science-pack"],"has_modifier":false},"productivity-module":{"unlocks":["productivity-module"],"requires":["modules"],"ingredients":["automation-science-pack","logistic-science-pack"],"has_modifier":false},"productivity-module-2":{"unlocks":["productivity-module-2"],"requires":["productivity-module","advanced-electronics-2"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack"],"has_modifier":false},"productivity-module-3":{"unlocks":["productivity-module-3"],"requires":["productivity-module-2","production-science-pack"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack","production-science-pack"],"has_modifier":false},"effectivity-module":{"unlocks":["effectivity-module"],"requires":["modules"],"ingredients":["automation-science-pack","logistic-science-pack"],"has_modifier":false},"effectivity-module-2":{"unlocks":["effectivity-module-2"],"requires":["effectivity-module","advanced-electronics-2"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack"],"has_modifier":false},"effectivity-module-3":{"unlocks":["effectivity-module-3"],"requires":["effectivity-module-2","production-science-pack"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack","production-science-pack"],"has_modifier":false},"effect-transmission":{"unlocks":["beacon"],"requires":["advanced-electronics-2","production-science-pack"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack","production-science-pack"],"has_modifier":false},"low-density-structure":{"unlocks":["low-density-structure"],"requires":["advanced-material-processing","chemical-science-pack"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack"],"has_modifier":false},"rocket-control-unit":{"unlocks":["rocket-control-unit"],"requires":["utility-science-pack","speed-module"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack","utility-science-pack"],"has_modifier":false},"rocket-fuel":{"unlocks":["rocket-fuel"],"requires":["flammables","advanced-oil-processing"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack"],"has_modifier":false},"rocket-silo":{"unlocks":["rocket-silo","rocket-part"],"requires":["concrete","speed-module-3","productivity-module-3","rocket-fuel","rocket-control-unit"],"ingredients":["automation-science-pack","logistic-science-pack","chemical-science-pack","production-science-pack","utility-science-pack"],"has_modifier":false},"cliff-explosives":{"unlocks":["cliff-explosives"],"requires":["explosives","military-2"],"ingredients":["automation-science-pack","logistic-science-pack"],"has_modifier":false}} \ No newline at end of file +{"advanced-circuit":{"unlocks":["advanced-circuit"],"requires":["plastics"],"has_modifier":false},"advanced-combinators":{"unlocks":["selector-combinator"],"requires":["circuit-network","chemical-science-pack"],"has_modifier":false},"advanced-material-processing":{"unlocks":["steel-furnace"],"requires":["steel-processing","logistic-science-pack"],"has_modifier":false},"advanced-material-processing-2":{"unlocks":["electric-furnace"],"requires":["advanced-material-processing","chemical-science-pack"],"has_modifier":false},"advanced-oil-processing":{"unlocks":["advanced-oil-processing","heavy-oil-cracking","light-oil-cracking","solid-fuel-from-heavy-oil","solid-fuel-from-light-oil"],"requires":["chemical-science-pack"],"has_modifier":false},"artillery":{"unlocks":["artillery-wagon","artillery-turret","artillery-shell"],"requires":["military-4","tank","concrete","radar"],"has_modifier":false},"atomic-bomb":{"unlocks":["atomic-bomb"],"requires":["military-4","kovarex-enrichment-process","rocketry"],"has_modifier":false},"automated-rail-transportation":{"unlocks":["train-stop","rail-signal","rail-chain-signal"],"requires":["railway"],"has_modifier":false},"automation":{"unlocks":["assembling-machine-1","long-handed-inserter"],"requires":["automation-science-pack"],"has_modifier":false},"automation-2":{"unlocks":["assembling-machine-2"],"requires":["automation","steel-processing","logistic-science-pack"],"has_modifier":false},"automation-3":{"unlocks":["assembling-machine-3"],"requires":["speed-module","production-science-pack","electric-engine"],"has_modifier":false},"automation-science-pack":{"unlocks":["automation-science-pack"],"requires":["steam-power","electronics"],"has_modifier":false},"automobilism":{"unlocks":["car"],"requires":["logistics-2","engine"],"has_modifier":false},"battery":{"unlocks":["battery"],"requires":["sulfur-processing"],"has_modifier":false},"battery-equipment":{"unlocks":["battery-equipment"],"requires":["battery","solar-panel-equipment"],"has_modifier":false},"battery-mk2-equipment":{"unlocks":["battery-mk2-equipment"],"requires":["battery-equipment","low-density-structure","power-armor"],"has_modifier":false},"belt-immunity-equipment":{"unlocks":["belt-immunity-equipment"],"requires":["solar-panel-equipment"],"has_modifier":false},"braking-force-1":{"unlocks":{},"requires":["railway","chemical-science-pack"],"has_modifier":true},"braking-force-2":{"unlocks":{},"requires":["braking-force-1"],"has_modifier":true},"braking-force-3":{"unlocks":{},"requires":["braking-force-2","production-science-pack"],"has_modifier":true},"braking-force-4":{"unlocks":{},"requires":["braking-force-3"],"has_modifier":true},"braking-force-5":{"unlocks":{},"requires":["braking-force-4"],"has_modifier":true},"braking-force-6":{"unlocks":{},"requires":["braking-force-5","utility-science-pack"],"has_modifier":true},"braking-force-7":{"unlocks":{},"requires":["braking-force-6"],"has_modifier":true},"bulk-inserter":{"unlocks":["bulk-inserter"],"requires":["fast-inserter","logistics-2","advanced-circuit"],"has_modifier":true},"chemical-science-pack":{"unlocks":["chemical-science-pack"],"requires":["advanced-circuit","sulfur-processing"],"has_modifier":false},"circuit-network":{"unlocks":["arithmetic-combinator","decider-combinator","constant-combinator","power-switch","programmable-speaker","display-panel","iron-stick"],"requires":["logistic-science-pack"],"has_modifier":true},"cliff-explosives":{"unlocks":["cliff-explosives"],"requires":["explosives","military-2"],"has_modifier":true},"coal-liquefaction":{"unlocks":["coal-liquefaction"],"requires":["advanced-oil-processing","production-science-pack"],"has_modifier":false},"concrete":{"unlocks":["concrete","hazard-concrete","refined-concrete","refined-hazard-concrete","iron-stick"],"requires":["advanced-material-processing","automation-2"],"has_modifier":false},"construction-robotics":{"unlocks":["roboport","passive-provider-chest","storage-chest","construction-robot"],"requires":["robotics"],"has_modifier":true},"defender":{"unlocks":["defender-capsule"],"requires":["military-science-pack"],"has_modifier":true},"destroyer":{"unlocks":["destroyer-capsule"],"requires":["military-4","distractor","speed-module"],"has_modifier":false},"discharge-defense-equipment":{"unlocks":["discharge-defense-equipment"],"requires":["laser-turret","military-3","power-armor","solar-panel-equipment"],"has_modifier":false},"distractor":{"unlocks":["distractor-capsule"],"requires":["defender","military-3","laser"],"has_modifier":false},"effect-transmission":{"unlocks":["beacon"],"requires":["processing-unit","production-science-pack"],"has_modifier":false},"efficiency-module":{"unlocks":["efficiency-module"],"requires":["modules"],"has_modifier":false},"efficiency-module-2":{"unlocks":["efficiency-module-2"],"requires":["efficiency-module","processing-unit"],"has_modifier":false},"efficiency-module-3":{"unlocks":["efficiency-module-3"],"requires":["efficiency-module-2","production-science-pack"],"has_modifier":false},"electric-energy-accumulators":{"unlocks":["accumulator"],"requires":["electric-energy-distribution-1","battery"],"has_modifier":false},"electric-energy-distribution-1":{"unlocks":["medium-electric-pole","big-electric-pole","iron-stick"],"requires":["steel-processing","logistic-science-pack"],"has_modifier":false},"electric-energy-distribution-2":{"unlocks":["substation"],"requires":["electric-energy-distribution-1","chemical-science-pack"],"has_modifier":false},"electric-engine":{"unlocks":["electric-engine-unit"],"requires":["lubricant"],"has_modifier":false},"electric-mining-drill":{"unlocks":["electric-mining-drill"],"requires":["automation-science-pack"],"has_modifier":false},"electronics":{"unlocks":["copper-cable","electronic-circuit","lab","inserter","small-electric-pole"],"requires":{},"has_modifier":false},"energy-shield-equipment":{"unlocks":["energy-shield-equipment"],"requires":["solar-panel-equipment","military-science-pack"],"has_modifier":false},"energy-shield-mk2-equipment":{"unlocks":["energy-shield-mk2-equipment"],"requires":["energy-shield-equipment","military-3","low-density-structure","power-armor"],"has_modifier":false},"engine":{"unlocks":["engine-unit"],"requires":["steel-processing","logistic-science-pack"],"has_modifier":false},"exoskeleton-equipment":{"unlocks":["exoskeleton-equipment"],"requires":["processing-unit","electric-engine","solar-panel-equipment"],"has_modifier":false},"explosive-rocketry":{"unlocks":["explosive-rocket"],"requires":["rocketry","military-3"],"has_modifier":false},"explosives":{"unlocks":["explosives"],"requires":["sulfur-processing"],"has_modifier":false},"fast-inserter":{"unlocks":["fast-inserter"],"requires":["automation-science-pack"],"has_modifier":false},"fission-reactor-equipment":{"unlocks":["fission-reactor-equipment"],"requires":["utility-science-pack","power-armor","military-science-pack","nuclear-power"],"has_modifier":false},"flamethrower":{"unlocks":["flamethrower","flamethrower-ammo","flamethrower-turret"],"requires":["flammables","military-science-pack"],"has_modifier":false},"flammables":{"unlocks":{},"requires":["oil-processing"],"has_modifier":false},"fluid-handling":{"unlocks":["storage-tank","pump","barrel","water-barrel","empty-water-barrel","sulfuric-acid-barrel","empty-sulfuric-acid-barrel","crude-oil-barrel","empty-crude-oil-barrel","heavy-oil-barrel","empty-heavy-oil-barrel","light-oil-barrel","empty-light-oil-barrel","petroleum-gas-barrel","empty-petroleum-gas-barrel","lubricant-barrel","empty-lubricant-barrel"],"requires":["automation-2","engine"],"has_modifier":false},"fluid-wagon":{"unlocks":["fluid-wagon"],"requires":["railway","fluid-handling"],"has_modifier":false},"follower-robot-count-1":{"unlocks":{},"requires":["defender"],"has_modifier":true},"follower-robot-count-2":{"unlocks":{},"requires":["follower-robot-count-1"],"has_modifier":true},"follower-robot-count-3":{"unlocks":{},"requires":["follower-robot-count-2","chemical-science-pack"],"has_modifier":true},"follower-robot-count-4":{"unlocks":{},"requires":["follower-robot-count-3","destroyer"],"has_modifier":true},"gate":{"unlocks":["gate"],"requires":["stone-wall","military-2"],"has_modifier":false},"gun-turret":{"unlocks":["gun-turret"],"requires":["automation-science-pack"],"has_modifier":false},"heavy-armor":{"unlocks":["heavy-armor"],"requires":["military","steel-processing"],"has_modifier":false},"inserter-capacity-bonus-1":{"unlocks":{},"requires":["bulk-inserter"],"has_modifier":true},"inserter-capacity-bonus-2":{"unlocks":{},"requires":["inserter-capacity-bonus-1"],"has_modifier":true},"inserter-capacity-bonus-3":{"unlocks":{},"requires":["inserter-capacity-bonus-2","chemical-science-pack"],"has_modifier":true},"inserter-capacity-bonus-4":{"unlocks":{},"requires":["inserter-capacity-bonus-3","production-science-pack"],"has_modifier":true},"inserter-capacity-bonus-5":{"unlocks":{},"requires":["inserter-capacity-bonus-4"],"has_modifier":true},"inserter-capacity-bonus-6":{"unlocks":{},"requires":["inserter-capacity-bonus-5"],"has_modifier":true},"inserter-capacity-bonus-7":{"unlocks":{},"requires":["inserter-capacity-bonus-6","utility-science-pack"],"has_modifier":true},"kovarex-enrichment-process":{"unlocks":["kovarex-enrichment-process","nuclear-fuel"],"requires":["production-science-pack","uranium-processing","rocket-fuel"],"has_modifier":false},"lamp":{"unlocks":["small-lamp"],"requires":["automation-science-pack"],"has_modifier":false},"land-mine":{"unlocks":["land-mine"],"requires":["explosives","military-science-pack"],"has_modifier":false},"landfill":{"unlocks":["landfill"],"requires":["logistic-science-pack"],"has_modifier":false},"laser":{"unlocks":{},"requires":["battery","chemical-science-pack"],"has_modifier":false},"laser-shooting-speed-1":{"unlocks":{},"requires":["laser","military-science-pack"],"has_modifier":true},"laser-shooting-speed-2":{"unlocks":{},"requires":["laser-shooting-speed-1"],"has_modifier":true},"laser-shooting-speed-3":{"unlocks":{},"requires":["laser-shooting-speed-2"],"has_modifier":true},"laser-shooting-speed-4":{"unlocks":{},"requires":["laser-shooting-speed-3"],"has_modifier":true},"laser-shooting-speed-5":{"unlocks":{},"requires":["laser-shooting-speed-4","utility-science-pack"],"has_modifier":true},"laser-shooting-speed-6":{"unlocks":{},"requires":["laser-shooting-speed-5"],"has_modifier":true},"laser-shooting-speed-7":{"unlocks":{},"requires":["laser-shooting-speed-6"],"has_modifier":true},"laser-turret":{"unlocks":["laser-turret"],"requires":["laser","military-science-pack"],"has_modifier":false},"laser-weapons-damage-1":{"unlocks":{},"requires":["laser","military-science-pack"],"has_modifier":true},"laser-weapons-damage-2":{"unlocks":{},"requires":["laser-weapons-damage-1"],"has_modifier":true},"laser-weapons-damage-3":{"unlocks":{},"requires":["laser-weapons-damage-2"],"has_modifier":true},"laser-weapons-damage-4":{"unlocks":{},"requires":["laser-weapons-damage-3"],"has_modifier":true},"laser-weapons-damage-5":{"unlocks":{},"requires":["laser-weapons-damage-4","utility-science-pack"],"has_modifier":true},"laser-weapons-damage-6":{"unlocks":{},"requires":["laser-weapons-damage-5"],"has_modifier":true},"logistic-robotics":{"unlocks":["roboport","passive-provider-chest","storage-chest","logistic-robot"],"requires":["robotics"],"has_modifier":true},"logistic-science-pack":{"unlocks":["logistic-science-pack"],"requires":["automation-science-pack"],"has_modifier":false},"logistic-system":{"unlocks":["active-provider-chest","requester-chest","buffer-chest"],"requires":["utility-science-pack","logistic-robotics"],"has_modifier":true},"logistics":{"unlocks":["underground-belt","splitter"],"requires":["automation-science-pack"],"has_modifier":false},"logistics-2":{"unlocks":["fast-transport-belt","fast-underground-belt","fast-splitter"],"requires":["logistics","logistic-science-pack"],"has_modifier":false},"logistics-3":{"unlocks":["express-transport-belt","express-underground-belt","express-splitter"],"requires":["production-science-pack","lubricant"],"has_modifier":false},"low-density-structure":{"unlocks":["low-density-structure"],"requires":["advanced-material-processing","chemical-science-pack"],"has_modifier":false},"lubricant":{"unlocks":["lubricant"],"requires":["advanced-oil-processing"],"has_modifier":false},"military":{"unlocks":["submachine-gun","shotgun","shotgun-shell"],"requires":["automation-science-pack"],"has_modifier":false},"military-2":{"unlocks":["piercing-rounds-magazine","grenade"],"requires":["military","steel-processing","logistic-science-pack"],"has_modifier":false},"military-3":{"unlocks":["poison-capsule","slowdown-capsule","combat-shotgun"],"requires":["chemical-science-pack","military-science-pack"],"has_modifier":false},"military-4":{"unlocks":["piercing-shotgun-shell","cluster-grenade"],"requires":["military-3","utility-science-pack","explosives"],"has_modifier":false},"military-science-pack":{"unlocks":["military-science-pack"],"requires":["military-2","stone-wall"],"has_modifier":false},"mining-productivity-1":{"unlocks":{},"requires":["advanced-circuit"],"has_modifier":true},"mining-productivity-2":{"unlocks":{},"requires":["mining-productivity-1","chemical-science-pack"],"has_modifier":true},"mining-productivity-3":{"unlocks":{},"requires":["mining-productivity-2","production-science-pack","utility-science-pack"],"has_modifier":true},"modular-armor":{"unlocks":["modular-armor"],"requires":["heavy-armor","advanced-circuit"],"has_modifier":false},"modules":{"unlocks":{},"requires":["advanced-circuit"],"has_modifier":false},"night-vision-equipment":{"unlocks":["night-vision-equipment"],"requires":["solar-panel-equipment"],"has_modifier":false},"nuclear-fuel-reprocessing":{"unlocks":["nuclear-fuel-reprocessing"],"requires":["nuclear-power","production-science-pack"],"has_modifier":false},"nuclear-power":{"unlocks":["nuclear-reactor","heat-exchanger","heat-pipe","steam-turbine","uranium-fuel-cell"],"requires":["uranium-processing"],"has_modifier":false},"oil-gathering":{"unlocks":["pumpjack"],"requires":["fluid-handling"],"has_modifier":false},"oil-processing":{"unlocks":["oil-refinery","chemical-plant","basic-oil-processing","solid-fuel-from-petroleum-gas"],"requires":["oil-gathering"],"has_modifier":false},"personal-laser-defense-equipment":{"unlocks":["personal-laser-defense-equipment"],"requires":["laser-turret","military-3","low-density-structure","power-armor","solar-panel-equipment"],"has_modifier":false},"personal-roboport-equipment":{"unlocks":["personal-roboport-equipment"],"requires":["construction-robotics","solar-panel-equipment"],"has_modifier":false},"personal-roboport-mk2-equipment":{"unlocks":["personal-roboport-mk2-equipment"],"requires":["personal-roboport-equipment","utility-science-pack"],"has_modifier":false},"physical-projectile-damage-1":{"unlocks":{},"requires":["military"],"has_modifier":true},"physical-projectile-damage-2":{"unlocks":{},"requires":["physical-projectile-damage-1","logistic-science-pack"],"has_modifier":true},"physical-projectile-damage-3":{"unlocks":{},"requires":["physical-projectile-damage-2","military-science-pack"],"has_modifier":true},"physical-projectile-damage-4":{"unlocks":{},"requires":["physical-projectile-damage-3"],"has_modifier":true},"physical-projectile-damage-5":{"unlocks":{},"requires":["physical-projectile-damage-4","chemical-science-pack"],"has_modifier":true},"physical-projectile-damage-6":{"unlocks":{},"requires":["physical-projectile-damage-5","utility-science-pack"],"has_modifier":true},"plastics":{"unlocks":["plastic-bar"],"requires":["oil-processing"],"has_modifier":false},"power-armor":{"unlocks":["power-armor"],"requires":["modular-armor","electric-engine","processing-unit"],"has_modifier":false},"power-armor-mk2":{"unlocks":["power-armor-mk2"],"requires":["power-armor","military-4","speed-module-2","efficiency-module-2"],"has_modifier":false},"processing-unit":{"unlocks":["processing-unit"],"requires":["chemical-science-pack"],"has_modifier":false},"production-science-pack":{"unlocks":["production-science-pack"],"requires":["productivity-module","advanced-material-processing-2","railway"],"has_modifier":false},"productivity-module":{"unlocks":["productivity-module"],"requires":["modules"],"has_modifier":false},"productivity-module-2":{"unlocks":["productivity-module-2"],"requires":["productivity-module","processing-unit"],"has_modifier":false},"productivity-module-3":{"unlocks":["productivity-module-3"],"requires":["productivity-module-2","production-science-pack"],"has_modifier":false},"radar":{"unlocks":["radar"],"requires":["automation-science-pack"],"has_modifier":false},"railway":{"unlocks":["rail","locomotive","cargo-wagon","iron-stick"],"requires":["logistics-2","engine"],"has_modifier":false},"refined-flammables-1":{"unlocks":{},"requires":["flamethrower"],"has_modifier":true},"refined-flammables-2":{"unlocks":{},"requires":["refined-flammables-1"],"has_modifier":true},"refined-flammables-3":{"unlocks":{},"requires":["refined-flammables-2","chemical-science-pack"],"has_modifier":true},"refined-flammables-4":{"unlocks":{},"requires":["refined-flammables-3","utility-science-pack"],"has_modifier":true},"refined-flammables-5":{"unlocks":{},"requires":["refined-flammables-4"],"has_modifier":true},"refined-flammables-6":{"unlocks":{},"requires":["refined-flammables-5"],"has_modifier":true},"repair-pack":{"unlocks":["repair-pack"],"requires":["automation-science-pack"],"has_modifier":false},"research-speed-1":{"unlocks":{},"requires":["automation-2"],"has_modifier":true},"research-speed-2":{"unlocks":{},"requires":["research-speed-1"],"has_modifier":true},"research-speed-3":{"unlocks":{},"requires":["research-speed-2","chemical-science-pack"],"has_modifier":true},"research-speed-4":{"unlocks":{},"requires":["research-speed-3"],"has_modifier":true},"research-speed-5":{"unlocks":{},"requires":["research-speed-4","production-science-pack"],"has_modifier":true},"research-speed-6":{"unlocks":{},"requires":["research-speed-5","utility-science-pack"],"has_modifier":true},"robotics":{"unlocks":["flying-robot-frame"],"requires":["electric-engine","battery"],"has_modifier":false},"rocket-fuel":{"unlocks":["rocket-fuel"],"requires":["flammables","advanced-oil-processing"],"has_modifier":false},"rocket-silo":{"unlocks":["rocket-silo","rocket-part","cargo-landing-pad","satellite"],"requires":["concrete","rocket-fuel","electric-energy-accumulators","solar-energy","utility-science-pack","speed-module-3","productivity-module-3","radar"],"has_modifier":false},"rocketry":{"unlocks":["rocket-launcher","rocket"],"requires":["explosives","flammables","military-science-pack"],"has_modifier":false},"solar-energy":{"unlocks":["solar-panel"],"requires":["steel-processing","logistic-science-pack"],"has_modifier":false},"solar-panel-equipment":{"unlocks":["solar-panel-equipment"],"requires":["modular-armor","solar-energy"],"has_modifier":false},"space-science-pack":{"unlocks":{},"requires":["rocket-silo"],"has_modifier":false},"speed-module":{"unlocks":["speed-module"],"requires":["modules"],"has_modifier":false},"speed-module-2":{"unlocks":["speed-module-2"],"requires":["speed-module","processing-unit"],"has_modifier":false},"speed-module-3":{"unlocks":["speed-module-3"],"requires":["speed-module-2","production-science-pack"],"has_modifier":false},"spidertron":{"unlocks":["spidertron"],"requires":["military-4","exoskeleton-equipment","fission-reactor-equipment","rocketry","efficiency-module-3","radar"],"has_modifier":false},"steam-power":{"unlocks":["pipe","pipe-to-ground","offshore-pump","boiler","steam-engine"],"requires":{},"has_modifier":false},"steel-axe":{"unlocks":{},"requires":["steel-processing"],"has_modifier":true},"steel-processing":{"unlocks":["steel-plate","steel-chest"],"requires":["automation-science-pack"],"has_modifier":false},"stone-wall":{"unlocks":["stone-wall"],"requires":["automation-science-pack"],"has_modifier":false},"stronger-explosives-1":{"unlocks":{},"requires":["military-2"],"has_modifier":true},"stronger-explosives-2":{"unlocks":{},"requires":["stronger-explosives-1","military-science-pack"],"has_modifier":true},"stronger-explosives-3":{"unlocks":{},"requires":["stronger-explosives-2","chemical-science-pack"],"has_modifier":true},"stronger-explosives-4":{"unlocks":{},"requires":["stronger-explosives-3","utility-science-pack"],"has_modifier":true},"stronger-explosives-5":{"unlocks":{},"requires":["stronger-explosives-4"],"has_modifier":true},"stronger-explosives-6":{"unlocks":{},"requires":["stronger-explosives-5"],"has_modifier":true},"sulfur-processing":{"unlocks":["sulfuric-acid","sulfur"],"requires":["oil-processing"],"has_modifier":false},"tank":{"unlocks":["tank","cannon-shell","explosive-cannon-shell"],"requires":["automobilism","military-3","explosives"],"has_modifier":false},"toolbelt":{"unlocks":{},"requires":["logistic-science-pack"],"has_modifier":true},"uranium-ammo":{"unlocks":["uranium-rounds-magazine","uranium-cannon-shell","explosive-uranium-cannon-shell"],"requires":["uranium-processing","military-4","tank"],"has_modifier":false},"uranium-mining":{"unlocks":{},"requires":["chemical-science-pack","concrete"],"has_modifier":true},"uranium-processing":{"unlocks":["centrifuge","uranium-processing"],"requires":["uranium-mining"],"has_modifier":false},"utility-science-pack":{"unlocks":["utility-science-pack"],"requires":["robotics","processing-unit","low-density-structure"],"has_modifier":false},"weapon-shooting-speed-1":{"unlocks":{},"requires":["military"],"has_modifier":true},"weapon-shooting-speed-2":{"unlocks":{},"requires":["weapon-shooting-speed-1","logistic-science-pack"],"has_modifier":true},"weapon-shooting-speed-3":{"unlocks":{},"requires":["weapon-shooting-speed-2","military-science-pack"],"has_modifier":true},"weapon-shooting-speed-4":{"unlocks":{},"requires":["weapon-shooting-speed-3"],"has_modifier":true},"weapon-shooting-speed-5":{"unlocks":{},"requires":["weapon-shooting-speed-4","chemical-science-pack"],"has_modifier":true},"weapon-shooting-speed-6":{"unlocks":{},"requires":["weapon-shooting-speed-5","utility-science-pack"],"has_modifier":true},"worker-robots-speed-1":{"unlocks":{},"requires":["robotics"],"has_modifier":true},"worker-robots-speed-2":{"unlocks":{},"requires":["worker-robots-speed-1"],"has_modifier":true},"worker-robots-speed-3":{"unlocks":{},"requires":["worker-robots-speed-2","utility-science-pack"],"has_modifier":true},"worker-robots-speed-4":{"unlocks":{},"requires":["worker-robots-speed-3"],"has_modifier":true},"worker-robots-speed-5":{"unlocks":{},"requires":["worker-robots-speed-4","production-science-pack"],"has_modifier":true},"worker-robots-storage-1":{"unlocks":{},"requires":["robotics"],"has_modifier":true},"worker-robots-storage-2":{"unlocks":{},"requires":["worker-robots-storage-1","production-science-pack"],"has_modifier":true},"worker-robots-storage-3":{"unlocks":{},"requires":["worker-robots-storage-2","utility-science-pack"],"has_modifier":true}} \ No newline at end of file diff --git a/worlds/ffmq/Client.py b/worlds/ffmq/Client.py index 93688a6116f6..401c240a46ba 100644 --- a/worlds/ffmq/Client.py +++ b/worlds/ffmq/Client.py @@ -47,6 +47,17 @@ def get_flag(data, flag): bit = int(0x80 / (2 ** (flag % 8))) return (data[byte] & bit) > 0 +def validate_read_state(data1, data2): + validation_array = bytes([0x01, 0x46, 0x46, 0x4D, 0x51, 0x52]) + + if data1 is None or data2 is None: + return False + for i in range(6): + if data1[i] != validation_array[i] or data2[i] != validation_array[i]: + return False; + return True + + class FFMQClient(SNIClient): game = "Final Fantasy Mystic Quest" @@ -67,11 +78,11 @@ async def validate_rom(self, ctx): async def game_watcher(self, ctx): from SNIClient import snes_buffered_write, snes_flush_writes, snes_read - check_1 = await snes_read(ctx, 0xF53749, 1) + check_1 = await snes_read(ctx, 0xF53749, 6) received = await snes_read(ctx, RECEIVED_DATA[0], RECEIVED_DATA[1]) data = await snes_read(ctx, READ_DATA_START, READ_DATA_END - READ_DATA_START) - check_2 = await snes_read(ctx, 0xF53749, 1) - if check_1 != b'\x01' or check_2 != b'\x01': + check_2 = await snes_read(ctx, 0xF53749, 6) + if not validate_read_state(check_1, check_2): return def get_range(data_range): diff --git a/worlds/generic/Rules.py b/worlds/generic/Rules.py index e930c4b8d6e9..31d725bff722 100644 --- a/worlds/generic/Rules.py +++ b/worlds/generic/Rules.py @@ -69,7 +69,7 @@ def forbid(sender: int, receiver: int, items: typing.Set[str]): if (location.player, location.item_rule) in func_cache: location.item_rule = func_cache[location.player, location.item_rule] # empty rule that just returns True, overwrite - elif location.item_rule is location.__class__.item_rule: + elif location.item_rule is Location.item_rule: func_cache[location.player, location.item_rule] = location.item_rule = \ lambda i, sending_blockers = forbid_data[location.player], \ old_rule = location.item_rule: \ @@ -103,7 +103,7 @@ def set_rule(spot: typing.Union["BaseClasses.Location", "BaseClasses.Entrance"], def add_rule(spot: typing.Union["BaseClasses.Location", "BaseClasses.Entrance"], rule: CollectionRule, combine="and"): old_rule = spot.access_rule # empty rule, replace instead of add - if old_rule is spot.__class__.access_rule: + if old_rule is Location.access_rule or old_rule is Entrance.access_rule: spot.access_rule = rule if combine == "and" else old_rule else: if combine == "and": @@ -115,7 +115,7 @@ def add_rule(spot: typing.Union["BaseClasses.Location", "BaseClasses.Entrance"], def forbid_item(location: "BaseClasses.Location", item: str, player: int): old_rule = location.item_rule # empty rule - if old_rule is location.__class__.item_rule: + if old_rule is Location.item_rule: location.item_rule = lambda i: i.name != item or i.player != player else: location.item_rule = lambda i: (i.name != item or i.player != player) and old_rule(i) @@ -135,7 +135,7 @@ def forbid_items(location: "BaseClasses.Location", items: typing.Set[str]): def add_item_rule(location: "BaseClasses.Location", rule: ItemRule, combine: str = "and"): old_rule = location.item_rule # empty rule, replace instead of add - if old_rule is location.__class__.item_rule: + if old_rule is Location.item_rule: location.item_rule = rule if combine == "and" else old_rule else: if combine == "and": diff --git a/worlds/hk/Extractor.py b/worlds/hk/Extractor.py index 61fabc4da0d9..866608489ec2 100644 --- a/worlds/hk/Extractor.py +++ b/worlds/hk/Extractor.py @@ -9,11 +9,7 @@ import jinja2 -try: - from ast import unparse -except ImportError: - # Py 3.8 and earlier compatibility module - from astunparse import unparse +from ast import unparse from Utils import get_text_between diff --git a/worlds/hk/Options.py b/worlds/hk/Options.py index c1206d41ee2c..fc8eae1c0aa3 100644 --- a/worlds/hk/Options.py +++ b/worlds/hk/Options.py @@ -77,7 +77,7 @@ "RandomizeLoreTablets": "Randomize Lore items into the itempool, one per Lore Tablet, and place randomized item " "grants on the tablets themselves.\n You must still read the tablet to get the item.", "PreciseMovement": "Places skips into logic which require extremely precise player movement, possibly without " - "movement skills such as\n dash or hook.", + "movement skills such as\n dash or claw.", "ProficientCombat": "Places skips into logic which require proficient combat, possibly with limited items.", "BackgroundObjectPogos": "Places skips into logic for locations which are reachable via pogoing off of " "background objects.", diff --git a/worlds/hk/__init__.py b/worlds/hk/__init__.py index 9ec77e6bf0cd..aede8e59cca5 100644 --- a/worlds/hk/__init__.py +++ b/worlds/hk/__init__.py @@ -231,7 +231,7 @@ def create_regions(self): all_event_names.update(set(godhome_event_names)) # Link regions - for event_name in all_event_names: + for event_name in sorted(all_event_names): #if event_name in wp_exclusions: # continue loc = HKLocation(self.player, event_name, None, menu_region) @@ -509,9 +509,13 @@ def set_goal(player, grub_rule: typing.Callable[[CollectionState], bool]): per_player_grubs_per_player[player][player] += 1 if grub.location and grub.location.player in group_lookup.keys(): - for real_player in group_lookup[grub.location.player]: + # will count the item linked grub instead + pass + elif player in group_lookup: + for real_player in group_lookup[player]: grub_count_per_player[real_player] += 1 else: + # for non-linked grubs grub_count_per_player[player] += 1 for player, count in grub_count_per_player.items(): diff --git a/worlds/hk/requirements.txt b/worlds/hk/requirements.txt deleted file mode 100644 index 1b410ffb2aed..000000000000 --- a/worlds/hk/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -astunparse>=1.6.3; python_version <= '3.8' \ No newline at end of file diff --git a/worlds/hk/test/__init__.py b/worlds/hk/test/__init__.py new file mode 100644 index 000000000000..c41d20127fcc --- /dev/null +++ b/worlds/hk/test/__init__.py @@ -0,0 +1,62 @@ +import typing +from argparse import Namespace +from BaseClasses import CollectionState, MultiWorld +from Options import ItemLinks +from test.bases import WorldTestBase +from worlds.AutoWorld import AutoWorldRegister, call_all +from .. import HKWorld + + +class linkedTestHK(): + run_default_tests = False + game = "Hollow Knight" + world: HKWorld + expected_grubs: int + item_link_group: typing.List[typing.Dict[str, typing.Any]] + + def setup_item_links(self, args): + setattr(args, "item_links", + { + 1: ItemLinks.from_any(self.item_link_group), + 2: ItemLinks.from_any([{ + "name": "ItemLinkTest", + "item_pool": ["Grub"], + "link_replacement": False, + "replacement_item": "One_Geo", + }]) + }) + return args + + def world_setup(self) -> None: + """ + Create a multiworld with two players that share an itemlink + """ + self.multiworld = MultiWorld(2) + self.multiworld.game = {1: self.game, 2: self.game} + self.multiworld.player_name = {1: "Linker 1", 2: "Linker 2"} + self.multiworld.set_seed() + args = Namespace() + options_dataclass = AutoWorldRegister.world_types[self.game].options_dataclass + for name, option in options_dataclass.type_hints.items(): + setattr(args, name, { + 1: option.from_any(self.options.get(name, option.default)), + 2: option.from_any(self.options.get(name, option.default)) + }) + args = self.setup_item_links(args) + self.multiworld.set_options(args) + self.multiworld.set_item_links() + # groups get added to state during its constructor so this has to be after item links are set + self.multiworld.state = CollectionState(self.multiworld) + gen_steps = ("generate_early", "create_regions", "create_items", "set_rules", "generate_basic") + for step in gen_steps: + call_all(self.multiworld, step) + # link the items together and stop at prefill + self.multiworld.link_items() + self.multiworld._all_state = None + call_all(self.multiworld, "pre_fill") + + self.world = self.multiworld.worlds[self.player] + + def test_grub_count(self) -> None: + assert self.world.grub_count == self.expected_grubs, \ + f"Expected {self.expected_grubs} but found {self.world.grub_count}" diff --git a/worlds/hk/test/test_grub_count.py b/worlds/hk/test/test_grub_count.py new file mode 100644 index 000000000000..dba15b614dd9 --- /dev/null +++ b/worlds/hk/test/test_grub_count.py @@ -0,0 +1,165 @@ +from . import linkedTestHK, WorldTestBase +from Options import ItemLinks + + +class test_grubcount_limited(linkedTestHK, WorldTestBase): + options = { + "RandomizeGrubs": True, + "GrubHuntGoal": 20, + "Goal": "any", + } + item_link_group = [{ + "name": "ItemLinkTest", + "item_pool": ["Grub"], + "link_replacement": True, + "replacement_item": "Grub", + }] + expected_grubs = 20 + + +class test_grubcount_default(linkedTestHK, WorldTestBase): + options = { + "RandomizeGrubs": True, + "Goal": "any", + } + item_link_group = [{ + "name": "ItemLinkTest", + "item_pool": ["Grub"], + "link_replacement": True, + "replacement_item": "Grub", + }] + expected_grubs = 46 + + +class test_grubcount_all_unlinked(linkedTestHK, WorldTestBase): + options = { + "RandomizeGrubs": True, + "GrubHuntGoal": "all", + "Goal": "any", + } + item_link_group = [] + expected_grubs = 46 + + +class test_grubcount_all_linked(linkedTestHK, WorldTestBase): + options = { + "RandomizeGrubs": True, + "GrubHuntGoal": "all", + "Goal": "any", + } + item_link_group = [{ + "name": "ItemLinkTest", + "item_pool": ["Grub"], + "link_replacement": True, + "replacement_item": "Grub", + }] + expected_grubs = 46 + 23 + + +class test_replacement_only(linkedTestHK, WorldTestBase): + options = { + "RandomizeGrubs": True, + "GrubHuntGoal": "all", + "Goal": "any", + } + expected_grubs = 46 + 18 # the count of grubs + skills removed from item links + + def setup_item_links(self, args): + setattr(args, "item_links", + { + 1: ItemLinks.from_any([{ + "name": "ItemLinkTest", + "item_pool": ["Skills"], + "link_replacement": True, + "replacement_item": "Grub", + }]), + 2: ItemLinks.from_any([{ + "name": "ItemLinkTest", + "item_pool": ["Skills"], + "link_replacement": True, + "replacement_item": "Grub", + }]) + }) + return args + + +class test_replacement_only_unlinked(linkedTestHK, WorldTestBase): + options = { + "RandomizeGrubs": True, + "GrubHuntGoal": "all", + "Goal": "any", + } + expected_grubs = 46 + 9 # Player1s replacement Grubs + + def setup_item_links(self, args): + setattr(args, "item_links", + { + 1: ItemLinks.from_any([{ + "name": "ItemLinkTest", + "item_pool": ["Skills"], + "link_replacement": False, + "replacement_item": "Grub", + }]), + 2: ItemLinks.from_any([{ + "name": "ItemLinkTest", + "item_pool": ["Skills"], + "link_replacement": False, + "replacement_item": "Grub", + }]) + }) + return args + + +class test_ignore_others(linkedTestHK, WorldTestBase): + options = { + "RandomizeGrubs": True, + "GrubHuntGoal": "all", + "Goal": "any", + } + # player2 has more than 46 grubs but they are unlinked so player1s grubs are vanilla + expected_grubs = 46 + + def setup_item_links(self, args): + setattr(args, "item_links", + { + 1: ItemLinks.from_any([{ + "name": "ItemLinkTest", + "item_pool": ["Skills"], + "link_replacement": False, + "replacement_item": "One_Geo", + }]), + 2: ItemLinks.from_any([{ + "name": "ItemLinkTest", + "item_pool": ["Skills"], + "link_replacement": False, + "replacement_item": "Grub", + }]) + }) + return args + + +class test_replacement_only_linked(linkedTestHK, WorldTestBase): + options = { + "RandomizeGrubs": True, + "GrubHuntGoal": "all", + "Goal": "any", + } + expected_grubs = 46 + 9 # Player2s linkreplacement grubs + + def setup_item_links(self, args): + setattr(args, "item_links", + { + 1: ItemLinks.from_any([{ + "name": "ItemLinkTest", + "item_pool": ["Skills"], + "link_replacement": True, + "replacement_item": "One_Geo", + }]), + 2: ItemLinks.from_any([{ + "name": "ItemLinkTest", + "item_pool": ["Skills"], + "link_replacement": True, + "replacement_item": "Grub", + }]) + }) + return args diff --git a/worlds/kdl3/__init__.py b/worlds/kdl3/__init__.py index f01c82dd16a3..1b5acbe97a3c 100644 --- a/worlds/kdl3/__init__.py +++ b/worlds/kdl3/__init__.py @@ -325,7 +325,7 @@ def generate_basic(self) -> None: def generate_output(self, output_directory: str) -> None: try: - patch = KDL3ProcedurePatch() + patch = KDL3ProcedurePatch(player=self.player, player_name=self.player_name) patch_rom(self, patch) self.rom_name = patch.name diff --git a/worlds/kh1/Rules.py b/worlds/kh1/Rules.py index e1f72f5b3e54..130238e5048e 100644 --- a/worlds/kh1/Rules.py +++ b/worlds/kh1/Rules.py @@ -235,6 +235,11 @@ def set_rules(kh1world): lambda state: ( state.has("Progressive Glide", player) or + ( + state.has("High Jump", player, 2) + and state.has("Footprints", player) + ) + or ( options.advanced_logic and state.has_all({ @@ -246,6 +251,11 @@ def set_rules(kh1world): lambda state: ( state.has("Progressive Glide", player) or + ( + state.has("High Jump", player, 2) + and state.has("Footprints", player) + ) + or ( options.advanced_logic and state.has_all({ @@ -258,7 +268,6 @@ def set_rules(kh1world): state.has("Footprints", player) or (options.advanced_logic and state.has("Progressive Glide", player)) - or state.has("High Jump", player, 2) )) add_rule(kh1world.get_location("Wonderland Tea Party Garden Across From Bizarre Room Entrance Chest"), lambda state: ( @@ -376,7 +385,7 @@ def set_rules(kh1world): lambda state: state.has("White Trinity", player)) add_rule(kh1world.get_location("Monstro Chamber 6 Other Platform Chest"), lambda state: ( - state.has("High Jump", player) + state.has_all(("High Jump", "Progressive Glide"), player) or (options.advanced_logic and state.has("Combo Master", player)) )) add_rule(kh1world.get_location("Monstro Chamber 6 Platform Near Chamber 5 Entrance Chest"), @@ -386,7 +395,7 @@ def set_rules(kh1world): )) add_rule(kh1world.get_location("Monstro Chamber 6 Raised Area Near Chamber 1 Entrance Chest"), lambda state: ( - state.has("High Jump", player) + state.has_all(("High Jump", "Progressive Glide"), player) or (options.advanced_logic and state.has("Combo Master", player)) )) add_rule(kh1world.get_location("Halloween Town Moonlight Hill White Trinity Chest"), @@ -595,6 +604,7 @@ def set_rules(kh1world): lambda state: ( state.has("Green Trinity", player) and has_all_magic_lvx(state, player, 2) + and has_defensive_tools(state, player) )) add_rule(kh1world.get_location("Neverland Hold Flight 2nd Chest"), lambda state: ( @@ -710,8 +720,7 @@ def set_rules(kh1world): lambda state: state.has("White Trinity", player)) add_rule(kh1world.get_location("End of the World Giant Crevasse 5th Chest"), lambda state: ( - state.has("High Jump", player) - or state.has("Progressive Glide", player) + state.has("Progressive Glide", player) )) add_rule(kh1world.get_location("End of the World Giant Crevasse 1st Chest"), lambda state: ( @@ -1441,10 +1450,11 @@ def set_rules(kh1world): has_emblems(state, player, options.keyblades_unlock_chests) and has_x_worlds(state, player, 7, options.keyblades_unlock_chests) and has_defensive_tools(state, player) + and state.has("Progressive Blizzard", player, 3) )) add_rule(kh1world.get_location("Agrabah Defeat Kurt Zisa Zantetsuken Event"), lambda state: ( - has_emblems(state, player, options.keyblades_unlock_chests) and has_x_worlds(state, player, 7, options.keyblades_unlock_chests) and has_defensive_tools(state, player) + has_emblems(state, player, options.keyblades_unlock_chests) and has_x_worlds(state, player, 7, options.keyblades_unlock_chests) and has_defensive_tools(state, player) and state.has("Progressive Blizzard", player, 3) )) if options.super_bosses or options.goal.current_key == "sephiroth": add_rule(kh1world.get_location("Olympus Coliseum Defeat Sephiroth Ansem's Report 12"), diff --git a/worlds/kh2/__init__.py b/worlds/kh2/__init__.py index faf0bed88567..2809460aed6a 100644 --- a/worlds/kh2/__init__.py +++ b/worlds/kh2/__init__.py @@ -101,7 +101,18 @@ def fill_slot_data(self) -> dict: if ability in self.goofy_ability_dict and self.goofy_ability_dict[ability] >= 1: self.goofy_ability_dict[ability] -= 1 - slot_data = self.options.as_dict("Goal", "FinalXemnas", "LuckyEmblemsRequired", "BountyRequired") + slot_data = self.options.as_dict( + "Goal", + "FinalXemnas", + "LuckyEmblemsRequired", + "BountyRequired", + "FightLogic", + "FinalFormLogic", + "AutoFormLogic", + "LevelDepth", + "DonaldGoofyStatsanity", + "CorSkipToggle" + ) slot_data.update({ "hitlist": [], # remove this after next update "PoptrackerVersionCheck": 4.3, diff --git a/worlds/kh2/docs/setup_en.md b/worlds/kh2/docs/setup_en.md index ed4d90bb54fb..9fe9b23a1350 100644 --- a/worlds/kh2/docs/setup_en.md +++ b/worlds/kh2/docs/setup_en.md @@ -36,10 +36,16 @@ When you generate a game you will see a download link for a KH2 .zip seed on the Make sure the seed is on the top of the list (Highest Priority)
After Installing the seed click `Mod Loader -> Build/Build and Run`. Every slot is a unique mod to install and will be needed be repatched for different slots/rooms. +

Optional Software:

+ +- [Kingdom Hearts 2 AP Tracker](https://github.com/palex00/kh2-ap-tracker/releases/latest/), for use with +[PopTracker](https://github.com/black-sliver/PopTracker/releases) +

What the Mod Manager Should Look Like.

![image](https://i.imgur.com/Si4oZ8w.png) +

Using the KH2 Client

Once you have started the game through OpenKH Mod Manager and are on the title screen run the [ArchipelagoKH2Client.exe](https://github.com/ArchipelagoMW/Archipelago/releases).
@@ -73,10 +79,24 @@ Enter `The room's port number` into the top box where the x's are and pr - Run the game in windows/borderless windowed mode. Fullscreen is stable but the game can crash if you alt-tab out. - Make sure to save in a different save slot when playing in an async or disconnecting from the server to play a different seed -

Logic Sheet

+

Logic Sheet & PopTracker Autotracking

Have any questions on what's in logic? This spreadsheet made by Bulcon has the answer [Requirements/logic sheet](https://docs.google.com/spreadsheets/d/1nNi8ohEs1fv-sDQQRaP45o6NoRcMlLJsGckBonweDMY/edit?usp=sharing) +Alternatively you can use the Kingdom Hearts 2 PopTracker Pack that is based off of the logic sheet above and does all the work for you. + +

PopTracker Pack

+ +1. Download [Kingdom Hearts 2 AP Tracker](https://github.com/palex00/kh2-ap-tracker/releases/latest/) and +[PopTracker](https://github.com/black-sliver/PopTracker/releases). +2. Put the tracker pack into packs/ in your PopTracker install. +3. Open PopTracker, and load the Kingdom Hearts 2 pack. +4. For autotracking, click on the "AP" symbol at the top. +5. Enter the Archipelago server address (the one you connected your client to), slot name, and password. + +This pack will handle logic, received items, checked locations and autotabbing for you! + +

F.A.Q.

- Why is my Client giving me a "Cannot Open Process: " error? diff --git a/worlds/ladx/test/__init__.py b/worlds/ladx/test/__init__.py index 0e616ac557d0..059a09b0728d 100644 --- a/worlds/ladx/test/__init__.py +++ b/worlds/ladx/test/__init__.py @@ -1,4 +1,4 @@ -from test.TestBase import WorldTestBase +from test.bases import WorldTestBase from ..Common import LINKS_AWAKENING class LADXTestBase(WorldTestBase): game = LINKS_AWAKENING diff --git a/worlds/landstalker/Locations.py b/worlds/landstalker/Locations.py index b0148269eab3..0fe63526c63b 100644 --- a/worlds/landstalker/Locations.py +++ b/worlds/landstalker/Locations.py @@ -34,7 +34,7 @@ def create_locations(player: int, regions_table: Dict[str, LandstalkerRegion], n for data in WORLD_PATHS_JSON: if "requiredNodes" in data: regions_with_entrance_checks.extend([region_id for region_id in data["requiredNodes"]]) - regions_with_entrance_checks = list(set(regions_with_entrance_checks)) + regions_with_entrance_checks = sorted(set(regions_with_entrance_checks)) for region_id in regions_with_entrance_checks: region = regions_table[region_id] location = LandstalkerLocation(player, 'event_visited_' + region_id, None, region, "event") diff --git a/worlds/landstalker/__init__.py b/worlds/landstalker/__init__.py index 2b3dc41239c3..8463e56e54c1 100644 --- a/worlds/landstalker/__init__.py +++ b/worlds/landstalker/__init__.py @@ -38,7 +38,7 @@ class LandstalkerWorld(World): item_name_to_id = build_item_name_to_id_table() location_name_to_id = build_location_name_to_id_table() - cached_spheres: ClassVar[List[Set[Location]]] + cached_spheres: List[Set[Location]] def __init__(self, multiworld, player): super().__init__(multiworld, player) @@ -47,6 +47,7 @@ def __init__(self, multiworld, player): self.dark_region_ids = [] self.teleport_tree_pairs = [] self.jewel_items = [] + self.cached_spheres = [] def fill_slot_data(self) -> dict: # Generate hints. @@ -220,14 +221,17 @@ def get_starting_health(self): return 4 @classmethod - def stage_post_fill(cls, multiworld): + def stage_post_fill(cls, multiworld: MultiWorld): # Cache spheres for hint calculation after fill completes. - cls.cached_spheres = list(multiworld.get_spheres()) + cached_spheres = list(multiworld.get_spheres()) + for world in multiworld.get_game_worlds(cls.game): + world.cached_spheres = cached_spheres @classmethod - def stage_modify_multidata(cls, *_): + def stage_modify_multidata(cls, multiworld: MultiWorld, *_): # Clean up all references in cached spheres after generation completes. - del cls.cached_spheres + for world in multiworld.get_game_worlds(cls.game): + world.cached_spheres = [] def adjust_shop_prices(self): # Calculate prices for items in shops once all items have their final position diff --git a/worlds/lingo/data/LL1.yaml b/worlds/lingo/data/LL1.yaml index bbed1464530b..3783b68af98c 100644 --- a/worlds/lingo/data/LL1.yaml +++ b/worlds/lingo/data/LL1.yaml @@ -1966,7 +1966,10 @@ entrances: The Observant: warp: True - Eight Room: True + Eight Room: + # It is possible to get to the second floor warpless, but there are no warpless exits from the second floor, + # meaning that this connection is essentially always a warp for the purposes of Pilgrimage. + warp: True Eight Alcove: door: Eight Door Orange Tower Sixth Floor: diff --git a/worlds/lingo/data/generated.dat b/worlds/lingo/data/generated.dat index 789fc0856d62..9abb0276c8b5 100644 Binary files a/worlds/lingo/data/generated.dat and b/worlds/lingo/data/generated.dat differ diff --git a/worlds/lingo/options.py b/worlds/lingo/options.py index 2fd57ff5ede3..2d6e9967dfc4 100644 --- a/worlds/lingo/options.py +++ b/worlds/lingo/options.py @@ -80,10 +80,15 @@ class ShuffleColors(DefaultOnToggle): class ShufflePanels(Choice): - """If on, the puzzles on each panel are randomized. + """Determines how panel puzzles are randomized. - On "rearrange", the puzzles are the same as the ones in the base game, but - are placed in different areas. + - **None:** Most panels remain the same as in the base game. Note that there are + some panels (in particular, in Starting Room and Second Room) that are changed + by the randomizer even when panel shuffle is disabled. + - **Rearrange:** The puzzles are the same as the ones in the base game, but are + placed in different areas. + + More options for puzzle randomization are planned in the future. """ display_name = "Shuffle Panels" option_none = 0 diff --git a/worlds/lufia2ac/__init__.py b/worlds/lufia2ac/__init__.py index 6433452cefea..96de24a4b6a0 100644 --- a/worlds/lufia2ac/__init__.py +++ b/worlds/lufia2ac/__init__.py @@ -118,7 +118,7 @@ def create_regions(self) -> None: L2ACItem("Progressive chest access", ItemClassification.progression, None, self.player)) chest_access.show_in_spoiler = False ancient_dungeon.locations.append(chest_access) - for iris in self.item_name_groups["Iris treasures"]: + for iris in sorted(self.item_name_groups["Iris treasures"]): treasure_name: str = f"Iris treasure {self.item_name_to_id[iris] - self.item_name_to_id['Iris sword'] + 1}" iris_treasure: Location = \ L2ACLocation(self.player, treasure_name, self.location_name_to_id[treasure_name], ancient_dungeon) diff --git a/worlds/lufia2ac/test/__init__.py b/worlds/lufia2ac/test/__init__.py index 24925675e36b..306ffa771660 100644 --- a/worlds/lufia2ac/test/__init__.py +++ b/worlds/lufia2ac/test/__init__.py @@ -1,4 +1,4 @@ -from test.TestBase import WorldTestBase +from test.bases import WorldTestBase class L2ACTestBase(WorldTestBase): diff --git a/worlds/messenger/__init__.py b/worlds/messenger/__init__.py index 9a38953ffbdf..59e724d3fb7f 100644 --- a/worlds/messenger/__init__.py +++ b/worlds/messenger/__init__.py @@ -1,5 +1,5 @@ import logging -from typing import Any, ClassVar, Dict, List, Optional, Set, TextIO +from typing import Any, ClassVar, TextIO from BaseClasses import CollectionState, Entrance, Item, ItemClassification, MultiWorld, Tutorial from Options import Accessibility @@ -120,16 +120,16 @@ class MessengerWorld(World): required_seals: int = 0 created_seals: int = 0 total_shards: int = 0 - shop_prices: Dict[str, int] - figurine_prices: Dict[str, int] - _filler_items: List[str] - starting_portals: List[str] - plando_portals: List[str] - spoiler_portal_mapping: Dict[str, str] - portal_mapping: List[int] - transitions: List[Entrance] + shop_prices: dict[str, int] + figurine_prices: dict[str, int] + _filler_items: list[str] + starting_portals: list[str] + plando_portals: list[str] + spoiler_portal_mapping: dict[str, str] + portal_mapping: list[int] + transitions: list[Entrance] reachable_locs: int = 0 - filler: Dict[str, int] + filler: dict[str, int] def generate_early(self) -> None: if self.options.goal == Goal.option_power_seal_hunt: @@ -178,7 +178,7 @@ def create_regions(self) -> None: for reg_name in sub_region] for region in complex_regions: - region_name = region.name.replace(f"{region.parent} - ", "") + region_name = region.name.removeprefix(f"{region.parent} - ") connection_data = CONNECTIONS[region.parent][region_name] for exit_region in connection_data: region.connect(self.multiworld.get_region(exit_region, self.player)) @@ -191,7 +191,7 @@ def create_items(self) -> None: # create items that are always in the item pool main_movement_items = ["Rope Dart", "Wingsuit"] precollected_names = [item.name for item in self.multiworld.precollected_items[self.player]] - itempool: List[MessengerItem] = [ + itempool: list[MessengerItem] = [ self.create_item(item) for item in self.item_name_to_id if item not in { @@ -290,7 +290,7 @@ def write_spoiler_header(self, spoiler_handle: TextIO) -> None: for portal, output in portal_info: spoiler.set_entrance(f"{portal} Portal", output, "I can write anything I want here lmao", self.player) - def fill_slot_data(self) -> Dict[str, Any]: + def fill_slot_data(self) -> dict[str, Any]: slot_data = { "shop": {SHOP_ITEMS[item].internal_name: price for item, price in self.shop_prices.items()}, "figures": {FIGURINES[item].internal_name: price for item, price in self.figurine_prices.items()}, @@ -316,7 +316,7 @@ def get_filler_item_name(self) -> str: return self._filler_items.pop(0) def create_item(self, name: str) -> MessengerItem: - item_id: Optional[int] = self.item_name_to_id.get(name, None) + item_id: int | None = self.item_name_to_id.get(name, None) return MessengerItem( name, ItemClassification.progression if item_id is None else self.get_item_classification(name), @@ -351,7 +351,7 @@ def get_item_classification(self, name: str) -> ItemClassification: return ItemClassification.filler @classmethod - def create_group(cls, multiworld: "MultiWorld", new_player_id: int, players: Set[int]) -> World: + def create_group(cls, multiworld: "MultiWorld", new_player_id: int, players: set[int]) -> World: group = super().create_group(multiworld, new_player_id, players) assert isinstance(group, MessengerWorld) diff --git a/worlds/messenger/client_setup.py b/worlds/messenger/client_setup.py index 77a0f634326c..6b98a1b44013 100644 --- a/worlds/messenger/client_setup.py +++ b/worlds/messenger/client_setup.py @@ -5,7 +5,7 @@ import subprocess import urllib.request from shutil import which -from typing import Any, Optional +from typing import Any from zipfile import ZipFile from Utils import open_file @@ -17,7 +17,7 @@ MOD_URL = "https://api.github.com/repos/alwaysintreble/TheMessengerRandomizerModAP/releases/latest" -def ask_yes_no_cancel(title: str, text: str) -> Optional[bool]: +def ask_yes_no_cancel(title: str, text: str) -> bool | None: """ Wrapper for tkinter.messagebox.askyesnocancel, that creates a popup dialog box with yes, no, and cancel buttons. @@ -33,7 +33,6 @@ def ask_yes_no_cancel(title: str, text: str) -> Optional[bool]: return ret - def launch_game(*args) -> None: """Check the game installation, then launch it""" def courier_installed() -> bool: diff --git a/worlds/messenger/connections.py b/worlds/messenger/connections.py index 69dd7aa7f286..79912a5688c2 100644 --- a/worlds/messenger/connections.py +++ b/worlds/messenger/connections.py @@ -1,6 +1,4 @@ -from typing import Dict, List - -CONNECTIONS: Dict[str, Dict[str, List[str]]] = { +CONNECTIONS: dict[str, dict[str, list[str]]] = { "Ninja Village": { "Right": [ "Autumn Hills - Left", @@ -640,7 +638,7 @@ }, } -RANDOMIZED_CONNECTIONS: Dict[str, str] = { +RANDOMIZED_CONNECTIONS: dict[str, str] = { "Ninja Village - Right": "Autumn Hills - Left", "Autumn Hills - Left": "Ninja Village - Right", "Autumn Hills - Right": "Forlorn Temple - Left", @@ -680,7 +678,7 @@ "Sunken Shrine - Left": "Howling Grotto - Bottom", } -TRANSITIONS: List[str] = [ +TRANSITIONS: list[str] = [ "Ninja Village - Right", "Autumn Hills - Left", "Autumn Hills - Right", diff --git a/worlds/messenger/constants.py b/worlds/messenger/constants.py index ea15c71068db..47b5a1a85cff 100644 --- a/worlds/messenger/constants.py +++ b/worlds/messenger/constants.py @@ -2,7 +2,7 @@ # items # listing individual groups first for easy lookup -NOTES = [ +NOTES: list[str] = [ "Key of Hope", "Key of Chaos", "Key of Courage", @@ -11,7 +11,7 @@ "Key of Symbiosis", ] -PROG_ITEMS = [ +PROG_ITEMS: list[str] = [ "Wingsuit", "Rope Dart", "Lightfoot Tabi", @@ -28,18 +28,18 @@ "Seashell", ] -PHOBEKINS = [ +PHOBEKINS: list[str] = [ "Necro", "Pyro", "Claustro", "Acro", ] -USEFUL_ITEMS = [ +USEFUL_ITEMS: list[str] = [ "Windmill Shuriken", ] -FILLER = { +FILLER: dict[str, int] = { "Time Shard": 5, "Time Shard (10)": 10, "Time Shard (50)": 20, @@ -48,13 +48,13 @@ "Time Shard (500)": 5, } -TRAPS = { +TRAPS: dict[str, int] = { "Teleport Trap": 5, "Prophecy Trap": 10, } # item_name_to_id needs to be deterministic and match upstream -ALL_ITEMS = [ +ALL_ITEMS: list[str] = [ *NOTES, "Windmill Shuriken", "Wingsuit", @@ -83,7 +83,7 @@ # locations # the names of these don't actually matter, but using the upstream's names for now # order must be exactly the same as upstream -ALWAYS_LOCATIONS = [ +ALWAYS_LOCATIONS: list[str] = [ # notes "Sunken Shrine - Key of Love", "Corrupted Future - Key of Courage", @@ -160,7 +160,7 @@ "Elemental Skylands Seal - Fire", ] -BOSS_LOCATIONS = [ +BOSS_LOCATIONS: list[str] = [ "Autumn Hills - Leaf Golem", "Catacombs - Ruxxtin", "Howling Grotto - Emerald Golem", diff --git a/worlds/messenger/options.py b/worlds/messenger/options.py index 59e694cd3963..8b61a9435422 100644 --- a/worlds/messenger/options.py +++ b/worlds/messenger/options.py @@ -1,5 +1,4 @@ from dataclasses import dataclass -from typing import Dict from schema import And, Optional, Or, Schema @@ -167,7 +166,7 @@ class ShopPrices(Range): default = 100 -def planned_price(location: str) -> Dict[Optional, Or]: +def planned_price(location: str) -> dict[Optional, Or]: return { Optional(location): Or( And(int, lambda n: n >= 0), diff --git a/worlds/messenger/portals.py b/worlds/messenger/portals.py index 17152a1a1538..896fefa686f1 100644 --- a/worlds/messenger/portals.py +++ b/worlds/messenger/portals.py @@ -1,5 +1,5 @@ from copy import deepcopy -from typing import List, TYPE_CHECKING +from typing import TYPE_CHECKING from BaseClasses import CollectionState, PlandoOptions from Options import PlandoConnection @@ -8,7 +8,7 @@ from . import MessengerWorld -PORTALS = [ +PORTALS: list[str] = [ "Autumn Hills", "Riviere Turquoise", "Howling Grotto", @@ -18,7 +18,7 @@ ] -SHOP_POINTS = { +SHOP_POINTS: dict[str, list[str]] = { "Autumn Hills": [ "Climbing Claws", "Hope Path", @@ -113,7 +113,7 @@ } -CHECKPOINTS = { +CHECKPOINTS: dict[str, list[str]] = { "Autumn Hills": [ "Hope Latch", "Key of Hope", @@ -186,7 +186,7 @@ } -REGION_ORDER = [ +REGION_ORDER: list[str] = [ "Autumn Hills", "Forlorn Temple", "Catacombs", @@ -228,7 +228,7 @@ def create_mapping(in_portal: str, warp: str) -> str: return parent - def handle_planned_portals(plando_connections: List[PlandoConnection]) -> None: + def handle_planned_portals(plando_connections: list[PlandoConnection]) -> None: """checks the provided plando connections for portals and connects them""" nonlocal available_portals diff --git a/worlds/messenger/regions.py b/worlds/messenger/regions.py index 153f8510f1bd..d53b84fe3401 100644 --- a/worlds/messenger/regions.py +++ b/worlds/messenger/regions.py @@ -1,7 +1,4 @@ -from typing import Dict, List - - -LOCATIONS: Dict[str, List[str]] = { +LOCATIONS: dict[str, list[str]] = { "Ninja Village - Nest": [ "Ninja Village - Candle", "Ninja Village - Astral Seed", @@ -201,7 +198,7 @@ } -SUB_REGIONS: Dict[str, List[str]] = { +SUB_REGIONS: dict[str, list[str]] = { "Ninja Village": [ "Right", ], @@ -385,7 +382,7 @@ # order is slightly funky here for back compat -MEGA_SHARDS: Dict[str, List[str]] = { +MEGA_SHARDS: dict[str, list[str]] = { "Autumn Hills - Lakeside Checkpoint": ["Autumn Hills Mega Shard"], "Forlorn Temple - Outside Shop": ["Hidden Entrance Mega Shard"], "Catacombs - Top Left": ["Catacombs Mega Shard"], @@ -414,7 +411,7 @@ } -REGION_CONNECTIONS: Dict[str, Dict[str, str]] = { +REGION_CONNECTIONS: dict[str, dict[str, str]] = { "Menu": {"Tower HQ": "Start Game"}, "Tower HQ": { "Autumn Hills - Portal": "ToTHQ Autumn Hills Portal", @@ -436,7 +433,7 @@ # regions that don't have sub-regions -LEVELS: List[str] = [ +LEVELS: list[str] = [ "Menu", "Tower HQ", "The Shop", diff --git a/worlds/messenger/rules.py b/worlds/messenger/rules.py index 85b73dec4147..f09025c7edce 100644 --- a/worlds/messenger/rules.py +++ b/worlds/messenger/rules.py @@ -1,4 +1,4 @@ -from typing import Dict, TYPE_CHECKING +from typing import TYPE_CHECKING from BaseClasses import CollectionState from worlds.generic.Rules import CollectionRule, add_rule, allow_self_locking_items @@ -12,9 +12,9 @@ class MessengerRules: player: int world: "MessengerWorld" - connection_rules: Dict[str, CollectionRule] - region_rules: Dict[str, CollectionRule] - location_rules: Dict[str, CollectionRule] + connection_rules: dict[str, CollectionRule] + region_rules: dict[str, CollectionRule] + location_rules: dict[str, CollectionRule] maximum_price: int required_seals: int @@ -220,6 +220,8 @@ def __init__(self, world: "MessengerWorld") -> None: } self.location_rules = { + # hq + "Money Wrench": self.can_shop, # ninja village "Ninja Village Seal - Tree House": self.has_dart, diff --git a/worlds/messenger/shop.py b/worlds/messenger/shop.py index 3c8c7bf6f21e..6ab72f9765f3 100644 --- a/worlds/messenger/shop.py +++ b/worlds/messenger/shop.py @@ -1,11 +1,11 @@ -from typing import Dict, List, NamedTuple, Optional, Set, TYPE_CHECKING, Tuple, Union +from typing import NamedTuple, TYPE_CHECKING if TYPE_CHECKING: from . import MessengerWorld else: MessengerWorld = object -PROG_SHOP_ITEMS: List[str] = [ +PROG_SHOP_ITEMS: list[str] = [ "Path of Resilience", "Meditation", "Strike of the Ninja", @@ -14,7 +14,7 @@ "Aerobatics Warrior", ] -USEFUL_SHOP_ITEMS: List[str] = [ +USEFUL_SHOP_ITEMS: list[str] = [ "Karuta Plates", "Serendipitous Bodies", "Kusari Jacket", @@ -29,10 +29,10 @@ class ShopData(NamedTuple): internal_name: str min_price: int max_price: int - prerequisite: Optional[Union[str, Set[str]]] = None + prerequisite: str | set[str] | None = None -SHOP_ITEMS: Dict[str, ShopData] = { +SHOP_ITEMS: dict[str, ShopData] = { "Karuta Plates": ShopData("HP_UPGRADE_1", 20, 200), "Serendipitous Bodies": ShopData("ENEMY_DROP_HP", 20, 300, "The Shop - Karuta Plates"), "Path of Resilience": ShopData("DAMAGE_REDUCTION", 100, 500, "The Shop - Serendipitous Bodies"), @@ -56,7 +56,7 @@ class ShopData(NamedTuple): "Focused Power Sense": ShopData("POWER_SEAL_WORLD_MAP", 300, 600, "The Shop - Power Sense"), } -FIGURINES: Dict[str, ShopData] = { +FIGURINES: dict[str, ShopData] = { "Green Kappa Figurine": ShopData("GREEN_KAPPA", 100, 500), "Blue Kappa Figurine": ShopData("BLUE_KAPPA", 100, 500), "Ountarde Figurine": ShopData("OUNTARDE", 100, 500), @@ -73,12 +73,12 @@ class ShopData(NamedTuple): } -def shuffle_shop_prices(world: MessengerWorld) -> Tuple[Dict[str, int], Dict[str, int]]: +def shuffle_shop_prices(world: MessengerWorld) -> tuple[dict[str, int], dict[str, int]]: shop_price_mod = world.options.shop_price.value shop_price_planned = world.options.shop_price_plan - shop_prices: Dict[str, int] = {} - figurine_prices: Dict[str, int] = {} + shop_prices: dict[str, int] = {} + figurine_prices: dict[str, int] = {} for item, price in shop_price_planned.value.items(): if not isinstance(price, int): price = world.random.choices(list(price.keys()), weights=list(price.values()))[0] diff --git a/worlds/messenger/subclasses.py b/worlds/messenger/subclasses.py index b60aeb179feb..29e3ea8953ec 100644 --- a/worlds/messenger/subclasses.py +++ b/worlds/messenger/subclasses.py @@ -1,5 +1,5 @@ from functools import cached_property -from typing import Optional, TYPE_CHECKING +from typing import TYPE_CHECKING from BaseClasses import CollectionState, Entrance, Item, ItemClassification, Location, Region from .regions import LOCATIONS, MEGA_SHARDS @@ -10,14 +10,14 @@ class MessengerEntrance(Entrance): - world: Optional["MessengerWorld"] = None + world: "MessengerWorld | None" = None class MessengerRegion(Region): parent: str entrance_type = MessengerEntrance - def __init__(self, name: str, world: "MessengerWorld", parent: Optional[str] = None) -> None: + def __init__(self, name: str, world: "MessengerWorld", parent: str | None = None) -> None: super().__init__(name, world.player, world.multiworld) self.parent = parent locations = [] @@ -48,7 +48,7 @@ def __init__(self, name: str, world: "MessengerWorld", parent: Optional[str] = N class MessengerLocation(Location): game = "The Messenger" - def __init__(self, player: int, name: str, loc_id: Optional[int], parent: MessengerRegion) -> None: + def __init__(self, player: int, name: str, loc_id: int | None, parent: MessengerRegion) -> None: super().__init__(player, name, loc_id, parent) if loc_id is None: if name == "Rescue Phantom": @@ -59,7 +59,7 @@ def __init__(self, player: int, name: str, loc_id: Optional[int], parent: Messen class MessengerShopLocation(MessengerLocation): @cached_property def cost(self) -> int: - name = self.name.replace("The Shop - ", "") # TODO use `remove_prefix` when 3.8 finally gets dropped + name = self.name.removeprefix("The Shop - ") world = self.parent_region.multiworld.worlds[self.player] shop_data = SHOP_ITEMS[name] if shop_data.prerequisite: diff --git a/worlds/messenger/test/test_shop.py b/worlds/messenger/test/test_shop.py index 971ff1763b47..21a0c352bff4 100644 --- a/worlds/messenger/test/test_shop.py +++ b/worlds/messenger/test/test_shop.py @@ -1,5 +1,6 @@ from typing import Dict +from BaseClasses import CollectionState from . import MessengerTestBase from ..shop import SHOP_ITEMS, FIGURINES @@ -76,7 +77,7 @@ def test_costs(self) -> None: loc = f"The Shop - {loc}" self.assertLessEqual(price, self.multiworld.get_location(loc, self.player).cost) - self.assertTrue(loc.replace("The Shop - ", "") in SHOP_ITEMS) + self.assertTrue(loc.removeprefix("The Shop - ") in SHOP_ITEMS) self.assertEqual(len(prices), len(SHOP_ITEMS)) figures = self.world.figurine_prices @@ -89,3 +90,15 @@ def test_costs(self) -> None: self.assertTrue(loc in FIGURINES) self.assertEqual(len(figures), len(FIGURINES)) + + max_cost_state = CollectionState(self.multiworld) + self.assertFalse(self.world.get_location("Money Wrench").can_reach(max_cost_state)) + prog_shards = [] + for item in self.multiworld.itempool: + if "Time Shard " in item.name: + value = int(item.name.strip("Time Shard ()")) + if value >= 100: + prog_shards.append(item) + for shard in prog_shards: + max_cost_state.collect(shard, True) + self.assertTrue(self.world.get_location("Money Wrench").can_reach(max_cost_state)) diff --git a/worlds/minecraft/Structures.py b/worlds/minecraft/Structures.py index df3d944a6c65..d4f62f3498e9 100644 --- a/worlds/minecraft/Structures.py +++ b/worlds/minecraft/Structures.py @@ -29,7 +29,7 @@ def set_pair(exit, struct): # Connect plando structures first if self.options.plando_connections: - for conn in self.plando_connections: + for conn in self.options.plando_connections: set_pair(conn.entrance, conn.exit) # The algorithm tries to place the most restrictive structures first. This algorithm always works on the diff --git a/worlds/mm2/__init__.py b/worlds/mm2/__init__.py index 07e1823f9387..4a43ee8df0f0 100644 --- a/worlds/mm2/__init__.py +++ b/worlds/mm2/__init__.py @@ -96,13 +96,13 @@ class MM2World(World): location_name_groups = location_groups web = MM2WebWorld() rom_name: bytearray - world_version: Tuple[int, int, int] = (0, 3, 1) + world_version: Tuple[int, int, int] = (0, 3, 2) wily_5_weapons: Dict[int, List[int]] - def __init__(self, world: MultiWorld, player: int): + def __init__(self, multiworld: MultiWorld, player: int): self.rom_name = bytearray() self.rom_name_available_event = threading.Event() - super().__init__(world, player) + super().__init__(multiworld, player) self.weapon_damage = deepcopy(weapon_damage) self.wily_5_weapons = {} diff --git a/worlds/mm2/rules.py b/worlds/mm2/rules.py index eddd09927445..7e2ce1f3c752 100644 --- a/worlds/mm2/rules.py +++ b/worlds/mm2/rules.py @@ -133,28 +133,6 @@ def set_rules(world: "MM2World") -> None: # Wily Machine needs all three weaknesses present, so allow elif 4 > world.weapon_damage[weapon][i] > 0: world.weapon_damage[weapon][i] = 0 - # handle special cases - for boss in range(14): - for weapon in (1, 3, 6, 8): - if (0 < world.weapon_damage[weapon][boss] < minimum_weakness_requirement[weapon] and - not any(world.weapon_damage[i][boss] > 0 for i in range(1, 8) if i != weapon)): - # Weapon does not have enough possible ammo to kill the boss, raise the damage - if boss == 9: - if weapon != 3: - # Atomic Fire and Crash Bomber cannot be Picopico-kun's only weakness - world.weapon_damage[weapon][boss] = 0 - weakness = world.random.choice((2, 3, 4, 5, 7, 8)) - world.weapon_damage[weakness][boss] = minimum_weakness_requirement[weakness] - elif boss == 11: - if weapon == 1: - # Atomic Fire cannot be Boobeam Trap's only weakness - world.weapon_damage[weapon][boss] = 0 - weakness = world.random.choice((2, 3, 4, 5, 6, 7, 8)) - world.weapon_damage[weakness][boss] = minimum_weakness_requirement[weakness] - else: - world.weapon_damage[weapon][boss] = minimum_weakness_requirement[weapon] - starting = world.options.starting_robot_master.value - world.weapon_damage[0][starting] = 1 for p_boss in world.options.plando_weakness: for p_weapon in world.options.plando_weakness[p_boss]: @@ -168,6 +146,28 @@ def set_rules(world: "MM2World") -> None: world.weapon_damage[weapons_to_id[p_weapon]][bosses[p_boss]] \ = world.options.plando_weakness[p_boss][p_weapon] + # handle special cases + for boss in range(14): + for weapon in (1, 2, 3, 6, 8): + if (0 < world.weapon_damage[weapon][boss] < minimum_weakness_requirement[weapon] and + not any(world.weapon_damage[i][boss] >= minimum_weakness_requirement[weapon] + for i in range(9) if i != weapon)): + # Weapon does not have enough possible ammo to kill the boss, raise the damage + if boss == 9: + if weapon in (1, 6): + # Atomic Fire and Crash Bomber cannot be Picopico-kun's only weakness + world.weapon_damage[weapon][boss] = 0 + weakness = world.random.choice((2, 3, 4, 5, 7, 8)) + world.weapon_damage[weakness][boss] = minimum_weakness_requirement[weakness] + elif boss == 11: + if weapon == 1: + # Atomic Fire cannot be Boobeam Trap's only weakness + world.weapon_damage[weapon][boss] = 0 + weakness = world.random.choice((2, 3, 4, 5, 6, 7, 8)) + world.weapon_damage[weakness][boss] = minimum_weakness_requirement[weakness] + else: + world.weapon_damage[weapon][boss] = minimum_weakness_requirement[weapon] + if world.weapon_damage[0][world.options.starting_robot_master.value] < 1: world.weapon_damage[0][world.options.starting_robot_master.value] = weapon_damage[0][world.options.starting_robot_master.value] @@ -209,11 +209,11 @@ def set_rules(world: "MM2World") -> None: continue highest, wp = max(zip(weapon_weight.values(), weapon_weight.keys())) uses = weapon_energy[wp] // weapon_costs[wp] - used_weapons[boss].add(wp) if int(uses * boss_damage[wp]) > boss_health[boss]: used = ceil(boss_health[boss] / boss_damage[wp]) weapon_energy[wp] -= weapon_costs[wp] * used boss_health[boss] = 0 + used_weapons[boss].add(wp) elif highest <= 0: # we are out of weapons that can actually damage the boss # so find the weapon that has the most uses, and apply that as an additional weakness @@ -221,18 +221,21 @@ def set_rules(world: "MM2World") -> None: # Quick Boomerang and no other, it would only be 28 off from defeating all 9, which Metal Blade should # be able to cover wp, max_uses = max((weapon, weapon_energy[weapon] // weapon_costs[weapon]) for weapon in weapon_weight - if weapon != 0) + if weapon != 0 and (weapon != 8 or boss != 12)) + # Wily Machine cannot under any circumstances take damage from Time Stopper, prevent this world.weapon_damage[wp][boss] = minimum_weakness_requirement[wp] used = min(int(weapon_energy[wp] // weapon_costs[wp]), - ceil(boss_health[boss] // minimum_weakness_requirement[wp])) + ceil(boss_health[boss] / minimum_weakness_requirement[wp])) weapon_energy[wp] -= weapon_costs[wp] * used boss_health[boss] -= int(used * minimum_weakness_requirement[wp]) weapon_weight.pop(wp) + used_weapons[boss].add(wp) else: # drain the weapon and continue boss_health[boss] -= int(uses * boss_damage[wp]) weapon_energy[wp] -= weapon_costs[wp] * uses weapon_weight.pop(wp) + used_weapons[boss].add(wp) world.wily_5_weapons = {boss: sorted(used_weapons[boss]) for boss in used_weapons} diff --git a/worlds/mm2/text.py b/worlds/mm2/text.py index 32d665bf6c7f..7dda12ac0346 100644 --- a/worlds/mm2/text.py +++ b/worlds/mm2/text.py @@ -1,7 +1,7 @@ from typing import DefaultDict from collections import defaultdict -MM2_WEAPON_ENCODING: DefaultDict[str, int] = defaultdict(lambda x: 0x6F, { +MM2_WEAPON_ENCODING: DefaultDict[str, int] = defaultdict(lambda: 0x6F, { ' ': 0x40, 'A': 0x41, 'B': 0x42, diff --git a/worlds/mmbn3/Items.py b/worlds/mmbn3/Items.py index 2e249ce79e8c..30ec311ecbe2 100644 --- a/worlds/mmbn3/Items.py +++ b/worlds/mmbn3/Items.py @@ -171,7 +171,7 @@ class MMBN3Item(Item): ItemData(0xB31063, ItemName.SandStage_C, ItemClassification.filler, ItemType.Chip, 182, chip_code('C')), ItemData(0xB31064, ItemName.SideGun_S, ItemClassification.filler, ItemType.Chip, 12, chip_code('S')), ItemData(0xB31065, ItemName.Slasher_B, ItemClassification.useful, ItemType.Chip, 43, chip_code('B')), - ItemData(0xB31066, ItemName.SloGuage_star, ItemClassification.filler, ItemType.Chip, 157, chip_code('*')), + ItemData(0xB31066, ItemName.SloGauge_star, ItemClassification.filler, ItemType.Chip, 157, chip_code('*')), ItemData(0xB31067, ItemName.Snake_D, ItemClassification.useful, ItemType.Chip, 131, chip_code('D')), ItemData(0xB31068, ItemName.Snctuary_C, ItemClassification.useful, ItemType.Chip, 184, chip_code('C')), ItemData(0xB31069, ItemName.Spreader_star, ItemClassification.useful, ItemType.Chip, 13, chip_code('*')), diff --git a/worlds/mmbn3/Names/ItemName.py b/worlds/mmbn3/Names/ItemName.py index 441bdc591c51..677eff22b353 100644 --- a/worlds/mmbn3/Names/ItemName.py +++ b/worlds/mmbn3/Names/ItemName.py @@ -72,7 +72,7 @@ class ItemName(): SandStage_C = "SandStage C" SideGun_S = "SideGun S" Slasher_B = "Slasher B" - SloGuage_star = "SloGuage *" + SloGauge_star = "SloGauge *" Snake_D = "Snake D" Snctuary_C = "Snctuary C" Spreader_star = "Spreader *" @@ -235,4 +235,4 @@ class ItemName(): RegUP3 = "RegUP3" SubMem = "SubMem" - Victory = "Victory" \ No newline at end of file + Victory = "Victory" diff --git a/worlds/musedash/MuseDashData.txt b/worlds/musedash/MuseDashData.txt index 1f1a2a011cff..d913449ed540 100644 --- a/worlds/musedash/MuseDashData.txt +++ b/worlds/musedash/MuseDashData.txt @@ -31,7 +31,7 @@ Blackest Luxury Car|0-18|Default Music|True|3|6|8| Medicine of Sing|0-19|Default Music|False|3|6|8| irregulyze|0-20|Default Music|True|3|6|8| I don't care about Christmas though|0-47|Default Music|False|4|6|8| -Imaginary World|0-21|Default Music|True|4|6|8| +Imaginary World|0-21|Default Music|True|4|6|8|10 Dysthymia|0-22|Default Music|True|4|7|9| From the New World|0-42|Default Music|False|2|5|7| NISEGAO|0-33|Default Music|True|4|7|9| @@ -266,7 +266,7 @@ Medusa|31-1|Happy Otaku Pack Vol.11|False|4|6|8|10 Final Step!|31-2|Happy Otaku Pack Vol.11|False|5|7|10| MAGENTA POTION|31-3|Happy Otaku Pack Vol.11|False|4|7|9| Cross Ray|31-4|Happy Otaku Pack Vol.11|False|3|6|9| -Square Lake|31-5|Happy Otaku Pack Vol.11|True|6|8|9|11 +Square Lake|31-5|Happy Otaku Pack Vol.11|False|6|8|9|11 Girly Cupid|30-0|Cute Is Everything Vol.6|False|3|6|8| sheep in the light|30-1|Cute Is Everything Vol.6|False|2|5|8| Breaker city|30-2|Cute Is Everything Vol.6|False|4|6|9| @@ -353,7 +353,7 @@ Re End of a Dream|16-1|Give Up TREATMENT Vol.6|False|5|8|11| Etude -Storm-|16-2|Give Up TREATMENT Vol.6|True|6|8|10| Unlimited Katharsis|16-3|Give Up TREATMENT Vol.6|False|4|6|10| Magic Knight Girl|16-4|Give Up TREATMENT Vol.6|False|4|7|9| -Eeliaas|16-5|Give Up TREATMENT Vol.6|True|6|9|11| +Eeliaas|16-5|Give Up TREATMENT Vol.6|False|6|9|11| Magic Spell|15-0|Cute Is Everything Vol.3|True|2|5|7| Colorful Star, Colored Drawing, Travel Poem|15-1|Cute Is Everything Vol.3|False|3|4|6| Satell Knight|15-2|Cute Is Everything Vol.3|False|3|6|8| @@ -396,7 +396,7 @@ Chronomia|9-2|Happy Otaku Pack Vol.4|False|5|7|10| Dandelion's Daydream|9-3|Happy Otaku Pack Vol.4|True|5|7|8| Lorikeet Flat design|9-4|Happy Otaku Pack Vol.4|True|5|7|10| GOODRAGE|9-5|Happy Otaku Pack Vol.4|False|6|9|11| -Altale|8-0|Give Up TREATMENT Vol.3|False|3|5|7| +Altale|8-0|Give Up TREATMENT Vol.3|False|3|5|7|10 Brain Power|8-1|Give Up TREATMENT Vol.3|False|4|7|10| Berry Go!!|8-2|Give Up TREATMENT Vol.3|False|3|6|9| Sweet* Witch* Girl*|8-3|Give Up TREATMENT Vol.3|False|6|8|10|? @@ -579,4 +579,19 @@ The Whole Rest|77-1|Let's Rhythm Jam!|False|5|8|10|11 Hydra|77-2|Let's Rhythm Jam!|False|4|7|11| Pastel Lines|77-3|Let's Rhythm Jam!|False|3|6|9| LINK x LIN#S|77-4|Let's Rhythm Jam!|False|3|6|9| -Arcade ViruZ|77-5|Let's Rhythm Jam!|False|6|8|10| +Arcade ViruZ|77-5|Let's Rhythm Jam!|False|6|8|11| +Eve Avenir|78-0|Endless Pirouette|True|6|8|10| +Silverstring|78-1|Endless Pirouette|True|5|7|10| +Melusia|78-2|Endless Pirouette|False|5|7|10|11 +Devil's Castle|78-3|Endless Pirouette|True|4|7|10| +Abatement|78-4|Endless Pirouette|True|6|8|10|11 +Azalea|78-5|Endless Pirouette|False|4|8|10| +Brightly World|78-6|Endless Pirouette|True|6|8|10| +We'll meet in every world ***|78-7|Endless Pirouette|True|7|9|11| +Collapsar|78-8|Endless Pirouette|True|7|9|10|11 +Parousia|78-9|Endless Pirouette|False|6|8|10| +Gunners in the Rain|79-0|Ensemble Arcanum|False|5|8|10| +Halzion|79-1|Ensemble Arcanum|False|2|5|8| +SHOWTIME!!|79-2|Ensemble Arcanum|False|6|8|10| +Achromic Riddle|79-3|Ensemble Arcanum|False|6|8|10|11 +karanosu|79-4|Ensemble Arcanum|False|3|6|8| diff --git a/worlds/musedash/Options.py b/worlds/musedash/Options.py index 7164aa3e1362..e647c18d7096 100644 --- a/worlds/musedash/Options.py +++ b/worlds/musedash/Options.py @@ -39,7 +39,7 @@ class AdditionalSongs(Range): - The final song count may be lower due to other settings. """ range_start = 15 - range_end = 534 # Note will probably not reach this high if any other settings are done. + range_end = 600 # Note will probably not reach this high if any other settings are done. default = 40 display_name = "Additional Song Count" diff --git a/worlds/noita/items.py b/worlds/noita/items.py index 6b662fbee692..1cb7d9601386 100644 --- a/worlds/noita/items.py +++ b/worlds/noita/items.py @@ -100,13 +100,13 @@ def create_all_items(world: NoitaWorld) -> None: "Wand (Tier 5)": ItemData(110010, "Wands", ItemClassification.useful, 1), "Wand (Tier 6)": ItemData(110011, "Wands", ItemClassification.useful, 1), "Kantele": ItemData(110012, "Wands", ItemClassification.useful), - "Fire Immunity Perk": ItemData(110013, "Perks", ItemClassification.progression, 1), - "Toxic Immunity Perk": ItemData(110014, "Perks", ItemClassification.progression, 1), - "Explosion Immunity Perk": ItemData(110015, "Perks", ItemClassification.progression, 1), - "Melee Immunity Perk": ItemData(110016, "Perks", ItemClassification.progression, 1), - "Electricity Immunity Perk": ItemData(110017, "Perks", ItemClassification.progression, 1), - "Tinker with Wands Everywhere Perk": ItemData(110018, "Perks", ItemClassification.progression, 1), - "All-Seeing Eye Perk": ItemData(110019, "Perks", ItemClassification.progression, 1), + "Fire Immunity Perk": ItemData(110013, "Perks", ItemClassification.progression | ItemClassification.useful, 1), + "Toxic Immunity Perk": ItemData(110014, "Perks", ItemClassification.progression | ItemClassification.useful, 1), + "Explosion Immunity Perk": ItemData(110015, "Perks", ItemClassification.progression | ItemClassification.useful, 1), + "Melee Immunity Perk": ItemData(110016, "Perks", ItemClassification.progression | ItemClassification.useful, 1), + "Electricity Immunity Perk": ItemData(110017, "Perks", ItemClassification.progression | ItemClassification.useful, 1), + "Tinker with Wands Everywhere Perk": ItemData(110018, "Perks", ItemClassification.progression | ItemClassification.useful, 1), + "All-Seeing Eye Perk": ItemData(110019, "Perks", ItemClassification.progression | ItemClassification.useful, 1), "Spatial Awareness Perk": ItemData(110020, "Perks", ItemClassification.progression), "Extra Life Perk": ItemData(110021, "Repeatable Perks", ItemClassification.useful, 1), "Orb": ItemData(110022, "Orbs", ItemClassification.progression_skip_balancing), diff --git a/worlds/oot/__init__.py b/worlds/oot/__init__.py index b93f60b2a08e..975902ae6e64 100644 --- a/worlds/oot/__init__.py +++ b/worlds/oot/__init__.py @@ -184,6 +184,10 @@ class OOTWorld(World): "Small Key Ring (Spirit Temple)", "Small Key Ring (Thieves Hideout)", "Small Key Ring (Water Temple)", "Boss Key (Fire Temple)", "Boss Key (Forest Temple)", "Boss Key (Ganons Castle)", "Boss Key (Shadow Temple)", "Boss Key (Spirit Temple)", "Boss Key (Water Temple)"}, + + # aliases + "Longshot": {"Progressive Hookshot"}, # fuzzy hinting thought Longshot was Slingshot + "Hookshot": {"Progressive Hookshot"}, # for consistency, mostly } location_name_groups = build_location_name_groups() @@ -1344,23 +1348,9 @@ def get_shuffled_entrances(self, type=None, only_primary=False): def get_locations(self): return self.multiworld.get_locations(self.player) - def get_location(self, location): - return self.multiworld.get_location(location, self.player) - - def get_region(self, region_name): - try: - return self._regions_cache[region_name] - except KeyError: - ret = self.multiworld.get_region(region_name, self.player) - self._regions_cache[region_name] = ret - return ret - def get_entrances(self): return self.multiworld.get_entrances(self.player) - def get_entrance(self, entrance): - return self.multiworld.get_entrance(entrance, self.player) - def is_major_item(self, item: OOTItem): if item.type == 'Token': return self.bridge == 'tokens' or self.lacs_condition == 'tokens' diff --git a/worlds/osrs/LogicCSV/locations_generated.py b/worlds/osrs/LogicCSV/locations_generated.py index 073e505ad8f4..2d617a7038fe 100644 --- a/worlds/osrs/LogicCSV/locations_generated.py +++ b/worlds/osrs/LogicCSV/locations_generated.py @@ -57,11 +57,11 @@ LocationRow('Catch a Swordfish', 'fishing', ['Lobster Spot', ], [SkillRequirement('Fishing', 50), ], [], 12), LocationRow('Bake a Redberry Pie', 'cooking', ['Redberry Bush', 'Wheat', 'Windmill', 'Pie Dish', ], [SkillRequirement('Cooking', 10), ], [], 0), LocationRow('Cook some Stew', 'cooking', ['Bowl', 'Meat', 'Potato', ], [SkillRequirement('Cooking', 25), ], [], 0), - LocationRow('Bake an Apple Pie', 'cooking', ['Cooking Apple', 'Wheat', 'Windmill', 'Pie Dish', ], [SkillRequirement('Cooking', 30), ], [], 2), + LocationRow('Bake an Apple Pie', 'cooking', ['Cooking Apple', 'Wheat', 'Windmill', 'Pie Dish', ], [SkillRequirement('Cooking', 32), ], [], 2), LocationRow('Bake a Cake', 'cooking', ['Wheat', 'Windmill', 'Egg', 'Milk', 'Cake Tin', ], [SkillRequirement('Cooking', 40), ], [], 6), LocationRow('Bake a Meat Pizza', 'cooking', ['Wheat', 'Windmill', 'Cheese', 'Tomato', 'Meat', ], [SkillRequirement('Cooking', 45), ], [], 8), - LocationRow('Burn some Oak Logs', 'firemaking', ['Oak Tree', ], [SkillRequirement('Firemaking', 15), ], [], 0), - LocationRow('Burn some Willow Logs', 'firemaking', ['Willow Tree', ], [SkillRequirement('Firemaking', 30), ], [], 0), + LocationRow('Burn some Oak Logs', 'firemaking', ['Oak Tree', ], [SkillRequirement('Firemaking', 15), SkillRequirement('Woodcutting', 15), ], [], 0), + LocationRow('Burn some Willow Logs', 'firemaking', ['Willow Tree', ], [SkillRequirement('Firemaking', 30), SkillRequirement('Woodcutting', 30), ], [], 0), LocationRow('Travel on a Canoe', 'woodcutting', ['Canoe Tree', ], [SkillRequirement('Woodcutting', 12), ], [], 0), LocationRow('Cut an Oak Log', 'woodcutting', ['Oak Tree', ], [SkillRequirement('Woodcutting', 15), ], [], 0), LocationRow('Cut a Willow Log', 'woodcutting', ['Willow Tree', ], [SkillRequirement('Woodcutting', 30), ], [], 0), diff --git a/worlds/osrs/Names.py b/worlds/osrs/Names.py index cc92439ef859..1a44aa389c6a 100644 --- a/worlds/osrs/Names.py +++ b/worlds/osrs/Names.py @@ -31,7 +31,7 @@ class RegionNames(str, Enum): Mudskipper_Point = "Mudskipper Point" Karamja = "Karamja" Corsair_Cove = "Corsair Cove" - Wilderness = "The Wilderness" + Wilderness = "Wilderness" Crandor = "Crandor" # Resource Regions Egg = "Egg" diff --git a/worlds/osrs/Rules.py b/worlds/osrs/Rules.py new file mode 100644 index 000000000000..22a19934c8e1 --- /dev/null +++ b/worlds/osrs/Rules.py @@ -0,0 +1,337 @@ +""" + Ensures a target level can be reached with available resources + """ +from worlds.generic.Rules import CollectionRule, add_rule +from .Names import RegionNames, ItemNames + + +def get_fishing_skill_rule(level, player, options) -> CollectionRule: + if options.max_fishing_level < level: + return lambda state: False + + if options.brutal_grinds or level < 5: + return lambda state: state.can_reach_region(RegionNames.Shrimp, player) + if level < 20: + return lambda state: state.can_reach_region(RegionNames.Shrimp, player) and \ + state.can_reach_region(RegionNames.Port_Sarim, player) + else: + return lambda state: state.can_reach_region(RegionNames.Shrimp, player) and \ + state.can_reach_region(RegionNames.Port_Sarim, player) and \ + state.can_reach_region(RegionNames.Fly_Fish, player) + + +def get_mining_skill_rule(level, player, options) -> CollectionRule: + if options.max_mining_level < level: + return lambda state: False + + if options.brutal_grinds or level < 15: + return lambda state: state.can_reach_region(RegionNames.Bronze_Ores, player) or \ + state.can_reach_region(RegionNames.Clay_Rock, player) + else: + # Iron is the best way to train all the way to 99, so having access to iron is all you need to check for + return lambda state: (state.can_reach_region(RegionNames.Bronze_Ores, player) or + state.can_reach_region(RegionNames.Clay_Rock, player)) and \ + state.can_reach_region(RegionNames.Iron_Rock, player) + + +def get_woodcutting_skill_rule(level, player, options) -> CollectionRule: + if options.max_woodcutting_level < level: + return lambda state: False + + if options.brutal_grinds or level < 15: + # I've checked. There is not a single chunk in the f2p that does not have at least one normal tree. + # Even the desert. + return lambda state: True + if level < 30: + return lambda state: state.can_reach_region(RegionNames.Oak_Tree, player) + else: + return lambda state: state.can_reach_region(RegionNames.Oak_Tree, player) and \ + state.can_reach_region(RegionNames.Willow_Tree, player) + + +def get_smithing_skill_rule(level, player, options) -> CollectionRule: + if options.max_smithing_level < level: + return lambda state: False + + if options.brutal_grinds: + return lambda state: state.can_reach_region(RegionNames.Bronze_Ores, player) and \ + state.can_reach_region(RegionNames.Furnace, player) + if level < 15: + # Lumbridge has a special bronze-only anvil. This is the only anvil of its type so it's not included + # in the "Anvil" resource region. We still need to check for it though. + return lambda state: state.can_reach_region(RegionNames.Bronze_Ores, player) and \ + state.can_reach_region(RegionNames.Furnace, player) and \ + (state.can_reach_region(RegionNames.Anvil, player) or + state.can_reach_region(RegionNames.Lumbridge, player)) + if level < 30: + # For levels between 15 and 30, the lumbridge anvil won't cut it. Only a real one will do + return lambda state: state.can_reach_region(RegionNames.Bronze_Ores, player) and \ + state.can_reach_region(RegionNames.Iron_Rock, player) and \ + state.can_reach_region(RegionNames.Furnace, player) and \ + state.can_reach_region(RegionNames.Anvil, player) + else: + return lambda state: state.can_reach_region(RegionNames.Bronze_Ores, player) and \ + state.can_reach_region(RegionNames.Iron_Rock, player) and \ + state.can_reach_region(RegionNames.Coal_Rock, player) and \ + state.can_reach_region(RegionNames.Furnace, player) and \ + state.can_reach_region(RegionNames.Anvil, player) + + +def get_crafting_skill_rule(level, player, options): + if options.max_crafting_level < level: + return lambda state: False + + # Crafting is really complex. Need a lot of sub-rules to make this even remotely readable + def can_spin(state): + return state.can_reach_region(RegionNames.Sheep, player) and \ + state.can_reach_region(RegionNames.Spinning_Wheel, player) + + def can_pot(state): + return state.can_reach_region(RegionNames.Clay_Rock, player) and \ + state.can_reach_region(RegionNames.Barbarian_Village, player) + + def can_tan(state): + return state.can_reach_region(RegionNames.Milk, player) and \ + state.can_reach_region(RegionNames.Al_Kharid, player) + + def mould_access(state): + return state.can_reach_region(RegionNames.Al_Kharid, player) or \ + state.can_reach_region(RegionNames.Rimmington, player) + + def can_silver(state): + return state.can_reach_region(RegionNames.Silver_Rock, player) and \ + state.can_reach_region(RegionNames.Furnace, player) and mould_access(state) + + def can_gold(state): + return state.can_reach_region(RegionNames.Gold_Rock, player) and \ + state.can_reach_region(RegionNames.Furnace, player) and mould_access(state) + + if options.brutal_grinds or level < 5: + return lambda state: can_spin(state) or can_pot(state) or can_tan(state) + + can_smelt_gold = get_smithing_skill_rule(40, player, options) + can_smelt_silver = get_smithing_skill_rule(20, player, options) + if level < 16: + return lambda state: can_pot(state) or can_tan(state) or (can_gold(state) and can_smelt_gold(state)) + else: + return lambda state: can_tan(state) or (can_silver(state) and can_smelt_silver(state)) or \ + (can_gold(state) and can_smelt_gold(state)) + + +def get_cooking_skill_rule(level, player, options) -> CollectionRule: + if options.max_cooking_level < level: + return lambda state: False + + if options.brutal_grinds or level < 15: + return lambda state: state.can_reach_region(RegionNames.Milk, player) or \ + state.can_reach_region(RegionNames.Egg, player) or \ + state.can_reach_region(RegionNames.Shrimp, player) or \ + (state.can_reach_region(RegionNames.Wheat, player) and + state.can_reach_region(RegionNames.Windmill, player)) + else: + can_catch_fly_fish = get_fishing_skill_rule(20, player, options) + + return lambda state: ( + (state.can_reach_region(RegionNames.Fly_Fish, player) and can_catch_fly_fish(state)) or + (state.can_reach_region(RegionNames.Port_Sarim, player)) + ) and ( + state.can_reach_region(RegionNames.Milk, player) or + state.can_reach_region(RegionNames.Egg, player) or + state.can_reach_region(RegionNames.Shrimp, player) or + (state.can_reach_region(RegionNames.Wheat, player) and + state.can_reach_region(RegionNames.Windmill, player)) + ) + + +def get_runecraft_skill_rule(level, player, options) -> CollectionRule: + if options.max_runecraft_level < level: + return lambda state: False + if not options.brutal_grinds: + # Ensure access to the relevant altars + if level >= 5: + return lambda state: state.has(ItemNames.QP_Rune_Mysteries, player) and \ + state.can_reach_region(RegionNames.Falador_Farm, player) and \ + state.can_reach_region(RegionNames.Lumbridge_Swamp, player) + if level >= 9: + return lambda state: state.has(ItemNames.QP_Rune_Mysteries, player) and \ + state.can_reach_region(RegionNames.Falador_Farm, player) and \ + state.can_reach_region(RegionNames.Lumbridge_Swamp, player) and \ + state.can_reach_region(RegionNames.East_Of_Varrock, player) + if level >= 14: + return lambda state: state.has(ItemNames.QP_Rune_Mysteries, player) and \ + state.can_reach_region(RegionNames.Falador_Farm, player) and \ + state.can_reach_region(RegionNames.Lumbridge_Swamp, player) and \ + state.can_reach_region(RegionNames.East_Of_Varrock, player) and \ + state.can_reach_region(RegionNames.Al_Kharid, player) + + return lambda state: state.has(ItemNames.QP_Rune_Mysteries, player) and \ + state.can_reach_region(RegionNames.Falador_Farm, player) + + +def get_magic_skill_rule(level, player, options) -> CollectionRule: + if options.max_magic_level < level: + return lambda state: False + + return lambda state: state.can_reach_region(RegionNames.Mind_Runes, player) + + +def get_firemaking_skill_rule(level, player, options) -> CollectionRule: + if options.max_firemaking_level < level: + return lambda state: False + if not options.brutal_grinds: + if level >= 30: + can_chop_willows = get_woodcutting_skill_rule(30, player, options) + return lambda state: state.can_reach_region(RegionNames.Willow_Tree, player) and can_chop_willows(state) + if level >= 15: + can_chop_oaks = get_woodcutting_skill_rule(15, player, options) + return lambda state: state.can_reach_region(RegionNames.Oak_Tree, player) and can_chop_oaks(state) + # If brutal grinds are on, or if the level is less than 15, you can train it. + return lambda state: True + + +def get_skill_rule(skill, level, player, options) -> CollectionRule: + if skill.lower() == "fishing": + return get_fishing_skill_rule(level, player, options) + if skill.lower() == "mining": + return get_mining_skill_rule(level, player, options) + if skill.lower() == "woodcutting": + return get_woodcutting_skill_rule(level, player, options) + if skill.lower() == "smithing": + return get_smithing_skill_rule(level, player, options) + if skill.lower() == "crafting": + return get_crafting_skill_rule(level, player, options) + if skill.lower() == "cooking": + return get_cooking_skill_rule(level, player, options) + if skill.lower() == "runecraft": + return get_runecraft_skill_rule(level, player, options) + if skill.lower() == "magic": + return get_magic_skill_rule(level, player, options) + if skill.lower() == "firemaking": + return get_firemaking_skill_rule(level, player, options) + + return lambda state: True + + +def generate_special_rules_for(entrance, region_row, outbound_region_name, player, options): + if outbound_region_name == RegionNames.Cooks_Guild: + add_rule(entrance, get_cooking_skill_rule(32, player, options)) + elif outbound_region_name == RegionNames.Crafting_Guild: + add_rule(entrance, get_crafting_skill_rule(40, player, options)) + elif outbound_region_name == RegionNames.Corsair_Cove: + # Need to be able to start Corsair Curse in addition to having the item + add_rule(entrance, lambda state: state.can_reach(RegionNames.Falador_Farm, "Region", player)) + elif outbound_region_name == "Camdozaal*": + add_rule(entrance, lambda state: state.has(ItemNames.QP_Below_Ice_Mountain, player)) + elif region_row.name == "Dwarven Mountain Pass" and outbound_region_name == "Anvil*": + add_rule(entrance, lambda state: state.has(ItemNames.QP_Dorics_Quest, player)) + + # Special logic for canoes + canoe_regions = [RegionNames.Lumbridge, RegionNames.South_Of_Varrock, RegionNames.Barbarian_Village, + RegionNames.Edgeville, RegionNames.Wilderness] + if region_row.name in canoe_regions: + # Skill rules for greater distances + woodcutting_rule_d1 = get_woodcutting_skill_rule(12, player, options) + woodcutting_rule_d2 = get_woodcutting_skill_rule(27, player, options) + woodcutting_rule_d3 = get_woodcutting_skill_rule(42, player, options) + woodcutting_rule_all = get_woodcutting_skill_rule(57, player, options) + + if region_row.name == RegionNames.Lumbridge: + # Canoe Tree access for the Location + if outbound_region_name == RegionNames.Canoe_Tree: + add_rule(entrance, + lambda state: (state.can_reach_region(RegionNames.South_Of_Varrock, player) + and woodcutting_rule_d1(state)) or + (state.can_reach_region(RegionNames.Barbarian_Village, player) + and woodcutting_rule_d2(state)) or + (state.can_reach_region(RegionNames.Edgeville, player) + and woodcutting_rule_d3(state)) or + (state.can_reach_region(RegionNames.Wilderness, player) + and woodcutting_rule_all(state))) + + # Access to other chunks based on woodcutting settings + elif outbound_region_name == RegionNames.South_Of_Varrock: + add_rule(entrance, woodcutting_rule_d1) + elif outbound_region_name == RegionNames.Barbarian_Village: + add_rule(entrance, woodcutting_rule_d2) + elif outbound_region_name == RegionNames.Edgeville: + add_rule(entrance, woodcutting_rule_d3) + elif outbound_region_name == RegionNames.Wilderness: + add_rule(entrance, woodcutting_rule_all) + + elif region_row.name == RegionNames.South_Of_Varrock: + if outbound_region_name == RegionNames.Canoe_Tree: + add_rule(entrance, + lambda state: (state.can_reach_region(RegionNames.Lumbridge, player) + and woodcutting_rule_d1(state)) or + (state.can_reach_region(RegionNames.Barbarian_Village, player) + and woodcutting_rule_d1(state)) or + (state.can_reach_region(RegionNames.Edgeville, player) + and woodcutting_rule_d2(state)) or + (state.can_reach_region(RegionNames.Wilderness, player) + and woodcutting_rule_d3(state))) + + # Access to other chunks based on woodcutting settings + elif outbound_region_name == RegionNames.Lumbridge: + add_rule(entrance, woodcutting_rule_d1) + elif outbound_region_name == RegionNames.Barbarian_Village: + add_rule(entrance, woodcutting_rule_d1) + elif outbound_region_name == RegionNames.Edgeville: + add_rule(entrance, woodcutting_rule_d3) + elif outbound_region_name == RegionNames.Wilderness: + add_rule(entrance, woodcutting_rule_all) + elif region_row.name == RegionNames.Barbarian_Village: + if outbound_region_name == RegionNames.Canoe_Tree: + add_rule(entrance, + lambda state: (state.can_reach_region(RegionNames.Lumbridge, player) + and woodcutting_rule_d2(state)) or (state.can_reach_region(RegionNames.South_Of_Varrock, player) + and woodcutting_rule_d1(state)) or (state.can_reach_region(RegionNames.Edgeville, player) + and woodcutting_rule_d1(state)) or (state.can_reach_region(RegionNames.Wilderness, player) + and woodcutting_rule_d2(state))) + + # Access to other chunks based on woodcutting settings + elif outbound_region_name == RegionNames.Lumbridge: + add_rule(entrance, woodcutting_rule_d2) + elif outbound_region_name == RegionNames.South_Of_Varrock: + add_rule(entrance, woodcutting_rule_d1) + # Edgeville does not need to be checked, because it's already adjacent + elif outbound_region_name == RegionNames.Wilderness: + add_rule(entrance, woodcutting_rule_d3) + elif region_row.name == RegionNames.Edgeville: + if outbound_region_name == RegionNames.Canoe_Tree: + add_rule(entrance, + lambda state: (state.can_reach_region(RegionNames.Lumbridge, player) + and woodcutting_rule_d3(state)) or + (state.can_reach_region(RegionNames.South_Of_Varrock, player) + and woodcutting_rule_d2(state)) or + (state.can_reach_region(RegionNames.Barbarian_Village, player) + and woodcutting_rule_d1(state)) or + (state.can_reach_region(RegionNames.Wilderness, player) + and woodcutting_rule_d1(state))) + + # Access to other chunks based on woodcutting settings + elif outbound_region_name == RegionNames.Lumbridge: + add_rule(entrance, woodcutting_rule_d3) + elif outbound_region_name == RegionNames.South_Of_Varrock: + add_rule(entrance, woodcutting_rule_d2) + # Barbarian Village does not need to be checked, because it's already adjacent + # Wilderness does not need to be checked, because it's already adjacent + elif region_row.name == RegionNames.Wilderness: + if outbound_region_name == RegionNames.Canoe_Tree: + add_rule(entrance, + lambda state: (state.can_reach_region(RegionNames.Lumbridge, player) + and woodcutting_rule_all(state)) or + (state.can_reach_region(RegionNames.South_Of_Varrock, player) + and woodcutting_rule_d3(state)) or + (state.can_reach_region(RegionNames.Barbarian_Village, player) + and woodcutting_rule_d2(state)) or + (state.can_reach_region(RegionNames.Edgeville, player) + and woodcutting_rule_d1(state))) + + # Access to other chunks based on woodcutting settings + elif outbound_region_name == RegionNames.Lumbridge: + add_rule(entrance, woodcutting_rule_all) + elif outbound_region_name == RegionNames.South_Of_Varrock: + add_rule(entrance, woodcutting_rule_d3) + elif outbound_region_name == RegionNames.Barbarian_Village: + add_rule(entrance, woodcutting_rule_d2) + # Edgeville does not need to be checked, because it's already adjacent diff --git a/worlds/osrs/__init__.py b/worlds/osrs/__init__.py index 9ed55f218d9f..d6ddd63875f4 100644 --- a/worlds/osrs/__init__.py +++ b/worlds/osrs/__init__.py @@ -1,12 +1,12 @@ import typing -from BaseClasses import Item, Tutorial, ItemClassification, Region, MultiWorld +from BaseClasses import Item, Tutorial, ItemClassification, Region, MultiWorld, CollectionState +from Fill import fill_restrictive, FillError from worlds.AutoWorld import WebWorld, World -from worlds.generic.Rules import add_rule, CollectionRule from .Items import OSRSItem, starting_area_dict, chunksanity_starting_chunks, QP_Items, ItemRow, \ chunksanity_special_region_names from .Locations import OSRSLocation, LocationRow - +from .Rules import * from .Options import OSRSOptions, StartingArea from .Names import LocationNames, ItemNames, RegionNames @@ -46,6 +46,7 @@ class OSRSWorld(World): web = OSRSWeb() base_id = 0x070000 data_version = 1 + explicit_indirect_conditions = False item_name_to_id = {item_rows[i].name: 0x070000 + i for i in range(len(item_rows))} location_name_to_id = {location_rows[i].name: 0x070000 + i for i in range(len(location_rows))} @@ -61,6 +62,7 @@ class OSRSWorld(World): starting_area_item: str locations_by_category: typing.Dict[str, typing.List[LocationRow]] + available_QP_locations: typing.List[str] def __init__(self, multiworld: MultiWorld, player: int): super().__init__(multiworld, player) @@ -75,6 +77,7 @@ def __init__(self, multiworld: MultiWorld, player: int): self.starting_area_item = "" self.locations_by_category = {} + self.available_QP_locations = [] def generate_early(self) -> None: location_categories = [location_row.category for location_row in location_rows] @@ -90,9 +93,9 @@ def generate_early(self) -> None: rnd = self.random starting_area = self.options.starting_area - + #UT specific override, if we are in normal gen, resolve starting area, we will get it from slot_data in UT - if not hasattr(self.multiworld, "generation_is_fake"): + if not hasattr(self.multiworld, "generation_is_fake"): if starting_area.value == StartingArea.option_any_bank: self.starting_area_item = rnd.choice(starting_area_dict) elif starting_area.value < StartingArea.option_chunksanity: @@ -127,7 +130,6 @@ def interpret_slot_data(self, slot_data: typing.Dict[str, typing.Any]) -> None: starting_entrance.access_rule = lambda state: state.has(self.starting_area_item, self.player) starting_entrance.connect(self.region_name_to_data[starting_area_region]) - def create_regions(self) -> None: """ called to place player's regions into the MultiWorld's regions list. If it's hard to separate, this can be done @@ -145,7 +147,8 @@ def create_regions(self) -> None: # Removes the word "Area: " from the item name to get the region it applies to. # I figured tacking "Area: " at the beginning would make it _easier_ to tell apart. Turns out it made it worse - if self.starting_area_item != "": #if area hasn't been set, then we shouldn't connect it + # if area hasn't been set, then we shouldn't connect it + if self.starting_area_item != "": if self.starting_area_item in chunksanity_special_region_names: starting_area_region = chunksanity_special_region_names[self.starting_area_item] else: @@ -164,11 +167,8 @@ def create_regions(self) -> None: entrance.connect(self.region_name_to_data[parsed_outbound]) item_name = self.region_rows_by_name[parsed_outbound].itemReq - if "*" not in outbound_region_name and "*" not in item_name: - entrance.access_rule = lambda state, item_name=item_name: state.has(item_name, self.player) - continue - - self.generate_special_rules_for(entrance, region_row, outbound_region_name) + entrance.access_rule = lambda state, item_name=item_name.replace("*",""): state.has(item_name, self.player) + generate_special_rules_for(entrance, region_row, outbound_region_name, self.player, self.options) for resource_region in region_row.resources: if not resource_region: @@ -178,217 +178,34 @@ def create_regions(self) -> None: if "*" not in resource_region: entrance.connect(self.region_name_to_data[resource_region]) else: - self.generate_special_rules_for(entrance, region_row, resource_region) entrance.connect(self.region_name_to_data[resource_region.replace('*', '')]) + generate_special_rules_for(entrance, region_row, resource_region, self.player, self.options) self.roll_locations() - def generate_special_rules_for(self, entrance, region_row, outbound_region_name): - # print(f"Special rules required to access region {outbound_region_name} from {region_row.name}") - if outbound_region_name == RegionNames.Cooks_Guild: - item_name = self.region_rows_by_name[outbound_region_name].itemReq.replace('*', '') - cooking_level_rule = self.get_skill_rule("cooking", 32) - entrance.access_rule = lambda state: state.has(item_name, self.player) and \ - cooking_level_rule(state) - return - if outbound_region_name == RegionNames.Crafting_Guild: - item_name = self.region_rows_by_name[outbound_region_name].itemReq.replace('*', '') - crafting_level_rule = self.get_skill_rule("crafting", 40) - entrance.access_rule = lambda state: state.has(item_name, self.player) and \ - crafting_level_rule(state) - return - if outbound_region_name == RegionNames.Corsair_Cove: - item_name = self.region_rows_by_name[outbound_region_name].itemReq.replace('*', '') - # Need to be able to start Corsair Curse in addition to having the item - entrance.access_rule = lambda state: state.has(item_name, self.player) and \ - state.can_reach(RegionNames.Falador_Farm, "Region", self.player) - self.multiworld.register_indirect_condition( - self.multiworld.get_region(RegionNames.Falador_Farm, self.player), entrance) - - return - if outbound_region_name == "Camdozaal*": - item_name = self.region_rows_by_name[outbound_region_name.replace('*', '')].itemReq - entrance.access_rule = lambda state: state.has(item_name, self.player) and \ - state.has(ItemNames.QP_Below_Ice_Mountain, self.player) - return - if region_row.name == "Dwarven Mountain Pass" and outbound_region_name == "Anvil*": - entrance.access_rule = lambda state: state.has(ItemNames.QP_Dorics_Quest, self.player) - return - # Special logic for canoes - canoe_regions = [RegionNames.Lumbridge, RegionNames.South_Of_Varrock, RegionNames.Barbarian_Village, - RegionNames.Edgeville, RegionNames.Wilderness] - if region_row.name in canoe_regions: - # Skill rules for greater distances - woodcutting_rule_d1 = self.get_skill_rule("woodcutting", 12) - woodcutting_rule_d2 = self.get_skill_rule("woodcutting", 27) - woodcutting_rule_d3 = self.get_skill_rule("woodcutting", 42) - woodcutting_rule_all = self.get_skill_rule("woodcutting", 57) - - if region_row.name == RegionNames.Lumbridge: - # Canoe Tree access for the Location - if outbound_region_name == RegionNames.Canoe_Tree: - entrance.access_rule = \ - lambda state: (state.can_reach_region(RegionNames.South_Of_Varrock, self.player) - and woodcutting_rule_d1(state) and self.options.max_woodcutting_level >= 12) or \ - (state.can_reach_region(RegionNames.Barbarian_Village) - and woodcutting_rule_d2(state) and self.options.max_woodcutting_level >= 27) or \ - (state.can_reach_region(RegionNames.Edgeville) - and woodcutting_rule_d3(state) and self.options.max_woodcutting_level >= 42) or \ - (state.can_reach_region(RegionNames.Wilderness) - and woodcutting_rule_all(state) and self.options.max_woodcutting_level >= 57) - self.multiworld.register_indirect_condition( - self.multiworld.get_region(RegionNames.South_Of_Varrock, self.player), entrance) - self.multiworld.register_indirect_condition( - self.multiworld.get_region(RegionNames.Barbarian_Village, self.player), entrance) - self.multiworld.register_indirect_condition( - self.multiworld.get_region(RegionNames.Edgeville, self.player), entrance) - self.multiworld.register_indirect_condition( - self.multiworld.get_region(RegionNames.Wilderness, self.player), entrance) - # Access to other chunks based on woodcutting settings - # South of Varrock does not need to be checked, because it's already adjacent - if outbound_region_name == RegionNames.Barbarian_Village: - entrance.access_rule = lambda state: woodcutting_rule_d2(state) \ - and self.options.max_woodcutting_level >= 27 - if outbound_region_name == RegionNames.Edgeville: - entrance.access_rule = lambda state: woodcutting_rule_d3(state) \ - and self.options.max_woodcutting_level >= 42 - if outbound_region_name == RegionNames.Wilderness: - entrance.access_rule = lambda state: woodcutting_rule_all(state) \ - and self.options.max_woodcutting_level >= 57 - - if region_row.name == RegionNames.South_Of_Varrock: - if outbound_region_name == RegionNames.Canoe_Tree: - entrance.access_rule = \ - lambda state: (state.can_reach_region(RegionNames.Lumbridge, self.player) - and woodcutting_rule_d1(state) and self.options.max_woodcutting_level >= 12) or \ - (state.can_reach_region(RegionNames.Barbarian_Village) - and woodcutting_rule_d1(state) and self.options.max_woodcutting_level >= 12) or \ - (state.can_reach_region(RegionNames.Edgeville) - and woodcutting_rule_d2(state) and self.options.max_woodcutting_level >= 27) or \ - (state.can_reach_region(RegionNames.Wilderness) - and woodcutting_rule_d3(state) and self.options.max_woodcutting_level >= 42) - self.multiworld.register_indirect_condition( - self.multiworld.get_region(RegionNames.Lumbridge, self.player), entrance) - self.multiworld.register_indirect_condition( - self.multiworld.get_region(RegionNames.Barbarian_Village, self.player), entrance) - self.multiworld.register_indirect_condition( - self.multiworld.get_region(RegionNames.Edgeville, self.player), entrance) - self.multiworld.register_indirect_condition( - self.multiworld.get_region(RegionNames.Wilderness, self.player), entrance) - # Access to other chunks based on woodcutting settings - # Lumbridge does not need to be checked, because it's already adjacent - if outbound_region_name == RegionNames.Barbarian_Village: - entrance.access_rule = lambda state: woodcutting_rule_d1(state) \ - and self.options.max_woodcutting_level >= 12 - if outbound_region_name == RegionNames.Edgeville: - entrance.access_rule = lambda state: woodcutting_rule_d3(state) \ - and self.options.max_woodcutting_level >= 27 - if outbound_region_name == RegionNames.Wilderness: - entrance.access_rule = lambda state: woodcutting_rule_all(state) \ - and self.options.max_woodcutting_level >= 42 - if region_row.name == RegionNames.Barbarian_Village: - if outbound_region_name == RegionNames.Canoe_Tree: - entrance.access_rule = \ - lambda state: (state.can_reach_region(RegionNames.Lumbridge, self.player) - and woodcutting_rule_d2(state) and self.options.max_woodcutting_level >= 27) or \ - (state.can_reach_region(RegionNames.South_Of_Varrock) - and woodcutting_rule_d1(state) and self.options.max_woodcutting_level >= 12) or \ - (state.can_reach_region(RegionNames.Edgeville) - and woodcutting_rule_d1(state) and self.options.max_woodcutting_level >= 12) or \ - (state.can_reach_region(RegionNames.Wilderness) - and woodcutting_rule_d2(state) and self.options.max_woodcutting_level >= 27) - self.multiworld.register_indirect_condition( - self.multiworld.get_region(RegionNames.Lumbridge, self.player), entrance) - self.multiworld.register_indirect_condition( - self.multiworld.get_region(RegionNames.South_Of_Varrock, self.player), entrance) - self.multiworld.register_indirect_condition( - self.multiworld.get_region(RegionNames.Edgeville, self.player), entrance) - self.multiworld.register_indirect_condition( - self.multiworld.get_region(RegionNames.Wilderness, self.player), entrance) - # Access to other chunks based on woodcutting settings - if outbound_region_name == RegionNames.Lumbridge: - entrance.access_rule = lambda state: woodcutting_rule_d2(state) \ - and self.options.max_woodcutting_level >= 27 - if outbound_region_name == RegionNames.South_Of_Varrock: - entrance.access_rule = lambda state: woodcutting_rule_d1(state) \ - and self.options.max_woodcutting_level >= 12 - # Edgeville does not need to be checked, because it's already adjacent - if outbound_region_name == RegionNames.Wilderness: - entrance.access_rule = lambda state: woodcutting_rule_d3(state) \ - and self.options.max_woodcutting_level >= 42 - if region_row.name == RegionNames.Edgeville: - if outbound_region_name == RegionNames.Canoe_Tree: - entrance.access_rule = \ - lambda state: (state.can_reach_region(RegionNames.Lumbridge, self.player) - and woodcutting_rule_d3(state) and self.options.max_woodcutting_level >= 42) or \ - (state.can_reach_region(RegionNames.South_Of_Varrock) - and woodcutting_rule_d2(state) and self.options.max_woodcutting_level >= 27) or \ - (state.can_reach_region(RegionNames.Barbarian_Village) - and woodcutting_rule_d1(state) and self.options.max_woodcutting_level >= 12) or \ - (state.can_reach_region(RegionNames.Wilderness) - and woodcutting_rule_d1(state) and self.options.max_woodcutting_level >= 12) - self.multiworld.register_indirect_condition( - self.multiworld.get_region(RegionNames.Lumbridge, self.player), entrance) - self.multiworld.register_indirect_condition( - self.multiworld.get_region(RegionNames.South_Of_Varrock, self.player), entrance) - self.multiworld.register_indirect_condition( - self.multiworld.get_region(RegionNames.Barbarian_Village, self.player), entrance) - self.multiworld.register_indirect_condition( - self.multiworld.get_region(RegionNames.Wilderness, self.player), entrance) - # Access to other chunks based on woodcutting settings - if outbound_region_name == RegionNames.Lumbridge: - entrance.access_rule = lambda state: woodcutting_rule_d3(state) \ - and self.options.max_woodcutting_level >= 42 - if outbound_region_name == RegionNames.South_Of_Varrock: - entrance.access_rule = lambda state: woodcutting_rule_d2(state) \ - and self.options.max_woodcutting_level >= 27 - # Barbarian Village does not need to be checked, because it's already adjacent - # Wilderness does not need to be checked, because it's already adjacent - if region_row.name == RegionNames.Wilderness: - if outbound_region_name == RegionNames.Canoe_Tree: - entrance.access_rule = \ - lambda state: (state.can_reach_region(RegionNames.Lumbridge, self.player) - and woodcutting_rule_all(state) and self.options.max_woodcutting_level >= 57) or \ - (state.can_reach_region(RegionNames.South_Of_Varrock) - and woodcutting_rule_d3(state) and self.options.max_woodcutting_level >= 42) or \ - (state.can_reach_region(RegionNames.Barbarian_Village) - and woodcutting_rule_d2(state) and self.options.max_woodcutting_level >= 27) or \ - (state.can_reach_region(RegionNames.Edgeville) - and woodcutting_rule_d1(state) and self.options.max_woodcutting_level >= 12) - self.multiworld.register_indirect_condition( - self.multiworld.get_region(RegionNames.Lumbridge, self.player), entrance) - self.multiworld.register_indirect_condition( - self.multiworld.get_region(RegionNames.South_Of_Varrock, self.player), entrance) - self.multiworld.register_indirect_condition( - self.multiworld.get_region(RegionNames.Barbarian_Village, self.player), entrance) - self.multiworld.register_indirect_condition( - self.multiworld.get_region(RegionNames.Edgeville, self.player), entrance) - # Access to other chunks based on woodcutting settings - if outbound_region_name == RegionNames.Lumbridge: - entrance.access_rule = lambda state: woodcutting_rule_all(state) \ - and self.options.max_woodcutting_level >= 57 - if outbound_region_name == RegionNames.South_Of_Varrock: - entrance.access_rule = lambda state: woodcutting_rule_d3(state) \ - and self.options.max_woodcutting_level >= 42 - if outbound_region_name == RegionNames.Barbarian_Village: - entrance.access_rule = lambda state: woodcutting_rule_d2(state) \ - and self.options.max_woodcutting_level >= 27 - # Edgeville does not need to be checked, because it's already adjacent + def task_within_skill_levels(self, skills_required): + # Loop through each required skill. If any of its requirements are out of the defined limit, return false + for skill in skills_required: + max_level_for_skill = getattr(self.options, f"max_{skill.skill.lower()}_level") + if skill.level > max_level_for_skill: + return False + return True def roll_locations(self): - locations_required = 0 generation_is_fake = hasattr(self.multiworld, "generation_is_fake") # UT specific override + locations_required = 0 for item_row in item_rows: locations_required += item_row.amount locations_added = 1 # At this point we've already added the starting area, so we start at 1 instead of 0 - # Quests are always added + # Quests are always added first, before anything else is rolled for i, location_row in enumerate(location_rows): if location_row.category in {"quest", "points", "goal"}: - self.create_and_add_location(i) - if location_row.category == "quest": - locations_added += 1 + if self.task_within_skill_levels(location_row.skills): + self.create_and_add_location(i) + if location_row.category == "quest": + locations_added += 1 # Build up the weighted Task Pool rnd = self.random @@ -412,10 +229,9 @@ def roll_locations(self): task_types = ["prayer", "magic", "runecraft", "mining", "crafting", "smithing", "fishing", "cooking", "firemaking", "woodcutting", "combat"] for task_type in task_types: - max_level_for_task_type = getattr(self.options, f"max_{task_type}_level") max_amount_for_task_type = getattr(self.options, f"max_{task_type}_tasks") tasks_for_this_type = [task for task in self.locations_by_category[task_type] - if task.skills[0].level <= max_level_for_task_type] + if self.task_within_skill_levels(task.skills)] if not self.options.progressive_tasks: rnd.shuffle(tasks_for_this_type) else: @@ -464,6 +280,7 @@ def roll_locations(self): self.add_location(task) locations_added += 1 + def add_location(self, location): index = [i for i in range(len(location_rows)) if location_rows[i].name == location.name][0] self.create_and_add_location(index) @@ -482,11 +299,15 @@ def get_filler_item_name(self) -> str: def create_and_add_location(self, row_index) -> None: location_row = location_rows[row_index] - # print(f"Adding task {location_row.name}") + + # Quest Points are handled differently now, but in case this gets fed an older version of the data sheet, + # the points might still be listed in a different row + if location_row.category == "points": + return # Create Location location_id = self.base_id + row_index - if location_row.category == "points" or location_row.category == "goal": + if location_row.category == "goal": location_id = None location = OSRSLocation(self.player, location_row.name, location_id) self.location_name_to_data[location_row.name] = location @@ -498,6 +319,14 @@ def create_and_add_location(self, row_index) -> None: location.parent_region = region region.locations.append(location) + # If it's a quest, generate a "Points" location we'll add an event to + if location_row.category == "quest": + points_name = location_row.name.replace("Quest:", "Points:") + points_location = OSRSLocation(self.player, points_name) + self.location_name_to_data[points_name] = points_location + points_location.parent_region = region + region.locations.append(points_location) + def set_rules(self) -> None: """ called to set access and item rules on locations and entrances. @@ -508,18 +337,26 @@ def set_rules(self) -> None: "Witchs_Potion", "Knights_Sword", "Goblin_Diplomacy", "Pirates_Treasure", "Rune_Mysteries", "Misthalin_Mystery", "Corsair_Curse", "X_Marks_the_Spot", "Below_Ice_Mountain"] - for qp_attr_name in quest_attr_names: - loc_name = getattr(LocationNames, f"QP_{qp_attr_name}") - item_name = getattr(ItemNames, f"QP_{qp_attr_name}") - self.multiworld.get_location(loc_name, self.player) \ - .place_locked_item(self.create_event(item_name)) for quest_attr_name in quest_attr_names: qp_loc_name = getattr(LocationNames, f"QP_{quest_attr_name}") + qp_loc = self.location_name_to_data.get(qp_loc_name) + q_loc_name = getattr(LocationNames, f"Q_{quest_attr_name}") - add_rule(self.multiworld.get_location(qp_loc_name, self.player), lambda state, q_loc_name=q_loc_name: ( - self.multiworld.get_location(q_loc_name, self.player).can_reach(state) - )) + q_loc = self.location_name_to_data.get(q_loc_name) + + # Checks to make sure the task is actually in the list before trying to create its rules + if qp_loc and q_loc: + # Create the QP Event Item + item_name = getattr(ItemNames, f"QP_{quest_attr_name}") + qp_loc.place_locked_item(self.create_event(item_name)) + + # If a quest is excluded, don't actually consider it for quest point progression + if q_loc_name not in self.options.exclude_locations: + self.available_QP_locations.append(item_name) + + # Set the access rule for the QP Location + add_rule(qp_loc, lambda state, loc=q_loc: (loc.can_reach(state))) # place "Victory" at "Dragon Slayer" and set collection as win condition self.multiworld.get_location(LocationNames.Q_Dragon_Slayer, self.player) \ @@ -535,7 +372,7 @@ def set_rules(self) -> None: lambda state, region_required=region_required: state.can_reach(region_required, "Region", self.player)) for skill_req in location_row.skills: - add_rule(location, self.get_skill_rule(skill_req.skill, skill_req.level)) + add_rule(location, get_skill_rule(skill_req.skill, skill_req.level, self.player, self.options)) for item_req in location_row.items: add_rule(location, lambda state, item_req=item_req: state.has(item_req, self.player)) if location_row.qp: @@ -560,124 +397,8 @@ def create_event(self, event: str): def quest_points(self, state): qp = 0 - for qp_event in QP_Items: + for qp_event in self.available_QP_locations: if state.has(qp_event, self.player): qp += int(qp_event[0]) return qp - """ - Ensures a target level can be reached with available resources - """ - - def get_skill_rule(self, skill, level) -> CollectionRule: - if skill.lower() == "fishing": - if self.options.brutal_grinds or level < 5: - return lambda state: state.can_reach(RegionNames.Shrimp, "Region", self.player) - if level < 20: - return lambda state: state.can_reach(RegionNames.Shrimp, "Region", self.player) and \ - state.can_reach(RegionNames.Port_Sarim, "Region", self.player) - else: - return lambda state: state.can_reach(RegionNames.Shrimp, "Region", self.player) and \ - state.can_reach(RegionNames.Port_Sarim, "Region", self.player) and \ - state.can_reach(RegionNames.Fly_Fish, "Region", self.player) - if skill.lower() == "mining": - if self.options.brutal_grinds or level < 15: - return lambda state: state.can_reach(RegionNames.Bronze_Ores, "Region", self.player) or \ - state.can_reach(RegionNames.Clay_Rock, "Region", self.player) - else: - # Iron is the best way to train all the way to 99, so having access to iron is all you need to check for - return lambda state: (state.can_reach(RegionNames.Bronze_Ores, "Region", self.player) or - state.can_reach(RegionNames.Clay_Rock, "Region", self.player)) and \ - state.can_reach(RegionNames.Iron_Rock, "Region", self.player) - if skill.lower() == "woodcutting": - if self.options.brutal_grinds or level < 15: - # I've checked. There is not a single chunk in the f2p that does not have at least one normal tree. - # Even the desert. - return lambda state: True - if level < 30: - return lambda state: state.can_reach(RegionNames.Oak_Tree, "Region", self.player) - else: - return lambda state: state.can_reach(RegionNames.Oak_Tree, "Region", self.player) and \ - state.can_reach(RegionNames.Willow_Tree, "Region", self.player) - if skill.lower() == "smithing": - if self.options.brutal_grinds: - return lambda state: state.can_reach(RegionNames.Bronze_Ores, "Region", self.player) and \ - state.can_reach(RegionNames.Furnace, "Region", self.player) - if level < 15: - # Lumbridge has a special bronze-only anvil. This is the only anvil of its type so it's not included - # in the "Anvil" resource region. We still need to check for it though. - return lambda state: state.can_reach(RegionNames.Bronze_Ores, "Region", self.player) and \ - state.can_reach(RegionNames.Furnace, "Region", self.player) and \ - (state.can_reach(RegionNames.Anvil, "Region", self.player) or - state.can_reach(RegionNames.Lumbridge, "Region", self.player)) - if level < 30: - # For levels between 15 and 30, the lumbridge anvil won't cut it. Only a real one will do - return lambda state: state.can_reach(RegionNames.Bronze_Ores, "Region", self.player) and \ - state.can_reach(RegionNames.Iron_Rock, "Region", self.player) and \ - state.can_reach(RegionNames.Furnace, "Region", self.player) and \ - state.can_reach(RegionNames.Anvil, "Region", self.player) - else: - return lambda state: state.can_reach(RegionNames.Bronze_Ores, "Region", self.player) and \ - state.can_reach(RegionNames.Iron_Rock, "Region", self.player) and \ - state.can_reach(RegionNames.Coal_Rock, "Region", self.player) and \ - state.can_reach(RegionNames.Furnace, "Region", self.player) and \ - state.can_reach(RegionNames.Anvil, "Region", self.player) - if skill.lower() == "crafting": - # Crafting is really complex. Need a lot of sub-rules to make this even remotely readable - def can_spin(state): - return state.can_reach(RegionNames.Sheep, "Region", self.player) and \ - state.can_reach(RegionNames.Spinning_Wheel, "Region", self.player) - - def can_pot(state): - return state.can_reach(RegionNames.Clay_Rock, "Region", self.player) and \ - state.can_reach(RegionNames.Barbarian_Village, "Region", self.player) - - def can_tan(state): - return state.can_reach(RegionNames.Milk, "Region", self.player) and \ - state.can_reach(RegionNames.Al_Kharid, "Region", self.player) - - def mould_access(state): - return state.can_reach(RegionNames.Al_Kharid, "Region", self.player) or \ - state.can_reach(RegionNames.Rimmington, "Region", self.player) - - def can_silver(state): - - return state.can_reach(RegionNames.Silver_Rock, "Region", self.player) and \ - state.can_reach(RegionNames.Furnace, "Region", self.player) and mould_access(state) - - def can_gold(state): - return state.can_reach(RegionNames.Gold_Rock, "Region", self.player) and \ - state.can_reach(RegionNames.Furnace, "Region", self.player) and mould_access(state) - - if self.options.brutal_grinds or level < 5: - return lambda state: can_spin(state) or can_pot(state) or can_tan(state) - - can_smelt_gold = self.get_skill_rule("smithing", 40) - can_smelt_silver = self.get_skill_rule("smithing", 20) - if level < 16: - return lambda state: can_pot(state) or can_tan(state) or (can_gold(state) and can_smelt_gold(state)) - else: - return lambda state: can_tan(state) or (can_silver(state) and can_smelt_silver(state)) or \ - (can_gold(state) and can_smelt_gold(state)) - if skill.lower() == "cooking": - if self.options.brutal_grinds or level < 15: - return lambda state: state.can_reach(RegionNames.Milk, "Region", self.player) or \ - state.can_reach(RegionNames.Egg, "Region", self.player) or \ - state.can_reach(RegionNames.Shrimp, "Region", self.player) or \ - (state.can_reach(RegionNames.Wheat, "Region", self.player) and - state.can_reach(RegionNames.Windmill, "Region", self.player)) - else: - can_catch_fly_fish = self.get_skill_rule("fishing", 20) - return lambda state: state.can_reach(RegionNames.Fly_Fish, "Region", self.player) and \ - can_catch_fly_fish(state) and \ - (state.can_reach(RegionNames.Milk, "Region", self.player) or - state.can_reach(RegionNames.Egg, "Region", self.player) or - state.can_reach(RegionNames.Shrimp, "Region", self.player) or - (state.can_reach(RegionNames.Wheat, "Region", self.player) and - state.can_reach(RegionNames.Windmill, "Region", self.player))) - if skill.lower() == "runecraft": - return lambda state: state.has(ItemNames.QP_Rune_Mysteries, self.player) - if skill.lower() == "magic": - return lambda state: state.can_reach(RegionNames.Mind_Runes, "Region", self.player) - - return lambda state: True diff --git a/worlds/overcooked2/Logic.py b/worlds/overcooked2/Logic.py index 20111aa01d66..cf268509493c 100644 --- a/worlds/overcooked2/Logic.py +++ b/worlds/overcooked2/Logic.py @@ -35,17 +35,13 @@ def has_requirements_for_level_star( state: CollectionState, level: Overcooked2GenericLevel, stars: int, player: int) -> bool: assert 0 <= stars <= 3 - # First ensure that previous stars are obtainable - if stars > 1: - if not has_requirements_for_level_star(state, level, stars-1, player): - return False - - # Second, ensure that global requirements are met + # First, ensure that global requirements for this many stars are met. + # Lower numbers of stars are implied meetable if this level is meetable. if not meets_requirements(state, "*", stars, player): return False - # Finally, return success only if this level's requirements are met - return meets_requirements(state, level.shortname, stars, player) + # Then return success only if this level's requirements are met at all stars up through this one + return all(meets_requirements(state, level.shortname, s, player) for s in range(1, stars + 1)) def meets_requirements(state: CollectionState, name: str, stars: int, player: int): @@ -421,6 +417,7 @@ def can_reach_kevin_eight_island(state: CollectionState, player: int, allow_tric }, ), ( # 3-star + # Necessarily implies 2-star [ # Exclusive "Progressive Dash", "Spare Plate", diff --git a/worlds/pokemon_emerald/CHANGELOG.md b/worlds/pokemon_emerald/CHANGELOG.md index 6a1844e79fde..0dd874b25029 100644 --- a/worlds/pokemon_emerald/CHANGELOG.md +++ b/worlds/pokemon_emerald/CHANGELOG.md @@ -8,6 +8,11 @@ ### Fixes +- Fixed a rare issue where receiving a wonder trade could partially corrupt the save data, preventing the player from +receiving new items. +- Fixed the client spamming the "goal complete" status update to the server instead of sending it once. +- Fixed the `trainer_party_blacklist` option checking for the existence of the "_Legendaries" shortcut in the +`starter_blacklist` option instead of itself. - Fixed a logic issue where the "Mauville City - Coin Case from Lady in House" location only required a Harbor Mail if the player randomized NPC gifts. - The Dig tutor has its compatibility percentage raised to 50% if the player's TM/tutor compatibility is set lower. @@ -15,6 +20,8 @@ the player randomized NPC gifts. with another NPC was moved to an unoccupied space. - Fixed a problem where the client would crash on certain operating systems while using certain python versions if the player tried to wonder trade. +- Prevent the poke flute sound from replacing the evolution fanfare, which would cause the game to wait in silence for +a long time during the evolution scene. # 2.2.0 diff --git a/worlds/pokemon_emerald/__init__.py b/worlds/pokemon_emerald/__init__.py index d281dde23cb0..a87f93ece56b 100644 --- a/worlds/pokemon_emerald/__init__.py +++ b/worlds/pokemon_emerald/__init__.py @@ -177,7 +177,7 @@ def generate_early(self) -> None: for species_name in self.options.trainer_party_blacklist.value if species_name != "_Legendaries" } - if "_Legendaries" in self.options.starter_blacklist.value: + if "_Legendaries" in self.options.trainer_party_blacklist.value: self.blacklisted_opponent_pokemon |= LEGENDARY_POKEMON # In race mode we don't patch any item location information into the ROM diff --git a/worlds/pokemon_emerald/client.py b/worlds/pokemon_emerald/client.py index 4405b34074e0..5add7b3fca40 100644 --- a/worlds/pokemon_emerald/client.py +++ b/worlds/pokemon_emerald/client.py @@ -117,6 +117,11 @@ DEFEATED_LEGENDARY_FLAG_MAP = {data.constants[f"FLAG_DEFEATED_{name}"]: name for name in LEGENDARY_NAMES.values()} CAUGHT_LEGENDARY_FLAG_MAP = {data.constants[f"FLAG_CAUGHT_{name}"]: name for name in LEGENDARY_NAMES.values()} +SHOAL_CAVE_MAPS = tuple(data.constants[map_name] for map_name in [ + "MAP_SHOAL_CAVE_LOW_TIDE_ENTRANCE_ROOM", + "MAP_SHOAL_CAVE_LOW_TIDE_INNER_ROOM", +]) + class PokemonEmeraldClient(BizHawkClient): game = "Pokemon Emerald" @@ -414,13 +419,17 @@ async def handle_tracker_info(self, ctx: "BizHawkClientContext", guards: Dict[st read_result = await bizhawk.guarded_read( ctx.bizhawk_ctx, - [(sb1_address + 0x4, 2, "System Bus")], - [guards["SAVE BLOCK 1"]] + [ + (sb1_address + 0x4, 2, "System Bus"), # Current map + (sb1_address + 0x1450 + (data.constants["FLAG_SYS_SHOAL_TIDE"] // 8), 1, "System Bus"), + ], + [guards["IN OVERWORLD"], guards["SAVE BLOCK 1"]] ) if read_result is None: # Save block moved return current_map = int.from_bytes(read_result[0], "big") + shoal_cave = int(read_result[1][0] & (1 << (data.constants["FLAG_SYS_SHOAL_TIDE"] % 8)) > 0) if current_map != self.current_map: self.current_map = current_map await ctx.send_msgs([{ @@ -429,6 +438,7 @@ async def handle_tracker_info(self, ctx: "BizHawkClientContext", guards: Dict[st "data": { "type": "MapUpdate", "mapId": current_map, + **({"tide": shoal_cave} if current_map in SHOAL_CAVE_MAPS else {}), }, }]) diff --git a/worlds/pokemon_emerald/docs/en_Pokemon Emerald.md b/worlds/pokemon_emerald/docs/en_Pokemon Emerald.md index 9a3991e97f75..732b2092a28c 100644 --- a/worlds/pokemon_emerald/docs/en_Pokemon Emerald.md +++ b/worlds/pokemon_emerald/docs/en_Pokemon Emerald.md @@ -30,7 +30,7 @@ randomizer. Here are some of the more important ones: - The Wally catching tutorial is skipped - All text is instant and, with an option, can be automatically progressed by holding A - When a Repel runs out, you will be prompted to use another -- Many more minor improvementsâ€Ļ +- [Many more minor improvementsâ€Ļ](/tutorial/Pokemon%20Emerald/rom_changes/en) ## Where is my starting inventory? diff --git a/worlds/pokemon_emerald/docs/rom changes.md b/worlds/pokemon_emerald/docs/rom_changes_en.md similarity index 100% rename from worlds/pokemon_emerald/docs/rom changes.md rename to worlds/pokemon_emerald/docs/rom_changes_en.md diff --git a/worlds/pokemon_emerald/options.py b/worlds/pokemon_emerald/options.py index e05b5d96ac74..8fcc74d1c34a 100644 --- a/worlds/pokemon_emerald/options.py +++ b/worlds/pokemon_emerald/options.py @@ -123,6 +123,8 @@ class Dexsanity(Toggle): Defeating gym leaders provides dex info, allowing you to see where on the map you can catch species you need. Each pokedex entry adds a Poke Ball, Great Ball, or Ultra Ball to the pool. + + Warning: This adds a lot of locations and will slow you down significantly. """ display_name = "Dexsanity" @@ -132,6 +134,8 @@ class Trainersanity(Toggle): Defeating a trainer gives you an item. Trainers are no longer missable. Trainers no longer give you money for winning. Each trainer adds a valuable item (Nugget, Stardust, etc.) to the pool. + + Warning: This adds a lot of locations and will slow you down significantly. """ display_name = "Trainersanity" @@ -265,6 +269,8 @@ class RandomizeWildPokemon(Choice): """ Randomizes wild pokemon encounters (grass, caves, water, fishing). + Warning: Matching both base stats and type may severely limit the variety for certain pokemon. + - Vanilla: Wild encounters are unchanged - Match Base Stats: Wild pokemon are replaced with species with approximately the same bst - Match Type: Wild pokemon are replaced with species that share a type with the original @@ -327,6 +333,8 @@ class RandomizeTrainerParties(Choice): """ Randomizes the parties of all trainers. + Warning: Matching both base stats and type may severely limit the variety for certain pokemon. + - Vanilla: Parties are unchanged - Match Base Stats: Trainer pokemon are replaced with species with approximately the same bst - Match Type: Trainer pokemon are replaced with species that share a type with the original @@ -357,6 +365,10 @@ class TrainerPartyBlacklist(OptionSet): class ForceFullyEvolved(Range): """ When an opponent uses a pokemon of the specified level or higher, restricts the species to only fully evolved pokemon. + + Only applies when trainer parties are randomized. + + Warning: Combining a low value with matched base stats may severely limit the variety for certain pokemon. """ display_name = "Force Fully Evolved" range_start = 1 diff --git a/worlds/pokemon_emerald/rom.py b/worlds/pokemon_emerald/rom.py index 2c0b5021d099..e2a7a4800bfb 100644 --- a/worlds/pokemon_emerald/rom.py +++ b/worlds/pokemon_emerald/rom.py @@ -73,6 +73,7 @@ "MUS_OBTAIN_SYMBOL": 318, "MUS_REGISTER_MATCH_CALL": 135, } +_EVOLUTION_FANFARE_INDEX = list(_FANFARES.keys()).index("MUS_EVOLVED") CAVE_EVENT_NAME_TO_ID = { "TERRA_CAVE_ROUTE_114_1": 1, @@ -661,6 +662,15 @@ def write_tokens(world: "PokemonEmeraldWorld", patch: PokemonEmeraldProcedurePat # Shuffle the lists, pair new tracks with original tracks, set the new track ids, and set new fanfare durations randomized_fanfares = [fanfare_name for fanfare_name in _FANFARES] world.random.shuffle(randomized_fanfares) + + # Prevent the evolution fanfare from receiving the poke flute by swapping it with something else. + # The poke flute sound causes the evolution scene to get stuck for like 40 seconds + if randomized_fanfares[_EVOLUTION_FANFARE_INDEX] == "MUS_RG_POKE_FLUTE": + swap_index = (_EVOLUTION_FANFARE_INDEX + 1) % len(_FANFARES) + temp = randomized_fanfares[_EVOLUTION_FANFARE_INDEX] + randomized_fanfares[_EVOLUTION_FANFARE_INDEX] = randomized_fanfares[swap_index] + randomized_fanfares[swap_index] = temp + for i, fanfare_pair in enumerate(zip(_FANFARES.keys(), randomized_fanfares)): patch.write_token( APTokenTypes.WRITE, diff --git a/worlds/pokemon_emerald/test/__init__.py b/worlds/pokemon_emerald/test/__init__.py index 84ce64003d57..bf2a8da5b0c5 100644 --- a/worlds/pokemon_emerald/test/__init__.py +++ b/worlds/pokemon_emerald/test/__init__.py @@ -1,4 +1,4 @@ -from test.TestBase import WorldTestBase +from test.bases import WorldTestBase class PokemonEmeraldTestBase(WorldTestBase): diff --git a/worlds/pokemon_emerald/test/test_warps.py b/worlds/pokemon_emerald/test/test_warps.py index 75a2417dfbe6..d1b5b01dcf7f 100644 --- a/worlds/pokemon_emerald/test/test_warps.py +++ b/worlds/pokemon_emerald/test/test_warps.py @@ -1,4 +1,4 @@ -from test.TestBase import TestBase +from test.bases import TestBase from ..data import Warp diff --git a/worlds/pokemon_rb/__init__.py b/worlds/pokemon_rb/__init__.py index 2065507e0d59..809179cbef74 100644 --- a/worlds/pokemon_rb/__init__.py +++ b/worlds/pokemon_rb/__init__.py @@ -526,15 +526,24 @@ def stage_post_fill(cls, multiworld): # This cuts down on time spent calculating the spoiler playthrough. found_mons = set() for sphere in multiworld.get_spheres(): + mon_locations_in_sphere = {} for location in sphere: - if (location.game == "Pokemon Red and Blue" and (location.item.name in poke_data.pokemon_data.keys() - or "Static " in location.item.name) + if (location.game == location.item.game == "Pokemon Red and Blue" + and (location.item.name in poke_data.pokemon_data.keys() or "Static " in location.item.name) and location.item.advancement): key = (location.player, location.item.name) if key in found_mons: location.item.classification = ItemClassification.useful else: - found_mons.add(key) + mon_locations_in_sphere.setdefault(key, []).append(location) + for key, mon_locations in mon_locations_in_sphere.items(): + found_mons.add(key) + if len(mon_locations) > 1: + # Sort for deterministic results. + mon_locations.sort() + # Convert all but the first to useful classification. + for location in mon_locations[1:]: + location.item.classification = ItemClassification.useful def create_regions(self): if (self.options.old_man == "vanilla" or @@ -703,6 +712,7 @@ def fill_slot_data(self) -> dict: "require_pokedex": self.options.require_pokedex.value, "area_1_to_1_mapping": self.options.area_1_to_1_mapping.value, "blind_trainers": self.options.blind_trainers.value, + "v5_update": True, } if self.options.type_chart_seed == "random" or self.options.type_chart_seed.value.isdigit(): diff --git a/worlds/pokemon_rb/rules.py b/worlds/pokemon_rb/rules.py index ba4bfd471c52..3c1cdc57e99b 100644 --- a/worlds/pokemon_rb/rules.py +++ b/worlds/pokemon_rb/rules.py @@ -94,6 +94,9 @@ def prize_rule(i): "Route 22 - Trainer Parties": lambda state: state.has("Oak's Parcel", player), + "Victory Road 1F - Top Item": lambda state: logic.can_strength(state, world, player), + "Victory Road 1F - Left Item": lambda state: logic.can_strength(state, world, player), + # # Rock Tunnel "Rock Tunnel 1F - PokeManiac": lambda state: logic.rock_tunnel(state, world, player), "Rock Tunnel 1F - Hiker 1": lambda state: logic.rock_tunnel(state, world, player), diff --git a/worlds/rogue_legacy/__init__.py b/worlds/rogue_legacy/__init__.py index 290f4a60ac21..7ffdd459db48 100644 --- a/worlds/rogue_legacy/__init__.py +++ b/worlds/rogue_legacy/__init__.py @@ -46,30 +46,6 @@ def fill_slot_data(self) -> dict: return self.options.as_dict(*[name for name in self.options_dataclass.type_hints.keys()]) def generate_early(self): - location_ids_used_per_game = { - world.game: set(world.location_id_to_name) for world in self.multiworld.worlds.values() - } - item_ids_used_per_game = { - world.game: set(world.item_id_to_name) for world in self.multiworld.worlds.values() - } - overlapping_games = set() - - for id_lookup in (location_ids_used_per_game, item_ids_used_per_game): - for game_1, ids_1 in id_lookup.items(): - for game_2, ids_2 in id_lookup.items(): - if game_1 == game_2: - continue - - if ids_1 & ids_2: - overlapping_games.add(tuple(sorted([game_1, game_2]))) - - if overlapping_games: - raise RuntimeError( - "In this multiworld, there are games with overlapping item/location IDs.\n" - "The current Rogue Legacy does not support these and a fix is not currently planned.\n" - f"The overlapping games are: {overlapping_games}" - ) - # Check validation of names. additional_lady_names = len(self.options.additional_lady_names.value) additional_sir_names = len(self.options.additional_sir_names.value) diff --git a/worlds/sc2/Locations.py b/worlds/sc2/Locations.py index bf9c06fa3f78..b9c30bb70106 100644 --- a/worlds/sc2/Locations.py +++ b/worlds/sc2/Locations.py @@ -1387,7 +1387,7 @@ def get_locations(world: Optional[World]) -> Tuple[LocationData, ...]: lambda state: logic.templars_return_requirement(state)), LocationData("The Host", "The Host: Victory", SC2LOTV_LOC_ID_OFFSET + 2100, LocationType.VICTORY, lambda state: logic.the_host_requirement(state)), - LocationData("The Host", "The Host: Southeast Void Shard", SC2LOTV_LOC_ID_OFFSET + 2101, LocationType.VICTORY, + LocationData("The Host", "The Host: Southeast Void Shard", SC2LOTV_LOC_ID_OFFSET + 2101, LocationType.EXTRA, lambda state: logic.the_host_requirement(state)), LocationData("The Host", "The Host: South Void Shard", SC2LOTV_LOC_ID_OFFSET + 2102, LocationType.EXTRA, lambda state: logic.the_host_requirement(state)), @@ -1445,11 +1445,11 @@ def get_locations(world: Optional[World]) -> Tuple[LocationData, ...]: LocationData("The Escape", "The Escape: Agent Stone", SC2NCO_LOC_ID_OFFSET + 105, LocationType.VANILLA, lambda state: logic.the_escape_requirement(state)), LocationData("Sudden Strike", "Sudden Strike: Victory", SC2NCO_LOC_ID_OFFSET + 200, LocationType.VICTORY, - lambda state: logic.sudden_strike_can_reach_objectives(state)), + lambda state: logic.sudden_strike_requirement(state)), LocationData("Sudden Strike", "Sudden Strike: Research Center", SC2NCO_LOC_ID_OFFSET + 201, LocationType.VANILLA, lambda state: logic.sudden_strike_can_reach_objectives(state)), LocationData("Sudden Strike", "Sudden Strike: Weaponry Labs", SC2NCO_LOC_ID_OFFSET + 202, LocationType.VANILLA, - lambda state: logic.sudden_strike_requirement(state)), + lambda state: logic.sudden_strike_can_reach_objectives(state)), LocationData("Sudden Strike", "Sudden Strike: Brutalisk", SC2NCO_LOC_ID_OFFSET + 203, LocationType.EXTRA, lambda state: logic.sudden_strike_requirement(state)), LocationData("Enemy Intelligence", "Enemy Intelligence: Victory", SC2NCO_LOC_ID_OFFSET + 300, LocationType.VICTORY, diff --git a/worlds/sc2/MissionTables.py b/worlds/sc2/MissionTables.py index 4dece46411bf..08e1f133deda 100644 --- a/worlds/sc2/MissionTables.py +++ b/worlds/sc2/MissionTables.py @@ -43,6 +43,9 @@ def __init__(self, campaign_id: int, name: str, goal_priority: SC2CampaignGoalPr self.goal_priority = goal_priority self.race = race + def __lt__(self, other: "SC2Campaign"): + return self.id < other.id + GLOBAL = 0, "Global", SC2CampaignGoalPriority.NONE, SC2Race.ANY WOL = 1, "Wings of Liberty", SC2CampaignGoalPriority.VERY_HARD, SC2Race.TERRAN PROPHECY = 2, "Prophecy", SC2CampaignGoalPriority.MINI_CAMPAIGN, SC2Race.PROTOSS diff --git a/worlds/sc2/Regions.py b/worlds/sc2/Regions.py index 84830a9a32bd..273bc4a5e87c 100644 --- a/worlds/sc2/Regions.py +++ b/worlds/sc2/Regions.py @@ -50,7 +50,7 @@ def create_vanilla_regions( names: Dict[str, int] = {} # Generating all regions and locations for each enabled campaign - for campaign in enabled_campaigns: + for campaign in sorted(enabled_campaigns): for region_name in vanilla_mission_req_table[campaign].keys(): regions.append(create_region(world, locations_per_region, location_cache, region_name)) world.multiworld.regions += regions diff --git a/worlds/sm64ex/Options.py b/worlds/sm64ex/Options.py index 60ec4bbe13c2..8269d3a262cd 100644 --- a/worlds/sm64ex/Options.py +++ b/worlds/sm64ex/Options.py @@ -91,12 +91,11 @@ class BuddyChecks(Toggle): display_name = "Bob-omb Buddy Checks" -class ExclamationBoxes(Choice): +class ExclamationBoxes(Toggle): """Include 1Up Exclamation Boxes during randomization. Adds 29 locations to the pool.""" display_name = "Randomize 1Up !-Blocks" - option_Off = 0 - option_1Ups_Only = 1 + alias_1Ups_Only = 1 class CompletionType(Choice): diff --git a/worlds/sm64ex/__init__.py b/worlds/sm64ex/__init__.py index 833ae56ca302..d4bafbafcc57 100644 --- a/worlds/sm64ex/__init__.py +++ b/worlds/sm64ex/__init__.py @@ -55,7 +55,7 @@ def generate_early(self): for action in self.options.move_rando_actions.value: max_stars -= 1 self.move_rando_bitvec |= (1 << (action_item_table[action] - action_item_table['Double Jump'])) - if (self.options.exclamation_boxes > 0): + if self.options.exclamation_boxes: max_stars += 29 self.number_of_stars = min(self.options.amount_of_stars, max_stars) self.filler_count = max_stars - self.number_of_stars @@ -133,7 +133,7 @@ def generate_basic(self): self.multiworld.get_location("THI: Bob-omb Buddy", self.player).place_locked_item(self.create_item("Cannon Unlock THI")) self.multiworld.get_location("RR: Bob-omb Buddy", self.player).place_locked_item(self.create_item("Cannon Unlock RR")) - if (self.options.exclamation_boxes == 0): + if not self.options.exclamation_boxes: self.multiworld.get_location("CCM: 1Up Block Near Snowman", self.player).place_locked_item(self.create_item("1Up Mushroom")) self.multiworld.get_location("CCM: 1Up Block Ice Pillar", self.player).place_locked_item(self.create_item("1Up Mushroom")) self.multiworld.get_location("CCM: 1Up Block Secret Slide", self.player).place_locked_item(self.create_item("1Up Mushroom")) diff --git a/worlds/sm64ex/docs/setup_en.md b/worlds/sm64ex/docs/setup_en.md index 7456bcb70b62..afb5bad50f71 100644 --- a/worlds/sm64ex/docs/setup_en.md +++ b/worlds/sm64ex/docs/setup_en.md @@ -36,6 +36,8 @@ Then continue to `Using the Launcher` - Windows: If you did not use the default install directory for MSYS, close this window, check `Show advanced options` and reopen using `Re-check Requirements`. You can then set the path manually. 5. When finished, use `Compile default SM64AP build` to continue - Advanced user can use `Show advanced options` to build with custom makeflags (`BETTERCAMERA`, `NODRAWINGDISTANCE`, ...), different repos and branches, and game patches such as 60FPS, Enhanced Moveset and others. + - [Available Makeflags](https://github.com/sm64pc/sm64ex/wiki/Build-options) + - [Included Game Patches](https://github.com/N00byKing/sm64ex/blob/archipelago/enhancements/README.md) 6. Press `Download Files` to prepare the build, afterwards `Create Build`. 7. SM64EX will now be compiled. This can take a while. diff --git a/worlds/stardew_valley/__init__.py b/worlds/stardew_valley/__init__.py index f9df8c292e37..44306011361c 100644 --- a/worlds/stardew_valley/__init__.py +++ b/worlds/stardew_valley/__init__.py @@ -319,7 +319,7 @@ def create_item(self, item: Union[str, ItemData], override_classification: ItemC if override_classification is None: override_classification = item.classification - if override_classification == ItemClassification.progression: + if override_classification & ItemClassification.progression: self.total_progression_items += 1 return StardewItem(item.name, override_classification, item.code, self.player) diff --git a/worlds/stardew_valley/content/unpacking.py b/worlds/stardew_valley/content/unpacking.py index f069866d56cd..3c57f91afe3a 100644 --- a/worlds/stardew_valley/content/unpacking.py +++ b/worlds/stardew_valley/content/unpacking.py @@ -1,16 +1,12 @@ from __future__ import annotations +from graphlib import TopologicalSorter from typing import Iterable, Mapping, Callable from .game_content import StardewContent, ContentPack, StardewFeatures from .vanilla.base import base_game as base_game_content_pack from ..data.game_item import GameItem, ItemSource -try: - from graphlib import TopologicalSorter -except ImportError: - from graphlib_backport import TopologicalSorter # noqa - def unpack_content(features: StardewFeatures, packs: Iterable[ContentPack]) -> StardewContent: # Base game is always registered first. diff --git a/worlds/stardew_valley/data/artisan.py b/worlds/stardew_valley/data/artisan.py index 593ab6a3ddf0..90be5b1684f0 100644 --- a/worlds/stardew_valley/data/artisan.py +++ b/worlds/stardew_valley/data/artisan.py @@ -1,9 +1,9 @@ from dataclasses import dataclass -from .game_item import kw_only, ItemSource +from .game_item import ItemSource -@dataclass(frozen=True, **kw_only) +@dataclass(frozen=True, kw_only=True) class MachineSource(ItemSource): item: str # this should be optional (worm bin) machine: str diff --git a/worlds/stardew_valley/data/craftable_data.py b/worlds/stardew_valley/data/craftable_data.py index d83478a62051..713db4732075 100644 --- a/worlds/stardew_valley/data/craftable_data.py +++ b/worlds/stardew_valley/data/craftable_data.py @@ -1,7 +1,7 @@ from typing import Dict, List, Optional from .recipe_source import RecipeSource, StarterSource, QueenOfSauceSource, ShopSource, SkillSource, FriendshipSource, ShopTradeSource, CutsceneSource, \ - ArchipelagoSource, LogicSource, SpecialOrderSource, FestivalShopSource, QuestSource, MasterySource + ArchipelagoSource, LogicSource, SpecialOrderSource, FestivalShopSource, QuestSource, MasterySource, SkillCraftsanitySource from ..mods.mod_data import ModNames from ..strings.animal_product_names import AnimalProduct from ..strings.artisan_good_names import ArtisanGood @@ -64,6 +64,11 @@ def skill_recipe(name: str, skill: str, level: int, ingredients: Dict[str, int], return create_recipe(name, ingredients, source, mod_name) +def skill_craftsanity_recipe(name: str, skill: str, level: int, ingredients: Dict[str, int], mod_name: Optional[str] = None) -> CraftingRecipe: + source = SkillCraftsanitySource(skill, level) + return create_recipe(name, ingredients, source, mod_name) + + def mastery_recipe(name: str, skill: str, ingredients: Dict[str, int], mod_name: Optional[str] = None) -> CraftingRecipe: source = MasterySource(skill) return create_recipe(name, ingredients, source, mod_name) @@ -249,7 +254,9 @@ def create_recipe(name: str, ingredients: Dict[str, int], source: RecipeSource, charcoal_kiln = skill_recipe(Machine.charcoal_kiln, Skill.foraging, 2, {Material.wood: 20, MetalBar.copper: 2}) crystalarium = skill_recipe(Machine.crystalarium, Skill.mining, 9, {Material.stone: 99, MetalBar.gold: 5, MetalBar.iridium: 2, ArtisanGood.battery_pack: 1}) -furnace = skill_recipe(Machine.furnace, Skill.mining, 1, {Ore.copper: 20, Material.stone: 25}) +# In-Game, the Furnace recipe is completely unique. It is the only recipe that is obtained in a cutscene after doing a skill-related action. +# So it has a custom source that needs both the craftsanity item from AP and the skill, if craftsanity is enabled. +furnace = skill_craftsanity_recipe(Machine.furnace, Skill.mining, 1, {Ore.copper: 20, Material.stone: 25}) geode_crusher = special_order_recipe(Machine.geode_crusher, SpecialOrder.cave_patrol, {MetalBar.gold: 2, Material.stone: 50, Mineral.diamond: 1}) mushroom_log = skill_recipe(Machine.mushroom_log, Skill.foraging, 4, {Material.hardwood: 10, Material.moss: 10}) heavy_tapper = ap_recipe(Machine.heavy_tapper, {Material.hardwood: 30, MetalBar.radioactive: 1}) diff --git a/worlds/stardew_valley/data/game_item.py b/worlds/stardew_valley/data/game_item.py index 6c8d30ed8e6f..c6e4717cd1e0 100644 --- a/worlds/stardew_valley/data/game_item.py +++ b/worlds/stardew_valley/data/game_item.py @@ -1,5 +1,4 @@ import enum -import sys from abc import ABC from dataclasses import dataclass, field from types import MappingProxyType @@ -7,11 +6,6 @@ from ..stardew_rule.protocol import StardewRule -if sys.version_info >= (3, 10): - kw_only = {"kw_only": True} -else: - kw_only = {} - DEFAULT_REQUIREMENT_TAGS = MappingProxyType({}) @@ -36,21 +30,17 @@ class ItemTag(enum.Enum): class ItemSource(ABC): add_tags: ClassVar[Tuple[ItemTag]] = () + other_requirements: Tuple[Requirement, ...] = field(kw_only=True, default_factory=tuple) + @property def requirement_tags(self) -> Mapping[str, Tuple[ItemTag, ...]]: return DEFAULT_REQUIREMENT_TAGS - # FIXME this should just be an optional field, but kw_only requires python 3.10... - @property - def other_requirements(self) -> Iterable[Requirement]: - return () - -@dataclass(frozen=True, **kw_only) +@dataclass(frozen=True, kw_only=True) class GenericSource(ItemSource): regions: Tuple[str, ...] = () """No region means it's available everywhere.""" - other_requirements: Tuple[Requirement, ...] = () @dataclass(frozen=True) @@ -59,7 +49,7 @@ class CustomRuleSource(ItemSource): create_rule: Callable[[Any], StardewRule] -@dataclass(frozen=True, **kw_only) +@dataclass(frozen=True, kw_only=True) class CompoundSource(ItemSource): sources: Tuple[ItemSource, ...] = () diff --git a/worlds/stardew_valley/data/harvest.py b/worlds/stardew_valley/data/harvest.py index 087d7c3fa86b..0fdae9549587 100644 --- a/worlds/stardew_valley/data/harvest.py +++ b/worlds/stardew_valley/data/harvest.py @@ -1,18 +1,17 @@ from dataclasses import dataclass from typing import Tuple, Sequence, Mapping -from .game_item import ItemSource, kw_only, ItemTag, Requirement +from .game_item import ItemSource, ItemTag from ..strings.season_names import Season -@dataclass(frozen=True, **kw_only) +@dataclass(frozen=True, kw_only=True) class ForagingSource(ItemSource): regions: Tuple[str, ...] seasons: Tuple[str, ...] = Season.all - other_requirements: Tuple[Requirement, ...] = () -@dataclass(frozen=True, **kw_only) +@dataclass(frozen=True, kw_only=True) class SeasonalForagingSource(ItemSource): season: str days: Sequence[int] @@ -22,17 +21,17 @@ def as_foraging_source(self) -> ForagingSource: return ForagingSource(seasons=(self.season,), regions=self.regions) -@dataclass(frozen=True, **kw_only) +@dataclass(frozen=True, kw_only=True) class FruitBatsSource(ItemSource): ... -@dataclass(frozen=True, **kw_only) +@dataclass(frozen=True, kw_only=True) class MushroomCaveSource(ItemSource): ... -@dataclass(frozen=True, **kw_only) +@dataclass(frozen=True, kw_only=True) class HarvestFruitTreeSource(ItemSource): add_tags = (ItemTag.CROPSANITY,) @@ -46,7 +45,7 @@ def requirement_tags(self) -> Mapping[str, Tuple[ItemTag, ...]]: } -@dataclass(frozen=True, **kw_only) +@dataclass(frozen=True, kw_only=True) class HarvestCropSource(ItemSource): add_tags = (ItemTag.CROPSANITY,) @@ -61,6 +60,6 @@ def requirement_tags(self) -> Mapping[str, Tuple[ItemTag, ...]]: } -@dataclass(frozen=True, **kw_only) +@dataclass(frozen=True, kw_only=True) class ArtifactSpotSource(ItemSource): amount: int diff --git a/worlds/stardew_valley/data/items.csv b/worlds/stardew_valley/data/items.csv index ffcae223e251..05af275ba472 100644 --- a/worlds/stardew_valley/data/items.csv +++ b/worlds/stardew_valley/data/items.csv @@ -7,7 +7,7 @@ id,name,classification,groups,mod_name 19,Glittering Boulder Removed,progression,COMMUNITY_REWARD, 20,Minecarts Repair,useful,COMMUNITY_REWARD, 21,Bus Repair,progression,COMMUNITY_REWARD, -22,Progressive Movie Theater,progression,COMMUNITY_REWARD, +22,Progressive Movie Theater,"progression,trap",COMMUNITY_REWARD, 23,Stardrop,progression,, 24,Progressive Backpack,progression,, 25,Rusty Sword,filler,"WEAPON,DEPRECATED", diff --git a/worlds/stardew_valley/data/recipe_source.py b/worlds/stardew_valley/data/recipe_source.py index 24b03bf77bd4..ead4d62f1650 100644 --- a/worlds/stardew_valley/data/recipe_source.py +++ b/worlds/stardew_valley/data/recipe_source.py @@ -94,6 +94,11 @@ def __repr__(self): return f"SkillSource at level {self.level} {self.skill}" +class SkillCraftsanitySource(SkillSource): + def __repr__(self): + return f"SkillCraftsanitySource at level {self.level} {self.skill}" + + class MasterySource(RecipeSource): skill: str diff --git a/worlds/stardew_valley/data/shop.py b/worlds/stardew_valley/data/shop.py index f14dbac82131..cc9506023f19 100644 --- a/worlds/stardew_valley/data/shop.py +++ b/worlds/stardew_valley/data/shop.py @@ -1,40 +1,39 @@ from dataclasses import dataclass from typing import Tuple, Optional -from .game_item import ItemSource, kw_only, Requirement +from .game_item import ItemSource from ..strings.season_names import Season ItemPrice = Tuple[int, str] -@dataclass(frozen=True, **kw_only) +@dataclass(frozen=True, kw_only=True) class ShopSource(ItemSource): shop_region: str money_price: Optional[int] = None items_price: Optional[Tuple[ItemPrice, ...]] = None seasons: Tuple[str, ...] = Season.all - other_requirements: Tuple[Requirement, ...] = () def __post_init__(self): assert self.money_price is not None or self.items_price is not None, "At least money price or items price need to be defined." assert self.items_price is None or all(isinstance(p, tuple) for p in self.items_price), "Items price should be a tuple." -@dataclass(frozen=True, **kw_only) +@dataclass(frozen=True, kw_only=True) class MysteryBoxSource(ItemSource): amount: int -@dataclass(frozen=True, **kw_only) +@dataclass(frozen=True, kw_only=True) class ArtifactTroveSource(ItemSource): amount: int -@dataclass(frozen=True, **kw_only) +@dataclass(frozen=True, kw_only=True) class PrizeMachineSource(ItemSource): amount: int -@dataclass(frozen=True, **kw_only) +@dataclass(frozen=True, kw_only=True) class FishingTreasureChestSource(ItemSource): amount: int diff --git a/worlds/stardew_valley/data/skill.py b/worlds/stardew_valley/data/skill.py index d0674f34c0e1..4c754ddd8716 100644 --- a/worlds/stardew_valley/data/skill.py +++ b/worlds/stardew_valley/data/skill.py @@ -1,9 +1,7 @@ from dataclasses import dataclass, field -from ..data.game_item import kw_only - @dataclass(frozen=True) class Skill: name: str - has_mastery: bool = field(**kw_only) + has_mastery: bool = field(kw_only=True) diff --git a/worlds/stardew_valley/docs/en_Stardew Valley.md b/worlds/stardew_valley/docs/en_Stardew Valley.md index 0ed693031b82..62755dad798d 100644 --- a/worlds/stardew_valley/docs/en_Stardew Valley.md +++ b/worlds/stardew_valley/docs/en_Stardew Valley.md @@ -138,7 +138,7 @@ This means that, for these specific mods, if you decide to include them in your with the assumption that you will install and play with these mods. The multiworld will contain related items and locations for these mods, the specifics will vary from mod to mod -[Supported Mods Documentation](https://github.com/agilbert1412/StardewArchipelago/blob/5.x.x/Documentation/Supported%20Mods.md) +[Supported Mods Documentation](https://github.com/agilbert1412/StardewArchipelago/blob/6.x.x/Documentation/Supported%20Mods.md) List of supported mods: diff --git a/worlds/stardew_valley/docs/setup_en.md b/worlds/stardew_valley/docs/setup_en.md index c672152543cf..801bf345e916 100644 --- a/worlds/stardew_valley/docs/setup_en.md +++ b/worlds/stardew_valley/docs/setup_en.md @@ -12,7 +12,7 @@ - Archipelago from the [Archipelago Releases Page](https://github.com/ArchipelagoMW/Archipelago/releases) * (Only for the TextClient) - Other Stardew Valley Mods [Nexus Mods](https://www.nexusmods.com/stardewvalley) - * There are [supported mods](https://github.com/agilbert1412/StardewArchipelago/blob/5.x.x/Documentation/Supported%20Mods.md) + * There are [supported mods](https://github.com/agilbert1412/StardewArchipelago/blob/6.x.x/Documentation/Supported%20Mods.md) that you can add to your yaml to include them with the Archipelago randomization * It is **not** recommended to further mod Stardew Valley with unsupported mods, although it is possible to do so. diff --git a/worlds/stardew_valley/items.py b/worlds/stardew_valley/items.py index 31c7da5e3ade..993863bf5bf5 100644 --- a/worlds/stardew_valley/items.py +++ b/worlds/stardew_valley/items.py @@ -2,6 +2,7 @@ import enum import logging from dataclasses import dataclass, field +from functools import reduce from pathlib import Path from random import Random from typing import Dict, List, Protocol, Union, Set, Optional @@ -124,17 +125,14 @@ def __call__(self, item: Item): def load_item_csv(): - try: - from importlib.resources import files - except ImportError: - from importlib_resources import files # noqa + from importlib.resources import files items = [] with files(data).joinpath("items.csv").open() as file: item_reader = csv.DictReader(file) for item in item_reader: id = int(item["id"]) if item["id"] else None - classification = ItemClassification[item["classification"]] + classification = reduce((lambda a, b: a | b), {ItemClassification[str_classification] for str_classification in item["classification"].split(",")}) groups = {Group[group] for group in item["groups"].split(",") if group} mod_name = str(item["mod_name"]) if item["mod_name"] else None items.append(ItemData(id, item["name"], classification, mod_name, groups)) diff --git a/worlds/stardew_valley/locations.py b/worlds/stardew_valley/locations.py index 43246a94a356..1d67d535ccee 100644 --- a/worlds/stardew_valley/locations.py +++ b/worlds/stardew_valley/locations.py @@ -130,10 +130,7 @@ def __call__(self, name: str, code: Optional[int], region: str) -> None: def load_location_csv() -> List[LocationData]: - try: - from importlib.resources import files - except ImportError: - from importlib_resources import files + from importlib.resources import files with files(data).joinpath("locations.csv").open() as file: reader = csv.DictReader(file) diff --git a/worlds/stardew_valley/logic/crafting_logic.py b/worlds/stardew_valley/logic/crafting_logic.py index e346e4ba238b..0403230eee34 100644 --- a/worlds/stardew_valley/logic/crafting_logic.py +++ b/worlds/stardew_valley/logic/crafting_logic.py @@ -14,7 +14,7 @@ from .. import options from ..data.craftable_data import CraftingRecipe, all_crafting_recipes_by_name from ..data.recipe_source import CutsceneSource, ShopTradeSource, ArchipelagoSource, LogicSource, SpecialOrderSource, \ - FestivalShopSource, QuestSource, StarterSource, ShopSource, SkillSource, MasterySource, FriendshipSource + FestivalShopSource, QuestSource, StarterSource, ShopSource, SkillSource, MasterySource, FriendshipSource, SkillCraftsanitySource from ..locations import locations_by_tag, LocationTags from ..options import Craftsanity, SpecialOrderLocations, ExcludeGingerIsland, SkillProgression from ..stardew_rule import StardewRule, True_, False_ @@ -54,8 +54,7 @@ def knows_recipe(self, recipe: CraftingRecipe) -> StardewRule: return self.logic.crafting.received_recipe(recipe.item) if self.options.craftsanity == Craftsanity.option_none: return self.logic.crafting.can_learn_recipe(recipe) - if isinstance(recipe.source, StarterSource) or isinstance(recipe.source, ShopTradeSource) or isinstance( - recipe.source, ShopSource): + if isinstance(recipe.source, (StarterSource, ShopTradeSource, ShopSource, SkillCraftsanitySource)): return self.logic.crafting.received_recipe(recipe.item) if isinstance(recipe.source, SpecialOrderSource) and self.options.special_order_locations & SpecialOrderLocations.option_board: return self.logic.crafting.received_recipe(recipe.item) @@ -71,6 +70,8 @@ def can_learn_recipe(self, recipe: CraftingRecipe) -> StardewRule: return self.logic.money.can_trade_at(recipe.source.region, recipe.source.currency, recipe.source.price) if isinstance(recipe.source, ShopSource): return self.logic.money.can_spend_at(recipe.source.region, recipe.source.price) + if isinstance(recipe.source, SkillCraftsanitySource): + return self.logic.skill.has_level(recipe.source.skill, recipe.source.level) & self.logic.skill.can_earn_level(recipe.source.skill, recipe.source.level) if isinstance(recipe.source, SkillSource): return self.logic.skill.has_level(recipe.source.skill, recipe.source.level) if isinstance(recipe.source, MasterySource): diff --git a/worlds/stardew_valley/presets.py b/worlds/stardew_valley/presets.py index 1861a914235c..62672f29e424 100644 --- a/worlds/stardew_valley/presets.py +++ b/worlds/stardew_valley/presets.py @@ -41,9 +41,7 @@ Friendsanity.internal_name: "random", FriendsanityHeartSize.internal_name: "random", Booksanity.internal_name: "random", - Walnutsanity.internal_name: "random", NumberOfMovementBuffs.internal_name: "random", - EnabledFillerBuffs.internal_name: "random", ExcludeGingerIsland.internal_name: "random", TrapItems.internal_name: "random", MultipleDaySleepEnabled.internal_name: "random", diff --git a/worlds/stardew_valley/requirements.txt b/worlds/stardew_valley/requirements.txt deleted file mode 100644 index 65e922a64483..000000000000 --- a/worlds/stardew_valley/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -importlib_resources; python_version <= '3.8' -graphlib_backport; python_version <= '3.8' diff --git a/worlds/stardew_valley/test/TestGeneration.py b/worlds/stardew_valley/test/TestGeneration.py index 8431e6857eaf..56f338fe8e11 100644 --- a/worlds/stardew_valley/test/TestGeneration.py +++ b/worlds/stardew_valley/test/TestGeneration.py @@ -35,7 +35,7 @@ def test_all_progression_items_are_added_to_the_pool(self): items_to_ignore.extend(baby.name for baby in items.items_by_group[Group.BABY]) items_to_ignore.extend(resource_pack.name for resource_pack in items.items_by_group[Group.RESOURCE_PACK]) items_to_ignore.append("The Gateway Gazette") - progression_items = [item for item in items.all_items if item.classification is ItemClassification.progression and item.name not in items_to_ignore] + progression_items = [item for item in items.all_items if item.classification & ItemClassification.progression and item.name not in items_to_ignore] for progression_item in progression_items: with self.subTest(f"{progression_item.name}"): self.assertIn(progression_item.name, all_created_items) @@ -86,7 +86,7 @@ def test_all_progression_items_except_island_are_added_to_the_pool(self): items_to_ignore.extend(season.name for season in items.items_by_group[Group.WEAPON]) items_to_ignore.extend(baby.name for baby in items.items_by_group[Group.BABY]) items_to_ignore.append("The Gateway Gazette") - progression_items = [item for item in items.all_items if item.classification is ItemClassification.progression and item.name not in items_to_ignore] + progression_items = [item for item in items.all_items if item.classification & ItemClassification.progression and item.name not in items_to_ignore] for progression_item in progression_items: with self.subTest(f"{progression_item.name}"): if Group.GINGER_ISLAND in progression_item.groups: diff --git a/worlds/stardew_valley/test/__init__.py b/worlds/stardew_valley/test/__init__.py index 3fe05d205ce0..8f4e5af28f84 100644 --- a/worlds/stardew_valley/test/__init__.py +++ b/worlds/stardew_valley/test/__init__.py @@ -306,7 +306,7 @@ def collect(self, item: Union[str, Item, Iterable[Item]], count: int = 1) -> Uni def create_item(self, item: str) -> StardewItem: created_item = self.world.create_item(item) - if created_item.classification == ItemClassification.progression: + if created_item.classification & ItemClassification.progression: self.multiworld.worlds[self.player].total_progression_items -= 1 return created_item diff --git a/worlds/stardew_valley/test/assertion/rule_assert.py b/worlds/stardew_valley/test/assertion/rule_assert.py index 5a1dad2925cf..1031a18e115c 100644 --- a/worlds/stardew_valley/test/assertion/rule_assert.py +++ b/worlds/stardew_valley/test/assertion/rule_assert.py @@ -1,3 +1,4 @@ +from typing import List from unittest import TestCase from BaseClasses import CollectionState, Location @@ -14,6 +15,10 @@ def assert_rule_true(self, rule: StardewRule, state: CollectionState): raise AssertionError(f"Error while checking rule {rule}: {e}" f"\nExplanation: {expl}") + def assert_rules_true(self, rules: List[StardewRule], state: CollectionState): + for rule in rules: + self.assert_rule_true(rule, state) + def assert_rule_false(self, rule: StardewRule, state: CollectionState): expl = explain(rule, state, expected=False) try: @@ -22,6 +27,10 @@ def assert_rule_false(self, rule: StardewRule, state: CollectionState): raise AssertionError(f"Error while checking rule {rule}: {e}" f"\nExplanation: {expl}") + def assert_rules_false(self, rules: List[StardewRule], state: CollectionState): + for rule in rules: + self.assert_rule_false(rule, state) + def assert_rule_can_be_resolved(self, rule: StardewRule, complete_state: CollectionState): expl = explain(rule, complete_state) try: diff --git a/worlds/stardew_valley/test/mods/TestMods.py b/worlds/stardew_valley/test/mods/TestMods.py index 97184b1338b8..07a75f21b1de 100644 --- a/worlds/stardew_valley/test/mods/TestMods.py +++ b/worlds/stardew_valley/test/mods/TestMods.py @@ -75,7 +75,7 @@ def test_all_progression_items_are_added_to_the_pool(self): items_to_ignore.extend(baby.name for baby in items.items_by_group[Group.BABY]) items_to_ignore.extend(resource_pack.name for resource_pack in items.items_by_group[Group.RESOURCE_PACK]) items_to_ignore.append("The Gateway Gazette") - progression_items = [item for item in items.all_items if item.classification is ItemClassification.progression + progression_items = [item for item in items.all_items if item.classification & ItemClassification.progression and item.name not in items_to_ignore] for progression_item in progression_items: with self.subTest(f"{progression_item.name}"): @@ -105,7 +105,7 @@ def test_all_progression_items_except_island_are_added_to_the_pool(self): items_to_ignore.extend(baby.name for baby in items.items_by_group[Group.BABY]) items_to_ignore.extend(resource_pack.name for resource_pack in items.items_by_group[Group.RESOURCE_PACK]) items_to_ignore.append("The Gateway Gazette") - progression_items = [item for item in items.all_items if item.classification is ItemClassification.progression + progression_items = [item for item in items.all_items if item.classification & ItemClassification.progression and item.name not in items_to_ignore] for progression_item in progression_items: with self.subTest(f"{progression_item.name}"): diff --git a/worlds/stardew_valley/test/rules/TestBundles.py b/worlds/stardew_valley/test/rules/TestBundles.py index ab376c90d4ea..0bc7f9bfdfd4 100644 --- a/worlds/stardew_valley/test/rules/TestBundles.py +++ b/worlds/stardew_valley/test/rules/TestBundles.py @@ -56,6 +56,7 @@ def test_raccoon_bundles_rely_on_previous_ones(self): self.collect("Mushroom Boxes") self.collect("Progressive Fishing Rod", 4) self.collect("Fishing Level", 10) + self.collect("Furnace Recipe") self.assertFalse(raccoon_rule_1(self.multiworld.state)) self.assertFalse(raccoon_rule_3(self.multiworld.state)) diff --git a/worlds/stardew_valley/test/rules/TestCraftingRecipes.py b/worlds/stardew_valley/test/rules/TestCraftingRecipes.py index 93c325ae5c5c..4719edea1d59 100644 --- a/worlds/stardew_valley/test/rules/TestCraftingRecipes.py +++ b/worlds/stardew_valley/test/rules/TestCraftingRecipes.py @@ -50,6 +50,23 @@ def test_can_craft_festival_recipe(self): self.multiworld.state.collect(self.create_item("Jack-O-Lantern Recipe"), prevent_sweep=False) self.assert_rule_true(rule, self.multiworld.state) + def test_require_furnace_recipe_for_smelting_checks(self): + locations = ["Craft Furnace", "Smelting", "Copper Pickaxe Upgrade", "Gold Trash Can Upgrade"] + rules = [self.world.logic.region.can_reach_location(location) for location in locations] + self.collect([self.create_item("Progressive Pickaxe")] * 4) + self.collect([self.create_item("Progressive Fishing Rod")] * 4) + self.collect([self.create_item("Progressive Sword")] * 4) + self.collect([self.create_item("Progressive Mine Elevator")] * 24) + self.collect([self.create_item("Progressive Trash Can")] * 2) + self.collect([self.create_item("Mining Level")] * 10) + self.collect([self.create_item("Combat Level")] * 10) + self.collect([self.create_item("Fishing Level")] * 10) + self.collect_all_the_money() + self.assert_rules_false(rules, self.multiworld.state) + + self.multiworld.state.collect(self.create_item("Furnace Recipe"), prevent_sweep=False) + self.assert_rules_true(rules, self.multiworld.state) + class TestCraftsanityWithFestivalsLogic(SVTestBase): options = { @@ -101,6 +118,23 @@ def test_can_craft_festival_recipe(self): self.collect([self.create_item("Progressive Season")] * 2) self.assert_rule_true(rule, self.multiworld.state) + def test_requires_mining_levels_for_smelting_checks(self): + locations = ["Smelting", "Copper Pickaxe Upgrade", "Gold Trash Can Upgrade"] + rules = [self.world.logic.region.can_reach_location(location) for location in locations] + self.collect([self.create_item("Progressive Pickaxe")] * 4) + self.collect([self.create_item("Progressive Fishing Rod")] * 4) + self.collect([self.create_item("Progressive Sword")] * 4) + self.collect([self.create_item("Progressive Mine Elevator")] * 24) + self.collect([self.create_item("Progressive Trash Can")] * 2) + self.multiworld.state.collect(self.create_item("Furnace Recipe"), prevent_sweep=False) + self.collect([self.create_item("Combat Level")] * 10) + self.collect([self.create_item("Fishing Level")] * 10) + self.collect_all_the_money() + self.assert_rules_false(rules, self.multiworld.state) + + self.collect([self.create_item("Mining Level")] * 10) + self.assert_rules_true(rules, self.multiworld.state) + class TestNoCraftsanityWithFestivalsLogic(SVTestBase): options = { diff --git a/worlds/stardew_valley/test/rules/TestStateRules.py b/worlds/stardew_valley/test/rules/TestStateRules.py index 4f53b9a7f536..7d10f4ceb1d3 100644 --- a/worlds/stardew_valley/test/rules/TestStateRules.py +++ b/worlds/stardew_valley/test/rules/TestStateRules.py @@ -8,5 +8,5 @@ class TestHasProgressionPercent(unittest.TestCase): def test_max_item_amount_is_full_collection(self): # Not caching because it fails too often for some reason with solo_multiworld(world_caching=False) as (multiworld, world): - progression_item_count = sum(1 for i in multiworld.get_items() if ItemClassification.progression in i.classification) + progression_item_count = sum(1 for i in multiworld.get_items() if i.classification & ItemClassification.progression) self.assertEqual(world.total_progression_items, progression_item_count - 1) # -1 to skip Victory diff --git a/worlds/stardew_valley/test/stability/TestStability.py b/worlds/stardew_valley/test/stability/TestStability.py index 8bb904a56ea2..137a7172aff4 100644 --- a/worlds/stardew_valley/test/stability/TestStability.py +++ b/worlds/stardew_valley/test/stability/TestStability.py @@ -12,8 +12,6 @@ # at 0x102ca98a0> lambda_regex = re.compile(r"^ at (.*)>$") -# Python 3.10.2\r\n -python_version_regex = re.compile(r"^Python (\d+)\.(\d+)\.(\d+)\s*$") class TestGenerationIsStable(SVTestCase): diff --git a/worlds/subnautica/options.py b/worlds/subnautica/options.py index 4bdd9aafa53f..6cdcb33d8954 100644 --- a/worlds/subnautica/options.py +++ b/worlds/subnautica/options.py @@ -112,8 +112,7 @@ def get_pool(self) -> typing.List[str]: class SubnauticaDeathLink(DeathLink): - """When you die, everyone dies. Of course the reverse is true too. - Note: can be toggled via in-game console command "deathlink".""" + __doc__ = DeathLink.__doc__ + "\n\n Note: can be toggled via in-game console command \"deathlink\"." class FillerItemsDistribution(ItemDict): diff --git a/worlds/timespinner/Options.py b/worlds/timespinner/Options.py index 20ad8132c45f..c06dd36797fd 100644 --- a/worlds/timespinner/Options.py +++ b/worlds/timespinner/Options.py @@ -379,6 +379,7 @@ class TimespinnerOptions(PerGameCommonOptions, DeathLinkMixin): cantoran: Cantoran lore_checks: LoreChecks boss_rando: BossRando + enemy_rando: EnemyRando damage_rando: DamageRando damage_rando_overrides: DamageRandoOverrides hp_cap: HpCap @@ -417,13 +418,16 @@ class HiddenTraps(Traps): """List of traps that may be in the item pool to find""" visibility = Visibility.none -class OptionsHider: - @classmethod - def hidden(cls, option: Type[Option[Any]]) -> Type[Option]: - new_option = AssembleOptions(f"{option}Hidden", option.__bases__, vars(option).copy()) - new_option.visibility = Visibility.none - new_option.__doc__ = option.__doc__ - return new_option +class HiddenDeathLink(DeathLink): + """When you die, everyone who enabled death link dies. Of course, the reverse is true too.""" + visibility = Visibility.none + +def hidden(option: Type[Option[Any]]) -> Type[Option]: + new_option = AssembleOptions(f"{option.__name__}Hidden", option.__bases__, vars(option).copy()) + new_option.visibility = Visibility.none + new_option.__doc__ = option.__doc__ + globals()[f"{option.__name__}Hidden"] = new_option + return new_option class HasReplacedCamelCase(Toggle): """For internal use will display a warning message if true""" @@ -431,41 +435,42 @@ class HasReplacedCamelCase(Toggle): @dataclass class BackwardsCompatiableTimespinnerOptions(TimespinnerOptions): - StartWithJewelryBox: OptionsHider.hidden(StartWithJewelryBox) # type: ignore - DownloadableItems: OptionsHider.hidden(DownloadableItems) # type: ignore - EyeSpy: OptionsHider.hidden(EyeSpy) # type: ignore - StartWithMeyef: OptionsHider.hidden(StartWithMeyef) # type: ignore - QuickSeed: OptionsHider.hidden(QuickSeed) # type: ignore - SpecificKeycards: OptionsHider.hidden(SpecificKeycards) # type: ignore - Inverted: OptionsHider.hidden(Inverted) # type: ignore - GyreArchives: OptionsHider.hidden(GyreArchives) # type: ignore - Cantoran: OptionsHider.hidden(Cantoran) # type: ignore - LoreChecks: OptionsHider.hidden(LoreChecks) # type: ignore - BossRando: OptionsHider.hidden(BossRando) # type: ignore - DamageRando: OptionsHider.hidden(DamageRando) # type: ignore + StartWithJewelryBox: hidden(StartWithJewelryBox) # type: ignore + DownloadableItems: hidden(DownloadableItems) # type: ignore + EyeSpy: hidden(EyeSpy) # type: ignore + StartWithMeyef: hidden(StartWithMeyef) # type: ignore + QuickSeed: hidden(QuickSeed) # type: ignore + SpecificKeycards: hidden(SpecificKeycards) # type: ignore + Inverted: hidden(Inverted) # type: ignore + GyreArchives: hidden(GyreArchives) # type: ignore + Cantoran: hidden(Cantoran) # type: ignore + LoreChecks: hidden(LoreChecks) # type: ignore + BossRando: hidden(BossRando) # type: ignore + EnemyRando: hidden(EnemyRando) # type: ignore + DamageRando: hidden(DamageRando) # type: ignore DamageRandoOverrides: HiddenDamageRandoOverrides - HpCap: OptionsHider.hidden(HpCap) # type: ignore - LevelCap: OptionsHider.hidden(LevelCap) # type: ignore - ExtraEarringsXP: OptionsHider.hidden(ExtraEarringsXP) # type: ignore - BossHealing: OptionsHider.hidden(BossHealing) # type: ignore - ShopFill: OptionsHider.hidden(ShopFill) # type: ignore - ShopWarpShards: OptionsHider.hidden(ShopWarpShards) # type: ignore - ShopMultiplier: OptionsHider.hidden(ShopMultiplier) # type: ignore - LootPool: OptionsHider.hidden(LootPool) # type: ignore - DropRateCategory: OptionsHider.hidden(DropRateCategory) # type: ignore - FixedDropRate: OptionsHider.hidden(FixedDropRate) # type: ignore - LootTierDistro: OptionsHider.hidden(LootTierDistro) # type: ignore - ShowBestiary: OptionsHider.hidden(ShowBestiary) # type: ignore - ShowDrops: OptionsHider.hidden(ShowDrops) # type: ignore - EnterSandman: OptionsHider.hidden(EnterSandman) # type: ignore - DadPercent: OptionsHider.hidden(DadPercent) # type: ignore - RisingTides: OptionsHider.hidden(RisingTides) # type: ignore + HpCap: hidden(HpCap) # type: ignore + LevelCap: hidden(LevelCap) # type: ignore + ExtraEarringsXP: hidden(ExtraEarringsXP) # type: ignore + BossHealing: hidden(BossHealing) # type: ignore + ShopFill: hidden(ShopFill) # type: ignore + ShopWarpShards: hidden(ShopWarpShards) # type: ignore + ShopMultiplier: hidden(ShopMultiplier) # type: ignore + LootPool: hidden(LootPool) # type: ignore + DropRateCategory: hidden(DropRateCategory) # type: ignore + FixedDropRate: hidden(FixedDropRate) # type: ignore + LootTierDistro: hidden(LootTierDistro) # type: ignore + ShowBestiary: hidden(ShowBestiary) # type: ignore + ShowDrops: hidden(ShowDrops) # type: ignore + EnterSandman: hidden(EnterSandman) # type: ignore + DadPercent: hidden(DadPercent) # type: ignore + RisingTides: hidden(RisingTides) # type: ignore RisingTidesOverrides: HiddenRisingTidesOverrides - UnchainedKeys: OptionsHider.hidden(UnchainedKeys) # type: ignore - PresentAccessWithWheelAndSpindle: OptionsHider.hidden(PresentAccessWithWheelAndSpindle) # type: ignore - TrapChance: OptionsHider.hidden(TrapChance) # type: ignore + UnchainedKeys: hidden(UnchainedKeys) # type: ignore + PresentAccessWithWheelAndSpindle: hidden(PresentAccessWithWheelAndSpindle) # type: ignore + TrapChance: hidden(TrapChance) # type: ignore Traps: HiddenTraps # type: ignore - DeathLink: OptionsHider.hidden(DeathLink) # type: ignore + DeathLink: HiddenDeathLink # type: ignore has_replaced_options: HasReplacedCamelCase def handle_backward_compatibility(self) -> None: @@ -513,6 +518,10 @@ def handle_backward_compatibility(self) -> None: self.boss_rando == BossRando.default: self.boss_rando.value = self.BossRando.value self.has_replaced_options.value = Toggle.option_true + if self.EnemyRando != EnemyRando.default and \ + self.enemy_rando == EnemyRando.default: + self.enemy_rando.value = self.EnemyRando.value + self.has_replaced_options.value = Toggle.option_true if self.DamageRando != DamageRando.default and \ self.damage_rando == DamageRando.default: self.damage_rando.value = self.DamageRando.value diff --git a/worlds/timespinner/__init__.py b/worlds/timespinner/__init__.py index 66744cffdf85..72903bd5ffea 100644 --- a/worlds/timespinner/__init__.py +++ b/worlds/timespinner/__init__.py @@ -98,6 +98,7 @@ def fill_slot_data(self) -> Dict[str, object]: "Cantoran": self.options.cantoran.value, "LoreChecks": self.options.lore_checks.value, "BossRando": self.options.boss_rando.value, + "EnemyRando": self.options.enemy_rando.value, "DamageRando": self.options.damage_rando.value, "DamageRandoOverrides": self.options.damage_rando_overrides.value, "HpCap": self.options.hp_cap.value, @@ -190,7 +191,7 @@ def write_spoiler_header(self, spoiler_handle: TextIO) -> None: if self.options.has_replaced_options: warning = \ - f"NOTICE: Timespinner options for player '{self.player_name}' where renamed from PasCalCase to snake_case, " \ + f"NOTICE: Timespinner options for player '{self.player_name}' were renamed from PascalCase to snake_case, " \ "please update your yaml" spoiler_handle.write("\n") diff --git a/worlds/tloz/Locations.py b/worlds/tloz/Locations.py index 9715cc684291..f95e5d80443e 100644 --- a/worlds/tloz/Locations.py +++ b/worlds/tloz/Locations.py @@ -108,11 +108,15 @@ ] food_locations = [ - "Level 7 Map", "Level 7 Boss", "Level 7 Triforce", "Level 7 Key Drop (Goriyas)", + "Level 7 Item (Red Candle)", "Level 7 Map", "Level 7 Boss", "Level 7 Triforce", "Level 7 Key Drop (Goriyas)", "Level 7 Bomb Drop (Moldorms North)", "Level 7 Bomb Drop (Goriyas North)", "Level 7 Bomb Drop (Dodongos)", "Level 7 Rupee Drop (Goriyas North)" ] +gohma_locations = [ + "Level 6 Boss", "Level 6 Triforce", "Level 8 Item (Magical Key)", "Level 8 Bomb Drop (Darknuts North)" +] + gleeok_locations = [ "Level 4 Boss", "Level 4 Triforce", "Level 8 Boss", "Level 8 Triforce" ] diff --git a/worlds/tloz/Rules.py b/worlds/tloz/Rules.py index 39c3b954f0d4..de627a533bd3 100644 --- a/worlds/tloz/Rules.py +++ b/worlds/tloz/Rules.py @@ -1,7 +1,7 @@ from typing import TYPE_CHECKING from worlds.generic.Rules import add_rule -from .Locations import food_locations, shop_locations, gleeok_locations +from .Locations import food_locations, shop_locations, gleeok_locations, gohma_locations from .ItemPool import dangerous_weapon_locations from .Options import StartingPosition @@ -10,13 +10,12 @@ def set_rules(tloz_world: "TLoZWorld"): player = tloz_world.player - world = tloz_world.multiworld options = tloz_world.options # Boss events for a nicer spoiler log play through for level in range(1, 9): - boss = world.get_location(f"Level {level} Boss", player) - boss_event = world.get_location(f"Level {level} Boss Status", player) + boss = tloz_world.get_location(f"Level {level} Boss") + boss_event = tloz_world.get_location(f"Level {level} Boss Status") status = tloz_world.create_event(f"Boss {level} Defeated") boss_event.place_locked_item(status) add_rule(boss_event, lambda state, b=boss: state.can_reach(b, "Location", player)) @@ -26,136 +25,131 @@ def set_rules(tloz_world: "TLoZWorld"): for location in level.locations: if options.StartingPosition < StartingPosition.option_dangerous \ or location.name not in dangerous_weapon_locations: - add_rule(world.get_location(location.name, player), + add_rule(tloz_world.get_location(location.name), lambda state: state.has_group("weapons", player)) # This part of the loop sets up an expected amount of defense needed for each dungeon if i > 0: # Don't need an extra heart for Level 1 - add_rule(world.get_location(location.name, player), + add_rule(tloz_world.get_location(location.name), lambda state, hearts=i: state.has("Heart Container", player, hearts) or (state.has("Blue Ring", player) and state.has("Heart Container", player, int(hearts / 2))) or (state.has("Red Ring", player) and state.has("Heart Container", player, int(hearts / 4)))) if "Pols Voice" in location.name: # This enemy needs specific weapons - add_rule(world.get_location(location.name, player), - lambda state: state.has_group("swords", player) or state.has("Bow", player)) + add_rule(tloz_world.get_location(location.name), + lambda state: state.has_group("swords", player) or + (state.has("Bow", player) and state.has_group("arrows", player))) # No requiring anything in a shop until we can farm for money for location in shop_locations: - add_rule(world.get_location(location, player), + add_rule(tloz_world.get_location(location), lambda state: state.has_group("weapons", player)) # Everything from 4 on up has dark rooms for level in tloz_world.levels[4:]: for location in level.locations: - add_rule(world.get_location(location.name, player), + add_rule(tloz_world.get_location(location.name), lambda state: state.has_group("candles", player) or (state.has("Magical Rod", player) and state.has("Book of Magic", player))) # Everything from 5 on up has gaps for level in tloz_world.levels[5:]: for location in level.locations: - add_rule(world.get_location(location.name, player), + add_rule(tloz_world.get_location(location.name), lambda state: state.has("Stepladder", player)) - add_rule(world.get_location("Level 5 Boss", player), - lambda state: state.has("Recorder", player)) - - add_rule(world.get_location("Level 6 Boss", player), - lambda state: state.has("Bow", player) and state.has_group("arrows", player)) + # Level 4 Access + for location in tloz_world.levels[4].locations: + add_rule(tloz_world.get_location(location.name), + lambda state: state.has_any(("Raft", "Recorder"), player)) - add_rule(world.get_location("Level 7 Item (Red Candle)", player), + # Digdogger boss. Rework this once ER happens + add_rule(tloz_world.get_location("Level 5 Boss"), lambda state: state.has("Recorder", player)) - add_rule(world.get_location("Level 7 Boss", player), + add_rule(tloz_world.get_location("Level 5 Triforce"), lambda state: state.has("Recorder", player)) - if options.ExpandedPool: - add_rule(world.get_location("Level 7 Key Drop (Stalfos)", player), - lambda state: state.has("Recorder", player)) - add_rule(world.get_location("Level 7 Bomb Drop (Digdogger)", player), - lambda state: state.has("Recorder", player)) - add_rule(world.get_location("Level 7 Rupee Drop (Dodongos)", player), + + for location in gohma_locations: + if options.ExpandedPool or "Drop" not in location: + add_rule(tloz_world.get_location(location), + lambda state: state.has("Bow", player) and state.has_group("arrows", player)) + + # Recorder Access for Level 7 + for location in tloz_world.levels[7].locations: + add_rule(tloz_world.get_location(location.name), lambda state: state.has("Recorder", player)) for location in food_locations: if options.ExpandedPool or "Drop" not in location: - add_rule(world.get_location(location, player), + add_rule(tloz_world.get_location(location), lambda state: state.has("Food", player)) for location in gleeok_locations: - add_rule(world.get_location(location, player), + add_rule(tloz_world.get_location(location), lambda state: state.has_group("swords", player) or state.has("Magical Rod", player)) # Candle access for Level 8 for location in tloz_world.levels[8].locations: - add_rule(world.get_location(location.name, player), + add_rule(tloz_world.get_location(location.name), lambda state: state.has_group("candles", player)) - add_rule(world.get_location("Level 8 Item (Magical Key)", player), + add_rule(tloz_world.get_location("Level 8 Item (Magical Key)"), lambda state: state.has("Bow", player) and state.has_group("arrows", player)) if options.ExpandedPool: - add_rule(world.get_location("Level 8 Bomb Drop (Darknuts North)", player), + add_rule(tloz_world.get_location("Level 8 Bomb Drop (Darknuts North)"), lambda state: state.has("Bow", player) and state.has_group("arrows", player)) for location in tloz_world.levels[9].locations: - add_rule(world.get_location(location.name, player), + add_rule(tloz_world.get_location(location.name), lambda state: state.has("Triforce Fragment", player, 8) and state.has_group("swords", player)) # Yes we are looping this range again for Triforce locations. No I can't add it to the boss event loop for level in range(1, 9): - add_rule(world.get_location(f"Level {level} Triforce", player), + add_rule(tloz_world.get_location(f"Level {level} Triforce"), lambda state, l=level: state.has(f"Boss {l} Defeated", player)) # Sword, raft, and ladder spots - add_rule(world.get_location("White Sword Pond", player), + add_rule(tloz_world.get_location("White Sword Pond"), lambda state: state.has("Heart Container", player, 2)) - add_rule(world.get_location("Magical Sword Grave", player), + add_rule(tloz_world.get_location("Magical Sword Grave"), lambda state: state.has("Heart Container", player, 9)) stepladder_locations = ["Ocean Heart Container", "Level 4 Triforce", "Level 4 Boss", "Level 4 Map"] stepladder_locations_expanded = ["Level 4 Key Drop (Keese North)"] for location in stepladder_locations: - add_rule(world.get_location(location, player), + add_rule(tloz_world.get_location(location), lambda state: state.has("Stepladder", player)) if options.ExpandedPool: for location in stepladder_locations_expanded: - add_rule(world.get_location(location, player), + add_rule(tloz_world.get_location(location), lambda state: state.has("Stepladder", player)) # Don't allow Take Any Items until we can actually get in one if options.ExpandedPool: - add_rule(world.get_location("Take Any Item Left", player), + add_rule(tloz_world.get_location("Take Any Item Left"), lambda state: state.has_group("candles", player) or state.has("Raft", player)) - add_rule(world.get_location("Take Any Item Middle", player), + add_rule(tloz_world.get_location("Take Any Item Middle"), lambda state: state.has_group("candles", player) or state.has("Raft", player)) - add_rule(world.get_location("Take Any Item Right", player), + add_rule(tloz_world.get_location("Take Any Item Right"), lambda state: state.has_group("candles", player) or state.has("Raft", player)) - for location in tloz_world.levels[4].locations: - add_rule(world.get_location(location.name, player), - lambda state: state.has("Raft", player) or state.has("Recorder", player)) - for location in tloz_world.levels[7].locations: - add_rule(world.get_location(location.name, player), - lambda state: state.has("Recorder", player)) - for location in tloz_world.levels[8].locations: - add_rule(world.get_location(location.name, player), - lambda state: state.has("Bow", player)) - add_rule(world.get_location("Potion Shop Item Left", player), + add_rule(tloz_world.get_location("Potion Shop Item Left"), lambda state: state.has("Letter", player)) - add_rule(world.get_location("Potion Shop Item Middle", player), + add_rule(tloz_world.get_location("Potion Shop Item Middle"), lambda state: state.has("Letter", player)) - add_rule(world.get_location("Potion Shop Item Right", player), + add_rule(tloz_world.get_location("Potion Shop Item Right"), lambda state: state.has("Letter", player)) - add_rule(world.get_location("Shield Shop Item Left", player), + add_rule(tloz_world.get_location("Shield Shop Item Left"), lambda state: state.has_group("candles", player) or state.has("Bomb", player)) - add_rule(world.get_location("Shield Shop Item Middle", player), + add_rule(tloz_world.get_location("Shield Shop Item Middle"), lambda state: state.has_group("candles", player) or state.has("Bomb", player)) - add_rule(world.get_location("Shield Shop Item Right", player), + add_rule(tloz_world.get_location("Shield Shop Item Right"), lambda state: state.has_group("candles", player) or - state.has("Bomb", player)) \ No newline at end of file + state.has("Bomb", player)) diff --git a/worlds/tunic/__init__.py b/worlds/tunic/__init__.py index cdd968acce44..d1430aac1895 100644 --- a/worlds/tunic/__init__.py +++ b/worlds/tunic/__init__.py @@ -83,6 +83,11 @@ class TunicWorld(World): shop_num: int = 1 # need to make it so that you can walk out of shops, but also that they aren't all connected er_regions: Dict[str, RegionInfo] # absolutely needed so outlet regions work + # so we only loop the multiworld locations once + # if these are locations instead of their info, it gives a memory leak error + item_link_locations: Dict[int, Dict[str, List[Tuple[int, str]]]] = {} + player_item_link_locations: Dict[str, List[Location]] + def generate_early(self) -> None: if self.options.logic_rules >= LogicRules.option_no_major_glitches: self.options.laurels_zips.value = LaurelsZips.option_true @@ -274,6 +279,12 @@ def remove_filler(amount: int) -> None: if items_to_create[page] > 0: tunic_items.append(self.create_item(page, ItemClassification.useful)) items_to_create[page] = 0 + # if ice grapple logic is on, probably really want icebolt + elif self.options.ice_grappling: + page = "Pages 52-53 (Icebolt)" + if items_to_create[page] > 0: + tunic_items.append(self.create_item(page, ItemClassification.progression | ItemClassification.useful)) + items_to_create[page] = 0 if self.options.maskless: tunic_items.append(self.create_item("Scavenger Mask", ItemClassification.useful)) @@ -381,6 +392,18 @@ def extend_hint_information(self, hint_data: Dict[int, Dict[int, str]]) -> None: if hint_text: hint_data[self.player][location.address] = hint_text + def get_real_location(self, location: Location) -> Tuple[str, int]: + # if it's not in a group, it's not in an item link + if location.player not in self.multiworld.groups or not location.item: + return location.name, location.player + try: + loc = self.player_item_link_locations[location.item.name].pop() + return loc.name, loc.player + except IndexError: + warning(f"TUNIC: Failed to parse item location for in-game hints for {self.player_name}. " + f"Using a potentially incorrect location name instead.") + return location.name, location.player + def fill_slot_data(self) -> Dict[str, Any]: slot_data: Dict[str, Any] = { "seed": self.random.randint(0, 2147483647), @@ -406,12 +429,35 @@ def fill_slot_data(self) -> Dict[str, Any]: "disable_local_spoiler": int(self.settings.disable_local_spoiler or self.multiworld.is_race), } + # this would be in a stage if there was an appropriate stage for it + self.player_item_link_locations = {} + groups = self.multiworld.get_player_groups(self.player) + # checking if groups so that this doesn't run if the player isn't in a group + if groups: + if not self.item_link_locations: + tunic_worlds: Tuple[TunicWorld] = self.multiworld.get_game_worlds("TUNIC") + # figure out our groups and the items in them + for tunic in tunic_worlds: + for group in self.multiworld.get_player_groups(tunic.player): + self.item_link_locations.setdefault(group, {}) + for location in self.multiworld.get_locations(): + if location.item and location.item.player in self.item_link_locations.keys(): + (self.item_link_locations[location.item.player].setdefault(location.item.name, []) + .append((location.player, location.name))) + + # if item links are on, set up the player's personal item link locations, so we can pop them as needed + for group, item_links in self.item_link_locations.items(): + if group in groups: + for item_name, locs in item_links.items(): + self.player_item_link_locations[item_name] = \ + [self.multiworld.get_location(location_name, player) for player, location_name in locs] + for tunic_item in filter(lambda item: item.location is not None and item.code is not None, self.slot_data_items): if tunic_item.name not in slot_data: slot_data[tunic_item.name] = [] if tunic_item.name == gold_hexagon and len(slot_data[gold_hexagon]) >= 6: continue - slot_data[tunic_item.name].extend([tunic_item.location.name, tunic_item.location.player]) + slot_data[tunic_item.name].extend(self.get_real_location(tunic_item.location)) for start_item in self.options.start_inventory_from_pool: if start_item in slot_data_item_names: @@ -430,7 +476,7 @@ def fill_slot_data(self) -> Dict[str, Any]: if item in slot_data_item_names: slot_data[item] = [] for item_location in self.multiworld.find_item_locations(item, self.player): - slot_data[item].extend([item_location.name, item_location.player]) + slot_data[item].extend(self.get_real_location(item_location)) return slot_data diff --git a/worlds/tunic/docs/en_TUNIC.md b/worlds/tunic/docs/en_TUNIC.md index b2e1a71897c0..ab751d8e669d 100644 --- a/worlds/tunic/docs/en_TUNIC.md +++ b/worlds/tunic/docs/en_TUNIC.md @@ -56,6 +56,7 @@ In general: - Bushes are not considered in logic. It is assumed that the player will find a way past them, whether it is with a sword, a bomb, fire, luring an enemy, etc. There is also an option in the in-game randomizer settings menu to clear some of the early bushes. - The Cathedral is accessible during the day by using the Hero's Laurels to reach the Overworld fuse near the Swamp entrance. - The Secret Legend chest at the Cathedral can be obtained during the day by opening the Holy Cross door from the outside. +- For the Ice Grappling, Ladder Storage, and Laurels Zips options, there is [this document](https://docs.google.com/document/d/1SFZBfsqZWH1_EAV9zyZobvrBcvCd3_54JP3iVnJ8rUg/edit?usp=sharing) that shows the individual applications of these tricks in logic. For the Entrance Randomizer: - Activating a fuse to turn on a yellow teleporter pad also activates its counterpart in the Far Shore. diff --git a/worlds/tunic/er_data.py b/worlds/tunic/er_data.py index 343bf3055378..1269f3b85e45 100644 --- a/worlds/tunic/er_data.py +++ b/worlds/tunic/er_data.py @@ -807,7 +807,7 @@ class DeadEnd(IntEnum): [], # drop a rudeling, icebolt or ice bomb "Overworld to West Garden from Furnace": - [["IG3"]], + [["IG3"], ["LS1"]], }, "East Overworld": { "Above Ruined Passage": diff --git a/worlds/tunic/er_rules.py b/worlds/tunic/er_rules.py index bd2498a56a35..3b111ad83488 100644 --- a/worlds/tunic/er_rules.py +++ b/worlds/tunic/er_rules.py @@ -344,9 +344,10 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ connecting_region=regions["Overworld"], rule=lambda state: state.has_any({grapple, laurels}, player)) - regions["Overworld"].connect( + cube_entrance = regions["Overworld"].connect( connecting_region=regions["Cube Cave Entrance Region"], rule=lambda state: state.has(gun, player) or can_shop(state, world)) + world.multiworld.register_indirect_condition(regions["Shop"], cube_entrance) regions["Cube Cave Entrance Region"].connect( connecting_region=regions["Overworld"]) @@ -500,9 +501,11 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ regions["Dark Tomb Upper"].connect( connecting_region=regions["Dark Tomb Entry Point"]) + # ice grapple through the wall, get the little secret sound to trigger regions["Dark Tomb Upper"].connect( connecting_region=regions["Dark Tomb Main"], - rule=lambda state: has_ladder("Ladder in Dark Tomb", state, world)) + rule=lambda state: has_ladder("Ladder in Dark Tomb", state, world) + or has_ice_grapple_logic(False, IceGrappling.option_hard, state, world)) regions["Dark Tomb Main"].connect( connecting_region=regions["Dark Tomb Upper"], rule=lambda state: has_ladder("Ladder in Dark Tomb", state, world)) @@ -778,12 +781,10 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ regions["Fortress East Shortcut Upper"].connect( connecting_region=regions["Fortress East Shortcut Lower"]) - # nmg: can ice grapple upwards regions["Fortress East Shortcut Lower"].connect( connecting_region=regions["Fortress East Shortcut Upper"], rule=lambda state: has_ice_grapple_logic(True, IceGrappling.option_easy, state, world)) - # nmg: ice grapple through the big gold door, can do it both ways regions["Eastern Vault Fortress"].connect( connecting_region=regions["Eastern Vault Fortress Gold Door"], rule=lambda state: state.has_all({"Activate Eastern Vault West Fuses", @@ -806,7 +807,6 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ regions["Fortress Hero's Grave Region"].connect( connecting_region=regions["Fortress Grave Path"]) - # nmg: ice grapple from upper grave path to lower regions["Fortress Grave Path Upper"].connect( connecting_region=regions["Fortress Grave Path"], rule=lambda state: has_ice_grapple_logic(True, IceGrappling.option_easy, state, world)) @@ -1138,6 +1138,9 @@ def ls_connect(origin_name: str, portal_sdt: str) -> None: for portal_dest in region_info.portals: ls_connect(ladder_region, "Overworld Redux, " + portal_dest) + # convenient staircase means this one is easy difficulty, even though there's an elevation change + ls_connect("LS Elev 0", "Overworld Redux, Furnace_gyro_west") + # connect ls elevation regions to regions where you can get an enemy to knock you down, also well rail if options.ladder_storage >= LadderStorage.option_medium: for ladder_region, region_info in ow_ladder_groups.items(): @@ -1153,6 +1156,7 @@ def ls_connect(origin_name: str, portal_sdt: str) -> None: if options.ladder_storage >= LadderStorage.option_hard: ls_connect("LS Elev 1", "Overworld Redux, EastFiligreeCache_") ls_connect("LS Elev 2", "Overworld Redux, Town_FiligreeRoom_") + ls_connect("LS Elev 2", "Overworld Redux, Ruins Passage_west") ls_connect("LS Elev 3", "Overworld Redux, Overworld Interiors_house") ls_connect("LS Elev 5", "Overworld Redux, Temple_main") diff --git a/worlds/tunic/items.py b/worlds/tunic/items.py index 55aa3468fc6b..b6ce5d8995a8 100644 --- a/worlds/tunic/items.py +++ b/worlds/tunic/items.py @@ -1,10 +1,10 @@ from itertools import groupby from typing import Dict, List, Set, NamedTuple -from BaseClasses import ItemClassification +from BaseClasses import ItemClassification as IC class TunicItemData(NamedTuple): - classification: ItemClassification + classification: IC quantity_in_item_pool: int item_id_offset: int item_group: str = "" @@ -13,157 +13,157 @@ class TunicItemData(NamedTuple): item_base_id = 509342400 item_table: Dict[str, TunicItemData] = { - "Firecracker x2": TunicItemData(ItemClassification.filler, 3, 0, "Bombs"), - "Firecracker x3": TunicItemData(ItemClassification.filler, 3, 1, "Bombs"), - "Firecracker x4": TunicItemData(ItemClassification.filler, 3, 2, "Bombs"), - "Firecracker x5": TunicItemData(ItemClassification.filler, 1, 3, "Bombs"), - "Firecracker x6": TunicItemData(ItemClassification.filler, 2, 4, "Bombs"), - "Fire Bomb x2": TunicItemData(ItemClassification.filler, 2, 5, "Bombs"), - "Fire Bomb x3": TunicItemData(ItemClassification.filler, 1, 6, "Bombs"), - "Ice Bomb x2": TunicItemData(ItemClassification.filler, 2, 7, "Bombs"), - "Ice Bomb x3": TunicItemData(ItemClassification.filler, 2, 8, "Bombs"), - "Ice Bomb x5": TunicItemData(ItemClassification.filler, 1, 9, "Bombs"), - "Lure": TunicItemData(ItemClassification.filler, 4, 10, "Consumables"), - "Lure x2": TunicItemData(ItemClassification.filler, 1, 11, "Consumables"), - "Pepper x2": TunicItemData(ItemClassification.filler, 4, 12, "Consumables"), - "Ivy x3": TunicItemData(ItemClassification.filler, 2, 13, "Consumables"), - "Effigy": TunicItemData(ItemClassification.useful, 12, 14, "Money"), - "HP Berry": TunicItemData(ItemClassification.filler, 2, 15, "Consumables"), - "HP Berry x2": TunicItemData(ItemClassification.filler, 4, 16, "Consumables"), - "HP Berry x3": TunicItemData(ItemClassification.filler, 2, 17, "Consumables"), - "MP Berry": TunicItemData(ItemClassification.filler, 4, 18, "Consumables"), - "MP Berry x2": TunicItemData(ItemClassification.filler, 2, 19, "Consumables"), - "MP Berry x3": TunicItemData(ItemClassification.filler, 7, 20, "Consumables"), - "Fairy": TunicItemData(ItemClassification.progression, 20, 21), - "Stick": TunicItemData(ItemClassification.progression, 1, 22, "Weapons"), - "Sword": TunicItemData(ItemClassification.progression, 3, 23, "Weapons"), - "Sword Upgrade": TunicItemData(ItemClassification.progression, 4, 24, "Weapons"), - "Magic Wand": TunicItemData(ItemClassification.progression, 1, 25, "Weapons"), - "Magic Dagger": TunicItemData(ItemClassification.progression, 1, 26), - "Magic Orb": TunicItemData(ItemClassification.progression, 1, 27), - "Hero's Laurels": TunicItemData(ItemClassification.progression, 1, 28), - "Lantern": TunicItemData(ItemClassification.progression, 1, 29), - "Gun": TunicItemData(ItemClassification.progression, 1, 30, "Weapons"), - "Shield": TunicItemData(ItemClassification.useful, 1, 31), - "Dath Stone": TunicItemData(ItemClassification.useful, 1, 32), - "Hourglass": TunicItemData(ItemClassification.useful, 1, 33), - "Old House Key": TunicItemData(ItemClassification.progression, 1, 34, "Keys"), - "Key": TunicItemData(ItemClassification.progression, 2, 35, "Keys"), - "Fortress Vault Key": TunicItemData(ItemClassification.progression, 1, 36, "Keys"), - "Flask Shard": TunicItemData(ItemClassification.useful, 12, 37), - "Potion Flask": TunicItemData(ItemClassification.useful, 5, 38, "Flask"), - "Golden Coin": TunicItemData(ItemClassification.progression, 17, 39), - "Card Slot": TunicItemData(ItemClassification.useful, 4, 40), - "Red Questagon": TunicItemData(ItemClassification.progression_skip_balancing, 1, 41, "Hexagons"), - "Green Questagon": TunicItemData(ItemClassification.progression_skip_balancing, 1, 42, "Hexagons"), - "Blue Questagon": TunicItemData(ItemClassification.progression_skip_balancing, 1, 43, "Hexagons"), - "Gold Questagon": TunicItemData(ItemClassification.progression_skip_balancing, 0, 44, "Hexagons"), - "ATT Offering": TunicItemData(ItemClassification.useful, 4, 45, "Offerings"), - "DEF Offering": TunicItemData(ItemClassification.useful, 4, 46, "Offerings"), - "Potion Offering": TunicItemData(ItemClassification.useful, 3, 47, "Offerings"), - "HP Offering": TunicItemData(ItemClassification.useful, 6, 48, "Offerings"), - "MP Offering": TunicItemData(ItemClassification.useful, 3, 49, "Offerings"), - "SP Offering": TunicItemData(ItemClassification.useful, 2, 50, "Offerings"), - "Hero Relic - ATT": TunicItemData(ItemClassification.progression_skip_balancing, 1, 51, "Hero Relics"), - "Hero Relic - DEF": TunicItemData(ItemClassification.progression_skip_balancing, 1, 52, "Hero Relics"), - "Hero Relic - HP": TunicItemData(ItemClassification.progression_skip_balancing, 1, 53, "Hero Relics"), - "Hero Relic - MP": TunicItemData(ItemClassification.progression_skip_balancing, 1, 54, "Hero Relics"), - "Hero Relic - POTION": TunicItemData(ItemClassification.progression_skip_balancing, 1, 55, "Hero Relics"), - "Hero Relic - SP": TunicItemData(ItemClassification.progression_skip_balancing, 1, 56, "Hero Relics"), - "Orange Peril Ring": TunicItemData(ItemClassification.useful, 1, 57, "Cards"), - "Tincture": TunicItemData(ItemClassification.useful, 1, 58, "Cards"), - "Scavenger Mask": TunicItemData(ItemClassification.progression, 1, 59, "Cards"), - "Cyan Peril Ring": TunicItemData(ItemClassification.useful, 1, 60, "Cards"), - "Bracer": TunicItemData(ItemClassification.useful, 1, 61, "Cards"), - "Dagger Strap": TunicItemData(ItemClassification.useful, 1, 62, "Cards"), - "Inverted Ash": TunicItemData(ItemClassification.useful, 1, 63, "Cards"), - "Lucky Cup": TunicItemData(ItemClassification.useful, 1, 64, "Cards"), - "Magic Echo": TunicItemData(ItemClassification.useful, 1, 65, "Cards"), - "Anklet": TunicItemData(ItemClassification.useful, 1, 66, "Cards"), - "Muffling Bell": TunicItemData(ItemClassification.useful, 1, 67, "Cards"), - "Glass Cannon": TunicItemData(ItemClassification.useful, 1, 68, "Cards"), - "Perfume": TunicItemData(ItemClassification.useful, 1, 69, "Cards"), - "Louder Echo": TunicItemData(ItemClassification.useful, 1, 70, "Cards"), - "Aura's Gem": TunicItemData(ItemClassification.useful, 1, 71, "Cards"), - "Bone Card": TunicItemData(ItemClassification.useful, 1, 72, "Cards"), - "Mr Mayor": TunicItemData(ItemClassification.useful, 1, 73, "Golden Treasures"), - "Secret Legend": TunicItemData(ItemClassification.useful, 1, 74, "Golden Treasures"), - "Sacred Geometry": TunicItemData(ItemClassification.useful, 1, 75, "Golden Treasures"), - "Vintage": TunicItemData(ItemClassification.useful, 1, 76, "Golden Treasures"), - "Just Some Pals": TunicItemData(ItemClassification.useful, 1, 77, "Golden Treasures"), - "Regal Weasel": TunicItemData(ItemClassification.useful, 1, 78, "Golden Treasures"), - "Spring Falls": TunicItemData(ItemClassification.useful, 1, 79, "Golden Treasures"), - "Power Up": TunicItemData(ItemClassification.useful, 1, 80, "Golden Treasures"), - "Back To Work": TunicItemData(ItemClassification.useful, 1, 81, "Golden Treasures"), - "Phonomath": TunicItemData(ItemClassification.useful, 1, 82, "Golden Treasures"), - "Dusty": TunicItemData(ItemClassification.useful, 1, 83, "Golden Treasures"), - "Forever Friend": TunicItemData(ItemClassification.useful, 1, 84, "Golden Treasures"), - "Fool Trap": TunicItemData(ItemClassification.trap, 0, 85), - "Money x1": TunicItemData(ItemClassification.filler, 3, 86, "Money"), - "Money x10": TunicItemData(ItemClassification.filler, 1, 87, "Money"), - "Money x15": TunicItemData(ItemClassification.filler, 10, 88, "Money"), - "Money x16": TunicItemData(ItemClassification.filler, 1, 89, "Money"), - "Money x20": TunicItemData(ItemClassification.filler, 17, 90, "Money"), - "Money x25": TunicItemData(ItemClassification.filler, 14, 91, "Money"), - "Money x30": TunicItemData(ItemClassification.filler, 4, 92, "Money"), - "Money x32": TunicItemData(ItemClassification.filler, 4, 93, "Money"), - "Money x40": TunicItemData(ItemClassification.filler, 3, 94, "Money"), - "Money x48": TunicItemData(ItemClassification.filler, 1, 95, "Money"), - "Money x50": TunicItemData(ItemClassification.filler, 7, 96, "Money"), - "Money x64": TunicItemData(ItemClassification.filler, 1, 97, "Money"), - "Money x100": TunicItemData(ItemClassification.filler, 5, 98, "Money"), - "Money x128": TunicItemData(ItemClassification.useful, 3, 99, "Money"), - "Money x200": TunicItemData(ItemClassification.useful, 1, 100, "Money"), - "Money x255": TunicItemData(ItemClassification.useful, 1, 101, "Money"), - "Pages 0-1": TunicItemData(ItemClassification.useful, 1, 102, "Pages"), - "Pages 2-3": TunicItemData(ItemClassification.useful, 1, 103, "Pages"), - "Pages 4-5": TunicItemData(ItemClassification.useful, 1, 104, "Pages"), - "Pages 6-7": TunicItemData(ItemClassification.useful, 1, 105, "Pages"), - "Pages 8-9": TunicItemData(ItemClassification.useful, 1, 106, "Pages"), - "Pages 10-11": TunicItemData(ItemClassification.useful, 1, 107, "Pages"), - "Pages 12-13": TunicItemData(ItemClassification.useful, 1, 108, "Pages"), - "Pages 14-15": TunicItemData(ItemClassification.useful, 1, 109, "Pages"), - "Pages 16-17": TunicItemData(ItemClassification.useful, 1, 110, "Pages"), - "Pages 18-19": TunicItemData(ItemClassification.useful, 1, 111, "Pages"), - "Pages 20-21": TunicItemData(ItemClassification.useful, 1, 112, "Pages"), - "Pages 22-23": TunicItemData(ItemClassification.useful, 1, 113, "Pages"), - "Pages 24-25 (Prayer)": TunicItemData(ItemClassification.progression, 1, 114, "Pages"), - "Pages 26-27": TunicItemData(ItemClassification.useful, 1, 115, "Pages"), - "Pages 28-29": TunicItemData(ItemClassification.useful, 1, 116, "Pages"), - "Pages 30-31": TunicItemData(ItemClassification.useful, 1, 117, "Pages"), - "Pages 32-33": TunicItemData(ItemClassification.useful, 1, 118, "Pages"), - "Pages 34-35": TunicItemData(ItemClassification.useful, 1, 119, "Pages"), - "Pages 36-37": TunicItemData(ItemClassification.useful, 1, 120, "Pages"), - "Pages 38-39": TunicItemData(ItemClassification.useful, 1, 121, "Pages"), - "Pages 40-41": TunicItemData(ItemClassification.useful, 1, 122, "Pages"), - "Pages 42-43 (Holy Cross)": TunicItemData(ItemClassification.progression, 1, 123, "Pages"), - "Pages 44-45": TunicItemData(ItemClassification.useful, 1, 124, "Pages"), - "Pages 46-47": TunicItemData(ItemClassification.useful, 1, 125, "Pages"), - "Pages 48-49": TunicItemData(ItemClassification.useful, 1, 126, "Pages"), - "Pages 50-51": TunicItemData(ItemClassification.useful, 1, 127, "Pages"), - "Pages 52-53 (Icebolt)": TunicItemData(ItemClassification.progression, 1, 128, "Pages"), - "Pages 54-55": TunicItemData(ItemClassification.useful, 1, 129, "Pages"), - "Ladders near Weathervane": TunicItemData(ItemClassification.progression, 0, 130, "Ladders"), - "Ladders near Overworld Checkpoint": TunicItemData(ItemClassification.progression, 0, 131, "Ladders"), - "Ladders near Patrol Cave": TunicItemData(ItemClassification.progression, 0, 132, "Ladders"), - "Ladder near Temple Rafters": TunicItemData(ItemClassification.progression, 0, 133, "Ladders"), - "Ladders near Dark Tomb": TunicItemData(ItemClassification.progression, 0, 134, "Ladders"), - "Ladder to Quarry": TunicItemData(ItemClassification.progression, 0, 135, "Ladders"), - "Ladders to West Bell": TunicItemData(ItemClassification.progression, 0, 136, "Ladders"), - "Ladders in Overworld Town": TunicItemData(ItemClassification.progression, 0, 137, "Ladders"), - "Ladder to Ruined Atoll": TunicItemData(ItemClassification.progression, 0, 138, "Ladders"), - "Ladder to Swamp": TunicItemData(ItemClassification.progression, 0, 139, "Ladders"), - "Ladders in Well": TunicItemData(ItemClassification.progression, 0, 140, "Ladders"), - "Ladder in Dark Tomb": TunicItemData(ItemClassification.progression, 0, 141, "Ladders"), - "Ladder to East Forest": TunicItemData(ItemClassification.progression, 0, 142, "Ladders"), - "Ladders to Lower Forest": TunicItemData(ItemClassification.progression, 0, 143, "Ladders"), - "Ladder to Beneath the Vault": TunicItemData(ItemClassification.progression, 0, 144, "Ladders"), - "Ladders in Hourglass Cave": TunicItemData(ItemClassification.progression, 0, 145, "Ladders"), - "Ladders in South Atoll": TunicItemData(ItemClassification.progression, 0, 146, "Ladders"), - "Ladders to Frog's Domain": TunicItemData(ItemClassification.progression, 0, 147, "Ladders"), - "Ladders in Library": TunicItemData(ItemClassification.progression, 0, 148, "Ladders"), - "Ladders in Lower Quarry": TunicItemData(ItemClassification.progression, 0, 149, "Ladders"), - "Ladders in Swamp": TunicItemData(ItemClassification.progression, 0, 150, "Ladders"), + "Firecracker x2": TunicItemData(IC.filler, 3, 0, "Bombs"), + "Firecracker x3": TunicItemData(IC.filler, 3, 1, "Bombs"), + "Firecracker x4": TunicItemData(IC.filler, 3, 2, "Bombs"), + "Firecracker x5": TunicItemData(IC.filler, 1, 3, "Bombs"), + "Firecracker x6": TunicItemData(IC.filler, 2, 4, "Bombs"), + "Fire Bomb x2": TunicItemData(IC.filler, 2, 5, "Bombs"), + "Fire Bomb x3": TunicItemData(IC.filler, 1, 6, "Bombs"), + "Ice Bomb x2": TunicItemData(IC.filler, 2, 7, "Bombs"), + "Ice Bomb x3": TunicItemData(IC.filler, 2, 8, "Bombs"), + "Ice Bomb x5": TunicItemData(IC.filler, 1, 9, "Bombs"), + "Lure": TunicItemData(IC.filler, 4, 10, "Consumables"), + "Lure x2": TunicItemData(IC.filler, 1, 11, "Consumables"), + "Pepper x2": TunicItemData(IC.filler, 4, 12, "Consumables"), + "Ivy x3": TunicItemData(IC.filler, 2, 13, "Consumables"), + "Effigy": TunicItemData(IC.useful, 12, 14, "Money"), + "HP Berry": TunicItemData(IC.filler, 2, 15, "Consumables"), + "HP Berry x2": TunicItemData(IC.filler, 4, 16, "Consumables"), + "HP Berry x3": TunicItemData(IC.filler, 2, 17, "Consumables"), + "MP Berry": TunicItemData(IC.filler, 4, 18, "Consumables"), + "MP Berry x2": TunicItemData(IC.filler, 2, 19, "Consumables"), + "MP Berry x3": TunicItemData(IC.filler, 7, 20, "Consumables"), + "Fairy": TunicItemData(IC.progression, 20, 21), + "Stick": TunicItemData(IC.progression | IC.useful, 1, 22, "Weapons"), + "Sword": TunicItemData(IC.progression | IC.useful, 3, 23, "Weapons"), + "Sword Upgrade": TunicItemData(IC.progression | IC.useful, 4, 24, "Weapons"), + "Magic Wand": TunicItemData(IC.progression | IC.useful, 1, 25, "Weapons"), + "Magic Dagger": TunicItemData(IC.progression | IC.useful, 1, 26), + "Magic Orb": TunicItemData(IC.progression | IC.useful, 1, 27), + "Hero's Laurels": TunicItemData(IC.progression | IC.useful, 1, 28), + "Lantern": TunicItemData(IC.progression, 1, 29), + "Gun": TunicItemData(IC.progression | IC.useful, 1, 30, "Weapons"), + "Shield": TunicItemData(IC.useful, 1, 31), + "Dath Stone": TunicItemData(IC.useful, 1, 32), + "Hourglass": TunicItemData(IC.useful, 1, 33), + "Old House Key": TunicItemData(IC.progression, 1, 34, "Keys"), + "Key": TunicItemData(IC.progression, 2, 35, "Keys"), + "Fortress Vault Key": TunicItemData(IC.progression, 1, 36, "Keys"), + "Flask Shard": TunicItemData(IC.useful, 12, 37), + "Potion Flask": TunicItemData(IC.useful, 5, 38, "Flask"), + "Golden Coin": TunicItemData(IC.progression, 17, 39), + "Card Slot": TunicItemData(IC.useful, 4, 40), + "Red Questagon": TunicItemData(IC.progression_skip_balancing, 1, 41, "Hexagons"), + "Green Questagon": TunicItemData(IC.progression_skip_balancing, 1, 42, "Hexagons"), + "Blue Questagon": TunicItemData(IC.progression_skip_balancing, 1, 43, "Hexagons"), + "Gold Questagon": TunicItemData(IC.progression_skip_balancing, 0, 44, "Hexagons"), + "ATT Offering": TunicItemData(IC.useful, 4, 45, "Offerings"), + "DEF Offering": TunicItemData(IC.useful, 4, 46, "Offerings"), + "Potion Offering": TunicItemData(IC.useful, 3, 47, "Offerings"), + "HP Offering": TunicItemData(IC.useful, 6, 48, "Offerings"), + "MP Offering": TunicItemData(IC.useful, 3, 49, "Offerings"), + "SP Offering": TunicItemData(IC.useful, 2, 50, "Offerings"), + "Hero Relic - ATT": TunicItemData(IC.progression_skip_balancing, 1, 51, "Hero Relics"), + "Hero Relic - DEF": TunicItemData(IC.progression_skip_balancing, 1, 52, "Hero Relics"), + "Hero Relic - HP": TunicItemData(IC.progression_skip_balancing, 1, 53, "Hero Relics"), + "Hero Relic - MP": TunicItemData(IC.progression_skip_balancing, 1, 54, "Hero Relics"), + "Hero Relic - POTION": TunicItemData(IC.progression_skip_balancing, 1, 55, "Hero Relics"), + "Hero Relic - SP": TunicItemData(IC.progression_skip_balancing, 1, 56, "Hero Relics"), + "Orange Peril Ring": TunicItemData(IC.useful, 1, 57, "Cards"), + "Tincture": TunicItemData(IC.useful, 1, 58, "Cards"), + "Scavenger Mask": TunicItemData(IC.progression, 1, 59, "Cards"), + "Cyan Peril Ring": TunicItemData(IC.useful, 1, 60, "Cards"), + "Bracer": TunicItemData(IC.useful, 1, 61, "Cards"), + "Dagger Strap": TunicItemData(IC.useful, 1, 62, "Cards"), + "Inverted Ash": TunicItemData(IC.useful, 1, 63, "Cards"), + "Lucky Cup": TunicItemData(IC.useful, 1, 64, "Cards"), + "Magic Echo": TunicItemData(IC.useful, 1, 65, "Cards"), + "Anklet": TunicItemData(IC.useful, 1, 66, "Cards"), + "Muffling Bell": TunicItemData(IC.useful, 1, 67, "Cards"), + "Glass Cannon": TunicItemData(IC.useful, 1, 68, "Cards"), + "Perfume": TunicItemData(IC.useful, 1, 69, "Cards"), + "Louder Echo": TunicItemData(IC.useful, 1, 70, "Cards"), + "Aura's Gem": TunicItemData(IC.useful, 1, 71, "Cards"), + "Bone Card": TunicItemData(IC.useful, 1, 72, "Cards"), + "Mr Mayor": TunicItemData(IC.useful, 1, 73, "Golden Treasures"), + "Secret Legend": TunicItemData(IC.useful, 1, 74, "Golden Treasures"), + "Sacred Geometry": TunicItemData(IC.useful, 1, 75, "Golden Treasures"), + "Vintage": TunicItemData(IC.useful, 1, 76, "Golden Treasures"), + "Just Some Pals": TunicItemData(IC.useful, 1, 77, "Golden Treasures"), + "Regal Weasel": TunicItemData(IC.useful, 1, 78, "Golden Treasures"), + "Spring Falls": TunicItemData(IC.useful, 1, 79, "Golden Treasures"), + "Power Up": TunicItemData(IC.useful, 1, 80, "Golden Treasures"), + "Back To Work": TunicItemData(IC.useful, 1, 81, "Golden Treasures"), + "Phonomath": TunicItemData(IC.useful, 1, 82, "Golden Treasures"), + "Dusty": TunicItemData(IC.useful, 1, 83, "Golden Treasures"), + "Forever Friend": TunicItemData(IC.useful, 1, 84, "Golden Treasures"), + "Fool Trap": TunicItemData(IC.trap, 0, 85), + "Money x1": TunicItemData(IC.filler, 3, 86, "Money"), + "Money x10": TunicItemData(IC.filler, 1, 87, "Money"), + "Money x15": TunicItemData(IC.filler, 10, 88, "Money"), + "Money x16": TunicItemData(IC.filler, 1, 89, "Money"), + "Money x20": TunicItemData(IC.filler, 17, 90, "Money"), + "Money x25": TunicItemData(IC.filler, 14, 91, "Money"), + "Money x30": TunicItemData(IC.filler, 4, 92, "Money"), + "Money x32": TunicItemData(IC.filler, 4, 93, "Money"), + "Money x40": TunicItemData(IC.filler, 3, 94, "Money"), + "Money x48": TunicItemData(IC.filler, 1, 95, "Money"), + "Money x50": TunicItemData(IC.filler, 7, 96, "Money"), + "Money x64": TunicItemData(IC.filler, 1, 97, "Money"), + "Money x100": TunicItemData(IC.filler, 5, 98, "Money"), + "Money x128": TunicItemData(IC.useful, 3, 99, "Money"), + "Money x200": TunicItemData(IC.useful, 1, 100, "Money"), + "Money x255": TunicItemData(IC.useful, 1, 101, "Money"), + "Pages 0-1": TunicItemData(IC.useful, 1, 102, "Pages"), + "Pages 2-3": TunicItemData(IC.useful, 1, 103, "Pages"), + "Pages 4-5": TunicItemData(IC.useful, 1, 104, "Pages"), + "Pages 6-7": TunicItemData(IC.useful, 1, 105, "Pages"), + "Pages 8-9": TunicItemData(IC.useful, 1, 106, "Pages"), + "Pages 10-11": TunicItemData(IC.useful, 1, 107, "Pages"), + "Pages 12-13": TunicItemData(IC.useful, 1, 108, "Pages"), + "Pages 14-15": TunicItemData(IC.useful, 1, 109, "Pages"), + "Pages 16-17": TunicItemData(IC.useful, 1, 110, "Pages"), + "Pages 18-19": TunicItemData(IC.useful, 1, 111, "Pages"), + "Pages 20-21": TunicItemData(IC.useful, 1, 112, "Pages"), + "Pages 22-23": TunicItemData(IC.useful, 1, 113, "Pages"), + "Pages 24-25 (Prayer)": TunicItemData(IC.progression | IC.useful, 1, 114, "Pages"), + "Pages 26-27": TunicItemData(IC.useful, 1, 115, "Pages"), + "Pages 28-29": TunicItemData(IC.useful, 1, 116, "Pages"), + "Pages 30-31": TunicItemData(IC.useful, 1, 117, "Pages"), + "Pages 32-33": TunicItemData(IC.useful, 1, 118, "Pages"), + "Pages 34-35": TunicItemData(IC.useful, 1, 119, "Pages"), + "Pages 36-37": TunicItemData(IC.useful, 1, 120, "Pages"), + "Pages 38-39": TunicItemData(IC.useful, 1, 121, "Pages"), + "Pages 40-41": TunicItemData(IC.useful, 1, 122, "Pages"), + "Pages 42-43 (Holy Cross)": TunicItemData(IC.progression | IC.useful, 1, 123, "Pages"), + "Pages 44-45": TunicItemData(IC.useful, 1, 124, "Pages"), + "Pages 46-47": TunicItemData(IC.useful, 1, 125, "Pages"), + "Pages 48-49": TunicItemData(IC.useful, 1, 126, "Pages"), + "Pages 50-51": TunicItemData(IC.useful, 1, 127, "Pages"), + "Pages 52-53 (Icebolt)": TunicItemData(IC.progression, 1, 128, "Pages"), + "Pages 54-55": TunicItemData(IC.useful, 1, 129, "Pages"), + "Ladders near Weathervane": TunicItemData(IC.progression, 0, 130, "Ladders"), + "Ladders near Overworld Checkpoint": TunicItemData(IC.progression, 0, 131, "Ladders"), + "Ladders near Patrol Cave": TunicItemData(IC.progression, 0, 132, "Ladders"), + "Ladder near Temple Rafters": TunicItemData(IC.progression, 0, 133, "Ladders"), + "Ladders near Dark Tomb": TunicItemData(IC.progression, 0, 134, "Ladders"), + "Ladder to Quarry": TunicItemData(IC.progression, 0, 135, "Ladders"), + "Ladders to West Bell": TunicItemData(IC.progression, 0, 136, "Ladders"), + "Ladders in Overworld Town": TunicItemData(IC.progression, 0, 137, "Ladders"), + "Ladder to Ruined Atoll": TunicItemData(IC.progression, 0, 138, "Ladders"), + "Ladder to Swamp": TunicItemData(IC.progression, 0, 139, "Ladders"), + "Ladders in Well": TunicItemData(IC.progression, 0, 140, "Ladders"), + "Ladder in Dark Tomb": TunicItemData(IC.progression, 0, 141, "Ladders"), + "Ladder to East Forest": TunicItemData(IC.progression, 0, 142, "Ladders"), + "Ladders to Lower Forest": TunicItemData(IC.progression, 0, 143, "Ladders"), + "Ladder to Beneath the Vault": TunicItemData(IC.progression, 0, 144, "Ladders"), + "Ladders in Hourglass Cave": TunicItemData(IC.progression, 0, 145, "Ladders"), + "Ladders in South Atoll": TunicItemData(IC.progression, 0, 146, "Ladders"), + "Ladders to Frog's Domain": TunicItemData(IC.progression, 0, 147, "Ladders"), + "Ladders in Library": TunicItemData(IC.progression, 0, 148, "Ladders"), + "Ladders in Lower Quarry": TunicItemData(IC.progression, 0, 149, "Ladders"), + "Ladders in Swamp": TunicItemData(IC.progression, 0, 150, "Ladders"), } # items to be replaced by fool traps @@ -208,7 +208,7 @@ class TunicItemData(NamedTuple): item_name_to_id: Dict[str, int] = {name: item_base_id + data.item_id_offset for name, data in item_table.items()} -filler_items: List[str] = [name for name, data in item_table.items() if data.classification == ItemClassification.filler] +filler_items: List[str] = [name for name, data in item_table.items() if data.classification == IC.filler] def get_item_group(item_name: str) -> str: diff --git a/worlds/tunic/ladder_storage_data.py b/worlds/tunic/ladder_storage_data.py index a29d50b4f455..c6dda42bca79 100644 --- a/worlds/tunic/ladder_storage_data.py +++ b/worlds/tunic/ladder_storage_data.py @@ -17,7 +17,7 @@ class OWLadderInfo(NamedTuple): ["Overworld Beach"]), # also the east filigree room "LS Elev 1": OWLadderInfo({"Ladders near Weathervane", "Ladders in Overworld Town", "Ladder to Swamp"}, - ["Furnace_gyro_lower", "Swamp Redux 2_wall"], + ["Furnace_gyro_lower", "Furnace_gyro_west", "Swamp Redux 2_wall"], ["Overworld Tunnel Turret"]), # also the fountain filigree room and ruined passage door "LS Elev 2": OWLadderInfo({"Ladders near Weathervane", "Ladders to West Bell"}, diff --git a/worlds/tunic/options.py b/worlds/tunic/options.py index 1683b3ca5aee..cdd37a889461 100644 --- a/worlds/tunic/options.py +++ b/worlds/tunic/options.py @@ -183,7 +183,7 @@ class IceGrappling(Choice): Easy includes ice grappling enemies that are in range without luring them. May include clips through terrain. Medium includes using ice grapples to push enemies through doors or off ledges without luring them. Also includes bringing an enemy over to the Temple Door to grapple through it. Hard includes luring or grappling enemies to get to where you want to go. - The Medium and Hard options will give the player the Torch to return to the Overworld checkpoint to avoid softlocks. Using the Torch is considered in logic. + Enabling any of these difficulty options will give the player the Torch to return to the Overworld checkpoint to avoid softlocks. Using the Torch is considered in logic. Note: You will still be expected to ice grapple to the slime in East Forest from below with this option off. """ internal_name = "ice_grappling" @@ -201,7 +201,7 @@ class LadderStorage(Choice): Easy includes uses of Ladder Storage to get to open doors over a long distance without too much difficulty. May include convenient elevation changes (going up Mountain stairs, stairs in front of Special Shop, etc.). Medium includes the above as well as changing your elevation using the environment and getting knocked down by melee enemies mid-LS. Hard includes the above as well as going behind the map to enter closed doors from behind, shooting a fuse with the magic wand to knock yourself down at close range, and getting into the Cathedral Secret Legend room mid-LS. - Enabling any of these difficulty options will give the player the Torch item to return to the Overworld checkpoint to avoid softlocks. Using the Torch is considered in logic. + Enabling any of these difficulty options will give the player the Torch to return to the Overworld checkpoint to avoid softlocks. Using the Torch is considered in logic. Opening individual chests while doing ladder storage is excluded due to tedium. Knocking yourself out of LS with a bomb is excluded due to the problematic nature of consumables in logic. """ diff --git a/worlds/witness/__init__.py b/worlds/witness/__init__.py index c9848f2ffe47..ac9197bd92bb 100644 --- a/worlds/witness/__init__.py +++ b/worlds/witness/__init__.py @@ -50,6 +50,8 @@ class WitnessWorld(World): topology_present = False web = WitnessWebWorld() + origin_region_name = "Entry" + options_dataclass = TheWitnessOptions options: TheWitnessOptions @@ -78,7 +80,7 @@ class WitnessWorld(World): def _get_slot_data(self) -> Dict[str, Any]: return { - "seed": self.random.randrange(0, 1000000), + "seed": self.options.puzzle_randomization_seed.value, "victory_location": int(self.player_logic.VICTORY_LOCATION, 16), "panelhex_to_id": self.player_locations.CHECK_PANELHEX_TO_ID, "item_id_to_door_hexes": static_witness_items.get_item_to_door_mappings(), diff --git a/worlds/witness/data/WitnessLogic.txt b/worlds/witness/data/WitnessLogic.txt index fabd1428810b..8fadf68c3131 100644 --- a/worlds/witness/data/WitnessLogic.txt +++ b/worlds/witness/data/WitnessLogic.txt @@ -1,7 +1,5 @@ ==Tutorial (Inside)== -Menu (Menu) - Entry - True: - Entry (Entry): Tutorial First Hallway (Tutorial First Hallway) - Entry - True - Tutorial First Hallway Room - 0x00064: diff --git a/worlds/witness/data/WitnessLogicExpert.txt b/worlds/witness/data/WitnessLogicExpert.txt index 200138dee1f7..c6d6efa96485 100644 --- a/worlds/witness/data/WitnessLogicExpert.txt +++ b/worlds/witness/data/WitnessLogicExpert.txt @@ -1,7 +1,5 @@ ==Tutorial (Inside)== -Menu (Menu) - Entry - True: - Entry (Entry): Tutorial First Hallway (Tutorial First Hallway) - Entry - True - Tutorial First Hallway Room - 0x00064: diff --git a/worlds/witness/data/WitnessLogicVanilla.txt b/worlds/witness/data/WitnessLogicVanilla.txt index 67a42ba7e4d4..1186c470233e 100644 --- a/worlds/witness/data/WitnessLogicVanilla.txt +++ b/worlds/witness/data/WitnessLogicVanilla.txt @@ -1,7 +1,5 @@ ==Tutorial (Inside)== -Menu (Menu) - Entry - True: - Entry (Entry): Tutorial First Hallway (Tutorial First Hallway) - Entry - True - Tutorial First Hallway Room - 0x00064: diff --git a/worlds/witness/data/WitnessLogicVariety.txt b/worlds/witness/data/WitnessLogicVariety.txt index a3c388dfb1e4..31263aa33790 100644 --- a/worlds/witness/data/WitnessLogicVariety.txt +++ b/worlds/witness/data/WitnessLogicVariety.txt @@ -1,7 +1,5 @@ ==Tutorial (Inside)== -Menu (Menu) - Entry - True: - Entry (Entry): Tutorial First Hallway (Tutorial First Hallway) - Entry - True - Tutorial First Hallway Room - 0x00064: diff --git a/worlds/witness/hints.py b/worlds/witness/hints.py index 99e8eea2eb89..dac7e3fb4d05 100644 --- a/worlds/witness/hints.py +++ b/worlds/witness/hints.py @@ -250,8 +250,11 @@ def word_direct_hint(world: "WitnessWorld", hint: WitnessLocationHint) -> Witnes elif group_type == "Group": location_name = f"a \"{chosen_group}\" location in {player_name}'s world" elif group_type == "Region": - if chosen_group == "Menu": - location_name = f"a location near the start of {player_name}'s game (\"Menu\" region)" + origin_region_name = world.multiworld.worlds[hint.location.player].origin_region_name + if chosen_group == origin_region_name: + location_name = ( + f"a location in the origin region of {player_name}'s world (\"{origin_region_name}\" region)" + ) else: location_name = f"a location in {player_name}'s \"{chosen_group}\" region" diff --git a/worlds/witness/options.py b/worlds/witness/options.py index 1711fe2cae8c..6302ac6e5052 100644 --- a/worlds/witness/options.py +++ b/worlds/witness/options.py @@ -422,6 +422,17 @@ class DeathLinkAmnesty(Range): default = 1 +class PuzzleRandomizationSeed(Range): + """ + Sigma Rando, which is the basis for all puzzle randomization in this randomizer, uses a seed from 1 to 9999999 for the puzzle randomization. + This option lets you set this seed yourself. + """ + display_name = "Puzzle Randomization Seed" + range_start = 1 + range_end = 9999999 + default = "random" + + @dataclass class TheWitnessOptions(PerGameCommonOptions): puzzle_randomization: PuzzleRandomization @@ -457,6 +468,7 @@ class TheWitnessOptions(PerGameCommonOptions): laser_hints: LaserHints death_link: DeathLink death_link_amnesty: DeathLinkAmnesty + puzzle_randomization_seed: PuzzleRandomizationSeed shuffle_dog: ShuffleDog @@ -467,7 +479,7 @@ class TheWitnessOptions(PerGameCommonOptions): MountainLasers, ChallengeLasers, ]), - OptionGroup("Panel Hunt Settings", [ + OptionGroup("Panel Hunt Options", [ PanelHuntRequiredPercentage, PanelHuntTotal, PanelHuntPostgame, @@ -506,6 +518,7 @@ class TheWitnessOptions(PerGameCommonOptions): ElevatorsComeToYou, DeathLink, DeathLinkAmnesty, + PuzzleRandomizationSeed, ]), OptionGroup("Silly Options", [ ShuffleDog, diff --git a/worlds/yachtdice/Items.py b/worlds/yachtdice/Items.py index c76dc538146e..d6488498f51a 100644 --- a/worlds/yachtdice/Items.py +++ b/worlds/yachtdice/Items.py @@ -16,7 +16,7 @@ class YachtDiceItem(Item): item_table = { - "Dice": ItemData(16871244000, ItemClassification.progression), + "Dice": ItemData(16871244000, ItemClassification.progression | ItemClassification.useful), "Dice Fragment": ItemData(16871244001, ItemClassification.progression), "Roll": ItemData(16871244002, ItemClassification.progression), "Roll Fragment": ItemData(16871244003, ItemClassification.progression), @@ -64,7 +64,7 @@ class YachtDiceItem(Item): # These points are included in the logic and might be necessary to progress. "1 Point": ItemData(16871244301, ItemClassification.progression_skip_balancing), "10 Points": ItemData(16871244302, ItemClassification.progression), - "100 Points": ItemData(16871244303, ItemClassification.progression), + "100 Points": ItemData(16871244303, ItemClassification.progression | ItemClassification.useful), } # item groups for better hinting diff --git a/worlds/yachtdice/Options.py b/worlds/yachtdice/Options.py index e687936224c3..f311caa5a993 100644 --- a/worlds/yachtdice/Options.py +++ b/worlds/yachtdice/Options.py @@ -80,7 +80,7 @@ class NumberRollFragmentsPerRoll(Range): """ Rolls can be split into fragments, gathering enough will give you an extra roll. You start with one roll, and there will always be one full roll in the pool. - The other three rolls are split into fragments, according to this option. + The other rolls are split into fragments, according to this option. Setting this to 1 fragment per roll just puts "Roll" objects in the pool. """ diff --git a/worlds/yachtdice/Rules.py b/worlds/yachtdice/Rules.py index d99f5b147493..3fb712fdca09 100644 --- a/worlds/yachtdice/Rules.py +++ b/worlds/yachtdice/Rules.py @@ -101,14 +101,15 @@ def dice_simulation_strings(categories, num_dice, num_rolls, fixed_mult, step_mu return yachtdice_cache[player][tup] # sort categories because for the step multiplier, you will want low-scoring categories first - categories.sort(key=lambda category: category.mean_score(num_dice, num_rolls)) + # to avoid errors with order changing when obtaining rolls, we order assuming 4 rolls + categories.sort(key=lambda category: category.mean_score(num_dice, 4)) # function to add two discrete distribution. # defaultdict is a dict where you don't need to check if an id is present, you can just use += (lot faster) def add_distributions(dist1, dist2): combined_dist = defaultdict(float) - for val1, prob1 in dist1.items(): - for val2, prob2 in dist2.items(): + for val2, prob2 in dist2.items(): + for val1, prob1 in dist1.items(): combined_dist[val1 + val2] += prob1 * prob2 return dict(combined_dist) diff --git a/worlds/yachtdice/YachtWeights.py b/worlds/yachtdice/YachtWeights.py index 5f647f3420ba..f18766d9498a 100644 --- a/worlds/yachtdice/YachtWeights.py +++ b/worlds/yachtdice/YachtWeights.py @@ -1,11 +1,3 @@ -# A file containing the results of our simulations. -# Every entry consists of a key. This key has input category, number of dice, and number of rolls. -# The value then shows a list of all possible scores to get, and how many times of 100000 it achieved. - -# example: ("Category Choice", 2, 2): -# {8: 13639, 9: 12220, 10: 13755, 5: 4889, 6: 9840, 7: 14772, 12: 7780, 11: 15622, 2: 1269, 3: 2445, 4: 3769} -# this example shows the outcomes for the category "Category Choice", with 2 dice and 2 rolls. -# 13639 out of 100000 times, a score of 8 was achieved for example. yacht_weights = { ("Category Ones", 0, 0): {0: 100000}, ("Category Ones", 0, 1): {0: 100000}, @@ -30,64 +22,64 @@ ("Category Ones", 2, 2): {0: 100000}, ("Category Ones", 2, 3): {0: 33544, 1: 66456}, ("Category Ones", 2, 4): {0: 23342, 1: 76658}, - ("Category Ones", 2, 5): {0: 16036, 2: 83964}, - ("Category Ones", 2, 6): {0: 11355, 2: 88645}, - ("Category Ones", 2, 7): {0: 7812, 2: 92188}, - ("Category Ones", 2, 8): {0: 5395, 2: 94605}, + ("Category Ones", 2, 5): {0: 16036, 1: 83964}, + ("Category Ones", 2, 6): {0: 11355, 1: 88645}, + ("Category Ones", 2, 7): {0: 7812, 1: 92188}, + ("Category Ones", 2, 8): {0: 5395, 1: 94605}, ("Category Ones", 3, 0): {0: 100000}, ("Category Ones", 3, 1): {0: 100000}, ("Category Ones", 3, 2): {0: 33327, 1: 66673}, - ("Category Ones", 3, 3): {0: 19432, 2: 80568}, - ("Category Ones", 3, 4): {0: 11191, 2: 88809}, - ("Category Ones", 3, 5): {0: 35427, 2: 64573}, - ("Category Ones", 3, 6): {0: 26198, 2: 73802}, - ("Category Ones", 3, 7): {0: 18851, 3: 81149}, - ("Category Ones", 3, 8): {0: 13847, 3: 86153}, + ("Category Ones", 3, 3): {0: 19432, 1: 80568}, + ("Category Ones", 3, 4): {0: 11191, 1: 88809}, + ("Category Ones", 3, 5): {0: 3963, 2: 64583, 1: 31454}, + ("Category Ones", 3, 6): {0: 3286, 2: 96714}, + ("Category Ones", 3, 7): {0: 57, 2: 99943}, + ("Category Ones", 3, 8): {2: 100000}, ("Category Ones", 4, 0): {0: 100000}, ("Category Ones", 4, 1): {0: 100000}, - ("Category Ones", 4, 2): {0: 23349, 2: 76651}, - ("Category Ones", 4, 3): {0: 11366, 2: 88634}, - ("Category Ones", 4, 4): {0: 28572, 3: 71428}, - ("Category Ones", 4, 5): {0: 17976, 3: 82024}, - ("Category Ones", 4, 6): {0: 1253, 3: 98747}, - ("Category Ones", 4, 7): {0: 31228, 3: 68772}, - ("Category Ones", 4, 8): {0: 23273, 4: 76727}, + ("Category Ones", 4, 2): {0: 23349, 1: 76651}, + ("Category Ones", 4, 3): {0: 11366, 1: 88634}, + ("Category Ones", 4, 4): {0: 3246, 2: 71438, 1: 25316}, + ("Category Ones", 4, 5): {0: 1466, 2: 98534}, + ("Category Ones", 4, 6): {0: 7, 2: 99993}, + ("Category Ones", 4, 7): {0: 2, 2: 31222, 3: 68776}, + ("Category Ones", 4, 8): {3: 99999, 2: 1}, ("Category Ones", 5, 0): {0: 100000}, ("Category Ones", 5, 1): {0: 100000}, - ("Category Ones", 5, 2): {0: 16212, 2: 83788}, - ("Category Ones", 5, 3): {0: 30104, 3: 69896}, - ("Category Ones", 5, 4): {0: 2552, 3: 97448}, - ("Category Ones", 5, 5): {0: 32028, 4: 67972}, - ("Category Ones", 5, 6): {0: 21215, 4: 78785}, - ("Category Ones", 5, 7): {0: 2295, 4: 97705}, - ("Category Ones", 5, 8): {0: 1167, 4: 98833}, + ("Category Ones", 5, 2): {0: 16212, 1: 83788}, + ("Category Ones", 5, 3): {0: 4879, 2: 69906, 1: 25215}, + ("Category Ones", 5, 4): {0: 1513, 2: 98487}, + ("Category Ones", 5, 5): {0: 484, 2: 31541, 3: 67975}, + ("Category Ones", 5, 6): {3: 99785, 2: 215}, + ("Category Ones", 5, 7): {3: 100000}, + ("Category Ones", 5, 8): {4: 66815, 3: 33185}, ("Category Ones", 6, 0): {0: 100000}, ("Category Ones", 6, 1): {0: 33501, 1: 66499}, - ("Category Ones", 6, 2): {0: 40705, 2: 59295}, - ("Category Ones", 6, 3): {0: 3764, 3: 96236}, - ("Category Ones", 6, 4): {0: 9324, 4: 90676}, - ("Category Ones", 6, 5): {0: 4208, 4: 95792}, - ("Category Ones", 6, 6): {0: 158, 5: 99842}, - ("Category Ones", 6, 7): {0: 5503, 5: 94497}, - ("Category Ones", 6, 8): {0: 2896, 5: 97104}, + ("Category Ones", 6, 2): {0: 11326, 1: 88674}, + ("Category Ones", 6, 3): {0: 2289, 2: 79783, 1: 17928}, + ("Category Ones", 6, 4): {0: 10, 3: 68933, 2: 30973, 1: 84}, + ("Category Ones", 6, 5): {0: 4, 3: 99996}, + ("Category Ones", 6, 6): {2: 1, 4: 67785, 3: 32214}, + ("Category Ones", 6, 7): {4: 100000}, + ("Category Ones", 6, 8): {4: 100000}, ("Category Ones", 7, 0): {0: 100000}, - ("Category Ones", 7, 1): {0: 27838, 2: 72162}, - ("Category Ones", 7, 2): {0: 7796, 3: 92204}, - ("Category Ones", 7, 3): {0: 13389, 4: 86611}, - ("Category Ones", 7, 4): {0: 5252, 4: 94748}, - ("Category Ones", 7, 5): {0: 9854, 5: 90146}, - ("Category Ones", 7, 6): {0: 4625, 5: 95375}, - ("Category Ones", 7, 7): {0: 30339, 6: 69661}, - ("Category Ones", 7, 8): {0: 5519, 6: 94481}, + ("Category Ones", 7, 1): {0: 27838, 1: 72162}, + ("Category Ones", 7, 2): {0: 8807, 2: 68364, 1: 22829}, + ("Category Ones", 7, 3): {0: 75, 3: 62348, 2: 35246, 1: 2331}, + ("Category Ones", 7, 4): {0: 6, 3: 99994}, + ("Category Ones", 7, 5): {3: 29500, 4: 70500}, + ("Category Ones", 7, 6): {4: 100000}, + ("Category Ones", 7, 7): {4: 30322, 5: 69678}, + ("Category Ones", 7, 8): {5: 100000}, ("Category Ones", 8, 0): {0: 100000}, - ("Category Ones", 8, 1): {0: 23156, 2: 76844}, - ("Category Ones", 8, 2): {0: 5472, 3: 94528}, - ("Category Ones", 8, 3): {0: 8661, 4: 91339}, - ("Category Ones", 8, 4): {0: 12125, 5: 87875}, - ("Category Ones", 8, 5): {0: 5173, 5: 94827}, - ("Category Ones", 8, 6): {0: 8872, 6: 91128}, - ("Category Ones", 8, 7): {0: 4236, 6: 95764}, - ("Category Ones", 8, 8): {0: 9107, 7: 90893}, + ("Category Ones", 8, 1): {0: 23156, 1: 76844}, + ("Category Ones", 8, 2): {0: 5678, 2: 75480, 1: 18842}, + ("Category Ones", 8, 3): {0: 28, 3: 99972}, + ("Category Ones", 8, 4): {3: 32486, 4: 67514}, + ("Category Ones", 8, 5): {4: 100000}, + ("Category Ones", 8, 6): {5: 74125, 4: 25875}, + ("Category Ones", 8, 7): {6: 60476, 5: 29297, 4: 10227}, + ("Category Ones", 8, 8): {6: 99999, 5: 1}, ("Category Twos", 0, 0): {0: 100000}, ("Category Twos", 0, 1): {0: 100000}, ("Category Twos", 0, 2): {0: 100000}, @@ -99,7 +91,7 @@ ("Category Twos", 0, 8): {0: 100000}, ("Category Twos", 1, 0): {0: 100000}, ("Category Twos", 1, 1): {0: 100000}, - ("Category Twos", 1, 2): {0: 100000}, + ("Category Twos", 1, 2): {0: 69690, 2: 30310}, ("Category Twos", 1, 3): {0: 57818, 2: 42182}, ("Category Twos", 1, 4): {0: 48418, 2: 51582}, ("Category Twos", 1, 5): {0: 40301, 2: 59699}, @@ -107,68 +99,68 @@ ("Category Twos", 1, 7): {0: 28182, 2: 71818}, ("Category Twos", 1, 8): {0: 23406, 2: 76594}, ("Category Twos", 2, 0): {0: 100000}, - ("Category Twos", 2, 1): {0: 100000}, + ("Category Twos", 2, 1): {0: 69724, 2: 30276}, ("Category Twos", 2, 2): {0: 48238, 2: 51762}, - ("Category Twos", 2, 3): {0: 33290, 4: 66710}, - ("Category Twos", 2, 4): {0: 23136, 4: 76864}, - ("Category Twos", 2, 5): {0: 16146, 4: 83854}, - ("Category Twos", 2, 6): {0: 11083, 4: 88917}, - ("Category Twos", 2, 7): {0: 7662, 4: 92338}, - ("Category Twos", 2, 8): {0: 5354, 4: 94646}, + ("Category Twos", 2, 3): {0: 33290, 2: 66710}, + ("Category Twos", 2, 4): {0: 23136, 2: 76864}, + ("Category Twos", 2, 5): {0: 16146, 2: 48200, 4: 35654}, + ("Category Twos", 2, 6): {0: 11083, 2: 44497, 4: 44420}, + ("Category Twos", 2, 7): {0: 7662, 2: 40343, 4: 51995}, + ("Category Twos", 2, 8): {0: 5354, 2: 35526, 4: 59120}, ("Category Twos", 3, 0): {0: 100000}, ("Category Twos", 3, 1): {0: 58021, 2: 41979}, - ("Category Twos", 3, 2): {0: 33548, 4: 66452}, - ("Category Twos", 3, 3): {0: 19375, 4: 80625}, - ("Category Twos", 3, 4): {0: 10998, 4: 89002}, - ("Category Twos", 3, 5): {0: 6519, 6: 93481}, - ("Category Twos", 3, 6): {0: 3619, 6: 96381}, - ("Category Twos", 3, 7): {0: 2195, 6: 97805}, - ("Category Twos", 3, 8): {0: 13675, 6: 86325}, + ("Category Twos", 3, 2): {0: 33548, 2: 66452}, + ("Category Twos", 3, 3): {0: 19375, 2: 42372, 4: 38253}, + ("Category Twos", 3, 4): {0: 10998, 2: 36435, 4: 52567}, + ("Category Twos", 3, 5): {0: 7954, 4: 92046}, + ("Category Twos", 3, 6): {0: 347, 4: 99653}, + ("Category Twos", 3, 7): {0: 2, 4: 62851, 6: 37147}, + ("Category Twos", 3, 8): {6: 99476, 4: 524}, ("Category Twos", 4, 0): {0: 100000}, ("Category Twos", 4, 1): {0: 48235, 2: 51765}, - ("Category Twos", 4, 2): {0: 23289, 4: 76711}, - ("Category Twos", 4, 3): {0: 11177, 6: 88823}, - ("Category Twos", 4, 4): {0: 5499, 6: 94501}, - ("Category Twos", 4, 5): {0: 18356, 6: 81644}, - ("Category Twos", 4, 6): {0: 11169, 8: 88831}, - ("Category Twos", 4, 7): {0: 6945, 8: 93055}, - ("Category Twos", 4, 8): {0: 4091, 8: 95909}, + ("Category Twos", 4, 2): {0: 23289, 2: 40678, 4: 36033}, + ("Category Twos", 4, 3): {0: 11177, 2: 32677, 4: 56146}, + ("Category Twos", 4, 4): {0: 5522, 4: 60436, 6: 34042}, + ("Category Twos", 4, 5): {0: 4358, 6: 95642}, + ("Category Twos", 4, 6): {0: 20, 6: 99980}, + ("Category Twos", 4, 7): {6: 100000}, + ("Category Twos", 4, 8): {6: 65250, 8: 34750}, ("Category Twos", 5, 0): {0: 100000}, - ("Category Twos", 5, 1): {0: 40028, 4: 59972}, - ("Category Twos", 5, 2): {0: 16009, 6: 83991}, - ("Category Twos", 5, 3): {0: 6489, 6: 93511}, - ("Category Twos", 5, 4): {0: 16690, 8: 83310}, - ("Category Twos", 5, 5): {0: 9016, 8: 90984}, - ("Category Twos", 5, 6): {0: 4602, 8: 95398}, - ("Category Twos", 5, 7): {0: 13627, 10: 86373}, - ("Category Twos", 5, 8): {0: 8742, 10: 91258}, + ("Category Twos", 5, 1): {0: 40028, 2: 59972}, + ("Category Twos", 5, 2): {0: 16009, 2: 35901, 4: 48090}, + ("Category Twos", 5, 3): {0: 6820, 4: 57489, 6: 35691}, + ("Category Twos", 5, 4): {0: 5285, 6: 94715}, + ("Category Twos", 5, 5): {0: 18, 6: 66613, 8: 33369}, + ("Category Twos", 5, 6): {8: 99073, 6: 927}, + ("Category Twos", 5, 7): {8: 100000}, + ("Category Twos", 5, 8): {8: 100000}, ("Category Twos", 6, 0): {0: 100000}, - ("Category Twos", 6, 1): {0: 33502, 4: 66498}, - ("Category Twos", 6, 2): {0: 11210, 6: 88790}, - ("Category Twos", 6, 3): {0: 3673, 6: 96327}, - ("Category Twos", 6, 4): {0: 9291, 8: 90709}, - ("Category Twos", 6, 5): {0: 441, 8: 99559}, - ("Category Twos", 6, 6): {0: 10255, 10: 89745}, - ("Category Twos", 6, 7): {0: 5646, 10: 94354}, - ("Category Twos", 6, 8): {0: 14287, 12: 85713}, + ("Category Twos", 6, 1): {0: 33502, 2: 66498}, + ("Category Twos", 6, 2): {0: 13681, 4: 59162, 2: 27157}, + ("Category Twos", 6, 3): {0: 5486, 6: 94514}, + ("Category Twos", 6, 4): {0: 190, 6: 62108, 8: 37702}, + ("Category Twos", 6, 5): {8: 99882, 6: 118}, + ("Category Twos", 6, 6): {8: 65144, 10: 34856}, + ("Category Twos", 6, 7): {10: 99524, 8: 476}, + ("Category Twos", 6, 8): {10: 100000}, ("Category Twos", 7, 0): {0: 100000}, - ("Category Twos", 7, 1): {0: 27683, 4: 72317}, - ("Category Twos", 7, 2): {0: 7824, 6: 92176}, - ("Category Twos", 7, 3): {0: 13167, 8: 86833}, - ("Category Twos", 7, 4): {0: 564, 10: 99436}, - ("Category Twos", 7, 5): {0: 9824, 10: 90176}, - ("Category Twos", 7, 6): {0: 702, 12: 99298}, - ("Category Twos", 7, 7): {0: 10186, 12: 89814}, - ("Category Twos", 7, 8): {0: 942, 12: 99058}, + ("Category Twos", 7, 1): {0: 27683, 2: 39060, 4: 33257}, + ("Category Twos", 7, 2): {0: 8683, 4: 54932, 6: 36385}, + ("Category Twos", 7, 3): {0: 373, 6: 66572, 8: 33055}, + ("Category Twos", 7, 4): {8: 99816, 6: 184}, + ("Category Twos", 7, 5): {8: 58124, 10: 41876}, + ("Category Twos", 7, 6): {10: 99948, 8: 52}, + ("Category Twos", 7, 7): {10: 62549, 12: 37451}, + ("Category Twos", 7, 8): {12: 99818, 10: 182}, ("Category Twos", 8, 0): {0: 100000}, - ("Category Twos", 8, 1): {0: 23378, 4: 76622}, - ("Category Twos", 8, 2): {0: 5420, 8: 94580}, - ("Category Twos", 8, 3): {0: 8560, 10: 91440}, - ("Category Twos", 8, 4): {0: 12199, 12: 87801}, - ("Category Twos", 8, 5): {0: 879, 12: 99121}, - ("Category Twos", 8, 6): {0: 9033, 14: 90967}, - ("Category Twos", 8, 7): {0: 15767, 14: 84233}, - ("Category Twos", 8, 8): {2: 9033, 14: 90967}, + ("Category Twos", 8, 1): {0: 23378, 2: 37157, 4: 39465}, + ("Category Twos", 8, 2): {0: 5602, 6: 94398}, + ("Category Twos", 8, 3): {0: 8, 6: 10911, 8: 89081}, + ("Category Twos", 8, 4): {8: 59809, 10: 40191}, + ("Category Twos", 8, 5): {10: 68808, 12: 31114, 8: 78}, + ("Category Twos", 8, 6): {12: 98712, 10: 1287, 8: 1}, + ("Category Twos", 8, 7): {12: 100000}, + ("Category Twos", 8, 8): {12: 59018, 14: 40982}, ("Category Threes", 0, 0): {0: 100000}, ("Category Threes", 0, 1): {0: 100000}, ("Category Threes", 0, 2): {0: 100000}, @@ -190,66 +182,66 @@ ("Category Threes", 2, 0): {0: 100000}, ("Category Threes", 2, 1): {0: 69419, 3: 30581}, ("Category Threes", 2, 2): {0: 48202, 3: 51798}, - ("Category Threes", 2, 3): {0: 33376, 6: 66624}, - ("Category Threes", 2, 4): {0: 23276, 6: 76724}, - ("Category Threes", 2, 5): {0: 16092, 6: 83908}, - ("Category Threes", 2, 6): {0: 11232, 6: 88768}, - ("Category Threes", 2, 7): {0: 7589, 6: 92411}, - ("Category Threes", 2, 8): {0: 5447, 6: 94553}, + ("Category Threes", 2, 3): {0: 33376, 3: 66624}, + ("Category Threes", 2, 4): {0: 23276, 3: 49810, 6: 26914}, + ("Category Threes", 2, 5): {0: 16092, 3: 47718, 6: 36190}, + ("Category Threes", 2, 6): {0: 11232, 3: 44515, 6: 44253}, + ("Category Threes", 2, 7): {0: 7589, 3: 40459, 6: 51952}, + ("Category Threes", 2, 8): {0: 5447, 3: 35804, 6: 58749}, ("Category Threes", 3, 0): {0: 100000}, ("Category Threes", 3, 1): {0: 57964, 3: 42036}, - ("Category Threes", 3, 2): {0: 33637, 6: 66363}, - ("Category Threes", 3, 3): {0: 19520, 6: 80480}, - ("Category Threes", 3, 4): {0: 11265, 6: 88735}, - ("Category Threes", 3, 5): {0: 6419, 6: 72177, 9: 21404}, - ("Category Threes", 3, 6): {0: 3810, 6: 66884, 9: 29306}, - ("Category Threes", 3, 7): {0: 2174, 6: 60595, 9: 37231}, - ("Category Threes", 3, 8): {0: 1237, 6: 53693, 9: 45070}, + ("Category Threes", 3, 2): {0: 33637, 3: 44263, 6: 22100}, + ("Category Threes", 3, 3): {0: 19520, 3: 42382, 6: 38098}, + ("Category Threes", 3, 4): {0: 11265, 3: 35772, 6: 52963}, + ("Category Threes", 3, 5): {0: 6419, 3: 28916, 6: 43261, 9: 21404}, + ("Category Threes", 3, 6): {0: 3810, 3: 22496, 6: 44388, 9: 29306}, + ("Category Threes", 3, 7): {0: 1317, 6: 30047, 9: 68636}, + ("Category Threes", 3, 8): {0: 750, 9: 99250}, ("Category Threes", 4, 0): {0: 100000}, - ("Category Threes", 4, 1): {0: 48121, 6: 51879}, - ("Category Threes", 4, 2): {0: 23296, 6: 76704}, - ("Category Threes", 4, 3): {0: 11233, 6: 68363, 9: 20404}, - ("Category Threes", 4, 4): {0: 5463, 6: 60738, 9: 33799}, - ("Category Threes", 4, 5): {0: 2691, 6: 50035, 12: 47274}, - ("Category Threes", 4, 6): {0: 11267, 9: 88733}, - ("Category Threes", 4, 7): {0: 6921, 9: 66034, 12: 27045}, - ("Category Threes", 4, 8): {0: 4185, 9: 61079, 12: 34736}, + ("Category Threes", 4, 1): {0: 48121, 3: 51879}, + ("Category Threes", 4, 2): {0: 23296, 3: 40989, 6: 35715}, + ("Category Threes", 4, 3): {0: 11233, 3: 32653, 6: 35710, 9: 20404}, + ("Category Threes", 4, 4): {0: 5463, 3: 23270, 6: 37468, 9: 33799}, + ("Category Threes", 4, 5): {0: 5225, 6: 29678, 9: 65097}, + ("Category Threes", 4, 6): {0: 3535, 9: 96465}, + ("Category Threes", 4, 7): {0: 6, 9: 72939, 12: 27055}, + ("Category Threes", 4, 8): {9: 25326, 12: 74674}, ("Category Threes", 5, 0): {0: 100000}, - ("Category Threes", 5, 1): {0: 40183, 6: 59817}, - ("Category Threes", 5, 2): {0: 16197, 6: 83803}, - ("Category Threes", 5, 3): {0: 6583, 6: 57826, 9: 35591}, - ("Category Threes", 5, 4): {0: 2636, 9: 76577, 12: 20787}, - ("Category Threes", 5, 5): {0: 8879, 9: 57821, 12: 33300}, - ("Category Threes", 5, 6): {0: 4652, 12: 95348}, - ("Category Threes", 5, 7): {0: 2365, 12: 97635}, - ("Category Threes", 5, 8): {0: 8671, 12: 64865, 15: 26464}, + ("Category Threes", 5, 1): {0: 40183, 3: 59817}, + ("Category Threes", 5, 2): {0: 16197, 3: 35494, 6: 48309}, + ("Category Threes", 5, 3): {0: 6583, 3: 23394, 6: 34432, 9: 35591}, + ("Category Threes", 5, 4): {0: 5007, 6: 25159, 9: 49038, 12: 20796}, + ("Category Threes", 5, 5): {0: 2900, 9: 38935, 12: 58165}, + ("Category Threes", 5, 6): {0: 2090, 12: 97910}, + ("Category Threes", 5, 7): {12: 99994, 9: 6}, + ("Category Threes", 5, 8): {12: 73524, 15: 26476}, ("Category Threes", 6, 0): {0: 100000}, - ("Category Threes", 6, 1): {0: 33473, 6: 66527}, - ("Category Threes", 6, 2): {0: 11147, 6: 62222, 9: 26631}, - ("Category Threes", 6, 3): {0: 3628, 9: 75348, 12: 21024}, - ("Category Threes", 6, 4): {0: 9498, 9: 52940, 15: 37562}, - ("Category Threes", 6, 5): {0: 4236, 12: 72944, 15: 22820}, - ("Category Threes", 6, 6): {0: 10168, 12: 55072, 15: 34760}, - ("Category Threes", 6, 7): {0: 5519, 15: 94481}, - ("Category Threes", 6, 8): {0: 2968, 15: 76504, 18: 20528}, + ("Category Threes", 6, 1): {0: 33473, 3: 40175, 6: 26352}, + ("Category Threes", 6, 2): {0: 11147, 3: 29592, 6: 32630, 9: 26631}, + ("Category Threes", 6, 3): {0: 2460, 6: 21148, 9: 55356, 12: 21036}, + ("Category Threes", 6, 4): {0: 997, 9: 29741, 12: 69262}, + ("Category Threes", 6, 5): {0: 831, 12: 76328, 15: 22841}, + ("Category Threes", 6, 6): {12: 29960, 15: 70040}, + ("Category Threes", 6, 7): {15: 100000}, + ("Category Threes", 6, 8): {15: 79456, 18: 20544}, ("Category Threes", 7, 0): {0: 100000}, - ("Category Threes", 7, 1): {0: 27933, 6: 72067}, - ("Category Threes", 7, 2): {0: 7794, 6: 55728, 12: 36478}, - ("Category Threes", 7, 3): {0: 2138, 9: 64554, 15: 33308}, - ("Category Threes", 7, 4): {0: 5238, 12: 69214, 15: 25548}, - ("Category Threes", 7, 5): {0: 9894, 15: 90106}, - ("Category Threes", 7, 6): {0: 4656, 15: 69353, 18: 25991}, - ("Category Threes", 7, 7): {0: 10005, 15: 52430, 18: 37565}, - ("Category Threes", 7, 8): {0: 5710, 18: 94290}, + ("Category Threes", 7, 1): {0: 27933, 3: 39105, 6: 32962}, + ("Category Threes", 7, 2): {0: 7794, 3: 23896, 6: 31832, 9: 36478}, + ("Category Threes", 7, 3): {0: 1321, 9: 40251, 12: 58428}, + ("Category Threes", 7, 4): {0: 370, 12: 74039, 15: 25591}, + ("Category Threes", 7, 5): {0: 6, 15: 98660, 12: 1334}, + ("Category Threes", 7, 6): {15: 73973, 18: 26027}, + ("Category Threes", 7, 7): {18: 100000}, + ("Category Threes", 7, 8): {18: 100000}, ("Category Threes", 8, 0): {0: 100000}, - ("Category Threes", 8, 1): {0: 23337, 6: 76663}, - ("Category Threes", 8, 2): {0: 5310, 9: 74178, 12: 20512}, - ("Category Threes", 8, 3): {0: 8656, 12: 70598, 15: 20746}, - ("Category Threes", 8, 4): {0: 291, 12: 59487, 18: 40222}, - ("Category Threes", 8, 5): {0: 5145, 15: 63787, 18: 31068}, - ("Category Threes", 8, 6): {0: 8804, 18: 91196}, - ("Category Threes", 8, 7): {0: 4347, 18: 65663, 21: 29990}, - ("Category Threes", 8, 8): {0: 9252, 21: 90748}, + ("Category Threes", 8, 1): {0: 23337, 3: 37232, 6: 39431}, + ("Category Threes", 8, 2): {0: 4652, 6: 29310, 9: 45517, 12: 20521}, + ("Category Threes", 8, 3): {0: 1300, 12: 77919, 15: 20781}, + ("Category Threes", 8, 4): {0: 21, 15: 98678, 12: 1301}, + ("Category Threes", 8, 5): {15: 68893, 18: 31107}, + ("Category Threes", 8, 6): {18: 100000}, + ("Category Threes", 8, 7): {18: 69986, 21: 30014}, + ("Category Threes", 8, 8): {21: 98839, 18: 1161}, ("Category Fours", 0, 0): {0: 100000}, ("Category Fours", 0, 1): {0: 100000}, ("Category Fours", 0, 2): {0: 100000}, @@ -276,61 +268,61 @@ ("Category Fours", 2, 5): {0: 16222, 4: 48009, 8: 35769}, ("Category Fours", 2, 6): {0: 11125, 4: 44400, 8: 44475}, ("Category Fours", 2, 7): {0: 7919, 4: 40216, 8: 51865}, - ("Category Fours", 2, 8): {0: 5348, 8: 94652}, + ("Category Fours", 2, 8): {0: 5348, 4: 35757, 8: 58895}, ("Category Fours", 3, 0): {0: 100000}, ("Category Fours", 3, 1): {0: 57914, 4: 42086}, ("Category Fours", 3, 2): {0: 33621, 4: 44110, 8: 22269}, ("Category Fours", 3, 3): {0: 19153, 4: 42425, 8: 38422}, - ("Category Fours", 3, 4): {0: 11125, 8: 88875}, - ("Category Fours", 3, 5): {0: 6367, 8: 72308, 12: 21325}, - ("Category Fours", 3, 6): {0: 3643, 8: 66934, 12: 29423}, - ("Category Fours", 3, 7): {0: 2178, 8: 60077, 12: 37745}, - ("Category Fours", 3, 8): {0: 1255, 8: 53433, 12: 45312}, + ("Category Fours", 3, 4): {0: 11125, 4: 36011, 8: 52864}, + ("Category Fours", 3, 5): {0: 6367, 4: 29116, 8: 43192, 12: 21325}, + ("Category Fours", 3, 6): {0: 3643, 4: 22457, 8: 44477, 12: 29423}, + ("Category Fours", 3, 7): {0: 2178, 4: 16802, 8: 43275, 12: 37745}, + ("Category Fours", 3, 8): {0: 488, 8: 20703, 12: 78809}, ("Category Fours", 4, 0): {0: 100000}, ("Category Fours", 4, 1): {0: 48465, 4: 51535}, - ("Category Fours", 4, 2): {0: 23296, 4: 40911, 12: 35793}, - ("Category Fours", 4, 3): {0: 11200, 8: 68528, 12: 20272}, - ("Category Fours", 4, 4): {0: 5447, 8: 60507, 12: 34046}, - ("Category Fours", 4, 5): {0: 2533, 8: 50449, 16: 47018}, - ("Category Fours", 4, 6): {0: 1314, 8: 39851, 12: 39425, 16: 19410}, - ("Category Fours", 4, 7): {0: 6823, 12: 66167, 16: 27010}, - ("Category Fours", 4, 8): {0: 4189, 12: 61034, 16: 34777}, + ("Category Fours", 4, 2): {0: 23296, 4: 40911, 8: 35793}, + ("Category Fours", 4, 3): {0: 11200, 4: 33191, 8: 35337, 12: 20272}, + ("Category Fours", 4, 4): {0: 5447, 4: 23066, 8: 37441, 12: 34046}, + ("Category Fours", 4, 5): {0: 2533, 4: 15668, 8: 34781, 12: 47018}, + ("Category Fours", 4, 6): {0: 2058, 8: 19749, 12: 58777, 16: 19416}, + ("Category Fours", 4, 7): {0: 1476, 12: 45913, 16: 52611}, + ("Category Fours", 4, 8): {0: 23, 12: 18149, 16: 81828}, ("Category Fours", 5, 0): {0: 100000}, ("Category Fours", 5, 1): {0: 40215, 4: 40127, 8: 19658}, - ("Category Fours", 5, 2): {0: 15946, 8: 66737, 12: 17317}, - ("Category Fours", 5, 3): {0: 6479, 8: 58280, 16: 35241}, - ("Category Fours", 5, 4): {0: 2635, 8: 43968, 16: 53397}, - ("Category Fours", 5, 5): {0: 8916, 12: 57586, 16: 33498}, - ("Category Fours", 5, 6): {0: 4682, 12: 49435, 20: 45883}, - ("Category Fours", 5, 7): {0: 2291, 12: 40537, 16: 37701, 20: 19471}, - ("Category Fours", 5, 8): {0: 75, 16: 73483, 20: 26442}, + ("Category Fours", 5, 2): {0: 15946, 4: 35579, 8: 31158, 12: 17317}, + ("Category Fours", 5, 3): {0: 6479, 4: 23705, 8: 34575, 12: 35241}, + ("Category Fours", 5, 4): {0: 4987, 8: 25190, 12: 48849, 16: 20974}, + ("Category Fours", 5, 5): {0: 1553, 12: 39966, 16: 58481}, + ("Category Fours", 5, 6): {0: 843, 16: 99157}, + ("Category Fours", 5, 7): {16: 80514, 20: 19486}, + ("Category Fours", 5, 8): {16: 38393, 20: 61607}, ("Category Fours", 6, 0): {0: 100000}, ("Category Fours", 6, 1): {0: 33632, 4: 39856, 8: 26512}, - ("Category Fours", 6, 2): {0: 11175, 8: 62205, 12: 26620}, - ("Category Fours", 6, 3): {0: 3698, 8: 46268, 16: 50034}, - ("Category Fours", 6, 4): {0: 9173, 12: 52855, 20: 37972}, - ("Category Fours", 6, 5): {0: 4254, 12: 41626, 20: 54120}, - ("Category Fours", 6, 6): {0: 1783, 16: 63190, 24: 35027}, - ("Category Fours", 6, 7): {0: 5456, 16: 47775, 24: 46769}, - ("Category Fours", 6, 8): {0: 2881, 16: 39229, 24: 57890}, + ("Category Fours", 6, 2): {0: 11175, 4: 29824, 8: 32381, 12: 26620}, + ("Category Fours", 6, 3): {0: 3698, 4: 16329, 8: 29939, 12: 29071, 16: 20963}, + ("Category Fours", 6, 4): {0: 2326, 12: 28286, 16: 69388}, + ("Category Fours", 6, 5): {0: 1030, 16: 76056, 20: 22914}, + ("Category Fours", 6, 6): {0: 7, 16: 29753, 20: 70240}, + ("Category Fours", 6, 7): {20: 99999, 16: 1}, + ("Category Fours", 6, 8): {20: 79470, 24: 20530}, ("Category Fours", 7, 0): {0: 100000}, - ("Category Fours", 7, 1): {0: 27821, 4: 39289, 12: 32890}, - ("Category Fours", 7, 2): {0: 7950, 8: 55659, 16: 36391}, - ("Category Fours", 7, 3): {0: 2194, 12: 64671, 20: 33135}, - ("Category Fours", 7, 4): {0: 5063, 12: 41118, 20: 53819}, - ("Category Fours", 7, 5): {0: 171, 16: 57977, 24: 41852}, - ("Category Fours", 7, 6): {0: 4575, 16: 38694, 24: 56731}, - ("Category Fours", 7, 7): {0: 252, 20: 62191, 28: 37557}, - ("Category Fours", 7, 8): {4: 5576, 20: 45351, 28: 49073}, + ("Category Fours", 7, 1): {0: 27821, 4: 39289, 8: 32890}, + ("Category Fours", 7, 2): {0: 7950, 4: 24026, 8: 31633, 12: 36391}, + ("Category Fours", 7, 3): {0: 1887, 12: 31108, 16: 67005}, + ("Category Fours", 7, 4): {0: 423, 16: 73837, 20: 25740}, + ("Category Fours", 7, 5): {0: 57, 16: 10063, 20: 74092, 24: 15788}, + ("Category Fours", 7, 6): {0: 6, 20: 31342, 24: 68652}, + ("Category Fours", 7, 7): {24: 99995, 20: 5}, + ("Category Fours", 7, 8): {24: 84330, 28: 15670}, ("Category Fours", 8, 0): {0: 100000}, - ("Category Fours", 8, 1): {0: 23275, 8: 76725}, - ("Category Fours", 8, 2): {0: 5421, 8: 48273, 16: 46306}, - ("Category Fours", 8, 3): {0: 8626, 12: 45516, 20: 45858}, - ("Category Fours", 8, 4): {0: 2852, 16: 56608, 24: 40540}, - ("Category Fours", 8, 5): {0: 5049, 20: 63834, 28: 31117}, - ("Category Fours", 8, 6): {0: 269, 20: 53357, 28: 46374}, - ("Category Fours", 8, 7): {0: 4394, 24: 65785, 28: 29821}, - ("Category Fours", 8, 8): {0: 266, 24: 58443, 32: 41291}, + ("Category Fours", 8, 1): {0: 23275, 4: 37161, 8: 39564}, + ("Category Fours", 8, 2): {0: 5421, 4: 19014, 8: 29259, 12: 25812, 16: 20494}, + ("Category Fours", 8, 3): {0: 649, 16: 78572, 20: 20779}, + ("Category Fours", 8, 4): {0: 15, 20: 80772, 24: 17355, 16: 1858}, + ("Category Fours", 8, 5): {20: 15615, 24: 84385}, + ("Category Fours", 8, 6): {24: 80655, 28: 19345}, + ("Category Fours", 8, 7): {24: 23969, 28: 76031}, + ("Category Fours", 8, 8): {28: 100000}, ("Category Fives", 0, 0): {0: 100000}, ("Category Fives", 0, 1): {0: 100000}, ("Category Fives", 0, 2): {0: 100000}, @@ -363,55 +355,55 @@ ("Category Fives", 3, 2): {0: 33466, 5: 44227, 10: 22307}, ("Category Fives", 3, 3): {0: 19231, 5: 42483, 10: 38286}, ("Category Fives", 3, 4): {0: 11196, 5: 36192, 10: 38673, 15: 13939}, - ("Category Fives", 3, 5): {0: 6561, 10: 72177, 15: 21262}, - ("Category Fives", 3, 6): {0: 3719, 10: 66792, 15: 29489}, - ("Category Fives", 3, 7): {0: 2099, 10: 60283, 15: 37618}, - ("Category Fives", 3, 8): {0: 1281, 10: 53409, 15: 45310}, + ("Category Fives", 3, 5): {0: 6561, 5: 29163, 10: 43014, 15: 21262}, + ("Category Fives", 3, 6): {0: 3719, 5: 22181, 10: 44611, 15: 29489}, + ("Category Fives", 3, 7): {0: 2099, 5: 16817, 10: 43466, 15: 37618}, + ("Category Fives", 3, 8): {0: 1281, 5: 12473, 10: 40936, 15: 45310}, ("Category Fives", 4, 0): {0: 100000}, ("Category Fives", 4, 1): {0: 48377, 5: 38345, 10: 13278}, - ("Category Fives", 4, 2): {0: 23126, 5: 40940, 15: 35934}, + ("Category Fives", 4, 2): {0: 23126, 5: 40940, 10: 35934}, ("Category Fives", 4, 3): {0: 11192, 5: 32597, 10: 35753, 15: 20458}, - ("Category Fives", 4, 4): {0: 5362, 10: 60452, 20: 34186}, - ("Category Fives", 4, 5): {0: 2655, 10: 50264, 15: 34186, 20: 12895}, - ("Category Fives", 4, 6): {0: 1291, 10: 39792, 15: 39417, 20: 19500}, - ("Category Fives", 4, 7): {0: 6854, 15: 66139, 20: 27007}, - ("Category Fives", 4, 8): {0: 4150, 15: 61121, 20: 34729}, + ("Category Fives", 4, 4): {0: 5362, 5: 23073, 10: 37379, 15: 34186}, + ("Category Fives", 4, 5): {0: 2655, 5: 15662, 10: 34602, 15: 34186, 20: 12895}, + ("Category Fives", 4, 6): {0: 2059, 10: 19678, 15: 48376, 20: 29887}, + ("Category Fives", 4, 7): {0: 1473, 15: 34402, 20: 64125}, + ("Category Fives", 4, 8): {0: 551, 20: 99449}, ("Category Fives", 5, 0): {0: 100000}, ("Category Fives", 5, 1): {0: 39911, 5: 40561, 10: 19528}, ("Category Fives", 5, 2): {0: 16178, 5: 35517, 10: 31246, 15: 17059}, - ("Category Fives", 5, 3): {0: 6526, 10: 58146, 20: 35328}, - ("Category Fives", 5, 4): {0: 2615, 10: 44108, 15: 32247, 20: 21030}, - ("Category Fives", 5, 5): {0: 1063, 10: 31079, 15: 34489, 25: 33369}, - ("Category Fives", 5, 6): {0: 4520, 15: 49551, 20: 32891, 25: 13038}, - ("Category Fives", 5, 7): {0: 2370, 15: 40714, 20: 37778, 25: 19138}, - ("Category Fives", 5, 8): {0: 1179, 15: 31909, 20: 40615, 25: 26297}, + ("Category Fives", 5, 3): {0: 6526, 5: 23716, 10: 34430, 15: 35328}, + ("Category Fives", 5, 4): {0: 2615, 5: 13975, 10: 30133, 15: 32247, 20: 21030}, + ("Category Fives", 5, 5): {0: 1482, 10: 13532, 15: 37597, 20: 47389}, + ("Category Fives", 5, 6): {0: 477, 15: 14484, 20: 71985, 25: 13054}, + ("Category Fives", 5, 7): {0: 273, 20: 52865, 25: 46862}, + ("Category Fives", 5, 8): {20: 16822, 25: 83178}, ("Category Fives", 6, 0): {0: 100000}, ("Category Fives", 6, 1): {0: 33476, 5: 40167, 10: 26357}, - ("Category Fives", 6, 2): {0: 11322, 10: 62277, 20: 26401}, - ("Category Fives", 6, 3): {0: 3765, 10: 46058, 20: 50177}, - ("Category Fives", 6, 4): {0: 1201, 15: 60973, 25: 37826}, - ("Category Fives", 6, 5): {0: 4307, 15: 41966, 20: 30800, 25: 22927}, - ("Category Fives", 6, 6): {0: 1827, 15: 30580, 20: 32744, 30: 34849}, - ("Category Fives", 6, 7): {0: 5496, 20: 47569, 25: 32784, 30: 14151}, - ("Category Fives", 6, 8): {0: 2920, 20: 39283, 25: 37178, 30: 20619}, + ("Category Fives", 6, 2): {0: 11322, 5: 29613, 10: 32664, 15: 26401}, + ("Category Fives", 6, 3): {0: 3765, 5: 16288, 10: 29770, 15: 29233, 20: 20944}, + ("Category Fives", 6, 4): {0: 1889, 10: 13525, 15: 33731, 20: 38179, 25: 12676}, + ("Category Fives", 6, 5): {0: 53, 10: 11118, 20: 47588, 25: 41241}, + ("Category Fives", 6, 6): {0: 10, 20: 8876, 25: 91114}, + ("Category Fives", 6, 7): {0: 7, 25: 85815, 30: 14178}, + ("Category Fives", 6, 8): {25: 43072, 30: 56928}, ("Category Fives", 7, 0): {0: 100000}, - ("Category Fives", 7, 1): {0: 27826, 5: 39154, 15: 33020}, - ("Category Fives", 7, 2): {0: 7609, 10: 55915, 20: 36476}, - ("Category Fives", 7, 3): {0: 2262, 10: 35456, 20: 62282}, - ("Category Fives", 7, 4): {0: 5201, 15: 40920, 25: 53879}, - ("Category Fives", 7, 5): {0: 1890, 20: 56509, 30: 41601}, - ("Category Fives", 7, 6): {0: 4506, 20: 38614, 25: 30456, 30: 26424}, - ("Category Fives", 7, 7): {0: 2107, 25: 60445, 35: 37448}, - ("Category Fives", 7, 8): {0: 5627, 25: 45590, 30: 33015, 35: 15768}, + ("Category Fives", 7, 1): {0: 27826, 5: 39154, 10: 33020}, + ("Category Fives", 7, 2): {0: 7609, 5: 24193, 10: 31722, 15: 23214, 20: 13262}, + ("Category Fives", 7, 3): {0: 1879, 15: 23021, 20: 75100}, + ("Category Fives", 7, 4): {0: 345, 20: 64636, 25: 35019}, + ("Category Fives", 7, 5): {0: 40, 20: 7522, 25: 76792, 30: 15646}, + ("Category Fives", 7, 6): {0: 8, 25: 26517, 30: 73475}, + ("Category Fives", 7, 7): {0: 2, 30: 99998}, + ("Category Fives", 7, 8): {30: 84211, 35: 15789}, ("Category Fives", 8, 0): {0: 100000}, - ("Category Fives", 8, 1): {0: 23333, 5: 37259, 15: 39408}, - ("Category Fives", 8, 2): {0: 5425, 10: 48295, 20: 46280}, - ("Category Fives", 8, 3): {0: 1258, 15: 53475, 25: 45267}, - ("Category Fives", 8, 4): {0: 2752, 20: 56808, 30: 40440}, - ("Category Fives", 8, 5): {0: 5203, 20: 35571, 30: 59226}, - ("Category Fives", 8, 6): {0: 1970, 25: 51621, 35: 46409}, - ("Category Fives", 8, 7): {0: 4281, 25: 35146, 30: 30426, 40: 30147}, - ("Category Fives", 8, 8): {0: 2040, 30: 56946, 40: 41014}, + ("Category Fives", 8, 1): {0: 23333, 5: 37259, 10: 25947, 15: 13461}, + ("Category Fives", 8, 2): {0: 5425, 5: 18915, 10: 29380, 15: 25994, 20: 20286}, + ("Category Fives", 8, 3): {0: 495, 20: 78726, 25: 20779}, + ("Category Fives", 8, 4): {20: 12998, 25: 70085, 30: 16917}, + ("Category Fives", 8, 5): {25: 15859, 30: 84141}, + ("Category Fives", 8, 6): {30: 80722, 35: 19278}, + ("Category Fives", 8, 7): {30: 23955, 35: 76045}, + ("Category Fives", 8, 8): {35: 100000}, ("Category Sixes", 0, 0): {0: 100000}, ("Category Sixes", 0, 1): {0: 100000}, ("Category Sixes", 0, 2): {0: 100000}, @@ -445,54 +437,54 @@ ("Category Sixes", 3, 3): {0: 19366, 6: 42246, 12: 38388}, ("Category Sixes", 3, 4): {0: 11144, 6: 36281, 12: 38817, 18: 13758}, ("Category Sixes", 3, 5): {0: 6414, 6: 28891, 12: 43114, 18: 21581}, - ("Category Sixes", 3, 6): {0: 3870, 12: 66712, 18: 29418}, - ("Category Sixes", 3, 7): {0: 2188, 12: 60290, 18: 37522}, - ("Category Sixes", 3, 8): {0: 1289, 12: 53503, 18: 45208}, + ("Category Sixes", 3, 6): {0: 3870, 6: 22394, 12: 44318, 18: 29418}, + ("Category Sixes", 3, 7): {0: 2188, 6: 16803, 12: 43487, 18: 37522}, + ("Category Sixes", 3, 8): {0: 1289, 6: 12421, 12: 41082, 18: 45208}, ("Category Sixes", 4, 0): {0: 100000}, ("Category Sixes", 4, 1): {0: 48197, 6: 38521, 12: 13282}, ("Category Sixes", 4, 2): {0: 23155, 6: 41179, 12: 35666}, ("Category Sixes", 4, 3): {0: 11256, 6: 32609, 12: 35588, 18: 20547}, - ("Category Sixes", 4, 4): {0: 5324, 12: 60474, 18: 34202}, - ("Category Sixes", 4, 5): {0: 2658, 12: 50173, 18: 34476, 24: 12693}, - ("Category Sixes", 4, 6): {0: 1282, 12: 39852, 18: 39379, 24: 19487}, - ("Category Sixes", 4, 7): {0: 588, 12: 30598, 18: 41935, 24: 26879}, - ("Category Sixes", 4, 8): {0: 4180, 18: 61222, 24: 34598}, + ("Category Sixes", 4, 4): {0: 5324, 6: 23265, 12: 37209, 18: 34202}, + ("Category Sixes", 4, 5): {0: 2658, 6: 15488, 12: 34685, 18: 34476, 24: 12693}, + ("Category Sixes", 4, 6): {0: 2045, 12: 19683, 18: 48559, 24: 29713}, + ("Category Sixes", 4, 7): {0: 1470, 18: 34646, 24: 63884}, + ("Category Sixes", 4, 8): {0: 22, 18: 12111, 24: 87867}, ("Category Sixes", 5, 0): {0: 100000}, ("Category Sixes", 5, 1): {0: 40393, 6: 39904, 12: 19703}, ("Category Sixes", 5, 2): {0: 16202, 6: 35664, 12: 31241, 18: 16893}, - ("Category Sixes", 5, 3): {0: 6456, 12: 58124, 18: 25020, 24: 10400}, - ("Category Sixes", 5, 4): {0: 2581, 12: 44335, 18: 32198, 24: 20886}, - ("Category Sixes", 5, 5): {0: 1119, 12: 30838, 18: 34716, 24: 33327}, - ("Category Sixes", 5, 6): {0: 4563, 18: 49516, 24: 32829, 30: 13092}, - ("Category Sixes", 5, 7): {0: 2315, 18: 40699, 24: 37560, 30: 19426}, - ("Category Sixes", 5, 8): {0: 1246, 18: 31964, 24: 40134, 30: 26656}, + ("Category Sixes", 5, 3): {0: 6456, 6: 23539, 12: 34585, 18: 25020, 24: 10400}, + ("Category Sixes", 5, 4): {0: 2581, 6: 13980, 12: 30355, 18: 32198, 24: 20886}, + ("Category Sixes", 5, 5): {0: 1472, 12: 13518, 18: 37752, 24: 47258}, + ("Category Sixes", 5, 6): {0: 476, 18: 14559, 24: 71856, 30: 13109}, + ("Category Sixes", 5, 7): {0: 275, 24: 52573, 30: 47152}, + ("Category Sixes", 5, 8): {24: 16500, 30: 83500}, ("Category Sixes", 6, 0): {0: 100000}, - ("Category Sixes", 6, 1): {0: 33316, 6: 40218, 18: 26466}, - ("Category Sixes", 6, 2): {0: 11256, 6: 29444, 12: 32590, 24: 26710}, - ("Category Sixes", 6, 3): {0: 3787, 12: 46139, 18: 29107, 24: 20967}, - ("Category Sixes", 6, 4): {0: 1286, 12: 29719, 18: 31264, 24: 25039, 30: 12692}, - ("Category Sixes", 6, 5): {0: 4190, 18: 41667, 24: 30919, 30: 23224}, - ("Category Sixes", 6, 6): {0: 1804, 18: 30702, 24: 32923, 30: 34571}, - ("Category Sixes", 6, 7): {0: 51, 24: 53324, 30: 32487, 36: 14138}, - ("Category Sixes", 6, 8): {0: 2886, 24: 39510, 30: 37212, 36: 20392}, + ("Category Sixes", 6, 1): {0: 33316, 6: 40218, 12: 26466}, + ("Category Sixes", 6, 2): {0: 11256, 6: 29444, 12: 32590, 18: 26710}, + ("Category Sixes", 6, 3): {0: 3787, 6: 16266, 12: 29873, 18: 29107, 24: 20967}, + ("Category Sixes", 6, 4): {0: 1875, 12: 13602, 18: 33731, 24: 38090, 30: 12702}, + ("Category Sixes", 6, 5): {0: 433, 18: 10665, 24: 47398, 30: 41504}, + ("Category Sixes", 6, 6): {0: 89, 24: 14905, 30: 85006}, + ("Category Sixes", 6, 7): {0: 19, 30: 85816, 36: 14165}, + ("Category Sixes", 6, 8): {30: 43219, 36: 56781}, ("Category Sixes", 7, 0): {0: 100000}, - ("Category Sixes", 7, 1): {0: 27852, 6: 38984, 18: 33164}, - ("Category Sixes", 7, 2): {0: 7883, 12: 55404, 24: 36713}, - ("Category Sixes", 7, 3): {0: 2186, 12: 35249, 18: 29650, 30: 32915}, - ("Category Sixes", 7, 4): {0: 5062, 18: 40976, 24: 28335, 36: 25627}, - ("Category Sixes", 7, 5): {0: 1947, 18: 27260, 24: 29254, 30: 25790, 36: 15749}, - ("Category Sixes", 7, 6): {0: 4568, 24: 38799, 30: 30698, 42: 25935}, - ("Category Sixes", 7, 7): {0: 2081, 24: 28590, 30: 31709, 36: 37620}, - ("Category Sixes", 7, 8): {0: 73, 30: 51135, 36: 33183, 42: 15609}, + ("Category Sixes", 7, 1): {0: 27852, 6: 38984, 12: 33164}, + ("Category Sixes", 7, 2): {0: 7883, 6: 23846, 12: 31558, 18: 23295, 24: 13418}, + ("Category Sixes", 7, 3): {0: 2186, 6: 10928, 12: 24321, 18: 29650, 24: 21177, 30: 11738}, + ("Category Sixes", 7, 4): {0: 1034, 18: 12857, 24: 37227, 30: 48882}, + ("Category Sixes", 7, 5): {0: 300, 30: 83887, 36: 15813}, + ("Category Sixes", 7, 6): {30: 31359, 36: 68641}, + ("Category Sixes", 7, 7): {36: 89879, 42: 10121}, + ("Category Sixes", 7, 8): {36: 49549, 42: 50451}, ("Category Sixes", 8, 0): {0: 100000}, ("Category Sixes", 8, 1): {0: 23220, 6: 37213, 12: 25961, 18: 13606}, - ("Category Sixes", 8, 2): {0: 5280, 12: 48607, 18: 25777, 30: 20336}, - ("Category Sixes", 8, 3): {0: 1246, 12: 25869, 18: 27277, 30: 45608}, - ("Category Sixes", 8, 4): {0: 2761, 18: 29831, 24: 27146, 36: 40262}, - ("Category Sixes", 8, 5): {0: 5100, 24: 35948, 30: 27655, 42: 31297}, - ("Category Sixes", 8, 6): {0: 2067, 30: 51586, 36: 27024, 42: 19323}, - ("Category Sixes", 8, 7): {0: 4269, 30: 35032, 36: 30772, 48: 29927}, - ("Category Sixes", 8, 8): {6: 2012, 30: 25871, 36: 31116, 42: 28870, 48: 12131}, + ("Category Sixes", 8, 2): {0: 5280, 6: 18943, 12: 29664, 18: 25777, 24: 20336}, + ("Category Sixes", 8, 3): {0: 2024, 12: 12586, 18: 28717, 24: 35860, 30: 20813}, + ("Category Sixes", 8, 4): {0: 175, 24: 10907, 30: 72017, 36: 16901}, + ("Category Sixes", 8, 5): {0: 1, 30: 23224, 36: 66215, 42: 10560}, + ("Category Sixes", 8, 6): {36: 29563, 42: 70437}, + ("Category Sixes", 8, 7): {42: 99990, 36: 10}, + ("Category Sixes", 8, 8): {42: 87843, 48: 12157}, ("Category Choice", 0, 0): {0: 100000}, ("Category Choice", 0, 1): {0: 100000}, ("Category Choice", 0, 2): {0: 100000}, @@ -503,77 +495,77 @@ ("Category Choice", 0, 7): {0: 100000}, ("Category Choice", 0, 8): {0: 100000}, ("Category Choice", 1, 0): {0: 100000}, - ("Category Choice", 1, 1): {1: 33315, 5: 66685}, - ("Category Choice", 1, 2): {1: 10921, 5: 89079}, - ("Category Choice", 1, 3): {1: 27995, 6: 72005}, - ("Category Choice", 1, 4): {1: 15490, 6: 84510}, - ("Category Choice", 1, 5): {1: 6390, 6: 93610}, - ("Category Choice", 1, 6): {1: 34656, 6: 65344}, - ("Category Choice", 1, 7): {1: 28829, 6: 71171}, - ("Category Choice", 1, 8): {1: 23996, 6: 76004}, + ("Category Choice", 1, 1): {1: 33315, 3: 66685}, + ("Category Choice", 1, 2): {1: 32981, 4: 67019}, + ("Category Choice", 1, 3): {1: 12312, 4: 25020, 5: 62668}, + ("Category Choice", 1, 4): {1: 11564, 5: 88436}, + ("Category Choice", 1, 5): {1: 2956, 5: 97044}, + ("Category Choice", 1, 6): {4: 1024, 6: 65357, 5: 33619}, + ("Category Choice", 1, 7): {6: 100000}, + ("Category Choice", 1, 8): {6: 100000}, ("Category Choice", 2, 0): {0: 100000}, - ("Category Choice", 2, 1): {2: 16796, 8: 83204}, - ("Category Choice", 2, 2): {2: 22212, 10: 77788}, - ("Category Choice", 2, 3): {2: 29002, 11: 70998}, - ("Category Choice", 2, 4): {2: 22485, 11: 77515}, - ("Category Choice", 2, 5): {2: 28019, 12: 71981}, - ("Category Choice", 2, 6): {2: 23193, 12: 76807}, - ("Category Choice", 2, 7): {2: 11255, 8: 38369, 12: 50376}, - ("Category Choice", 2, 8): {2: 9297, 12: 90703}, + ("Category Choice", 2, 1): {2: 27810, 6: 72190}, + ("Category Choice", 2, 2): {2: 10285, 6: 26698, 8: 63017}, + ("Category Choice", 2, 3): {2: 3965, 8: 96035}, + ("Category Choice", 2, 4): {2: 143, 8: 33731, 9: 66126}, + ("Category Choice", 2, 5): {8: 12687, 10: 62544, 9: 24769}, + ("Category Choice", 2, 6): {10: 100000}, + ("Category Choice", 2, 7): {11: 66194, 10: 33806}, + ("Category Choice", 2, 8): {11: 100000}, ("Category Choice", 3, 0): {0: 100000}, - ("Category Choice", 3, 1): {3: 25983, 12: 74017}, - ("Category Choice", 3, 2): {3: 24419, 14: 75581}, - ("Category Choice", 3, 3): {3: 24466, 15: 75534}, - ("Category Choice", 3, 4): {3: 25866, 16: 74134}, - ("Category Choice", 3, 5): {3: 30994, 17: 69006}, - ("Category Choice", 3, 6): {3: 13523, 13: 41606, 17: 44871}, - ("Category Choice", 3, 7): {3: 28667, 18: 71333}, - ("Category Choice", 3, 8): {3: 23852, 18: 76148}, + ("Category Choice", 3, 1): {3: 10461, 6: 27156, 10: 62383}, + ("Category Choice", 3, 2): {3: 3586, 6: 31281, 12: 65133}, + ("Category Choice", 3, 3): {3: 1491, 12: 33737, 13: 64772}, + ("Category Choice", 3, 4): {12: 13802, 14: 60820, 13: 25378}, + ("Category Choice", 3, 5): {14: 99999, 13: 1}, + ("Category Choice", 3, 6): {15: 64851, 14: 35149}, + ("Category Choice", 3, 7): {16: 62341, 15: 24422, 14: 13237}, + ("Category Choice", 3, 8): {16: 100000}, ("Category Choice", 4, 0): {0: 100000}, - ("Category Choice", 4, 1): {4: 1125, 7: 32443, 16: 66432}, - ("Category Choice", 4, 2): {4: 18156, 14: 39494, 18: 42350}, - ("Category Choice", 4, 3): {4: 538, 9: 32084, 20: 67378}, - ("Category Choice", 4, 4): {4: 30873, 21: 69127}, - ("Category Choice", 4, 5): {4: 31056, 22: 68944}, - ("Category Choice", 4, 6): {4: 22939, 19: 43956, 23: 33105}, - ("Category Choice", 4, 7): {5: 16935, 19: 41836, 23: 41229}, - ("Category Choice", 4, 8): {5: 31948, 24: 68052}, + ("Category Choice", 4, 1): {4: 4748, 10: 28815, 13: 66437}, + ("Category Choice", 4, 2): {12: 12006, 16: 64226, 13: 23768}, + ("Category Choice", 4, 3): {16: 32567, 17: 67433}, + ("Category Choice", 4, 4): {16: 30845, 18: 69155}, + ("Category Choice", 4, 5): {16: 9568, 19: 68981, 18: 21451}, + ("Category Choice", 4, 6): {18: 10841, 20: 65051, 19: 24108}, + ("Category Choice", 4, 7): {18: 4198, 21: 61270, 20: 25195, 19: 9337}, + ("Category Choice", 4, 8): {21: 99999, 20: 1}, ("Category Choice", 5, 0): {0: 100000}, - ("Category Choice", 5, 1): {5: 21998, 15: 38001, 19: 40001}, - ("Category Choice", 5, 2): {5: 26627, 19: 38217, 23: 35156}, - ("Category Choice", 5, 3): {6: 22251, 24: 77749}, - ("Category Choice", 5, 4): {5: 27098, 22: 39632, 26: 33270}, - ("Category Choice", 5, 5): {6: 1166, 16: 32131, 27: 66703}, - ("Category Choice", 5, 6): {7: 1177, 17: 32221, 28: 66602}, - ("Category Choice", 5, 7): {8: 25048, 25: 42590, 29: 32362}, - ("Category Choice", 5, 8): {9: 18270, 25: 41089, 29: 40641}, + ("Category Choice", 5, 1): {5: 4485, 13: 35340, 17: 60175}, + ("Category Choice", 5, 2): {16: 35286, 20: 64714}, + ("Category Choice", 5, 3): {20: 39254, 22: 60746}, + ("Category Choice", 5, 4): {20: 35320, 23: 64680}, + ("Category Choice", 5, 5): {22: 33253, 24: 66747}, + ("Category Choice", 5, 6): {22: 11089, 25: 66653, 24: 22258}, + ("Category Choice", 5, 7): {24: 12216, 26: 63214, 22: 50, 25: 24520}, + ("Category Choice", 5, 8): {24: 4866, 27: 60314, 26: 25089, 25: 9731}, ("Category Choice", 6, 0): {0: 100000}, - ("Category Choice", 6, 1): {6: 27848, 23: 72152}, - ("Category Choice", 6, 2): {8: 27078, 27: 72922}, - ("Category Choice", 6, 3): {6: 27876, 29: 72124}, - ("Category Choice", 6, 4): {9: 30912, 31: 69088}, - ("Category Choice", 6, 5): {10: 27761, 28: 38016, 32: 34223}, - ("Category Choice", 6, 6): {13: 25547, 29: 39452, 33: 35001}, - ("Category Choice", 6, 7): {12: 767, 22: 32355, 34: 66878}, - ("Category Choice", 6, 8): {12: 25224, 31: 41692, 35: 33084}, + ("Category Choice", 6, 1): {6: 2276, 17: 33774, 20: 63950}, + ("Category Choice", 6, 2): {20: 34853, 24: 65147}, + ("Category Choice", 6, 3): {22: 12477, 26: 64201, 24: 23322}, + ("Category Choice", 6, 4): {24: 14073, 28: 60688, 26: 25239}, + ("Category Choice", 6, 5): {26: 35591, 29: 64409}, + ("Category Choice", 6, 6): {26: 33229, 30: 66771}, + ("Category Choice", 6, 7): {28: 33078, 31: 66922}, + ("Category Choice", 6, 8): {28: 12143, 31: 24567, 32: 63290}, ("Category Choice", 7, 0): {0: 100000}, - ("Category Choice", 7, 1): {7: 1237, 15: 32047, 27: 66716}, - ("Category Choice", 7, 2): {10: 27324, 31: 72676}, - ("Category Choice", 7, 3): {10: 759, 20: 32233, 34: 67008}, - ("Category Choice", 7, 4): {13: 26663, 35: 73337}, - ("Category Choice", 7, 5): {12: 29276, 37: 70724}, - ("Category Choice", 7, 6): {14: 26539, 38: 73461}, - ("Category Choice", 7, 7): {16: 24675, 35: 38365, 39: 36960}, - ("Category Choice", 7, 8): {14: 2, 19: 31688, 40: 68310}, + ("Category Choice", 7, 1): {7: 1558, 20: 31716, 23: 66726}, + ("Category Choice", 7, 2): {23: 34663, 28: 65337}, + ("Category Choice", 7, 3): {28: 32932, 30: 67062, 23: 6}, + ("Category Choice", 7, 4): {28: 11163, 32: 66108, 30: 22729}, + ("Category Choice", 7, 5): {30: 12528, 34: 63034, 32: 24438}, + ("Category Choice", 7, 6): {30: 4270, 35: 65916, 34: 21485, 32: 8329}, + ("Category Choice", 7, 7): {32: 4014, 36: 68134, 35: 21006, 34: 6846}, + ("Category Choice", 7, 8): {34: 3434, 37: 68373, 36: 21550, 35: 6643}, ("Category Choice", 8, 0): {0: 100000}, - ("Category Choice", 8, 1): {10: 23768, 25: 38280, 30: 37952}, - ("Category Choice", 8, 2): {11: 27666, 31: 38472, 36: 33862}, - ("Category Choice", 8, 3): {12: 24387, 33: 37477, 38: 38136}, - ("Category Choice", 8, 4): {16: 23316, 35: 38117, 40: 38567}, - ("Category Choice", 8, 5): {16: 30949, 42: 69051}, - ("Category Choice", 8, 6): {16: 26968, 43: 73032}, - ("Category Choice", 8, 7): {20: 24559, 44: 75441}, - ("Category Choice", 8, 8): {20: 1, 23: 22731, 41: 37835, 45: 39433}, + ("Category Choice", 8, 1): {10: 1400, 23: 36664, 27: 61936}, + ("Category Choice", 8, 2): {27: 34595, 32: 65405}, + ("Category Choice", 8, 3): {30: 13097, 35: 62142, 32: 24761}, + ("Category Choice", 8, 4): {35: 36672, 37: 63328}, + ("Category Choice", 8, 5): {39: 61292, 35: 14195, 37: 24513}, + ("Category Choice", 8, 6): {40: 65672, 39: 21040, 35: 4873, 37: 8415}, + ("Category Choice", 8, 7): {39: 13561, 42: 60493, 40: 25946}, + ("Category Choice", 8, 8): {39: 5250, 43: 61284, 42: 23421, 40: 10045}, ("Category Inverse Choice", 0, 0): {0: 100000}, ("Category Inverse Choice", 0, 1): {0: 100000}, ("Category Inverse Choice", 0, 2): {0: 100000}, @@ -584,77 +576,77 @@ ("Category Inverse Choice", 0, 7): {0: 100000}, ("Category Inverse Choice", 0, 8): {0: 100000}, ("Category Inverse Choice", 1, 0): {0: 100000}, - ("Category Inverse Choice", 1, 1): {1: 33315, 5: 66685}, - ("Category Inverse Choice", 1, 2): {1: 10921, 5: 89079}, - ("Category Inverse Choice", 1, 3): {1: 27995, 6: 72005}, - ("Category Inverse Choice", 1, 4): {1: 15490, 6: 84510}, - ("Category Inverse Choice", 1, 5): {1: 6390, 6: 93610}, - ("Category Inverse Choice", 1, 6): {1: 34656, 6: 65344}, - ("Category Inverse Choice", 1, 7): {1: 28829, 6: 71171}, - ("Category Inverse Choice", 1, 8): {1: 23996, 6: 76004}, + ("Category Inverse Choice", 1, 1): {1: 33315, 3: 66685}, + ("Category Inverse Choice", 1, 2): {1: 32981, 4: 67019}, + ("Category Inverse Choice", 1, 3): {1: 12312, 4: 25020, 5: 62668}, + ("Category Inverse Choice", 1, 4): {1: 11564, 5: 88436}, + ("Category Inverse Choice", 1, 5): {1: 2956, 5: 97044}, + ("Category Inverse Choice", 1, 6): {4: 1024, 6: 65357, 5: 33619}, + ("Category Inverse Choice", 1, 7): {6: 100000}, + ("Category Inverse Choice", 1, 8): {6: 100000}, ("Category Inverse Choice", 2, 0): {0: 100000}, - ("Category Inverse Choice", 2, 1): {2: 16796, 8: 83204}, - ("Category Inverse Choice", 2, 2): {2: 22212, 10: 77788}, - ("Category Inverse Choice", 2, 3): {2: 29002, 11: 70998}, - ("Category Inverse Choice", 2, 4): {2: 22485, 11: 77515}, - ("Category Inverse Choice", 2, 5): {2: 28019, 12: 71981}, - ("Category Inverse Choice", 2, 6): {2: 23193, 12: 76807}, - ("Category Inverse Choice", 2, 7): {2: 11255, 8: 38369, 12: 50376}, - ("Category Inverse Choice", 2, 8): {2: 9297, 12: 90703}, + ("Category Inverse Choice", 2, 1): {2: 27810, 6: 72190}, + ("Category Inverse Choice", 2, 2): {2: 10285, 6: 26698, 8: 63017}, + ("Category Inverse Choice", 2, 3): {2: 3965, 8: 96035}, + ("Category Inverse Choice", 2, 4): {2: 143, 8: 33731, 9: 66126}, + ("Category Inverse Choice", 2, 5): {8: 12687, 10: 62544, 9: 24769}, + ("Category Inverse Choice", 2, 6): {10: 100000}, + ("Category Inverse Choice", 2, 7): {11: 66194, 10: 33806}, + ("Category Inverse Choice", 2, 8): {11: 100000}, ("Category Inverse Choice", 3, 0): {0: 100000}, - ("Category Inverse Choice", 3, 1): {3: 25983, 12: 74017}, - ("Category Inverse Choice", 3, 2): {3: 24419, 14: 75581}, - ("Category Inverse Choice", 3, 3): {3: 24466, 15: 75534}, - ("Category Inverse Choice", 3, 4): {3: 25866, 16: 74134}, - ("Category Inverse Choice", 3, 5): {3: 30994, 17: 69006}, - ("Category Inverse Choice", 3, 6): {3: 13523, 13: 41606, 17: 44871}, - ("Category Inverse Choice", 3, 7): {3: 28667, 18: 71333}, - ("Category Inverse Choice", 3, 8): {3: 23852, 18: 76148}, + ("Category Inverse Choice", 3, 1): {3: 10461, 6: 27156, 10: 62383}, + ("Category Inverse Choice", 3, 2): {3: 3586, 6: 31281, 12: 65133}, + ("Category Inverse Choice", 3, 3): {3: 1491, 12: 33737, 13: 64772}, + ("Category Inverse Choice", 3, 4): {12: 13802, 14: 60820, 13: 25378}, + ("Category Inverse Choice", 3, 5): {14: 99999, 13: 1}, + ("Category Inverse Choice", 3, 6): {15: 64851, 14: 35149}, + ("Category Inverse Choice", 3, 7): {16: 62341, 15: 24422, 14: 13237}, + ("Category Inverse Choice", 3, 8): {16: 100000}, ("Category Inverse Choice", 4, 0): {0: 100000}, - ("Category Inverse Choice", 4, 1): {4: 1125, 7: 32443, 16: 66432}, - ("Category Inverse Choice", 4, 2): {4: 18156, 14: 39494, 18: 42350}, - ("Category Inverse Choice", 4, 3): {4: 538, 9: 32084, 20: 67378}, - ("Category Inverse Choice", 4, 4): {4: 30873, 21: 69127}, - ("Category Inverse Choice", 4, 5): {4: 31056, 22: 68944}, - ("Category Inverse Choice", 4, 6): {4: 22939, 19: 43956, 23: 33105}, - ("Category Inverse Choice", 4, 7): {5: 16935, 19: 41836, 23: 41229}, - ("Category Inverse Choice", 4, 8): {5: 31948, 24: 68052}, + ("Category Inverse Choice", 4, 1): {4: 4748, 10: 28815, 13: 66437}, + ("Category Inverse Choice", 4, 2): {12: 12006, 16: 64226, 13: 23768}, + ("Category Inverse Choice", 4, 3): {16: 32567, 17: 67433}, + ("Category Inverse Choice", 4, 4): {16: 30845, 18: 69155}, + ("Category Inverse Choice", 4, 5): {16: 9568, 19: 68981, 18: 21451}, + ("Category Inverse Choice", 4, 6): {18: 10841, 20: 65051, 19: 24108}, + ("Category Inverse Choice", 4, 7): {18: 4198, 21: 61270, 20: 25195, 19: 9337}, + ("Category Inverse Choice", 4, 8): {21: 99999, 20: 1}, ("Category Inverse Choice", 5, 0): {0: 100000}, - ("Category Inverse Choice", 5, 1): {5: 21998, 15: 38001, 19: 40001}, - ("Category Inverse Choice", 5, 2): {5: 26627, 19: 38217, 23: 35156}, - ("Category Inverse Choice", 5, 3): {6: 22251, 24: 77749}, - ("Category Inverse Choice", 5, 4): {5: 27098, 22: 39632, 26: 33270}, - ("Category Inverse Choice", 5, 5): {6: 1166, 16: 32131, 27: 66703}, - ("Category Inverse Choice", 5, 6): {7: 1177, 17: 32221, 28: 66602}, - ("Category Inverse Choice", 5, 7): {8: 25048, 25: 42590, 29: 32362}, - ("Category Inverse Choice", 5, 8): {9: 18270, 25: 41089, 29: 40641}, + ("Category Inverse Choice", 5, 1): {5: 4485, 13: 35340, 17: 60175}, + ("Category Inverse Choice", 5, 2): {16: 35286, 20: 64714}, + ("Category Inverse Choice", 5, 3): {20: 39254, 22: 60746}, + ("Category Inverse Choice", 5, 4): {20: 35320, 23: 64680}, + ("Category Inverse Choice", 5, 5): {22: 33253, 24: 66747}, + ("Category Inverse Choice", 5, 6): {22: 11089, 25: 66653, 24: 22258}, + ("Category Inverse Choice", 5, 7): {24: 12216, 26: 63214, 22: 50, 25: 24520}, + ("Category Inverse Choice", 5, 8): {24: 4866, 27: 60314, 26: 25089, 25: 9731}, ("Category Inverse Choice", 6, 0): {0: 100000}, - ("Category Inverse Choice", 6, 1): {6: 27848, 23: 72152}, - ("Category Inverse Choice", 6, 2): {8: 27078, 27: 72922}, - ("Category Inverse Choice", 6, 3): {6: 27876, 29: 72124}, - ("Category Inverse Choice", 6, 4): {9: 30912, 31: 69088}, - ("Category Inverse Choice", 6, 5): {10: 27761, 28: 38016, 32: 34223}, - ("Category Inverse Choice", 6, 6): {13: 25547, 29: 39452, 33: 35001}, - ("Category Inverse Choice", 6, 7): {12: 767, 22: 32355, 34: 66878}, - ("Category Inverse Choice", 6, 8): {12: 25224, 31: 41692, 35: 33084}, + ("Category Inverse Choice", 6, 1): {6: 2276, 17: 33774, 20: 63950}, + ("Category Inverse Choice", 6, 2): {20: 34853, 24: 65147}, + ("Category Inverse Choice", 6, 3): {22: 12477, 26: 64201, 24: 23322}, + ("Category Inverse Choice", 6, 4): {24: 14073, 28: 60688, 26: 25239}, + ("Category Inverse Choice", 6, 5): {26: 35591, 29: 64409}, + ("Category Inverse Choice", 6, 6): {26: 33229, 30: 66771}, + ("Category Inverse Choice", 6, 7): {28: 33078, 31: 66922}, + ("Category Inverse Choice", 6, 8): {28: 12143, 31: 24567, 32: 63290}, ("Category Inverse Choice", 7, 0): {0: 100000}, - ("Category Inverse Choice", 7, 1): {7: 1237, 15: 32047, 27: 66716}, - ("Category Inverse Choice", 7, 2): {10: 27324, 31: 72676}, - ("Category Inverse Choice", 7, 3): {10: 759, 20: 32233, 34: 67008}, - ("Category Inverse Choice", 7, 4): {13: 26663, 35: 73337}, - ("Category Inverse Choice", 7, 5): {12: 29276, 37: 70724}, - ("Category Inverse Choice", 7, 6): {14: 26539, 38: 73461}, - ("Category Inverse Choice", 7, 7): {16: 24675, 35: 38365, 39: 36960}, - ("Category Inverse Choice", 7, 8): {14: 2, 19: 31688, 40: 68310}, + ("Category Inverse Choice", 7, 1): {7: 1558, 20: 31716, 23: 66726}, + ("Category Inverse Choice", 7, 2): {23: 34663, 28: 65337}, + ("Category Inverse Choice", 7, 3): {28: 32932, 30: 67062, 23: 6}, + ("Category Inverse Choice", 7, 4): {28: 11163, 32: 66108, 30: 22729}, + ("Category Inverse Choice", 7, 5): {30: 12528, 34: 63034, 32: 24438}, + ("Category Inverse Choice", 7, 6): {30: 4270, 35: 65916, 34: 21485, 32: 8329}, + ("Category Inverse Choice", 7, 7): {32: 4014, 36: 68134, 35: 21006, 34: 6846}, + ("Category Inverse Choice", 7, 8): {34: 3434, 37: 68373, 36: 21550, 35: 6643}, ("Category Inverse Choice", 8, 0): {0: 100000}, - ("Category Inverse Choice", 8, 1): {10: 23768, 25: 38280, 30: 37952}, - ("Category Inverse Choice", 8, 2): {11: 27666, 31: 38472, 36: 33862}, - ("Category Inverse Choice", 8, 3): {12: 24387, 33: 37477, 38: 38136}, - ("Category Inverse Choice", 8, 4): {16: 23316, 35: 38117, 40: 38567}, - ("Category Inverse Choice", 8, 5): {16: 30949, 42: 69051}, - ("Category Inverse Choice", 8, 6): {16: 26968, 43: 73032}, - ("Category Inverse Choice", 8, 7): {20: 24559, 44: 75441}, - ("Category Inverse Choice", 8, 8): {20: 1, 23: 22731, 41: 37835, 45: 39433}, + ("Category Inverse Choice", 8, 1): {10: 1400, 23: 36664, 27: 61936}, + ("Category Inverse Choice", 8, 2): {27: 34595, 32: 65405}, + ("Category Inverse Choice", 8, 3): {30: 13097, 35: 62142, 32: 24761}, + ("Category Inverse Choice", 8, 4): {35: 36672, 37: 63328}, + ("Category Inverse Choice", 8, 5): {39: 61292, 35: 14195, 37: 24513}, + ("Category Inverse Choice", 8, 6): {40: 65672, 39: 21040, 35: 4873, 37: 8415}, + ("Category Inverse Choice", 8, 7): {39: 13561, 42: 60493, 40: 25946}, + ("Category Inverse Choice", 8, 8): {39: 5250, 43: 61284, 42: 23421, 40: 10045}, ("Category Pair", 0, 0): {0: 100000}, ("Category Pair", 0, 1): {0: 100000}, ("Category Pair", 0, 2): {0: 100000}, @@ -1319,54 +1311,54 @@ ("Category Distincts", 2, 6): {1: 1, 2: 99999}, ("Category Distincts", 2, 7): {2: 100000}, ("Category Distincts", 2, 8): {2: 100000}, - ("Category Distincts", 3, 1): {1: 2760, 3: 97240}, - ("Category Distincts", 3, 2): {1: 15014, 3: 84986}, - ("Category Distincts", 3, 3): {1: 4866, 3: 95134}, - ("Category Distincts", 3, 4): {2: 1659, 3: 98341}, - ("Category Distincts", 3, 5): {2: 575, 3: 99425}, - ("Category Distincts", 3, 6): {2: 200, 3: 99800}, - ("Category Distincts", 3, 7): {2: 69, 3: 99931}, - ("Category Distincts", 3, 8): {2: 22, 3: 99978}, - ("Category Distincts", 4, 1): {1: 16634, 3: 83366}, - ("Category Distincts", 4, 2): {1: 1893, 4: 98107}, - ("Category Distincts", 4, 3): {2: 19861, 4: 80139}, - ("Category Distincts", 4, 4): {2: 9879, 4: 90121}, - ("Category Distincts", 4, 5): {2: 4906, 4: 95094}, - ("Category Distincts", 4, 6): {3: 2494, 4: 97506}, - ("Category Distincts", 4, 7): {3: 1297, 4: 98703}, - ("Category Distincts", 4, 8): {3: 611, 4: 99389}, - ("Category Distincts", 5, 1): {1: 5798, 4: 94202}, - ("Category Distincts", 5, 2): {2: 11843, 4: 88157}, - ("Category Distincts", 5, 3): {2: 3022, 5: 96978}, - ("Category Distincts", 5, 4): {3: 32354, 5: 67646}, - ("Category Distincts", 5, 5): {3: 21606, 5: 78394}, - ("Category Distincts", 5, 6): {3: 14525, 5: 85475}, - ("Category Distincts", 5, 7): {3: 9660, 5: 90340}, - ("Category Distincts", 5, 8): {3: 6463, 5: 93537}, - ("Category Distincts", 6, 1): {1: 25012, 4: 74988}, - ("Category Distincts", 6, 2): {2: 3299, 5: 96701}, - ("Category Distincts", 6, 3): {3: 17793, 5: 82207}, - ("Category Distincts", 6, 4): {3: 7831, 5: 92169}, - ("Category Distincts", 6, 5): {3: 3699, 6: 96301}, - ("Category Distincts", 6, 6): {4: 1557, 6: 98443}, - ("Category Distincts", 6, 7): {4: 728, 6: 99272}, - ("Category Distincts", 6, 8): {4: 321, 6: 99679}, - ("Category Distincts", 7, 1): {1: 13671, 5: 86329}, - ("Category Distincts", 7, 2): {2: 19686, 5: 80314}, - ("Category Distincts", 7, 3): {3: 6051, 6: 93949}, - ("Category Distincts", 7, 4): {3: 1796, 6: 98204}, - ("Category Distincts", 7, 5): {4: 28257, 6: 71743}, - ("Category Distincts", 7, 6): {4: 19581, 6: 80419}, - ("Category Distincts", 7, 7): {4: 13618, 6: 86382}, - ("Category Distincts", 7, 8): {4: 9545, 6: 90455}, - ("Category Distincts", 8, 1): {1: 7137, 5: 92863}, - ("Category Distincts", 8, 2): {2: 9414, 6: 90586}, - ("Category Distincts", 8, 3): {3: 1976, 6: 98024}, - ("Category Distincts", 8, 4): {4: 21397, 6: 78603}, - ("Category Distincts", 8, 5): {4: 12592, 6: 87408}, - ("Category Distincts", 8, 6): {4: 7177, 6: 92823}, - ("Category Distincts", 8, 7): {4: 4179, 6: 95821}, - ("Category Distincts", 8, 8): {5: 2440, 6: 97560}, + ("Category Distincts", 3, 1): {1: 2760, 2: 97240}, + ("Category Distincts", 3, 2): {1: 414, 3: 84996, 2: 14590}, + ("Category Distincts", 3, 3): {1: 109, 3: 99891}, + ("Category Distincts", 3, 4): {2: 11, 3: 99989}, + ("Category Distincts", 3, 5): {3: 100000}, + ("Category Distincts", 3, 6): {3: 100000}, + ("Category Distincts", 3, 7): {3: 100000}, + ("Category Distincts", 3, 8): {3: 100000}, + ("Category Distincts", 4, 1): {1: 458, 3: 83376, 2: 16166}, + ("Category Distincts", 4, 2): {1: 26, 4: 61232, 3: 37802, 2: 940}, + ("Category Distincts", 4, 3): {2: 3, 4: 97020, 3: 2977}, + ("Category Distincts", 4, 4): {4: 100000}, + ("Category Distincts", 4, 5): {4: 100000}, + ("Category Distincts", 4, 6): {4: 100000}, + ("Category Distincts", 4, 7): {4: 100000}, + ("Category Distincts", 4, 8): {4: 100000}, + ("Category Distincts", 5, 1): {1: 159, 3: 99841}, + ("Category Distincts", 5, 2): {2: 18, 4: 88167, 3: 11815}, + ("Category Distincts", 5, 3): {4: 100000}, + ("Category Distincts", 5, 4): {5: 67650, 4: 32350}, + ("Category Distincts", 5, 5): {5: 100000}, + ("Category Distincts", 5, 6): {5: 100000}, + ("Category Distincts", 5, 7): {5: 100000}, + ("Category Distincts", 5, 8): {5: 100000}, + ("Category Distincts", 6, 1): {1: 39, 4: 74998, 3: 24963}, + ("Category Distincts", 6, 2): {2: 1, 5: 61568, 4: 37296, 3: 1135}, + ("Category Distincts", 6, 3): {5: 93157, 4: 6843}, + ("Category Distincts", 6, 4): {5: 100000}, + ("Category Distincts", 6, 5): {5: 100000}, + ("Category Distincts", 6, 6): {5: 100000}, + ("Category Distincts", 6, 7): {5: 100000}, + ("Category Distincts", 6, 8): {6: 65828, 5: 34172}, + ("Category Distincts", 7, 1): {1: 13, 4: 99987}, + ("Category Distincts", 7, 2): {5: 99580, 4: 420}, + ("Category Distincts", 7, 3): {5: 100000}, + ("Category Distincts", 7, 4): {5: 100000}, + ("Category Distincts", 7, 5): {6: 71744, 5: 28256}, + ("Category Distincts", 7, 6): {6: 100000}, + ("Category Distincts", 7, 7): {6: 100000}, + ("Category Distincts", 7, 8): {6: 100000}, + ("Category Distincts", 8, 1): {4: 100000}, + ("Category Distincts", 8, 2): {5: 99981, 4: 19}, + ("Category Distincts", 8, 3): {6: 63291, 5: 36709}, + ("Category Distincts", 8, 4): {6: 99994, 5: 6}, + ("Category Distincts", 8, 5): {6: 100000}, + ("Category Distincts", 8, 6): {6: 100000}, + ("Category Distincts", 8, 7): {6: 100000}, + ("Category Distincts", 8, 8): {6: 100000}, ("Category Two times Ones", 0, 0): {0: 100000}, ("Category Two times Ones", 0, 1): {0: 100000}, ("Category Two times Ones", 0, 2): {0: 100000}, @@ -1378,7 +1370,7 @@ ("Category Two times Ones", 0, 8): {0: 100000}, ("Category Two times Ones", 1, 0): {0: 100000}, ("Category Two times Ones", 1, 1): {0: 100000}, - ("Category Two times Ones", 1, 2): {0: 100000}, + ("Category Two times Ones", 1, 2): {0: 69690, 2: 30310}, ("Category Two times Ones", 1, 3): {0: 57818, 2: 42182}, ("Category Two times Ones", 1, 4): {0: 48418, 2: 51582}, ("Category Two times Ones", 1, 5): {0: 40301, 2: 59699}, @@ -1386,68 +1378,68 @@ ("Category Two times Ones", 1, 7): {0: 28182, 2: 71818}, ("Category Two times Ones", 1, 8): {0: 23406, 2: 76594}, ("Category Two times Ones", 2, 0): {0: 100000}, - ("Category Two times Ones", 2, 1): {0: 100000}, + ("Category Two times Ones", 2, 1): {0: 69724, 2: 30276}, ("Category Two times Ones", 2, 2): {0: 48238, 2: 51762}, - ("Category Two times Ones", 2, 3): {0: 33290, 4: 66710}, - ("Category Two times Ones", 2, 4): {0: 23136, 4: 76864}, - ("Category Two times Ones", 2, 5): {0: 16146, 4: 83854}, - ("Category Two times Ones", 2, 6): {0: 11083, 4: 88917}, - ("Category Two times Ones", 2, 7): {0: 7662, 4: 92338}, - ("Category Two times Ones", 2, 8): {0: 5354, 4: 94646}, + ("Category Two times Ones", 2, 3): {0: 33290, 2: 66710}, + ("Category Two times Ones", 2, 4): {0: 23136, 2: 76864}, + ("Category Two times Ones", 2, 5): {0: 16146, 2: 48200, 4: 35654}, + ("Category Two times Ones", 2, 6): {0: 11083, 2: 44497, 4: 44420}, + ("Category Two times Ones", 2, 7): {0: 7662, 2: 40343, 4: 51995}, + ("Category Two times Ones", 2, 8): {0: 5354, 2: 35526, 4: 59120}, ("Category Two times Ones", 3, 0): {0: 100000}, ("Category Two times Ones", 3, 1): {0: 58021, 2: 41979}, - ("Category Two times Ones", 3, 2): {0: 33548, 4: 66452}, - ("Category Two times Ones", 3, 3): {0: 19375, 4: 80625}, - ("Category Two times Ones", 3, 4): {0: 10998, 4: 89002}, - ("Category Two times Ones", 3, 5): {0: 6519, 6: 93481}, - ("Category Two times Ones", 3, 6): {0: 3619, 6: 96381}, - ("Category Two times Ones", 3, 7): {0: 2195, 6: 97805}, - ("Category Two times Ones", 3, 8): {0: 13675, 6: 86325}, + ("Category Two times Ones", 3, 2): {0: 33548, 2: 66452}, + ("Category Two times Ones", 3, 3): {0: 19375, 2: 42372, 4: 38253}, + ("Category Two times Ones", 3, 4): {0: 10998, 2: 36435, 4: 52567}, + ("Category Two times Ones", 3, 5): {0: 7954, 4: 92046}, + ("Category Two times Ones", 3, 6): {0: 347, 4: 99653}, + ("Category Two times Ones", 3, 7): {0: 2, 4: 62851, 6: 37147}, + ("Category Two times Ones", 3, 8): {6: 99476, 4: 524}, ("Category Two times Ones", 4, 0): {0: 100000}, ("Category Two times Ones", 4, 1): {0: 48235, 2: 51765}, - ("Category Two times Ones", 4, 2): {0: 23289, 4: 76711}, - ("Category Two times Ones", 4, 3): {0: 11177, 6: 88823}, - ("Category Two times Ones", 4, 4): {0: 5499, 6: 94501}, - ("Category Two times Ones", 4, 5): {0: 18356, 6: 81644}, - ("Category Two times Ones", 4, 6): {0: 11169, 8: 88831}, - ("Category Two times Ones", 4, 7): {0: 6945, 8: 93055}, - ("Category Two times Ones", 4, 8): {0: 4091, 8: 95909}, + ("Category Two times Ones", 4, 2): {0: 23289, 2: 40678, 4: 36033}, + ("Category Two times Ones", 4, 3): {0: 11177, 2: 32677, 4: 56146}, + ("Category Two times Ones", 4, 4): {0: 5522, 4: 60436, 6: 34042}, + ("Category Two times Ones", 4, 5): {0: 4358, 6: 95642}, + ("Category Two times Ones", 4, 6): {0: 20, 6: 99980}, + ("Category Two times Ones", 4, 7): {6: 100000}, + ("Category Two times Ones", 4, 8): {6: 65250, 8: 34750}, ("Category Two times Ones", 5, 0): {0: 100000}, - ("Category Two times Ones", 5, 1): {0: 40028, 4: 59972}, - ("Category Two times Ones", 5, 2): {0: 16009, 6: 83991}, - ("Category Two times Ones", 5, 3): {0: 6489, 6: 93511}, - ("Category Two times Ones", 5, 4): {0: 16690, 8: 83310}, - ("Category Two times Ones", 5, 5): {0: 9016, 8: 90984}, - ("Category Two times Ones", 5, 6): {0: 4602, 8: 95398}, - ("Category Two times Ones", 5, 7): {0: 13627, 10: 86373}, - ("Category Two times Ones", 5, 8): {0: 8742, 10: 91258}, + ("Category Two times Ones", 5, 1): {0: 40028, 2: 59972}, + ("Category Two times Ones", 5, 2): {0: 16009, 2: 35901, 4: 48090}, + ("Category Two times Ones", 5, 3): {0: 6820, 4: 57489, 6: 35691}, + ("Category Two times Ones", 5, 4): {0: 5285, 6: 94715}, + ("Category Two times Ones", 5, 5): {0: 18, 6: 66613, 8: 33369}, + ("Category Two times Ones", 5, 6): {8: 99073, 6: 927}, + ("Category Two times Ones", 5, 7): {8: 100000}, + ("Category Two times Ones", 5, 8): {8: 100000}, ("Category Two times Ones", 6, 0): {0: 100000}, - ("Category Two times Ones", 6, 1): {0: 33502, 4: 66498}, - ("Category Two times Ones", 6, 2): {0: 11210, 6: 88790}, - ("Category Two times Ones", 6, 3): {0: 3673, 6: 96327}, - ("Category Two times Ones", 6, 4): {0: 9291, 8: 90709}, - ("Category Two times Ones", 6, 5): {0: 441, 8: 99559}, - ("Category Two times Ones", 6, 6): {0: 10255, 10: 89745}, - ("Category Two times Ones", 6, 7): {0: 5646, 10: 94354}, - ("Category Two times Ones", 6, 8): {0: 14287, 12: 85713}, + ("Category Two times Ones", 6, 1): {0: 33502, 2: 66498}, + ("Category Two times Ones", 6, 2): {0: 13681, 4: 59162, 2: 27157}, + ("Category Two times Ones", 6, 3): {0: 5486, 6: 94514}, + ("Category Two times Ones", 6, 4): {0: 190, 6: 62108, 8: 37702}, + ("Category Two times Ones", 6, 5): {8: 99882, 6: 118}, + ("Category Two times Ones", 6, 6): {8: 65144, 10: 34856}, + ("Category Two times Ones", 6, 7): {10: 99524, 8: 476}, + ("Category Two times Ones", 6, 8): {10: 100000}, ("Category Two times Ones", 7, 0): {0: 100000}, - ("Category Two times Ones", 7, 1): {0: 27683, 4: 72317}, - ("Category Two times Ones", 7, 2): {0: 7824, 6: 92176}, - ("Category Two times Ones", 7, 3): {0: 13167, 8: 86833}, - ("Category Two times Ones", 7, 4): {0: 564, 10: 99436}, - ("Category Two times Ones", 7, 5): {0: 9824, 10: 90176}, - ("Category Two times Ones", 7, 6): {0: 702, 12: 99298}, - ("Category Two times Ones", 7, 7): {0: 10186, 12: 89814}, - ("Category Two times Ones", 7, 8): {0: 942, 12: 99058}, + ("Category Two times Ones", 7, 1): {0: 27683, 2: 39060, 4: 33257}, + ("Category Two times Ones", 7, 2): {0: 8683, 4: 54932, 6: 36385}, + ("Category Two times Ones", 7, 3): {0: 373, 6: 66572, 8: 33055}, + ("Category Two times Ones", 7, 4): {8: 99816, 6: 184}, + ("Category Two times Ones", 7, 5): {8: 58124, 10: 41876}, + ("Category Two times Ones", 7, 6): {10: 99948, 8: 52}, + ("Category Two times Ones", 7, 7): {10: 62549, 12: 37451}, + ("Category Two times Ones", 7, 8): {12: 99818, 10: 182}, ("Category Two times Ones", 8, 0): {0: 100000}, - ("Category Two times Ones", 8, 1): {0: 23378, 4: 76622}, - ("Category Two times Ones", 8, 2): {0: 5420, 8: 94580}, - ("Category Two times Ones", 8, 3): {0: 8560, 10: 91440}, - ("Category Two times Ones", 8, 4): {0: 12199, 12: 87801}, - ("Category Two times Ones", 8, 5): {0: 879, 12: 99121}, - ("Category Two times Ones", 8, 6): {0: 9033, 14: 90967}, - ("Category Two times Ones", 8, 7): {0: 15767, 14: 84233}, - ("Category Two times Ones", 8, 8): {2: 9033, 14: 90967}, + ("Category Two times Ones", 8, 1): {0: 23378, 2: 37157, 4: 39465}, + ("Category Two times Ones", 8, 2): {0: 5602, 6: 94398}, + ("Category Two times Ones", 8, 3): {0: 8, 6: 10911, 8: 89081}, + ("Category Two times Ones", 8, 4): {8: 59809, 10: 40191}, + ("Category Two times Ones", 8, 5): {10: 68808, 12: 31114, 8: 78}, + ("Category Two times Ones", 8, 6): {12: 98712, 10: 1287, 8: 1}, + ("Category Two times Ones", 8, 7): {12: 100000}, + ("Category Two times Ones", 8, 8): {12: 59018, 14: 40982}, ("Category Half of Sixes", 0, 0): {0: 100000}, ("Category Half of Sixes", 0, 1): {0: 100000}, ("Category Half of Sixes", 0, 2): {0: 100000}, @@ -1469,386 +1461,386 @@ ("Category Half of Sixes", 2, 0): {0: 100000}, ("Category Half of Sixes", 2, 1): {0: 69419, 3: 30581}, ("Category Half of Sixes", 2, 2): {0: 48202, 3: 51798}, - ("Category Half of Sixes", 2, 3): {0: 33376, 6: 66624}, - ("Category Half of Sixes", 2, 4): {0: 23276, 6: 76724}, - ("Category Half of Sixes", 2, 5): {0: 16092, 6: 83908}, - ("Category Half of Sixes", 2, 6): {0: 11232, 6: 88768}, - ("Category Half of Sixes", 2, 7): {0: 7589, 6: 92411}, - ("Category Half of Sixes", 2, 8): {0: 5447, 6: 94553}, + ("Category Half of Sixes", 2, 3): {0: 33376, 3: 66624}, + ("Category Half of Sixes", 2, 4): {0: 23276, 3: 49810, 6: 26914}, + ("Category Half of Sixes", 2, 5): {0: 16092, 3: 47718, 6: 36190}, + ("Category Half of Sixes", 2, 6): {0: 11232, 3: 44515, 6: 44253}, + ("Category Half of Sixes", 2, 7): {0: 7589, 3: 40459, 6: 51952}, + ("Category Half of Sixes", 2, 8): {0: 5447, 3: 35804, 6: 58749}, ("Category Half of Sixes", 3, 0): {0: 100000}, ("Category Half of Sixes", 3, 1): {0: 57964, 3: 42036}, - ("Category Half of Sixes", 3, 2): {0: 33637, 6: 66363}, - ("Category Half of Sixes", 3, 3): {0: 19520, 6: 80480}, - ("Category Half of Sixes", 3, 4): {0: 11265, 6: 88735}, - ("Category Half of Sixes", 3, 5): {0: 6419, 6: 72177, 9: 21404}, - ("Category Half of Sixes", 3, 6): {0: 3810, 6: 66884, 9: 29306}, - ("Category Half of Sixes", 3, 7): {0: 2174, 6: 60595, 9: 37231}, - ("Category Half of Sixes", 3, 8): {0: 1237, 6: 53693, 9: 45070}, + ("Category Half of Sixes", 3, 2): {0: 33637, 3: 44263, 6: 22100}, + ("Category Half of Sixes", 3, 3): {0: 19520, 3: 42382, 6: 38098}, + ("Category Half of Sixes", 3, 4): {0: 11265, 3: 35772, 6: 52963}, + ("Category Half of Sixes", 3, 5): {0: 6419, 3: 28916, 6: 43261, 9: 21404}, + ("Category Half of Sixes", 3, 6): {0: 3810, 3: 22496, 6: 44388, 9: 29306}, + ("Category Half of Sixes", 3, 7): {0: 1317, 6: 30047, 9: 68636}, + ("Category Half of Sixes", 3, 8): {0: 750, 9: 99250}, ("Category Half of Sixes", 4, 0): {0: 100000}, - ("Category Half of Sixes", 4, 1): {0: 48121, 6: 51879}, - ("Category Half of Sixes", 4, 2): {0: 23296, 6: 76704}, - ("Category Half of Sixes", 4, 3): {0: 11233, 6: 68363, 9: 20404}, - ("Category Half of Sixes", 4, 4): {0: 5463, 6: 60738, 9: 33799}, - ("Category Half of Sixes", 4, 5): {0: 2691, 6: 50035, 12: 47274}, - ("Category Half of Sixes", 4, 6): {0: 11267, 9: 88733}, - ("Category Half of Sixes", 4, 7): {0: 6921, 9: 66034, 12: 27045}, - ("Category Half of Sixes", 4, 8): {0: 4185, 9: 61079, 12: 34736}, + ("Category Half of Sixes", 4, 1): {0: 48121, 3: 51879}, + ("Category Half of Sixes", 4, 2): {0: 23296, 3: 40989, 6: 35715}, + ("Category Half of Sixes", 4, 3): {0: 11233, 3: 32653, 6: 35710, 9: 20404}, + ("Category Half of Sixes", 4, 4): {0: 5463, 3: 23270, 6: 37468, 9: 33799}, + ("Category Half of Sixes", 4, 5): {0: 5225, 6: 29678, 9: 65097}, + ("Category Half of Sixes", 4, 6): {0: 3535, 9: 96465}, + ("Category Half of Sixes", 4, 7): {0: 6, 9: 72939, 12: 27055}, + ("Category Half of Sixes", 4, 8): {9: 25326, 12: 74674}, ("Category Half of Sixes", 5, 0): {0: 100000}, - ("Category Half of Sixes", 5, 1): {0: 40183, 6: 59817}, - ("Category Half of Sixes", 5, 2): {0: 16197, 6: 83803}, - ("Category Half of Sixes", 5, 3): {0: 6583, 6: 57826, 9: 35591}, - ("Category Half of Sixes", 5, 4): {0: 2636, 9: 76577, 12: 20787}, - ("Category Half of Sixes", 5, 5): {0: 8879, 9: 57821, 12: 33300}, - ("Category Half of Sixes", 5, 6): {0: 4652, 12: 95348}, - ("Category Half of Sixes", 5, 7): {0: 2365, 12: 97635}, - ("Category Half of Sixes", 5, 8): {0: 8671, 12: 64865, 15: 26464}, + ("Category Half of Sixes", 5, 1): {0: 40183, 3: 59817}, + ("Category Half of Sixes", 5, 2): {0: 16197, 3: 35494, 6: 48309}, + ("Category Half of Sixes", 5, 3): {0: 6583, 3: 23394, 6: 34432, 9: 35591}, + ("Category Half of Sixes", 5, 4): {0: 5007, 6: 25159, 9: 49038, 12: 20796}, + ("Category Half of Sixes", 5, 5): {0: 2900, 9: 38935, 12: 58165}, + ("Category Half of Sixes", 5, 6): {0: 2090, 12: 97910}, + ("Category Half of Sixes", 5, 7): {12: 99994, 9: 6}, + ("Category Half of Sixes", 5, 8): {12: 73524, 15: 26476}, ("Category Half of Sixes", 6, 0): {0: 100000}, - ("Category Half of Sixes", 6, 1): {0: 33473, 6: 66527}, - ("Category Half of Sixes", 6, 2): {0: 11147, 6: 62222, 9: 26631}, - ("Category Half of Sixes", 6, 3): {0: 3628, 9: 75348, 12: 21024}, - ("Category Half of Sixes", 6, 4): {0: 9498, 9: 52940, 15: 37562}, - ("Category Half of Sixes", 6, 5): {0: 4236, 12: 72944, 15: 22820}, - ("Category Half of Sixes", 6, 6): {0: 10168, 12: 55072, 15: 34760}, - ("Category Half of Sixes", 6, 7): {0: 5519, 15: 94481}, - ("Category Half of Sixes", 6, 8): {0: 2968, 15: 76504, 18: 20528}, + ("Category Half of Sixes", 6, 1): {0: 33473, 3: 40175, 6: 26352}, + ("Category Half of Sixes", 6, 2): {0: 11147, 3: 29592, 6: 32630, 9: 26631}, + ("Category Half of Sixes", 6, 3): {0: 2460, 6: 21148, 9: 55356, 12: 21036}, + ("Category Half of Sixes", 6, 4): {0: 997, 9: 29741, 12: 69262}, + ("Category Half of Sixes", 6, 5): {0: 831, 12: 76328, 15: 22841}, + ("Category Half of Sixes", 6, 6): {12: 29960, 15: 70040}, + ("Category Half of Sixes", 6, 7): {15: 100000}, + ("Category Half of Sixes", 6, 8): {15: 79456, 18: 20544}, ("Category Half of Sixes", 7, 0): {0: 100000}, - ("Category Half of Sixes", 7, 1): {0: 27933, 6: 72067}, - ("Category Half of Sixes", 7, 2): {0: 7794, 6: 55728, 12: 36478}, - ("Category Half of Sixes", 7, 3): {0: 2138, 9: 64554, 15: 33308}, - ("Category Half of Sixes", 7, 4): {0: 5238, 12: 69214, 15: 25548}, - ("Category Half of Sixes", 7, 5): {0: 9894, 15: 90106}, - ("Category Half of Sixes", 7, 6): {0: 4656, 15: 69353, 18: 25991}, - ("Category Half of Sixes", 7, 7): {0: 10005, 15: 52430, 18: 37565}, - ("Category Half of Sixes", 7, 8): {0: 5710, 18: 94290}, + ("Category Half of Sixes", 7, 1): {0: 27933, 3: 39105, 6: 32962}, + ("Category Half of Sixes", 7, 2): {0: 7794, 3: 23896, 6: 31832, 9: 36478}, + ("Category Half of Sixes", 7, 3): {0: 1321, 9: 40251, 12: 58428}, + ("Category Half of Sixes", 7, 4): {0: 370, 12: 74039, 15: 25591}, + ("Category Half of Sixes", 7, 5): {0: 6, 15: 98660, 12: 1334}, + ("Category Half of Sixes", 7, 6): {15: 73973, 18: 26027}, + ("Category Half of Sixes", 7, 7): {18: 100000}, + ("Category Half of Sixes", 7, 8): {18: 100000}, ("Category Half of Sixes", 8, 0): {0: 100000}, - ("Category Half of Sixes", 8, 1): {0: 23337, 6: 76663}, - ("Category Half of Sixes", 8, 2): {0: 5310, 9: 74178, 12: 20512}, - ("Category Half of Sixes", 8, 3): {0: 8656, 12: 70598, 15: 20746}, - ("Category Half of Sixes", 8, 4): {0: 291, 12: 59487, 18: 40222}, - ("Category Half of Sixes", 8, 5): {0: 5145, 15: 63787, 18: 31068}, - ("Category Half of Sixes", 8, 6): {0: 8804, 18: 91196}, - ("Category Half of Sixes", 8, 7): {0: 4347, 18: 65663, 21: 29990}, - ("Category Half of Sixes", 8, 8): {0: 9252, 21: 90748}, + ("Category Half of Sixes", 8, 1): {0: 23337, 3: 37232, 6: 39431}, + ("Category Half of Sixes", 8, 2): {0: 4652, 6: 29310, 9: 45517, 12: 20521}, + ("Category Half of Sixes", 8, 3): {0: 1300, 12: 77919, 15: 20781}, + ("Category Half of Sixes", 8, 4): {0: 21, 15: 98678, 12: 1301}, + ("Category Half of Sixes", 8, 5): {15: 68893, 18: 31107}, + ("Category Half of Sixes", 8, 6): {18: 100000}, + ("Category Half of Sixes", 8, 7): {18: 69986, 21: 30014}, + ("Category Half of Sixes", 8, 8): {21: 98839, 18: 1161}, ("Category Twos and Threes", 1, 1): {0: 66466, 2: 33534}, ("Category Twos and Threes", 1, 2): {0: 55640, 2: 44360}, - ("Category Twos and Threes", 1, 3): {0: 57822, 3: 42178}, - ("Category Twos and Threes", 1, 4): {0: 48170, 3: 51830}, - ("Category Twos and Threes", 1, 5): {0: 40294, 3: 59706}, - ("Category Twos and Threes", 1, 6): {0: 33417, 3: 66583}, - ("Category Twos and Threes", 1, 7): {0: 27852, 3: 72148}, - ("Category Twos and Threes", 1, 8): {0: 23364, 3: 76636}, - ("Category Twos and Threes", 2, 1): {0: 44565, 3: 55435}, - ("Category Twos and Threes", 2, 2): {0: 46335, 3: 53665}, - ("Category Twos and Threes", 2, 3): {0: 32347, 3: 67653}, - ("Category Twos and Threes", 2, 4): {0: 22424, 5: 77576}, - ("Category Twos and Threes", 2, 5): {0: 15661, 6: 84339}, - ("Category Twos and Threes", 2, 6): {0: 10775, 6: 89225}, - ("Category Twos and Threes", 2, 7): {0: 7375, 6: 92625}, - ("Category Twos and Threes", 2, 8): {0: 5212, 6: 94788}, - ("Category Twos and Threes", 3, 1): {0: 29892, 3: 70108}, - ("Category Twos and Threes", 3, 2): {0: 17285, 5: 82715}, - ("Category Twos and Threes", 3, 3): {0: 17436, 6: 82564}, - ("Category Twos and Threes", 3, 4): {0: 9962, 6: 90038}, - ("Category Twos and Threes", 3, 5): {0: 3347, 6: 96653}, - ("Category Twos and Threes", 3, 6): {0: 1821, 8: 98179}, - ("Category Twos and Threes", 3, 7): {0: 1082, 6: 61417, 9: 37501}, - ("Category Twos and Threes", 3, 8): {0: 13346, 9: 86654}, - ("Category Twos and Threes", 4, 1): {0: 19619, 5: 80381}, - ("Category Twos and Threes", 4, 2): {0: 18914, 6: 81086}, - ("Category Twos and Threes", 4, 3): {0: 4538, 5: 61859, 8: 33603}, - ("Category Twos and Threes", 4, 4): {0: 2183, 6: 62279, 9: 35538}, - ("Category Twos and Threes", 4, 5): {0: 16416, 9: 83584}, - ("Category Twos and Threes", 4, 6): {0: 6285, 9: 93715}, - ("Category Twos and Threes", 4, 7): {0: 30331, 11: 69669}, - ("Category Twos and Threes", 4, 8): {0: 22305, 12: 77695}, - ("Category Twos and Threes", 5, 1): {0: 13070, 5: 86930}, - ("Category Twos and Threes", 5, 2): {0: 5213, 5: 61441, 8: 33346}, - ("Category Twos and Threes", 5, 3): {0: 2126, 6: 58142, 9: 39732}, - ("Category Twos and Threes", 5, 4): {0: 848, 2: 30734, 11: 68418}, - ("Category Twos and Threes", 5, 5): {0: 29502, 12: 70498}, - ("Category Twos and Threes", 5, 6): {0: 123, 9: 52792, 12: 47085}, - ("Category Twos and Threes", 5, 7): {0: 8241, 12: 91759}, - ("Category Twos and Threes", 5, 8): {0: 13, 2: 31670, 14: 68317}, - ("Category Twos and Threes", 6, 1): {0: 22090, 6: 77910}, - ("Category Twos and Threes", 6, 2): {0: 2944, 6: 62394, 9: 34662}, - ("Category Twos and Threes", 6, 3): {0: 977, 2: 30626, 11: 68397}, - ("Category Twos and Threes", 6, 4): {0: 320, 8: 58370, 12: 41310}, - ("Category Twos and Threes", 6, 5): {0: 114, 2: 31718, 14: 68168}, - ("Category Twos and Threes", 6, 6): {0: 29669, 15: 70331}, - ("Category Twos and Threes", 6, 7): {0: 19855, 15: 80145}, - ("Category Twos and Threes", 6, 8): {0: 8524, 15: 91476}, - ("Category Twos and Threes", 7, 1): {0: 5802, 4: 54580, 7: 39618}, - ("Category Twos and Threes", 7, 2): {0: 1605, 6: 62574, 10: 35821}, - ("Category Twos and Threes", 7, 3): {0: 471, 8: 59691, 12: 39838}, - ("Category Twos and Threes", 7, 4): {0: 26620, 14: 73380}, - ("Category Twos and Threes", 7, 5): {0: 17308, 11: 37515, 15: 45177}, - ("Category Twos and Threes", 7, 6): {0: 30281, 17: 69719}, - ("Category Twos and Threes", 7, 7): {0: 28433, 18: 71567}, - ("Category Twos and Threes", 7, 8): {0: 13274, 18: 86726}, - ("Category Twos and Threes", 8, 1): {0: 3799, 5: 56614, 8: 39587}, - ("Category Twos and Threes", 8, 2): {0: 902, 7: 58003, 11: 41095}, - ("Category Twos and Threes", 8, 3): {0: 29391, 14: 70609}, - ("Category Twos and Threes", 8, 4): {0: 26041, 12: 40535, 16: 33424}, - ("Category Twos and Threes", 8, 5): {0: 26328, 14: 38760, 18: 34912}, - ("Category Twos and Threes", 8, 6): {0: 22646, 15: 45218, 19: 32136}, - ("Category Twos and Threes", 8, 7): {0: 25908, 20: 74092}, - ("Category Twos and Threes", 8, 8): {3: 18441, 17: 38826, 21: 42733}, - ("Category Sum of Odds", 1, 1): {0: 66572, 5: 33428}, - ("Category Sum of Odds", 1, 2): {0: 44489, 5: 55511}, - ("Category Sum of Odds", 1, 3): {0: 37185, 5: 62815}, - ("Category Sum of Odds", 1, 4): {0: 30917, 5: 69083}, - ("Category Sum of Odds", 1, 5): {0: 41833, 5: 58167}, - ("Category Sum of Odds", 1, 6): {0: 34902, 5: 65098}, - ("Category Sum of Odds", 1, 7): {0: 29031, 5: 70969}, - ("Category Sum of Odds", 1, 8): {0: 24051, 5: 75949}, - ("Category Sum of Odds", 2, 1): {0: 66460, 5: 33540}, - ("Category Sum of Odds", 2, 2): {0: 11216, 5: 65597, 8: 23187}, - ("Category Sum of Odds", 2, 3): {0: 30785, 8: 69215}, - ("Category Sum of Odds", 2, 4): {0: 21441, 10: 78559}, - ("Category Sum of Odds", 2, 5): {0: 14948, 10: 85052}, - ("Category Sum of Odds", 2, 6): {0: 4657, 3: 35569, 10: 59774}, - ("Category Sum of Odds", 2, 7): {0: 7262, 5: 42684, 10: 50054}, - ("Category Sum of Odds", 2, 8): {0: 4950, 5: 37432, 10: 57618}, - ("Category Sum of Odds", 3, 1): {0: 29203, 6: 70797}, - ("Category Sum of Odds", 3, 2): {0: 34454, 9: 65546}, - ("Category Sum of Odds", 3, 3): {0: 5022, 3: 32067, 8: 45663, 13: 17248}, - ("Category Sum of Odds", 3, 4): {0: 6138, 4: 33396, 13: 60466}, - ("Category Sum of Odds", 3, 5): {0: 29405, 15: 70595}, - ("Category Sum of Odds", 3, 6): {0: 21390, 15: 78610}, - ("Category Sum of Odds", 3, 7): {0: 8991, 8: 38279, 15: 52730}, - ("Category Sum of Odds", 3, 8): {0: 6340, 8: 34003, 15: 59657}, - ("Category Sum of Odds", 4, 1): {0: 28095, 4: 38198, 8: 33707}, - ("Category Sum of Odds", 4, 2): {0: 27003, 11: 72997}, - ("Category Sum of Odds", 4, 3): {0: 18712, 8: 40563, 13: 40725}, - ("Category Sum of Odds", 4, 4): {0: 30691, 15: 69309}, - ("Category Sum of Odds", 4, 5): {0: 433, 3: 32140, 13: 43150, 18: 24277}, - ("Category Sum of Odds", 4, 6): {0: 6549, 9: 32451, 15: 43220, 20: 17780}, - ("Category Sum of Odds", 4, 7): {0: 29215, 15: 45491, 20: 25294}, - ("Category Sum of Odds", 4, 8): {0: 11807, 13: 38927, 20: 49266}, - ("Category Sum of Odds", 5, 1): {0: 25139, 9: 74861}, - ("Category Sum of Odds", 5, 2): {0: 25110, 9: 40175, 14: 34715}, - ("Category Sum of Odds", 5, 3): {0: 23453, 11: 37756, 16: 38791}, - ("Category Sum of Odds", 5, 4): {0: 22993, 13: 37263, 18: 39744}, - ("Category Sum of Odds", 5, 5): {0: 25501, 15: 38407, 20: 36092}, - ("Category Sum of Odds", 5, 6): {0: 2542, 10: 32537, 18: 41122, 23: 23799}, - ("Category Sum of Odds", 5, 7): {0: 8228, 14: 32413, 20: 41289, 25: 18070}, - ("Category Sum of Odds", 5, 8): {0: 2, 2: 31173, 20: 43652, 25: 25173}, - ("Category Sum of Odds", 6, 1): {0: 23822, 6: 40166, 11: 36012}, - ("Category Sum of Odds", 6, 2): {0: 24182, 11: 37137, 16: 38681}, - ("Category Sum of Odds", 6, 3): {0: 27005, 14: 35759, 19: 37236}, - ("Category Sum of Odds", 6, 4): {0: 25133, 16: 35011, 21: 39856}, - ("Category Sum of Odds", 6, 5): {0: 24201, 18: 34934, 23: 40865}, - ("Category Sum of Odds", 6, 6): {0: 12978, 17: 32943, 23: 36836, 28: 17243}, - ("Category Sum of Odds", 6, 7): {0: 2314, 14: 32834, 23: 40134, 28: 24718}, - ("Category Sum of Odds", 6, 8): {0: 5464, 18: 34562, 25: 40735, 30: 19239}, - ("Category Sum of Odds", 7, 1): {0: 29329, 8: 37697, 13: 32974}, - ("Category Sum of Odds", 7, 2): {0: 29935, 14: 34878, 19: 35187}, - ("Category Sum of Odds", 7, 3): {0: 30638, 17: 33733, 22: 35629}, - ("Category Sum of Odds", 7, 4): {0: 163, 6: 32024, 20: 33870, 25: 33943}, - ("Category Sum of Odds", 7, 5): {0: 31200, 22: 35565, 27: 33235}, - ("Category Sum of Odds", 7, 6): {2: 30174, 24: 36670, 29: 33156}, - ("Category Sum of Odds", 7, 7): {4: 8712, 21: 35208, 28: 36799, 33: 19281}, - ("Category Sum of Odds", 7, 8): {0: 1447, 18: 32027, 28: 39941, 33: 26585}, - ("Category Sum of Odds", 8, 1): {0: 26931, 9: 35423, 14: 37646}, - ("Category Sum of Odds", 8, 2): {0: 29521, 16: 32919, 21: 37560}, - ("Category Sum of Odds", 8, 3): {0: 412, 7: 32219, 20: 32055, 25: 35314}, - ("Category Sum of Odds", 8, 4): {1: 27021, 22: 36376, 28: 36603}, - ("Category Sum of Odds", 8, 5): {1: 1069, 14: 32451, 26: 32884, 31: 33596}, - ("Category Sum of Odds", 8, 6): {4: 31598, 28: 33454, 33: 34948}, - ("Category Sum of Odds", 8, 7): {6: 27327, 29: 35647, 34: 37026}, - ("Category Sum of Odds", 8, 8): {4: 1, 26: 40489, 33: 37825, 38: 21685}, - ("Category Sum of Evens", 1, 1): {0: 49585, 6: 50415}, - ("Category Sum of Evens", 1, 2): {0: 44331, 6: 55669}, - ("Category Sum of Evens", 1, 3): {0: 29576, 6: 70424}, - ("Category Sum of Evens", 1, 4): {0: 24744, 6: 75256}, - ("Category Sum of Evens", 1, 5): {0: 20574, 6: 79426}, - ("Category Sum of Evens", 1, 6): {0: 17182, 6: 82818}, - ("Category Sum of Evens", 1, 7): {0: 14152, 6: 85848}, - ("Category Sum of Evens", 1, 8): {0: 8911, 6: 91089}, - ("Category Sum of Evens", 2, 1): {0: 25229, 8: 74771}, - ("Category Sum of Evens", 2, 2): {0: 18682, 6: 58078, 10: 23240}, - ("Category Sum of Evens", 2, 3): {0: 8099, 10: 91901}, - ("Category Sum of Evens", 2, 4): {0: 16906, 12: 83094}, - ("Category Sum of Evens", 2, 5): {0: 11901, 12: 88099}, - ("Category Sum of Evens", 2, 6): {0: 8054, 12: 91946}, - ("Category Sum of Evens", 2, 7): {0: 5695, 12: 94305}, - ("Category Sum of Evens", 2, 8): {0: 3950, 12: 96050}, - ("Category Sum of Evens", 3, 1): {0: 25054, 6: 51545, 10: 23401}, - ("Category Sum of Evens", 3, 2): {0: 17863, 10: 64652, 14: 17485}, - ("Category Sum of Evens", 3, 3): {0: 7748, 12: 75072, 16: 17180}, - ("Category Sum of Evens", 3, 4): {0: 1318, 12: 70339, 16: 28343}, - ("Category Sum of Evens", 3, 5): {0: 7680, 12: 53582, 18: 38738}, - ("Category Sum of Evens", 3, 6): {0: 1475, 12: 50152, 18: 48373}, - ("Category Sum of Evens", 3, 7): {0: 14328, 18: 85672}, - ("Category Sum of Evens", 3, 8): {0: 10001, 18: 89999}, - ("Category Sum of Evens", 4, 1): {0: 6214, 8: 67940, 12: 25846}, - ("Category Sum of Evens", 4, 2): {0: 16230, 12: 55675, 16: 28095}, - ("Category Sum of Evens", 4, 3): {0: 11069, 16: 70703, 20: 18228}, - ("Category Sum of Evens", 4, 4): {0: 13339, 20: 86661}, - ("Category Sum of Evens", 4, 5): {0: 8193, 18: 66423, 22: 25384}, - ("Category Sum of Evens", 4, 6): {0: 11127, 18: 53742, 22: 35131}, - ("Category Sum of Evens", 4, 7): {0: 7585, 18: 48073, 24: 44342}, - ("Category Sum of Evens", 4, 8): {0: 642, 18: 46588, 24: 52770}, - ("Category Sum of Evens", 5, 1): {0: 8373, 8: 50641, 16: 40986}, - ("Category Sum of Evens", 5, 2): {0: 7271, 12: 42254, 20: 50475}, - ("Category Sum of Evens", 5, 3): {0: 8350, 16: 44711, 24: 46939}, - ("Category Sum of Evens", 5, 4): {0: 8161, 18: 44426, 26: 47413}, - ("Category Sum of Evens", 5, 5): {0: 350, 8: 16033, 24: 67192, 28: 16425}, - ("Category Sum of Evens", 5, 6): {0: 10318, 24: 64804, 28: 24878}, - ("Category Sum of Evens", 5, 7): {0: 12783, 24: 52804, 28: 34413}, - ("Category Sum of Evens", 5, 8): {0: 1, 24: 56646, 30: 43353}, - ("Category Sum of Evens", 6, 1): {0: 10482, 10: 48137, 18: 41381}, - ("Category Sum of Evens", 6, 2): {0: 12446, 16: 43676, 24: 43878}, - ("Category Sum of Evens", 6, 3): {0: 11037, 20: 44249, 28: 44714}, - ("Category Sum of Evens", 6, 4): {0: 10005, 22: 42316, 30: 47679}, - ("Category Sum of Evens", 6, 5): {0: 9751, 24: 42204, 32: 48045}, - ("Category Sum of Evens", 6, 6): {0: 9692, 26: 45108, 34: 45200}, - ("Category Sum of Evens", 6, 7): {4: 1437, 26: 42351, 34: 56212}, - ("Category Sum of Evens", 6, 8): {4: 13017, 30: 51814, 36: 35169}, - ("Category Sum of Evens", 7, 1): {0: 12688, 12: 45275, 20: 42037}, - ("Category Sum of Evens", 7, 2): {0: 1433, 20: 60350, 28: 38217}, - ("Category Sum of Evens", 7, 3): {0: 13724, 24: 43514, 32: 42762}, - ("Category Sum of Evens", 7, 4): {0: 11285, 26: 40694, 34: 48021}, - ("Category Sum of Evens", 7, 5): {4: 5699, 28: 43740, 36: 50561}, - ("Category Sum of Evens", 7, 6): {4: 5478, 30: 43711, 38: 50811}, - ("Category Sum of Evens", 7, 7): {6: 9399, 32: 43251, 40: 47350}, - ("Category Sum of Evens", 7, 8): {10: 1490, 32: 40719, 40: 57791}, - ("Category Sum of Evens", 8, 1): {0: 14585, 14: 42804, 22: 42611}, - ("Category Sum of Evens", 8, 2): {0: 15891, 22: 39707, 30: 44402}, - ("Category Sum of Evens", 8, 3): {2: 297, 12: 16199, 28: 42274, 36: 41230}, - ("Category Sum of Evens", 8, 4): {0: 7625, 30: 43948, 38: 48427}, - ("Category Sum of Evens", 8, 5): {4: 413, 18: 16209, 34: 43301, 42: 40077}, - ("Category Sum of Evens", 8, 6): {6: 14927, 36: 43139, 44: 41934}, - ("Category Sum of Evens", 8, 7): {8: 5042, 36: 40440, 44: 54518}, - ("Category Sum of Evens", 8, 8): {10: 5005, 38: 44269, 46: 50726}, - ("Category Double Threes and Fours", 1, 1): {0: 66749, 8: 33251}, - ("Category Double Threes and Fours", 1, 2): {0: 44675, 8: 55325}, - ("Category Double Threes and Fours", 1, 3): {0: 29592, 8: 70408}, - ("Category Double Threes and Fours", 1, 4): {0: 24601, 8: 75399}, - ("Category Double Threes and Fours", 1, 5): {0: 20499, 8: 79501}, - ("Category Double Threes and Fours", 1, 6): {0: 17116, 8: 82884}, - ("Category Double Threes and Fours", 1, 7): {0: 14193, 8: 85807}, - ("Category Double Threes and Fours", 1, 8): {0: 11977, 8: 88023}, - ("Category Double Threes and Fours", 2, 1): {0: 44382, 8: 55618}, - ("Category Double Threes and Fours", 2, 2): {0: 19720, 8: 57236, 14: 23044}, - ("Category Double Threes and Fours", 2, 3): {0: 8765, 8: 41937, 14: 49298}, - ("Category Double Threes and Fours", 2, 4): {0: 6164, 16: 93836}, - ("Category Double Threes and Fours", 2, 5): {0: 4307, 8: 38682, 16: 57011}, - ("Category Double Threes and Fours", 2, 6): {0: 2879, 8: 32717, 16: 64404}, - ("Category Double Threes and Fours", 2, 7): {0: 6679, 16: 93321}, - ("Category Double Threes and Fours", 2, 8): {0: 4758, 16: 95242}, - ("Category Double Threes and Fours", 3, 1): {0: 29378, 8: 50024, 14: 20598}, - ("Category Double Threes and Fours", 3, 2): {0: 8894, 14: 74049, 18: 17057}, - ("Category Double Threes and Fours", 3, 3): {0: 2643, 14: 62555, 22: 34802}, - ("Category Double Threes and Fours", 3, 4): {0: 1523, 6: 19996, 16: 50281, 22: 28200}, - ("Category Double Threes and Fours", 3, 5): {0: 845, 16: 60496, 24: 38659}, - ("Category Double Threes and Fours", 3, 6): {0: 499, 16: 51131, 24: 48370}, - ("Category Double Threes and Fours", 3, 7): {0: 5542, 16: 37755, 24: 56703}, - ("Category Double Threes and Fours", 3, 8): {0: 3805, 16: 32611, 24: 63584}, - ("Category Double Threes and Fours", 4, 1): {0: 19809, 8: 39303, 16: 40888}, - ("Category Double Threes and Fours", 4, 2): {0: 3972, 16: 71506, 22: 24522}, - ("Category Double Threes and Fours", 4, 3): {0: 745, 18: 53727, 22: 28503, 28: 17025}, - ("Category Double Threes and Fours", 4, 4): {0: 4862, 16: 34879, 22: 33529, 28: 26730}, - ("Category Double Threes and Fours", 4, 5): {0: 2891, 16: 25367, 24: 46333, 30: 25409}, - ("Category Double Threes and Fours", 4, 6): {0: 2525, 24: 62353, 30: 35122}, - ("Category Double Threes and Fours", 4, 7): {0: 1042, 24: 54543, 32: 44415}, - ("Category Double Threes and Fours", 4, 8): {0: 2510, 24: 44681, 32: 52809}, - ("Category Double Threes and Fours", 5, 1): {0: 13122, 14: 68022, 20: 18856}, - ("Category Double Threes and Fours", 5, 2): {0: 1676, 14: 37791, 22: 40810, 28: 19723}, - ("Category Double Threes and Fours", 5, 3): {0: 2945, 16: 28193, 22: 26795, 32: 42067}, - ("Category Double Threes and Fours", 5, 4): {0: 2807, 26: 53419, 30: 26733, 36: 17041}, - ("Category Double Threes and Fours", 5, 5): {0: 3651, 24: 38726, 32: 41484, 38: 16139}, - ("Category Double Threes and Fours", 5, 6): {0: 362, 12: 13070, 32: 61608, 38: 24960}, - ("Category Double Threes and Fours", 5, 7): {0: 161, 12: 15894, 32: 49464, 38: 34481}, - ("Category Double Threes and Fours", 5, 8): {0: 82, 12: 11438, 32: 45426, 40: 43054}, - ("Category Double Threes and Fours", 6, 1): {0: 8738, 6: 26451, 16: 43879, 22: 20932}, - ("Category Double Threes and Fours", 6, 2): {0: 784, 16: 38661, 28: 42164, 32: 18391}, - ("Category Double Threes and Fours", 6, 3): {0: 1062, 22: 34053, 28: 27996, 38: 36889}, - ("Category Double Threes and Fours", 6, 4): {0: 439, 12: 13100, 30: 43296, 40: 43165}, - ("Category Double Threes and Fours", 6, 5): {0: 3957, 34: 51190, 38: 26734, 44: 18119}, - ("Category Double Threes and Fours", 6, 6): {0: 4226, 32: 37492, 40: 40719, 46: 17563}, - ("Category Double Threes and Fours", 6, 7): {0: 31, 12: 13933, 40: 60102, 46: 25934}, - ("Category Double Threes and Fours", 6, 8): {8: 388, 22: 16287, 40: 48255, 48: 35070}, - ("Category Double Threes and Fours", 7, 1): {0: 5803, 8: 28280, 14: 26186, 26: 39731}, - ("Category Double Threes and Fours", 7, 2): {0: 3319, 20: 36331, 30: 38564, 36: 21786}, - ("Category Double Threes and Fours", 7, 3): {0: 2666, 18: 16444, 34: 41412, 44: 39478}, - ("Category Double Threes and Fours", 7, 4): {0: 99, 12: 9496, 38: 50302, 46: 40103}, - ("Category Double Threes and Fours", 7, 5): {0: 45, 12: 13200, 42: 52460, 50: 34295}, - ("Category Double Threes and Fours", 7, 6): {8: 2400, 28: 16653, 46: 60564, 52: 20383}, - ("Category Double Threes and Fours", 7, 7): {6: 7, 12: 11561, 44: 44119, 54: 44313}, - ("Category Double Threes and Fours", 7, 8): {8: 4625, 44: 40601, 48: 26475, 54: 28299}, - ("Category Double Threes and Fours", 8, 1): {0: 3982, 16: 56447, 28: 39571}, - ("Category Double Threes and Fours", 8, 2): {0: 1645, 20: 25350, 30: 37385, 42: 35620}, - ("Category Double Threes and Fours", 8, 3): {0: 6, 26: 23380, 40: 40181, 50: 36433}, - ("Category Double Threes and Fours", 8, 4): {0: 541, 20: 16547, 42: 38406, 52: 44506}, - ("Category Double Threes and Fours", 8, 5): {6: 2956, 30: 16449, 46: 43983, 56: 36612}, - ("Category Double Threes and Fours", 8, 6): {0: 2, 12: 7360, 38: 19332, 54: 53627, 58: 19679}, - ("Category Double Threes and Fours", 8, 7): {6: 9699, 48: 38611, 54: 28390, 60: 23300}, - ("Category Double Threes and Fours", 8, 8): {8: 5, 20: 10535, 52: 41790, 62: 47670}, - ("Category Quadruple Ones and Twos", 1, 1): {0: 66567, 8: 33433}, - ("Category Quadruple Ones and Twos", 1, 2): {0: 44809, 8: 55191}, - ("Category Quadruple Ones and Twos", 1, 3): {0: 37100, 8: 62900}, - ("Category Quadruple Ones and Twos", 1, 4): {0: 30963, 8: 69037}, - ("Category Quadruple Ones and Twos", 1, 5): {0: 25316, 8: 74684}, - ("Category Quadruple Ones and Twos", 1, 6): {0: 21505, 8: 78495}, - ("Category Quadruple Ones and Twos", 1, 7): {0: 17676, 8: 82324}, - ("Category Quadruple Ones and Twos", 1, 8): {0: 14971, 8: 85029}, - ("Category Quadruple Ones and Twos", 2, 1): {0: 44566, 8: 55434}, - ("Category Quadruple Ones and Twos", 2, 2): {0: 19963, 8: 57152, 12: 22885}, - ("Category Quadruple Ones and Twos", 2, 3): {0: 13766, 8: 52065, 16: 34169}, - ("Category Quadruple Ones and Twos", 2, 4): {0: 9543, 8: 46446, 16: 44011}, - ("Category Quadruple Ones and Twos", 2, 5): {0: 6472, 8: 40772, 16: 52756}, - ("Category Quadruple Ones and Twos", 2, 6): {0: 10306, 12: 46932, 16: 42762}, - ("Category Quadruple Ones and Twos", 2, 7): {0: 7120, 12: 42245, 16: 50635}, - ("Category Quadruple Ones and Twos", 2, 8): {0: 4989, 12: 37745, 16: 57266}, - ("Category Quadruple Ones and Twos", 3, 1): {0: 29440, 8: 50321, 16: 20239}, - ("Category Quadruple Ones and Twos", 3, 2): {0: 8857, 8: 42729, 16: 48414}, - ("Category Quadruple Ones and Twos", 3, 3): {0: 5063, 12: 53387, 20: 41550}, - ("Category Quadruple Ones and Twos", 3, 4): {0: 8395, 16: 64605, 24: 27000}, - ("Category Quadruple Ones and Twos", 3, 5): {0: 4895, 16: 58660, 24: 36445}, - ("Category Quadruple Ones and Twos", 3, 6): {0: 2681, 16: 52710, 24: 44609}, - ("Category Quadruple Ones and Twos", 3, 7): {0: 586, 16: 46781, 24: 52633}, - ("Category Quadruple Ones and Twos", 3, 8): {0: 941, 16: 39406, 24: 59653}, - ("Category Quadruple Ones and Twos", 4, 1): {0: 19691, 8: 46945, 16: 33364}, - ("Category Quadruple Ones and Twos", 4, 2): {0: 4023, 12: 50885, 24: 45092}, - ("Category Quadruple Ones and Twos", 4, 3): {0: 6553, 16: 52095, 28: 41352}, - ("Category Quadruple Ones and Twos", 4, 4): {0: 3221, 16: 41367, 24: 39881, 28: 15531}, - ("Category Quadruple Ones and Twos", 4, 5): {0: 1561, 20: 48731, 28: 49708}, - ("Category Quadruple Ones and Twos", 4, 6): {0: 190, 20: 38723, 28: 42931, 32: 18156}, - ("Category Quadruple Ones and Twos", 4, 7): {0: 5419, 24: 53017, 32: 41564}, - ("Category Quadruple Ones and Twos", 4, 8): {0: 3135, 24: 47352, 32: 49513}, - ("Category Quadruple Ones and Twos", 5, 1): {0: 13112, 8: 41252, 20: 45636}, - ("Category Quadruple Ones and Twos", 5, 2): {0: 7293, 16: 50711, 28: 41996}, - ("Category Quadruple Ones and Twos", 5, 3): {0: 719, 20: 55921, 32: 43360}, - ("Category Quadruple Ones and Twos", 5, 4): {0: 1152, 20: 38570, 32: 60278}, - ("Category Quadruple Ones and Twos", 5, 5): {0: 5647, 24: 40910, 36: 53443}, - ("Category Quadruple Ones and Twos", 5, 6): {0: 194, 28: 51527, 40: 48279}, - ("Category Quadruple Ones and Twos", 5, 7): {0: 1449, 28: 39301, 36: 41332, 40: 17918}, - ("Category Quadruple Ones and Twos", 5, 8): {0: 6781, 32: 52834, 40: 40385}, - ("Category Quadruple Ones and Twos", 6, 1): {0: 8646, 12: 53753, 24: 37601}, - ("Category Quadruple Ones and Twos", 6, 2): {0: 844, 16: 40583, 28: 58573}, - ("Category Quadruple Ones and Twos", 6, 3): {0: 1241, 24: 54870, 36: 43889}, - ("Category Quadruple Ones and Twos", 6, 4): {0: 1745, 28: 53286, 40: 44969}, - ("Category Quadruple Ones and Twos", 6, 5): {0: 2076, 32: 56909, 44: 41015}, - ("Category Quadruple Ones and Twos", 6, 6): {0: 6827, 32: 39400, 44: 53773}, - ("Category Quadruple Ones and Twos", 6, 7): {0: 1386, 36: 49865, 48: 48749}, - ("Category Quadruple Ones and Twos", 6, 8): {0: 1841, 36: 38680, 44: 40600, 48: 18879}, - ("Category Quadruple Ones and Twos", 7, 1): {0: 5780, 12: 46454, 24: 47766}, - ("Category Quadruple Ones and Twos", 7, 2): {0: 6122, 20: 38600, 32: 55278}, - ("Category Quadruple Ones and Twos", 7, 3): {0: 2065, 28: 52735, 40: 45200}, - ("Category Quadruple Ones and Twos", 7, 4): {0: 1950, 32: 50270, 44: 47780}, - ("Category Quadruple Ones and Twos", 7, 5): {0: 2267, 36: 49235, 48: 48498}, - ("Category Quadruple Ones and Twos", 7, 6): {0: 2500, 40: 53934, 52: 43566}, - ("Category Quadruple Ones and Twos", 7, 7): {0: 6756, 44: 53730, 56: 39514}, - ("Category Quadruple Ones and Twos", 7, 8): {0: 3625, 44: 45159, 56: 51216}, - ("Category Quadruple Ones and Twos", 8, 1): {0: 11493, 16: 50043, 28: 38464}, - ("Category Quadruple Ones and Twos", 8, 2): {0: 136, 24: 47795, 36: 52069}, - ("Category Quadruple Ones and Twos", 8, 3): {0: 2744, 32: 51640, 48: 45616}, - ("Category Quadruple Ones and Twos", 8, 4): {0: 2293, 36: 45979, 48: 51728}, - ("Category Quadruple Ones and Twos", 8, 5): {0: 2181, 40: 44909, 52: 52910}, - ("Category Quadruple Ones and Twos", 8, 6): {4: 2266, 44: 44775, 56: 52959}, - ("Category Quadruple Ones and Twos", 8, 7): {8: 2344, 48: 50198, 60: 47458}, - ("Category Quadruple Ones and Twos", 8, 8): {8: 2808, 48: 37515, 56: 37775, 64: 21902}, + ("Category Twos and Threes", 1, 3): {0: 46223, 2: 53777}, + ("Category Twos and Threes", 1, 4): {0: 38552, 2: 61448}, + ("Category Twos and Threes", 1, 5): {0: 32320, 2: 67680}, + ("Category Twos and Threes", 1, 6): {0: 10797, 3: 66593, 2: 22610}, + ("Category Twos and Threes", 1, 7): {0: 9307, 3: 90693}, + ("Category Twos and Threes", 1, 8): {0: 2173, 3: 97827}, + ("Category Twos and Threes", 2, 1): {0: 44565, 2: 55435}, + ("Category Twos and Threes", 2, 2): {0: 30855, 2: 69145}, + ("Category Twos and Threes", 2, 3): {0: 9977, 3: 67663, 2: 22360}, + ("Category Twos and Threes", 2, 4): {0: 7252, 3: 92748}, + ("Category Twos and Threes", 2, 5): {0: 1135, 3: 98865}, + ("Category Twos and Threes", 2, 6): {0: 121, 3: 99879}, + ("Category Twos and Threes", 2, 7): {2: 48, 5: 60169, 3: 39783}, + ("Category Twos and Threes", 2, 8): {5: 99998, 3: 2}, + ("Category Twos and Threes", 3, 1): {0: 29892, 2: 70108}, + ("Category Twos and Threes", 3, 2): {0: 8977, 3: 69968, 2: 21055}, + ("Category Twos and Threes", 3, 3): {0: 5237, 3: 94763}, + ("Category Twos and Threes", 3, 4): {2: 1781, 5: 65980, 3: 32239}, + ("Category Twos and Threes", 3, 5): {2: 609, 6: 65803, 5: 22563, 3: 11025}, + ("Category Twos and Threes", 3, 6): {6: 100000}, + ("Category Twos and Threes", 3, 7): {6: 100000}, + ("Category Twos and Threes", 3, 8): {6: 100000}, + ("Category Twos and Threes", 4, 1): {0: 11769, 3: 60627, 2: 27604}, + ("Category Twos and Threes", 4, 2): {2: 15639, 4: 60280, 3: 24081}, + ("Category Twos and Threes", 4, 3): {5: 72517, 2: 4298, 4: 16567, 3: 6618}, + ("Category Twos and Threes", 4, 4): {6: 73910, 5: 18921, 2: 1121, 4: 4322, 3: 1726}, + ("Category Twos and Threes", 4, 5): {2: 430, 7: 61608, 6: 28377, 5: 7264, 4: 1659, 3: 662}, + ("Category Twos and Threes", 4, 6): {9: 60343, 7: 24434, 6: 15223}, + ("Category Twos and Threes", 4, 7): {9: 100000}, + ("Category Twos and Threes", 4, 8): {9: 100000}, + ("Category Twos and Threes", 5, 1): {0: 11610, 3: 88390}, + ("Category Twos and Threes", 5, 2): {5: 70562, 3: 11158, 2: 534, 4: 17746}, + ("Category Twos and Threes", 5, 3): {6: 74716, 5: 23240, 3: 774, 2: 37, 4: 1233}, + ("Category Twos and Threes", 5, 4): {8: 68531, 6: 29461, 5: 1962, 3: 18, 4: 28}, + ("Category Twos and Threes", 5, 5): {9: 70635, 8: 26461, 6: 2860, 5: 44}, + ("Category Twos and Threes", 5, 6): {9: 100000}, + ("Category Twos and Threes", 5, 7): {11: 67606, 9: 32394}, + ("Category Twos and Threes", 5, 8): {12: 68354, 11: 21395, 9: 10251}, + ("Category Twos and Threes", 6, 1): {2: 4096, 4: 64713, 3: 31191}, + ("Category Twos and Threes", 6, 2): {2: 169, 6: 68210, 5: 22433, 3: 3547, 4: 5641}, + ("Category Twos and Threes", 6, 3): {2: 11, 8: 68425, 6: 23593, 5: 7338, 3: 244, 4: 389}, + ("Category Twos and Threes", 6, 4): {9: 73054, 8: 26109, 6: 787, 5: 50}, + ("Category Twos and Threes", 6, 5): {8: 8568, 11: 68223, 9: 23209}, + ("Category Twos and Threes", 6, 6): {12: 70373, 11: 20213, 9: 9414}, + ("Category Twos and Threes", 6, 7): {12: 100000}, + ("Category Twos and Threes", 6, 8): {14: 68062, 12: 31938}, + ("Category Twos and Threes", 7, 1): {2: 1390, 5: 66048, 4: 21972, 3: 10590}, + ("Category Twos and Threes", 7, 2): {2: 22, 8: 60665, 5: 11253, 6: 26834, 3: 473, 4: 753}, + ("Category Twos and Threes", 7, 3): {9: 70126, 8: 26169, 5: 909, 6: 2772, 3: 9, 4: 15}, + ("Category Twos and Threes", 7, 4): {11: 70543, 9: 28824, 8: 633}, + ("Category Twos and Threes", 7, 5): {12: 74745, 11: 22893, 9: 2173, 8: 189}, + ("Category Twos and Threes", 7, 6): {11: 7636, 14: 69766, 12: 22598}, + ("Category Twos and Threes", 7, 7): {15: 71620, 14: 19800, 12: 8580}, + ("Category Twos and Threes", 7, 8): {14: 10952, 16: 61407, 15: 27641}, + ("Category Twos and Threes", 8, 1): {2: 555, 6: 60067, 5: 26375, 4: 8774, 3: 4229}, + ("Category Twos and Threes", 8, 2): {8: 99967, 2: 13, 6: 20}, + ("Category Twos and Threes", 8, 3): {8: 10167, 11: 65964, 9: 23869}, + ("Category Twos and Threes", 8, 4): {11: 37966, 13: 62034}, + ("Category Twos and Threes", 8, 5): {11: 9059, 15: 64126, 12: 26815}, + ("Category Twos and Threes", 8, 6): {14: 14139, 17: 60581, 11: 2, 15: 25278}, + ("Category Twos and Threes", 8, 7): {14: 5173, 18: 63415, 17: 22164, 15: 9248}, + ("Category Twos and Threes", 8, 8): {18: 100000}, + ("Category Sum of Odds", 1, 1): {0: 66572, 3: 33428}, + ("Category Sum of Odds", 1, 2): {0: 44489, 3: 55511}, + ("Category Sum of Odds", 1, 3): {0: 26778, 3: 33412, 5: 39810}, + ("Category Sum of Odds", 1, 4): {0: 18191, 5: 81809}, + ("Category Sum of Odds", 1, 5): {0: 2299, 5: 97701}, + ("Category Sum of Odds", 1, 6): {0: 101, 5: 99899}, + ("Category Sum of Odds", 1, 7): {5: 100000}, + ("Category Sum of Odds", 1, 8): {5: 100000}, + ("Category Sum of Odds", 2, 1): {0: 66571, 3: 33429}, + ("Category Sum of Odds", 2, 2): {0: 38206, 4: 61794}, + ("Category Sum of Odds", 2, 3): {3: 15100, 8: 34337, 4: 24422, 5: 26141}, + ("Category Sum of Odds", 2, 4): {3: 4389, 8: 75870, 5: 19741}, + ("Category Sum of Odds", 2, 5): {8: 66180, 10: 33820}, + ("Category Sum of Odds", 2, 6): {10: 99075, 8: 925}, + ("Category Sum of Odds", 2, 7): {10: 100000}, + ("Category Sum of Odds", 2, 8): {10: 100000}, + ("Category Sum of Odds", 3, 1): {0: 19440, 3: 80560}, + ("Category Sum of Odds", 3, 2): {0: 3843, 3: 30607, 6: 65550}, + ("Category Sum of Odds", 3, 3): {8: 99451, 3: 126, 4: 204, 5: 219}, + ("Category Sum of Odds", 3, 4): {8: 39493, 9: 60507}, + ("Category Sum of Odds", 3, 5): {8: 25186, 13: 36226, 9: 38588}, + ("Category Sum of Odds", 3, 6): {13: 99387, 8: 242, 9: 371}, + ("Category Sum of Odds", 3, 7): {13: 63989, 15: 36011}, + ("Category Sum of Odds", 3, 8): {15: 99350, 13: 650}, + ("Category Sum of Odds", 4, 1): {0: 7100, 3: 29425, 5: 63475}, + ("Category Sum of Odds", 4, 2): {0: 1227, 3: 30702, 8: 68071}, + ("Category Sum of Odds", 4, 3): {8: 34941, 10: 65059}, + ("Category Sum of Odds", 4, 4): {8: 30671, 11: 69329}, + ("Category Sum of Odds", 4, 5): {8: 20766, 13: 79234}, + ("Category Sum of Odds", 4, 6): {13: 67313, 18: 32687}, + ("Category Sum of Odds", 4, 7): {13: 12063, 18: 87937}, + ("Category Sum of Odds", 4, 8): {18: 66936, 20: 33064}, + ("Category Sum of Odds", 5, 1): {0: 2404, 3: 31470, 6: 66126}, + ("Category Sum of Odds", 5, 2): {6: 12689, 11: 60256, 8: 27055}, + ("Category Sum of Odds", 5, 3): {10: 36853, 13: 63147}, + ("Category Sum of Odds", 5, 4): {13: 38005, 15: 61994, 10: 1}, + ("Category Sum of Odds", 5, 5): {13: 33747, 16: 66253}, + ("Category Sum of Odds", 5, 6): {13: 23587, 18: 76413}, + ("Category Sum of Odds", 5, 7): {18: 67776, 23: 32224}, + ("Category Sum of Odds", 5, 8): {23: 99176, 18: 824}, + ("Category Sum of Odds", 6, 1): {0: 791, 3: 32146, 7: 67063}, + ("Category Sum of Odds", 6, 2): {11: 38567, 13: 61432, 8: 1}, + ("Category Sum of Odds", 6, 3): {15: 65880, 11: 5075, 13: 29045}, + ("Category Sum of Odds", 6, 4): {15: 37367, 18: 62633}, + ("Category Sum of Odds", 6, 5): {18: 38038, 20: 61948, 15: 14}, + ("Category Sum of Odds", 6, 6): {18: 33838, 21: 66162}, + ("Category Sum of Odds", 6, 7): {18: 16130, 23: 83870}, + ("Category Sum of Odds", 6, 8): {23: 66748, 28: 33252}, + ("Category Sum of Odds", 7, 1): {5: 12019, 9: 63507, 7: 24474}, + ("Category Sum of Odds", 7, 2): {11: 37365, 15: 62635}, + ("Category Sum of Odds", 7, 3): {15: 36250, 18: 63750}, + ("Category Sum of Odds", 7, 4): {18: 37627, 21: 62373}, + ("Category Sum of Odds", 7, 5): {20: 35127, 23: 64873}, + ("Category Sum of Odds", 7, 6): {20: 12629, 25: 64047, 23: 23324}, + ("Category Sum of Odds", 7, 7): {23: 32409, 26: 67591}, + ("Category Sum of Odds", 7, 8): {23: 22322, 28: 77678}, + ("Category Sum of Odds", 8, 1): {5: 4088, 10: 65985, 9: 21602, 7: 8325}, + ("Category Sum of Odds", 8, 2): {13: 35686, 17: 64314}, + ("Category Sum of Odds", 8, 3): {17: 13770, 21: 62013, 18: 24217}, + ("Category Sum of Odds", 8, 4): {21: 37763, 24: 62237}, + ("Category Sum of Odds", 8, 5): {23: 12631, 26: 66541, 21: 4, 24: 20824}, + ("Category Sum of Odds", 8, 6): {23: 4929, 29: 60982, 26: 25964, 24: 8125}, + ("Category Sum of Odds", 8, 7): {23: 1608, 30: 67370, 29: 19899, 26: 8472, 24: 2651}, + ("Category Sum of Odds", 8, 8): {28: 4861, 32: 61811, 30: 25729, 29: 7599}, + ("Category Sum of Evens", 1, 1): {0: 66318, 4: 33682}, + ("Category Sum of Evens", 1, 2): {0: 44331, 4: 55669}, + ("Category Sum of Evens", 1, 3): {0: 29576, 4: 35040, 6: 35384}, + ("Category Sum of Evens", 1, 4): {0: 22612, 6: 77388}, + ("Category Sum of Evens", 1, 5): {0: 3566, 6: 96434}, + ("Category Sum of Evens", 1, 6): {0: 209, 6: 99791}, + ("Category Sum of Evens", 1, 7): {0: 3, 6: 99997}, + ("Category Sum of Evens", 1, 8): {6: 100000}, + ("Category Sum of Evens", 2, 1): {0: 25229, 2: 36083, 6: 38688}, + ("Category Sum of Evens", 2, 2): {0: 57, 4: 38346, 8: 37232, 2: 81, 6: 24284}, + ("Category Sum of Evens", 2, 3): {6: 39504, 10: 37060, 4: 1, 8: 23435}, + ("Category Sum of Evens", 2, 4): {10: 99495, 6: 317, 8: 188}, + ("Category Sum of Evens", 2, 5): {10: 69597, 12: 30403}, + ("Category Sum of Evens", 2, 6): {12: 98377, 10: 1623}, + ("Category Sum of Evens", 2, 7): {12: 100000}, + ("Category Sum of Evens", 2, 8): {12: 100000}, + ("Category Sum of Evens", 3, 1): {0: 76, 4: 38332, 8: 37178, 2: 109, 6: 24305}, + ("Category Sum of Evens", 3, 2): {8: 67248, 12: 32556, 4: 196}, + ("Category Sum of Evens", 3, 3): {10: 44843, 14: 33195, 8: 213, 12: 21749}, + ("Category Sum of Evens", 3, 4): {10: 37288, 14: 62712}, + ("Category Sum of Evens", 3, 5): {14: 61196, 16: 38802, 10: 2}, + ("Category Sum of Evens", 3, 6): {16: 99621, 14: 379}, + ("Category Sum of Evens", 3, 7): {16: 67674, 18: 32326}, + ("Category Sum of Evens", 3, 8): {18: 100000}, + ("Category Sum of Evens", 4, 1): {6: 37636, 10: 40039, 4: 32, 8: 22293}, + ("Category Sum of Evens", 4, 2): {10: 57689, 14: 42258, 6: 53}, + ("Category Sum of Evens", 4, 3): {14: 67801, 18: 32152, 10: 47}, + ("Category Sum of Evens", 4, 4): {18: 98878, 14: 1122}, + ("Category Sum of Evens", 4, 5): {18: 60401, 20: 39599}, + ("Category Sum of Evens", 4, 6): {20: 64396, 22: 35186, 18: 418}, + ("Category Sum of Evens", 4, 7): {22: 99697, 20: 302, 18: 1}, + ("Category Sum of Evens", 4, 8): {22: 100000}, + ("Category Sum of Evens", 5, 1): {8: 35338, 12: 41027, 6: 22, 10: 23613}, + ("Category Sum of Evens", 5, 2): {12: 37027, 18: 35856, 10: 10, 14: 27107}, + ("Category Sum of Evens", 5, 3): {18: 68230, 22: 31735, 14: 35}, + ("Category Sum of Evens", 5, 4): {18: 14880, 22: 53608, 24: 31512}, + ("Category Sum of Evens", 5, 5): {24: 98732, 18: 275, 22: 993}, + ("Category Sum of Evens", 5, 6): {24: 61498, 26: 38502}, + ("Category Sum of Evens", 5, 7): {26: 65201, 28: 34488, 24: 311}, + ("Category Sum of Evens", 5, 8): {28: 99648, 26: 351, 24: 1}, + ("Category Sum of Evens", 6, 1): {10: 34538, 14: 41426, 8: 4, 12: 24032}, + ("Category Sum of Evens", 6, 2): {16: 43552, 22: 31546, 14: 235, 12: 121, 18: 24546}, + ("Category Sum of Evens", 6, 3): {22: 68714, 26: 31239, 18: 47}, + ("Category Sum of Evens", 6, 4): {26: 59168, 28: 33835, 22: 4791, 18: 1, 24: 2205}, + ("Category Sum of Evens", 6, 5): {26: 44386, 30: 32920, 28: 22694}, + ("Category Sum of Evens", 6, 6): {30: 98992, 26: 667, 28: 341}, + ("Category Sum of Evens", 6, 7): {30: 60806, 32: 39194}, + ("Category Sum of Evens", 6, 8): {32: 64584, 34: 35252, 30: 164}, + ("Category Sum of Evens", 7, 1): {12: 40703, 18: 30507, 10: 1, 14: 28789}, + ("Category Sum of Evens", 7, 2): {22: 60249, 24: 38366, 12: 1, 18: 767, 16: 614, 14: 3}, + ("Category Sum of Evens", 7, 3): {24: 47964, 30: 30240, 22: 4, 26: 21792}, + ("Category Sum of Evens", 7, 4): {30: 63108, 32: 35114, 24: 1778}, + ("Category Sum of Evens", 7, 5): {32: 62062, 34: 37406, 30: 523, 26: 6, 28: 3}, + ("Category Sum of Evens", 7, 6): {32: 40371, 36: 35507, 34: 24122}, + ("Category Sum of Evens", 7, 7): {34: 44013, 38: 31749, 32: 4, 36: 24234}, + ("Category Sum of Evens", 7, 8): {38: 99116, 34: 570, 36: 314}, + ("Category Sum of Evens", 8, 1): {18: 66673, 20: 31528, 12: 1054, 14: 745}, + ("Category Sum of Evens", 8, 2): {22: 40918, 28: 33610, 24: 25472}, + ("Category Sum of Evens", 8, 3): {28: 40893, 32: 41346, 24: 17, 30: 17737, 26: 7}, + ("Category Sum of Evens", 8, 4): {32: 63665, 36: 36316, 28: 19}, + ("Category Sum of Evens", 8, 5): {36: 58736, 38: 40234, 32: 1030}, + ("Category Sum of Evens", 8, 6): {36: 57946, 40: 42054}, + ("Category Sum of Evens", 8, 7): {38: 34984, 42: 39622, 36: 2, 40: 25392}, + ("Category Sum of Evens", 8, 8): {42: 65137, 44: 34611, 38: 146, 40: 106}, + ("Category Double Threes and Fours", 1, 1): {0: 66749, 6: 33251}, + ("Category Double Threes and Fours", 1, 2): {0: 44675, 6: 55325}, + ("Category Double Threes and Fours", 1, 3): {0: 29592, 6: 35261, 8: 35147}, + ("Category Double Threes and Fours", 1, 4): {0: 24601, 6: 29406, 8: 45993}, + ("Category Double Threes and Fours", 1, 5): {0: 20499, 6: 24420, 8: 55081}, + ("Category Double Threes and Fours", 1, 6): {0: 17116, 6: 20227, 8: 62657}, + ("Category Double Threes and Fours", 1, 7): {0: 14193, 6: 17060, 8: 68747}, + ("Category Double Threes and Fours", 1, 8): {0: 11977, 6: 13924, 8: 74099}, + ("Category Double Threes and Fours", 2, 1): {0: 44382, 6: 22191, 8: 33427}, + ("Category Double Threes and Fours", 2, 2): {0: 5, 6: 46088, 12: 30763, 8: 23144}, + ("Category Double Threes and Fours", 2, 3): {0: 5, 6: 30159, 12: 32725, 14: 37111}, + ("Category Double Threes and Fours", 2, 4): {6: 20533, 14: 79467}, + ("Category Double Threes and Fours", 2, 5): {14: 69789, 16: 30211}, + ("Category Double Threes and Fours", 2, 6): {16: 99978, 14: 22}, + ("Category Double Threes and Fours", 2, 7): {16: 100000}, + ("Category Double Threes and Fours", 2, 8): {16: 100000}, + ("Category Double Threes and Fours", 3, 1): {0: 8, 6: 49139, 12: 26176, 8: 24677}, + ("Category Double Threes and Fours", 3, 2): {0: 5, 6: 24942, 12: 27065, 14: 47988}, + ("Category Double Threes and Fours", 3, 3): {6: 12743, 14: 56776, 20: 30481}, + ("Category Double Threes and Fours", 3, 4): {14: 9753, 20: 90247}, + ("Category Double Threes and Fours", 3, 5): {20: 61293, 22: 38707}, + ("Category Double Threes and Fours", 3, 6): {22: 99615, 20: 385}, + ("Category Double Threes and Fours", 3, 7): {22: 67267, 24: 32733}, + ("Category Double Threes and Fours", 3, 8): {24: 100000}, + ("Category Double Threes and Fours", 4, 1): {6: 26819, 12: 39789, 14: 33392}, + ("Category Double Threes and Fours", 4, 2): {14: 63726, 20: 36011, 6: 106, 12: 157}, + ("Category Double Threes and Fours", 4, 3): {20: 69628, 24: 30158, 14: 214}, + ("Category Double Threes and Fours", 4, 4): {20: 11409, 24: 57067, 26: 31524}, + ("Category Double Threes and Fours", 4, 5): {20: 6566, 26: 57047, 28: 36387}, + ("Category Double Threes and Fours", 4, 6): {28: 63694, 30: 35203, 20: 113, 26: 990}, + ("Category Double Threes and Fours", 4, 7): {30: 98893, 28: 1092, 26: 15}, + ("Category Double Threes and Fours", 4, 8): {30: 100000}, + ("Category Double Threes and Fours", 5, 1): {6: 16042, 14: 83958}, + ("Category Double Threes and Fours", 5, 2): {14: 44329, 20: 24912, 24: 30759}, + ("Category Double Threes and Fours", 5, 3): {24: 57603, 28: 42155, 20: 242}, + ("Category Double Threes and Fours", 5, 4): {26: 32446, 30: 43875, 24: 21, 28: 23658}, + ("Category Double Threes and Fours", 5, 5): {30: 69209, 34: 30672, 26: 69, 28: 50}, + ("Category Double Threes and Fours", 5, 6): {34: 63882, 36: 35323, 30: 795}, + ("Category Double Threes and Fours", 5, 7): {36: 65178, 38: 34598, 34: 222, 30: 2}, + ("Category Double Threes and Fours", 5, 8): {38: 99654, 36: 345, 34: 1}, + ("Category Double Threes and Fours", 6, 1): {14: 68079, 18: 31921}, + ("Category Double Threes and Fours", 6, 2): {14: 14542, 24: 48679, 28: 36779}, + ("Category Double Threes and Fours", 6, 3): {28: 62757, 34: 36962, 24: 281}, + ("Category Double Threes and Fours", 6, 4): {34: 68150, 38: 30771, 28: 604, 26: 1, 30: 474}, + ("Category Double Threes and Fours", 6, 5): {38: 68332, 40: 30833, 34: 823, 28: 12}, + ("Category Double Threes and Fours", 6, 6): {40: 67631, 42: 31174, 38: 1181, 34: 14}, + ("Category Double Threes and Fours", 6, 7): {42: 63245, 44: 35699, 40: 1038, 38: 18}, + ("Category Double Threes and Fours", 6, 8): {44: 64056, 46: 35162, 42: 770, 40: 12}, + ("Category Double Threes and Fours", 7, 1): {14: 14976, 18: 54685, 22: 30339}, + ("Category Double Threes and Fours", 7, 2): {14: 10532, 28: 55372, 32: 34096}, + ("Category Double Threes and Fours", 7, 3): {32: 42786, 40: 32123, 28: 2, 34: 25089}, + ("Category Double Threes and Fours", 7, 4): {38: 46172, 44: 31648, 32: 226, 40: 21954}, + ("Category Double Threes and Fours", 7, 5): {44: 64883, 46: 34437, 38: 460, 32: 2, 40: 218}, + ("Category Double Threes and Fours", 7, 6): {44: 43458, 48: 33715, 46: 22827}, + ("Category Double Threes and Fours", 7, 7): {46: 44472, 50: 32885, 44: 15, 48: 22628}, + ("Category Double Threes and Fours", 7, 8): {48: 41682, 52: 37868, 46: 18, 50: 20432}, + ("Category Double Threes and Fours", 8, 1): {14: 14227, 22: 85773}, + ("Category Double Threes and Fours", 8, 2): {22: 7990, 32: 56319, 36: 35691}, + ("Category Double Threes and Fours", 8, 3): {32: 19914, 40: 43585, 44: 36501}, + ("Category Double Threes and Fours", 8, 4): {44: 63232, 48: 36613, 32: 48, 40: 107}, + ("Category Double Threes and Fours", 8, 5): {48: 62939, 52: 36798, 44: 263}, + ("Category Double Threes and Fours", 8, 6): {52: 60756, 54: 38851, 48: 392, 44: 1}, + ("Category Double Threes and Fours", 8, 7): {54: 62281, 56: 37262, 52: 455, 48: 2}, + ("Category Double Threes and Fours", 8, 8): {56: 67295, 60: 32064, 54: 637, 52: 4}, + ("Category Quadruple Ones and Twos", 1, 1): {0: 66567, 4: 16803, 8: 16630}, + ("Category Quadruple Ones and Twos", 1, 2): {0: 44809, 4: 27448, 8: 27743}, + ("Category Quadruple Ones and Twos", 1, 3): {0: 37100, 4: 23184, 8: 39716}, + ("Category Quadruple Ones and Twos", 1, 4): {0: 30963, 4: 19221, 8: 49816}, + ("Category Quadruple Ones and Twos", 1, 5): {0: 25316, 4: 16079, 8: 58605}, + ("Category Quadruple Ones and Twos", 1, 6): {0: 14381, 8: 85619}, + ("Category Quadruple Ones and Twos", 1, 7): {0: 4137, 8: 95863}, + ("Category Quadruple Ones and Twos", 1, 8): {0: 1004, 8: 98996}, + ("Category Quadruple Ones and Twos", 2, 1): {0: 44566, 4: 22273, 8: 33161}, + ("Category Quadruple Ones and Twos", 2, 2): {0: 19963, 4: 24890, 8: 32262, 12: 22885}, + ("Category Quadruple Ones and Twos", 2, 3): {0: 13766, 4: 17158, 8: 34907, 12: 18539, 16: 15630}, + ("Category Quadruple Ones and Twos", 2, 4): {0: 6655, 8: 30200, 12: 26499, 16: 36646}, + ("Category Quadruple Ones and Twos", 2, 5): {0: 982, 8: 16426, 12: 24307, 16: 58285}, + ("Category Quadruple Ones and Twos", 2, 6): {0: 68, 8: 9887, 16: 90045}, + ("Category Quadruple Ones and Twos", 2, 7): {0: 11, 16: 99989}, + ("Category Quadruple Ones and Twos", 2, 8): {16: 100000}, + ("Category Quadruple Ones and Twos", 3, 1): {0: 29440, 4: 22574, 8: 27747, 12: 20239}, + ("Category Quadruple Ones and Twos", 3, 2): {0: 8857, 4: 16295, 8: 26434, 12: 22986, 16: 25428}, + ("Category Quadruple Ones and Twos", 3, 3): {0: 3649, 8: 15314, 12: 24619, 16: 38944, 20: 17474}, + ("Category Quadruple Ones and Twos", 3, 4): {0: 11, 8: 8430, 16: 41259, 20: 50300}, + ("Category Quadruple Ones and Twos", 3, 5): {20: 80030, 24: 19902, 8: 11, 16: 57}, + ("Category Quadruple Ones and Twos", 3, 6): {20: 23895, 24: 76105}, + ("Category Quadruple Ones and Twos", 3, 7): {24: 100000}, + ("Category Quadruple Ones and Twos", 3, 8): {24: 100000}, + ("Category Quadruple Ones and Twos", 4, 1): {0: 19691, 4: 19657, 8: 27288, 12: 16126, 16: 17238}, + ("Category Quadruple Ones and Twos", 4, 2): {0: 1222, 4: 15703, 12: 24015, 16: 34944, 20: 24116}, + ("Category Quadruple Ones and Twos", 4, 3): {0: 227, 12: 14519, 20: 62257, 24: 22997}, + ("Category Quadruple Ones and Twos", 4, 4): {0: 11, 20: 17266, 24: 67114, 28: 15609}, + ("Category Quadruple Ones and Twos", 4, 5): {24: 27365, 28: 72632, 20: 3}, + ("Category Quadruple Ones and Twos", 4, 6): {28: 81782, 32: 18215, 24: 3}, + ("Category Quadruple Ones and Twos", 4, 7): {28: 22319, 32: 77681}, + ("Category Quadruple Ones and Twos", 4, 8): {32: 100000}, + ("Category Quadruple Ones and Twos", 5, 1): {0: 13112, 4: 16534, 8: 24718, 12: 18558, 16: 27078}, + ("Category Quadruple Ones and Twos", 5, 2): {0: 21, 4: 15200, 16: 28784, 20: 32131, 24: 23864}, + ("Category Quadruple Ones and Twos", 5, 3): {0: 4, 16: 8475, 24: 66718, 28: 24803}, + ("Category Quadruple Ones and Twos", 5, 4): {28: 76149, 32: 23289, 24: 550, 20: 12}, + ("Category Quadruple Ones and Twos", 5, 5): {32: 81110, 36: 16222, 28: 2663, 24: 5}, + ("Category Quadruple Ones and Twos", 5, 6): {32: 18542, 36: 81458}, + ("Category Quadruple Ones and Twos", 5, 7): {36: 82036, 40: 17964}, + ("Category Quadruple Ones and Twos", 5, 8): {36: 27864, 40: 72136}, + ("Category Quadruple Ones and Twos", 6, 1): {0: 6419, 8: 16963, 12: 22116, 16: 33903, 20: 20599}, + ("Category Quadruple Ones and Twos", 6, 2): {0: 5, 16: 8913, 24: 67749, 28: 23333}, + ("Category Quadruple Ones and Twos", 6, 3): {28: 71779, 32: 27514, 16: 82, 24: 625}, + ("Category Quadruple Ones and Twos", 6, 4): {32: 72333, 36: 27328, 28: 337, 24: 2}, + ("Category Quadruple Ones and Twos", 6, 5): {36: 73993, 40: 25138, 32: 865, 28: 4}, + ("Category Quadruple Ones and Twos", 6, 6): {40: 80918, 44: 17126, 36: 1934, 32: 22}, + ("Category Quadruple Ones and Twos", 6, 7): {40: 20298, 44: 79702}, + ("Category Quadruple Ones and Twos", 6, 8): {44: 81077, 48: 18923}, + ("Category Quadruple Ones and Twos", 7, 1): {0: 508, 8: 10298, 16: 41828, 20: 30853, 24: 16513}, + ("Category Quadruple Ones and Twos", 7, 2): {16: 7429, 28: 69817, 32: 22754}, + ("Category Quadruple Ones and Twos", 7, 3): {32: 82871, 40: 16531, 16: 57, 28: 541}, + ("Category Quadruple Ones and Twos", 7, 4): {36: 67601, 44: 17916, 32: 909, 40: 13569, 28: 5}, + ("Category Quadruple Ones and Twos", 7, 5): {40: 67395, 48: 17447, 36: 364, 44: 14790, 32: 4}, + ("Category Quadruple Ones and Twos", 7, 6): {48: 91242, 40: 7151, 36: 38, 44: 1569}, + ("Category Quadruple Ones and Twos", 7, 7): {48: 80854, 52: 19146}, + ("Category Quadruple Ones and Twos", 7, 8): {48: 25334, 52: 74666}, + ("Category Quadruple Ones and Twos", 8, 1): {0: 119, 16: 17496, 20: 26705, 24: 55680}, + ("Category Quadruple Ones and Twos", 8, 2): {24: 569, 32: 72257, 36: 21817, 28: 5357}, + ("Category Quadruple Ones and Twos", 8, 3): {36: 66654, 44: 18473, 32: 1396, 40: 13477}, + ("Category Quadruple Ones and Twos", 8, 4): {44: 73954, 48: 22240, 36: 3178, 40: 628}, + ("Category Quadruple Ones and Twos", 8, 5): {48: 76082, 52: 22415, 44: 1500, 36: 3}, + ("Category Quadruple Ones and Twos", 8, 6): {52: 74901, 56: 21332, 48: 3766, 44: 1}, + ("Category Quadruple Ones and Twos", 8, 7): {56: 96171, 52: 3640, 48: 189}, + ("Category Quadruple Ones and Twos", 8, 8): {56: 78035, 60: 21965}, ("Category Micro Straight", 1, 1): {0: 100000}, ("Category Micro Straight", 1, 2): {0: 100000}, ("Category Micro Straight", 1, 3): {0: 100000}, diff --git a/worlds/yachtdice/__init__.py b/worlds/yachtdice/__init__.py index 75993fd39443..7efb8f94187c 100644 --- a/worlds/yachtdice/__init__.py +++ b/worlds/yachtdice/__init__.py @@ -56,7 +56,7 @@ class YachtDiceWorld(World): item_name_groups = item_groups - ap_world_version = "2.1.3" + ap_world_version = "2.1.4" def _get_yachtdice_data(self): return { @@ -468,7 +468,7 @@ def create_regions(self): menu.exits.append(connection) connection.connect(board) self.multiworld.regions += [menu, board] - + def get_filler_item_name(self) -> str: return "Good RNG" diff --git a/worlds/yachtdice/docs/en_Yacht Dice.md b/worlds/yachtdice/docs/en_Yacht Dice.md index 53eefe9e9c4b..c671dcee50b8 100644 --- a/worlds/yachtdice/docs/en_Yacht Dice.md +++ b/worlds/yachtdice/docs/en_Yacht Dice.md @@ -3,7 +3,7 @@ Welcome to Yacht Dice, the ultimate dice-rolling adventure in Archipelago! Cast your dice, chase high scores, and unlock valuable treasures. Discover new dice, extra rolls, multipliers, and special scoring categories to enhance your game. Roll your way to victory by reaching the target score! ## Understanding Location Checks -In Yacht Dice, location checks happen when you hit certain scores for the first time. The target score for your next location check is always displayed on the website. +In Yacht Dice, location checks happen when you hit certain scores for the first time. The target score for your next location check is always displayed in the game. ## Items and Their Effects When you receive an item, it could be extra dice, extra rolls, score multipliers, or new scoring categories. These boosts help you sail towards higher scores and more loot. Other items include extra points, lore, and fun facts to enrich your journey. diff --git a/worlds/yachtdice/docs/setup_en.md b/worlds/yachtdice/docs/setup_en.md index c76cd398ce55..f6c15af2b63c 100644 --- a/worlds/yachtdice/docs/setup_en.md +++ b/worlds/yachtdice/docs/setup_en.md @@ -3,19 +3,13 @@ ## Required Software - A browser (you are probably using one right now!). -- Archipelago from the [Archipelago Releases Page](https://github.com/ArchipelagoMW/Archipelago/releases). ## Playing the game Open the Yacht Dice website. There are two options: -- Download the latest release from [Yacht Dice Release](https://github.com/spinerak/ArchipelagoYachtDice/releases/latest) and unzip the Website.zip. Then open player.html in your browser. -- Cruise over to the [Yacht Dice website](https://yacht-dice-ap.netlify.app/). This also works on mobile. If the website is not available, use the first option. +- Cruise over to the [Yacht Dice Website](https://yacht-dice-ap.netlify.app/). This is the easiest option. If the website is unavailable, use the next option. +- Download the latest release from [Yacht Dice Release](https://github.com/spinerak/ArchipelagoYachtDice/releases/latest) and unzip the Website.zip. Then open index.html in your browser. -Both options have an "offline" play option to try out the game without having to generate a game first. +Press Archipelago, and after logging in, you are good to go. The website has a built-in client, where you can chat and send commands. +Both options also have a "Solo play" mode to try out the game without having to generate a game first. -## Play with Archipelago - -- Create your yaml file via the [Yacht Dice Player Options Page](../player-options). -- After generating, open the Yacht Dice website. After the tutoroll, fill in the room information. -- After logging in, you are good to go. The website has a built-in client, where you can chat and send commands. - -For more information on yaml files, generating Archipelago games, and connecting to servers, please see the [Basic Multiworld Setup Guide](/tutorial/Archipelago/setup/en). +For more information on generating Archipelago games and connecting to servers, please see the [Basic Multiworld Setup Guide](/tutorial/Archipelago/setup/en). \ No newline at end of file diff --git a/worlds/zillion/options.py b/worlds/zillion/options.py index 5de0b65c82f0..ec0fdb0b22e1 100644 --- a/worlds/zillion/options.py +++ b/worlds/zillion/options.py @@ -1,7 +1,6 @@ from collections import Counter from dataclasses import dataclass -from typing import ClassVar, Dict, Literal, Tuple -from typing_extensions import TypeGuard # remove when Python >= 3.10 +from typing import ClassVar, Dict, Literal, Tuple, TypeGuard from Options import Choice, DefaultOnToggle, NamedRange, OptionGroup, PerGameCommonOptions, Range, Removed, Toggle @@ -233,6 +232,7 @@ class ZillionSkill(Range): range_start = 0 range_end = 5 default = 2 + display_name = "skill" class ZillionStartingCards(NamedRange):