#!/usr/bin/env python """ Pseudocode Validator Tool This tool validates pseudocode entries in manual_pseudocode.json and cards.json to detect common issues that can cause infinite loops or runtime errors. Checks performed: 1. Missing TRIGGER: for abilities with effects 2. ACTIVATED/ON_ACTIVATE abilities without COST: 3. CHEER_REVEAL effects without proper handling 4. Unknown trigger types 5. Abilities with no effects """ import json import re import sys from pathlib import Path # Add project root to path PROJECT_ROOT = Path(__file__).parent.parent sys.path.insert(0, str(PROJECT_ROOT)) VALID_TRIGGERS = { "ON_PLAY", "ON_LIVE_START", "ON_LIVE_SUCCESS", "TURN_START", "TURN_END", "CONSTANT", "ACTIVATED", "ON_ACTIVATE", "ON_LEAVES", "ON_REVEAL", "ON_POSITION_CHANGE", "ON_MEMBER_DISCARD", "ON_OPPONENT_TAP", "ON_YELL", "ON_YELL_SUCCESS", "ON_OPPONENT_YELL", "ON_DISCARD", "ON_STAGE_ENTRY", "ON_REMOVE", "ON_ACTIVATE_FROM_DISCARD", "ACTIVATED_FROM_DISCARD", "ON_MOVE_TO_DISCARD", "ON_ENERGY_CHARGE", } VALID_EFFECTS = { "DRAW", "ADD_BLADES", "ADD_HEARTS", "REDUCE_COST", "LOOK_DECK", "RECOVER_LIVE", "BOOST_SCORE", "RECOVER_MEMBER", "BUFF_POWER", "IMMUNITY", "TAP_MEMBER", "TAP_OPPONENT", "ACTIVATE_MEMBER", "ACTIVATE_ENERGY", "DISCARD_HAND", "MOVE_TO_DECK", "MOVE_TO_DISCARD", "LOOK_AND_CHOOSE", "LOOK_AND_CHOOSE_REVEAL", "LOOK_AND_CHOOSE_ORDER", "SELECT_MODE", "COLOR_SELECT", "CHEER_REVEAL", "REVEAL_UNTIL", "PLAY_MEMBER", "PLAY_MEMBER_FROM_DISCARD", "PLAY_LIVE_FROM_DISCARD", "SWAP_CARDS", "PREVENT_ACTIVATE", "REVEAL_HAND", "REVEAL_CARDS", "SELECT_MEMBER", "SELECT_REVEALED", "REDUCE_HEART", "INCREASE_COST", "MOVE_MEMBER", "ORDER_DECK", "PREVENT_LIVE", "TAP_PLAYER", "ACTIVATE_SELF", "SWAP_AREA", } VALID_COSTS = { "TAP_SELF", "DISCARD_HAND", "PAY_ENERGY", "REMOVE_SELF", "TAP_MEMBER", "TAP_PLAYER", "REVEAL_HAND", "MOVE_TO_DECK", "PAY_HEART", "DISCARD_ENERGY", } class PseudocodeIssue: def __init__(self, card_no: str, issue_type: str, message: str, line_content: str = "", severity: str = "WARNING"): self.card_no = card_no self.issue_type = issue_type self.message = message self.line_content = line_content self.severity = severity def __str__(self): line_info = f" (Line: '{self.line_content}')" if self.line_content else "" return f"[{self.severity}] {self.card_no}: {self.issue_type} - {self.message}{line_info}" def validate_pseudocode(card_no: str, pseudocode: str) -> list: """Validate a single pseudocode entry and return list of issues.""" issues = [] if not pseudocode or not pseudocode.strip(): return issues # Handle escaped newlines pseudocode = pseudocode.replace("\\n", "\n") lines = pseudocode.strip().split("\n") # Track state current_trigger = None has_cost = False has_effect = False def check_previous_ability(): nonlocal current_trigger, has_cost, has_effect if current_trigger: if not has_effect: issues.append( PseudocodeIssue(card_no, "NO_EFFECT", f"Trigger '{current_trigger}' has no EFFECT:", "", "WARNING") ) if current_trigger in ("ACTIVATED", "ON_ACTIVATE") and not has_cost: issues.append( PseudocodeIssue( card_no, "ACTIVATED_NO_COST", "ACTIVATED ability has no COST: - will cause infinite loop!", "", "ERROR", ) ) for line in lines: line = line.strip() if not line: continue if line.upper().startswith("TRIGGER:"): check_previous_ability() trigger_text = line[8:].strip() # Remove modifiers trigger_text = re.sub(r"\s*\([^)]*\)\s*", "", trigger_text).strip() # Split by possible combined triggers if any (though usually one per line) if trigger_text.upper() not in VALID_TRIGGERS: issues.append( PseudocodeIssue( card_no, "INVALID_TRIGGER", f"Unknown trigger type: '{trigger_text}'", line, "ERROR" ) ) current_trigger = trigger_text.upper() has_cost = False has_effect = False elif line.upper().startswith("COST:"): has_cost = True elif line.upper().startswith("EFFECT:"): has_effect = True if "CHEER_REVEAL" in line.upper(): if current_trigger in ("ACTIVATED", "ON_ACTIVATE") and not has_cost: issues.append( PseudocodeIssue( card_no, "CHEER_REVEAL_LOOP", "CHEER_REVEAL in ACTIVATED ability without cost!", line, "ERROR", ) ) elif line.upper().startswith("CONDITION:") or line.upper().startswith("OPTION:"): pass elif not line.startswith("#"): # Orphan effect check first_word = line.split("(")[0].split(" ")[0].split("{")[0].strip().upper() if first_word in VALID_EFFECTS: if current_trigger is None: issues.append( PseudocodeIssue( card_no, "EFFECT_WITHOUT_TRIGGER", "Effect found before any TRIGGER:", line, "ERROR" ) ) check_previous_ability() return issues return issues def validate_manual_pseudocode(filepath: str) -> list: """Validate all entries in manual_pseudocode.json""" issues = [] with open(filepath, "r", encoding="utf-8") as f: data = json.load(f) for card_no, entry in data.items(): pseudocode = entry.get("pseudocode", "") card_issues = validate_pseudocode(card_no, pseudocode) issues.extend(card_issues) return issues def validate_cards_json(filepath: str) -> list: """Validate all pseudocode entries in cards.json""" issues = [] with open(filepath, "r", encoding="utf-8") as f: data = json.load(f) for card_no, card_data in data.items(): if isinstance(card_data, dict): pseudocode = card_data.get("pseudocode", "") card_issues = validate_pseudocode(card_no, pseudocode) issues.extend(card_issues) return issues def main(): data_dir = PROJECT_ROOT / "data" manual_path = data_dir / "manual_pseudocode.json" cards_path = data_dir / "cards.json" all_issues = [] # Validate manual_pseudocode.json if manual_path.exists(): manual_issues = validate_manual_pseudocode(str(manual_path)) all_issues.extend(manual_issues) # Validate cards.json if cards_path.exists(): cards_issues = validate_cards_json(str(cards_path)) all_issues.extend(cards_issues) # Filter for Errors errors = [i for i in all_issues if i.severity == "ERROR"] with open("validate_report.txt", "w", encoding="utf-8") as f: f.write("=" * 60 + "\n") f.write("Pseudocode Validator Report\n") f.write("=" * 60 + "\n") if errors: f.write(f"\nFOUND {len(errors)} ERRORS:\n") for issue in errors: f.write(f" {issue}\n") else: f.write("\nNO ERRORS FOUND\n") print(f"Validation complete. Found {len(errors)} errors. Report saved to validate_report.txt") return len(errors) return len(errors) if __name__ == "__main__": sys.exit(main())