Tests: modern PEP8-ify core test modules and methods (#2298)
* rename modules * rename methods * add docstrings to the general tests * add base import stub * test_base -> bases * print deprecation warning * redo 2346
This commit is contained in:
parent
6e6fa13e44
commit
30da81c390
|
@ -759,8 +759,9 @@ multiworld for each test written using it. Within subsequent modules, classes sh
|
||||||
TestBase, and can then define options to test in the class body, and run tests in each test method.
|
TestBase, and can then define options to test in the class body, and run tests in each test method.
|
||||||
|
|
||||||
Example `__init__.py`
|
Example `__init__.py`
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from test.TestBase import WorldTestBase
|
from test.test_base import WorldTestBase
|
||||||
|
|
||||||
|
|
||||||
class MyGameTestBase(WorldTestBase):
|
class MyGameTestBase(WorldTestBase):
|
||||||
|
|
313
test/TestBase.py
313
test/TestBase.py
|
@ -1,310 +1,3 @@
|
||||||
import typing
|
from .bases import TestBase, WorldTestBase
|
||||||
import unittest
|
from warnings import warn
|
||||||
from argparse import Namespace
|
warn("TestBase was renamed to bases", DeprecationWarning)
|
||||||
|
|
||||||
from test.general import gen_steps
|
|
||||||
from worlds import AutoWorld
|
|
||||||
from worlds.AutoWorld import call_all
|
|
||||||
|
|
||||||
from BaseClasses import Location, MultiWorld, CollectionState, ItemClassification, Item
|
|
||||||
from worlds.alttp.Items import ItemFactory
|
|
||||||
|
|
||||||
|
|
||||||
class TestBase(unittest.TestCase):
|
|
||||||
multiworld: MultiWorld
|
|
||||||
_state_cache = {}
|
|
||||||
|
|
||||||
def get_state(self, items):
|
|
||||||
if (self.multiworld, tuple(items)) in self._state_cache:
|
|
||||||
return self._state_cache[self.multiworld, tuple(items)]
|
|
||||||
state = CollectionState(self.multiworld)
|
|
||||||
for item in items:
|
|
||||||
item.classification = ItemClassification.progression
|
|
||||||
state.collect(item, event=True)
|
|
||||||
state.sweep_for_events()
|
|
||||||
state.update_reachable_regions(1)
|
|
||||||
self._state_cache[self.multiworld, tuple(items)] = state
|
|
||||||
return state
|
|
||||||
|
|
||||||
def get_path(self, state, region):
|
|
||||||
def flist_to_iter(node):
|
|
||||||
while node:
|
|
||||||
value, node = node
|
|
||||||
yield value
|
|
||||||
|
|
||||||
from itertools import zip_longest
|
|
||||||
reversed_path_as_flist = state.path.get(region, (region, None))
|
|
||||||
string_path_flat = reversed(list(map(str, flist_to_iter(reversed_path_as_flist))))
|
|
||||||
# Now we combine the flat string list into (region, exit) pairs
|
|
||||||
pathsiter = iter(string_path_flat)
|
|
||||||
pathpairs = zip_longest(pathsiter, pathsiter)
|
|
||||||
return list(pathpairs)
|
|
||||||
|
|
||||||
def run_location_tests(self, access_pool):
|
|
||||||
for i, (location, access, *item_pool) in enumerate(access_pool):
|
|
||||||
items = item_pool[0]
|
|
||||||
all_except = item_pool[1] if len(item_pool) > 1 else None
|
|
||||||
state = self._get_items(item_pool, all_except)
|
|
||||||
path = self.get_path(state, self.multiworld.get_location(location, 1).parent_region)
|
|
||||||
with self.subTest(msg="Reach Location", location=location, access=access, items=items,
|
|
||||||
all_except=all_except, path=path, entry=i):
|
|
||||||
|
|
||||||
self.assertEqual(self.multiworld.get_location(location, 1).can_reach(state), access,
|
|
||||||
f"failed {self.multiworld.get_location(location, 1)} with: {item_pool}")
|
|
||||||
|
|
||||||
# check for partial solution
|
|
||||||
if not all_except and access: # we are not supposed to be able to reach location with partial inventory
|
|
||||||
for missing_item in item_pool[0]:
|
|
||||||
with self.subTest(msg="Location reachable without required item", location=location,
|
|
||||||
items=item_pool[0], missing_item=missing_item, entry=i):
|
|
||||||
state = self._get_items_partial(item_pool, missing_item)
|
|
||||||
|
|
||||||
self.assertEqual(self.multiworld.get_location(location, 1).can_reach(state), False,
|
|
||||||
f"failed {self.multiworld.get_location(location, 1)}: succeeded with "
|
|
||||||
f"{missing_item} removed from: {item_pool}")
|
|
||||||
|
|
||||||
def run_entrance_tests(self, access_pool):
|
|
||||||
for i, (entrance, access, *item_pool) in enumerate(access_pool):
|
|
||||||
items = item_pool[0]
|
|
||||||
all_except = item_pool[1] if len(item_pool) > 1 else None
|
|
||||||
state = self._get_items(item_pool, all_except)
|
|
||||||
path = self.get_path(state, self.multiworld.get_entrance(entrance, 1).parent_region)
|
|
||||||
with self.subTest(msg="Reach Entrance", entrance=entrance, access=access, items=items,
|
|
||||||
all_except=all_except, path=path, entry=i):
|
|
||||||
|
|
||||||
self.assertEqual(self.multiworld.get_entrance(entrance, 1).can_reach(state), access)
|
|
||||||
|
|
||||||
# check for partial solution
|
|
||||||
if not all_except and access: # we are not supposed to be able to reach location with partial inventory
|
|
||||||
for missing_item in item_pool[0]:
|
|
||||||
with self.subTest(msg="Entrance reachable without required item", entrance=entrance,
|
|
||||||
items=item_pool[0], missing_item=missing_item, entry=i):
|
|
||||||
state = self._get_items_partial(item_pool, missing_item)
|
|
||||||
self.assertEqual(self.multiworld.get_entrance(entrance, 1).can_reach(state), False,
|
|
||||||
f"failed {self.multiworld.get_entrance(entrance, 1)} with: {item_pool}")
|
|
||||||
|
|
||||||
def _get_items(self, item_pool, all_except):
|
|
||||||
if all_except and len(all_except) > 0:
|
|
||||||
items = self.multiworld.itempool[:]
|
|
||||||
items = [item for item in items if
|
|
||||||
item.name not in all_except and not ("Bottle" in item.name and "AnyBottle" in all_except)]
|
|
||||||
items.extend(ItemFactory(item_pool[0], 1))
|
|
||||||
else:
|
|
||||||
items = ItemFactory(item_pool[0], 1)
|
|
||||||
return self.get_state(items)
|
|
||||||
|
|
||||||
def _get_items_partial(self, item_pool, missing_item):
|
|
||||||
new_items = item_pool[0].copy()
|
|
||||||
new_items.remove(missing_item)
|
|
||||||
items = ItemFactory(new_items, 1)
|
|
||||||
return self.get_state(items)
|
|
||||||
|
|
||||||
|
|
||||||
class WorldTestBase(unittest.TestCase):
|
|
||||||
options: typing.Dict[str, typing.Any] = {}
|
|
||||||
multiworld: MultiWorld
|
|
||||||
|
|
||||||
game: typing.ClassVar[str] # define game name in subclass, example "Secret of Evermore"
|
|
||||||
auto_construct: typing.ClassVar[bool] = True
|
|
||||||
""" automatically set up a world for each test in this class """
|
|
||||||
|
|
||||||
def setUp(self) -> None:
|
|
||||||
if self.auto_construct:
|
|
||||||
self.world_setup()
|
|
||||||
|
|
||||||
def world_setup(self, seed: typing.Optional[int] = None) -> None:
|
|
||||||
if type(self) is WorldTestBase or \
|
|
||||||
(hasattr(WorldTestBase, self._testMethodName)
|
|
||||||
and not self.run_default_tests and
|
|
||||||
getattr(self, self._testMethodName).__code__ is
|
|
||||||
getattr(WorldTestBase, self._testMethodName, None).__code__):
|
|
||||||
return # setUp gets called for tests defined in the base class. We skip world_setup here.
|
|
||||||
if not hasattr(self, "game"):
|
|
||||||
raise NotImplementedError("didn't define game name")
|
|
||||||
self.multiworld = MultiWorld(1)
|
|
||||||
self.multiworld.game[1] = self.game
|
|
||||||
self.multiworld.player_name = {1: "Tester"}
|
|
||||||
self.multiworld.set_seed(seed)
|
|
||||||
self.multiworld.state = CollectionState(self.multiworld)
|
|
||||||
args = Namespace()
|
|
||||||
for name, option in AutoWorld.AutoWorldRegister.world_types[self.game].options_dataclass.type_hints.items():
|
|
||||||
setattr(args, name, {
|
|
||||||
1: option.from_any(self.options.get(name, getattr(option, "default")))
|
|
||||||
})
|
|
||||||
self.multiworld.set_options(args)
|
|
||||||
for step in gen_steps:
|
|
||||||
call_all(self.multiworld, step)
|
|
||||||
|
|
||||||
# methods that can be called within tests
|
|
||||||
def collect_all_but(self, item_names: typing.Union[str, typing.Iterable[str]],
|
|
||||||
state: typing.Optional[CollectionState] = None) -> None:
|
|
||||||
"""Collects all pre-placed items and items in the multiworld itempool except those provided"""
|
|
||||||
if isinstance(item_names, str):
|
|
||||||
item_names = (item_names,)
|
|
||||||
if not state:
|
|
||||||
state = self.multiworld.state
|
|
||||||
for item in self.multiworld.get_items():
|
|
||||||
if item.name not in item_names:
|
|
||||||
state.collect(item)
|
|
||||||
|
|
||||||
def get_item_by_name(self, item_name: str) -> Item:
|
|
||||||
"""Returns the first item found in placed items, or in the itempool with the matching name"""
|
|
||||||
for item in self.multiworld.get_items():
|
|
||||||
if item.name == item_name:
|
|
||||||
return item
|
|
||||||
raise ValueError("No such item")
|
|
||||||
|
|
||||||
def get_items_by_name(self, item_names: typing.Union[str, typing.Iterable[str]]) -> typing.List[Item]:
|
|
||||||
"""Returns actual items from the itempool that match the provided name(s)"""
|
|
||||||
if isinstance(item_names, str):
|
|
||||||
item_names = (item_names,)
|
|
||||||
return [item for item in self.multiworld.itempool if item.name in item_names]
|
|
||||||
|
|
||||||
def collect_by_name(self, item_names: typing.Union[str, typing.Iterable[str]]) -> typing.List[Item]:
|
|
||||||
""" collect all of the items in the item pool that have the given names """
|
|
||||||
items = self.get_items_by_name(item_names)
|
|
||||||
self.collect(items)
|
|
||||||
return items
|
|
||||||
|
|
||||||
def collect(self, items: typing.Union[Item, typing.Iterable[Item]]) -> None:
|
|
||||||
"""Collects the provided item(s) into state"""
|
|
||||||
if isinstance(items, Item):
|
|
||||||
items = (items,)
|
|
||||||
for item in items:
|
|
||||||
self.multiworld.state.collect(item)
|
|
||||||
|
|
||||||
def remove_by_name(self, item_names: typing.Union[str, typing.Iterable[str]]) -> typing.List[Item]:
|
|
||||||
"""Remove all of the items in the item pool with the given names from state"""
|
|
||||||
items = self.get_items_by_name(item_names)
|
|
||||||
self.remove(items)
|
|
||||||
return items
|
|
||||||
|
|
||||||
def remove(self, items: typing.Union[Item, typing.Iterable[Item]]) -> None:
|
|
||||||
"""Removes the provided item(s) from state"""
|
|
||||||
if isinstance(items, Item):
|
|
||||||
items = (items,)
|
|
||||||
for item in items:
|
|
||||||
if item.location and item.location.event and item.location in self.multiworld.state.events:
|
|
||||||
self.multiworld.state.events.remove(item.location)
|
|
||||||
self.multiworld.state.remove(item)
|
|
||||||
|
|
||||||
def can_reach_location(self, location: str) -> bool:
|
|
||||||
"""Determines if the current state can reach the provided location name"""
|
|
||||||
return self.multiworld.state.can_reach(location, "Location", 1)
|
|
||||||
|
|
||||||
def can_reach_entrance(self, entrance: str) -> bool:
|
|
||||||
"""Determines if the current state can reach the provided entrance name"""
|
|
||||||
return self.multiworld.state.can_reach(entrance, "Entrance", 1)
|
|
||||||
|
|
||||||
def can_reach_region(self, region: str) -> bool:
|
|
||||||
"""Determines if the current state can reach the provided region name"""
|
|
||||||
return self.multiworld.state.can_reach(region, "Region", 1)
|
|
||||||
|
|
||||||
def count(self, item_name: str) -> int:
|
|
||||||
"""Returns the amount of an item currently in state"""
|
|
||||||
return self.multiworld.state.count(item_name, 1)
|
|
||||||
|
|
||||||
def assertAccessDependency(self,
|
|
||||||
locations: typing.List[str],
|
|
||||||
possible_items: typing.Iterable[typing.Iterable[str]],
|
|
||||||
only_check_listed: bool = False) -> None:
|
|
||||||
"""Asserts that the provided locations can't be reached without the listed items but can be reached with any
|
|
||||||
one of the provided combinations"""
|
|
||||||
all_items = [item_name for item_names in possible_items for item_name in item_names]
|
|
||||||
|
|
||||||
state = CollectionState(self.multiworld)
|
|
||||||
self.collect_all_but(all_items, state)
|
|
||||||
if only_check_listed:
|
|
||||||
for location in locations:
|
|
||||||
self.assertFalse(state.can_reach(location, "Location", 1), f"{location} is reachable without {all_items}")
|
|
||||||
else:
|
|
||||||
for location in self.multiworld.get_locations():
|
|
||||||
loc_reachable = state.can_reach(location, "Location", 1)
|
|
||||||
self.assertEqual(loc_reachable, location.name not in locations,
|
|
||||||
f"{location.name} is reachable without {all_items}" if loc_reachable
|
|
||||||
else f"{location.name} is not reachable without {all_items}")
|
|
||||||
for item_names in possible_items:
|
|
||||||
items = self.get_items_by_name(item_names)
|
|
||||||
for item in items:
|
|
||||||
state.collect(item)
|
|
||||||
for location in locations:
|
|
||||||
self.assertTrue(state.can_reach(location, "Location", 1),
|
|
||||||
f"{location} not reachable with {item_names}")
|
|
||||||
for item in items:
|
|
||||||
state.remove(item)
|
|
||||||
|
|
||||||
def assertBeatable(self, beatable: bool):
|
|
||||||
"""Asserts that the game can be beaten with the current state"""
|
|
||||||
self.assertEqual(self.multiworld.can_beat_game(self.multiworld.state), beatable)
|
|
||||||
|
|
||||||
# following tests are automatically run
|
|
||||||
@property
|
|
||||||
def run_default_tests(self) -> bool:
|
|
||||||
"""Not possible or identical to the base test that's always being run already"""
|
|
||||||
return (self.options
|
|
||||||
or self.setUp.__code__ is not WorldTestBase.setUp.__code__
|
|
||||||
or self.world_setup.__code__ is not WorldTestBase.world_setup.__code__)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def constructed(self) -> bool:
|
|
||||||
"""A multiworld has been constructed by this point"""
|
|
||||||
return hasattr(self, "game") and hasattr(self, "multiworld")
|
|
||||||
|
|
||||||
def testAllStateCanReachEverything(self):
|
|
||||||
"""Ensure all state can reach everything and complete the game with the defined options"""
|
|
||||||
if not (self.run_default_tests and self.constructed):
|
|
||||||
return
|
|
||||||
with self.subTest("Game", game=self.game):
|
|
||||||
excluded = self.multiworld.exclude_locations[1].value
|
|
||||||
state = self.multiworld.get_all_state(False)
|
|
||||||
for location in self.multiworld.get_locations():
|
|
||||||
if location.name not in excluded:
|
|
||||||
with self.subTest("Location should be reached", location=location):
|
|
||||||
reachable = location.can_reach(state)
|
|
||||||
self.assertTrue(reachable, f"{location.name} unreachable")
|
|
||||||
with self.subTest("Beatable"):
|
|
||||||
self.multiworld.state = state
|
|
||||||
self.assertBeatable(True)
|
|
||||||
|
|
||||||
def testEmptyStateCanReachSomething(self):
|
|
||||||
"""Ensure empty state can reach at least one location with the defined options"""
|
|
||||||
if not (self.run_default_tests and self.constructed):
|
|
||||||
return
|
|
||||||
with self.subTest("Game", game=self.game):
|
|
||||||
state = CollectionState(self.multiworld)
|
|
||||||
locations = self.multiworld.get_reachable_locations(state, 1)
|
|
||||||
self.assertGreater(len(locations), 0,
|
|
||||||
"Need to be able to reach at least one location to get started.")
|
|
||||||
|
|
||||||
def testFill(self):
|
|
||||||
"""Generates a multiworld and validates placements with the defined options"""
|
|
||||||
if not (self.run_default_tests and self.constructed):
|
|
||||||
return
|
|
||||||
from Fill import distribute_items_restrictive
|
|
||||||
|
|
||||||
# basically a shortened reimplementation of this method from core, in order to force the check is done
|
|
||||||
def fulfills_accessibility():
|
|
||||||
locations = self.multiworld.get_locations(1).copy()
|
|
||||||
state = CollectionState(self.multiworld)
|
|
||||||
while locations:
|
|
||||||
sphere: typing.List[Location] = []
|
|
||||||
for n in range(len(locations) - 1, -1, -1):
|
|
||||||
if locations[n].can_reach(state):
|
|
||||||
sphere.append(locations.pop(n))
|
|
||||||
self.assertTrue(sphere or self.multiworld.accessibility[1] == "minimal",
|
|
||||||
f"Unreachable locations: {locations}")
|
|
||||||
if not sphere:
|
|
||||||
break
|
|
||||||
for location in sphere:
|
|
||||||
if location.item:
|
|
||||||
state.collect(location.item, True, location)
|
|
||||||
|
|
||||||
return self.multiworld.has_beaten_game(state, 1)
|
|
||||||
|
|
||||||
with self.subTest("Game", game=self.game, seed=self.multiworld.seed):
|
|
||||||
distribute_items_restrictive(self.multiworld)
|
|
||||||
call_all(self.multiworld, "post_fill")
|
|
||||||
self.assertTrue(fulfills_accessibility(), "Collected all locations, but can't beat the game.")
|
|
||||||
placed_items = [loc.item for loc in self.multiworld.get_locations() if loc.item and loc.item.code]
|
|
||||||
self.assertLessEqual(len(self.multiworld.itempool), len(placed_items),
|
|
||||||
"Unplaced Items remaining in itempool")
|
|
||||||
|
|
|
@ -0,0 +1,309 @@
|
||||||
|
import typing
|
||||||
|
import unittest
|
||||||
|
from argparse import Namespace
|
||||||
|
|
||||||
|
from test.general import gen_steps
|
||||||
|
from worlds import AutoWorld
|
||||||
|
from worlds.AutoWorld import call_all
|
||||||
|
|
||||||
|
from BaseClasses import Location, MultiWorld, CollectionState, ItemClassification, Item
|
||||||
|
from worlds.alttp.Items import ItemFactory
|
||||||
|
|
||||||
|
|
||||||
|
class TestBase(unittest.TestCase):
|
||||||
|
multiworld: MultiWorld
|
||||||
|
_state_cache = {}
|
||||||
|
|
||||||
|
def get_state(self, items):
|
||||||
|
if (self.multiworld, tuple(items)) in self._state_cache:
|
||||||
|
return self._state_cache[self.multiworld, tuple(items)]
|
||||||
|
state = CollectionState(self.multiworld)
|
||||||
|
for item in items:
|
||||||
|
item.classification = ItemClassification.progression
|
||||||
|
state.collect(item, event=True)
|
||||||
|
state.sweep_for_events()
|
||||||
|
state.update_reachable_regions(1)
|
||||||
|
self._state_cache[self.multiworld, tuple(items)] = state
|
||||||
|
return state
|
||||||
|
|
||||||
|
def get_path(self, state, region):
|
||||||
|
def flist_to_iter(node):
|
||||||
|
while node:
|
||||||
|
value, node = node
|
||||||
|
yield value
|
||||||
|
|
||||||
|
from itertools import zip_longest
|
||||||
|
reversed_path_as_flist = state.path.get(region, (region, None))
|
||||||
|
string_path_flat = reversed(list(map(str, flist_to_iter(reversed_path_as_flist))))
|
||||||
|
# Now we combine the flat string list into (region, exit) pairs
|
||||||
|
pathsiter = iter(string_path_flat)
|
||||||
|
pathpairs = zip_longest(pathsiter, pathsiter)
|
||||||
|
return list(pathpairs)
|
||||||
|
|
||||||
|
def run_location_tests(self, access_pool):
|
||||||
|
for i, (location, access, *item_pool) in enumerate(access_pool):
|
||||||
|
items = item_pool[0]
|
||||||
|
all_except = item_pool[1] if len(item_pool) > 1 else None
|
||||||
|
state = self._get_items(item_pool, all_except)
|
||||||
|
path = self.get_path(state, self.multiworld.get_location(location, 1).parent_region)
|
||||||
|
with self.subTest(msg="Reach Location", location=location, access=access, items=items,
|
||||||
|
all_except=all_except, path=path, entry=i):
|
||||||
|
|
||||||
|
self.assertEqual(self.multiworld.get_location(location, 1).can_reach(state), access,
|
||||||
|
f"failed {self.multiworld.get_location(location, 1)} with: {item_pool}")
|
||||||
|
|
||||||
|
# check for partial solution
|
||||||
|
if not all_except and access: # we are not supposed to be able to reach location with partial inventory
|
||||||
|
for missing_item in item_pool[0]:
|
||||||
|
with self.subTest(msg="Location reachable without required item", location=location,
|
||||||
|
items=item_pool[0], missing_item=missing_item, entry=i):
|
||||||
|
state = self._get_items_partial(item_pool, missing_item)
|
||||||
|
|
||||||
|
self.assertEqual(self.multiworld.get_location(location, 1).can_reach(state), False,
|
||||||
|
f"failed {self.multiworld.get_location(location, 1)}: succeeded with "
|
||||||
|
f"{missing_item} removed from: {item_pool}")
|
||||||
|
|
||||||
|
def run_entrance_tests(self, access_pool):
|
||||||
|
for i, (entrance, access, *item_pool) in enumerate(access_pool):
|
||||||
|
items = item_pool[0]
|
||||||
|
all_except = item_pool[1] if len(item_pool) > 1 else None
|
||||||
|
state = self._get_items(item_pool, all_except)
|
||||||
|
path = self.get_path(state, self.multiworld.get_entrance(entrance, 1).parent_region)
|
||||||
|
with self.subTest(msg="Reach Entrance", entrance=entrance, access=access, items=items,
|
||||||
|
all_except=all_except, path=path, entry=i):
|
||||||
|
|
||||||
|
self.assertEqual(self.multiworld.get_entrance(entrance, 1).can_reach(state), access)
|
||||||
|
|
||||||
|
# check for partial solution
|
||||||
|
if not all_except and access: # we are not supposed to be able to reach location with partial inventory
|
||||||
|
for missing_item in item_pool[0]:
|
||||||
|
with self.subTest(msg="Entrance reachable without required item", entrance=entrance,
|
||||||
|
items=item_pool[0], missing_item=missing_item, entry=i):
|
||||||
|
state = self._get_items_partial(item_pool, missing_item)
|
||||||
|
self.assertEqual(self.multiworld.get_entrance(entrance, 1).can_reach(state), False,
|
||||||
|
f"failed {self.multiworld.get_entrance(entrance, 1)} with: {item_pool}")
|
||||||
|
|
||||||
|
def _get_items(self, item_pool, all_except):
|
||||||
|
if all_except and len(all_except) > 0:
|
||||||
|
items = self.multiworld.itempool[:]
|
||||||
|
items = [item for item in items if
|
||||||
|
item.name not in all_except and not ("Bottle" in item.name and "AnyBottle" in all_except)]
|
||||||
|
items.extend(ItemFactory(item_pool[0], 1))
|
||||||
|
else:
|
||||||
|
items = ItemFactory(item_pool[0], 1)
|
||||||
|
return self.get_state(items)
|
||||||
|
|
||||||
|
def _get_items_partial(self, item_pool, missing_item):
|
||||||
|
new_items = item_pool[0].copy()
|
||||||
|
new_items.remove(missing_item)
|
||||||
|
items = ItemFactory(new_items, 1)
|
||||||
|
return self.get_state(items)
|
||||||
|
|
||||||
|
|
||||||
|
class WorldTestBase(unittest.TestCase):
|
||||||
|
options: typing.Dict[str, typing.Any] = {}
|
||||||
|
multiworld: MultiWorld
|
||||||
|
|
||||||
|
game: typing.ClassVar[str] # define game name in subclass, example "Secret of Evermore"
|
||||||
|
auto_construct: typing.ClassVar[bool] = True
|
||||||
|
""" automatically set up a world for each test in this class """
|
||||||
|
|
||||||
|
def setUp(self) -> None:
|
||||||
|
if self.auto_construct:
|
||||||
|
self.world_setup()
|
||||||
|
|
||||||
|
def world_setup(self, seed: typing.Optional[int] = None) -> None:
|
||||||
|
if type(self) is WorldTestBase or \
|
||||||
|
(hasattr(WorldTestBase, self._testMethodName)
|
||||||
|
and not self.run_default_tests and
|
||||||
|
getattr(self, self._testMethodName).__code__ is
|
||||||
|
getattr(WorldTestBase, self._testMethodName, None).__code__):
|
||||||
|
return # setUp gets called for tests defined in the base class. We skip world_setup here.
|
||||||
|
if not hasattr(self, "game"):
|
||||||
|
raise NotImplementedError("didn't define game name")
|
||||||
|
self.multiworld = MultiWorld(1)
|
||||||
|
self.multiworld.game[1] = self.game
|
||||||
|
self.multiworld.player_name = {1: "Tester"}
|
||||||
|
self.multiworld.set_seed(seed)
|
||||||
|
self.multiworld.state = CollectionState(self.multiworld)
|
||||||
|
args = Namespace()
|
||||||
|
for name, option in AutoWorld.AutoWorldRegister.world_types[self.game].options_dataclass.type_hints.items():
|
||||||
|
setattr(args, name, {
|
||||||
|
1: option.from_any(self.options.get(name, getattr(option, "default")))
|
||||||
|
})
|
||||||
|
self.multiworld.set_options(args)
|
||||||
|
for step in gen_steps:
|
||||||
|
call_all(self.multiworld, step)
|
||||||
|
|
||||||
|
# methods that can be called within tests
|
||||||
|
def collect_all_but(self, item_names: typing.Union[str, typing.Iterable[str]],
|
||||||
|
state: typing.Optional[CollectionState] = None) -> None:
|
||||||
|
"""Collects all pre-placed items and items in the multiworld itempool except those provided"""
|
||||||
|
if isinstance(item_names, str):
|
||||||
|
item_names = (item_names,)
|
||||||
|
if not state:
|
||||||
|
state = self.multiworld.state
|
||||||
|
for item in self.multiworld.get_items():
|
||||||
|
if item.name not in item_names:
|
||||||
|
state.collect(item)
|
||||||
|
|
||||||
|
def get_item_by_name(self, item_name: str) -> Item:
|
||||||
|
"""Returns the first item found in placed items, or in the itempool with the matching name"""
|
||||||
|
for item in self.multiworld.get_items():
|
||||||
|
if item.name == item_name:
|
||||||
|
return item
|
||||||
|
raise ValueError("No such item")
|
||||||
|
|
||||||
|
def get_items_by_name(self, item_names: typing.Union[str, typing.Iterable[str]]) -> typing.List[Item]:
|
||||||
|
"""Returns actual items from the itempool that match the provided name(s)"""
|
||||||
|
if isinstance(item_names, str):
|
||||||
|
item_names = (item_names,)
|
||||||
|
return [item for item in self.multiworld.itempool if item.name in item_names]
|
||||||
|
|
||||||
|
def collect_by_name(self, item_names: typing.Union[str, typing.Iterable[str]]) -> typing.List[Item]:
|
||||||
|
""" collect all of the items in the item pool that have the given names """
|
||||||
|
items = self.get_items_by_name(item_names)
|
||||||
|
self.collect(items)
|
||||||
|
return items
|
||||||
|
|
||||||
|
def collect(self, items: typing.Union[Item, typing.Iterable[Item]]) -> None:
|
||||||
|
"""Collects the provided item(s) into state"""
|
||||||
|
if isinstance(items, Item):
|
||||||
|
items = (items,)
|
||||||
|
for item in items:
|
||||||
|
self.multiworld.state.collect(item)
|
||||||
|
|
||||||
|
def remove_by_name(self, item_names: typing.Union[str, typing.Iterable[str]]) -> typing.List[Item]:
|
||||||
|
"""Remove all of the items in the item pool with the given names from state"""
|
||||||
|
items = self.get_items_by_name(item_names)
|
||||||
|
self.remove(items)
|
||||||
|
return items
|
||||||
|
|
||||||
|
def remove(self, items: typing.Union[Item, typing.Iterable[Item]]) -> None:
|
||||||
|
"""Removes the provided item(s) from state"""
|
||||||
|
if isinstance(items, Item):
|
||||||
|
items = (items,)
|
||||||
|
for item in items:
|
||||||
|
if item.location and item.location.event and item.location in self.multiworld.state.events:
|
||||||
|
self.multiworld.state.events.remove(item.location)
|
||||||
|
self.multiworld.state.remove(item)
|
||||||
|
|
||||||
|
def can_reach_location(self, location: str) -> bool:
|
||||||
|
"""Determines if the current state can reach the provided location name"""
|
||||||
|
return self.multiworld.state.can_reach(location, "Location", 1)
|
||||||
|
|
||||||
|
def can_reach_entrance(self, entrance: str) -> bool:
|
||||||
|
"""Determines if the current state can reach the provided entrance name"""
|
||||||
|
return self.multiworld.state.can_reach(entrance, "Entrance", 1)
|
||||||
|
|
||||||
|
def can_reach_region(self, region: str) -> bool:
|
||||||
|
"""Determines if the current state can reach the provided region name"""
|
||||||
|
return self.multiworld.state.can_reach(region, "Region", 1)
|
||||||
|
|
||||||
|
def count(self, item_name: str) -> int:
|
||||||
|
"""Returns the amount of an item currently in state"""
|
||||||
|
return self.multiworld.state.count(item_name, 1)
|
||||||
|
|
||||||
|
def assertAccessDependency(self,
|
||||||
|
locations: typing.List[str],
|
||||||
|
possible_items: typing.Iterable[typing.Iterable[str]],
|
||||||
|
only_check_listed: bool = False) -> None:
|
||||||
|
"""Asserts that the provided locations can't be reached without the listed items but can be reached with any
|
||||||
|
one of the provided combinations"""
|
||||||
|
all_items = [item_name for item_names in possible_items for item_name in item_names]
|
||||||
|
|
||||||
|
state = CollectionState(self.multiworld)
|
||||||
|
self.collect_all_but(all_items, state)
|
||||||
|
if only_check_listed:
|
||||||
|
for location in locations:
|
||||||
|
self.assertFalse(state.can_reach(location, "Location", 1), f"{location} is reachable without {all_items}")
|
||||||
|
else:
|
||||||
|
for location in self.multiworld.get_locations():
|
||||||
|
loc_reachable = state.can_reach(location, "Location", 1)
|
||||||
|
self.assertEqual(loc_reachable, location.name not in locations,
|
||||||
|
f"{location.name} is reachable without {all_items}" if loc_reachable
|
||||||
|
else f"{location.name} is not reachable without {all_items}")
|
||||||
|
for item_names in possible_items:
|
||||||
|
items = self.get_items_by_name(item_names)
|
||||||
|
for item in items:
|
||||||
|
state.collect(item)
|
||||||
|
for location in locations:
|
||||||
|
self.assertTrue(state.can_reach(location, "Location", 1),
|
||||||
|
f"{location} not reachable with {item_names}")
|
||||||
|
for item in items:
|
||||||
|
state.remove(item)
|
||||||
|
|
||||||
|
def assertBeatable(self, beatable: bool):
|
||||||
|
"""Asserts that the game can be beaten with the current state"""
|
||||||
|
self.assertEqual(self.multiworld.can_beat_game(self.multiworld.state), beatable)
|
||||||
|
|
||||||
|
# following tests are automatically run
|
||||||
|
@property
|
||||||
|
def run_default_tests(self) -> bool:
|
||||||
|
"""Not possible or identical to the base test that's always being run already"""
|
||||||
|
return (self.options
|
||||||
|
or self.setUp.__code__ is not WorldTestBase.setUp.__code__
|
||||||
|
or self.world_setup.__code__ is not WorldTestBase.world_setup.__code__)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def constructed(self) -> bool:
|
||||||
|
"""A multiworld has been constructed by this point"""
|
||||||
|
return hasattr(self, "game") and hasattr(self, "multiworld")
|
||||||
|
|
||||||
|
def test_all_state_can_reach_everything(self):
|
||||||
|
"""Ensure all state can reach everything and complete the game with the defined options"""
|
||||||
|
if not (self.run_default_tests and self.constructed):
|
||||||
|
return
|
||||||
|
with self.subTest("Game", game=self.game):
|
||||||
|
excluded = self.multiworld.exclude_locations[1].value
|
||||||
|
state = self.multiworld.get_all_state(False)
|
||||||
|
for location in self.multiworld.get_locations():
|
||||||
|
if location.name not in excluded:
|
||||||
|
with self.subTest("Location should be reached", location=location):
|
||||||
|
reachable = location.can_reach(state)
|
||||||
|
self.assertTrue(reachable, f"{location.name} unreachable")
|
||||||
|
with self.subTest("Beatable"):
|
||||||
|
self.multiworld.state = state
|
||||||
|
self.assertBeatable(True)
|
||||||
|
|
||||||
|
def test_empty_state_can_reach_something(self):
|
||||||
|
"""Ensure empty state can reach at least one location with the defined options"""
|
||||||
|
if not (self.run_default_tests and self.constructed):
|
||||||
|
return
|
||||||
|
with self.subTest("Game", game=self.game):
|
||||||
|
state = CollectionState(self.multiworld)
|
||||||
|
locations = self.multiworld.get_reachable_locations(state, 1)
|
||||||
|
self.assertGreater(len(locations), 0,
|
||||||
|
"Need to be able to reach at least one location to get started.")
|
||||||
|
|
||||||
|
def test_fill(self):
|
||||||
|
"""Generates a multiworld and validates placements with the defined options"""
|
||||||
|
if not (self.run_default_tests and self.constructed):
|
||||||
|
return
|
||||||
|
from Fill import distribute_items_restrictive
|
||||||
|
|
||||||
|
# basically a shortened reimplementation of this method from core, in order to force the check is done
|
||||||
|
def fulfills_accessibility() -> bool:
|
||||||
|
locations = self.multiworld.get_locations(1).copy()
|
||||||
|
state = CollectionState(self.multiworld)
|
||||||
|
while locations:
|
||||||
|
sphere: typing.List[Location] = []
|
||||||
|
for n in range(len(locations) - 1, -1, -1):
|
||||||
|
if locations[n].can_reach(state):
|
||||||
|
sphere.append(locations.pop(n))
|
||||||
|
self.assertTrue(sphere or self.multiworld.accessibility[1] == "minimal",
|
||||||
|
f"Unreachable locations: {locations}")
|
||||||
|
if not sphere:
|
||||||
|
break
|
||||||
|
for location in sphere:
|
||||||
|
if location.item:
|
||||||
|
state.collect(location.item, True, location)
|
||||||
|
return self.multiworld.has_beaten_game(state, 1)
|
||||||
|
|
||||||
|
with self.subTest("Game", game=self.game, seed=self.multiworld.seed):
|
||||||
|
distribute_items_restrictive(self.multiworld)
|
||||||
|
call_all(self.multiworld, "post_fill")
|
||||||
|
self.assertTrue(fulfills_accessibility(), "Collected all locations, but can't beat the game.")
|
||||||
|
placed_items = [loc.item for loc in self.multiworld.get_locations() if loc.item and loc.item.code]
|
||||||
|
self.assertLessEqual(len(self.multiworld.itempool), len(placed_items),
|
||||||
|
"Unplaced Items remaining in itempool")
|
|
@ -8,6 +8,13 @@ gen_steps = ("generate_early", "create_regions", "create_items", "set_rules", "g
|
||||||
|
|
||||||
|
|
||||||
def setup_solo_multiworld(world_type: Type[World], steps: Tuple[str, ...] = gen_steps) -> MultiWorld:
|
def setup_solo_multiworld(world_type: Type[World], steps: Tuple[str, ...] = gen_steps) -> MultiWorld:
|
||||||
|
"""
|
||||||
|
Creates a multiworld with a single player of `world_type`, sets default options, and calls provided gen steps.
|
||||||
|
|
||||||
|
:param world_type: Type of the world to generate a multiworld for
|
||||||
|
:param steps: The gen steps that should be called on the generated multiworld before returning. Default calls
|
||||||
|
steps through pre_fill
|
||||||
|
"""
|
||||||
multiworld = MultiWorld(1)
|
multiworld = MultiWorld(1)
|
||||||
multiworld.game[1] = world_type.game
|
multiworld.game[1] = world_type.game
|
||||||
multiworld.player_name = {1: "Tester"}
|
multiworld.player_name = {1: "Tester"}
|
||||||
|
|
|
@ -72,7 +72,7 @@ class PlayerDefinition(object):
|
||||||
return region
|
return region
|
||||||
|
|
||||||
|
|
||||||
def fillRegion(world: MultiWorld, region: Region, items: List[Item]) -> List[Item]:
|
def fill_region(world: MultiWorld, region: Region, items: List[Item]) -> List[Item]:
|
||||||
items = items.copy()
|
items = items.copy()
|
||||||
while len(items) > 0:
|
while len(items) > 0:
|
||||||
location = region.locations.pop(0)
|
location = region.locations.pop(0)
|
||||||
|
@ -86,7 +86,7 @@ def fillRegion(world: MultiWorld, region: Region, items: List[Item]) -> List[Ite
|
||||||
return items
|
return items
|
||||||
|
|
||||||
|
|
||||||
def regionContains(region: Region, item: Item) -> bool:
|
def region_contains(region: Region, item: Item) -> bool:
|
||||||
for location in region.locations:
|
for location in region.locations:
|
||||||
if location.item == item:
|
if location.item == item:
|
||||||
return True
|
return True
|
||||||
|
@ -133,6 +133,7 @@ def names(objs: list) -> Iterable[str]:
|
||||||
|
|
||||||
class TestFillRestrictive(unittest.TestCase):
|
class TestFillRestrictive(unittest.TestCase):
|
||||||
def test_basic_fill(self):
|
def test_basic_fill(self):
|
||||||
|
"""Tests `fill_restrictive` fills and removes the locations and items from their respective lists"""
|
||||||
multi_world = generate_multi_world()
|
multi_world = generate_multi_world()
|
||||||
player1 = generate_player_data(multi_world, 1, 2, 2)
|
player1 = generate_player_data(multi_world, 1, 2, 2)
|
||||||
|
|
||||||
|
@ -150,6 +151,7 @@ class TestFillRestrictive(unittest.TestCase):
|
||||||
self.assertEqual([], player1.prog_items)
|
self.assertEqual([], player1.prog_items)
|
||||||
|
|
||||||
def test_ordered_fill(self):
|
def test_ordered_fill(self):
|
||||||
|
"""Tests `fill_restrictive` fulfills set rules"""
|
||||||
multi_world = generate_multi_world()
|
multi_world = generate_multi_world()
|
||||||
player1 = generate_player_data(multi_world, 1, 2, 2)
|
player1 = generate_player_data(multi_world, 1, 2, 2)
|
||||||
items = player1.prog_items
|
items = player1.prog_items
|
||||||
|
@ -166,6 +168,7 @@ class TestFillRestrictive(unittest.TestCase):
|
||||||
self.assertEqual(locations[1].item, items[1])
|
self.assertEqual(locations[1].item, items[1])
|
||||||
|
|
||||||
def test_partial_fill(self):
|
def test_partial_fill(self):
|
||||||
|
"""Tests that `fill_restrictive` returns unfilled locations"""
|
||||||
multi_world = generate_multi_world()
|
multi_world = generate_multi_world()
|
||||||
player1 = generate_player_data(multi_world, 1, 3, 2)
|
player1 = generate_player_data(multi_world, 1, 3, 2)
|
||||||
|
|
||||||
|
@ -191,6 +194,7 @@ class TestFillRestrictive(unittest.TestCase):
|
||||||
self.assertEqual(player1.locations[0], loc2)
|
self.assertEqual(player1.locations[0], loc2)
|
||||||
|
|
||||||
def test_minimal_fill(self):
|
def test_minimal_fill(self):
|
||||||
|
"""Test that fill for minimal player can have unreachable items"""
|
||||||
multi_world = generate_multi_world()
|
multi_world = generate_multi_world()
|
||||||
player1 = generate_player_data(multi_world, 1, 2, 2)
|
player1 = generate_player_data(multi_world, 1, 2, 2)
|
||||||
|
|
||||||
|
@ -246,6 +250,7 @@ class TestFillRestrictive(unittest.TestCase):
|
||||||
f'{item} is unreachable in {item.location}')
|
f'{item} is unreachable in {item.location}')
|
||||||
|
|
||||||
def test_reversed_fill(self):
|
def test_reversed_fill(self):
|
||||||
|
"""Test a different set of rules can be satisfied"""
|
||||||
multi_world = generate_multi_world()
|
multi_world = generate_multi_world()
|
||||||
player1 = generate_player_data(multi_world, 1, 2, 2)
|
player1 = generate_player_data(multi_world, 1, 2, 2)
|
||||||
|
|
||||||
|
@ -264,6 +269,7 @@ class TestFillRestrictive(unittest.TestCase):
|
||||||
self.assertEqual(loc1.item, item0)
|
self.assertEqual(loc1.item, item0)
|
||||||
|
|
||||||
def test_multi_step_fill(self):
|
def test_multi_step_fill(self):
|
||||||
|
"""Test that fill is able to satisfy multiple spheres"""
|
||||||
multi_world = generate_multi_world()
|
multi_world = generate_multi_world()
|
||||||
player1 = generate_player_data(multi_world, 1, 4, 4)
|
player1 = generate_player_data(multi_world, 1, 4, 4)
|
||||||
|
|
||||||
|
@ -288,6 +294,7 @@ class TestFillRestrictive(unittest.TestCase):
|
||||||
self.assertEqual(locations[3].item, items[3])
|
self.assertEqual(locations[3].item, items[3])
|
||||||
|
|
||||||
def test_impossible_fill(self):
|
def test_impossible_fill(self):
|
||||||
|
"""Test that fill raises an error when it can't place any items"""
|
||||||
multi_world = generate_multi_world()
|
multi_world = generate_multi_world()
|
||||||
player1 = generate_player_data(multi_world, 1, 2, 2)
|
player1 = generate_player_data(multi_world, 1, 2, 2)
|
||||||
items = player1.prog_items
|
items = player1.prog_items
|
||||||
|
@ -304,6 +311,7 @@ class TestFillRestrictive(unittest.TestCase):
|
||||||
player1.locations.copy(), player1.prog_items.copy())
|
player1.locations.copy(), player1.prog_items.copy())
|
||||||
|
|
||||||
def test_circular_fill(self):
|
def test_circular_fill(self):
|
||||||
|
"""Test that fill raises an error when it can't place all items"""
|
||||||
multi_world = generate_multi_world()
|
multi_world = generate_multi_world()
|
||||||
player1 = generate_player_data(multi_world, 1, 3, 3)
|
player1 = generate_player_data(multi_world, 1, 3, 3)
|
||||||
|
|
||||||
|
@ -324,6 +332,7 @@ class TestFillRestrictive(unittest.TestCase):
|
||||||
player1.locations.copy(), player1.prog_items.copy())
|
player1.locations.copy(), player1.prog_items.copy())
|
||||||
|
|
||||||
def test_competing_fill(self):
|
def test_competing_fill(self):
|
||||||
|
"""Test that fill raises an error when it can't place items in a way to satisfy the conditions"""
|
||||||
multi_world = generate_multi_world()
|
multi_world = generate_multi_world()
|
||||||
player1 = generate_player_data(multi_world, 1, 2, 2)
|
player1 = generate_player_data(multi_world, 1, 2, 2)
|
||||||
|
|
||||||
|
@ -340,6 +349,7 @@ class TestFillRestrictive(unittest.TestCase):
|
||||||
player1.locations.copy(), player1.prog_items.copy())
|
player1.locations.copy(), player1.prog_items.copy())
|
||||||
|
|
||||||
def test_multiplayer_fill(self):
|
def test_multiplayer_fill(self):
|
||||||
|
"""Test that items can be placed across worlds"""
|
||||||
multi_world = generate_multi_world(2)
|
multi_world = generate_multi_world(2)
|
||||||
player1 = generate_player_data(multi_world, 1, 2, 2)
|
player1 = generate_player_data(multi_world, 1, 2, 2)
|
||||||
player2 = generate_player_data(multi_world, 2, 2, 2)
|
player2 = generate_player_data(multi_world, 2, 2, 2)
|
||||||
|
@ -360,6 +370,7 @@ class TestFillRestrictive(unittest.TestCase):
|
||||||
self.assertEqual(player2.locations[1].item, player2.prog_items[0])
|
self.assertEqual(player2.locations[1].item, player2.prog_items[0])
|
||||||
|
|
||||||
def test_multiplayer_rules_fill(self):
|
def test_multiplayer_rules_fill(self):
|
||||||
|
"""Test that fill across worlds satisfies the rules"""
|
||||||
multi_world = generate_multi_world(2)
|
multi_world = generate_multi_world(2)
|
||||||
player1 = generate_player_data(multi_world, 1, 2, 2)
|
player1 = generate_player_data(multi_world, 1, 2, 2)
|
||||||
player2 = generate_player_data(multi_world, 2, 2, 2)
|
player2 = generate_player_data(multi_world, 2, 2, 2)
|
||||||
|
@ -383,6 +394,7 @@ class TestFillRestrictive(unittest.TestCase):
|
||||||
self.assertEqual(player2.locations[1].item, player1.prog_items[1])
|
self.assertEqual(player2.locations[1].item, player1.prog_items[1])
|
||||||
|
|
||||||
def test_restrictive_progress(self):
|
def test_restrictive_progress(self):
|
||||||
|
"""Test that various spheres with different requirements can be filled"""
|
||||||
multi_world = generate_multi_world()
|
multi_world = generate_multi_world()
|
||||||
player1 = generate_player_data(multi_world, 1, prog_item_count=25)
|
player1 = generate_player_data(multi_world, 1, prog_item_count=25)
|
||||||
items = player1.prog_items.copy()
|
items = player1.prog_items.copy()
|
||||||
|
@ -405,6 +417,7 @@ class TestFillRestrictive(unittest.TestCase):
|
||||||
locations, player1.prog_items)
|
locations, player1.prog_items)
|
||||||
|
|
||||||
def test_swap_to_earlier_location_with_item_rule(self):
|
def test_swap_to_earlier_location_with_item_rule(self):
|
||||||
|
"""Test that item swap happens and works as intended"""
|
||||||
# test for PR#1109
|
# test for PR#1109
|
||||||
multi_world = generate_multi_world(1)
|
multi_world = generate_multi_world(1)
|
||||||
player1 = generate_player_data(multi_world, 1, 4, 4)
|
player1 = generate_player_data(multi_world, 1, 4, 4)
|
||||||
|
@ -430,6 +443,7 @@ class TestFillRestrictive(unittest.TestCase):
|
||||||
self.assertEqual(sphere1_loc.item, allowed_item, "Wrong item in Sphere 1")
|
self.assertEqual(sphere1_loc.item, allowed_item, "Wrong item in Sphere 1")
|
||||||
|
|
||||||
def test_double_sweep(self):
|
def test_double_sweep(self):
|
||||||
|
"""Test that sweep doesn't duplicate Event items when sweeping"""
|
||||||
# test for PR1114
|
# test for PR1114
|
||||||
multi_world = generate_multi_world(1)
|
multi_world = generate_multi_world(1)
|
||||||
player1 = generate_player_data(multi_world, 1, 1, 1)
|
player1 = generate_player_data(multi_world, 1, 1, 1)
|
||||||
|
@ -445,6 +459,7 @@ class TestFillRestrictive(unittest.TestCase):
|
||||||
self.assertEqual(multi_world.state.prog_items[item.name, item.player], 1, "Sweep collected multiple times")
|
self.assertEqual(multi_world.state.prog_items[item.name, item.player], 1, "Sweep collected multiple times")
|
||||||
|
|
||||||
def test_correct_item_instance_removed_from_pool(self):
|
def test_correct_item_instance_removed_from_pool(self):
|
||||||
|
"""Test that a placed item gets removed from the submitted pool"""
|
||||||
multi_world = generate_multi_world()
|
multi_world = generate_multi_world()
|
||||||
player1 = generate_player_data(multi_world, 1, 2, 2)
|
player1 = generate_player_data(multi_world, 1, 2, 2)
|
||||||
|
|
||||||
|
@ -461,6 +476,7 @@ class TestFillRestrictive(unittest.TestCase):
|
||||||
|
|
||||||
class TestDistributeItemsRestrictive(unittest.TestCase):
|
class TestDistributeItemsRestrictive(unittest.TestCase):
|
||||||
def test_basic_distribute(self):
|
def test_basic_distribute(self):
|
||||||
|
"""Test that distribute_items_restrictive is deterministic"""
|
||||||
multi_world = generate_multi_world()
|
multi_world = generate_multi_world()
|
||||||
player1 = generate_player_data(
|
player1 = generate_player_data(
|
||||||
multi_world, 1, 4, prog_item_count=2, basic_item_count=2)
|
multi_world, 1, 4, prog_item_count=2, basic_item_count=2)
|
||||||
|
@ -480,6 +496,7 @@ class TestDistributeItemsRestrictive(unittest.TestCase):
|
||||||
self.assertFalse(locations[3].event)
|
self.assertFalse(locations[3].event)
|
||||||
|
|
||||||
def test_excluded_distribute(self):
|
def test_excluded_distribute(self):
|
||||||
|
"""Test that distribute_items_restrictive doesn't put advancement items on excluded locations"""
|
||||||
multi_world = generate_multi_world()
|
multi_world = generate_multi_world()
|
||||||
player1 = generate_player_data(
|
player1 = generate_player_data(
|
||||||
multi_world, 1, 4, prog_item_count=2, basic_item_count=2)
|
multi_world, 1, 4, prog_item_count=2, basic_item_count=2)
|
||||||
|
@ -494,6 +511,7 @@ class TestDistributeItemsRestrictive(unittest.TestCase):
|
||||||
self.assertFalse(locations[2].item.advancement)
|
self.assertFalse(locations[2].item.advancement)
|
||||||
|
|
||||||
def test_non_excluded_item_distribute(self):
|
def test_non_excluded_item_distribute(self):
|
||||||
|
"""Test that useful items aren't placed on excluded locations"""
|
||||||
multi_world = generate_multi_world()
|
multi_world = generate_multi_world()
|
||||||
player1 = generate_player_data(
|
player1 = generate_player_data(
|
||||||
multi_world, 1, 4, prog_item_count=2, basic_item_count=2)
|
multi_world, 1, 4, prog_item_count=2, basic_item_count=2)
|
||||||
|
@ -508,6 +526,7 @@ class TestDistributeItemsRestrictive(unittest.TestCase):
|
||||||
self.assertEqual(locations[1].item, basic_items[0])
|
self.assertEqual(locations[1].item, basic_items[0])
|
||||||
|
|
||||||
def test_too_many_excluded_distribute(self):
|
def test_too_many_excluded_distribute(self):
|
||||||
|
"""Test that fill fails if it can't place all progression items due to too many excluded locations"""
|
||||||
multi_world = generate_multi_world()
|
multi_world = generate_multi_world()
|
||||||
player1 = generate_player_data(
|
player1 = generate_player_data(
|
||||||
multi_world, 1, 4, prog_item_count=2, basic_item_count=2)
|
multi_world, 1, 4, prog_item_count=2, basic_item_count=2)
|
||||||
|
@ -520,6 +539,7 @@ class TestDistributeItemsRestrictive(unittest.TestCase):
|
||||||
self.assertRaises(FillError, distribute_items_restrictive, multi_world)
|
self.assertRaises(FillError, distribute_items_restrictive, multi_world)
|
||||||
|
|
||||||
def test_non_excluded_item_must_distribute(self):
|
def test_non_excluded_item_must_distribute(self):
|
||||||
|
"""Test that fill fails if it can't place useful items due to too many excluded locations"""
|
||||||
multi_world = generate_multi_world()
|
multi_world = generate_multi_world()
|
||||||
player1 = generate_player_data(
|
player1 = generate_player_data(
|
||||||
multi_world, 1, 4, prog_item_count=2, basic_item_count=2)
|
multi_world, 1, 4, prog_item_count=2, basic_item_count=2)
|
||||||
|
@ -534,6 +554,7 @@ class TestDistributeItemsRestrictive(unittest.TestCase):
|
||||||
self.assertRaises(FillError, distribute_items_restrictive, multi_world)
|
self.assertRaises(FillError, distribute_items_restrictive, multi_world)
|
||||||
|
|
||||||
def test_priority_distribute(self):
|
def test_priority_distribute(self):
|
||||||
|
"""Test that priority locations receive advancement items"""
|
||||||
multi_world = generate_multi_world()
|
multi_world = generate_multi_world()
|
||||||
player1 = generate_player_data(
|
player1 = generate_player_data(
|
||||||
multi_world, 1, 4, prog_item_count=2, basic_item_count=2)
|
multi_world, 1, 4, prog_item_count=2, basic_item_count=2)
|
||||||
|
@ -548,6 +569,7 @@ class TestDistributeItemsRestrictive(unittest.TestCase):
|
||||||
self.assertTrue(locations[3].item.advancement)
|
self.assertTrue(locations[3].item.advancement)
|
||||||
|
|
||||||
def test_excess_priority_distribute(self):
|
def test_excess_priority_distribute(self):
|
||||||
|
"""Test that if there's more priority locations than advancement items, they can still fill"""
|
||||||
multi_world = generate_multi_world()
|
multi_world = generate_multi_world()
|
||||||
player1 = generate_player_data(
|
player1 = generate_player_data(
|
||||||
multi_world, 1, 4, prog_item_count=2, basic_item_count=2)
|
multi_world, 1, 4, prog_item_count=2, basic_item_count=2)
|
||||||
|
@ -562,6 +584,7 @@ class TestDistributeItemsRestrictive(unittest.TestCase):
|
||||||
self.assertFalse(locations[3].item.advancement)
|
self.assertFalse(locations[3].item.advancement)
|
||||||
|
|
||||||
def test_multiple_world_priority_distribute(self):
|
def test_multiple_world_priority_distribute(self):
|
||||||
|
"""Test that priority fill can be satisfied for multiple worlds"""
|
||||||
multi_world = generate_multi_world(3)
|
multi_world = generate_multi_world(3)
|
||||||
player1 = generate_player_data(
|
player1 = generate_player_data(
|
||||||
multi_world, 1, 4, prog_item_count=2, basic_item_count=2)
|
multi_world, 1, 4, prog_item_count=2, basic_item_count=2)
|
||||||
|
@ -591,7 +614,7 @@ class TestDistributeItemsRestrictive(unittest.TestCase):
|
||||||
self.assertTrue(player3.locations[3].item.advancement)
|
self.assertTrue(player3.locations[3].item.advancement)
|
||||||
|
|
||||||
def test_can_remove_locations_in_fill_hook(self):
|
def test_can_remove_locations_in_fill_hook(self):
|
||||||
|
"""Test that distribute_items_restrictive calls the fill hook and allows for item and location removal"""
|
||||||
multi_world = generate_multi_world()
|
multi_world = generate_multi_world()
|
||||||
player1 = generate_player_data(
|
player1 = generate_player_data(
|
||||||
multi_world, 1, 4, prog_item_count=2, basic_item_count=2)
|
multi_world, 1, 4, prog_item_count=2, basic_item_count=2)
|
||||||
|
@ -611,6 +634,7 @@ class TestDistributeItemsRestrictive(unittest.TestCase):
|
||||||
self.assertIsNone(removed_location[0].item)
|
self.assertIsNone(removed_location[0].item)
|
||||||
|
|
||||||
def test_seed_robust_to_item_order(self):
|
def test_seed_robust_to_item_order(self):
|
||||||
|
"""Test deterministic fill"""
|
||||||
mw1 = generate_multi_world()
|
mw1 = generate_multi_world()
|
||||||
gen1 = generate_player_data(
|
gen1 = generate_player_data(
|
||||||
mw1, 1, 4, prog_item_count=2, basic_item_count=2)
|
mw1, 1, 4, prog_item_count=2, basic_item_count=2)
|
||||||
|
@ -628,6 +652,7 @@ class TestDistributeItemsRestrictive(unittest.TestCase):
|
||||||
self.assertEqual(gen1.locations[3].item, gen2.locations[3].item)
|
self.assertEqual(gen1.locations[3].item, gen2.locations[3].item)
|
||||||
|
|
||||||
def test_seed_robust_to_location_order(self):
|
def test_seed_robust_to_location_order(self):
|
||||||
|
"""Test deterministic fill even if locations in a region are reordered"""
|
||||||
mw1 = generate_multi_world()
|
mw1 = generate_multi_world()
|
||||||
gen1 = generate_player_data(
|
gen1 = generate_player_data(
|
||||||
mw1, 1, 4, prog_item_count=2, basic_item_count=2)
|
mw1, 1, 4, prog_item_count=2, basic_item_count=2)
|
||||||
|
@ -646,6 +671,7 @@ class TestDistributeItemsRestrictive(unittest.TestCase):
|
||||||
self.assertEqual(gen1.locations[3].item, gen2.locations[3].item)
|
self.assertEqual(gen1.locations[3].item, gen2.locations[3].item)
|
||||||
|
|
||||||
def test_can_reserve_advancement_items_for_general_fill(self):
|
def test_can_reserve_advancement_items_for_general_fill(self):
|
||||||
|
"""Test that priority locations fill still satisfies item rules"""
|
||||||
multi_world = generate_multi_world()
|
multi_world = generate_multi_world()
|
||||||
player1 = generate_player_data(
|
player1 = generate_player_data(
|
||||||
multi_world, 1, location_count=5, prog_item_count=5)
|
multi_world, 1, location_count=5, prog_item_count=5)
|
||||||
|
@ -655,14 +681,14 @@ class TestDistributeItemsRestrictive(unittest.TestCase):
|
||||||
|
|
||||||
location = player1.locations[0]
|
location = player1.locations[0]
|
||||||
location.progress_type = LocationProgressType.PRIORITY
|
location.progress_type = LocationProgressType.PRIORITY
|
||||||
location.item_rule = lambda item: item != items[
|
location.item_rule = lambda item: item not in items[:4]
|
||||||
0] and item != items[1] and item != items[2] and item != items[3]
|
|
||||||
|
|
||||||
distribute_items_restrictive(multi_world)
|
distribute_items_restrictive(multi_world)
|
||||||
|
|
||||||
self.assertEqual(location.item, items[4])
|
self.assertEqual(location.item, items[4])
|
||||||
|
|
||||||
def test_non_excluded_local_items(self):
|
def test_non_excluded_local_items(self):
|
||||||
|
"""Test that local items get placed locally in a multiworld"""
|
||||||
multi_world = generate_multi_world(2)
|
multi_world = generate_multi_world(2)
|
||||||
player1 = generate_player_data(
|
player1 = generate_player_data(
|
||||||
multi_world, 1, location_count=5, basic_item_count=5)
|
multi_world, 1, location_count=5, basic_item_count=5)
|
||||||
|
@ -683,6 +709,7 @@ class TestDistributeItemsRestrictive(unittest.TestCase):
|
||||||
self.assertFalse(item.location.event, False)
|
self.assertFalse(item.location.event, False)
|
||||||
|
|
||||||
def test_early_items(self) -> None:
|
def test_early_items(self) -> None:
|
||||||
|
"""Test that the early items API successfully places items early"""
|
||||||
mw = generate_multi_world(2)
|
mw = generate_multi_world(2)
|
||||||
player1 = generate_player_data(mw, 1, location_count=5, basic_item_count=5)
|
player1 = generate_player_data(mw, 1, location_count=5, basic_item_count=5)
|
||||||
player2 = generate_player_data(mw, 2, location_count=5, basic_item_count=5)
|
player2 = generate_player_data(mw, 2, location_count=5, basic_item_count=5)
|
||||||
|
@ -762,21 +789,22 @@ class TestBalanceMultiworldProgression(unittest.TestCase):
|
||||||
|
|
||||||
# Sphere 1
|
# Sphere 1
|
||||||
region = player1.generate_region(player1.menu, 20)
|
region = player1.generate_region(player1.menu, 20)
|
||||||
items = fillRegion(multi_world, region, [
|
items = fill_region(multi_world, region, [
|
||||||
player1.prog_items[0]] + items)
|
player1.prog_items[0]] + items)
|
||||||
|
|
||||||
# Sphere 2
|
# Sphere 2
|
||||||
region = player1.generate_region(
|
region = player1.generate_region(
|
||||||
player1.regions[1], 20, lambda state: state.has(player1.prog_items[0].name, player1.id))
|
player1.regions[1], 20, lambda state: state.has(player1.prog_items[0].name, player1.id))
|
||||||
items = fillRegion(
|
items = fill_region(
|
||||||
multi_world, region, [player1.prog_items[1], player2.prog_items[0]] + items)
|
multi_world, region, [player1.prog_items[1], player2.prog_items[0]] + items)
|
||||||
|
|
||||||
# Sphere 3
|
# Sphere 3
|
||||||
region = player2.generate_region(
|
region = player2.generate_region(
|
||||||
player2.menu, 20, lambda state: state.has(player2.prog_items[0].name, player2.id))
|
player2.menu, 20, lambda state: state.has(player2.prog_items[0].name, player2.id))
|
||||||
fillRegion(multi_world, region, [player2.prog_items[1]] + items)
|
fill_region(multi_world, region, [player2.prog_items[1]] + items)
|
||||||
|
|
||||||
def test_balances_progression(self) -> None:
|
def test_balances_progression(self) -> None:
|
||||||
|
"""Tests that progression balancing moves progression items earlier"""
|
||||||
self.multi_world.progression_balancing[self.player1.id].value = 50
|
self.multi_world.progression_balancing[self.player1.id].value = 50
|
||||||
self.multi_world.progression_balancing[self.player2.id].value = 50
|
self.multi_world.progression_balancing[self.player2.id].value = 50
|
||||||
|
|
||||||
|
@ -789,6 +817,7 @@ class TestBalanceMultiworldProgression(unittest.TestCase):
|
||||||
self.player1.regions[1], self.player2.prog_items[0])
|
self.player1.regions[1], self.player2.prog_items[0])
|
||||||
|
|
||||||
def test_balances_progression_light(self) -> None:
|
def test_balances_progression_light(self) -> None:
|
||||||
|
"""Test that progression balancing still moves items earlier on minimum value"""
|
||||||
self.multi_world.progression_balancing[self.player1.id].value = 1
|
self.multi_world.progression_balancing[self.player1.id].value = 1
|
||||||
self.multi_world.progression_balancing[self.player2.id].value = 1
|
self.multi_world.progression_balancing[self.player2.id].value = 1
|
||||||
|
|
||||||
|
@ -802,6 +831,7 @@ class TestBalanceMultiworldProgression(unittest.TestCase):
|
||||||
self.player1.regions[1], self.player2.prog_items[0])
|
self.player1.regions[1], self.player2.prog_items[0])
|
||||||
|
|
||||||
def test_balances_progression_heavy(self) -> None:
|
def test_balances_progression_heavy(self) -> None:
|
||||||
|
"""Test that progression balancing moves items earlier on maximum value"""
|
||||||
self.multi_world.progression_balancing[self.player1.id].value = 99
|
self.multi_world.progression_balancing[self.player1.id].value = 99
|
||||||
self.multi_world.progression_balancing[self.player2.id].value = 99
|
self.multi_world.progression_balancing[self.player2.id].value = 99
|
||||||
|
|
||||||
|
@ -815,6 +845,7 @@ class TestBalanceMultiworldProgression(unittest.TestCase):
|
||||||
self.player1.regions[1], self.player2.prog_items[0])
|
self.player1.regions[1], self.player2.prog_items[0])
|
||||||
|
|
||||||
def test_skips_balancing_progression(self) -> None:
|
def test_skips_balancing_progression(self) -> None:
|
||||||
|
"""Test that progression balancing is skipped when players have it disabled"""
|
||||||
self.multi_world.progression_balancing[self.player1.id].value = 0
|
self.multi_world.progression_balancing[self.player1.id].value = 0
|
||||||
self.multi_world.progression_balancing[self.player2.id].value = 0
|
self.multi_world.progression_balancing[self.player2.id].value = 0
|
||||||
|
|
||||||
|
@ -827,6 +858,7 @@ class TestBalanceMultiworldProgression(unittest.TestCase):
|
||||||
self.player1.regions[2], self.player2.prog_items[0])
|
self.player1.regions[2], self.player2.prog_items[0])
|
||||||
|
|
||||||
def test_ignores_priority_locations(self) -> None:
|
def test_ignores_priority_locations(self) -> None:
|
||||||
|
"""Test that progression items on priority locations don't get moved by balancing"""
|
||||||
self.multi_world.progression_balancing[self.player1.id].value = 50
|
self.multi_world.progression_balancing[self.player1.id].value = 50
|
||||||
self.multi_world.progression_balancing[self.player2.id].value = 50
|
self.multi_world.progression_balancing[self.player2.id].value = 50
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
from argparse import Namespace
|
|
||||||
from typing import Dict, Optional, Callable
|
|
||||||
|
|
||||||
from BaseClasses import MultiWorld, CollectionState, Region
|
|
||||||
import unittest
|
import unittest
|
||||||
|
from typing import Callable, Dict, Optional
|
||||||
|
|
||||||
|
from BaseClasses import CollectionState, MultiWorld, Region
|
||||||
|
|
||||||
|
|
||||||
class TestHelpers(unittest.TestCase):
|
class TestHelpers(unittest.TestCase):
|
||||||
|
@ -15,7 +14,8 @@ class TestHelpers(unittest.TestCase):
|
||||||
self.multiworld.player_name = {1: "Tester"}
|
self.multiworld.player_name = {1: "Tester"}
|
||||||
self.multiworld.set_seed()
|
self.multiworld.set_seed()
|
||||||
|
|
||||||
def testRegionHelpers(self) -> None:
|
def test_region_helpers(self) -> None:
|
||||||
|
"""Tests `Region.add_locations()` and `Region.add_exits()` have correct behavior"""
|
||||||
regions: Dict[str, str] = {
|
regions: Dict[str, str] = {
|
||||||
"TestRegion1": "I'm an apple",
|
"TestRegion1": "I'm an apple",
|
||||||
"TestRegion2": "I'm a banana",
|
"TestRegion2": "I'm a banana",
|
||||||
|
@ -79,4 +79,5 @@ class TestHelpers(unittest.TestCase):
|
||||||
current_region.add_exits(reg_exit_set[region])
|
current_region.add_exits(reg_exit_set[region])
|
||||||
exit_names = {_exit.name for _exit in current_region.exits}
|
exit_names = {_exit.name for _exit in current_region.exits}
|
||||||
for reg_exit in reg_exit_set[region]:
|
for reg_exit in reg_exit_set[region]:
|
||||||
self.assertTrue(f"{region} -> {reg_exit}" in exit_names, f"{region} -> {reg_exit} not in {exit_names}")
|
self.assertTrue(f"{region} -> {reg_exit}" in exit_names,
|
||||||
|
f"{region} -> {reg_exit} not in {exit_names}")
|
|
@ -15,6 +15,7 @@ class TestIDs(unittest.TestCase):
|
||||||
cls.yaml_options = Utils.parse_yaml(f.read())
|
cls.yaml_options = Utils.parse_yaml(f.read())
|
||||||
|
|
||||||
def test_utils_in_yaml(self) -> None:
|
def test_utils_in_yaml(self) -> None:
|
||||||
|
"""Tests that the auto generated host.yaml has default settings in it"""
|
||||||
for option_key, option_set in Utils.get_default_options().items():
|
for option_key, option_set in Utils.get_default_options().items():
|
||||||
with self.subTest(option_key):
|
with self.subTest(option_key):
|
||||||
self.assertIn(option_key, self.yaml_options)
|
self.assertIn(option_key, self.yaml_options)
|
||||||
|
@ -22,6 +23,7 @@ class TestIDs(unittest.TestCase):
|
||||||
self.assertIn(sub_option_key, self.yaml_options[option_key])
|
self.assertIn(sub_option_key, self.yaml_options[option_key])
|
||||||
|
|
||||||
def test_yaml_in_utils(self) -> None:
|
def test_yaml_in_utils(self) -> None:
|
||||||
|
"""Tests that the auto generated host.yaml shows up in reference calls"""
|
||||||
utils_options = Utils.get_default_options()
|
utils_options = Utils.get_default_options()
|
||||||
for option_key, option_set in self.yaml_options.items():
|
for option_key, option_set in self.yaml_options.items():
|
||||||
with self.subTest(option_key):
|
with self.subTest(option_key):
|
|
@ -3,35 +3,37 @@ from worlds.AutoWorld import AutoWorldRegister
|
||||||
|
|
||||||
|
|
||||||
class TestIDs(unittest.TestCase):
|
class TestIDs(unittest.TestCase):
|
||||||
def testUniqueItems(self):
|
def test_unique_items(self):
|
||||||
|
"""Tests that every game has a unique ID per item in the datapackage"""
|
||||||
known_item_ids = set()
|
known_item_ids = set()
|
||||||
for gamename, world_type in AutoWorldRegister.world_types.items():
|
for gamename, world_type in AutoWorldRegister.world_types.items():
|
||||||
current = len(known_item_ids)
|
current = len(known_item_ids)
|
||||||
known_item_ids |= set(world_type.item_id_to_name)
|
known_item_ids |= set(world_type.item_id_to_name)
|
||||||
self.assertEqual(len(known_item_ids) - len(world_type.item_id_to_name), current)
|
self.assertEqual(len(known_item_ids) - len(world_type.item_id_to_name), current)
|
||||||
|
|
||||||
def testUniqueLocations(self):
|
def test_unique_locations(self):
|
||||||
|
"""Tests that every game has a unique ID per location in the datapackage"""
|
||||||
known_location_ids = set()
|
known_location_ids = set()
|
||||||
for gamename, world_type in AutoWorldRegister.world_types.items():
|
for gamename, world_type in AutoWorldRegister.world_types.items():
|
||||||
current = len(known_location_ids)
|
current = len(known_location_ids)
|
||||||
known_location_ids |= set(world_type.location_id_to_name)
|
known_location_ids |= set(world_type.location_id_to_name)
|
||||||
self.assertEqual(len(known_location_ids) - len(world_type.location_id_to_name), current)
|
self.assertEqual(len(known_location_ids) - len(world_type.location_id_to_name), current)
|
||||||
|
|
||||||
def testRangeItems(self):
|
def test_range_items(self):
|
||||||
"""There are Javascript clients, which are limited to Number.MAX_SAFE_INTEGER due to 64bit float precision."""
|
"""There are Javascript clients, which are limited to Number.MAX_SAFE_INTEGER due to 64bit float precision."""
|
||||||
for gamename, world_type in AutoWorldRegister.world_types.items():
|
for gamename, world_type in AutoWorldRegister.world_types.items():
|
||||||
with self.subTest(game=gamename):
|
with self.subTest(game=gamename):
|
||||||
for item_id in world_type.item_id_to_name:
|
for item_id in world_type.item_id_to_name:
|
||||||
self.assertLess(item_id, 2**53)
|
self.assertLess(item_id, 2**53)
|
||||||
|
|
||||||
def testRangeLocations(self):
|
def test_range_locations(self):
|
||||||
"""There are Javascript clients, which are limited to Number.MAX_SAFE_INTEGER due to 64bit float precision."""
|
"""There are Javascript clients, which are limited to Number.MAX_SAFE_INTEGER due to 64bit float precision."""
|
||||||
for gamename, world_type in AutoWorldRegister.world_types.items():
|
for gamename, world_type in AutoWorldRegister.world_types.items():
|
||||||
with self.subTest(game=gamename):
|
with self.subTest(game=gamename):
|
||||||
for location_id in world_type.location_id_to_name:
|
for location_id in world_type.location_id_to_name:
|
||||||
self.assertLess(location_id, 2**53)
|
self.assertLess(location_id, 2**53)
|
||||||
|
|
||||||
def testReservedItems(self):
|
def test_reserved_items(self):
|
||||||
"""negative item IDs are reserved to the special "Archipelago" world."""
|
"""negative item IDs are reserved to the special "Archipelago" world."""
|
||||||
for gamename, world_type in AutoWorldRegister.world_types.items():
|
for gamename, world_type in AutoWorldRegister.world_types.items():
|
||||||
with self.subTest(game=gamename):
|
with self.subTest(game=gamename):
|
||||||
|
@ -42,7 +44,7 @@ class TestIDs(unittest.TestCase):
|
||||||
for item_id in world_type.item_id_to_name:
|
for item_id in world_type.item_id_to_name:
|
||||||
self.assertGreater(item_id, 0)
|
self.assertGreater(item_id, 0)
|
||||||
|
|
||||||
def testReservedLocations(self):
|
def test_reserved_locations(self):
|
||||||
"""negative location IDs are reserved to the special "Archipelago" world."""
|
"""negative location IDs are reserved to the special "Archipelago" world."""
|
||||||
for gamename, world_type in AutoWorldRegister.world_types.items():
|
for gamename, world_type in AutoWorldRegister.world_types.items():
|
||||||
with self.subTest(game=gamename):
|
with self.subTest(game=gamename):
|
||||||
|
@ -53,12 +55,14 @@ class TestIDs(unittest.TestCase):
|
||||||
for location_id in world_type.location_id_to_name:
|
for location_id in world_type.location_id_to_name:
|
||||||
self.assertGreater(location_id, 0)
|
self.assertGreater(location_id, 0)
|
||||||
|
|
||||||
def testDuplicateItemIDs(self):
|
def test_duplicate_item_ids(self):
|
||||||
|
"""Test that a game doesn't have item id overlap within its own datapackage"""
|
||||||
for gamename, world_type in AutoWorldRegister.world_types.items():
|
for gamename, world_type in AutoWorldRegister.world_types.items():
|
||||||
with self.subTest(game=gamename):
|
with self.subTest(game=gamename):
|
||||||
self.assertEqual(len(world_type.item_id_to_name), len(world_type.item_name_to_id))
|
self.assertEqual(len(world_type.item_id_to_name), len(world_type.item_name_to_id))
|
||||||
|
|
||||||
def testDuplicateLocationIDs(self):
|
def test_duplicate_location_ids(self):
|
||||||
|
"""Test that a game doesn't have location id overlap within its own datapackage"""
|
||||||
for gamename, world_type in AutoWorldRegister.world_types.items():
|
for gamename, world_type in AutoWorldRegister.world_types.items():
|
||||||
with self.subTest(game=gamename):
|
with self.subTest(game=gamename):
|
||||||
self.assertEqual(len(world_type.location_id_to_name), len(world_type.location_name_to_id))
|
self.assertEqual(len(world_type.location_id_to_name), len(world_type.location_name_to_id))
|
|
@ -5,7 +5,7 @@ from . import setup_solo_multiworld
|
||||||
|
|
||||||
|
|
||||||
class TestImplemented(unittest.TestCase):
|
class TestImplemented(unittest.TestCase):
|
||||||
def testCompletionCondition(self):
|
def test_completion_condition(self):
|
||||||
"""Ensure a completion condition is set that has requirements."""
|
"""Ensure a completion condition is set that has requirements."""
|
||||||
for game_name, world_type in AutoWorldRegister.world_types.items():
|
for game_name, world_type in AutoWorldRegister.world_types.items():
|
||||||
if not world_type.hidden and game_name not in {"Sudoku"}:
|
if not world_type.hidden and game_name not in {"Sudoku"}:
|
||||||
|
@ -13,7 +13,7 @@ class TestImplemented(unittest.TestCase):
|
||||||
multiworld = setup_solo_multiworld(world_type)
|
multiworld = setup_solo_multiworld(world_type)
|
||||||
self.assertFalse(multiworld.completion_condition[1](multiworld.state))
|
self.assertFalse(multiworld.completion_condition[1](multiworld.state))
|
||||||
|
|
||||||
def testEntranceParents(self):
|
def test_entrance_parents(self):
|
||||||
"""Tests that the parents of created Entrances match the exiting Region."""
|
"""Tests that the parents of created Entrances match the exiting Region."""
|
||||||
for game_name, world_type in AutoWorldRegister.world_types.items():
|
for game_name, world_type in AutoWorldRegister.world_types.items():
|
||||||
if not world_type.hidden:
|
if not world_type.hidden:
|
||||||
|
@ -23,7 +23,7 @@ class TestImplemented(unittest.TestCase):
|
||||||
for exit in region.exits:
|
for exit in region.exits:
|
||||||
self.assertEqual(exit.parent_region, region)
|
self.assertEqual(exit.parent_region, region)
|
||||||
|
|
||||||
def testStageMethods(self):
|
def test_stage_methods(self):
|
||||||
"""Tests that worlds don't try to implement certain steps that are only ever called as stage."""
|
"""Tests that worlds don't try to implement certain steps that are only ever called as stage."""
|
||||||
for game_name, world_type in AutoWorldRegister.world_types.items():
|
for game_name, world_type in AutoWorldRegister.world_types.items():
|
||||||
if not world_type.hidden:
|
if not world_type.hidden:
|
|
@ -4,7 +4,8 @@ from . import setup_solo_multiworld
|
||||||
|
|
||||||
|
|
||||||
class TestBase(unittest.TestCase):
|
class TestBase(unittest.TestCase):
|
||||||
def testCreateItem(self):
|
def test_create_item(self):
|
||||||
|
"""Test that a world can successfully create all items in its datapackage"""
|
||||||
for game_name, world_type in AutoWorldRegister.world_types.items():
|
for game_name, world_type in AutoWorldRegister.world_types.items():
|
||||||
proxy_world = world_type(None, 0) # this is identical to MultiServer.py creating worlds
|
proxy_world = world_type(None, 0) # this is identical to MultiServer.py creating worlds
|
||||||
for item_name in world_type.item_name_to_id:
|
for item_name in world_type.item_name_to_id:
|
||||||
|
@ -12,7 +13,7 @@ class TestBase(unittest.TestCase):
|
||||||
item = proxy_world.create_item(item_name)
|
item = proxy_world.create_item(item_name)
|
||||||
self.assertEqual(item.name, item_name)
|
self.assertEqual(item.name, item_name)
|
||||||
|
|
||||||
def testItemNameGroupHasValidItem(self):
|
def test_item_name_group_has_valid_item(self):
|
||||||
"""Test that all item name groups contain valid items. """
|
"""Test that all item name groups contain valid items. """
|
||||||
# This cannot test for Event names that you may have declared for logic, only sendable Items.
|
# This cannot test for Event names that you may have declared for logic, only sendable Items.
|
||||||
# In such a case, you can add your entries to this Exclusion dict. Game Name -> Group Names
|
# In such a case, you can add your entries to this Exclusion dict. Game Name -> Group Names
|
||||||
|
@ -33,7 +34,7 @@ class TestBase(unittest.TestCase):
|
||||||
for item in items:
|
for item in items:
|
||||||
self.assertIn(item, world_type.item_name_to_id)
|
self.assertIn(item, world_type.item_name_to_id)
|
||||||
|
|
||||||
def testItemNameGroupConflict(self):
|
def test_item_name_group_conflict(self):
|
||||||
"""Test that all item name groups aren't also item names."""
|
"""Test that all item name groups aren't also item names."""
|
||||||
for game_name, world_type in AutoWorldRegister.world_types.items():
|
for game_name, world_type in AutoWorldRegister.world_types.items():
|
||||||
with self.subTest(game_name, game_name=game_name):
|
with self.subTest(game_name, game_name=game_name):
|
||||||
|
@ -41,7 +42,8 @@ class TestBase(unittest.TestCase):
|
||||||
with self.subTest(group_name, group_name=group_name):
|
with self.subTest(group_name, group_name=group_name):
|
||||||
self.assertNotIn(group_name, world_type.item_name_to_id)
|
self.assertNotIn(group_name, world_type.item_name_to_id)
|
||||||
|
|
||||||
def testItemCountGreaterEqualLocations(self):
|
def test_item_count_greater_equal_locations(self):
|
||||||
|
"""Test that by the pre_fill step under default settings, each game submits items >= locations"""
|
||||||
for game_name, world_type in AutoWorldRegister.world_types.items():
|
for game_name, world_type in AutoWorldRegister.world_types.items():
|
||||||
with self.subTest("Game", game=game_name):
|
with self.subTest("Game", game=game_name):
|
||||||
multiworld = setup_solo_multiworld(world_type)
|
multiworld = setup_solo_multiworld(world_type)
|
|
@ -5,7 +5,7 @@ from . import setup_solo_multiworld
|
||||||
|
|
||||||
|
|
||||||
class TestBase(unittest.TestCase):
|
class TestBase(unittest.TestCase):
|
||||||
def testCreateDuplicateLocations(self):
|
def test_create_duplicate_locations(self):
|
||||||
"""Tests that no two Locations share a name or ID."""
|
"""Tests that no two Locations share a name or ID."""
|
||||||
for game_name, world_type in AutoWorldRegister.world_types.items():
|
for game_name, world_type in AutoWorldRegister.world_types.items():
|
||||||
multiworld = setup_solo_multiworld(world_type)
|
multiworld = setup_solo_multiworld(world_type)
|
||||||
|
@ -20,7 +20,7 @@ class TestBase(unittest.TestCase):
|
||||||
self.assertLessEqual(locations.most_common(1)[0][1], 1,
|
self.assertLessEqual(locations.most_common(1)[0][1], 1,
|
||||||
f"{world_type.game} has duplicate of location ID {locations.most_common(1)}")
|
f"{world_type.game} has duplicate of location ID {locations.most_common(1)}")
|
||||||
|
|
||||||
def testLocationsInDatapackage(self):
|
def test_locations_in_datapackage(self):
|
||||||
"""Tests that created locations not filled before fill starts exist in the datapackage."""
|
"""Tests that created locations not filled before fill starts exist in the datapackage."""
|
||||||
for game_name, world_type in AutoWorldRegister.world_types.items():
|
for game_name, world_type in AutoWorldRegister.world_types.items():
|
||||||
with self.subTest("Game", game_name=game_name):
|
with self.subTest("Game", game_name=game_name):
|
||||||
|
@ -30,7 +30,7 @@ class TestBase(unittest.TestCase):
|
||||||
self.assertIn(location.name, world_type.location_name_to_id)
|
self.assertIn(location.name, world_type.location_name_to_id)
|
||||||
self.assertEqual(location.address, world_type.location_name_to_id[location.name])
|
self.assertEqual(location.address, world_type.location_name_to_id[location.name])
|
||||||
|
|
||||||
def testLocationCreationSteps(self):
|
def test_location_creation_steps(self):
|
||||||
"""Tests that Regions and Locations aren't created after `create_items`."""
|
"""Tests that Regions and Locations aren't created after `create_items`."""
|
||||||
gen_steps = ("generate_early", "create_regions", "create_items")
|
gen_steps = ("generate_early", "create_regions", "create_items")
|
||||||
for game_name, world_type in AutoWorldRegister.world_types.items():
|
for game_name, world_type in AutoWorldRegister.world_types.items():
|
||||||
|
@ -60,7 +60,7 @@ class TestBase(unittest.TestCase):
|
||||||
self.assertGreaterEqual(location_count, len(multiworld.get_locations()),
|
self.assertGreaterEqual(location_count, len(multiworld.get_locations()),
|
||||||
f"{game_name} modified locations count during pre_fill")
|
f"{game_name} modified locations count during pre_fill")
|
||||||
|
|
||||||
def testLocationGroup(self):
|
def test_location_group(self):
|
||||||
"""Test that all location name groups contain valid locations and don't share names."""
|
"""Test that all location name groups contain valid locations and don't share names."""
|
||||||
for game_name, world_type in AutoWorldRegister.world_types.items():
|
for game_name, world_type in AutoWorldRegister.world_types.items():
|
||||||
with self.subTest(game_name, game_name=game_name):
|
with self.subTest(game_name, game_name=game_name):
|
|
@ -3,7 +3,7 @@ from worlds.AutoWorld import AutoWorldRegister
|
||||||
|
|
||||||
|
|
||||||
class TestNames(unittest.TestCase):
|
class TestNames(unittest.TestCase):
|
||||||
def testItemNamesFormat(self):
|
def test_item_names_format(self):
|
||||||
"""Item names must not be all numeric in order to differentiate between ID and name in !hint"""
|
"""Item names must not be all numeric in order to differentiate between ID and name in !hint"""
|
||||||
for gamename, world_type in AutoWorldRegister.world_types.items():
|
for gamename, world_type in AutoWorldRegister.world_types.items():
|
||||||
with self.subTest(game=gamename):
|
with self.subTest(game=gamename):
|
||||||
|
@ -11,7 +11,7 @@ class TestNames(unittest.TestCase):
|
||||||
self.assertFalse(item_name.isnumeric(),
|
self.assertFalse(item_name.isnumeric(),
|
||||||
f"Item name \"{item_name}\" is invalid. It must not be numeric.")
|
f"Item name \"{item_name}\" is invalid. It must not be numeric.")
|
||||||
|
|
||||||
def testLocationNameFormat(self):
|
def test_location_name_format(self):
|
||||||
"""Location names must not be all numeric in order to differentiate between ID and name in !hint_location"""
|
"""Location names must not be all numeric in order to differentiate between ID and name in !hint_location"""
|
||||||
for gamename, world_type in AutoWorldRegister.world_types.items():
|
for gamename, world_type in AutoWorldRegister.world_types.items():
|
||||||
with self.subTest(game=gamename):
|
with self.subTest(game=gamename):
|
|
@ -3,7 +3,8 @@ from worlds.AutoWorld import AutoWorldRegister
|
||||||
|
|
||||||
|
|
||||||
class TestOptions(unittest.TestCase):
|
class TestOptions(unittest.TestCase):
|
||||||
def testOptionsHaveDocString(self):
|
def test_options_have_doc_string(self):
|
||||||
|
"""Test that submitted options have their own specified docstring"""
|
||||||
for gamename, world_type in AutoWorldRegister.world_types.items():
|
for gamename, world_type in AutoWorldRegister.world_types.items():
|
||||||
if not world_type.hidden:
|
if not world_type.hidden:
|
||||||
for option_key, option in world_type.options_dataclass.type_hints.items():
|
for option_key, option in world_type.options_dataclass.type_hints.items():
|
|
@ -31,7 +31,8 @@ class TestBase(unittest.TestCase):
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def testDefaultAllStateCanReachEverything(self):
|
def test_default_all_state_can_reach_everything(self):
|
||||||
|
"""Ensure all state can reach everything and complete the game with the defined options"""
|
||||||
for game_name, world_type in AutoWorldRegister.world_types.items():
|
for game_name, world_type in AutoWorldRegister.world_types.items():
|
||||||
unreachable_regions = self.default_settings_unreachable_regions.get(game_name, set())
|
unreachable_regions = self.default_settings_unreachable_regions.get(game_name, set())
|
||||||
with self.subTest("Game", game=game_name):
|
with self.subTest("Game", game=game_name):
|
||||||
|
@ -54,7 +55,8 @@ class TestBase(unittest.TestCase):
|
||||||
with self.subTest("Completion Condition"):
|
with self.subTest("Completion Condition"):
|
||||||
self.assertTrue(world.can_beat_game(state))
|
self.assertTrue(world.can_beat_game(state))
|
||||||
|
|
||||||
def testDefaultEmptyStateCanReachSomething(self):
|
def test_default_empty_state_can_reach_something(self):
|
||||||
|
"""Ensure empty state can reach at least one location with the defined options"""
|
||||||
for game_name, world_type in AutoWorldRegister.world_types.items():
|
for game_name, world_type in AutoWorldRegister.world_types.items():
|
||||||
with self.subTest("Game", game=game_name):
|
with self.subTest("Game", game=game_name):
|
||||||
world = setup_solo_multiworld(world_type)
|
world = setup_solo_multiworld(world_type)
|
|
@ -16,7 +16,7 @@ class TestGenerateMain(unittest.TestCase):
|
||||||
|
|
||||||
generate_dir = Path(Generate.__file__).parent
|
generate_dir = Path(Generate.__file__).parent
|
||||||
run_dir = generate_dir / "test" # reproducible cwd that's neither __file__ nor Generate.__file__
|
run_dir = generate_dir / "test" # reproducible cwd that's neither __file__ nor Generate.__file__
|
||||||
abs_input_dir = Path(__file__).parent / 'data' / 'OnePlayer'
|
abs_input_dir = Path(__file__).parent / 'data' / 'one_player'
|
||||||
rel_input_dir = abs_input_dir.relative_to(run_dir) # directly supplied relative paths are relative to cwd
|
rel_input_dir = abs_input_dir.relative_to(run_dir) # directly supplied relative paths are relative to cwd
|
||||||
yaml_input_dir = abs_input_dir.relative_to(generate_dir) # yaml paths are relative to user_path
|
yaml_input_dir = abs_input_dir.relative_to(generate_dir) # yaml paths are relative to user_path
|
||||||
|
|
|
@ -19,11 +19,11 @@ class TestDocs(unittest.TestCase):
|
||||||
|
|
||||||
cls.client = app.test_client()
|
cls.client = app.test_client()
|
||||||
|
|
||||||
def testCorrectErrorEmptyRequest(self):
|
def test_correct_error_empty_request(self):
|
||||||
response = self.client.post("/api/generate")
|
response = self.client.post("/api/generate")
|
||||||
self.assertIn("No options found. Expected file attachment or json weights.", response.text)
|
self.assertIn("No options found. Expected file attachment or json weights.", response.text)
|
||||||
|
|
||||||
def testGenerationQueued(self):
|
def test_generation_queued(self):
|
||||||
options = {
|
options = {
|
||||||
"Tester1":
|
"Tester1":
|
||||||
{
|
{
|
|
@ -11,7 +11,7 @@ class TestDocs(unittest.TestCase):
|
||||||
def setUpClass(cls) -> None:
|
def setUpClass(cls) -> None:
|
||||||
cls.tutorials_data = WebHost.create_ordered_tutorials_file()
|
cls.tutorials_data = WebHost.create_ordered_tutorials_file()
|
||||||
|
|
||||||
def testHasTutorial(self):
|
def test_has_tutorial(self):
|
||||||
games_with_tutorial = set(entry["gameTitle"] for entry in self.tutorials_data)
|
games_with_tutorial = set(entry["gameTitle"] for entry in self.tutorials_data)
|
||||||
for game_name, world_type in AutoWorldRegister.world_types.items():
|
for game_name, world_type in AutoWorldRegister.world_types.items():
|
||||||
if not world_type.hidden:
|
if not world_type.hidden:
|
||||||
|
@ -27,7 +27,7 @@ class TestDocs(unittest.TestCase):
|
||||||
self.fail(f"{game_name} has no setup tutorial. "
|
self.fail(f"{game_name} has no setup tutorial. "
|
||||||
f"Games with Tutorial: {games_with_tutorial}")
|
f"Games with Tutorial: {games_with_tutorial}")
|
||||||
|
|
||||||
def testHasGameInfo(self):
|
def test_has_game_info(self):
|
||||||
for game_name, world_type in AutoWorldRegister.world_types.items():
|
for game_name, world_type in AutoWorldRegister.world_types.items():
|
||||||
if not world_type.hidden:
|
if not world_type.hidden:
|
||||||
target_path = Utils.local_path("WebHostLib", "static", "generated", "docs", game_name)
|
target_path = Utils.local_path("WebHostLib", "static", "generated", "docs", game_name)
|
|
@ -13,7 +13,7 @@ class TestFileGeneration(unittest.TestCase):
|
||||||
# should not create the folder *here*
|
# should not create the folder *here*
|
||||||
cls.incorrect_path = os.path.join(os.path.split(os.path.dirname(__file__))[0], "WebHostLib")
|
cls.incorrect_path = os.path.join(os.path.split(os.path.dirname(__file__))[0], "WebHostLib")
|
||||||
|
|
||||||
def testOptions(self):
|
def test_options(self):
|
||||||
from WebHostLib.options import create as create_options_files
|
from WebHostLib.options import create as create_options_files
|
||||||
create_options_files()
|
create_options_files()
|
||||||
target = os.path.join(self.correct_path, "static", "generated", "configs")
|
target = os.path.join(self.correct_path, "static", "generated", "configs")
|
||||||
|
@ -30,7 +30,7 @@ class TestFileGeneration(unittest.TestCase):
|
||||||
for value in roll_options({file.name: f.read()})[0].values():
|
for value in roll_options({file.name: f.read()})[0].values():
|
||||||
self.assertTrue(value is True, f"Default Options for template {file.name} cannot be run.")
|
self.assertTrue(value is True, f"Default Options for template {file.name} cannot be run.")
|
||||||
|
|
||||||
def testTutorial(self):
|
def test_tutorial(self):
|
||||||
WebHost.create_ordered_tutorials_file()
|
WebHost.create_ordered_tutorials_file()
|
||||||
self.assertTrue(os.path.exists(os.path.join(self.correct_path, "static", "generated", "tutorials.json")))
|
self.assertTrue(os.path.exists(os.path.join(self.correct_path, "static", "generated", "tutorials.json")))
|
||||||
self.assertFalse(os.path.exists(os.path.join(self.incorrect_path, "static", "generated", "tutorials.json")))
|
self.assertFalse(os.path.exists(os.path.join(self.incorrect_path, "static", "generated", "tutorials.json")))
|
Loading…
Reference in New Issue