From 078d7930734066f1766d2f35b5f595b626666c31 Mon Sep 17 00:00:00 2001 From: Aaron Wagener Date: Mon, 11 Mar 2024 17:22:30 -0500 Subject: [PATCH] Tests: add test for 2-player-multiworlds (#2386) * Tests: add test for all games multiworld and test for two player multiworld per game * make assertSteps behave like call_all * review improvements * fix stage calling and loc copying in accessibility * add docstrings * lttp is on the options api now * skip the all games multiworld for now. likely needs to be modified to test specific worlds * move skip to the class --- test/general/__init__.py | 35 ++++++++++--- test/multiworld/__init__.py | 0 test/multiworld/test_multiworlds.py | 77 +++++++++++++++++++++++++++++ 3 files changed, 104 insertions(+), 8 deletions(-) create mode 100644 test/multiworld/__init__.py create mode 100644 test/multiworld/test_multiworlds.py diff --git a/test/general/__init__.py b/test/general/__init__.py index 2819628dd03d..fe890e0b340b 100644 --- a/test/general/__init__.py +++ b/test/general/__init__.py @@ -1,8 +1,8 @@ from argparse import Namespace -from typing import Optional, Tuple, Type +from typing import List, Optional, Tuple, Type, Union -from BaseClasses import MultiWorld, CollectionState -from worlds.AutoWorld import call_all, World +from BaseClasses import CollectionState, MultiWorld +from worlds.AutoWorld import World, call_all gen_steps = ("generate_early", "create_regions", "create_items", "set_rules", "generate_basic", "pre_fill") @@ -18,14 +18,33 @@ def setup_solo_multiworld( steps through pre_fill :param seed: The seed to be used when creating this multiworld """ - multiworld = MultiWorld(1) - multiworld.game[1] = world_type.game - multiworld.player_name = {1: "Tester"} + return setup_multiworld(world_type, steps, seed) + + +def setup_multiworld(worlds: Union[List[Type[World]], Type[World]], steps: Tuple[str, ...] = gen_steps, + seed: Optional[int] = None) -> MultiWorld: + """ + Creates a multiworld with a player for each provided world type, allowing duplicates, setting default options, and + calling the provided gen steps. + + :param worlds: type/s of worlds to generate a multiworld for + :param steps: gen steps that should be called before returning. Default calls through pre_fill + :param seed: The seed to be used when creating this multiworld + """ + if not isinstance(worlds, list): + worlds = [worlds] + players = len(worlds) + multiworld = MultiWorld(players) + multiworld.game = {player: world_type.game for player, world_type in enumerate(worlds, 1)} + multiworld.player_name = {player: f"Tester{player}" for player in multiworld.player_ids} multiworld.set_seed(seed) multiworld.state = CollectionState(multiworld) args = Namespace() - for name, option in world_type.options_dataclass.type_hints.items(): - setattr(args, name, {1: option.from_any(option.default)}) + for player, world_type in enumerate(worlds, 1): + for key, option in world_type.options_dataclass.type_hints.items(): + updated_options = getattr(args, key, {}) + updated_options[player] = option.from_any(option.default) + setattr(args, key, updated_options) multiworld.set_options(args) for step in steps: call_all(multiworld, step) diff --git a/test/multiworld/__init__.py b/test/multiworld/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/test/multiworld/test_multiworlds.py b/test/multiworld/test_multiworlds.py new file mode 100644 index 000000000000..677f0de82930 --- /dev/null +++ b/test/multiworld/test_multiworlds.py @@ -0,0 +1,77 @@ +import unittest +from typing import List, Tuple +from unittest import TestCase + +from BaseClasses import CollectionState, Location, MultiWorld +from Fill import distribute_items_restrictive +from Options import Accessibility +from worlds.AutoWorld import AutoWorldRegister, call_all, call_single +from ..general import gen_steps, setup_multiworld + + +class MultiworldTestBase(TestCase): + multiworld: MultiWorld + + # similar to the implementation in WorldTestBase.test_fill + # but for multiple players and doesn't allow minimal accessibility + def fulfills_accessibility(self) -> bool: + """ + Checks that the multiworld satisfies locations accessibility requirements, failing if all locations are cleared + but not beatable, or some locations are unreachable. + """ + locations = [loc for loc in self.multiworld.get_locations()] + state = CollectionState(self.multiworld) + while locations: + sphere: 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, f"Unreachable locations: {locations}") + if not sphere: + return False + for location in sphere: + if location.item: + state.collect(location.item, True, location) + return self.multiworld.has_beaten_game(state, 1) + + def assertSteps(self, steps: Tuple[str, ...]) -> None: + """Calls each step individually, continuing if a step for a specific world step fails.""" + world_types = {world.__class__ for world in self.multiworld.worlds.values()} + for step in steps: + for player, world in self.multiworld.worlds.items(): + with self.subTest(game=world.game, step=step): + call_single(self.multiworld, step, player) + for world_type in sorted(world_types, key=lambda world: world.__name__): + with self.subTest(game=world_type.game, step=f"stage_{step}"): + stage_callable = getattr(world_type, f"stage_{step}", None) + if stage_callable: + stage_callable(self.multiworld) + + +@unittest.skip("too slow for main") +class TestAllGamesMultiworld(MultiworldTestBase): + def test_fills(self) -> None: + """Tests that a multiworld with one of every registered game world can generate.""" + all_worlds = list(AutoWorldRegister.world_types.values()) + self.multiworld = setup_multiworld(all_worlds, ()) + for world in self.multiworld.worlds.values(): + world.options.accessibility.value = Accessibility.option_locations + self.assertSteps(gen_steps) + with self.subTest("filling multiworld", seed=self.multiworld.seed): + distribute_items_restrictive(self.multiworld) + call_all(self.multiworld, "post_fill") + self.assertTrue(self.fulfills_accessibility(), "Collected all locations, but can't beat the game") + + +class TestTwoPlayerMulti(MultiworldTestBase): + def test_two_player_single_game_fills(self) -> None: + """Tests that a multiworld of two players for each registered game world can generate.""" + for world in AutoWorldRegister.world_types.values(): + self.multiworld = setup_multiworld([world, world], ()) + for world in self.multiworld.worlds.values(): + world.options.accessibility.value = Accessibility.option_locations + self.assertSteps(gen_steps) + with self.subTest("filling multiworld", seed=self.multiworld.seed): + distribute_items_restrictive(self.multiworld) + call_all(self.multiworld, "post_fill") + self.assertTrue(self.fulfills_accessibility(), "Collected all locations, but can't beat the game")