Add MTG rules engine source files
This commit is contained in:
@@ -0,0 +1,319 @@
|
||||
"""
|
||||
Magic: The Gathering Rules Engine - Keyword Validator
|
||||
|
||||
This module provides validation functionality for Magic keywords.
|
||||
It checks if keywords are valid, finds all keywords in text, and
|
||||
provides detailed analysis of keyword usage.
|
||||
|
||||
Usage:
|
||||
from mtg_rules_engine.validator import KeywordValidator
|
||||
|
||||
# Check if a keyword is valid
|
||||
validator = KeywordValidator()
|
||||
if validator.is_valid_keyword("flying"):
|
||||
print("flying is a valid keyword")
|
||||
|
||||
# Find all keywords in text
|
||||
text = "Flying creatures can't be blocked except by flying creatures."
|
||||
found = validator.find_keywords_in_text(text)
|
||||
print(f"Found keywords: {found}")
|
||||
|
||||
# Validate card text
|
||||
card_text = "Flying creature. Haste. Flying and trample."
|
||||
errors = validator.validate_card_text(card_text)
|
||||
print(f"Validation errors: {errors}")
|
||||
"""
|
||||
|
||||
from typing import Dict, List, Optional, Set, Tuple, Any
|
||||
from .keywords import (
|
||||
ABILITY_WORDS,
|
||||
KEYWORD_ACTIONS,
|
||||
KEYWORD_ABILITIES,
|
||||
KEYWORD_VARIANTS,
|
||||
get_all_keywords,
|
||||
get_keyword_info,
|
||||
is_valid_keyword,
|
||||
get_keyword_type,
|
||||
)
|
||||
|
||||
|
||||
class KeywordValidator:
|
||||
"""
|
||||
Validates and analyzes Magic keyword usage in rules text and card text.
|
||||
|
||||
This validator can:
|
||||
- Check if a keyword is valid
|
||||
- Find all keywords in a given text
|
||||
- Validate card text for unrecognized keywords
|
||||
- Analyze keyword patterns in rules text
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize the validator with all keywords."""
|
||||
self._all_keywords: Set[str] = get_all_keywords()
|
||||
self._keyword_info_cache: Dict[str, Dict[str, Any]] = {}
|
||||
|
||||
def is_valid_keyword(self, keyword: str) -> bool:
|
||||
"""
|
||||
Check if a keyword is a valid Magic keyword.
|
||||
|
||||
Args:
|
||||
keyword: The keyword to check
|
||||
|
||||
Returns:
|
||||
True if the keyword is valid, False otherwise
|
||||
"""
|
||||
return keyword.lower() in self._all_keywords
|
||||
|
||||
def get_keyword_type(self, keyword: str) -> str:
|
||||
"""
|
||||
Get the type of a keyword (ability_word, keyword_action, or keyword_ability).
|
||||
|
||||
Args:
|
||||
keyword: The keyword to check
|
||||
|
||||
Returns:
|
||||
The type of the keyword, or "unknown" if not found
|
||||
"""
|
||||
return get_keyword_type(keyword.lower())
|
||||
|
||||
def get_keyword_info(self, keyword: str) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
Get detailed information about a keyword.
|
||||
|
||||
Args:
|
||||
keyword: The keyword to look up
|
||||
|
||||
Returns:
|
||||
Dictionary with keyword information, or None if not found
|
||||
"""
|
||||
keyword = keyword.lower()
|
||||
if keyword not in self._keyword_info_cache:
|
||||
self._keyword_info_cache[keyword] = get_keyword_info(keyword)
|
||||
return self._keyword_info_cache[keyword]
|
||||
|
||||
def find_keywords_in_text(self, text: str,
|
||||
case_sensitive: bool = False) -> Dict[str, int]:
|
||||
"""
|
||||
Find all keywords present in the given text.
|
||||
|
||||
Args:
|
||||
text: The text to search
|
||||
case_sensitive: Whether the search should be case-sensitive
|
||||
|
||||
Returns:
|
||||
Dictionary mapping keyword names to their counts in the text
|
||||
"""
|
||||
keywords_found: Dict[str, int] = {}
|
||||
|
||||
if case_sensitive:
|
||||
words = text.split()
|
||||
for word in words:
|
||||
# Clean punctuation from words
|
||||
clean_word = ''.join(c for c in word if c.isalnum() or c == "'")
|
||||
if clean_word in self._all_keywords:
|
||||
keywords_found[clean_word] = keywords_found.get(clean_word, 0) + 1
|
||||
else:
|
||||
words = text.lower().split()
|
||||
for word in words:
|
||||
# Clean punctuation from words
|
||||
clean_word = ''.join(c for c in word if c.isalnum() or c == "'")
|
||||
if clean_word in self._all_keywords:
|
||||
keywords_found[clean_word] = keywords_found.get(clean_word, 0) + 1
|
||||
|
||||
return keywords_found
|
||||
|
||||
def find_keyword_occurrences(self, text: str,
|
||||
keyword: str,
|
||||
case_sensitive: bool = False) -> List[Tuple[int, int]]:
|
||||
"""
|
||||
Find all occurrences of a keyword in the given text.
|
||||
|
||||
Args:
|
||||
text: The text to search
|
||||
keyword: The keyword to find
|
||||
case_sensitive: Whether the search should be case-sensitive
|
||||
|
||||
Returns:
|
||||
List of (start_index, end_index) tuples for each occurrence
|
||||
"""
|
||||
occurrences = []
|
||||
search_text = text if case_sensitive else text.lower()
|
||||
search_keyword = keyword if case_sensitive else keyword.lower()
|
||||
|
||||
start = 0
|
||||
while True:
|
||||
start = search_text.find(search_keyword, start)
|
||||
if start == -1:
|
||||
break
|
||||
end = start + len(search_keyword)
|
||||
occurrences.append((start, end))
|
||||
start = end
|
||||
|
||||
return occurrences
|
||||
|
||||
def validate_card_text(self, card_text: str,
|
||||
ignore_unrecognized: bool = False) -> List[Dict[str, str]]:
|
||||
"""
|
||||
Validate a card's rules text for keyword usage.
|
||||
|
||||
Args:
|
||||
card_text: The card's rules text
|
||||
ignore_unrecognized: If True, don't report unrecognized keywords
|
||||
|
||||
Returns:
|
||||
List of validation errors (empty if no errors)
|
||||
"""
|
||||
errors: List[Dict[str, str]] = []
|
||||
keywords_found = self.find_keywords_in_text(card_text)
|
||||
|
||||
for keyword, count in keywords_found.items():
|
||||
info = self.get_keyword_info(keyword)
|
||||
if info is None:
|
||||
if not ignore_unrecognized:
|
||||
errors.append({
|
||||
"keyword": keyword,
|
||||
"message": f"Unrecognized keyword: '{keyword}' (found {count} time(s))",
|
||||
"type": "unrecognized_keyword"
|
||||
})
|
||||
else:
|
||||
# Check for common issues
|
||||
info_type = info.get("category", "unknown")
|
||||
if info_type == "keyword_action":
|
||||
# Check if keyword is used as a verb in the text
|
||||
pass # Actions are typically used as verbs
|
||||
elif info_type == "keyword_ability":
|
||||
pass # Abilities are typically used as adjectives or nouns
|
||||
|
||||
return errors
|
||||
|
||||
def analyze_rules_text(self, text: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Perform a comprehensive analysis of keywords in rules text.
|
||||
|
||||
Args:
|
||||
text: The rules text to analyze
|
||||
|
||||
Returns:
|
||||
Dictionary with analysis results
|
||||
"""
|
||||
keywords_found = self.find_keywords_in_text(text)
|
||||
|
||||
analysis = {
|
||||
"total_keywords": len(keywords_found),
|
||||
"keywords_found": keywords_found,
|
||||
"ability_words_found": {},
|
||||
"keyword_actions_found": {},
|
||||
"keyword_abilities_found": {},
|
||||
}
|
||||
|
||||
for keyword, count in keywords_found.items():
|
||||
info = self.get_keyword_info(keyword)
|
||||
if info is None:
|
||||
continue
|
||||
|
||||
category = info.get("category", "unknown")
|
||||
if category == "ability_word":
|
||||
analysis["ability_words_found"][keyword] = count
|
||||
elif category == "keyword_action":
|
||||
analysis["keyword_actions_found"][keyword] = count
|
||||
elif category == "keyword_ability":
|
||||
analysis["keyword_abilities_found"][keyword] = count
|
||||
|
||||
return analysis
|
||||
|
||||
def check_for_misspelled_keywords(self, text: str) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Check for potentially misspelled keywords in text.
|
||||
|
||||
Args:
|
||||
text: The text to check
|
||||
|
||||
Returns:
|
||||
List of potential misspellings with suggestions
|
||||
"""
|
||||
words = text.lower().split()
|
||||
misspellings = []
|
||||
|
||||
for word in words:
|
||||
clean_word = ''.join(c for c in word if c.isalnum() or c == "'")
|
||||
if len(clean_word) < 3:
|
||||
continue
|
||||
|
||||
if clean_word not in self._all_keywords:
|
||||
# Try to find similar keywords
|
||||
similar = self._find_similar_keywords(clean_word)
|
||||
if similar:
|
||||
misspellings.append({
|
||||
"word": clean_word,
|
||||
"suggestions": similar[:3], # Top 3 suggestions
|
||||
"message": f"Did you mean one of: {', '.join(similar[:3])}"
|
||||
})
|
||||
|
||||
return misspellings
|
||||
|
||||
def _find_similar_keywords(self, word: str) -> List[str]:
|
||||
"""Find keywords that are similar to the given word."""
|
||||
similar = []
|
||||
for keyword in self._all_keywords:
|
||||
# Use simple Levenshtein distance
|
||||
distance = self._levenshtein_distance(word, keyword)
|
||||
if distance <= 3 and len(keyword) <= len(word) + 2:
|
||||
similar.append((keyword, distance))
|
||||
|
||||
# Sort by distance and return unique keywords
|
||||
similar.sort(key=lambda x: x[1])
|
||||
return [k for k, _ in similar[:10]]
|
||||
|
||||
@staticmethod
|
||||
def _levenshtein_distance(s1: str, s2: str) -> int:
|
||||
"""Calculate the Levenshtein distance between two strings."""
|
||||
if len(s1) < len(s2):
|
||||
return KeywordValidator._levenshtein_distance(s2, s1)
|
||||
|
||||
if len(s2) == 0:
|
||||
return len(s1)
|
||||
|
||||
previous_row = range(len(s2) + 1)
|
||||
for i, c1 in enumerate(s1):
|
||||
current_row = [i + 1]
|
||||
for j, c2 in enumerate(s2):
|
||||
insertions = previous_row[j + 1] + 1
|
||||
deletions = current_row[j] + 1
|
||||
substitutions = previous_row[j] + (c1 != c2)
|
||||
current_row.append(min(insertions, deletions, substitutions))
|
||||
previous_row = current_row
|
||||
|
||||
return previous_row[-1]
|
||||
|
||||
def get_all_keywords_by_type(self) -> Dict[str, Set[str]]:
|
||||
"""
|
||||
Get all keywords grouped by type.
|
||||
|
||||
Returns:
|
||||
Dictionary mapping keyword types to sets of keywords
|
||||
"""
|
||||
result = {
|
||||
"ability_words": set(ABILITY_WORDS.keys()),
|
||||
"keyword_actions": set(KEYWORD_ACTIONS.keys()),
|
||||
"keyword_abilities": set(KEYWORD_ABILITIES.keys()),
|
||||
}
|
||||
return result
|
||||
|
||||
def export_keywords(self) -> Dict[str, Any]:
|
||||
"""
|
||||
Export all keywords as a structured dictionary.
|
||||
|
||||
Returns:
|
||||
Dictionary with all keyword data
|
||||
"""
|
||||
return {
|
||||
"total_keywords": len(self._all_keywords),
|
||||
"ability_words": list(ABILITY_WORDS.keys()),
|
||||
"keyword_actions": list(KEYWORD_ACTIONS.keys()),
|
||||
"keyword_abilities": list(KEYWORD_ABILITIES.keys()),
|
||||
"keyword_variants": {
|
||||
k: v.get("variants", [])
|
||||
for k, v in KEYWORD_ABILITIES.items() if v.get("variants")
|
||||
},
|
||||
}
|
||||
Reference in New Issue
Block a user