File size: 5,610 Bytes
2113a6a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
"""Base pattern definitions for the ability parser."""

import re
from dataclasses import dataclass, field
from enum import Enum
from typing import Any, Callable, Dict, List, Match, Optional, Tuple


class PatternPhase(Enum):
    """Phases of parsing, executed in order."""

    TRIGGER = 1  # Determine when ability activates
    CONDITION = 2  # Extract gating conditions
    EFFECT = 3  # Extract effects/actions
    MODIFIER = 4  # Apply flags (optional, once per turn, duration)


@dataclass
class Pattern:
    """A declarative pattern definition for ability text parsing.



    Patterns are matched in priority order within each phase.

    Lower priority number = higher precedence.

    """

    name: str
    phase: PatternPhase
    priority: int = 100

    # Matching criteria (at least one must be specified)
    regex: Optional[str] = None  # Regex pattern to search for
    keywords: List[str] = field(default_factory=list)  # Must contain ALL keywords
    any_keywords: List[str] = field(default_factory=list)  # Must contain ANY keyword

    # Filter conditions
    requires: List[str] = field(default_factory=list)  # Context must contain ALL
    excludes: List[str] = field(default_factory=list)  # Context must NOT contain ANY
    look_ahead_excludes: List[str] = field(default_factory=list)  # Text after match must NOT contain

    # Behavior
    exclusive: bool = False  # If True, stops further matching in this phase
    consumes: bool = False  # If True, removes matched text from further processing

    # Output specification
    output_type: Optional[str] = None  # e.g., "TriggerType.ON_PLAY", "EffectType.DRAW"
    output_value: Optional[Any] = None  # Default value for effect
    output_params: Dict[str, Any] = field(default_factory=dict)  # Additional parameters

    # Custom extraction (for complex patterns)
    extractor: Optional[Callable[[str, Match], Dict[str, Any]]] = None

    def __post_init__(self):
        """Compile regex if provided."""
        self._compiled_regex = re.compile(self.regex) if self.regex else None

    def matches(self, text: str, context: Optional[str] = None) -> Optional[Match]:
        """Check if pattern matches the text.



        Args:

            text: The text to match against

            context: Full sentence context for requires/excludes checks



        Returns:

            Match object if pattern matches, None otherwise

        """
        ctx = context or text

        # Check requires
        if self.requires and not all(kw in ctx for kw in self.requires):
            return None

        # Check excludes
        if self.excludes and any(kw in ctx for kw in self.excludes):
            return None

        # Check keywords (must contain ALL)
        if self.keywords and not all(kw in text for kw in self.keywords):
            return None

        # Check any_keywords (must contain ANY)
        if self.any_keywords and not any(kw in text for kw in self.any_keywords):
            return None

        # Check regex
        if self._compiled_regex:
            m = self._compiled_regex.search(text)
            if m:
                # Check look-ahead excludes
                if self.look_ahead_excludes:
                    look_ahead = text[m.start() : m.start() + 20]
                    if any(kw in look_ahead for kw in self.look_ahead_excludes):
                        return None
                return m
            return None

        # If no regex but keywords matched, return a pseudo-match
        if self.keywords or self.any_keywords:
            # Find first keyword position
            for kw in self.keywords or self.any_keywords:
                if kw in text:
                    idx = text.find(kw)
                    # Create a fake match-like object
                    return _KeywordMatch(kw, idx)

        return None

    def extract(self, text: str, match: Match) -> Dict[str, Any]:
        """Extract structured data from a match.



        Returns dict with:

            - 'type': output_type if specified

            - 'value': extracted value or output_value

            - 'params': output_params merged with any extracted params

        """
        if self.extractor:
            return self.extractor(text, match)

        result = {}
        if self.output_type:
            result["type"] = self.output_type
        if self.output_value is not None:
            result["value"] = self.output_value
        if self.output_params:
            result["params"] = self.output_params.copy()

        # Try to extract numeric value from match groups
        if match.lastindex and match.lastindex >= 1:
            try:
                result["value"] = int(match.group(1))
            except (ValueError, TypeError):
                # Don't assign non-numeric strings to 'value' as it breaks bytecode compilation
                pass

        return result


class _KeywordMatch:
    """Fake match object for keyword-based patterns."""

    def __init__(self, keyword: str, start: int):
        self._keyword = keyword
        self._start = start
        self.lastindex = None

    def start(self) -> int:
        return self._start

    def end(self) -> int:
        return self._start + len(self._keyword)

    def span(self) -> Tuple[int, int]:
        return (self.start(), self.end())

    def group(self, n: int = 0) -> str:
        return self._keyword if n == 0 else ""

    def groups(self) -> Tuple:
        return ()