Skip to content

Commit

Permalink
Merge pull request ArchipelagoMW#14 from el-u/options_dict
Browse files Browse the repository at this point in the history
core: auto initialize a dataclass on the World class with the option results
  • Loading branch information
alwaysintreble authored Jan 9, 2023
2 parents 2581d57 + 82ff125 commit ad2f59e
Show file tree
Hide file tree
Showing 8 changed files with 126 additions and 106 deletions.
10 changes: 6 additions & 4 deletions BaseClasses.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@

import copy
import itertools
from argparse import Namespace
from enum import unique, IntEnum, IntFlag
import logging
import json
import functools
from collections import OrderedDict, Counter, deque
from typing import List, Dict, Optional, Set, Iterable, Union, Any, Tuple, TypedDict, Callable, NamedTuple
from typing import List, Dict, Optional, Set, Iterable, Union, Any, Tuple, TypedDict, Callable, NamedTuple, Type, \
get_type_hints
import typing # this can go away when Python 3.8 support is dropped
import secrets
import random
Expand Down Expand Up @@ -214,8 +214,10 @@ def set_options(self, args: Namespace) -> None:
world_type.option_definitions):
option_values = getattr(args, option_key, {})
setattr(self, option_key, option_values)
if player in option_values:
self.worlds[player].options[option_key] = option_values[player]
# TODO - remove this loop once all worlds use options dataclasses
options_dataclass: Type[Options.GameOptions] = self.worlds[player].options_dataclass
self.worlds[player].o = options_dataclass(**{option_key: getattr(args, option_key)[player]
for option_key in get_type_hints(options_dataclass)})

def set_item_links(self):
item_links = {}
Expand Down
2 changes: 1 addition & 1 deletion Fill.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations:
spot_to_fill: typing.Optional[Location] = None

# if minimal accessibility, only check whether location is reachable if game not beatable
if world.worlds[item_to_place.player].options["accessibility"] == Accessibility.option_minimal:
if world.worlds[item_to_place.player].o.accessibility == Accessibility.option_minimal:
perform_access_check = not world.has_beaten_game(maximum_exploration_state,
item_to_place.player) \
if single_player_placement else not has_beaten_game
Expand Down
42 changes: 26 additions & 16 deletions Options.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from __future__ import annotations
import abc
from copy import deepcopy
from dataclasses import dataclass
import math
import numbers
import typing
Expand Down Expand Up @@ -862,10 +863,13 @@ class ProgressionBalancing(SpecialRange):
}


common_options = {
"progression_balancing": ProgressionBalancing,
"accessibility": Accessibility
}
@dataclass
class CommonOptions:
progression_balancing: ProgressionBalancing
accessibility: Accessibility

common_options = typing.get_type_hints(CommonOptions)
# TODO - remove this dict once all worlds use options dataclasses


class ItemSet(OptionSet):
Expand Down Expand Up @@ -982,18 +986,24 @@ def verify(self, world, player_name: str, plando_options) -> None:
raise Exception(f"item_link {link['name']} has {intersection} items in both its local_items and non_local_items pool.")


per_game_common_options = {
**common_options, # can be overwritten per-game
"local_items": LocalItems,
"non_local_items": NonLocalItems,
"early_items": EarlyItems,
"start_inventory": StartInventory,
"start_hints": StartHints,
"start_location_hints": StartLocationHints,
"exclude_locations": ExcludeLocations,
"priority_locations": PriorityLocations,
"item_links": ItemLinks
}
@dataclass
class PerGameCommonOptions(CommonOptions):
local_items: LocalItems
non_local_items: NonLocalItems
early_items: EarlyItems
start_inventory: StartInventory
start_hints: StartHints
start_location_hints: StartLocationHints
exclude_locations: ExcludeLocations
priority_locations: PriorityLocations
item_links: ItemLinks

per_game_common_options = typing.get_type_hints(PerGameCommonOptions)
# TODO - remove this dict once all worlds use options dataclasses


GameOptions = typing.TypeVar("GameOptions", bound=PerGameCommonOptions)


if __name__ == "__main__":

Expand Down
53 changes: 28 additions & 25 deletions docs/world api.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,9 +85,10 @@ inside a World object.
### Player Options

Players provide customized settings for their World in the form of yamls.
Those are accessible through `self.options["<option_name>"]`. A dict
of valid options has to be provided in `self.option_definitions`. Options are automatically
added to the `World` object for easy access.
A `dataclass` of valid options definitions has to be provided in `self.options_dataclass`.
(It must be a subclass of `PerGameCommonOptions`.)
Option results are automatically added to the `World` object for easy access.
Those are accessible through `self.o.<option_name>`.

### World Options

Expand Down Expand Up @@ -209,11 +210,11 @@ See [pip documentation](https://pip.pypa.io/en/stable/cli/pip_install/#requireme
AP will only import the `__init__.py`. Depending on code size it makes sense to
use multiple files and use relative imports to access them.

e.g. `from .Options import mygame_options` from your `__init__.py` will load
`world/[world_name]/Options.py` and make its `mygame_options` accessible.
e.g. `from .Options import MyGameOptions` from your `__init__.py` will load
`world/[world_name]/Options.py` and make its `MyGameOptions` accessible.

When imported names pile up it may be easier to use `from . import Options`
and access the variable as `Options.mygame_options`.
and access the variable as `Options.MyGameOptions`.

Imports from directories outside your world should use absolute imports.
Correct use of relative / absolute imports is required for zipped worlds to
Expand Down Expand Up @@ -261,9 +262,9 @@ Each option has its own class, inherits from a base option type, has a docstring
to describe it and a `display_name` property for display on the website and in
spoiler logs.

The actual name as used in the yaml is defined in a `dict[str, Option]`, that is
assigned to the world under `self.option_definitions`. By convention, the string
that defines your option should be in `snake_case`.
The actual name as used in the yaml is defined via the field names of a `dataclass` that is
assigned to the world under `self.options_dataclass`. By convention, the strings
that define your option names should be in `snake_case`.

Common option types are `Toggle`, `DefaultOnToggle`, `Choice`, `Range`.
For more see `Options.py` in AP's base directory.
Expand Down Expand Up @@ -298,8 +299,8 @@ default = 0
```python
# Options.py

from Options import Toggle, Range, Choice, Option
import typing
from dataclasses import dataclass
from Options import Toggle, Range, Choice, PerGameCommonOptions

class Difficulty(Choice):
"""Sets overall game difficulty."""
Expand All @@ -322,25 +323,26 @@ class FixXYZGlitch(Toggle):
"""Fixes ABC when you do XYZ"""
display_name = "Fix XYZ Glitch"

# By convention we call the options dict variable `<world>_options`.
mygame_options: typing.Dict[str, type(Option)] = {
"difficulty": Difficulty,
"final_boss_hp": FinalBossHP,
"fix_xyz_glitch": FixXYZGlitch
}
# By convention, we call the options dataclass `<world>Options`.
@dataclass
class MyGameOptions(PerGameCommonOptions):
difficulty: Difficulty
final_boss_hp: FinalBossHP
fix_xyz_glitch: FixXYZGlitch
```
```python
# __init__.py

from worlds.AutoWorld import World
from .Options import mygame_options # import the options dict
from .Options import MyGameOptions # import the options dataclass

class MyGameWorld(World):
#...
option_definitions = mygame_options # assign the options dict to the world
options_dataclass = MyGameOptions # assign the options dataclass to the world
o: MyGameOptions # typing for option results
#...
```

### Local or Remote

A world with `remote_items` set to `True` gets all items items from the server
Expand All @@ -358,7 +360,7 @@ more natural. These games typically have been edited to 'bake in' the items.
```python
# world/mygame/__init__.py

from .Options import mygame_options # the options we defined earlier
from .Options import MyGameOptions # the options we defined earlier
from .Items import mygame_items # data used below to add items to the World
from .Locations import mygame_locations # same as above
from worlds.AutoWorld import World
Expand All @@ -374,7 +376,8 @@ class MyGameLocation(Location): # or from Locations import MyGameLocation
class MyGameWorld(World):
"""Insert description of the world/game here."""
game: str = "My Game" # name of the game/world
option_definitions = mygame_options # options the player can set
options_dataclass = MyGameOptions # options the player can set
o: MyGameOptions # typing for option results
topology_present: bool = True # show path to required location checks in spoiler
remote_items: bool = False # True if all items come from the server
remote_start_inventory: bool = False # True if start inventory comes from the server
Expand Down Expand Up @@ -456,7 +459,7 @@ In addition, the following methods can be implemented and attributes can be set
```python
def generate_early(self) -> None:
# read player settings to world instance
self.final_boss_hp = self.options["final_boss_hp"].value
self.final_boss_hp = self.o.final_boss_hp.value
```

#### create_item
Expand Down Expand Up @@ -676,9 +679,9 @@ def generate_output(self, output_directory: str):
in self.world.precollected_items[self.player]],
"final_boss_hp": self.final_boss_hp,
# store option name "easy", "normal" or "hard" for difficuly
"difficulty": self.options["difficulty"].current_key,
"difficulty": self.o.difficulty.current_key,
# store option value True or False for fixing a glitch
"fix_xyz_glitch": self.options["fix_xyz_glitch"].value
"fix_xyz_glitch": self.o.fix_xyz_glitch.value
}
# point to a ROM specified by the installation
src = Utils.get_options()["mygame_options"]["rom_file"]
Expand Down
8 changes: 5 additions & 3 deletions test/general/TestFill.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import itertools
from typing import List, Iterable
from typing import get_type_hints, List, Iterable
import unittest

import Options
Expand Down Expand Up @@ -28,7 +28,9 @@ def generate_multi_world(players: int = 1) -> MultiWorld:
for option_key in itertools.chain(Options.common_options, Options.per_game_common_options):
option_value = getattr(args, option_key, {})
setattr(multi_world, option_key, option_value)
multi_world.worlds[player_id].options[option_key] = option_value[player_id]
# TODO - remove this loop once all worlds use options dataclasses
world.o = world.options_dataclass(**{option_key: getattr(args, option_key)[player_id]
for option_key in get_type_hints(world.options_dataclass)})

multi_world.set_seed(0)

Expand Down Expand Up @@ -196,7 +198,7 @@ def test_minimal_fill(self):
items = player1.prog_items
locations = player1.locations

multi_world.worlds[player1.id].options["accessibility"] = Accessibility.from_any(Accessibility.option_minimal)
multi_world.worlds[player1.id].o.accessibility = Accessibility.from_any(Accessibility.option_minimal)
multi_world.completion_condition[player1.id] = lambda state: state.has(
items[1].name, player1.id)
set_rule(locations[1], lambda state: state.has(
Expand Down
9 changes: 5 additions & 4 deletions worlds/AutoWorld.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import pathlib
from typing import Dict, FrozenSet, Set, Tuple, List, Optional, TextIO, Any, Callable, Type, Union, TYPE_CHECKING

from Options import AssembleOptions, Option
from Options import AssembleOptions, GameOptions, PerGameCommonOptions
from BaseClasses import CollectionState

if TYPE_CHECKING:
Expand Down Expand Up @@ -130,8 +130,10 @@ class World(metaclass=AutoWorldRegister):
"""A World object encompasses a game's Items, Locations, Rules and additional data or functionality required.
A Game should have its own subclass of World in which it defines the required data structures."""

option_definitions: Dict[str, AssembleOptions] = {} # link your Options mapping
options: Dict[str, Option[Any]] # automatically populated option names to resulting option object
option_definitions: Dict[str, AssembleOptions] = {} # TODO - remove this once all worlds use options dataclasses
options_dataclass: Type[GameOptions] = PerGameCommonOptions # link your Options mapping
o: PerGameCommonOptions

game: str # name the game
topology_present: bool = False # indicate if world type has any meaningful layout/pathing

Expand Down Expand Up @@ -199,7 +201,6 @@ class World(metaclass=AutoWorldRegister):
def __init__(self, world: "MultiWorld", player: int):
self.world = world
self.player = player
self.options = {}

# overridable methods that get called by Main.py, sorted by execution order
# can also be implemented as a classmethod and called "stage_<original_name>",
Expand Down
54 changes: 27 additions & 27 deletions worlds/ror2/Options.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from typing import Dict
from Options import Option, DefaultOnToggle, Range, Choice
from dataclasses import dataclass
from Options import DefaultOnToggle, Range, Choice, PerGameCommonOptions


class TotalLocations(Range):
Expand Down Expand Up @@ -150,28 +150,28 @@ class ItemWeights(Choice):
option_scraps_only = 8


# define a dictionary for the weights of the generated item pool.
ror2_weights: Dict[str, type(Option)] = {
"green_scrap": GreenScrap,
"red_scrap": RedScrap,
"yellow_scrap": YellowScrap,
"white_scrap": WhiteScrap,
"common_item": CommonItem,
"uncommon_item": UncommonItem,
"legendary_item": LegendaryItem,
"boss_item": BossItem,
"lunar_item": LunarItem,
"equipment": Equipment
}

ror2_options: Dict[str, type(Option)] = {
"total_locations": TotalLocations,
"total_revivals": TotalRevivals,
"start_with_revive": StartWithRevive,
"final_stage_death": FinalStageDeath,
"item_pickup_step": ItemPickupStep,
"enable_lunar": AllowLunarItems,
"item_weights": ItemWeights,
"item_pool_presets": ItemPoolPresetToggle,
**ror2_weights
}
# define a class for the weights of the generated item pool.
@dataclass
class ROR2Weights:
green_scrap: GreenScrap
red_scrap: RedScrap
yellow_scrap: YellowScrap
white_scrap: WhiteScrap
common_item: CommonItem
uncommon_item: UncommonItem
legendary_item: LegendaryItem
boss_item: BossItem
lunar_item: LunarItem
equipment: Equipment


@dataclass
class ROR2Options(PerGameCommonOptions, ROR2Weights):
total_locations: TotalLocations
total_revivals: TotalRevivals
start_with_revive: StartWithRevive
final_stage_death: FinalStageDeath
item_pickup_step: ItemPickupStep
enable_lunar: AllowLunarItems
item_weights: ItemWeights
item_pool_presets: ItemPoolPresetToggle
Loading

0 comments on commit ad2f59e

Please sign in to comment.