-
-
Notifications
You must be signed in to change notification settings - Fork 88
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Conditionals with boolean algebra (#487)
- Loading branch information
Showing
7 changed files
with
462 additions
and
12 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
import ast | ||
import re | ||
from typing import Callable | ||
|
||
replacements = {"!": "not ", "^": " and ", "v": " or "} | ||
|
||
pattern = re.compile(r"\!|\^|\bv\b") | ||
|
||
|
||
def replace_operators(expr: str) -> str: | ||
# preprocess the expression adding support for classical logical operators | ||
def match_func(match): | ||
return replacements[match.group(0)] | ||
|
||
return pattern.sub(match_func, expr) | ||
|
||
|
||
def custom_not(predicate: Callable) -> Callable: | ||
def decorated(*args, **kwargs) -> bool: | ||
return not predicate(*args, **kwargs) | ||
|
||
decorated.__name__ = f"not({predicate.__name__})" | ||
unique_key = getattr(predicate, "unique_key", "") | ||
decorated.unique_key = f"not({unique_key})" # type: ignore[attr-defined] | ||
return decorated | ||
|
||
|
||
def _unique_key(left, right, operator) -> str: | ||
left_key = getattr(left, "unique_key", "") | ||
right_key = getattr(right, "unique_key", "") | ||
return f"{left_key} {operator} {right_key}" | ||
|
||
|
||
def custom_and(left: Callable, right: Callable) -> Callable: | ||
def decorated(*args, **kwargs) -> bool: | ||
return left(*args, **kwargs) and right(*args, **kwargs) # type: ignore[no-any-return] | ||
|
||
decorated.__name__ = f"({left.__name__} and {right.__name__})" | ||
decorated.unique_key = _unique_key(left, right, "and") # type: ignore[attr-defined] | ||
return decorated | ||
|
||
|
||
def custom_or(left: Callable, right: Callable) -> Callable: | ||
def decorated(*args, **kwargs) -> bool: | ||
return left(*args, **kwargs) or right(*args, **kwargs) # type: ignore[no-any-return] | ||
|
||
decorated.__name__ = f"({left.__name__} or {right.__name__})" | ||
decorated.unique_key = _unique_key(left, right, "or") # type: ignore[attr-defined] | ||
return decorated | ||
|
||
|
||
def build_expression(node, variable_hook, operator_mapping): | ||
if isinstance(node, ast.BoolOp): | ||
# Handle `and` / `or` operations | ||
operator_fn = operator_mapping[type(node.op)] | ||
left_expr = build_expression(node.values[0], variable_hook, operator_mapping) | ||
for right in node.values[1:]: | ||
right_expr = build_expression(right, variable_hook, operator_mapping) | ||
left_expr = operator_fn(left_expr, right_expr) | ||
return left_expr | ||
elif isinstance(node, ast.UnaryOp) and isinstance(node.op, ast.Not): | ||
# Handle `not` operation | ||
operand_expr = build_expression(node.operand, variable_hook, operator_mapping) | ||
return operator_mapping[type(node.op)](operand_expr) | ||
elif isinstance(node, ast.Name): | ||
# Handle variables by calling the variable_hook | ||
return variable_hook(node.id) | ||
else: | ||
raise ValueError(f"Unsupported expression structure: {node.__class__.__name__}") | ||
|
||
|
||
def parse_boolean_expr(expr, variable_hook, operator_mapping): | ||
"""Parses the expression into an AST and build a custom expression tree""" | ||
expr = replace_operators(expr) | ||
tree = ast.parse(expr, mode="eval") | ||
return build_expression(tree.body, variable_hook, operator_mapping) | ||
|
||
|
||
operator_mapping = {ast.Or: custom_or, ast.And: custom_and, ast.Not: custom_not} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,102 @@ | ||
""" | ||
Lord of the Rings Quest - Boolean algebra | ||
========================================= | ||
Example that demonstrates the use of Boolean algebra in conditions. | ||
""" | ||
|
||
from statemachine import State | ||
from statemachine import StateMachine | ||
from statemachine.exceptions import TransitionNotAllowed | ||
|
||
|
||
class LordOfTheRingsQuestStateMachine(StateMachine): | ||
# Define the states | ||
shire = State("In the Shire", initial=True) | ||
bree = State("In Bree") | ||
rivendell = State("At Rivendell") | ||
moria = State("In Moria") | ||
lothlorien = State("In Lothlorien") | ||
mordor = State("In Mordor") | ||
mount_doom = State("At Mount Doom", final=True) | ||
|
||
# Define transitions with Boolean conditions | ||
start_journey = shire.to(bree, cond="frodo_has_ring and !sauron_alive") | ||
meet_elves = bree.to(rivendell, cond="gandalf_present and frodo_has_ring") | ||
enter_moria = rivendell.to(moria, cond="orc_army_nearby or frodo_has_ring") | ||
reach_lothlorien = moria.to(lothlorien, cond="!orc_army_nearby") | ||
journey_to_mordor = lothlorien.to(mordor, cond="frodo_has_ring and sam_is_loyal") | ||
destroy_ring = mordor.to(mount_doom, cond="frodo_has_ring and frodo_resists_ring") | ||
|
||
# Conditions (attributes representing the state of conditions) | ||
frodo_has_ring: bool = True | ||
sauron_alive: bool = True # Initially, Sauron is alive | ||
gandalf_present: bool = False # Gandalf is not present at the start | ||
orc_army_nearby: bool = False | ||
sam_is_loyal: bool = True | ||
frodo_resists_ring: bool = False # Initially, Frodo is not resisting the ring | ||
|
||
|
||
# %% | ||
# Playing | ||
|
||
quest = LordOfTheRingsQuestStateMachine() | ||
|
||
# Track state changes | ||
print(f"Current State: {quest.current_state.id}") # Should start at "shire" | ||
|
||
# Step 1: Start the journey | ||
quest.sauron_alive = False # Assume Sauron is no longer alive | ||
try: | ||
quest.start_journey() | ||
print(f"Current State: {quest.current_state.id}") # Should be "bree" | ||
except TransitionNotAllowed: | ||
print("Unable to start journey: conditions not met.") | ||
|
||
# Step 2: Meet the elves in Rivendell | ||
quest.gandalf_present = True # Gandalf is now present | ||
try: | ||
quest.meet_elves() | ||
print(f"Current State: {quest.current_state.id}") # Should be "rivendell" | ||
except TransitionNotAllowed: | ||
print("Unable to meet elves: conditions not met.") | ||
|
||
# Step 3: Enter Moria | ||
quest.orc_army_nearby = True # Orc army is nearby | ||
try: | ||
quest.enter_moria() | ||
print(f"Current State: {quest.current_state.id}") # Should be "moria" | ||
except TransitionNotAllowed: | ||
print("Unable to enter Moria: conditions not met.") | ||
|
||
# Step 4: Reach Lothlorien | ||
quest.orc_army_nearby = False # Orcs are no longer nearby | ||
try: | ||
quest.reach_lothlorien() | ||
print(f"Current State: {quest.current_state.id}") # Should be "lothlorien" | ||
except TransitionNotAllowed: | ||
print("Unable to reach Lothlorien: conditions not met.") | ||
|
||
# Step 5: Journey to Mordor | ||
try: | ||
quest.journey_to_mordor() | ||
print(f"Current State: {quest.current_state.id}") # Should be "mordor" | ||
except TransitionNotAllowed: | ||
print("Unable to journey to Mordor: conditions not met.") | ||
|
||
# Step 6: Fight with Smeagol | ||
try: | ||
quest.destroy_ring() | ||
print(f"Current State: {quest.current_state.id}") # Should be "mount_doom" | ||
except TransitionNotAllowed: | ||
print("Unable to destroy the ring: conditions not met.") | ||
|
||
|
||
# Step 7: Destroy the ring at Mount Doom | ||
quest.frodo_resists_ring = True # Frodo is now resisting the ring | ||
try: | ||
quest.destroy_ring() | ||
print(f"Current State: {quest.current_state.id}") # Should be "mount_doom" | ||
except TransitionNotAllowed: | ||
print("Unable to destroy the ring: conditions not met.") |
Oops, something went wrong.