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