diff --git a/docs/world api.md b/docs/world api.md index 6714fa3a..37638c3c 100644 --- a/docs/world api.md +++ b/docs/world api.md @@ -121,6 +121,53 @@ class RLWeb(WebWorld): # ... ``` +* `location_descriptions` (optional) WebWorlds can provide a map that contains human-friendly descriptions of locations +or location groups. + + ```python + # 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. + + This doesn't include the item on the spaceship door, since it can be + accessed without the Spaceship Key. + """ + } + + # __init__.py + from worlds.AutoWorld import WebWorld + from .locations import location_descriptions + + + class MyGameWeb(WebWorld): + location_descriptions = location_descriptions + ``` + +* `item_descriptions` (optional) WebWorlds can provide a map that contains human-friendly descriptions of items or item +groups. + + ```python + # items.py + item_descriptions = { + "Red Potion": "A standard health potion", + "Spaceship Key": """ + The key to the spaceship in Level 2. + + This is necessary to get to the Star Realm. + """, + } + + # __init__.py + from worlds.AutoWorld import WebWorld + from .items import item_descriptions + + + class MyGameWeb(WebWorld): + item_descriptions = item_descriptions + ``` + ### MultiWorld Object The `MultiWorld` object references the whole multiworld (all items and locations for all players) and is accessible @@ -178,36 +225,6 @@ 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 required, and will prevent progression and useful items from being placed at excluded locations. -#### Documenting Locations - -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 options pages. - -```python -# 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. - - This doesn't include the item on the spaceship door, since it can be accessed without the Spaceship Key. - """ -} -``` - -```python -# __init__.py - -from worlds.AutoWorld import World -from .locations import location_descriptions - - -class MyGameWorld(World): - location_descriptions = location_descriptions -``` - ### Items Items are all things that can "drop" for your game. This may be RPG items like weapons, or technologies you normally @@ -232,36 +249,6 @@ Other classifications include: * `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, to not flood early spheres -#### Documenting Items - -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 options pages. - -```python -# items.py - -item_descriptions = { - "Red Potion": "A standard health potion", - "Spaceship Key": - """ - The key to the spaceship in Level 2. - - This is necessary to get to the Star Realm. - """ -} -``` - -```python -# __init__.py - -from worlds.AutoWorld import World -from .items import item_descriptions - - -class MyGameWorld(World): - item_descriptions = item_descriptions -``` - ### Events An Event is a special combination of a Location and an Item, with both having an `id` of `None`. These can be used to diff --git a/test/general/test_items.py b/test/general/test_items.py index 7c0b7050..9cc91a1b 100644 --- a/test/general/test_items.py +++ b/test/general/test_items.py @@ -64,15 +64,6 @@ class TestBase(unittest.TestCase): for item in multiworld.itempool: self.assertIn(item.name, world_type.item_name_to_id) - def test_item_descriptions_have_valid_names(self): - """Ensure all item descriptions match an item name or item group name""" - for game_name, world_type in AutoWorldRegister.world_types.items(): - valid_names = world_type.item_names.union(world_type.item_name_groups) - for name in world_type.item_descriptions: - with self.subTest("Name should be valid", game=game_name, item=name): - self.assertIn(name, valid_names, - "All item descriptions must match defined item names") - def test_itempool_not_modified(self): """Test that worlds don't modify the itempool after `create_items`""" gen_steps = ("generate_early", "create_regions", "create_items") diff --git a/test/general/test_locations.py b/test/general/test_locations.py index 2ac05931..4b95ebd2 100644 --- a/test/general/test_locations.py +++ b/test/general/test_locations.py @@ -66,12 +66,3 @@ class TestBase(unittest.TestCase): for location in locations: self.assertIn(location, world_type.location_name_to_id) self.assertNotIn(group_name, world_type.location_name_to_id) - - def test_location_descriptions_have_valid_names(self): - """Ensure all location descriptions match a location name or location group name""" - for game_name, world_type in AutoWorldRegister.world_types.items(): - valid_names = world_type.location_names.union(world_type.location_name_groups) - for name in world_type.location_descriptions: - with self.subTest("Name should be valid", game=game_name, location=name): - self.assertIn(name, valid_names, - "All location descriptions must match defined location names") diff --git a/test/webhost/test_descriptions.py b/test/webhost/test_descriptions.py new file mode 100644 index 00000000..70f375b5 --- /dev/null +++ b/test/webhost/test_descriptions.py @@ -0,0 +1,23 @@ +import unittest + +from worlds.AutoWorld import AutoWorldRegister + + +class TestWebDescriptions(unittest.TestCase): + def test_item_descriptions_have_valid_names(self) -> None: + """Ensure all item descriptions match an item name or item group name""" + for game_name, world_type in AutoWorldRegister.world_types.items(): + valid_names = world_type.item_names.union(world_type.item_name_groups) + for name in world_type.web.item_descriptions: + with self.subTest("Name should be valid", game=game_name, item=name): + self.assertIn(name, valid_names, + "All item descriptions must match defined item names") + + def test_location_descriptions_have_valid_names(self) -> None: + """Ensure all location descriptions match a location name or location group name""" + for game_name, world_type in AutoWorldRegister.world_types.items(): + valid_names = world_type.location_names.union(world_type.location_name_groups) + for name in world_type.web.location_descriptions: + with self.subTest("Name should be valid", game=game_name, location=name): + self.assertIn(name, valid_names, + "All location descriptions must match defined location names") diff --git a/worlds/AutoWorld.py b/worlds/AutoWorld.py index b564932e..32a84f5d 100644 --- a/worlds/AutoWorld.py +++ b/worlds/AutoWorld.py @@ -3,13 +3,12 @@ from __future__ import annotations import hashlib import logging import pathlib -from random import Random -import re import sys import time +from random import Random from dataclasses import make_dataclass -from typing import (Any, Callable, ClassVar, Dict, FrozenSet, List, Mapping, - Optional, Set, TextIO, Tuple, TYPE_CHECKING, Type, Union) +from typing import (Any, Callable, ClassVar, Dict, FrozenSet, List, Mapping, Optional, Set, TextIO, Tuple, + TYPE_CHECKING, Type, Union) from Options import ( ExcludeLocations, ItemLinks, LocalItems, NonLocalItems, OptionGroup, PerGameCommonOptions, @@ -55,17 +54,12 @@ class AutoWorldRegister(type): dct["item_name_groups"] = {group_name: frozenset(group_set) for group_name, group_set in dct.get("item_name_groups", {}).items()} dct["item_name_groups"]["Everything"] = dct["item_names"] - dct["item_descriptions"] = {name: _normalize_description(description) for name, description - in dct.get("item_descriptions", {}).items()} - dct["item_descriptions"]["Everything"] = "All items in the entire game." + dct["location_names"] = frozenset(dct["location_name_to_id"]) dct["location_name_groups"] = {group_name: frozenset(group_set) for group_name, group_set in dct.get("location_name_groups", {}).items()} dct["location_name_groups"]["Everywhere"] = dct["location_names"] dct["all_item_and_group_names"] = frozenset(dct["item_names"] | set(dct.get("item_name_groups", {}))) - dct["location_descriptions"] = {name: _normalize_description(description) for name, description - in dct.get("location_descriptions", {}).items()} - dct["location_descriptions"]["Everywhere"] = "All locations in the entire game." # move away from get_required_client_version function if "game" in dct: @@ -226,6 +220,12 @@ class WebWorld(metaclass=WebWorldRegister): option_groups: ClassVar[List[OptionGroup]] = [] """Ordered list of option groupings. Any options not set in a group will be placed in a pre-built "Game Options".""" + location_descriptions: Dict[str, str] = {} + """An optional map from location names (or location group names) to brief descriptions for users.""" + + item_descriptions: Dict[str, str] = {} + """An optional map from item names (or item group names) to brief descriptions for users.""" + class World(metaclass=AutoWorldRegister): """A World object encompasses a game's Items, Locations, Rules and additional data or functionality required. @@ -252,23 +252,9 @@ class World(metaclass=AutoWorldRegister): item_name_groups: ClassVar[Dict[str, Set[str]]] = {} """maps item group names to sets of items. Example: {"Weapons": {"Sword", "Bow"}}""" - item_descriptions: ClassVar[Dict[str, str]] = {} - """An optional map from item names (or item group names) to brief descriptions for users. - - Individual newlines and indentation will be collapsed into spaces before these descriptions are - displayed. This may cover only a subset of items. - """ - location_name_groups: ClassVar[Dict[str, Set[str]]] = {} """maps location group names to sets of locations. Example: {"Sewer": {"Sewer Key Drop 1", "Sewer Key Drop 2"}}""" - location_descriptions: ClassVar[Dict[str, str]] = {} - """An optional map from location names (or location group names) to brief descriptions for users. - - Individual newlines and indentation will be collapsed into spaces before these descriptions are - displayed. This may cover only a subset of locations. - """ - data_version: ClassVar[int] = 0 """ Increment this every time something in your world's names/id mappings changes. @@ -572,18 +558,3 @@ def data_package_checksum(data: "GamesPackage") -> str: assert sorted(data) == list(data), "Data not ordered" from NetUtils import encode return hashlib.sha1(encode(data).encode()).hexdigest() - - -def _normalize_description(description): - """ - Normalizes a description in item_descriptions or location_descriptions. - - This allows authors to write descritions with nice indentation and line lengths in their world - definitions without having it affect the rendered format. - """ - # First, collapse the whitespace around newlines and the ends of the description. - description = re.sub(r' *\n *', '\n', description.strip()) - # Next, condense individual newlines into spaces. - description = re.sub(r'(?