from __future__ import annotations import re from typing import Any, Dict, List from engine.models.ability import Condition, ConditionType from .parser_lexer import StructuralLexer from .parser_patterns import ( CONDITION_SEMANTIC_SPECIAL_CASES, CONDITION_TRUE_ALIASES, IGNORED_CONDITIONS, KEYWORD_CONDITIONS, ) def parse_pseudocode_conditions(parser: Any, text: str) -> List[Condition]: conditions: List[Condition] = [] stripped_text = text.strip() if stripped_text.upper().startswith("OR(") and stripped_text.endswith(")"): inner = stripped_text[3:-1].strip() clauses = [] for clause_text in StructuralLexer.split_respecting_nesting(inner, delimiter=","): clause_text = clause_text.strip() if not clause_text: continue parsed_clause = parse_pseudocode_conditions(parser, clause_text) if parsed_clause: clauses.append(parser._serialize_condition_clause(parsed_clause[0])) if clauses: return [Condition(ConditionType.NONE, {"raw_cond": "OR", "clauses": clauses})] top_level_or_parts = StructuralLexer.split_respecting_nesting(text, delimiter=" OR ") if len(top_level_or_parts) > 1: clauses = [] for clause_text in top_level_or_parts: clause_text = clause_text.strip() if not clause_text: continue parsed_clause = parse_pseudocode_conditions(parser, clause_text) if parsed_clause: clauses.append(parser._serialize_condition_clause(parsed_clause[0])) if clauses: return [Condition(ConditionType.NONE, {"raw_cond": "OR", "clauses": clauses})] parts = StructuralLexer.split_respecting_nesting(text, delimiter=",", extra_delimiters=[";"]) for part in parts: if not part: continue negated = part.startswith("NOT ") or part.startswith("NOT_") name_part = part[4:] if negated else part if not negated and name_part.startswith("!"): negated = True name_part = name_part[1:] params: Dict[str, Any] = {} for brace_block in re.findall(r"\{([^{}]*)\}", name_part): params.update(parser._parse_pseudocode_params("{" + brace_block + "}")) name_part_no_braces = re.sub(r"\s*\{[^{}]*\}", "", name_part).strip() match = re.match(r"(\w+)(?:\((.*?)\))?", name_part_no_braces) if not match: continue name = match.group(1).upper() val_in_parens = match.group(2) if val_in_parens: value_parts = [vp.strip() for vp in val_in_parens.split(",")] for value_part in value_parts: value_upper = value_part.upper() if value_upper.startswith("UNIT_"): params["unit"] = value_part[5:] elif value_upper.startswith("GROUP_"): params["group"] = value_part[6:] elif value_upper in [ "STAGE", "HAND", "DISCARD", "ENERGY", "SUCCESS_LIVE", "LIVE_ZONE", "SUCCESS_PILE", ]: params["zone"] = value_upper elif "=" in value_part: key, value = [s.strip().strip('"').strip("'") for s in value_part.split("=", 1)] params[key.lower()] = value elif ":" in value_part: key, value = [s.strip().strip('"').strip("'") for s in value_part.split(":", 1)] params[key.lower()] = value else: params["val"] = value_part if "->" in name_part_no_braces: arrow_pos = name_part_no_braces.find("->") target_part = name_part_no_braces[arrow_pos + 2 :].strip() target_word = target_part.split()[0].strip().upper() if target_word: params["target"] = target_word remaining_part = name_part_no_braces[len(match.group(0)) :].strip() if "->" in remaining_part: remaining_part = remaining_part[: remaining_part.find("->")].strip() if remaining_part: comparison_match = re.match(r"(>=|<=|>|<|=)\s*[\"']?(.*?)[\"']?$", remaining_part) if comparison_match: op_map = {">=": "GE", "<=": "LE", ">": "GT", "<": "LT", "=": "EQ"} params["comparison"] = op_map.get(comparison_match.group(1), "GE") params["val"] = comparison_match.group(2) else: equals_match = re.search(r"=\s*[\"']?(.*?)[\"']?$", remaining_part) if equals_match: params["val"] = equals_match.group(1) params["raw_cond"] = name is_negated = negated if name == "PLAYER_CENTER_COST_GT_OPPONENT_CENTER_COST": name = "SYNC_COST" params["area"] = "CENTER" params["comparison"] = "GT" params["val"] = "0" elif name == "OPPONENT_CENTER_COST_GT_PLAYER_CENTER_COST": name = "SYNC_COST" params["area"] = "CENTER" params["comparison"] = "LT" params["val"] = "0" elif name == "HEARTS_COUNT" and "OPPONENT" in str(params.get("val", "")).upper(): name = "HEART_LEAD" params["target"] = "opponent" params["val"] = "0" if name in IGNORED_CONDITIONS: conditions.append(Condition(ConditionType.NONE, params, is_negated=is_negated)) continue if name in KEYWORD_CONDITIONS: params["keyword"] = KEYWORD_CONDITIONS[name] conditions.append(Condition(ConditionType.HAS_KEYWORD, params, is_negated=is_negated)) continue if name.startswith("MATCH_") or name.startswith("DID_ACTIVATE_"): params["keyword"] = name conditions.append(Condition(ConditionType.HAS_KEYWORD, params, is_negated=is_negated)) continue if name in ["COUNT_SUCCESS_LIVES", "COUNT_SUCCESS_LIVE", "COUNT_CARDS"]: zone_value = str(params.get("zone") or "").upper() if name == "COUNT_CARDS" and zone_value not in ["SUCCESS_PILE", "SUCCESS_LIVE"]: pass else: if "target" not in params: target_value = str(params.get("PLAYER", params.get("val", "self"))).upper() params["target"] = "opponent" if target_value in {"OPPONENT", "1"} else "self" if "PLAYER" in params: del params["PLAYER"] if "val" in params and str(params["val"]).upper() in ["PLAYER", "OPPONENT"]: del params["val"] if "COUNT" in params: params["value"] = params["COUNT"] params["comparison"] = "EQ" del params["COUNT"] conditions.append(Condition(ConditionType.COUNT_SUCCESS_LIVE, params, is_negated=is_negated)) continue if name in CONDITION_TRUE_ALIASES: canonical_name, extra_params = CONDITION_TRUE_ALIASES[name] try: condition_type = ConditionType[canonical_name] except KeyError: condition_type = ConditionType.NONE conditions.append(Condition(condition_type, {**params, **extra_params}, is_negated=is_negated)) continue if name in CONDITION_SEMANTIC_SPECIAL_CASES: canonical_name, extra_params = CONDITION_SEMANTIC_SPECIAL_CASES[name] try: condition_type = ConditionType[canonical_name] except KeyError: condition_type = ConditionType.NONE for key, value in extra_params.items(): if key not in params: params[key] = value if name == "NOT_MOVED_THIS_TURN": is_negated = True conditions.append(Condition(condition_type, params, is_negated=is_negated)) continue if name in {"AREA_IN", "AREA"}: value = str(params.get("val", "")).upper().strip('"') if value == "CENTER" or params.get("zone") == "CENTER" or params.get("area") == "CENTER": condition_type = ConditionType.IS_CENTER else: condition_type = ConditionType.AREA_CHECK params["keyword"] = "AREA_CHECK" area_map = {"LEFT_SIDE": 0, "LEFT": 0, "RIGHT_SIDE": 2, "RIGHT": 2} if value in area_map: params["value"] = area_map[value] conditions.append(Condition(condition_type, params, is_negated=is_negated)) continue if name == "COST_LEAD" and params.get("area") == "CENTER": params["zone"] = "CENTER_STAGE" del params["area"] if name == "REVEALED_CONTAINS": if "TYPE_LIVE" in params: params["value"] = "live" if "TYPE_MEMBER" in params: params["value"] = "member" if name == "SELECT_CARD" and str(params.get("val", "")).upper() == "REVEALED_CARD": filter_str = str(params.get("FILTER") or params.get("filter") or "").upper() if "TYPE_LIVE" in filter_str: params["card_type"] = "live" conditions.append(Condition(ConditionType.TYPE_CHECK, params, is_negated=is_negated)) continue if "TYPE_MEMBER" in filter_str: params["card_type"] = "member" conditions.append(Condition(ConditionType.TYPE_CHECK, params, is_negated=is_negated)) continue condition_type = getattr(ConditionType, name, ConditionType.NONE) conditions.append(Condition(condition_type, params, is_negated=is_negated)) return conditions def looks_like_condition_instruction(text: str) -> bool: """Return True if the instruction text should be routed through the condition parser.""" stripped = text.strip() if not stripped: return False upper = stripped.upper() if upper.startswith("CONDITION:") or upper.startswith("OR("): return True name_match = re.match(r"^([\w_]+)", stripped) if not name_match: return False name = name_match.group(1).upper() if ( name in CONDITION_TRUE_ALIASES or name in CONDITION_SEMANTIC_SPECIAL_CASES or name in KEYWORD_CONDITIONS or name in IGNORED_CONDITIONS or hasattr(ConditionType, name) or name.startswith("MATCH_") or name.startswith("DID_ACTIVATE_") or name in { "AREA", "AREA_IN", "REVEALED_CONTAINS", "SELECT_CARD", "PLAYER_CENTER_COST_GT_OPPONENT_CENTER_COST", "OPPONENT_CENTER_COST_GT_PLAYER_CENTER_COST", "HEARTS_COUNT", "COUNT_SUCCESS_LIVES", "COUNT_SUCCESS_LIVE", "COUNT_CARDS", } ): return True return False __all__ = ["parse_pseudocode_conditions", "looks_like_condition_instruction"]