Files
mtgonline/backend/mtg_rules_engine/validator.py
T

320 lines
11 KiB
Python

"""
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")
},
}