From 06ba9ba76d1c54b023f91f3e96887d4645af7693 Mon Sep 17 00:00:00 2001 From: beauxq Date: Wed, 27 Sep 2023 18:41:09 -0700 Subject: [PATCH 1/3] change Zillion to new options dataclass --- worlds/zillion/__init__.py | 14 +++- worlds/zillion/options.py | 127 +++++++++++++---------------- worlds/zillion/test/TestOptions.py | 10 ++- 3 files changed, 75 insertions(+), 76 deletions(-) diff --git a/worlds/zillion/__init__.py b/worlds/zillion/__init__.py index 7c927c10eb92..40475656bd46 100644 --- a/worlds/zillion/__init__.py +++ b/worlds/zillion/__init__.py @@ -13,7 +13,7 @@ from .config import detect_test from .logic import cs_to_zz_locs from .region import ZillionLocation, ZillionRegion -from .options import ZillionStartChar, zillion_options, validate +from .options import ZillionOptions, ZillionStartChar, validate from .id_maps import item_name_to_id as _item_name_to_id, \ loc_name_to_id as _loc_name_to_id, make_id_to_others, \ zz_reg_name_to_reg_name, base_id @@ -70,7 +70,9 @@ class ZillionWorld(World): game = "Zillion" web = ZillionWebWorld() - option_definitions = zillion_options + options_dataclass = ZillionOptions + options: ZillionOptions + settings: typing.ClassVar[ZillionSettings] topology_present = True # indicate if world type has any meaningful layout/pathing @@ -142,7 +144,10 @@ def generate_early(self) -> None: if not hasattr(self.multiworld, "zillion_logic_cache"): setattr(self.multiworld, "zillion_logic_cache", {}) - zz_op, item_counts = validate(self.multiworld, self.player) + zz_op, item_counts = validate(self.options, self.player) + + if zz_op.early_scope: + self.multiworld.early_items[self.player]["Scope"] = 1 self._item_counts = item_counts @@ -299,7 +304,8 @@ def stage_generate_basic(multiworld: MultiWorld, *args: Any) -> None: elif start_char_counts["Champ"] > start_char_counts["Apple"]: to_stay = "Champ" else: # equal - to_stay = multiworld.random.choice(("Apple", "Champ")) + choices: Tuple[Literal['Apple', 'Champ', 'JJ'], ...] = ("Apple", "Champ") + to_stay = multiworld.random.choice(choices) for p, sc in players_start_chars: if sc != to_stay: diff --git a/worlds/zillion/options.py b/worlds/zillion/options.py index 6aa88f5b220f..7ff23ebd796d 100644 --- a/worlds/zillion/options.py +++ b/worlds/zillion/options.py @@ -1,13 +1,13 @@ from collections import Counter -# import logging -from typing import TYPE_CHECKING, Any, Dict, Tuple, cast -from Options import AssembleOptions, DefaultOnToggle, Range, SpecialRange, Toggle, Choice +from dataclasses import dataclass +from typing import Dict, Tuple + +from Options import DefaultOnToggle, PerGameCommonOptions, Range, SpecialRange, Toggle, Choice + from zilliandomizer.options import \ Options as ZzOptions, char_to_gun, char_to_jump, ID, \ VBLR as ZzVBLR, chars, Chars, ItemCounts as ZzItemCounts from zilliandomizer.options.parsing import validate as zz_validate -if TYPE_CHECKING: - from BaseClasses import MultiWorld class ZillionContinues(SpecialRange): @@ -41,6 +41,12 @@ class VBLR(Choice): option_restrictive = 3 default = 1 + def to_zz_vblr(self) -> ZzVBLR: + key = self.current_key + assert key in ("vanilla", "balanced", "low", "restrictive"), f"{key=}" + return key # type: ignore + # mypy can't do type narrowing to literals + class ZillionGunLevels(VBLR): """ @@ -225,27 +231,27 @@ class ZillionRoomGen(Toggle): display_name = "room generation" -zillion_options: Dict[str, AssembleOptions] = { - "continues": ZillionContinues, - "floppy_req": ZillionFloppyReq, - "gun_levels": ZillionGunLevels, - "jump_levels": ZillionJumpLevels, - "randomize_alarms": ZillionRandomizeAlarms, - "max_level": ZillionMaxLevel, - "start_char": ZillionStartChar, - "opas_per_level": ZillionOpasPerLevel, - "id_card_count": ZillionIDCardCount, - "bread_count": ZillionBreadCount, - "opa_opa_count": ZillionOpaOpaCount, - "zillion_count": ZillionZillionCount, - "floppy_disk_count": ZillionFloppyDiskCount, - "scope_count": ZillionScopeCount, - "red_id_card_count": ZillionRedIDCardCount, - "early_scope": ZillionEarlyScope, - "skill": ZillionSkill, - "starting_cards": ZillionStartingCards, - "room_gen": ZillionRoomGen, -} +@dataclass +class ZillionOptions(PerGameCommonOptions): + continues: ZillionContinues + floppy_req: ZillionFloppyReq + gun_levels: ZillionGunLevels + jump_levels: ZillionJumpLevels + randomize_alarms: ZillionRandomizeAlarms + max_level: ZillionMaxLevel + start_char: ZillionStartChar + opas_per_level: ZillionOpasPerLevel + id_card_count: ZillionIDCardCount + bread_count: ZillionBreadCount + opa_opa_count: ZillionOpaOpaCount + zillion_count: ZillionZillionCount + floppy_disk_count: ZillionFloppyDiskCount + scope_count: ZillionScopeCount + red_id_card_count: ZillionRedIDCardCount + early_scope: ZillionEarlyScope + skill: ZillionSkill + starting_cards: ZillionStartingCards + room_gen: ZillionRoomGen def convert_item_counts(ic: "Counter[str]") -> ZzItemCounts: @@ -262,47 +268,35 @@ def convert_item_counts(ic: "Counter[str]") -> ZzItemCounts: return tr -def validate(world: "MultiWorld", p: int) -> "Tuple[ZzOptions, Counter[str]]": +def validate(options: ZillionOptions, p: int) -> "Tuple[ZzOptions, Counter[str]]": """ adjusts options to make game completion possible - `world` parameter is MultiWorld object that has my options on it + `options` parameter is ZillionOptions object that was put on my world by the core `p` is my player id """ - for option_name in zillion_options: - assert hasattr(world, option_name), f"Zillion option {option_name} didn't get put in world object" - wo = cast(Any, world) # so I don't need getattr on all the options - skill = wo.skill[p].value + skill = options.skill.value - jump_levels = cast(ZillionJumpLevels, wo.jump_levels[p]) - jump_option = jump_levels.current_key - required_level = char_to_jump["Apple"][cast(ZzVBLR, jump_option)].index(3) + 1 + jump_option = options.jump_levels.to_zz_vblr() + required_level = char_to_jump["Apple"][jump_option].index(3) + 1 if skill == 0: # because of hp logic on final boss required_level = 8 - gun_levels = cast(ZillionGunLevels, wo.gun_levels[p]) - gun_option = gun_levels.current_key - guns_required = char_to_gun["Champ"][cast(ZzVBLR, gun_option)].index(3) + gun_option = options.gun_levels.to_zz_vblr() + guns_required = char_to_gun["Champ"][gun_option].index(3) - floppy_req = cast(ZillionFloppyReq, wo.floppy_req[p]) + floppy_req = options.floppy_req - card = cast(ZillionIDCardCount, wo.id_card_count[p]) - bread = cast(ZillionBreadCount, wo.bread_count[p]) - opa = cast(ZillionOpaOpaCount, wo.opa_opa_count[p]) - gun = cast(ZillionZillionCount, wo.zillion_count[p]) - floppy = cast(ZillionFloppyDiskCount, wo.floppy_disk_count[p]) - scope = cast(ZillionScopeCount, wo.scope_count[p]) - red = cast(ZillionRedIDCardCount, wo.red_id_card_count[p]) item_counts = Counter({ - "ID Card": card, - "Bread": bread, - "Opa-Opa": opa, - "Zillion": gun, - "Floppy Disk": floppy, - "Scope": scope, - "Red ID Card": red + "ID Card": options.id_card_count, + "Bread": options.bread_count, + "Opa-Opa": options.opa_opa_count, + "Zillion": options.zillion_count, + "Floppy Disk": options.floppy_disk_count, + "Scope": options.scope_count, + "Red ID Card": options.red_id_card_count }) minimums = Counter({ "ID Card": 0, @@ -335,10 +329,10 @@ def validate(world: "MultiWorld", p: int) -> "Tuple[ZzOptions, Counter[str]]": item_counts["Empty"] += diff assert sum(item_counts.values()) == 144 - max_level = cast(ZillionMaxLevel, wo.max_level[p]) + max_level = options.max_level max_level.value = max(required_level, max_level.value) - opas_per_level = cast(ZillionOpasPerLevel, wo.opas_per_level[p]) + opas_per_level = options.opas_per_level while (opas_per_level.value > 1) and (1 + item_counts["Opa-Opa"] // opas_per_level.value < max_level.value): # logging.warning( # "zillion options validate: option opas_per_level incompatible with options max_level and opa_opa_count" @@ -347,39 +341,34 @@ def validate(world: "MultiWorld", p: int) -> "Tuple[ZzOptions, Counter[str]]": # that should be all of the level requirements met - name_capitalization = { + name_capitalization: Dict[str, Chars] = { "jj": "JJ", "apple": "Apple", "champ": "Champ", } - start_char = cast(ZillionStartChar, wo.start_char[p]) + start_char = options.start_char start_char_name = name_capitalization[start_char.current_key] assert start_char_name in chars - start_char_name = cast(Chars, start_char_name) - - starting_cards = cast(ZillionStartingCards, wo.starting_cards[p]) - room_gen = cast(ZillionRoomGen, wo.room_gen[p]) + starting_cards = options.starting_cards - early_scope = cast(ZillionEarlyScope, wo.early_scope[p]) - if early_scope: - world.early_items[p]["Scope"] = 1 + room_gen = options.room_gen zz_item_counts = convert_item_counts(item_counts) zz_op = ZzOptions( zz_item_counts, - cast(ZzVBLR, jump_option), - cast(ZzVBLR, gun_option), + jump_option, + gun_option, opas_per_level.value, max_level.value, False, # tutorial skill, start_char_name, floppy_req.value, - wo.continues[p].value, - wo.randomize_alarms[p].value, - False, # early scope is done with AP early_items API + options.continues.value, + bool(options.randomize_alarms.value), + bool(options.early_scope.value), True, # balance defense starting_cards.value, bool(room_gen.value) diff --git a/worlds/zillion/test/TestOptions.py b/worlds/zillion/test/TestOptions.py index 1ec186dae50a..bb91ae438d11 100644 --- a/worlds/zillion/test/TestOptions.py +++ b/worlds/zillion/test/TestOptions.py @@ -1,6 +1,6 @@ from . import ZillionTestBase -from worlds.zillion.options import ZillionJumpLevels, ZillionGunLevels, validate +from worlds.zillion.options import ZillionJumpLevels, ZillionGunLevels, ZillionOptions, validate from zilliandomizer.options import VBLR_CHOICES @@ -9,7 +9,9 @@ class OptionsTest(ZillionTestBase): def test_validate_default(self) -> None: self.world_setup() - validate(self.multiworld, 1) + options = self.multiworld.worlds[1].options + assert isinstance(options, ZillionOptions) + validate(options, 1) def test_vblr_ap_to_zz(self) -> None: """ all of the valid values for the AP options map to valid values for ZZ options """ @@ -20,7 +22,9 @@ def test_vblr_ap_to_zz(self) -> None: for value in vblr_class.name_lookup.values(): self.options = {option_name: value} self.world_setup() - zz_options, _item_counts = validate(self.multiworld, 1) + options = self.multiworld.worlds[1].options + assert isinstance(options, ZillionOptions) + zz_options, _item_counts = validate(options, 1) assert getattr(zz_options, option_name) in VBLR_CHOICES # TODO: test validate with invalid combinations of options From 7d09402b157e8c17aab29f2843f2d9a20395965d Mon Sep 17 00:00:00 2001 From: beauxq Date: Wed, 27 Sep 2023 18:48:38 -0700 Subject: [PATCH 2/3] remove unused parameter to function --- worlds/zillion/__init__.py | 2 +- worlds/zillion/options.py | 3 +-- worlds/zillion/test/TestOptions.py | 4 ++-- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/worlds/zillion/__init__.py b/worlds/zillion/__init__.py index 40475656bd46..c39511728d4a 100644 --- a/worlds/zillion/__init__.py +++ b/worlds/zillion/__init__.py @@ -144,7 +144,7 @@ def generate_early(self) -> None: if not hasattr(self.multiworld, "zillion_logic_cache"): setattr(self.multiworld, "zillion_logic_cache", {}) - zz_op, item_counts = validate(self.options, self.player) + zz_op, item_counts = validate(self.options) if zz_op.early_scope: self.multiworld.early_items[self.player]["Scope"] = 1 diff --git a/worlds/zillion/options.py b/worlds/zillion/options.py index 7ff23ebd796d..e03ed9bb7899 100644 --- a/worlds/zillion/options.py +++ b/worlds/zillion/options.py @@ -268,12 +268,11 @@ def convert_item_counts(ic: "Counter[str]") -> ZzItemCounts: return tr -def validate(options: ZillionOptions, p: int) -> "Tuple[ZzOptions, Counter[str]]": +def validate(options: ZillionOptions) -> "Tuple[ZzOptions, Counter[str]]": """ adjusts options to make game completion possible `options` parameter is ZillionOptions object that was put on my world by the core - `p` is my player id """ skill = options.skill.value diff --git a/worlds/zillion/test/TestOptions.py b/worlds/zillion/test/TestOptions.py index bb91ae438d11..c4f02d4bd3be 100644 --- a/worlds/zillion/test/TestOptions.py +++ b/worlds/zillion/test/TestOptions.py @@ -11,7 +11,7 @@ def test_validate_default(self) -> None: self.world_setup() options = self.multiworld.worlds[1].options assert isinstance(options, ZillionOptions) - validate(options, 1) + validate(options) def test_vblr_ap_to_zz(self) -> None: """ all of the valid values for the AP options map to valid values for ZZ options """ @@ -24,7 +24,7 @@ def test_vblr_ap_to_zz(self) -> None: self.world_setup() options = self.multiworld.worlds[1].options assert isinstance(options, ZillionOptions) - zz_options, _item_counts = validate(options, 1) + zz_options, _item_counts = validate(options) assert getattr(zz_options, option_name) in VBLR_CHOICES # TODO: test validate with invalid combinations of options From ddb630bcd659a295558acfcb730b29b6b53c089d Mon Sep 17 00:00:00 2001 From: beauxq Date: Sun, 1 Oct 2023 10:01:11 -0700 Subject: [PATCH 3/3] use TypeGuard for Literal narrowing --- worlds/zillion/options.py | 14 +++++++++++--- worlds/zillion/requirements.txt | 1 + 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/worlds/zillion/options.py b/worlds/zillion/options.py index e03ed9bb7899..80f9469ec8c0 100644 --- a/worlds/zillion/options.py +++ b/worlds/zillion/options.py @@ -1,6 +1,7 @@ from collections import Counter from dataclasses import dataclass from typing import Dict, Tuple +from typing_extensions import TypeGuard # remove when Python >= 3.10 from Options import DefaultOnToggle, PerGameCommonOptions, Range, SpecialRange, Toggle, Choice @@ -42,10 +43,17 @@ class VBLR(Choice): default = 1 def to_zz_vblr(self) -> ZzVBLR: + def is_vblr(o: str) -> TypeGuard[ZzVBLR]: + """ + This function is because mypy doesn't support narrowing with `in`, + https://github.com/python/mypy/issues/12535 + so this is the only way I see to get type narrowing to `Literal`. + """ + return o in ("vanilla", "balanced", "low", "restrictive") + key = self.current_key - assert key in ("vanilla", "balanced", "low", "restrictive"), f"{key=}" - return key # type: ignore - # mypy can't do type narrowing to literals + assert is_vblr(key), f"{key=}" + return key class ZillionGunLevels(VBLR): diff --git a/worlds/zillion/requirements.txt b/worlds/zillion/requirements.txt index 2af057decef9..69ae1d04cb4c 100644 --- a/worlds/zillion/requirements.txt +++ b/worlds/zillion/requirements.txt @@ -1 +1,2 @@ zilliandomizer @ git+https://github.com/beauxq/zilliandomizer@4b27d115269db25fe73b0471b73495f41df1323c#0.5.3 +typing-extensions>=4.7, <5