Core: move option results to the World class instead of MultiWorld (#993)

🤞 

* map option objects to a `World.options` dict

* convert RoR2 to options dict system for testing

* add temp behavior for lttp with notes

* copy/paste bad

* convert `set_default_common_options` to a namespace property

* reorganize test call order

* have fill_restrictive use the new options system

* update world api

* update soe tests

* fix world api

* core: auto initialize a dataclass on the World class with the option results

* core: auto initialize a dataclass on the World class with the option results: small tying improvement

* add `as_dict` method to the options dataclass

* fix namespace issues with tests

* have current option updates use `.value` instead of changing the option

* update ror2 to use the new options system again

* revert the junk pool dict since it's cased differently

* fix begin_with_loop typo

* write new and old options to spoiler

* change factorio option behavior back

* fix comparisons

* move common and per_game_common options to new system

* core: automatically create missing options_dataclass from legacy option_definitions

* remove spoiler special casing and add back the Factorio option changing but in new system

* give ArchipIDLE the default options_dataclass so its options get generated and spoilered properly

* reimplement `inspect.get_annotations`

* move option info generation for webhost to new system

* need to include Common and PerGame common since __annotations__ doesn't include super

* use get_type_hints for the options dictionary

* typing.get_type_hints returns the bases too.

* forgot to sweep through generate

* sweep through all the tests

* swap to a metaclass property

* move remaining usages from get_type_hints to metaclass property

* move remaining usages from __annotations__ to metaclass property

* move remaining usages from legacy dictionaries to metaclass property

* remove legacy dictionaries

* cache the metaclass property

* clarify inheritance in world api

* move the messenger to new options system

* add an assert for my dumb

* update the doc

* rename o to options

* missed a spot

* update new messenger options

* comment spacing

Co-authored-by: Doug Hoskisson <beauxq@users.noreply.github.com>

* fix tests

* fix missing import

* make the documentation definition more accurate

* use options system for loc creation

* type cast MessengerWorld

* fix typo and use quotes for cast

* LTTP: set random seed in tests

* ArchipIdle: remove change here as it's default on AutoWorld

* Stardew: Need to set state because `set_default_common_options` used to

* The Messenger: update shop rando and helpers to new system; optimize imports

* Add a kwarg to `as_dict` to do the casing for you

* RoR2: use new kwarg for less code

* RoR2: revert some accidental reverts

* The Messenger: remove an unnecessary variable

* remove TypeVar that isn't used

* CommonOptions not abstract

* Docs: fix mistake in options api.md

Co-authored-by: Doug Hoskisson <beauxq@users.noreply.github.com>

* create options for item link worlds

* revert accidental doc removals

* Item Links: set default options on group

* change Zillion to new options dataclass

* remove unused parameter to function

* use TypeGuard for Literal narrowing

* move dlc quest to new api

* move overcooked 2 to new api

* fixed some missed code in oc2

* - Tried to be compliant with 993 (WIP?)

* - I think it all works now

* - Removed last trace of me touching core

* typo

* It now passes all tests!

* Improve options, fix all issues I hope

* - Fixed init options

* dlcquest: fix bad imports

* missed a file

* - Reduce code duplication

* add as_dict documentation

* - Use .items(), get option name more directly, fix slot data content

* - Remove generic options from the slot data

* improve slot data documentation

* remove `CommonOptions.get_value` (#21)

* better slot data description

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>

---------

Co-authored-by: el-u <109771707+el-u@users.noreply.github.com>
Co-authored-by: Doug Hoskisson <beauxq@users.noreply.github.com>
Co-authored-by: Doug Hoskisson <beauxq@yahoo.com>
Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
Co-authored-by: Alex Gilbert <alexgilbert@yahoo.com>
This commit is contained in:
Aaron Wagener
2023-10-10 15:30:20 -05:00
committed by GitHub
parent a7b4914bb7
commit 7193182294
69 changed files with 1587 additions and 1603 deletions

View File

@@ -12,7 +12,7 @@ from BaseClasses import ItemClassification, LocationProgressType, \
MultiWorld, Item, CollectionState, Entrance, Tutorial
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 @@ class ZillionWorld(World):
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)
if zz_op.early_scope:
self.multiworld.early_items[self.player]["Scope"] = 1
self._item_counts = item_counts
@@ -299,7 +304,8 @@ class ZillionWorld(World):
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:

View File

@@ -1,13 +1,14 @@
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 typing_extensions import TypeGuard # remove when Python >= 3.10
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 +42,19 @@ class VBLR(Choice):
option_restrictive = 3
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 is_vblr(key), f"{key=}"
return key
class ZillionGunLevels(VBLR):
"""
@@ -225,27 +239,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 +276,34 @@ def convert_item_counts(ic: "Counter[str]") -> ZzItemCounts:
return tr
def validate(world: "MultiWorld", p: int) -> "Tuple[ZzOptions, Counter[str]]":
def validate(options: ZillionOptions) -> "Tuple[ZzOptions, Counter[str]]":
"""
adjusts options to make game completion possible
`world` parameter is MultiWorld object that has my options on it
`p` is my player id
`options` parameter is ZillionOptions object that was put on my world by the core
"""
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 +336,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 +348,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])
starting_cards = options.starting_cards
room_gen = cast(ZillionRoomGen, wo.room_gen[p])
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)

View File

@@ -1 +1,2 @@
zilliandomizer @ git+https://github.com/beauxq/zilliandomizer@d7122bcbeda40da5db26d60fad06246a1331706f#0.5.4
typing-extensions>=4.7, <5

View File

@@ -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)
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 @@ class OptionsTest(ZillionTestBase):
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)
assert getattr(zz_options, option_name) in VBLR_CHOICES
# TODO: test validate with invalid combinations of options