2024-05-08 01:40:53 +00:00
|
|
|
from functools import partial, wraps
|
2024-05-05 14:14:54 +00:00
|
|
|
from logging import getLogger
|
|
|
|
from random import random
|
|
|
|
from typing import Callable, Dict, List, Optional
|
|
|
|
|
|
|
|
from pydantic import Field
|
2024-05-05 18:54:39 +00:00
|
|
|
from rule_engine import Rule
|
2024-05-05 14:14:54 +00:00
|
|
|
from yaml import Loader, load
|
|
|
|
|
2024-05-16 04:12:06 +00:00
|
|
|
from adventure.game_system import FormatPerspective, GameSystem
|
2024-05-09 02:11:16 +00:00
|
|
|
from adventure.models.entity import (
|
2024-05-05 22:46:24 +00:00
|
|
|
Attributes,
|
|
|
|
AttributeValue,
|
|
|
|
World,
|
2024-05-16 04:12:06 +00:00
|
|
|
WorldEntity,
|
2024-05-05 22:46:24 +00:00
|
|
|
dataclass,
|
|
|
|
)
|
2024-05-05 14:14:54 +00:00
|
|
|
from adventure.plugins import get_plugin_function
|
|
|
|
|
|
|
|
logger = getLogger(__name__)
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass
|
|
|
|
class LogicLabel:
|
|
|
|
backstory: str
|
2024-05-05 22:46:24 +00:00
|
|
|
description: str | None = None
|
2024-05-05 14:14:54 +00:00
|
|
|
|
|
|
|
|
|
|
|
@dataclass
|
|
|
|
class LogicRule:
|
|
|
|
chance: float = 1.0
|
|
|
|
group: Optional[str] = None
|
2024-05-05 22:46:24 +00:00
|
|
|
match: Optional[Attributes] = None
|
2024-05-05 14:14:54 +00:00
|
|
|
remove: Optional[List[str]] = None
|
|
|
|
rule: Optional[str] = None
|
2024-05-05 22:46:24 +00:00
|
|
|
set: Optional[Attributes] = None
|
2024-05-05 14:14:54 +00:00
|
|
|
trigger: Optional[List[str]] = None
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass
|
|
|
|
class LogicTable:
|
|
|
|
rules: List[LogicRule]
|
2024-05-05 22:46:24 +00:00
|
|
|
labels: Dict[str, Dict[AttributeValue, LogicLabel]] = Field(default_factory=dict)
|
2024-05-05 14:14:54 +00:00
|
|
|
|
|
|
|
|
2024-05-16 04:12:06 +00:00
|
|
|
LogicTrigger = Callable[[WorldEntity], None]
|
2024-05-08 01:40:53 +00:00
|
|
|
TriggerTable = Dict[str, LogicTrigger]
|
2024-05-05 14:14:54 +00:00
|
|
|
|
|
|
|
|
|
|
|
def update_attributes(
|
2024-05-16 04:12:06 +00:00
|
|
|
entity: WorldEntity,
|
2024-05-05 14:14:54 +00:00
|
|
|
rules: LogicTable,
|
|
|
|
triggers: TriggerTable,
|
2024-05-16 04:12:06 +00:00
|
|
|
) -> None:
|
2024-05-05 14:14:54 +00:00
|
|
|
entity_type = entity.__class__.__name__.lower()
|
|
|
|
skip_groups = set()
|
|
|
|
|
|
|
|
for rule in rules.rules:
|
|
|
|
if rule.group:
|
|
|
|
if rule.group in skip_groups:
|
2024-05-05 22:46:24 +00:00
|
|
|
logger.debug("already ran a rule from group %s, skipping", rule.group)
|
2024-05-05 14:14:54 +00:00
|
|
|
continue
|
|
|
|
|
|
|
|
typed_attributes = {
|
2024-05-16 04:12:06 +00:00
|
|
|
**entity.attributes,
|
2024-05-05 14:14:54 +00:00
|
|
|
"type": entity_type,
|
|
|
|
}
|
|
|
|
|
|
|
|
if rule.rule:
|
|
|
|
# TODO: pre-compile rules
|
|
|
|
rule_impl = Rule(rule.rule)
|
2024-05-05 18:54:39 +00:00
|
|
|
if not rule_impl.matches(
|
|
|
|
{
|
|
|
|
"attributes": typed_attributes,
|
|
|
|
}
|
|
|
|
):
|
2024-05-05 14:14:54 +00:00
|
|
|
logger.debug("logic rule did not match attributes: %s", rule.rule)
|
|
|
|
continue
|
|
|
|
|
2024-05-05 18:54:39 +00:00
|
|
|
if rule.match and not (rule.match.items() <= typed_attributes.items()):
|
2024-05-05 14:14:54 +00:00
|
|
|
logger.debug("logic did not match attributes: %s", rule.match)
|
|
|
|
continue
|
|
|
|
|
|
|
|
logger.info("matched logic: %s", rule.match)
|
|
|
|
if rule.chance < 1:
|
|
|
|
if random() > rule.chance:
|
|
|
|
logger.info("logic skipped by chance: %s", rule.chance)
|
|
|
|
continue
|
|
|
|
|
2024-05-05 22:46:24 +00:00
|
|
|
if rule.group:
|
|
|
|
skip_groups.add(rule.group)
|
|
|
|
|
2024-05-05 14:14:54 +00:00
|
|
|
for key in rule.remove or []:
|
2024-05-16 04:12:06 +00:00
|
|
|
entity.attributes.pop(key, None)
|
2024-05-05 14:14:54 +00:00
|
|
|
|
|
|
|
if rule.set:
|
2024-05-16 04:12:06 +00:00
|
|
|
entity.attributes.update(rule.set)
|
2024-05-05 14:14:54 +00:00
|
|
|
logger.info("logic set state: %s", rule.set)
|
|
|
|
|
2024-05-08 01:40:53 +00:00
|
|
|
if rule.trigger:
|
|
|
|
for trigger in rule.trigger:
|
|
|
|
if trigger in triggers:
|
2024-05-16 04:12:06 +00:00
|
|
|
triggers[trigger](entity)
|
2024-05-05 14:14:54 +00:00
|
|
|
|
|
|
|
|
2024-05-05 18:54:39 +00:00
|
|
|
def update_logic(
|
|
|
|
world: World, step: int, rules: LogicTable, triggers: TriggerTable
|
|
|
|
) -> None:
|
2024-05-05 14:14:54 +00:00
|
|
|
for room in world.rooms:
|
2024-05-16 04:12:06 +00:00
|
|
|
update_attributes(room, rules=rules, triggers=triggers)
|
2024-05-05 14:14:54 +00:00
|
|
|
for actor in room.actors:
|
2024-05-16 04:12:06 +00:00
|
|
|
update_attributes(actor, rules=rules, triggers=triggers)
|
2024-05-05 14:14:54 +00:00
|
|
|
for item in actor.items:
|
2024-05-16 04:12:06 +00:00
|
|
|
update_attributes(item, rules=rules, triggers=triggers)
|
2024-05-05 14:14:54 +00:00
|
|
|
for item in room.items:
|
2024-05-16 04:12:06 +00:00
|
|
|
update_attributes(item, rules=rules, triggers=triggers)
|
2024-05-05 14:14:54 +00:00
|
|
|
|
|
|
|
logger.info("updated world attributes")
|
|
|
|
|
|
|
|
|
2024-05-16 04:12:06 +00:00
|
|
|
def format_logic(
|
|
|
|
entity: WorldEntity,
|
|
|
|
rules: LogicTable,
|
|
|
|
perspective: FormatPerspective = FormatPerspective.SECOND_PERSON,
|
|
|
|
) -> str:
|
2024-05-05 14:14:54 +00:00
|
|
|
labels = []
|
|
|
|
|
2024-05-16 04:12:06 +00:00
|
|
|
for attribute, value in entity.attributes.items():
|
2024-05-05 14:14:54 +00:00
|
|
|
if attribute in rules.labels and value in rules.labels[attribute]:
|
|
|
|
label = rules.labels[attribute][value]
|
2024-05-16 04:12:06 +00:00
|
|
|
if perspective == FormatPerspective.SECOND_PERSON:
|
2024-05-05 14:14:54 +00:00
|
|
|
labels.append(label.backstory)
|
2024-05-16 04:12:06 +00:00
|
|
|
elif perspective == FormatPerspective.THIRD_PERSON and label.description:
|
2024-05-05 14:14:54 +00:00
|
|
|
labels.append(label.description)
|
2024-05-05 22:46:24 +00:00
|
|
|
else:
|
|
|
|
logger.debug("label has no relevant description: %s", label)
|
2024-05-05 14:14:54 +00:00
|
|
|
|
|
|
|
if len(labels) > 0:
|
2024-05-12 20:47:18 +00:00
|
|
|
logger.debug("adding attribute labels: %s", labels)
|
2024-05-05 14:14:54 +00:00
|
|
|
|
|
|
|
return " ".join(labels)
|
|
|
|
|
|
|
|
|
2024-05-16 04:12:06 +00:00
|
|
|
def load_logic(filename: str):
|
2024-05-05 14:14:54 +00:00
|
|
|
logger.info("loading logic from file: %s", filename)
|
|
|
|
with open(filename) as file:
|
|
|
|
logic_rules = LogicTable(**load(file, Loader=Loader))
|
2024-05-08 01:40:53 +00:00
|
|
|
logic_triggers = {}
|
|
|
|
|
|
|
|
for rule in logic_rules.rules:
|
|
|
|
if rule.trigger:
|
|
|
|
for trigger in rule.trigger:
|
|
|
|
logic_triggers[trigger] = get_plugin_function(trigger)
|
2024-05-05 14:14:54 +00:00
|
|
|
|
|
|
|
logger.info("initialized logic system")
|
2024-05-16 04:12:06 +00:00
|
|
|
system_simulate = wraps(update_logic)(
|
|
|
|
partial(update_logic, rules=logic_rules, triggers=logic_triggers)
|
|
|
|
)
|
|
|
|
system_format = wraps(format_logic)(partial(format_logic, rules=logic_rules))
|
|
|
|
|
|
|
|
return GameSystem(
|
|
|
|
format=system_format,
|
|
|
|
simulate=system_simulate,
|
2024-05-05 14:14:54 +00:00
|
|
|
)
|