Docs: clean up world api.md a bit (#1958)

This commit is contained in:
el-u 2023-07-09 18:04:24 +02:00 committed by GitHub
parent 07d74ac186
commit ab22b11bac
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 43 additions and 42 deletions

View File

@ -1,7 +1,7 @@
# apworld Specification # apworld Specification
Archipelago depends on worlds to provide game-specific details like items, locations and output generation. Archipelago depends on worlds to provide game-specific details like items, locations and output generation.
Those are located in the `worlds/` folder (source) or `<insall dir>/lib/worlds/` (when installed). Those are located in the `worlds/` folder (source) or `<install dir>/lib/worlds/` (when installed).
See [world api.md](world%20api.md) for details. See [world api.md](world%20api.md) for details.
apworld provides a way to package and ship a world that is not part of the main distribution by placing a `*.apworld` apworld provides a way to package and ship a world that is not part of the main distribution by placing a `*.apworld`

View File

@ -22,8 +22,8 @@ allows using WebSockets.
## Coding style ## Coding style
AP follows all the PEPs. When in doubt use an IDE with coding style AP follows [style.md](https://github.com/ArchipelagoMW/Archipelago/blob/main/docs/style.md).
linter, for example PyCharm Community Edition. When in doubt use an IDE with coding style linter, for example PyCharm Community Edition.
## Docstrings ## Docstrings
@ -44,7 +44,7 @@ class MyGameWorld(World):
## Definitions ## Definitions
This section will cover various classes and objects you can use for your world. 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, While some of the attributes and methods are mentioned here, not all of them are,
but you can find them in `BaseClasses.py`. but you can find them in `BaseClasses.py`.
### World Class ### World Class
@ -56,11 +56,12 @@ game.
### WebWorld Class ### WebWorld Class
A `WebWorld` class contains specific attributes and methods that can be modified A `WebWorld` class contains specific attributes and methods that can be modified
for your world specifically on the webhost. for your world specifically on the webhost:
`settings_page` which can be changed to a link instead of an AP generated settings page. `settings_page`, which can be changed to a link instead of an AP generated settings 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 |
|---|---|---|---|---|---|---|---| |---|---|---|---|---|---|---|---|
| <img src="img/theme_dirt.JPG" width="100"> | <img src="img/theme_grass.JPG" width="100"> | <img src="img/theme_grassFlowers.JPG" width="100"> | <img src="img/theme_ice.JPG" width="100"> | <img src="img/theme_jungle.JPG" width="100"> | <img src="img/theme_ocean.JPG" width="100"> | <img src="img/theme_partyTime.JPG" width="100"> | <img src="img/theme_stone.JPG" width="100"> | | <img src="img/theme_dirt.JPG" width="100"> | <img src="img/theme_grass.JPG" width="100"> | <img src="img/theme_grassFlowers.JPG" width="100"> | <img src="img/theme_ice.JPG" width="100"> | <img src="img/theme_jungle.JPG" width="100"> | <img src="img/theme_ocean.JPG" width="100"> | <img src="img/theme_partyTime.JPG" width="100"> | <img src="img/theme_stone.JPG" width="100"> |
@ -75,26 +76,27 @@ prefixed with the same string as defined here. Default already has 'en'.
### MultiWorld Object ### MultiWorld Object
The `MultiWorld` object references the whole multiworld (all items and locations The `MultiWorld` object references the whole multiworld (all items and locations
for all players) and is accessible through `self.world` inside a `World` object. for all players) and is accessible through `self.multiworld` inside a `World` object.
### Player ### Player
The player is just an integer in AP and is accessible through `self.player` The player is just an integer in AP and is accessible through `self.player`
inside a World object. inside a `World` object.
### Player Options ### Player Options
Players provide customized settings for their World in the form of yamls. Players provide customized settings for their World in the form of yamls.
Those are accessible through `self.world.<option_name>[self.player]`. A dict Those are accessible through `self.multiworld.<option_name>[self.player]`. A dict
of valid options has to be provided in `self.option_definitions`. Options are automatically of valid options has to be provided in `self.option_definitions`. Options are automatically
added to the `World` object for easy access. added to the `World` object for easy access.
### World Options ### World Settings
Any AP installation can provide settings for a world, for example a ROM file, accessible through `self.settings.option` Any AP installation can provide settings for a world, for example a ROM file, accessible through
or `cls.settings.option` (new API) or `Utils.get_options()["<world>_options"]["<option>"]` (deprecated). `self.settings.<setting_name>` or `cls.settings.<setting_name>` (new API)
or `Utils.get_options()["<world>_options"]["<setting_name>"]` (deprecated).
Users can set those in their `host.yaml` file. Some options may automatically open a file browser if a file is missing. Users can set those 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) Refer to [settings api.md](https://github.com/ArchipelagoMW/Archipelago/blob/main/docs/settings%20api.md)
for details. for details.
@ -135,10 +137,10 @@ same ID. Name must not be numeric (has to contain at least 1 letter or symbol).
Special items with ID `None` can mark events (read below). Special items with ID `None` can mark events (read below).
Other classifications include Other classifications include
* filler: a regular item or trash item * `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
* trap: negative impact on the player * `trap`: negative impact on the player
* skip_balancing: add to progression to skip balancing; e.g. currency or tokens * `skip_balancing`: add to `progression` to skip balancing; e.g. currency or tokens
### Events ### Events
@ -162,10 +164,10 @@ or more event locations based on player options.
Regions are logical groups of locations that share some common access rules. If Regions are logical groups of locations that share some common access rules. If
location logic is written from scratch, using regions greatly simplifies the location logic is written from scratch, using regions greatly simplifies the
definition and allow to somewhat easily implement things like entrance definition and allows to somewhat easily implement things like entrance
randomizer in logic. randomizer in logic.
Regions have a list called `exits` which are `Entrance` objects representing Regions have a list called `exits`, which are `Entrance` objects representing
transitions to other regions. transitions to other regions.
There has to be one special region "Menu" from which the logic unfolds. AP There has to be one special region "Menu" from which the logic unfolds. AP
@ -182,7 +184,7 @@ They can be static (regular logic) or be defined/connected during generation
### Access Rules ### Access Rules
An access rule is a function that returns `True` or `False` for a `Location` or An access rule is a function that returns `True` or `False` for a `Location` or
`Entrance` based on the the current `state` (items that can be collected). `Entrance` based on the current `state` (items that can be collected).
### Item Rules ### Item Rules
@ -199,14 +201,14 @@ the `/worlds` directory. The starting point for the package is `__init__.py`.
Conventionally, your world class is placed in that file. Conventionally, your world class is placed in that file.
World classes must inherit from the `World` class in `/worlds/AutoWorld.py`, World classes must inherit from the `World` class in `/worlds/AutoWorld.py`,
which can be imported as `worlds.AutoWorld.World` from your package. 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. AP will pick up your world automatically due to the `AutoWorld` implementation.
### Requirements ### Requirements
If your world needs specific python packages, they can be listed in If your world needs specific python packages, they can be listed in
`world/[world_name]/requirements.txt`. ModuleUpdate.py will automatically `worlds/<world_name>/requirements.txt`. ModuleUpdate.py will automatically
pick up and install them. pick up and install them.
See [pip documentation](https://pip.pypa.io/en/stable/cli/pip_install/#requirements-file-format). See [pip documentation](https://pip.pypa.io/en/stable/cli/pip_install/#requirements-file-format).
@ -217,7 +219,7 @@ 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. use multiple files and use relative imports to access them.
e.g. `from .Options import mygame_options` from your `__init__.py` will load e.g. `from .Options import mygame_options` from your `__init__.py` will load
`world/[world_name]/Options.py` and make its `mygame_options` accesible. `worlds/<world_name>/Options.py` and make its `mygame_options` accessible.
When imported names pile up it may be easier to use `from . import Options` 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.mygame_options`.
@ -228,12 +230,12 @@ function, see [apworld specification.md](apworld%20specification.md).
### Your Item Type ### Your Item Type
Each world uses its own subclass of `BaseClasses.Item`. The constuctor can be 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". 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 Since the constructor is only ever called from your code, you can add whatever
arguments you like to the constructor. arguments you like to the constructor.
In its simplest form we only set the game name and use the default constuctor In its simplest form we only set the game name and use the default constructor
```python ```python
from BaseClasses import Item from BaseClasses import Item
@ -268,7 +270,7 @@ 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 to describe it and a `display_name` property for display on the website and in
spoiler logs. spoiler logs.
The actual name as used in the yaml is defined in a `dict[str, Option]`, that is The actual name as used in the yaml is defined in a `Dict[str, AssembleOptions]`, that is
assigned to the world under `self.option_definitions`. assigned to the world under `self.option_definitions`.
Common option types are `Toggle`, `DefaultOnToggle`, `Choice`, `Range`. Common option types are `Toggle`, `DefaultOnToggle`, `Choice`, `Range`.
@ -329,10 +331,10 @@ class FixXYZGlitch(Toggle):
display_name = "Fix XYZ Glitch" display_name = "Fix XYZ Glitch"
# By convention we call the options dict variable `<world>_options`. # By convention we call the options dict variable `<world>_options`.
mygame_options: typing.Dict[str, type(Option)] = { mygame_options: typing.Dict[str, AssembleOptions] = {
"difficulty": Difficulty, "difficulty": Difficulty,
"final_boss_hp": FinalBossHP, "final_boss_hp": FinalBossHP,
"fix_xyz_glitch": FixXYZGlitch "fix_xyz_glitch": FixXYZGlitch,
} }
``` ```
```python ```python
@ -359,7 +361,6 @@ from .Items import mygame_items # data used below to add items to the World
from .Locations import mygame_locations # same as above from .Locations import mygame_locations # same as above
from worlds.AutoWorld import World from worlds.AutoWorld import World
from BaseClasses import Region, Location, Entrance, Item, RegionType, ItemClassification from BaseClasses import Region, Location, Entrance, Item, RegionType, ItemClassification
from Utils import get_options, output_path
class MyGameItem(Item): # or from Items import MyGameItem class MyGameItem(Item): # or from Items import MyGameItem
@ -385,7 +386,7 @@ class MyGameWorld(World):
topology_present = True # show path to required location checks in spoiler topology_present = True # show path to required location checks in spoiler
# ID of first item and location, could be hard-coded but code may be easier # ID of first item and location, could be hard-coded but code may be easier
# to read with this as a propery. # to read with this as a property.
base_id = 1234 base_id = 1234
# Instead of dynamic numbering, IDs could be part of data. # Instead of dynamic numbering, IDs could be part of data.
@ -400,7 +401,7 @@ class MyGameWorld(World):
# Items can be grouped using their names to allow easy checking if any item # Items can be grouped using their names to allow easy checking if any item
# from that group has been collected. Group names can also be used for !hint # from that group has been collected. Group names can also be used for !hint
item_name_groups = { item_name_groups = {
"weapons": {"sword", "lance"} "weapons": {"sword", "lance"},
} }
``` ```
@ -414,7 +415,7 @@ The world has to provide the following things for generation
* locations placed inside those regions * locations placed inside those regions
* a `def create_item(self, item: str) -> MyGameItem` to create any item on demand * a `def create_item(self, item: str) -> MyGameItem` to create any item on demand
* applying `self.multiworld.push_precollected` for start inventory * applying `self.multiworld.push_precollected` for start inventory
* `required_client_version: Tuple(int, int, int)` * `required_client_version: Tuple[int, int, int]`
Optional client version as tuple of 3 ints to make sure the client is compatible to 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. this world (e.g. implements all required features) when connecting.
@ -587,7 +588,7 @@ def set_rules(self) -> None:
# require one item from an item group # require one item from an item group
add_rule(self.multiworld.get_location("Chest3", self.player), add_rule(self.multiworld.get_location("Chest3", self.player),
lambda state: state.has_group("weapons", self.player)) lambda state: state.has_group("weapons", self.player))
# state also has .item_count() for items, .has_any() and.has_all() for sets # state also has .item_count() for items, .has_any() and .has_all() for sets
# and .count_group() for groups # and .count_group() for groups
# set_rule is likely to be a bit faster than add_rule # set_rule is likely to be a bit faster than add_rule
@ -625,7 +626,7 @@ public members with `mygame_`.
More advanced uses could be to add additional variables to the state object, More advanced uses could be to add additional variables to the state object,
override `World.collect(self, state, item)` and `remove(self, state, item)` override `World.collect(self, state, item)` and `remove(self, state, item)`
to update the state object, and check those added variables in added methods. to update the state object, and check those added variables in added methods.
Please do this with caution and only when neccessary. Please do this with caution and only when necessary.
#### Sample #### Sample
@ -637,7 +638,7 @@ from worlds.AutoWorld import LogicMixin
class MyGameLogic(LogicMixin): class MyGameLogic(LogicMixin):
def mygame_has_key(self, player: int): def mygame_has_key(self, player: int):
# Arguments above are free to choose # Arguments above are free to choose
# MultiWorld can be accessed through self.world, explicitly passing in # MultiWorld can be accessed through self.multiworld, explicitly passing in
# MyGameWorld instance for easy options access is also a valid approach # MyGameWorld instance for easy options access is also a valid approach
return self.has("key", player) # or whatever return self.has("key", player) # or whatever
``` ```
@ -650,8 +651,8 @@ import .Logic # apply the mixin by importing its file
class MyGameWorld(World): class MyGameWorld(World):
# ... # ...
def set_rules(self): def set_rules(self):
set_rule(self.world.get_location("A Door", self.player), set_rule(self.multiworld.get_location("A Door", self.player),
lamda state: state.mygame_has_key(self.player)) lambda state: state.mygame_has_key(self.player))
``` ```
### Generate Output ### Generate Output
@ -679,14 +680,14 @@ def generate_output(self, output_directory: str):
# store option name "easy", "normal" or "hard" for difficuly # store option name "easy", "normal" or "hard" for difficuly
"difficulty": self.multiworld.difficulty[self.player].current_key, "difficulty": self.multiworld.difficulty[self.player].current_key,
# store option value True or False for fixing a glitch # store option value True or False for fixing a glitch
"fix_xyz_glitch": self.multiworld.fix_xyz_glitch[self.player].value "fix_xyz_glitch": self.multiworld.fix_xyz_glitch[self.player].value,
} }
# point to a ROM specified by the installation # point to a ROM specified by the installation
src = self.settings.rom_file src = self.settings.rom_file
# or point to worlds/mygame/data/mod_template # or point to worlds/mygame/data/mod_template
src = os.path.join(os.path.dirname(__file__), "data", "mod_template") src = os.path.join(os.path.dirname(__file__), "data", "mod_template")
# generate output path # generate output path
mod_name = f"AP-{self.multiworld.seed_name}-P{self.player}-{self.multiworld.player_name[self.player]}" mod_name = self.multiworld.get_out_file_name_base(self.player)
out_file = os.path.join(output_directory, mod_name + ".zip") out_file = os.path.join(output_directory, mod_name + ".zip")
# generate the file # generate the file
generate_mod(src, out_file, data) generate_mod(src, out_file, data)
@ -735,14 +736,14 @@ from . import MyGameTestBase
class TestChestAccess(MyGameTestBase): class TestChestAccess(MyGameTestBase):
def testSwordChests(self): def test_sword_chests(self):
"""Test locations that require a sword""" """Test locations that require a sword"""
locations = ["Chest1", "Chest2"] locations = ["Chest1", "Chest2"]
items = [["Sword"]] 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) self.assertAccessDependency(locations, items)
def testAnyWeaponChests(self): def test_any_weapon_chests(self):
"""Test locations that require any weapon""" """Test locations that require any weapon"""
locations = [f"Chest{i}" for i in range(3, 6)] locations = [f"Chest{i}" for i in range(3, 6)]
items = [["Sword"], ["Axe"], ["Spear"]] items = [["Sword"], ["Axe"], ["Spear"]]