373 lines
14 KiB
Python
373 lines
14 KiB
Python
#!/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()
|