From 016c1e9bb4e539e80df260d114285a48f7fd6e76 Mon Sep 17 00:00:00 2001 From: Aaron Wagener Date: Tue, 30 Jan 2024 14:42:33 -0600 Subject: [PATCH] Docs: world api general cleanup/overhaul (#2598) * Docs: world api general cleanup/overhaul * add pep-0287 to style doc * some cleanup, reorganization, and grammar improvements * reorder item and region creation * address review comments * fix indent * linter grammar Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> --------- Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> --- docs/options api.md | 31 +- docs/style.md | 7 +- docs/world api.md | 747 ++++++++++++++++++++------------------------ 3 files changed, 359 insertions(+), 426 deletions(-) diff --git a/docs/options api.md b/docs/options api.md index 48a3f763..bfab0096 100644 --- a/docs/options api.md +++ b/docs/options api.md @@ -27,14 +27,15 @@ Choice, and defining `alias_true = option_full`. - All options support `random` as a generic option. `random` chooses from any of the available values for that option, and is reserved by AP. You can set this as your default value, but you cannot define your own `option_random`. -As an example, suppose we want an option that lets the user start their game with a sword in their inventory. Let's -create our option class (with a docstring), give it a `display_name`, and add it to our game's options dataclass: +As an example, suppose we want an option that lets the user start their game with a sword in their inventory, an option +to let the player choose the difficulty, and an option to choose how much health the final boss has. Let's create our +option classes (with a docstring), give them a `display_name`, and add them to our game's options dataclass: ```python # options.py from dataclasses import dataclass -from Options import Toggle, PerGameCommonOptions +from Options import Toggle, Range, Choice, PerGameCommonOptions class StartingSword(Toggle): @@ -42,13 +43,33 @@ class StartingSword(Toggle): display_name = "Start With Sword" +class Difficulty(Choice): + """Sets overall game difficulty.""" + display_name = "Difficulty" + option_easy = 0 + option_normal = 1 + option_hard = 2 + alias_beginner = 0 # same as easy but allows the player to use beginner as an alternative for easy in the result in their options + alias_expert = 2 # same as hard + default = 1 # default to normal + + +class FinalBossHP(Range): + """Sets the HP of the final boss""" + display_name = "Final Boss HP" + range_start = 100 + range_end = 10000 + default = 2000 + + @dataclass class ExampleGameOptions(PerGameCommonOptions): starting_sword: StartingSword + difficulty: Difficulty + final_boss_health: FinalBossHP ``` -This will create a `Toggle` option, internally called `starting_sword`. To then submit this to the multiworld, we add it -to our world's `__init__.py`: +To then submit this to the multiworld, we add it to our world's `__init__.py`: ```python from worlds.AutoWorld import World diff --git a/docs/style.md b/docs/style.md index 4cc81114..fbf681f2 100644 --- a/docs/style.md +++ b/docs/style.md @@ -6,7 +6,6 @@ * 120 character per line for all source files. * Avoid white space errors like trailing spaces. - ## Python Code * We mostly follow [PEP8](https://peps.python.org/pep-0008/). Read below to see the differences. @@ -18,9 +17,10 @@ * Use type annotations where possible for function signatures and class members. * Use type annotations where appropriate for local variables (e.g. `var: List[int] = []`, or when the type is hard or impossible to deduce.) Clear annotations help developers look up and validate API calls. +* New classes, attributes, and methods in core code should have docstrings that follow + [reST style](https://peps.python.org/pep-0287/). * Worlds that do not follow PEP8 should still have a consistent style across its files to make reading easier. - ## Markdown * We almost follow [Google's styleguide](https://google.github.io/styleguide/docguide/style.html). @@ -30,20 +30,17 @@ * One space between bullet/number and text. * No lazy numbering. - ## HTML * Indent with 2 spaces for new code. * kebab-case for ids and classes. - ## CSS * Indent with 2 spaces for new code. * `{` on the same line as the selector. * No space between selector and `{`. - ## JS * Indent with 2 spaces. diff --git a/docs/world api.md b/docs/world api.md index 0ab06da6..72a67bca 100644 --- a/docs/world api.md +++ b/docs/world api.md @@ -1,95 +1,95 @@ # Archipelago API -This document tries to explain some internals required to implement a game for -Archipelago's generation and server. Once a seed is generated, a client or mod is -required to send and receive items between the game and server. +This document tries to explain some aspects of the Archipelago World API used when implementing the generation logic of +a game. -Client implementation is out of scope of this document. Please refer to an -existing game that provides a similar API to yours. -Refer to the following documents as well: -- [network protocol.md](https://github.com/ArchipelagoMW/Archipelago/blob/main/docs/network%20protocol.md) -- [adding games.md](https://github.com/ArchipelagoMW/Archipelago/blob/main/docs/adding%20games.md) +Client implementation is out of scope of this document. Please refer to an existing game that provides a similar API to +yours, and the following documents: + +* [network protocol.md](https://github.com/ArchipelagoMW/Archipelago/blob/main/docs/network%20protocol.md) +* [adding games.md](https://github.com/ArchipelagoMW/Archipelago/blob/main/docs/adding%20games.md) Archipelago will be abbreviated as "AP" from now on. - ## Language AP worlds are written in python3. -Clients that connect to the server to sync items can be in any language that -allows using WebSockets. - +Clients that connect to the server to sync items can be in any language that allows using WebSockets. ## Coding style -AP follows [style.md](https://github.com/ArchipelagoMW/Archipelago/blob/main/docs/style.md). -When in doubt use an IDE with coding style linter, for example PyCharm Community Edition. - +AP follows a [style guide](https://github.com/ArchipelagoMW/Archipelago/blob/main/docs/style.md). +When in doubt, use an IDE with a code-style linter, for example PyCharm Community Edition. ## Docstrings -Docstrings are strings attached to an object in Python that describe what the -object is supposed to be. Certain docstrings will be picked up and used by AP. -They are assigned by writing a string without any assignment right below a -definition. The string must be a triple-quoted string. +Docstrings are strings attached to an object in Python that describe what the object is supposed to be. Certain +docstrings will be picked up and used by AP. They are assigned by writing a string without any assignment right below a +definition. The string must be a triple-quoted string, and should +follow [reST style](https://peps.python.org/pep-0287/). + Example: + ```python from worlds.AutoWorld import World -class MyGameWorld(World): - """This is the description of My Game that will be displayed on the AP - website.""" -``` +class MyGameWorld(World): + """This is the description of My Game that will be displayed on the AP website.""" +``` + ## Definitions -This section will cover various classes and objects you can use for your world. -While some of the attributes and methods are mentioned here, not all of them are, -but you can find them in `BaseClasses.py`. +This section covers various classes and objects you can use for your world. While some of the attributes and methods +are mentioned here, not all of them are, but you can find them in +[`BaseClasses.py`](https://github.com/ArchipelagoMW/Archipelago/blob/main/BaseClasses.py). ### World Class -A `World` class is the class with all the specifics of a certain game to be -included. It will be instantiated for each player that rolls a seed for that -game. +A `World` is the class with all the specifics of a certain game that is to be included. A new instance will be created +for each player of the game for any given generated multiworld. ### WebWorld Class -A `WebWorld` class contains specific attributes and methods that can be modified -for your world specifically on the webhost: +A `WebWorld` class contains specific attributes and methods that can be modified for your world specifically on the +webhost: -`settings_page`, which can be changed to a link instead of an AP generated settings page. +* `options_page` can be changed to a link instead of an AP-generated options page. -`theme` to be used for your game specific AP pages. Available themes: +* `theme` to be used for your game-specific AP pages. Available themes: -| dirt | grass (default) | grassFlowers | ice | jungle | ocean | partyTime | stone | -|---|---|---|---|---|---|---|---| -| | | | | | | | | + | dirt | grass (default) | grassFlowers | ice | jungle | ocean | partyTime | stone | + |--------------------------------------------|---------------------------------------------|----------------------------------------------------|-------------------------------------------|----------------------------------------------|---------------------------------------------|-------------------------------------------------|---------------------------------------------| + | | | | | | | | | -`bug_report_page` (optional) can be a link to a bug reporting page, most likely a GitHub issue page, that will be placed by the site to help direct users to report bugs. +* `bug_report_page` (optional) can be a link to a bug reporting page, most likely a GitHub issue page, that will be + placed by the site to help users report bugs. -`tutorials` list of `Tutorial` classes where each class represents a guide to be generated on the webhost. +* `tutorials` list of `Tutorial` classes where each class represents a guide to be generated on the webhost. -`game_info_languages` (optional) List of strings for defining the existing gameinfo pages your game supports. The documents must be -prefixed with the same string as defined here. Default already has 'en'. +* `game_info_languages` (optional) list of strings for defining the existing game info pages your game supports. The + documents must be prefixed with the same string as defined here. Default already has 'en'. -`options_presets` (optional) A `Dict[str, Dict[str, Any]]` where the keys are the names of the presets and the values -are the options to be set for that preset. The options are defined as a `Dict[str, Any]` where the keys are the names of -the options and the values are the values to be set for that option. These presets will be available for users to select from on the game's options page. +* `options_presets` (optional) `Dict[str, Dict[str, Any]]` where the keys are the names of the presets and the values + are the options to be set for that preset. The options are defined as a `Dict[str, Any]` where the keys are the names + of the options and the values are the values to be set for that option. These presets will be available for users to + select from on the game's options page. Note: The values must be a non-aliased value for the option type and can only include the following option types: - - If you have a `Range`/`NamedRange` option, the value should be an `int` between the `range_start` and `range_end` - values. - - If you have a `NamedRange` option, the value can alternatively be a `str` that is one of the +* If you have a `Range`/`NamedRange` option, the value should be an `int` between the `range_start` and `range_end` + values. + * If you have a `NamedRange` option, the value can alternatively be a `str` that is one of the `special_range_names` keys. - - If you have a `Choice` option, the value should be a `str` that is one of the `option_` values. - - If you have a `Toggle`/`DefaultOnToggle` option, the value should be a `bool`. - - `random` is also a valid value for any of these option types. +* If you have a `Choice` option, the value should be a `str` that is one of the `option_` values. +* If you have a `Toggle`/`DefaultOnToggle` option, the value should be a `bool`. +* `random` is also a valid value for any of these option types. -`OptionDict`, `OptionList`, `OptionSet`, `FreeText`, or custom `Option`-derived classes are not supported for presets on the webhost at this time. +`OptionDict`, `OptionList`, `OptionSet`, `FreeText`, or custom `Option`-derived classes are not supported for presets on +the webhost at this time. Here is an example of a defined preset: + ```python # presets.py options_presets = { @@ -114,6 +114,7 @@ options_presets = { } } + # __init__.py class RLWeb(WebWorld): options_presets = options_presets @@ -122,47 +123,55 @@ class RLWeb(WebWorld): ### MultiWorld Object -The `MultiWorld` object references the whole multiworld (all items and locations -for all players) and is accessible through `self.multiworld` inside a `World` object. +The `MultiWorld` object references the whole multiworld (all items and locations for all players) and is accessible +through `self.multiworld` from your `World` object. ### Player -The player is just an integer in AP and is accessible through `self.player` -inside a `World` object. +The player is just an `int` in AP and is accessible through `self.player` from your `World` object. ### Player Options -Players provide customized settings for their World in the form of yamls. -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.options.`, and you can get a dictionary of the option values via -`self.options.as_dict()`, passing the desired options as strings. +Options are provided by the user as part of the generation process, intended to affect how their randomizer experience +should play out. These can control aspects such as what locations should be shuffled, what items are in the itempool, +etc. Players provide the customized options for their World in the form of yamls. + +By convention, options are defined in `options.py` and will be used when parsing the players' yaml files. Each option +has its own class, which inherits from a base option type, a docstring to describe it, and a `display_name` property +shown on the website and in spoiler logs. + +The available options are defined by creating a `dataclass`, which must be a subclass of `PerGameCommonOptions`. It has +defined fields for the option names used in the player yamls and used for options access, with their types matching the +appropriate Option class. By convention, the strings that define your option names should be in `snake_case`. The +`dataclass` is then assigned to your `World` by defining its `options_dataclass`. Option results are then automatically +added to the `World` object for easy access, between `World` creation and `generate_early`. These are accessible through +`self.options.`, and you can get a dictionary with option values +via `self.options.as_dict()`, +passing the desired option names as strings. + +Common option types are `Toggle`, `DefaultOnToggle`, `Choice`, and `Range`. +For more information, see the [options api doc](options%20api.md). ### World Settings -Any AP installation can provide settings for a world, for example a ROM file, accessible through -`self.settings.` or `cls.settings.` (new API) -or `Utils.get_options()["_options"][""]` (deprecated). +Settings are set by the user outside the generation process. They can be used for those settings that may affect +generation or client behavior, but should remain static between generations, such as the path to a ROM file. +These settings are accessible through `self.settings.` or `cls.settings.`. -Users can set those in their `host.yaml` file. Some settings may automatically open a file browser if a file is missing. +Users can set these in their `host.yaml` file. Some settings may automatically open a file browser if a file is missing. -Refer to [settings api.md](https://github.com/ArchipelagoMW/Archipelago/blob/main/docs/settings%20api.md) -for details. +Refer to [settings api.md](https://github.com/ArchipelagoMW/Archipelago/blob/main/docs/settings%20api.md) for details. ### Locations -Locations are places where items can be located in your game. This may be chests -or boss drops for RPG-like games but could also be progress in a research tree. +Locations are places where items can be located in your game. This may be chests or boss drops for RPG-like games, but +could also be progress in a research tree, or even something more abstract like a level up. -Each location has a `name` and an `id` (a.k.a. "code" or "address"), is placed -in a Region, has access rules and a classification. -The name needs to be unique in each game and must not be numeric (has to -contain least 1 letter or symbol). The ID needs to be unique across all games -and is best in the same range as the item IDs. -World-specific IDs are 1 to 253-1, IDs ≤ 0 are global and reserved. +Each location has a `name` and an `address` (hereafter referred to as an `id`), is placed in a Region, has access rules, +and has a classification. The name needs to be unique within each game and must not be numeric (must contain least 1 +letter or symbol). The ID needs to be unique across all games, and is best kept in the same range as the item IDs. -Special locations with ID `None` can hold events. +World-specific IDs must be in the range 1 to 253-1; IDs ≤ 0 are global and reserved. Classification is one of `LocationProgressType.DEFAULT`, `PRIORITY` or `EXCLUDED`. The Fill algorithm will force progression items to be placed at priority locations, giving a higher chance of them being @@ -170,22 +179,21 @@ required, and will prevent progression and useful items from being placed at exc #### Documenting Locations -Worlds can optionally provide a `location_descriptions` map which contains -human-friendly descriptions of locations or location groups. These descriptions -will show up in location-selection options in the Weighted Options page. Extra +Worlds can optionally provide a `location_descriptions` map which contains human-friendly descriptions of locations and +location groups. These descriptions will show up in location-selection options on the Weighted Options page. Extra indentation and single newlines will be collapsed into spaces. ```python -# Locations.py +# locations.py location_descriptions = { "Red Potion #6": "In a secret destructible block under the second stairway", - "L2 Spaceship": """ - The group of all items in the spaceship in Level 2. + "L2 Spaceship": + """ + The group of all items in the spaceship in Level 2. - This doesn't include the item on the spaceship door, since it can be - accessed without the Spaeship Key. - """ + This doesn't include the item on the spaceship door, since it can be accessed without the Spaceship Key. + """ } ``` @@ -193,7 +201,7 @@ location_descriptions = { # __init__.py from worlds.AutoWorld import World -from .Locations import location_descriptions +from .locations import location_descriptions class MyGameWorld(World): @@ -202,47 +210,45 @@ class MyGameWorld(World): ### Items -Items are all things that can "drop" for your game. This may be RPG items like -weapons, could as well be technologies you normally research in a research tree. +Items are all things that can "drop" for your game. This may be RPG items like weapons, or technologies you normally +research in a research tree. -Each item has a `name`, an `id` (can be known as "code"), and a classification. -The most important classification is `progression` (formerly advancement). -Progression items are items which a player may require to progress in -their world. Progression items will be assigned to locations with higher -priority and moved around to meet defined rules and accomplish progression -balancing. +Each item has a `name`, a `code` (hereafter referred to as `id`), and a classification. +The most important classification is `progression`. Progression items are items which a player *may* require to progress +in their world. If an item can possibly be considered for logic (it's referenced in a location's rules) it *must* be +progression. Progression items will be assigned to locations with higher priority, and moved around to meet defined rules +and satisfy progression balancing. -The name needs to be unique in each game, meaning a duplicate item has the -same ID. Name must not be numeric (has to contain at least 1 letter or symbol). +The name needs to be unique within each game, meaning if you need to create multiple items with the same name, they +will all have the same ID. Name must not be numeric (must contain at least 1 letter or symbol). -Special items with ID `None` can mark events (read below). +Other classifications include: -Other classifications include * `filler`: a regular item or trash item -* `useful`: generally quite useful, but not required for anything logical +* `useful`: generally quite useful, but not required for anything logical. Cannot be placed on excluded locations * `trap`: negative impact on the player * `skip_balancing`: denotes that an item should not be moved to an earlier sphere for the purpose of balancing (to be combined with `progression`; see below) * `progression_skip_balancing`: the combination of `progression` and `skip_balancing`, i.e., a progression item that - will not be moved around by progression balancing; used, e.g., for currency or tokens + will not be moved around by progression balancing; used, e.g., for currency or tokens, to not flood early spheres #### Documenting Items -Worlds can optionally provide an `item_descriptions` map which contains -human-friendly descriptions of items or item groups. These descriptions will -show up in item-selection options in the Weighted Options page. Extra -indentation and single newlines will be collapsed into spaces. +Worlds can optionally provide an `item_descriptions` map which contains human-friendly descriptions of items and item +groups. These descriptions will show up in item-selection options on the Weighted Options page. Extra indentation and +single newlines will be collapsed into spaces. ```python -# Items.py +# items.py item_descriptions = { "Red Potion": "A standard health potion", - "Spaceship Key": """ - The key to the spaceship in Level 2. + "Spaceship Key": + """ + The key to the spaceship in Level 2. - This is necessary to get to the Star Realm. - """ + This is necessary to get to the Star Realm. + """ } ``` @@ -250,7 +256,7 @@ item_descriptions = { # __init__.py from worlds.AutoWorld import World -from .Items import item_descriptions +from .items import item_descriptions class MyGameWorld(World): @@ -259,216 +265,129 @@ class MyGameWorld(World): ### Events -Events will mark some progress. You define an event location, an -event item, strap some rules to the location (i.e. hold certain -items) and manually place the event item at the event location. +An Event is a special combination of a Location and an Item, with both having an `id` of `None`. These can be used to +track certain logic interactions, with the Event Item being required for access in other locations or regions, but not +being "real". Since the item and location have no ID, they get dropped at the end of generation and so the server is +never made aware of them and these locations can never be checked, nor can the items be received during play. +They may also be used for making the spoiler log look nicer, i.e. by having a `"Victory"` Event Item, that +is required to finish the game. This makes it very clear when the player finishes, rather than only seeing their last +relevant Item. Events function just like any other Location, and can still have their own access rules, etc. +By convention, the Event "pair" of Location and Item typically have the same name, though this is not a requirement. +They must not exist in the `name_to_id` lookups, as they have no ID. -Events can be used to either simplify the logic or to get better spoiler logs. -Events will show up in the spoiler playthrough but they do not represent actual -items or locations within the game. +The most common way to create an Event pair is to create and place the Item on the Location as soon as it's created: -There is one special case for events: Victory. To get the win condition to show -up in the spoiler log, you create an event item and place it at an event -location with the `access_rules` for game completion. Once that's done, the -world's win condition can be as simple as checking for that item. +```python +from worlds.AutoWorld import World +from BaseClasses import ItemClassification +from .subclasses import MyGameLocation, MyGameItem -By convention the victory event is called `"Victory"`. It can be placed at one -or more event locations based on player options. + +class MyGameWorld(World): + victory_loc = MyGameLocation(self.player, "Victory", None) + victory_loc.place_locked_item(MyGameItem("Victory", ItemClassification.progression, None, self.player)) +``` ### Regions -Regions are logical groups of locations that share some common access rules. If -location logic is written from scratch, using regions greatly simplifies the -definition and allows to somewhat easily implement things like entrance -randomizer in logic. +Regions are logical containers that typically hold locations that share some common access rules. If location logic is +written from scratch, using regions greatly simplifies the requirements and can help with implementing things +like entrance randomization in logic. -Regions have a list called `exits`, which are `Entrance` objects representing -transitions to other regions. +Regions have a list called `exits`, containing `Entrance` objects representing transitions to other regions. -There has to be one special region "Menu" from which the logic unfolds. AP -assumes that a player will always be able to return to the "Menu" region by -resetting the game ("Save and quit"). +There must be one special region, "Menu", from which the logic unfolds. AP assumes that a player will always be able to +return to the "Menu" region by resetting the game ("Save and quit"). ### Entrances -An `Entrance` connects to a region, is assigned to region's exits and has rules -to define if it and thus the connected region is accessible. -They can be static (regular logic) or be defined/connected during generation -(entrance randomizer). +An `Entrance` has a `parent_region` and `connected_region`, where it is in the `exits` of its parent, and the +`entrances` of its connected region. The `Entrance` then has rules assigned to it to determine if it can be passed +through, making the connected region accessible. They can be static (regular logic) or be defined/connected during +generation (entrance randomization). ### Access Rules -An access rule is a function that returns `True` or `False` for a `Location` or -`Entrance` based on the current `state` (items that can be collected). +An access rule is a function that returns `True` or `False` for a `Location` or `Entrance` based on the current `state` +(items that have been collected). ### Item Rules -An item rule is a function that returns `True` or `False` for a `Location` based -on a single item. It can be used to reject placement of an item there. - +An item rule is a function that returns `True` or `False` for a `Location` based on a single item. It can be used to +reject the placement of an item there. ## Implementation ### Your World -All code for your world implementation should be placed in a python package in -the `/worlds` directory. The starting point for the package is `__init__.py`. -Conventionally, your world class is placed in that file. +All code for your world implementation should be placed in a python package in the `/worlds` directory. The starting +point for the package is `__init__.py`. Conventionally, your `World` class is placed in that file. -World classes must inherit from the `World` class in `/worlds/AutoWorld.py`, -which can be imported as `from worlds.AutoWorld import World` from your package. +World classes must inherit from the `World` class in `/worlds/AutoWorld.py`, which can be imported as +`from worlds.AutoWorld import World` from your package. AP will pick up your world automatically due to the `AutoWorld` implementation. ### Requirements -If your world needs specific python packages, they can be listed in -`worlds//requirements.txt`. ModuleUpdate.py will automatically -pick up and install them. +If your world needs specific python packages, they can be listed in `worlds//requirements.txt`. +ModuleUpdate.py will automatically pick up and install them. See [pip documentation](https://pip.pypa.io/en/stable/cli/pip_install/#requirements-file-format). ### Relative Imports -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. +AP will only import the `__init__.py`. Depending on code size, it may make sense to use multiple files and use relative +imports to access them. -e.g. `from .options import MyGameOptions` from your `__init__.py` will load -`world/[world_name]/options.py` and make its `MyGameOptions` 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.MyGameOptions`. +When imported names pile up, it may be easier to use `from . import 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 -function, see [apworld specification.md](apworld%20specification.md). +Imports from directories outside your world should use absolute imports. Correct use of relative / absolute imports is +required for zipped worlds to function, see [apworld specification.md](apworld%20specification.md). ### Your Item Type -Each world uses its own subclass of `BaseClasses.Item`. The constructor can be -overridden to attach additional data to it, e.g. "price in shop". -Since the constructor is only ever called from your code, you can add whatever -arguments you like to the constructor. +Each world uses its own subclass of `BaseClasses.Item`. The constructor can be overridden to attach additional data to +it, e.g. "price in shop". Since the constructor is only ever called from your code, you can add whatever arguments you +like to the constructor. + +In its simplest form, we only set the game name and use the default constructor: -In its simplest form we only set the game name and use the default constructor ```python from BaseClasses import Item + class MyGameItem(Item): game: str = "My Game" ``` -By convention this class definition will either be placed in your `__init__.py` -or your `items.py`. For a more elaborate example see `worlds/oot/Items.py`. -### Your location type +By convention, this class definition will either be placed in your `__init__.py` or your `items.py`. For a more +elaborate example see +[`worlds/oot/Items.py`](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/oot/Items.py). + +### Your Location Type + +The same thing we did for items above, we will now do for locations: -The same we have done for items above, we will do for locations ```python from BaseClasses import Location + class MyGameLocation(Location): game: str = "My Game" # override constructor to automatically mark event locations as such - def __init__(self, player: int, name = "", code = None, parent = None) -> None: + def __init__(self, player: int, name="", code=None, parent=None) -> None: super(MyGameLocation, self).__init__(player, name, code, parent) self.event = code is None ``` + in your `__init__.py` or your `locations.py`. -### Options - -By convention options are defined in `options.py` and will be used when parsing -the players' yaml files. - -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 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. - -#### Toggle, DefaultOnToggle - -These don't need any additional properties defined. After parsing the option, -its `value` will either be True or False. - -#### Range - -Define properties `range_start`, `range_end` and `default`. Ranges will be -displayed as sliders on the website and can be set to random in the yaml. - -#### Choice - -Choices are like toggles, but have more options than just True and False. -Define a property `option_ = ` per selectable value and -`default = ` to set the default selection. Aliases can be set by -defining a property `alias_ = `. - -```python -option_off = 0 -option_on = 1 -option_some = 2 -alias_disabled = 0 -alias_enabled = 1 -default = 0 -``` - -#### Sample -```python -# options.py - -from dataclasses import dataclass -from Options import Toggle, Range, Choice, PerGameCommonOptions - -class Difficulty(Choice): - """Sets overall game difficulty.""" - display_name = "Difficulty" - option_easy = 0 - option_normal = 1 - option_hard = 2 - alias_beginner = 0 # same as easy - alias_expert = 2 # same as hard - default = 1 # default to normal - -class FinalBossHP(Range): - """Sets the HP of the final boss""" - display_name = "Final Boss HP" - range_start = 100 - range_end = 10000 - default = 2000 - -class FixXYZGlitch(Toggle): - """Fixes ABC when you do XYZ""" - display_name = "Fix XYZ Glitch" - -# By convention, we call the options dataclass `Options`. -# It has to be derived from 'PerGameCommonOptions'. -@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 MyGameOptions # import the options dataclass - - -class MyGameWorld(World): - # ... - options_dataclass = MyGameOptions # assign the options dataclass to the world - options: MyGameOptions # typing for option results - # ... -``` - ### A World Class Skeleton ```python @@ -483,7 +402,6 @@ from worlds.AutoWorld import World from BaseClasses import Region, Location, Entrance, Item, RegionType, ItemClassification - class MyGameItem(Item): # or from Items import MyGameItem game = "My Game" # name of the game/world this item is from @@ -492,7 +410,6 @@ class MyGameLocation(Location): # or from Locations import MyGameLocation game = "My Game" # name of the game/world this location is in - class MyGameSettings(settings.Group): class RomFile(settings.SNESRomPath): """Insert help text for host.yaml here.""" @@ -511,7 +428,7 @@ class MyGameWorld(World): # ID of first item and location, could be hard-coded but code may be easier # to read with this as a property. base_id = 1234 - # Instead of dynamic numbering, IDs could be part of data. + # instead of dynamic numbering, IDs could be part of data # The following two dicts are required for the generation to know which # items exist. They could be generated from json or something else. They can @@ -530,74 +447,106 @@ class MyGameWorld(World): ### Generation -The world has to provide the following things for generation +The world has to provide the following things for generation: -* the properties mentioned above +* the properties mentioned above * additions to the item pool * additions to the regions list: at least one called "Menu" * locations placed inside those regions * a `def create_item(self, item: str) -> MyGameItem` to create any item on demand -* applying `self.multiworld.push_precollected` for world defined start inventory -* `required_client_version: Tuple[int, int, int]` - Optional client version as tuple of 3 ints to make sure the client is compatible to - this world (e.g. implements all required features) when connecting. +* applying `self.multiworld.push_precollected` for world-defined start inventory -In addition, the following methods can be implemented and are called in this order during generation +In addition, the following methods can be implemented and are called in this order during generation: -* `stage_assert_generate(cls, multiworld)` is a class method called at the start of - generation to check the existence of prerequisite files, usually a ROM for +* `stage_assert_generate(cls, multiworld: MultiWorld)` + a class method called at the start of generation to check for the existence of prerequisite files, usually a ROM for games which require one. * `generate_early(self)` - called per player before any items or locations are created. You can set properties on your world here. Already has - access to player options and RNG. This is the earliest step where the world should start setting up for the current - multiworld as any steps before this, the multiworld itself is still getting set up + called per player before any items or locations are created. You can set properties on your + world here. Already has access to player options and RNG. This is the earliest step where the world should start + setting up for the current multiworld, as the multiworld itself is still setting up before this point. * `create_regions(self)` - called to place player's regions and their locations into the MultiWorld's regions list. If it's - hard to separate, this can be done during `generate_early` or `create_items` as well. + called to place player's regions and their locations into the MultiWorld's regions list. + If it's hard to separate, this can be done during `generate_early` or `create_items` as well. * `create_items(self)` - called to place player's items into the MultiWorld's itempool. After this step all regions and items have to be in - the MultiWorld's regions and itempool, and these lists should not be modified afterwards. + called to place player's items into the MultiWorld's itempool. After this step all regions + and items have to be in the MultiWorld's regions and itempool, and these lists should not be modified afterward. * `set_rules(self)` - called to set access and item rules on locations and entrances. - Locations have to be defined before this, or rule application can miss them. + called to set access and item rules on locations and entrances. * `generate_basic(self)` - called after the previous steps. Some placement and player specific - randomizations can be done here. -* `pre_fill(self)`, `fill_hook(self)` and `post_fill(self)` are called to modify item placement - before, during and after the regular fill process, before `generate_output`. - If items need to be placed during pre_fill, these items can be determined - and created using `get_prefill_items` -* `generate_output(self, output_directory: str)` that creates the output - files if there is output to be generated. When this is - called, `self.multiworld.get_locations(self.player)` has all locations for the player, with - attribute `item` pointing to the item. - `location.item.player` can be used to see if it's a local item. + player-specific randomization that does not affect logic can be done here. +* `pre_fill(self)`, `fill_hook(self)` and `post_fill(self)` + called to modify item placement before, during, and after the regular fill process; all finishing before + `generate_output`. Any items that need to be placed during `pre_fill` should not exist in the itempool, and if there + are any items that need to be filled this way, but need to be in state while you fill other items, they can be + returned from `get_prefill_items`. +* `generate_output(self, output_directory: str)` + creates the output files if there is output to be generated. When this is called, + `self.multiworld.get_locations(self.player)` has all locations for the player, with attribute `item` pointing to the + item. `location.item.player` can be used to see if it's a local item. * `fill_slot_data(self)` and `modify_multidata(self, multidata: Dict[str, Any])` can be used to modify the data that will be used by the server to host the MultiWorld. +All instance methods can, optionally, have a class method defined which will be called after all instance methods are +finished running, by defining a method with `stage_` in front of the method name. These class methods will have the +args `(cls, multiworld: MultiWorld)`, followed by any other args that the relevant instance method has. #### generate_early ```python def generate_early(self) -> None: - # read player settings to world instance + # read player options to world instance self.final_boss_hp = self.options.final_boss_hp.value ``` +#### create_regions + +```python +def create_regions(self) -> None: + # Add regions to the multiworld. "Menu" is the required starting point. + # Arguments to Region() are name, player, multiworld, and optionally hint_text + menu_region = Region("Menu", self.player, self.multiworld) + self.multiworld.regions.append(menu_region) # or use += [menu_region...] + + main_region = Region("Main Area", self.player, self.multiworld) + # add main area's locations to main area (all but final boss) + main_region.add_locations(main_region_locations, MyGameLocation) + # or + # main_region.locations = \ + # [MyGameLocation(self.player, location_name, self.location_name_to_id[location_name], main_region] + self.multiworld.regions.append(main_region) + + boss_region = Region("Boss Room", self.player, self.multiworld) + # add event to Boss Room + boss_region.locations.append(MyGameLocation(self.player, "Final Boss", None, boss_region)) + + # if entrances are not randomized, they should be connected here, otherwise they can also be connected at a later stage + # create Entrances and connect the Regions + menu_region.connect(main_region) # connects the "Menu" and "Main Area", can also pass a rule + # or + main_region.add_exits({"Boss Room": "Boss Door"}, {"Boss Room": lambda state: state.has("Sword", self.player)}) + # connects the "Main Area" and "Boss Room" regions, and places a rule requiring the "Sword" item to traverse + + # if setting location access rules from data is easier here, set_rules can possibly be omitted +``` + #### create_item ```python -# we need a way to know if an item provides progress in the game ("key item") -# this can be part of the items definition, or depend on recipe randomization +# we need a way to know if an item provides progress in the game ("key item") this can be part of the items definition, +# or depend on recipe randomization from .items import is_progression # this is just a dummy + def create_item(self, item: str) -> MyGameItem: - # This is called when AP wants to create an item by name (for plando) or - # when you call it from your own code. - classification = ItemClassification.progression if is_progression(item) else \ - ItemClassification.filler - return MyGameItem(item, classification, self.item_name_to_id[item], - self.player) + # this is called when AP wants to create an item by name (for plando) or when you call it from your own code + classification = ItemClassification.progression if is_progression(item) else + ItemClassification.filler + + +return MyGameItem(item, classification, self.item_name_to_id[item], + self.player) + def create_event(self, event: str) -> MyGameItem: # while we are at it, we can also add a helper to create events @@ -610,8 +559,7 @@ def create_event(self, event: str) -> MyGameItem: def create_items(self) -> None: # Add items to the Multiworld. # If there are two of the same item, the item has to be twice in the pool. - # Which items are added to the pool may depend on player settings, - # e.g. custom win condition like triforce hunt. + # Which items are added to the pool may depend on player options, e.g. custom win condition like triforce hunt. # Having an item in the start inventory won't remove it from the pool. # If an item can't have duplicates it has to be excluded manually. @@ -627,67 +575,10 @@ def create_items(self) -> None: # itempool and number of locations should match up. # If this is not the case we want to fill the itempool with junk. - junk = 0 # calculate this based on player settings + junk = 0 # calculate this based on player options self.multiworld.itempool += [self.create_item("nothing") for _ in range(junk)] ``` -#### create_regions - -```python -def create_regions(self) -> None: - # Add regions to the multiworld. "Menu" is the required starting point. - # Arguments to Region() are name, player, world, and optionally hint_text - menu_region = Region("Menu", self.player, self.multiworld) - self.multiworld.regions.append(menu_region) # or use += [menu_region...] - - main_region = Region("Main Area", self.player, self.multiworld) - # Add main area's locations to main area (all but final boss) - main_region.add_locations(main_region_locations, MyGameLocation) - # or - # main_region.locations = \ - # [MyGameLocation(self.player, location_name, self.location_name_to_id[location_name], main_region] - self.multiworld.regions.append(main_region) - - boss_region = Region("Boss Room", self.player, self.multiworld) - # Add event to Boss Room - boss_region.locations.append(MyGameLocation(self.player, "Final Boss", None, boss_region)) - - # If entrances are not randomized, they should be connected here, - # otherwise they can also be connected at a later stage. - # Create Entrances and connect the Regions - menu_region.connect(main_region) # connects the "Menu" and "Main Area", can also pass a rule - # or - main_region.add_exits({"Boss Room": "Boss Door"}, {"Boss Room": lambda state: state.has("Sword", self.player)}) - # Connects the "Main Area" and "Boss Room" regions, and places a rule requiring the "Sword" item to traverse - - # If setting location access rules from data is easier here, set_rules can - # possibly omitted. -``` - -#### generate_basic - -```python -def generate_basic(self) -> None: - # place "Victory" at "Final Boss" and set collection as win condition - self.multiworld.get_location("Final Boss", self.player) - .place_locked_item(self.create_event("Victory")) - self.multiworld.completion_condition[self.player] = - lambda state: state.has("Victory", self.player) - - # place item Herb into location Chest1 for some reason - item = self.create_item("Herb") - self.multiworld.get_location("Chest1", self.player).place_locked_item(item) - # in most cases it's better to do this at the same time the itempool is - # filled to avoid accidental duplicates: - # manually placed and still in the itempool - - # for debugging purposes, you may want to visualize the layout of your world. Uncomment the following code to - # write a PlantUML diagram to the file "my_world.puml" that can help you see whether your regions and locations - # are connected and placed as desired - # from Utils import visualize_regions - # visualize_regions(self.multiworld.get_region("Menu", self.player), "my_world.puml") -``` - ### Setting Rules ```python @@ -703,6 +594,7 @@ def set_rules(self) -> None: # set a simple rule for an region set_rule(self.multiworld.get_entrance("Boss Door", self.player), lambda state: state.has("Boss Key", self.player)) + # location.access_rule = ... is likely to be a bit faster # combine rules to require two items add_rule(self.multiworld.get_location("Chest2", self.player), lambda state: state.has("Sword", self.player)) @@ -730,59 +622,80 @@ def set_rules(self) -> None: # get_item_type needs to take player/world into account # if MyGameItem has a type property, a more direct implementation would be add_item_rule(self.multiworld.get_location("Chest5", self.player), - lambda item: item.player != self.player or\ + lambda item: item.player != self.player or item.my_type == "weapon") # location.item_rule = ... is likely to be a bit faster + + # place "Victory" at "Final Boss" and set collection as win condition + self.multiworld.get_location("Final Boss", self.player).place_locked_item(self.create_event("Victory")) + + self.multiworld.completion_condition[self.player] = lambda state: state.has("Victory", self.player) + +# for debugging purposes, you may want to visualize the layout of your world. Uncomment the following code to +# write a PlantUML diagram to the file "my_world.puml" that can help you see whether your regions and locations +# are connected and placed as desired +# from Utils import visualize_regions +# visualize_regions(self.multiworld.get_region("Menu", self.player), "my_world.puml") ``` -### Logic Mixin +### Custom Logic Rules -While lambdas and events could do pretty much anything, by convention we -implement more complex logic in logic mixins, even if there is no need to add -properties to the `BaseClasses.CollectionState` state object. - -When importing a file that defines a class that inherits from -`worlds.AutoWorld.LogicMixin` the state object's class is automatically extended by -the mixin's members. These members should be prefixed with underscore following -the name of the implementing world. This is due to sharing a namespace with all -other logic mixins. - -Typical uses are defining methods that are used instead of `state.has` -in lambdas, e.g.`state.mygame_has(custom, player)` or recurring checks -like `state.mygame_can_do_something(player)` to simplify lambdas. -Private members, only accessible from mixins, should start with `_mygame_`, -public members with `mygame_`. - -More advanced uses could be to add additional variables to the state object, -override `World.collect(self, state, item)` and `remove(self, state, item)` -to update the state object, and check those added variables in added methods. -Please do this with caution and only when necessary. - -#### Sample +Custom methods can be defined for your logic rules. The access rule that ultimately gets assigned to the Location or +Entrance should be +a [`CollectionRule`](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/generic/Rules.py#L9). +Typically, this is done by defining a lambda expression on demand at the relevant bit, typically calling other +functions, but this can also be achieved by defining a method with the appropriate format and assigning it directly. +For an example, see [The Messenger](/worlds/messenger/rules.py). ```python # logic.py -from worlds.AutoWorld import LogicMixin +from BaseClasses import CollectionState -class MyGameLogic(LogicMixin): - def mygame_has_key(self, player: int) -> bool: - # Arguments above are free to choose - # MultiWorld can be accessed through self.multiworld, explicitly passing in - # MyGameWorld instance for easy options access is also a valid approach - return self.has("key", player) # or whatever + +def mygame_has_key(self, state: CollectionState, player: int) -> bool: + # More arguments above are free to choose, since you can expect this is only called in your world + # MultiWorld can be accessed through state.multiworld. + # Explicitly passing in MyGameWorld instance for easy options access is also a valid approach, but it's generally + # better to check options before rule assignment since the individual functions can be called thousands of times + return state.has("key", player) # or whatever ``` + ```python # __init__.py from worlds.generic.Rules import set_rule -import .logic # apply the mixin by importing its file +from . import logic + class MyGameWorld(World): # ... def set_rules(self) -> None: set_rule(self.multiworld.get_location("A Door", self.player), - lambda state: state.mygame_has_key(self.player)) + lambda state: logic.mygame_has_key(state, self.player)) +``` + +### Logic Mixin + +While lambdas and events can do pretty much anything, more complex logic can be handled in logic mixins. + +When importing a file that defines a class that inherits from `worlds.AutoWorld.LogicMixin`, the `CollectionState` class +is automatically extended by the mixin's members. These members should be prefixed with the name of the implementing +world since the namespace is shared with all other logic mixins. + +Some uses could be to add additional variables to the state object, or to have a custom state machine that gets modified +with the state. +Please do this with caution and only when necessary. + +#### pre_fill + +```python +def pre_fill(self) -> None: + # place item Herb into location Chest1 for some reason + item = self.create_item("Herb") + self.multiworld.get_location("Chest1", self.player).place_locked_item(item) + # in most cases it's better to do this at the same time the itempool is + # filled to avoid accidental duplicates, such as manually placed and still in the itempool ``` ### Generate Output @@ -792,9 +705,9 @@ from .mod import generate_mod def generate_output(self, output_directory: str) -> None: - # How to generate the mod or ROM highly depends on the game - # if the mod is written in Lua, Jinja can be used to fill a template - # if the mod reads a json file, `json.dump()` can be used to generate that + # How to generate the mod or ROM highly depends on the game. + # If the mod is written in Lua, Jinja can be used to fill a template. + # If the mod reads a json file, `json.dump()` can be used to generate that. # code below is a dummy data = { "seed": self.multiworld.seed_name, # to verify the server's multiworld @@ -804,8 +717,7 @@ def generate_output(self, output_directory: str) -> None: for location in self.multiworld.get_filled_locations(self.player)}, # store start_inventory from player's .yaml # make sure to mark as not remote_start_inventory when connecting if stored in rom/mod - "starter_items": [item.name for item - in self.multiworld.precollected_items[self.player]], + "starter_items": [item.name for item in self.multiworld.precollected_items[self.player]], } # add needed option results to the dictionary @@ -824,20 +736,20 @@ def generate_output(self, output_directory: str) -> None: ### Slot Data If the game client needs to know information about the generated seed, a preferred method of transferring the data -is through the slot data. This can be filled from the `fill_slot_data` method of your world by returning a `Dict[str, Any]`, -but should be limited to data that is absolutely necessary to not waste resources. Slot data is sent to your client once -it has successfully [connected](network%20protocol.md#connected). -If you need to know information about locations in your world, instead -of propagating the slot data, it is preferable to use [LocationScouts](network%20protocol.md#locationscouts) since that -data already exists on the server. The most common usage of slot data is to send option results that the client needs -to be aware of. +is through the slot data. This is filled with the `fill_slot_data` method of your world by returning +a `Dict[str, Any]`, but, to not waste resources, should be limited to data that is absolutely necessary. Slot data is +sent to your client once it has successfully [connected](network%20protocol.md#connected). +If you need to know information about locations in your world, instead of propagating the slot data, it is preferable +to use [LocationScouts](network%20protocol.md#locationscouts), since that data already exists on the server. The most +common usage of slot data is sending option results that the client needs to be aware of. ```python def fill_slot_data(self) -> Dict[str, Any]: - # in order for our game client to handle the generated seed correctly we need to know what the user selected - # for their difficulty and final boss HP - # a dictionary returned from this method gets set as the slot_data and will be sent to the client after connecting - # the options dataclass has a method to return a `Dict[str, Any]` of each option name provided and the option's value + # In order for our game client to handle the generated seed correctly we need to know what the user selected + # for their difficulty and final boss HP. + # A dictionary returned from this method gets set as the slot_data and will be sent to the client after connecting. + # The options dataclass has a method to return a `Dict[str, Any]` of each option name provided and the relevant + # option's value. return self.options.as_dict("difficulty", "final_boss_hp") ``` @@ -847,15 +759,17 @@ Each world implementation should have a tutorial and a game info page. These are the `.md` files in your world's `/docs` directory. #### Game Info + The game info page is for a short breakdown of what your game is and how it works in Archipelago. Any additional information that may be useful to the player when learning your randomizer should also go here. The file name format is `_.md`. While you can write these docs for multiple languages, currently only the english version is displayed on the website. #### Tutorials + Your game can have as many tutorials in as many languages as you like, with each one having a relevant `Tutorial` -defined in the `WebWorld`. The file name you use aren't particularly important, but it should be descriptive of what -the tutorial is covering, and the name of the file must match the relative URL provided in the `Tutorial`. Currently, +defined in the `WebWorld`. The file name you use isn't particularly important, but it should be descriptive of what +the tutorial covers, and the name of the file must match the relative URL provided in the `Tutorial`. Currently, the JS that determines this ignores the provided file name and will search for `game/document_lang.md`, where `game/document/lang` is the provided URL. @@ -874,12 +788,13 @@ from test.bases import WorldTestBase class MyGameTestBase(WorldTestBase): - game = "My Game" + game = "My Game" ``` -Next using the rules defined in the above `set_rules` we can test that the chests have the correct access rules. +Next, using the rules defined in the above `set_rules` we can test that the chests have the correct access rules. Example `test_chest_access.py` + ```python from . import MyGameTestBase @@ -889,15 +804,15 @@ class TestChestAccess(MyGameTestBase): """Test locations that require a sword""" locations = ["Chest1", "Chest2"] items = [["Sword"]] - # this will test that each location can't be accessed without the "Sword", but can be accessed once obtained. + # this will test that each location can't be accessed without the "Sword", but can be accessed once obtained self.assertAccessDependency(locations, items) def test_any_weapon_chests(self) -> None: """Test locations that require any weapon""" locations = [f"Chest{i}" for i in range(3, 6)] items = [["Sword"], ["Axe"], ["Spear"]] - # this will test that chests 3-5 can't be accessed without any weapon, but can be with just one of them. + # this will test that chests 3-5 can't be accessed without any weapon, but can be with just one of them self.assertAccessDependency(locations, items) ``` -For more information on tests check the [tests doc](tests.md). +For more information on tests, check the [tests doc](tests.md).