Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

The Witness: Add some unit tests #3328

Merged
merged 35 commits into from
Jul 6, 2024
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
b57d283
Add hidden early symbol item option, make some unit tests
NewSoupVi May 18, 2024
a3a906c
Add early symbol item false to the arrows test
NewSoupVi May 18, 2024
f9025bb
I guess it's not an issue
NewSoupVi May 18, 2024
a32ec90
more tests
NewSoupVi May 18, 2024
4a2a08a
assertEqual
NewSoupVi May 18, 2024
8552494
cleanup
NewSoupVi May 18, 2024
adb0374
Merge branch 'main' into some_unit_tests
NewSoupVi Jun 1, 2024
d9840b5
add minimum symbols test for all 3 modes
NewSoupVi Jun 3, 2024
de2e8ab
Merge branch 'some_unit_tests' of https://github.com/NewSoupVi/Archip…
NewSoupVi Jun 3, 2024
9e94d7e
Formatting
NewSoupVi Jun 3, 2024
5e22318
Add more minimal beatability tests
NewSoupVi Jun 3, 2024
356c80f
one more for the road
NewSoupVi Jun 3, 2024
47511fe
Merge branch 'main' into some_unit_tests
NewSoupVi Jun 6, 2024
bc3bd06
I HATE THIS AAAAAAAAAAAHHHHHHHHHHH WHY DID WE GO WITH OPTIONS
NewSoupVi Jun 9, 2024
b9b010c
loiaqeäsdhgalikSDGHjasDÖKHGASKLDÖGHJASKLJGHJSAÖkfaöslifjasöfASGJÖASDL…
NewSoupVi Jun 9, 2024
d1f3011
fix imports (within apworld needs to be relative)
NewSoupVi Jun 9, 2024
25c1ef4
Update worlds/witness/options.py
NewSoupVi Jun 9, 2024
dab6d04
Sure
NewSoupVi Jun 9, 2024
9884f0b
Merge branch 'some_unit_tests' of https://github.com/NewSoupVi/Archip…
NewSoupVi Jun 9, 2024
4065827
good suggestion
NewSoupVi Jun 9, 2024
15116ec
subtest
NewSoupVi Jun 9, 2024
dc50a18
Merge branch 'main' into some_unit_tests
NewSoupVi Jun 15, 2024
4a92583
Add some EP shuffle unit tests, also an explicit event-checking unit …
NewSoupVi Jun 30, 2024
fe7acde
add more tests yay
NewSoupVi Jun 30, 2024
5df659a
oops
NewSoupVi Jun 30, 2024
3781913
Merge branch 'main' into some_unit_tests
NewSoupVi Jul 2, 2024
36afcef
mypy
NewSoupVi Jul 2, 2024
1c386ff
Merge branch 'main' into some_unit_tests
NewSoupVi Jul 2, 2024
2eb36ea
Update worlds/witness/options.py
NewSoupVi Jul 5, 2024
9741555
Collapse into one test :(
NewSoupVi Jul 5, 2024
c953f7c
Merge branch 'some_unit_tests' of https://github.com/NewSoupVi/Archip…
NewSoupVi Jul 5, 2024
29e8370
More efficiency
NewSoupVi Jul 5, 2024
e6986c9
line length
NewSoupVi Jul 5, 2024
b2c3a50
More collapsing
NewSoupVi Jul 5, 2024
89276e3
Cleanup and docstrings
NewSoupVi Jul 5, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 16 additions & 15 deletions worlds/witness/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -183,21 +183,22 @@ def create_regions(self) -> None:

self.items_placed_early.append("Puzzle Skip")

# Pick an early item to place on the tutorial gate.
early_items = [
item for item in self.player_items.get_early_items() if item in self.player_items.get_mandatory_items()
]
if early_items:
random_early_item = self.random.choice(early_items)
if self.options.puzzle_randomization == "sigma_expert":
# In Expert, only tag the item as early, rather than forcing it onto the gate.
self.multiworld.local_early_items[self.player][random_early_item] = 1
else:
# Force the item onto the tutorial gate check and remove it from our random pool.
gate_item = self.create_item(random_early_item)
self.get_location("Tutorial Gate Open").place_locked_item(gate_item)
self.own_itempool.append(gate_item)
self.items_placed_early.append(random_early_item)
if self.options.early_symbol_item:
# Pick an early item to place on the tutorial gate.
early_items = [
item for item in self.player_items.get_early_items() if item in self.player_items.get_mandatory_items()
]
if early_items:
random_early_item = self.random.choice(early_items)
if self.options.puzzle_randomization == "sigma_expert":
# In Expert, only tag the item as early, rather than forcing it onto the gate.
self.multiworld.local_early_items[self.player][random_early_item] = 1
else:
# Force the item onto the tutorial gate check and remove it from our random pool.
gate_item = self.create_item(random_early_item)
self.get_location("Tutorial Gate Open").place_locked_item(gate_item)
self.own_itempool.append(gate_item)
self.items_placed_early.append(random_early_item)

# There are some really restrictive settings in The Witness.
# They are rarely played, but when they are, we add some extra sphere 1 locations.
Expand Down
11 changes: 10 additions & 1 deletion worlds/witness/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from schema import And, Schema

from Options import Choice, DefaultOnToggle, OptionDict, OptionGroup, PerGameCommonOptions, Range, Toggle
from Options import Choice, DefaultOnToggle, OptionDict, OptionGroup, PerGameCommonOptions, Range, Toggle, Visibility

from .data import static_logic as static_witness_logic
from .data.item_definition_classes import ItemCategory, WeightedItemDefinition
Expand Down Expand Up @@ -35,6 +35,14 @@ class EarlyCaves(Choice):
alias_on = 2


class EarlySymbolItem(DefaultOnToggle):
"""
Put a random helpful symbol item on an early check, specifically Tutorial Gate Open if it available early.
NewSoupVi marked this conversation as resolved.
Show resolved Hide resolved
"""

visibility = Visibility.none


class ShuffleSymbols(DefaultOnToggle):
"""
If on, you will need to unlock puzzle symbols as items to be able to solve the panels that contain those symbols.
Expand Down Expand Up @@ -325,6 +333,7 @@ class TheWitnessOptions(PerGameCommonOptions):
mountain_lasers: MountainLasers
challenge_lasers: ChallengeLasers
early_caves: EarlyCaves
early_symbol_item: EarlySymbolItem
elevators_come_to_you: ElevatorsComeToYou
trap_percentage: TrapPercentage
trap_weights: TrapWeights
Expand Down
86 changes: 86 additions & 0 deletions worlds/witness/test/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
from test.bases import WorldTestBase
from test.general import gen_steps, setup_multiworld
from test.multiworld.test_multiworlds import MultiworldTestBase
from typing import Any, ClassVar, Dict, Iterable, List, Mapping, Union

from BaseClasses import CollectionState, Item

from worlds.witness import WitnessWorld


class WitnessTestBase(WorldTestBase):
game = "The Witness"
player: ClassVar[int] = 1

world: WitnessWorld

def can_beat_game_with_items(self, items: Iterable[Item]) -> bool:
state = CollectionState(self.multiworld)
for item in items:
state.collect(item)
return state.multiworld.can_beat_game(state)

def assert_can_beat_with_minimally(self, required_item_counts: Mapping[str, int]):
"""
Assert that the specified mapping of items is enough to beat the game,
and that having one less of any item would result in the game being unbeatable.
"""
# Find the actual items
found_items = [item for item in self.multiworld.get_items() if item.name in required_item_counts]
actual_items = {item_name: [] for item_name in required_item_counts}
for item in found_items:
if len(actual_items[item.name]) < required_item_counts[item.name]:
actual_items[item.name].append(item)

# Assert that enough items exist in the item pool to satisfy the specified required counts
for item_name, item_objects in actual_items.items():
self.assertEqual(
len(item_objects),
required_item_counts[item_name],
f"Couldn't find {required_item_counts[item_name]} copies of item {item_name} available in the pool, "
f"only found {len(item_objects)}",
)

# assert that multiworld is beatable with the items specified
self.assertTrue(
self.can_beat_game_with_items(item for items in actual_items.values() for item in items),
f"Could not beat game with items: {required_item_counts}",
)

# assert that one less copy of any item would result in the multiworld being unbeatable
for item_name, item_objects in actual_items.items():
NewSoupVi marked this conversation as resolved.
Show resolved Hide resolved
removed_item = item_objects.pop()
self.assertFalse(
self.can_beat_game_with_items(item for items in actual_items.values() for item in items),
f"Game was beatable despite having {len(item_objects)} copies of {item_name} "
f"instead of the specified {required_item_counts[item_name]}",
)
item_objects.append(removed_item)


class WitnessMultiworldTestBase(MultiworldTestBase):
options_per_world: List[Dict[str, Any]]
common_options: Dict[str, Any] = {}

def setUp(self):
self.multiworld = setup_multiworld([WitnessWorld] * len(self.options_per_world), ())

for world, options in zip(self.multiworld.worlds.values(), self.options_per_world):
for option_name, option_value in {**self.common_options, **options}.items():
option = getattr(world.options, option_name)
self.assertIsNotNone(option)

option.value = option.from_any(option_value).value

self.assertSteps(gen_steps)

def collect_by_name(self, item_names: Union[str, Iterable[str]], player: int) -> List[Item]:
items = self.get_items_by_name(item_names, player)
for item in items:
self.multiworld.state.collect(item)
return items

def get_items_by_name(self, item_names: Union[str, Iterable[str]], player: int) -> List[Item]:
if isinstance(item_names, str):
item_names = (item_names,)
return [item for item in self.multiworld.itempool if item.name in item_names and item.player == player]
55 changes: 55 additions & 0 deletions worlds/witness/test/test_auto_elevators.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
from worlds.witness.test import WitnessMultiworldTestBase, WitnessTestBase


class TestElevatorsComeToYou(WitnessTestBase):
options = {
"elevators_come_to_you": True,
"shuffle_doors": "mixed",
"shuffle_symbols": False,
}

def test_bunker_laser(self):
self.collect_by_name("Bunker Drop-Down Door Controls (Panel)")

self.assertFalse(self.multiworld.state.can_reach("Bunker Laser Panel", "Location", self.player))

self.collect_by_name("Bunker Elevator Control (Panel)")

self.assertTrue(self.multiworld.state.can_reach("Bunker Laser Panel", "Location", self.player))
self.assertFalse(self.multiworld.state.can_reach("Bunker UV Room 2", "Location", self.player))

self.collect_by_name("Bunker Elevator Room Entry (Door)")

self.assertTrue(self.multiworld.state.can_reach("Bunker UV Room 2", "Location", self.player))


class TestElevatorsComeToYouBleed(WitnessMultiworldTestBase):
options_per_world = [
{
"elevators_come_to_you": False,
},
{
"elevators_come_to_you": True,
},
{
"elevators_come_to_you": False,
},
]

common_options = {
"shuffle_symbols": False,
"shuffle_doors": "panels",
}

def test_correct_access_per_player(self):
self.assertFalse(self.multiworld.state.can_reach("Bunker Laser Panel", "Location", 1))
self.assertFalse(self.multiworld.state.can_reach("Bunker Laser Panel", "Location", 2))
self.assertFalse(self.multiworld.state.can_reach("Bunker Laser Panel", "Location", 3))

self.collect_by_name(["Bunker Elevator Control (Panel)"], 1)
self.collect_by_name(["Bunker Elevator Control (Panel)"], 2)
self.collect_by_name(["Bunker Elevator Control (Panel)"], 3)

self.assertFalse(self.multiworld.state.can_reach("Bunker Laser Panel", "Location", 1))
self.assertTrue(self.multiworld.state.can_reach("Bunker Laser Panel", "Location", 2))
self.assertFalse(self.multiworld.state.can_reach("Bunker Laser Panel", "Location", 3))
21 changes: 21 additions & 0 deletions worlds/witness/test/test_door_shuffle.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from worlds.witness.test import WitnessTestBase


class TestIndividualDoors(WitnessTestBase):
options = {
"shuffle_doors": "doors",
"door_groupings": "off",
}

def test_swamp_laser_shortcut(self):
self.assertTrue(self.get_items_by_name("Swamp Laser Shortcut (Door)"))

self.assertAccessDependency(
["Swamp Laser Panel"],
[
["Swamp Laser Shortcut (Door)"],
NewSoupVi marked this conversation as resolved.
Show resolved Hide resolved
["Swamp Entry (Door)"],
["Boat"],
],
only_check_listed=True,
)
Loading
Loading