Spaces:
Sleeping
Sleeping
| #!/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()) | |