Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
36 changes: 30 additions & 6 deletions worlds/crosscode/codegen/ast.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,22 +5,46 @@
import typing
import ast

from ..types.condition import Condition
from ..types.condition import AndCondition, Condition, OrCondition, QuestCondition
from ..types.locations import AccessInfo, LocationData
from ..types.regions import Goal, RegionConnection
from ..types.items import ItemData, ItemPoolEntry, ProgressiveChainEntry, SingleItemData
from ..types.shops import ShopData


def create_expression_dataclass(cls) -> ast.Call:
result = ast.Call(
func=ast.Name(cls.__class__.__name__),
args=[],
keywords=[
ast.keyword(arg=key, value=ast.Constant(value))
for key, value in cls.__dict__.items()
if cls.__dataclass_fields__[key].init
],
)
ast.fix_missing_locations(result)

return result

def create_expression_condition(condition: Condition) -> ast.Call:
"""
Create an expression representing a singular condition.
"""
result = ast.Call(
func=ast.Name(condition.__class__.__name__),
args=[],
keywords=[ast.keyword(arg=key, value=ast.Constant(value)) for key, value in condition.__dict__.items()],
)
if isinstance(condition, OrCondition) or isinstance(condition, AndCondition):
# we handle these conditions in a special way, since their lists cannot be encoded by ast.Constant
result = ast.Call(
func=ast.Name(condition.__class__.__name__),
args=[],
keywords=[
ast.keyword(
arg="subconditions",
value=create_expression_condition_list(condition.subconditions)
)
],
)
else:
# this block should handle most cases, unless you make a condition that has a complex type
result = create_expression_dataclass(condition)
ast.fix_missing_locations(result)

return result
Expand Down
5 changes: 2 additions & 3 deletions worlds/crosscode/logic.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -15,7 +15,6 @@ def condition_satisfied(
"""
Factory function. Return value is a rule that checks whether all the conditions are satisfied.
"""
def conditions_satisfied_internal(state: CollectionState) -> bool:
return all(c.satisfied(state, player, location, cond_args) for c in conditions)
callbacks = [c.satisfied(player, location, cond_args) for c in conditions]

return conditions_satisfied_internal
return lambda state: all(map(lambda x: x(state), callbacks))
117 changes: 69 additions & 48 deletions worlds/crosscode/types/condition.py
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
import typing
import abc
from dataclasses import field, dataclass
from dataclasses import dataclass, field

from BaseClasses import CollectionState
from ..options import ShopReceiveMode
Expand All@@ -23,63 +23,75 @@ class LogicDict(typing.TypedDict):

class Condition(abc.ABC):
@abc.abstractmethod
def satisfied(self, state: CollectionState, player: int, location: int | None, args: LogicDict) -> bool:
def satisfied(self, player: int, location: int | None, args: LogicDict) -> typing.Callable[[CollectionState], bool]:
pass

@dataclass
class ItemCondition(Condition):
item_name: str
amount: int = 1

def satisfied(self, state: CollectionState, player: int, location: int | None, args: LogicDict) -> bool:
def satisfied(self, player: int, location: int | None, args: LogicDict) -> typing.Callable[[CollectionState], bool]:
target = self.amount
if self.item_name in args["keyrings"]:
target = 1

replacements = args["item_progressive_replacements"]

if self.item_name in replacements:
for prog_item_name, quantity in replacements[self.item_name]:
if state.has(prog_item_name, player, quantity):
return True
def callback(state: CollectionState) -> bool:
for prog_item_name, quantity in replacements[self.item_name]:
if state.has(prog_item_name, player, quantity):
return True
return state.has(self.item_name, player, target)

return state.has(self.item_name, player, target)
return callback

return lambda state: state.has(self.item_name, player, target)

@dataclass
class QuestCondition(Condition):
quest_name: str
event_name: str = field(init=False)

def __post_init__(self):
self.event_name = f"{self.quest_name} (Event)"

def satisfied(self, state: CollectionState, player: int, location: int | None, args: LogicDict) -> bool:
return state.has(f"{self.quest_name} (Event)", player)
def satisfied(self, player: int, location: int | None, args: LogicDict) -> typing.Callable[[CollectionState], bool]:
return lambda state: state.has(self.event_name, player)

@dataclass
class LocationCondition(Condition):
location_name: str
event_name: str = field(init=False)

def __post_init__(self):
self.event_name = f"{self.location_name} (Event)"

def satisfied(self, state: CollectionState, player: int, location: int | None, args: LogicDict) -> bool:
return state.has(f"{self.location_name} (Event)", player)
def satisfied(self, player: int, location: int | None, args: LogicDict) -> typing.Callable[[CollectionState], bool]:
return lambda state: state.has(self.event_name, player)

@dataclass
class RegionCondition(Condition):
target_mode: typing.Optional[str]
region_name: str

def satisfied(self, state: CollectionState, player: int, location: int | None, args: LogicDict) -> bool:
def satisfied(self, player: int, location: int | None, args: LogicDict) -> typing.Callable[[CollectionState], bool]:
# target_mode == None means that it matches in all modes.
# so if target_mode == None, check if we can reach that region.
# else, if the target mode matches, also check if we can reach that region.
# otherwise, if the target mode does not match, return true (assume that it's being ANDed with other conditions)

mode: str = args["mode"]
if self.target_mode is None or mode == self.target_mode:
return state.can_reach_region(self.region_name, player)
return lambda state: state.can_reach_region(self.region_name, player)

return True
return lambda _: True

@dataclass
class AnyElementCondition(Condition):
def satisfied(self, state: CollectionState, player: int, location: int | None, args: LogicDict) -> bool:
return any([
def satisfied(self, player: int, location: int | None, args: LogicDict) -> typing.Callable[[CollectionState], bool]:
return lambda state: any([
state.has("Heat", player),
state.has("Cold", player),
state.has("Shock", player),
Expand All@@ -90,43 +102,50 @@ def satisfied(self, state: CollectionState, player: int, location: int | None, a
class OrCondition(Condition):
subconditions: list[Condition]

def satisfied(self, state: CollectionState, player: int, location: int | None, args: LogicDict) -> bool:
return any(map(lambda x: x.satisfied(state, player, location, args), self.subconditions))
def satisfied(self, player: int, location: int | None, args: LogicDict) -> typing.Callable[[CollectionState], bool]:
callbacks = [x.satisfied(player, location, args) for x in self.subconditions]
return lambda state: any(map(lambda x: x(state), callbacks))

@dataclass
class AndCondition(Condition):
subconditions: list[Condition]

def satisfied(self, state: CollectionState, player: int, location: int | None, args: LogicDict) -> bool:
return all(map(lambda x: x.satisfied(state, player, location, args), self.subconditions))
def satisfied(self, player: int, location: int | None, args: LogicDict) -> typing.Callable[[CollectionState], bool]:
callbacks = [x.satisfied(player, location, args) for x in self.subconditions]
return lambda state: all(map(lambda x: x(state), callbacks))

@dataclass
class VariableCondition(Condition):
name: str

def satisfied(self, state: CollectionState, player: int, location: int | None, args: LogicDict) -> bool:
def satisfied(self, player: int, location: int | None, args: LogicDict) -> typing.Callable[[CollectionState], bool]:
variables = args["variables"]
variable_definitions = args["variable_definitions"]

if self.name not in variables:
return True
return lambda _: True

for value in variables[self.name]:
if not all(map(lambda c: c.satisfied(state, player, location, args), variable_definitions[self.name][value])):
return False
callbacks = sum(
[
[
x.satisfied(player, location, args)
for x in variable_definitions[self.name][value]
] for value in variables[self.name]
],
start=[]
)

return True
return lambda state: all(map(lambda x: x(state), callbacks))

@dataclass
class VariableEntryCondition(Condition):
name: str
value: str
desired: bool

def satisfied(self, state: CollectionState, player: int, location: int | None, args: LogicDict) -> bool:
variables = args["variables"]

return (self.value in variables[self.name]) == self.desired
def satisfied(self, player: int, location: int | None, args: LogicDict) -> typing.Callable[[CollectionState], bool]:
valid = (self.value in args["variables"][self.name]) == self.desired
return lambda _: valid

@dataclass
class ChestKeyCondition(Condition):
Expand All@@ -138,7 +157,7 @@ class ChestKeyCondition(Condition):
"Gold": "Radiant Key",
}

def satisfied(self, state: CollectionState, player: int, location: int | None, args: LogicDict) -> bool:
def satisfied(self, player: int, location: int | None, args: LogicDict) -> typing.Callable[[CollectionState], bool]:
chest_levels = args["chest_clearance_levels"]

if location is None:
Expand All@@ -147,42 +166,44 @@ def satisfied(self, state: CollectionState, player: int, location: int | None, a
level: str = chest_levels.get(location, self.default_level)

if level == "Default":
return True
return lambda _: True

return state.has(ChestKeyCondition.clearance_items[level], player)
return lambda state: state.has(ChestKeyCondition.clearance_items[level], player)

@dataclass
class ShopSlotCondition(Condition):
shop_name: str
item_id: int

def satisfied(self, state: CollectionState, player: int, location: int | None, args: LogicDict) -> bool:
def satisfied(self, player: int, location: int | None, args: LogicDict) -> typing.Callable[[CollectionState], bool]:
if args["shop_receive_mode"] is None:
return True
return lambda _: True
if args["shop_receive_mode"] == ShopReceiveMode.option_per_item_type:
return state.has(args["shop_unlock_by_id"][self.item_id].item.name, player)
return lambda state: state.has(args["shop_unlock_by_id"][self.item_id].item.name, player)
if args["shop_receive_mode"] == ShopReceiveMode.option_per_shop:
return state.has(args["shop_unlock_by_shop"][self.shop_name].item.name, player)
return lambda state: state.has(args["shop_unlock_by_shop"][self.shop_name].item.name, player)
if args["shop_receive_mode"] == ShopReceiveMode.option_per_slot:
return state.has(args["shop_unlock_by_shop_and_id"][self.shop_name, self.item_id].item.name, player)
return True
return lambda state: state.has(args["shop_unlock_by_shop_and_id"][self.shop_name, self.item_id].item.name, player)
return lambda _: True

@dataclass
class BotanicsCompletionCondition(Condition):
amount: float

def satisfied(self, state: CollectionState, player: int, location: int | None, args: LogicDict) -> bool:
collected = sum([
amount
for region, amount in args["region_botanics_amounts"].items()
if state.can_reach_region(region, player)
])
def satisfied(self, player: int, location: int | None, args: LogicDict) -> typing.Callable[[CollectionState], bool]:
def satisfied_internal(state: CollectionState):
collected = sum([
amount
for region, amount in args["region_botanics_amounts"].items()
if state.can_reach_region(region, player)
])

return collected / args["botanics_completion_amount"] >= self.amount
return collected / args["botanics_completion_amount"] >= self.amount
return satisfied_internal

class NeverCondition(Condition):
def satisfied(self, state: CollectionState, player: int, location: int | None, args: LogicDict) -> bool:
return False
def satisfied(self, player: int, location: int | None, args: LogicDict) -> typing.Callable[[CollectionState], bool]:
return lambda _: False

__all__ = [
"Condition",
Expand Down
Loading