Skip to content

Commit

Permalink
The Witness: Rules Optimisation (#3617)
Browse files Browse the repository at this point in the history
* Attempt at optimizing rules

* docstrings

* Python 3.8

* Lasers optimisation

* Simplify conversion code and make it even faster

* mypy

* ruff

* Neat

* Add redirect to the other two modes

* Update WitnessLogic.txt

* Update WitnessLogicExpert.txt

* Update WitnessLogicVanilla.txt

* Use NamedTuple

* Ruff

* mypy thing

* Mypy stuff

* Move Redirect Event to Desert Region so it has a better name
  • Loading branch information
NewSoupVi authored Aug 28, 2024
1 parent 0fb69dc commit 906b230
Show file tree
Hide file tree
Showing 11 changed files with 139 additions and 85 deletions.
7 changes: 5 additions & 2 deletions worlds/witness/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -204,8 +204,11 @@ def create_regions(self) -> None:
]
if early_items:
random_early_item = self.random.choice(early_items)
if self.options.puzzle_randomization == "sigma_expert" or self.options.victory_condition == "panel_hunt":
# In Expert, only tag the item as early, rather than forcing it onto the gate.
if (
self.options.puzzle_randomization == "sigma_expert"
or self.options.victory_condition == "panel_hunt"
):
# In Expert and Panel Hunt, 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.
Expand Down
3 changes: 2 additions & 1 deletion worlds/witness/data/WitnessLogic.txt
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@ Door - 0x03444 (Vault Door) - 0x0CC7B
Door - 0x09FEE (Light Room Entry) - 0x0C339
158701 - 0x03608 (Laser Panel) - 0x012D7 & 0x0A15F - True
Laser - 0x012FB (Laser) - 0x03608
159804 - 0xFFD03 (Laser Activated + Redirected) - 0x09F98 & 0x012FB - True
159020 - 0x3351D (Sand Snake EP) - True - True
159030 - 0x0053C (Facade Right EP) - True - True
159031 - 0x00771 (Facade Left EP) - True - True
Expand Down Expand Up @@ -980,7 +981,7 @@ Mountainside Obelisk (Mountainside) - Entry - True:
159739 - 0x00367 (Obelisk) - True - True

Mountainside (Mountainside) - Main Island - True - Mountaintop - True - Mountainside Vault - 0x00085:
159550 - 0x28B91 (Thundercloud EP) - 0x09F98 & 0x012FB - True
159550 - 0x28B91 (Thundercloud EP) - 0xFFD03 - True
158612 - 0x17C42 (Discard) - True - Triangles
158665 - 0x002A6 (Vault Panel) - True - Symmetry & Colored Dots & Black/White Squares & Dots
Door - 0x00085 (Vault Door) - 0x002A6
Expand Down
3 changes: 2 additions & 1 deletion worlds/witness/data/WitnessLogicExpert.txt
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@ Door - 0x03444 (Vault Door) - 0x0CC7B
Door - 0x09FEE (Light Room Entry) - 0x0C339
158701 - 0x03608 (Laser Panel) - 0x012D7 & 0x0A15F - True
Laser - 0x012FB (Laser) - 0x03608
159804 - 0xFFD03 (Laser Activated + Redirected) - 0x09F98 & 0x012FB - True
159020 - 0x3351D (Sand Snake EP) - True - True
159030 - 0x0053C (Facade Right EP) - True - True
159031 - 0x00771 (Facade Left EP) - True - True
Expand Down Expand Up @@ -980,7 +981,7 @@ Mountainside Obelisk (Mountainside) - Entry - True:
159739 - 0x00367 (Obelisk) - True - True

Mountainside (Mountainside) - Main Island - True - Mountaintop - True - Mountainside Vault - 0x00085:
159550 - 0x28B91 (Thundercloud EP) - 0x09F98 & 0x012FB - True
159550 - 0x28B91 (Thundercloud EP) - 0xFFD03 - True
158612 - 0x17C42 (Discard) - True - Arrows
158665 - 0x002A6 (Vault Panel) - True - Symmetry & Colored Squares & Triangles & Stars & Stars + Same Colored Symbol
Door - 0x00085 (Vault Door) - 0x002A6
Expand Down
3 changes: 2 additions & 1 deletion worlds/witness/data/WitnessLogicVanilla.txt
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@ Door - 0x03444 (Vault Door) - 0x0CC7B
Door - 0x09FEE (Light Room Entry) - 0x0C339
158701 - 0x03608 (Laser Panel) - 0x012D7 & 0x0A15F - True
Laser - 0x012FB (Laser) - 0x03608
159804 - 0xFFD03 (Laser Activated + Redirected) - 0x09F98 & 0x012FB - True
159020 - 0x3351D (Sand Snake EP) - True - True
159030 - 0x0053C (Facade Right EP) - True - True
159031 - 0x00771 (Facade Left EP) - True - True
Expand Down Expand Up @@ -980,7 +981,7 @@ Mountainside Obelisk (Mountainside) - Entry - True:
159739 - 0x00367 (Obelisk) - True - True

Mountainside (Mountainside) - Main Island - True - Mountaintop - True - Mountainside Vault - 0x00085:
159550 - 0x28B91 (Thundercloud EP) - 0x09F98 & 0x012FB - True
159550 - 0x28B91 (Thundercloud EP) - 0xFFD03 - True
158612 - 0x17C42 (Discard) - True - Triangles
158665 - 0x002A6 (Vault Panel) - True - Symmetry & Colored Dots & Black/White Squares
Door - 0x00085 (Vault Door) - 0x002A6
Expand Down
3 changes: 1 addition & 2 deletions worlds/witness/hints.py
Original file line number Diff line number Diff line change
Expand Up @@ -712,8 +712,7 @@ def get_compact_hint_args(hint: WitnessWordedHint, local_player_number: int) ->
if hint.vague_location_hint and location.player == local_player_number:
assert hint.area is not None # A local vague location hint should have an area argument
return location.address, "containing_area:" + hint.area
else:
return location.address, location.player # Scouting does not matter for other players (currently)
return location.address, location.player # Scouting does not matter for other players (currently)

# Is junk / undefined hint
return -1, local_player_number
Expand Down
2 changes: 1 addition & 1 deletion worlds/witness/player_items.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ def __init__(self, world: "WitnessWorld", player_logic: WitnessPlayerLogic,
player_locations: WitnessPlayerLocations) -> None:
"""Adds event items after logic changes due to options"""

self._world: "WitnessWorld" = world
self._world: WitnessWorld = world
self._multiworld: MultiWorld = world.multiworld
self._player_id: int = world.player
self._logic: WitnessPlayerLogic = player_logic
Expand Down
23 changes: 12 additions & 11 deletions worlds/witness/player_logic.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,18 +116,19 @@ def __init__(self, world: "WitnessWorld", disabled_locations: Set[str], start_in
self.HUNT_ENTITIES: Set[str] = set()

self.ALWAYS_EVENT_NAMES_BY_HEX = {
"0x00509": "+1 Laser (Symmetry Laser)",
"0x012FB": "+1 Laser (Desert Laser)",
"0x00509": "+1 Laser",
"0x012FB": "+1 Laser (Unredirected)",
"0x09F98": "Desert Laser Redirection",
"0x01539": "+1 Laser (Quarry Laser)",
"0x181B3": "+1 Laser (Shadows Laser)",
"0x014BB": "+1 Laser (Keep Laser)",
"0x17C65": "+1 Laser (Monastery Laser)",
"0x032F9": "+1 Laser (Town Laser)",
"0x00274": "+1 Laser (Jungle Laser)",
"0x0C2B2": "+1 Laser (Bunker Laser)",
"0x00BF6": "+1 Laser (Swamp Laser)",
"0x028A4": "+1 Laser (Treehouse Laser)",
"0xFFD03": "+1 Laser (Redirected)",
"0x01539": "+1 Laser",
"0x181B3": "+1 Laser",
"0x014BB": "+1 Laser",
"0x17C65": "+1 Laser",
"0x032F9": "+1 Laser",
"0x00274": "+1 Laser",
"0x0C2B2": "+1 Laser",
"0x00BF6": "+1 Laser",
"0x028A4": "+1 Laser",
"0x17C34": "Mountain Entry",
"0xFFF00": "Bottom Floor Discard Turns On",
}
Expand Down
8 changes: 5 additions & 3 deletions worlds/witness/regions.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
and connects them with the proper requirements
"""
from collections import defaultdict
from typing import TYPE_CHECKING, Dict, List, Set, Tuple
from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple

from BaseClasses import Entrance, Region

Expand Down Expand Up @@ -38,7 +38,7 @@ def __init__(self, player_locations: WitnessPlayerLocations, world: "WitnessWorl
self.created_region_names: Set[str] = set()

@staticmethod
def make_lambda(item_requirement: WitnessRule, world: "WitnessWorld") -> CollectionRule:
def make_lambda(item_requirement: WitnessRule, world: "WitnessWorld") -> Optional[CollectionRule]:
from .rules import _meets_item_requirements

"""
Expand Down Expand Up @@ -79,7 +79,9 @@ def connect_if_possible(self, world: "WitnessWorld", source: str, target: str, r
source_region
)

connection.access_rule = self.make_lambda(final_requirement, world)
rule = self.make_lambda(final_requirement, world)
if rule is not None:
connection.access_rule = rule

source_region.exits.append(connection)
connection.connect(target_region)
Expand Down
138 changes: 91 additions & 47 deletions worlds/witness/rules.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
Defines the rules by which locations can be accessed,
depending on the items received
"""
from typing import TYPE_CHECKING
from collections import Counter
from typing import TYPE_CHECKING, Dict, List, NamedTuple, Optional, Union

from BaseClasses import CollectionState

Expand All @@ -15,50 +16,22 @@
if TYPE_CHECKING:
from . import WitnessWorld

laser_hexes = [
"0x028A4",
"0x00274",
"0x032F9",
"0x01539",
"0x181B3",
"0x0C2B2",
"0x00509",
"0x00BF6",
"0x014BB",
"0x012FB",
"0x17C65",
]


def _can_do_panel_hunt(world: "WitnessWorld") -> CollectionRule:
required = world.panel_hunt_required_count
player = world.player
return lambda state: state.has("+1 Panel Hunt", player, required)


def _has_laser(laser_hex: str, world: "WitnessWorld", redirect_required: bool) -> CollectionRule:
player = world.player
laser_name = static_witness_logic.ENTITIES_BY_HEX[laser_hex]["checkName"]

# Workaround for intentional naming inconsistency
if laser_name == "Symmetry Island Laser":
laser_name = "Symmetry Laser"
class SimpleItemRepresentation(NamedTuple):
item_name: str
item_count: int

if laser_hex == "0x012FB" and redirect_required:
return lambda state: state.has_all([f"+1 Laser ({laser_name})", "Desert Laser Redirection"], player)

return lambda state: state.has(f"+1 Laser ({laser_name})", player)
def _can_do_panel_hunt(world: "WitnessWorld") -> SimpleItemRepresentation:
required = world.panel_hunt_required_count
return SimpleItemRepresentation("+1 Panel Hunt", required)


def _has_lasers(amount: int, world: "WitnessWorld", redirect_required: bool) -> CollectionRule:
laser_lambdas = []

for laser_hex in laser_hexes:
has_laser_lambda = _has_laser(laser_hex, world, redirect_required)

laser_lambdas.append(has_laser_lambda)
if redirect_required:
return lambda state: state.has_from_list(["+1 Laser", "+1 Laser (Redirected)"], world.player, amount)

return lambda state: sum(laser_lambda(state) for laser_lambda in laser_lambdas) >= amount
return lambda state: state.has_from_list(["+1 Laser", "+1 Laser (Unredirected)"], world.player, amount)


def _can_do_expert_pp2(state: CollectionState, world: "WitnessWorld") -> bool:
Expand Down Expand Up @@ -196,7 +169,13 @@ def _can_do_theater_to_tunnels(state: CollectionState, world: "WitnessWorld") ->
)


def _has_item(item: str, world: "WitnessWorld", player: int, player_logic: WitnessPlayerLogic) -> CollectionRule:
def _has_item(item: str, world: "WitnessWorld",
player_logic: WitnessPlayerLogic) -> Union[CollectionRule, SimpleItemRepresentation]:
"""
Convert a single element of a WitnessRule into a CollectionRule, unless it is referring to an item,
in which case we return it as an item-count pair ("SimpleItemRepresentation"). This allows some optimisation later.
"""

assert item not in static_witness_logic.ENTITIES_BY_HEX, "Requirements can no longer contain entity hexes directly."

if item in player_logic.REFERENCE_LOGIC.ALL_REGIONS_BY_NAME:
Expand All @@ -223,27 +202,90 @@ def _has_item(item: str, world: "WitnessWorld", player: int, player_logic: Witne
return lambda state: _can_do_theater_to_tunnels(state, world)

prog_item = static_witness_logic.get_parent_progressive_item(item)
return lambda state: state.has(prog_item, player, player_logic.MULTI_AMOUNTS[item])
needed_amount = player_logic.MULTI_AMOUNTS[item]

simple_rule: SimpleItemRepresentation = SimpleItemRepresentation(prog_item, needed_amount)
return simple_rule


def optimize_requirement_option(requirement_option: List[Union[CollectionRule, SimpleItemRepresentation]])\
-> List[Union[CollectionRule, SimpleItemRepresentation]]:
"""
This optimises out a requirement like [("Progressive Dots": 1), ("Progressive Dots": 2)] to only the "2" version.
"""

direct_items = [rule for rule in requirement_option if isinstance(rule, tuple)]
if not direct_items:
return requirement_option

def _meets_item_requirements(requirements: WitnessRule, world: "WitnessWorld") -> CollectionRule:
max_per_item: Dict[str, int] = Counter()
for item_rule in direct_items:
max_per_item[item_rule[0]] = max(max_per_item[item_rule[0]], item_rule[1])

return [
rule for rule in requirement_option
if not (isinstance(rule, tuple) and rule[1] < max_per_item[rule[0]])
]


def convert_requirement_option(requirement: List[Union[CollectionRule, SimpleItemRepresentation]],
player: int) -> List[CollectionRule]:
"""
Converts a list of CollectionRules and SimpleItemRepresentations to just a list of CollectionRules.
If the list is ONLY SimpleItemRepresentations, we can just return a CollectionRule based on state.has_all_counts()
"""
converted_sublist = []

for rule in requirement:
if not isinstance(rule, tuple):
converted_sublist.append(rule)
continue

collection_rules = [rule for rule in requirement if not isinstance(rule, SimpleItemRepresentation)]
item_rules = [rule for rule in requirement if isinstance(rule, SimpleItemRepresentation)]

if len(item_rules) == 0:
item_rules_converted = []
elif len(item_rules) == 1:
item = item_rules[0][0]
count = item_rules[0][1]
item_rules_converted = [lambda state: state.has(item, player, count)]
else:
item_counts = {item_rule.item_name: item_rule.item_count for item_rule in item_rules}
item_rules_converted = [lambda state: state.has_all_counts(item_counts, player)]

return collection_rules + item_rules_converted


def _meets_item_requirements(requirements: WitnessRule, world: "WitnessWorld") -> Optional[CollectionRule]:
"""
Checks whether item and panel requirements are met for
a panel
Converts a WitnessRule into a CollectionRule.
"""
player = world.player

if requirements == frozenset({frozenset()}):
return None

lambda_conversion = [
[_has_item(item, world, world.player, world.player_logic) for item in subset]
rule_conversion = [
[_has_item(item, world, world.player_logic) for item in subset]
for subset in requirements
]

optimized_rule_conversion = [optimize_requirement_option(sublist) for sublist in rule_conversion]

fully_converted_rules = [convert_requirement_option(sublist, player) for sublist in optimized_rule_conversion]

if len(fully_converted_rules) == 1:
if len(fully_converted_rules[0]) == 1:
return fully_converted_rules[0][0]
return lambda state: all(condition(state) for condition in fully_converted_rules[0])
return lambda state: any(
all(condition(state) for condition in sub_requirement)
for sub_requirement in lambda_conversion
for sub_requirement in fully_converted_rules
)


def make_lambda(entity_hex: str, world: "WitnessWorld") -> CollectionRule:
def make_lambda(entity_hex: str, world: "WitnessWorld") -> Optional[CollectionRule]:
"""
Lambdas are created in a for loop so values need to be captured
"""
Expand All @@ -268,6 +310,8 @@ def set_rules(world: "WitnessWorld") -> None:
entity_hex = associated_entity["entity_hex"]

rule = make_lambda(entity_hex, world)
if rule is None:
continue

location = world.get_location(location)

Expand Down
7 changes: 4 additions & 3 deletions worlds/witness/test/__init__.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
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, cast

from BaseClasses import CollectionState, Entrance, Item, Location, Region

from test.bases import WorldTestBase
from test.general import gen_steps, setup_multiworld
from test.multiworld.test_multiworlds import MultiworldTestBase

from .. import WitnessWorld


Expand Down
Loading

0 comments on commit 906b230

Please sign in to comment.