#!/usr/bin/env python3 """ Ability Validator - Reviews processed abilities and identifies issues for correction. This tool helps ensure all abilities are correctly parsed before shipping to players. Run this after ability_processor.py to review and fix any problems. Usage: python tools/ability_validator.py # Full validation report python tools/ability_validator.py --unparsed # Show only unparsed abilities python tools/ability_validator.py --low-confidence # Show low confidence parses python tools/ability_validator.py --card 1-001H # Validate specific card python tools/ability_validator.py --effect DAMAGE # Show all DAMAGE effects python tools/ability_validator.py --summary # Quick summary only python tools/ability_validator.py --export issues.json # Export issues for review """ import json import argparse from pathlib import Path from collections import defaultdict from typing import Optional from datetime import datetime ABILITIES_FILE = Path(__file__).parent.parent / "data" / "abilities_processed.json" CARDS_FILE = Path(__file__).parent.parent / "data" / "cards.json" def load_abilities() -> dict: """Load processed abilities file.""" if not ABILITIES_FILE.exists(): print(f"ERROR: {ABILITIES_FILE} not found!") print("Run: python tools/ability_processor.py first") return {} with open(ABILITIES_FILE, 'r', encoding='utf-8') as f: return json.load(f) def load_cards() -> dict: """Load cards file for reference.""" if not CARDS_FILE.exists(): return {} with open(CARDS_FILE, 'r', encoding='utf-8') as f: data = json.load(f) cards = data.get('cards', data) if isinstance(data, dict) else data return {card['id']: card for card in cards} def get_issues(abilities_data: dict) -> dict: """Analyze abilities and categorize issues.""" issues = { 'unparsed': [], # Completely failed to parse 'low_confidence': [], # Parsed but low confidence 'unknown_effects': [], # Has UNKNOWN effect type 'missing_targets': [], # Effect needs target but none specified 'complex_conditions': [], # Has conditions we can't fully parse } abilities = abilities_data.get('abilities', {}) for card_id, card_abilities in abilities.items(): for ability in card_abilities: parsed = ability.get('parsed', {}) confidence = ability.get('parse_confidence', 'LOW') original = ability.get('original', {}) # Check for unparsed if not parsed or parsed.get('type') == 'UNPARSED': issues['unparsed'].append({ 'card_id': card_id, 'ability_index': ability.get('ability_index', 0), 'original': original, 'reason': 'Failed to parse ability type' }) continue # Check confidence level if confidence == 'LOW': issues['low_confidence'].append({ 'card_id': card_id, 'ability_index': ability.get('ability_index', 0), 'original': original, 'parsed': parsed, 'reason': 'Low confidence parse - may be incorrect' }) # Check for UNKNOWN effects for effect in parsed.get('effects', []): if effect.get('type') == 'UNKNOWN': issues['unknown_effects'].append({ 'card_id': card_id, 'ability_index': ability.get('ability_index', 0), 'original': original, 'effect': effect, 'reason': f"Unknown effect: {effect.get('raw', 'no text')[:80]}" }) # Check for missing targets on effects that need them effect_type = effect.get('type', '') needs_target = effect_type in ['DAMAGE', 'BREAK', 'DULL', 'ACTIVATE', 'RETURN', 'POWER_MOD'] has_target = effect.get('target', {}) if needs_target and not has_target: issues['missing_targets'].append({ 'card_id': card_id, 'ability_index': ability.get('ability_index', 0), 'effect_type': effect_type, 'original': original, 'reason': f'{effect_type} effect missing target specification' }) # Check for complex conditions trigger = parsed.get('trigger', {}) if trigger.get('condition'): issues['complex_conditions'].append({ 'card_id': card_id, 'ability_index': ability.get('ability_index', 0), 'original': original, 'condition': trigger.get('condition'), 'reason': 'Has conditional trigger - verify correctness' }) return issues def print_summary(abilities_data: dict, issues: dict) -> None: """Print a summary of the validation results.""" stats = abilities_data.get('statistics', {}) print("=" * 70) print("ABILITY VALIDATION SUMMARY") print("=" * 70) print(f"Generated: {abilities_data.get('generated_at', 'unknown')}") print(f"Version: {abilities_data.get('version', 'unknown')}") print() print(f"Total Cards: {stats.get('total_cards', 0):,}") print(f"Total Abilities: {stats.get('total_abilities', 0):,}") print() print("Parse Confidence:") print(f" HIGH: {stats.get('parsed_high', 0):,} ({100*stats.get('parsed_high', 0)/max(1, stats.get('total_abilities', 1)):.1f}%)") print(f" MEDIUM: {stats.get('parsed_medium', 0):,} ({100*stats.get('parsed_medium', 0)/max(1, stats.get('total_abilities', 1)):.1f}%)") print(f" LOW: {stats.get('parsed_low', 0):,} ({100*stats.get('parsed_low', 0)/max(1, stats.get('total_abilities', 1)):.1f}%)") print() print("Issues Found:") print(f" Unparsed abilities: {len(issues['unparsed']):,}") print(f" Low confidence: {len(issues['low_confidence']):,}") print(f" Unknown effects: {len(issues['unknown_effects']):,}") print(f" Missing targets: {len(issues['missing_targets']):,}") print(f" Complex conditions: {len(issues['complex_conditions']):,}") print() total_issues = sum(len(v) for v in issues.values()) if total_issues == 0: print("✓ No issues found! Abilities are ready for shipping.") else: print(f"⚠ {total_issues} total issues to review") print() print("Run with --unparsed, --low-confidence, or --effect to see details") print("=" * 70) def print_issues(issues: list, title: str, cards: dict, limit: int = 50) -> None: """Print a list of issues with details.""" if not issues: print(f"\n{title}: None found ✓") return print(f"\n{'=' * 70}") print(f"{title} ({len(issues)} total)") print("=" * 70) for i, issue in enumerate(issues[:limit]): card_id = issue['card_id'] card = cards.get(card_id, {}) card_name = card.get('name', 'Unknown') print(f"\n[{i+1}] {card_id} - {card_name}") print(f" Reason: {issue.get('reason', 'Unknown')}") original = issue.get('original', {}) if isinstance(original, dict): if original.get('trigger'): print(f" Trigger: {original['trigger'][:80]}...") if original.get('effect'): effect_text = original['effect'] if len(effect_text) > 100: effect_text = effect_text[:100] + "..." print(f" Effect: {effect_text}") if issue.get('parsed'): parsed = issue['parsed'] print(f" Parsed type: {parsed.get('type')}") if parsed.get('effects'): print(f" Effects: {[e.get('type') for e in parsed['effects']]}") if len(issues) > limit: print(f"\n... and {len(issues) - limit} more") def print_effect_analysis(abilities_data: dict, effect_type: str) -> None: """Show all abilities with a specific effect type.""" effect_type = effect_type.upper() abilities = abilities_data.get('abilities', {}) matches = [] for card_id, card_abilities in abilities.items(): for ability in card_abilities: parsed = ability.get('parsed', {}) for effect in parsed.get('effects', []): if effect.get('type') == effect_type: matches.append({ 'card_id': card_id, 'ability': ability, 'effect': effect }) print(f"\n{'=' * 70}") print(f"EFFECT TYPE: {effect_type} ({len(matches)} occurrences)") print("=" * 70) # Group by effect structure structures = defaultdict(list) for match in matches: effect = match['effect'] # Create a structure key keys = sorted([k for k in effect.keys() if k != 'raw']) structure = tuple(keys) structures[structure].append(match) print(f"\nStructure variations: {len(structures)}") for structure, examples in structures.items(): print(f"\n Keys: {list(structure)}") print(f" Count: {len(examples)}") # Show one example ex = examples[0] print(f" Example ({ex['card_id']}): {ex['effect']}") def validate_card(abilities_data: dict, card_id: str, cards: dict) -> None: """Show detailed validation for a specific card.""" abilities = abilities_data.get('abilities', {}) card_abilities = abilities.get(card_id, []) card = cards.get(card_id, {}) print(f"\n{'=' * 70}") print(f"CARD: {card_id} - {card.get('name', 'Unknown')}") print("=" * 70) if not card_abilities: print("No abilities found for this card.") return for i, ability in enumerate(card_abilities): print(f"\n--- Ability {i + 1} ---") original = ability.get('original', {}) print(f"Type: {original.get('type', 'unknown')}") if original.get('trigger'): print(f"Trigger: {original['trigger']}") if original.get('effect'): print(f"Effect: {original['effect']}") print(f"\nConfidence: {ability.get('parse_confidence', 'UNKNOWN')}") parsed = ability.get('parsed', {}) print(f"Parsed Type: {parsed.get('type', 'none')}") if parsed.get('trigger'): print(f"Parsed Trigger: {json.dumps(parsed['trigger'], indent=2)}") if parsed.get('effects'): print("Parsed Effects:") for j, effect in enumerate(parsed['effects']): print(f" [{j+1}] {json.dumps(effect, indent=4)}") def export_issues(issues: dict, cards: dict, output_path: str) -> None: """Export issues to a JSON file for external review/fixing.""" export_data = { 'generated_at': datetime.now().isoformat(), 'summary': { 'unparsed': len(issues['unparsed']), 'low_confidence': len(issues['low_confidence']), 'unknown_effects': len(issues['unknown_effects']), 'missing_targets': len(issues['missing_targets']), 'complex_conditions': len(issues['complex_conditions']), }, 'issues': {} } # Group issues by card for issue_type, issue_list in issues.items(): for issue in issue_list: card_id = issue['card_id'] if card_id not in export_data['issues']: card = cards.get(card_id, {}) export_data['issues'][card_id] = { 'name': card.get('name', 'Unknown'), 'problems': [] } export_data['issues'][card_id]['problems'].append({ 'type': issue_type, 'ability_index': issue.get('ability_index', 0), 'reason': issue.get('reason', ''), 'original': issue.get('original', {}) }) with open(output_path, 'w', encoding='utf-8') as f: json.dump(export_data, f, indent=2) print(f"Exported {len(export_data['issues'])} cards with issues to: {output_path}") def main(): parser = argparse.ArgumentParser(description='Validate processed abilities') parser.add_argument('--unparsed', action='store_true', help='Show unparsed abilities') parser.add_argument('--low-confidence', action='store_true', help='Show low confidence parses') parser.add_argument('--unknown', action='store_true', help='Show unknown effects') parser.add_argument('--card', type=str, help='Validate specific card by ID') parser.add_argument('--effect', type=str, help='Show all abilities with effect type') parser.add_argument('--summary', action='store_true', help='Show summary only') parser.add_argument('--export', type=str, help='Export issues to JSON file') args = parser.parse_args() abilities_data = load_abilities() if not abilities_data: return cards = load_cards() issues = get_issues(abilities_data) # Always show summary print_summary(abilities_data, issues) if args.summary: return if args.card: validate_card(abilities_data, args.card, cards) return if args.effect: print_effect_analysis(abilities_data, args.effect) return if args.export: export_issues(issues, cards, args.export) return # Show specific issue types if args.unparsed: print_issues(issues['unparsed'], "UNPARSED ABILITIES", cards) if args.low_confidence: print_issues(issues['low_confidence'], "LOW CONFIDENCE PARSES", cards) if args.unknown: print_issues(issues['unknown_effects'], "UNKNOWN EFFECTS", cards) # If no specific flag, show a sample of each issue type if not any([args.unparsed, args.low_confidence, args.unknown]): print("\nUse --unparsed, --low-confidence, --unknown, --card, or --effect for details") print("Use --export issues.json to export all issues for review") if __name__ == '__main__': main()