From dedabad290ccec512776f732c3619509d1d091b1 Mon Sep 17 00:00:00 2001 From: Emily <35015090+EmilyV99@users.noreply.github.com> Date: Sun, 2 Jun 2024 12:45:46 -0400 Subject: [PATCH 01/28] APSudoku: take over maintaining hintgame sudoku from bk_sudoku (#3432) --- docs/CODEOWNERS | 6 ++-- worlds/{bk_sudoku => apsudoku}/__init__.py | 25 +++++---------- worlds/apsudoku/docs/en_Sudoku.md | 13 ++++++++ worlds/apsudoku/docs/setup_en.md | 37 ++++++++++++++++++++++ worlds/bk_sudoku/docs/de_Sudoku.md | 21 ------------ worlds/bk_sudoku/docs/en_Sudoku.md | 13 -------- worlds/bk_sudoku/docs/setup_de.md | 27 ---------------- worlds/bk_sudoku/docs/setup_en.md | 24 -------------- 8 files changed, 61 insertions(+), 105 deletions(-) rename worlds/{bk_sudoku => apsudoku}/__init__.py (50%) create mode 100644 worlds/apsudoku/docs/en_Sudoku.md create mode 100644 worlds/apsudoku/docs/setup_en.md delete mode 100644 worlds/bk_sudoku/docs/de_Sudoku.md delete mode 100644 worlds/bk_sudoku/docs/en_Sudoku.md delete mode 100644 worlds/bk_sudoku/docs/setup_de.md delete mode 100644 worlds/bk_sudoku/docs/setup_en.md diff --git a/docs/CODEOWNERS b/docs/CODEOWNERS index f54132e24aa0..10b962d49970 100644 --- a/docs/CODEOWNERS +++ b/docs/CODEOWNERS @@ -15,15 +15,15 @@ # A Link to the Past /worlds/alttp/ @Berserker66 +# Sudoku (APSudoku) +/worlds/apsudoku/ @EmilyV99 + # Aquaria /worlds/aquaria/ @tioui # ArchipIDLE /worlds/archipidle/ @LegendaryLinux -# Sudoku (BK Sudoku) -/worlds/bk_sudoku/ @Jarno458 - # Blasphemous /worlds/blasphemous/ @TRPG0 diff --git a/worlds/bk_sudoku/__init__.py b/worlds/apsudoku/__init__.py similarity index 50% rename from worlds/bk_sudoku/__init__.py rename to worlds/apsudoku/__init__.py index 2c57bc7301ff..c6bd02bdc262 100644 --- a/worlds/bk_sudoku/__init__.py +++ b/worlds/apsudoku/__init__.py @@ -3,41 +3,32 @@ from BaseClasses import Tutorial from ..AutoWorld import WebWorld, World - -class Bk_SudokuWebWorld(WebWorld): +class AP_SudokuWebWorld(WebWorld): options_page = "games/Sudoku/info/en" theme = 'partyTime' setup_en = Tutorial( tutorial_name='Setup Guide', - description='A guide to playing BK Sudoku', + description='A guide to playing APSudoku', language='English', file_name='setup_en.md', link='setup/en', - authors=['Jarno'] - ) - setup_de = Tutorial( - tutorial_name='Setup Anleitung', - description='Eine Anleitung um BK-Sudoku zu spielen', - language='Deutsch', - file_name='setup_de.md', - link='setup/de', - authors=['Held_der_Zeit'] + authors=['EmilyV'] ) - tutorials = [setup_en, setup_de] + tutorials = [setup_en] - -class Bk_SudokuWorld(World): +class AP_SudokuWorld(World): """ Play a little Sudoku while you're in BK mode to maybe get some useful hints """ game = "Sudoku" - web = Bk_SudokuWebWorld() + web = AP_SudokuWebWorld() item_name_to_id: Dict[str, int] = {} location_name_to_id: Dict[str, int] = {} @classmethod def stage_assert_generate(cls, multiworld): - raise Exception("BK Sudoku cannot be used for generating worlds, the client can instead connect to any other world") + raise Exception("APSudoku cannot be used for generating worlds, the client can instead connect to any slot from any world") + diff --git a/worlds/apsudoku/docs/en_Sudoku.md b/worlds/apsudoku/docs/en_Sudoku.md new file mode 100644 index 000000000000..e81f773e0291 --- /dev/null +++ b/worlds/apsudoku/docs/en_Sudoku.md @@ -0,0 +1,13 @@ +# APSudoku + +## Hint Games + +HintGames do not need to be added at the start of a seed, and do not create a 'slot'- instead, you connect the HintGame client to a different game's slot. By playing a HintGame, you can earn hints for the connected slot. + +## What is this game? + +Play Sudoku puzzles of varying difficulties, earning a hint for each puzzle correctly solved. Harder puzzles are more likely to grant a hint towards a Progression item, though otherwise what hint is granted is random. + +## Where is the options page? + +There is no options page; this game cannot be used in your .yamls. Instead, the client can connect to any slot in a multiworld. diff --git a/worlds/apsudoku/docs/setup_en.md b/worlds/apsudoku/docs/setup_en.md new file mode 100644 index 000000000000..cf2c755bd837 --- /dev/null +++ b/worlds/apsudoku/docs/setup_en.md @@ -0,0 +1,37 @@ +# APSudoku Setup Guide + +## Required Software +- [APSudoku](https://github.com/EmilyV99/APSudoku) +- Windows (most tested on Win10) +- Other platforms might be able to build from source themselves; and may be included in the future. + +## General Concept + +This is a HintGame client, which can connect to any multiworld slot, allowing you to play Sudoku to unlock random hints for that slot's locations. + +Does not need to be added at the start of a seed, as it does not create any slots of its own, nor does it have any YAML files. + +## Installation Procedures + +Go to the latest release from the [APSudoku Releases page](https://github.com/EmilyV99/APSudoku/releases). Download and extract the `APSudoku.zip` file. + +## Joining a MultiWorld Game + +1. Run APSudoku.exe +2. Under the 'Archipelago' tab at the top-right: + - Enter the server url & port number + - Enter the name of the slot you wish to connect to + - Enter the room password (optional) + - Select DeathLink related settings (optional) + - Press connect +3. Go back to the 'Sudoku' tab + - Click the various '?' buttons for information on how to play / control +4. Choose puzzle difficulty +5. Try to solve the Sudoku. Click 'Check' when done. + +## DeathLink Support + +If 'DeathLink' is enabled when you click 'Connect': +- Lose a life if you check an incorrect puzzle (not an _incomplete_ puzzle- if any cells are empty, you get off with a warning), or quit a puzzle without solving it (including disconnecting). +- Life count customizable (default 0). Dying with 0 lives left kills linked players AND resets your puzzle. +- On receiving a DeathLink from another player, your puzzle resets. diff --git a/worlds/bk_sudoku/docs/de_Sudoku.md b/worlds/bk_sudoku/docs/de_Sudoku.md deleted file mode 100644 index abb50c5498d1..000000000000 --- a/worlds/bk_sudoku/docs/de_Sudoku.md +++ /dev/null @@ -1,21 +0,0 @@ -# BK-Sudoku - -## Was ist das für ein Spiel? - -BK-Sudoku ist kein typisches Archipelago-Spiel; stattdessen ist es ein gewöhnlicher Sudoku-Client der sich zu jeder -beliebigen Multiworld verbinden kann. Einmal verbunden kannst du ein 9x9 Sudoku spielen um einen zufälligen Hinweis -für dein Spiel zu erhalten. Es ist zwar langsam, aber es gibt dir etwas zu tun, solltest du mal nicht in der Lage sein -weitere „Checks” zu erreichen. -(Wer mag kann auch einfach so Sudoku spielen. Man muss nicht mit einer Multiworld verbunden sein, um ein Sudoku zu -spielen/generieren.) - -## Wie werden Hinweise freigeschalten? - -Nach dem Lösen eines Sudokus wird für den verbundenen Slot ein zufällig ausgewählter Hinweis freigegeben, für einen -Gegenstand der noch nicht gefunden wurde. - -## Wo ist die Seite für die Einstellungen? - -Es gibt keine Seite für die Einstellungen. Dieses Spiel kann nicht in deinen YAML-Dateien benutzt werden. Stattdessen -kann sich der Client mit einem beliebigen Slot einer Multiworld verbinden. In dem Client selbst kann aber der -Schwierigkeitsgrad des Sudoku ausgewählt werden. diff --git a/worlds/bk_sudoku/docs/en_Sudoku.md b/worlds/bk_sudoku/docs/en_Sudoku.md deleted file mode 100644 index dae5a9e3e513..000000000000 --- a/worlds/bk_sudoku/docs/en_Sudoku.md +++ /dev/null @@ -1,13 +0,0 @@ -# Bk Sudoku - -## What is this game? - -BK Sudoku is not a typical Archipelago game; instead, it is a generic Sudoku client that can connect to any existing multiworld. When connected, you can play Sudoku to unlock random hints for your game. While slow, it will give you something to do when you can't reach the checks in your game. - -## What hints are unlocked? - -After completing a Sudoku puzzle, the game will unlock 1 random hint for an unchecked location in the slot you are connected to. - -## Where is the options page? - -There is no options page; this game cannot be used in your .yamls. Instead, the client can connect to any slot in a multiworld. diff --git a/worlds/bk_sudoku/docs/setup_de.md b/worlds/bk_sudoku/docs/setup_de.md deleted file mode 100644 index 71a8e5f6245d..000000000000 --- a/worlds/bk_sudoku/docs/setup_de.md +++ /dev/null @@ -1,27 +0,0 @@ -# BK-Sudoku Setup Anleitung - -## Benötigte Software -- [Bk-Sudoku](https://github.com/Jarno458/sudoku) -- Windows 8 oder höher - -## Generelles Konzept - -Dies ist ein Client, der sich mit jedem beliebigen Slot einer Multiworld verbinden kann. Er lässt dich ein (9x9) Sudoku -spielen, um zufällige Hinweise für den verbundenen Slot freizuschalten. - -Aufgrund des Fakts, dass der Sudoku-Client sich zu jedem beliebigen Slot verbinden kann, ist es daher nicht notwendig -eine YAML für dieses Spiel zu generieren, da es keinen neuen Slot zur Multiworld-Session hinzufügt. - -## Installationsprozess - -Gehe zu der aktuellsten (latest) Veröffentlichung der [BK-Sudoku Releases](https://github.com/Jarno458/sudoku/releases). -Downloade und extrahiere/entpacke die `Bk_Sudoku.zip`-Datei. - -## Verbinden mit einer Multiworld - -1. Starte `Bk_Sudoku.exe` -2. Trage den Namen des Slots ein, mit dem du dich verbinden möchtest -3. Trage die Server-URL und den Port ein -4. Drücke auf Verbinden (connect) -5. Wähle deinen Schwierigkeitsgrad -6. Versuche das Sudoku zu Lösen diff --git a/worlds/bk_sudoku/docs/setup_en.md b/worlds/bk_sudoku/docs/setup_en.md deleted file mode 100644 index eda17e701bb8..000000000000 --- a/worlds/bk_sudoku/docs/setup_en.md +++ /dev/null @@ -1,24 +0,0 @@ -# BK Sudoku Setup Guide - -## Required Software -- [Bk Sudoku](https://github.com/Jarno458/sudoku) -- Windows 8 or higher - -## General Concept - -This is a client that can connect to any multiworld slot, and lets you play Sudoku to unlock random hints for that slot's locations. - -Due to the fact that the Sudoku client may connect to any slot, it is not necessary to generate a YAML for this game as it does not generate any new slots in the multiworld session. - -## Installation Procedures - -Go to the latest release on [BK Sudoku Releases](https://github.com/Jarno458/sudoku/releases). Download and extract the `Bk_Sudoku.zip` file. - -## Joining a MultiWorld Game - -1. Run Bk_Sudoku.exe -2. Enter the name of the slot you wish to connect to -3. Enter the server url & port number -4. Press connect -5. Choose difficulty -6. Try to solve the Sudoku From 6432560fe5643aec302c4cc96761a12a03a5b8a2 Mon Sep 17 00:00:00 2001 From: qwint Date: Sun, 2 Jun 2024 21:39:34 -0500 Subject: [PATCH 02/28] Fix Egg_Shop typo in costsanity (#3447) --- worlds/hk/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/hk/__init__.py b/worlds/hk/__init__.py index fdaece8d34cd..78287305df5f 100644 --- a/worlds/hk/__init__.py +++ b/worlds/hk/__init__.py @@ -405,7 +405,7 @@ def _compute_weights(weights: dict, desc: str) -> typing.Dict[str, int]: continue if setting == CostSanity.option_shopsonly and location.basename not in multi_locations: continue - if location.basename in {'Grubfather', 'Seer', 'Eggshop'}: + if location.basename in {'Grubfather', 'Seer', 'Egg_Shop'}: our_weights = dict(weights_geoless) else: our_weights = dict(weights) From 424c8b0be9654f9c8556c1e68fcc093d00f860c6 Mon Sep 17 00:00:00 2001 From: Remy Jette Date: Sun, 2 Jun 2024 22:42:15 -0400 Subject: [PATCH 03/28] Pokemon RB: Add an item group for each HM to improve hinting (#3311) * Pokemon RB: Add an item group for each HM HMs are suffixed with the name of the move, e.g. "HM02 Fly". If TM move are randomized, they do not have the move name, e.g. "TM02". If someone hints for an HM using the just the number, the fuzzy matching sees "TM02" as closer than "HM02 Fly", and in fact sees it as close enough to not ask the user to confirm, leading them to waste hint points on non-progression item that they didn't intend. Emerald already does this for this reason, adding the same for RB. * Add the new groups for HMs in the item_table instead --- worlds/pokemon_rb/items.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/worlds/pokemon_rb/items.py b/worlds/pokemon_rb/items.py index 24cad13252b1..de29f341c6df 100644 --- a/worlds/pokemon_rb/items.py +++ b/worlds/pokemon_rb/items.py @@ -119,11 +119,11 @@ def __init__(self, item_id, classification, groups): "Card Key 11F": ItemData(109, ItemClassification.progression, ["Unique", "Key Items", "Card Keys"]), "Progressive Card Key": ItemData(110, ItemClassification.progression, ["Unique", "Key Items", "Card Keys"]), "Sleep Trap": ItemData(111, ItemClassification.trap, ["Traps"]), - "HM01 Cut": ItemData(196, ItemClassification.progression, ["Unique", "HMs", "Key Items"]), - "HM02 Fly": ItemData(197, ItemClassification.progression, ["Unique", "HMs", "Key Items"]), - "HM03 Surf": ItemData(198, ItemClassification.progression, ["Unique", "HMs", "Key Items"]), - "HM04 Strength": ItemData(199, ItemClassification.progression, ["Unique", "HMs", "Key Items"]), - "HM05 Flash": ItemData(200, ItemClassification.progression, ["Unique", "HMs", "Key Items"]), + "HM01 Cut": ItemData(196, ItemClassification.progression, ["Unique", "HMs", "HM01", "Key Items"]), + "HM02 Fly": ItemData(197, ItemClassification.progression, ["Unique", "HMs", "HM02", "Key Items"]), + "HM03 Surf": ItemData(198, ItemClassification.progression, ["Unique", "HMs", "HM03", "Key Items"]), + "HM04 Strength": ItemData(199, ItemClassification.progression, ["Unique", "HMs", "HM04", "Key Items"]), + "HM05 Flash": ItemData(200, ItemClassification.progression, ["Unique", "HMs", "HM05", "Key Items"]), "TM01 Mega Punch": ItemData(201, ItemClassification.useful, ["Unique", "TMs"]), "TM02 Razor Wind": ItemData(202, ItemClassification.filler, ["Unique", "TMs"]), "TM03 Swords Dance": ItemData(203, ItemClassification.useful, ["Unique", "TMs"]), From d9120f0bea7fb564c51d3257e2ed67624d73261f Mon Sep 17 00:00:00 2001 From: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> Date: Mon, 3 Jun 2024 04:42:27 -0400 Subject: [PATCH 04/28] WebHost: Allowing options that work on WebHost to be used in presets (#3441) --- WebHostLib/options.py | 2 +- test/webhost/test_option_presets.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/WebHostLib/options.py b/WebHostLib/options.py index 62ba86a56626..53c3a6151b82 100644 --- a/WebHostLib/options.py +++ b/WebHostLib/options.py @@ -91,7 +91,7 @@ def option_presets(game: str) -> Response: f"Expected {option.special_range_names.keys()} or {option.range_start}-{option.range_end}." presets[preset_name][preset_option_name] = option.value - elif isinstance(option, Options.Range): + elif isinstance(option, (Options.Range, Options.OptionSet, Options.OptionList, Options.ItemDict)): presets[preset_name][preset_option_name] = option.value elif isinstance(preset_option, str): # Ensure the option value is valid for Choice and Toggle options diff --git a/test/webhost/test_option_presets.py b/test/webhost/test_option_presets.py index 0c88b6c2ee6f..b0af8a871183 100644 --- a/test/webhost/test_option_presets.py +++ b/test/webhost/test_option_presets.py @@ -1,7 +1,7 @@ import unittest from worlds import AutoWorldRegister -from Options import Choice, NamedRange, Toggle, Range +from Options import ItemDict, NamedRange, NumericOption, OptionList, OptionSet class TestOptionPresets(unittest.TestCase): @@ -14,7 +14,7 @@ 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) - supported_types = [Choice, Toggle, Range, NamedRange] + 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}' " f"is not a supported type for webhost. " From 70e9ccb13c600072b234027a944a9a190835c37a Mon Sep 17 00:00:00 2001 From: Scipio Wright Date: Mon, 3 Jun 2024 04:44:37 -0400 Subject: [PATCH 05/28] TUNIC: Fix plando connections, seed groups, and UT support (#3429) --- worlds/tunic/__init__.py | 10 ++++++---- worlds/tunic/er_scripts.py | 33 +++++++++++++++++++-------------- worlds/tunic/options.py | 4 ++-- 3 files changed, 27 insertions(+), 20 deletions(-) diff --git a/worlds/tunic/__init__.py b/worlds/tunic/__init__.py index 9ef5800955aa..624208da3a0b 100644 --- a/worlds/tunic/__init__.py +++ b/worlds/tunic/__init__.py @@ -8,7 +8,7 @@ from .regions import tunic_regions from .er_scripts import create_er_regions from .er_data import portal_mapping -from .options import TunicOptions, EntranceRando, tunic_option_groups, tunic_option_presets +from .options import TunicOptions, EntranceRando, tunic_option_groups, tunic_option_presets, TunicPlandoConnections from worlds.AutoWorld import WebWorld, World from Options import PlandoConnection from decimal import Decimal, ROUND_HALF_UP @@ -43,7 +43,7 @@ class SeedGroup(TypedDict): logic_rules: int # logic rules value laurels_at_10_fairies: bool # laurels location value fixed_shop: bool # fixed shop value - plando: List[PlandoConnection] # consolidated list of plando connections for the seed group + plando: TunicPlandoConnections # consolidated of plando connections for the seed group class TunicWorld(World): @@ -96,13 +96,15 @@ def generate_early(self) -> None: self.options.hexagon_quest.value = passthrough["hexagon_quest"] self.options.entrance_rando.value = passthrough["entrance_rando"] self.options.shuffle_ladders.value = passthrough["shuffle_ladders"] + self.options.fixed_shop.value = self.options.fixed_shop.option_false + self.options.laurels_location.value = self.options.laurels_location.option_anywhere @classmethod def stage_generate_early(cls, multiworld: MultiWorld) -> None: tunic_worlds: Tuple[TunicWorld] = multiworld.get_game_worlds("TUNIC") for tunic in tunic_worlds: # if it's one of the options, then it isn't a custom seed group - if tunic.options.entrance_rando.value in EntranceRando.options: + if tunic.options.entrance_rando.value in EntranceRando.options.values(): continue group = tunic.options.entrance_rando.value # if this is the first world in the group, set the rules equal to its rules @@ -147,7 +149,7 @@ def stage_generate_early(cls, multiworld: MultiWorld) -> None: f"{tunic.multiworld.get_player_name(tunic.player)}'s plando " f"connection {cxn.entrance} <-> {cxn.exit}") if new_cxn: - cls.seed_groups[group]["plando"].append(cxn) + cls.seed_groups[group]["plando"].value.append(cxn) def create_item(self, name: str) -> TunicItem: item_data = item_table[name] diff --git a/worlds/tunic/er_scripts.py b/worlds/tunic/er_scripts.py index 7e022c9f3a0d..9d25137ba469 100644 --- a/worlds/tunic/er_scripts.py +++ b/worlds/tunic/er_scripts.py @@ -140,7 +140,7 @@ def pair_portals(world: "TunicWorld") -> Dict[Portal, Portal]: waterfall_plando = False # if it's not one of the EntranceRando options, it's a custom seed - if world.options.entrance_rando.value not in EntranceRando.options: + if world.options.entrance_rando.value not in EntranceRando.options.values(): seed_group = world.seed_groups[world.options.entrance_rando.value] logic_rules = seed_group["logic_rules"] fixed_shop = seed_group["fixed_shop"] @@ -162,6 +162,11 @@ def pair_portals(world: "TunicWorld") -> Dict[Portal, Portal]: portal_map.remove(portal) break + # If using Universal Tracker, restore portal_map. Could be cleaner, but it does not matter for UT even a little bit + if hasattr(world.multiworld, "re_gen_passthrough"): + if "TUNIC" in world.multiworld.re_gen_passthrough: + portal_map = portal_mapping.copy() + # create separate lists for dead ends and non-dead ends for portal in portal_map: dead_end_status = tunic_er_regions[portal.region].dead_end @@ -193,7 +198,7 @@ def pair_portals(world: "TunicWorld") -> Dict[Portal, Portal]: connected_regions.add(start_region) connected_regions = update_reachable_regions(connected_regions, traversal_reqs, has_laurels, logic_rules) - if world.options.entrance_rando.value in EntranceRando.options: + if world.options.entrance_rando.value in EntranceRando.options.values(): plando_connections = world.options.plando_connections.value else: plando_connections = world.seed_groups[world.options.entrance_rando.value]["plando"] @@ -255,7 +260,7 @@ def pair_portals(world: "TunicWorld") -> Dict[Portal, Portal]: else: # if not both, they're both dead ends if not portal2: - if world.options.entrance_rando.value not in EntranceRando.options: + if world.options.entrance_rando.value not in EntranceRando.options.values(): raise Exception(f"Tunic ER seed group {world.options.entrance_rando.value} paired a dead " "end to a dead end in their plando connections.") else: @@ -302,21 +307,21 @@ def pair_portals(world: "TunicWorld") -> Dict[Portal, Portal]: traversal_reqs.setdefault(portal1.region, dict())[portal2.region] = [] traversal_reqs.setdefault(portal2.region, dict())[portal1.region] = [] - if portal1.region == "Zig Skip Exit" or portal2.region == "Zig Skip Exit": - if portal1_dead_end or portal2_dead_end or \ - portal1.region == "Secret Gathering Place" or portal2.region == "Secret Gathering Place": - if world.options.entrance_rando.value not in EntranceRando.options: - raise Exception(f"Tunic ER seed group {world.options.entrance_rando.value} paired a dead " - "end to a dead end in their plando connections.") - else: - raise Exception(f"{player_name} paired a dead end to a dead end in their " - "plando connections.") + if (portal1.region == "Zig Skip Exit" and (portal2_dead_end or portal2.region == "Secret Gathering Place") + or portal2.region == "Zig Skip Exit" and (portal1_dead_end or portal1.region == "Secret Gathering Place")): + if world.options.entrance_rando.value not in EntranceRando.options.values(): + raise Exception(f"Tunic ER seed group {world.options.entrance_rando.value} paired a dead " + "end to a dead end in their plando connections.") + else: + raise Exception(f"{player_name} paired a dead end to a dead end in their " + "plando connections.") - if portal1.region == "Secret Gathering Place" or portal2.region == "Secret Gathering Place": + if (portal1.region == "Secret Gathering Place" and (portal2_dead_end or portal2.region == "Zig Skip Exit") + or portal2.region == "Secret Gathering Place" and (portal1_dead_end or portal1.region == "Zig Skip Exit")): # need to make sure you didn't pair this to a dead end or zig skip if portal1_dead_end or portal2_dead_end or \ portal1.region == "Zig Skip Exit" or portal2.region == "Zig Skip Exit": - if world.options.entrance_rando.value not in EntranceRando.options: + if world.options.entrance_rando.value not in EntranceRando.options.values(): raise Exception(f"Tunic ER seed group {world.options.entrance_rando.value} paired a dead " "end to a dead end in their plando connections.") else: diff --git a/worlds/tunic/options.py b/worlds/tunic/options.py index b3b6b3b96fb0..ff9872ab4807 100644 --- a/worlds/tunic/options.py +++ b/worlds/tunic/options.py @@ -173,7 +173,7 @@ class ShuffleLadders(Toggle): display_name = "Shuffle Ladders" -class TUNICPlandoConnections(PlandoConnections): +class TunicPlandoConnections(PlandoConnections): entrances = {*(portal.name for portal in portal_mapping), "Shop", "Shop Portal"} exits = {*(portal.name for portal in portal_mapping), "Shop", "Shop Portal"} @@ -198,7 +198,7 @@ class TunicOptions(PerGameCommonOptions): lanternless: Lanternless maskless: Maskless laurels_location: LaurelsLocation - plando_connections: TUNICPlandoConnections + plando_connections: TunicPlandoConnections tunic_option_groups = [ From cff7327558979c3088ff5ac792a4e3d0149fd4e9 Mon Sep 17 00:00:00 2001 From: Zach Parks Date: Mon, 3 Jun 2024 03:45:01 -0500 Subject: [PATCH 06/28] Utils: Fix mistake made with `KeyedDefaultDict` from #1933 that broke tracker functionality. (#3433) --- Utils.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/Utils.py b/Utils.py index eea81a2d3201..a7fd7f4f334c 100644 --- a/Utils.py +++ b/Utils.py @@ -458,8 +458,14 @@ class KeyedDefaultDict(collections.defaultdict): """defaultdict variant that uses the missing key as argument to default_factory""" default_factory: typing.Callable[[typing.Any], typing.Any] - def __init__(self, default_factory: typing.Callable[[Any], Any] = None, **kwargs): - super().__init__(default_factory, **kwargs) + def __init__(self, + default_factory: typing.Callable[[Any], Any] = None, + seq: typing.Union[typing.Mapping, typing.Iterable, None] = None, + **kwargs): + if seq is not None: + super().__init__(default_factory, seq, **kwargs) + else: + super().__init__(default_factory, **kwargs) def __missing__(self, key): self[key] = value = self.default_factory(key) From fb2c194e3733c3ba200cfff52bf89ddb4c4a6912 Mon Sep 17 00:00:00 2001 From: Star Rauchenberger Date: Mon, 3 Jun 2024 04:51:27 -0400 Subject: [PATCH 07/28] Lingo: Fix Basement access with THE MASTER (#3231) --- worlds/lingo/player_logic.py | 19 ++++++++++++++----- worlds/lingo/regions.py | 9 ++++++++- worlds/lingo/rules.py | 9 +++------ worlds/lingo/test/TestMastery.py | 19 ++++++++++++++++++- 4 files changed, 43 insertions(+), 13 deletions(-) diff --git a/worlds/lingo/player_logic.py b/worlds/lingo/player_logic.py index b6941f37eed1..1621620e1e14 100644 --- a/worlds/lingo/player_logic.py +++ b/worlds/lingo/player_logic.py @@ -18,19 +18,23 @@ class AccessRequirements: rooms: Set[str] doors: Set[RoomAndDoor] colors: Set[str] + the_master: bool def __init__(self): self.rooms = set() self.doors = set() self.colors = set() + self.the_master = False def merge(self, other: "AccessRequirements"): self.rooms |= other.rooms self.doors |= other.doors self.colors |= other.colors + self.the_master |= other.the_master def __str__(self): - return f"AccessRequirements(rooms={self.rooms}, doors={self.doors}, colors={self.colors})" + return f"AccessRequirements(rooms={self.rooms}, doors={self.doors}, colors={self.colors})," \ + f" the_master={self.the_master}" class PlayerLocation(NamedTuple): @@ -463,6 +467,9 @@ def calculate_panel_requirements(self, room: str, panel: str, world: "LingoWorld req_panel.panel, world) access_reqs.merge(sub_access_reqs) + if panel == "THE MASTER": + access_reqs.the_master = True + self.panel_reqs[room][panel] = access_reqs return self.panel_reqs[room][panel] @@ -502,15 +509,17 @@ def create_panel_hunt_events(self, world: "LingoWorld"): unhindered_panels_by_color: dict[Optional[str], int] = {} for panel_name, panel_data in room_data.items(): - # We won't count non-counting panels. THE MASTER has special access rules and is handled separately. - if panel_data.non_counting or panel_name == "THE MASTER": + # We won't count non-counting panels. + if panel_data.non_counting: continue # We won't coalesce any panels that have requirements beyond colors. To simplify things for now, we will - # only coalesce single-color panels. Chains/stacks/combo puzzles will be separate. + # only coalesce single-color panels. Chains/stacks/combo puzzles will be separate. THE MASTER has + # special access rules and is handled separately. if len(panel_data.required_panels) > 0 or len(panel_data.required_doors) > 0\ or len(panel_data.required_rooms) > 0\ - or (world.options.shuffle_colors and len(panel_data.colors) > 1): + or (world.options.shuffle_colors and len(panel_data.colors) > 1)\ + or panel_name == "THE MASTER": self.counting_panel_reqs.setdefault(room_name, []).append( (self.calculate_panel_requirements(room_name, panel_name, world), 1)) else: diff --git a/worlds/lingo/regions.py b/worlds/lingo/regions.py index 4b357db261b4..9834f04f9de7 100644 --- a/worlds/lingo/regions.py +++ b/worlds/lingo/regions.py @@ -49,8 +49,15 @@ def connect_entrance(regions: Dict[str, Region], source_region: Region, target_r if door is not None: effective_room = target_region.name if door.room is None else door.room if door.door not in world.player_logic.item_by_door.get(effective_room, {}): - for region in world.player_logic.calculate_door_requirements(effective_room, door.door, world).rooms: + access_reqs = world.player_logic.calculate_door_requirements(effective_room, door.door, world) + for region in access_reqs.rooms: world.multiworld.register_indirect_condition(regions[region], connection) + + # This pretty much only applies to Orange Tower Sixth Floor -> Orange Tower Basement. + if access_reqs.the_master: + for mastery_req in world.player_logic.mastery_reqs: + for region in mastery_req.rooms: + world.multiworld.register_indirect_condition(regions[region], connection) if not pilgrimage and world.options.enable_pilgrimage and is_acceptable_pilgrimage_entrance(entrance_type, world)\ and source_region.name != "Menu": diff --git a/worlds/lingo/rules.py b/worlds/lingo/rules.py index 9cc11fdaea31..d91c53f05b47 100644 --- a/worlds/lingo/rules.py +++ b/worlds/lingo/rules.py @@ -42,12 +42,6 @@ def lingo_can_use_level_2_location(state: CollectionState, world: "LingoWorld"): counted_panels += panel_count if counted_panels >= world.options.level_2_requirement.value - 1: return True - # THE MASTER has to be handled separately, because it has special access rules. - if state.can_reach("Orange Tower Seventh Floor", "Region", world.player)\ - and lingo_can_use_mastery_location(state, world): - counted_panels += 1 - if counted_panels >= world.options.level_2_requirement.value - 1: - return True return False @@ -65,6 +59,9 @@ def _lingo_can_satisfy_requirements(state: CollectionState, access: AccessRequir if not state.has(color.capitalize(), world.player): return False + if access.the_master and not lingo_can_use_mastery_location(state, world): + return False + return True diff --git a/worlds/lingo/test/TestMastery.py b/worlds/lingo/test/TestMastery.py index 3fb3c95a0208..6e563393cf7f 100644 --- a/worlds/lingo/test/TestMastery.py +++ b/worlds/lingo/test/TestMastery.py @@ -36,4 +36,21 @@ def test_requirement(self): self.assertFalse(self.can_reach_location("Orange Tower Seventh Floor - Mastery Achievements")) self.collect_by_name(["Green", "Gray", "Brown", "Yellow"]) - self.assertTrue(self.can_reach_location("Orange Tower Seventh Floor - Mastery Achievements")) \ No newline at end of file + self.assertTrue(self.can_reach_location("Orange Tower Seventh Floor - Mastery Achievements")) + + +class TestMasteryBlocksDependents(LingoTestBase): + options = { + "mastery_achievements": "24", + "shuffle_colors": "true", + "location_checks": "insanity" + } + + def test_requirement(self): + self.collect_all_but("Gray") + self.assertFalse(self.can_reach_location("Orange Tower Basement - THE LIBRARY")) + self.assertFalse(self.can_reach_location("Orange Tower Seventh Floor - MASTERY")) + + self.collect_by_name("Gray") + self.assertTrue(self.can_reach_location("Orange Tower Basement - THE LIBRARY")) + self.assertTrue(self.can_reach_location("Orange Tower Seventh Floor - MASTERY")) From c7eef13b335204f750759e75a43034400ec7cc82 Mon Sep 17 00:00:00 2001 From: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> Date: Mon, 3 Jun 2024 10:36:51 -0400 Subject: [PATCH 08/28] Accounting for name change (#3449) --- worlds/lingo/test/TestMastery.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/worlds/lingo/test/TestMastery.py b/worlds/lingo/test/TestMastery.py index 6e563393cf7f..3ebe40aa22d7 100644 --- a/worlds/lingo/test/TestMastery.py +++ b/worlds/lingo/test/TestMastery.py @@ -49,8 +49,8 @@ class TestMasteryBlocksDependents(LingoTestBase): def test_requirement(self): self.collect_all_but("Gray") self.assertFalse(self.can_reach_location("Orange Tower Basement - THE LIBRARY")) - self.assertFalse(self.can_reach_location("Orange Tower Seventh Floor - MASTERY")) + self.assertFalse(self.can_reach_location("The Fearless - MASTERY")) self.collect_by_name("Gray") self.assertTrue(self.can_reach_location("Orange Tower Basement - THE LIBRARY")) - self.assertTrue(self.can_reach_location("Orange Tower Seventh Floor - MASTERY")) + self.assertTrue(self.can_reach_location("The Fearless - MASTERY")) From 06e65c1dc6ce4a1564d1b6924b83a0c9546011ec Mon Sep 17 00:00:00 2001 From: Chris Wilson Date: Mon, 3 Jun 2024 18:43:01 -0400 Subject: [PATCH 09/28] WebHost: weighted-options bugfixes (#3448) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix improper css for word-break on player-options page * Add default handling to weighted-options types * Remove random-low/mid/high from Toggle, Choice, and TextChoice, * Port key sorting for OptionList and OptionSet from player-options to weighted-options * Ensure Choice and TextChoice values are set properly * Remove debug line 🤦‍♂️ --- .../styles/playerOptions/playerOptions.css | 2 +- .../styles/playerOptions/playerOptions.scss | 2 +- .../templates/weightedOptions/macros.html | 35 +++++++++++-------- 3 files changed, 23 insertions(+), 16 deletions(-) diff --git a/WebHostLib/static/styles/playerOptions/playerOptions.css b/WebHostLib/static/styles/playerOptions/playerOptions.css index 6165e3a0f622..56c9263d3330 100644 --- a/WebHostLib/static/styles/playerOptions/playerOptions.css +++ b/WebHostLib/static/styles/playerOptions/playerOptions.css @@ -15,7 +15,7 @@ html { border-radius: 8px; padding: 1rem; color: #eeffeb; - word-break: break-all; + word-break: break-word; } #player-options #player-options-header h1 { margin-bottom: 0; diff --git a/WebHostLib/static/styles/playerOptions/playerOptions.scss b/WebHostLib/static/styles/playerOptions/playerOptions.scss index 525b8ef15403..06bde759d263 100644 --- a/WebHostLib/static/styles/playerOptions/playerOptions.scss +++ b/WebHostLib/static/styles/playerOptions/playerOptions.scss @@ -16,7 +16,7 @@ html{ border-radius: 8px; padding: 1rem; color: #eeffeb; - word-break: break-all; + word-break: break-word; #player-options-header{ h1{ diff --git a/WebHostLib/templates/weightedOptions/macros.html b/WebHostLib/templates/weightedOptions/macros.html index 5b8944a43887..a6e4545fdaf7 100644 --- a/WebHostLib/templates/weightedOptions/macros.html +++ b/WebHostLib/templates/weightedOptions/macros.html @@ -1,9 +1,9 @@ {% macro Toggle(option_name, option) %} - {{ RangeRow(option_name, option, "No", "false") }} - {{ RangeRow(option_name, option, "Yes", "true") }} - {{ RandomRows(option_name, option) }} + {{ RangeRow(option_name, option, "No", "false", False, "true" if option.default else "false") }} + {{ RangeRow(option_name, option, "Yes", "true", False, "true" if option.default else "false") }} + {{ RandomRow(option_name, option) }}
{% endmacro %} @@ -18,10 +18,10 @@ {% for id, name in option.name_lookup.items() %} {% if name != 'random' %} - {{ RangeRow(option_name, option, option.get_option_name(id), name) }} + {{ RangeRow(option_name, option, option.get_option_name(id), name, False, name if option.get_option_name(option.default)|lower == name|lower else None) }} {% endif %} {% endfor %} - {{ RandomRows(option_name, option) }} + {{ RandomRow(option_name, option) }} {% endmacro %} @@ -72,7 +72,9 @@ - + {% if option.default %} + {{ RangeRow(option_name, option, option.default, option.default) }} + {% endif %}
@@ -90,10 +92,10 @@ {% for id, name in option.name_lookup.items() %} {% if name != 'random' %} - {{ RangeRow(option_name, option, option.get_option_name(id), name) }} + {{ RangeRow(option_name, option, option.get_option_name(id), name, False, name if option.get_option_name(option.default)|lower == name else None) }} {% endif %} {% endfor %} - {{ RandomRows(option_name, option) }} + {{ RandomRow(option_name, option) }} {% endmacro %} @@ -112,7 +114,7 @@ type="number" id="{{ option_name }}-{{ item_name }}-qty" name="{{ option_name }}||{{ item_name }}" - value="0" + value="{{ option.default[item_name] if item_name in option.default else "0" }}" /> {% endfor %} @@ -121,13 +123,14 @@ {% macro OptionList(option_name, option) %}
- {% for key in option.valid_keys|sort %} + {% for key in (option.valid_keys if option.valid_keys is ordered else option.valid_keys|sort) %}