from engine.game.enums import Phase from engine.game.state_utils import get_base_id def get_action_desc(a, gs): """ Generate clear, informative action descriptions. Shows card names, costs, and ability sources for better user understanding. """ if gs is None: return f"Action {a}" # Handle both Python and Rust engine (PyGameState) if hasattr(gs, "get_player"): p_idx = gs.current_player p = gs.get_player(p_idx) else: p = gs.active_player p_idx = gs.current_player member_db = gs.member_db live_db = gs.live_db # Helper to get from DB, handling int/str keys def get_from_db(db, key, default=None): if not db: return default if hasattr(db, "get"): res = db.get(key) if res is not None: return res return db.get(str(key), default) try: if key in db: return db[key] if str(key) in db: return db[str(key)] except: pass return default # Helper to get card name def get_card_name(cid, gs_override=None): _gs = gs_override or gs if cid < 0: return "なし" base_id = get_base_id(int(cid)) # Try all DBs with the helper m = get_from_db(member_db, base_id) if m: name = get_v(m, "name", f"メンバー #{base_id}") card_no = get_v(m, "card_no", "??") return f"{name} ({card_no})" l = get_from_db(live_db, base_id) if l: name = get_v(l, "name", f"ライブ #{base_id}") card_no = get_v(l, "card_no", "??") return f"{name} ({card_no})" e = get_from_db(getattr(_gs, "energy_db", None), base_id) if e: name = get_v(e, "name", f"エネルギー #{base_id}") card_no = get_v(e, "card_no", "??") return f"{name} ({card_no})" return f"カード #{cid}" # Helpers for general property access def get_v(obj, key, default=None): if obj is None: return default if isinstance(obj, dict): return obj.get(key, default) return getattr(obj, key, default) # Helper for pending choices def get_top_pending(): if not gs.pending_choices: return None, {} choice_type, params = gs.pending_choices[0] if isinstance(params, str): import json try: return choice_type, json.loads(params) except: return choice_type, {} return choice_type, params # --- ACTION HANDLERS (Order Matters: Specific to General) --- # The order of these blocks determines priority. Specific contextual handlers # (like Color selection or Discard/Recover) must come BEFORE broad ranges # to avoid being shadowed by more generic messages. # 1. SPECIAL & UNIVERSAL ACTIONS # -------------------------------------------------------------------------- # Action 0: Pass / Confirm / Skip # Used for ending phases (Main, LiveSet, Mulligan) or skipping optional effects. if a == 0: if int(gs.phase) == int(Phase.MAIN): return "【終了】メインフェイズを終了する" if int(gs.phase) == int(Phase.LIVE_SET): return "【確認】ライブカードをセットして続行" if int(gs.phase) == int(Phase.LIVE_RESULT): return "【進む】次へ進む" if int(gs.phase) in (int(Phase.MULLIGAN_P1), int(Phase.MULLIGAN_P2)): return "【確認】マリガンを実行" choice_type, params = get_top_pending() if choice_type: source_name = params.get("source_member", "アビリティ") return f"【スキップ】{source_name}の効果を使用しない" return "【パス】何もしない" # 2. SPECIFIC INTERACTIVE SELECTIONS (Must precede broad ranges) # -------------------------------------------------------------------------- # 580-585: Color Selection # Triggered by O_COLOR_SELECT. Maps to Pink, Red, Yellow, Green, Blue, Purple. if 580 <= a <= 585: colors = ["赤", "青", "緑", "黄", "紫", "ピンク"] return f"【色選択】 {colors[a - 580]}" # 560-562: Stage Slot Selection (Targeting/Wait) # Triggered when choosing a slot on the player's own stage for an effect. if 560 <= a <= 562: idx = a - 560 areas = ["左", "センター", "右"] cid = p.stage[idx] name = "空エリア" base_id = get_base_id(int(cid)) if cid >= 0: m = get_from_db(member_db, base_id) if m: name = get_v(m, "name", "メンバー") desc = "選択" choice_type, params = get_top_pending() if choice_type: if choice_type == "MOVE_MEMBER": desc = "移動元" elif choice_type == "TAP_MEMBER": desc = "ウェイト" elif choice_type in ("PLAY_MEMBER_FROM_HAND", "PLAY_MEMBER_FROM_DISCARD"): desc = "に置く" return f"【ステージ選択】 {areas[idx]}: {name}を{desc}" # 500-509: Hand Card Selection (Discard/Recover) # Triggered when specifically selecting a card from hand as an effect cost or target. # Note: Normal playing of cards (1-180) is handled separately. if 500 <= a <= 509: idx = a - 500 if idx < len(p.hand): cid = p.hand[idx] name = get_card_name(cid) desc = "選択" choice_type, params = get_top_pending() if choice_type: if choice_type == "RECOVER_MEMBER": desc = "回収" elif choice_type == "DISCARD": desc = "捨てる" return f"【手札選択】 {name}を{desc}" # 570-579: Mode Selection (Branching Effects) # Triggered by O_SELECT_MODE when a card offers multiple choices (e.g., Mode A/B). if 570 <= a <= 579: mode_label = f"モード {a - 570 + 1}" choice_type, params = get_top_pending() if choice_type: options = params.get("options", []) if a - 570 < len(options): mode_label = options[a - 570] return f"【モード選択】 {mode_label}" # 590-599: Ability Trigger/Resolution Order # Triggered when multiple abilities are pending (e.g., [OnPlay] triggers). if 590 <= a <= 599: idx = a - 590 if idx < len(gs.triggered_abilities): t = gs.triggered_abilities[idx] if len(t) >= 2: cid = getattr(t[2], "card_id", -1) if len(t) > 2 else -1 src_name = get_card_name(cid) if cid >= 0 else "不明" return f"【能力解決】 {src_name}の効果を発動 ({idx + 1}/{len(gs.triggered_abilities)})" return f"【能力解決】 ({idx + 1}/{len(gs.triggered_abilities)})" # 820-822: Live Zone Targeting if 820 <= a <= 822: idx = a - 820 areas = ["左", "センター", "右"] cid = p.live_zone[idx] if idx < len(p.live_zone) else -1 name = "なし" if cid >= 0: name = get_card_name(cid) return f"【ライブ選択】 {areas[idx]}: {name}" # 900-902: Performance Execution # Standard action to clear a Live card in the Performance phase. if 900 <= a <= 902: idx = a - 900 areas = ["左", "センター", "右"] cid = p.live_zone[idx] if idx < len(p.live_zone) else -1 name = "なし" summary = "パフォーマンス" if cid >= 0: name = get_card_name(cid) base_id = get_base_id(cid) live = get_from_db(live_db, base_id) if live: abilities = get_v(live, "abilities", []) if abilities: raw_text = get_v(abilities[0], "raw_text", "").strip() if raw_text: summary = raw_text.split("\n")[0][:40] if len(raw_text) > 40: summary += "..." return f"【パフォーマンス】 {areas[idx]}: {name} ({summary})" # 600-719: Broad Choice Range (General Targets/Opponent) if 600 <= a <= 719: idx = a - 600 choice_type, params = get_top_pending() if choice_type == "ORDER_DECK": cards = params.get("cards", []) if idx < len(cards): return f"【並べ替え】 {get_card_name(cards[idx])}を一番上へ" return "【確定】 並び替えを終了" if 0 <= idx <= 2: if choice_type in ("SELECT_MEMBER", "TARGET_OPPONENT_MEMBER"): areas = ["左", "センター", "右"] opp = gs.get_player(1 - p_idx) if hasattr(gs, "get_player") else gs.inactive_player cid = opp.stage[idx] name = "空エリア" if cid >= 0: base_id = get_base_id(int(cid)) m = get_from_db(member_db, base_id) if m: name = get_v(m, "name", "メンバー") return f"【ターゲット】 相手のステージ ({areas[idx]}: {name}) を選択" if choice_type == "SELECT_FROM_LIST": cards = params.get("cards", []) if idx < len(cards): return f"【リスト選択】 {get_card_name(cards[idx])}" elif choice_type == "SELECT_MODE": options = params.get("options", []) if idx < len(options): return f"【選択】 {options[idx]}" elif choice_type == "SELECT_SUCCESS_LIVE": cards = params.get("cards", []) if idx < len(cards): return f"【獲得選択】 {get_card_name(cards[idx])}" elif choice_type == "SELECT_FROM_DISCARD": cards = params.get("cards", []) if idx < len(cards): return f"【控え室回収】 {get_card_name(cards[idx])}" return f"【選択】 項目 {idx}" # 550-849: Complex Choice Resolution (Consolidated) # Used for choices that require specific card/slot context (e.g., Order Deck, Color Choose). if 550 <= a <= 849: adj = a - 550 area_idx = adj // 100 ab_idx = (adj % 100) // 10 choice_idx = adj % 10 area_name = ["左", "中", "右"][area_idx] if area_idx < 3 else f"Slot {area_idx}" cid = p.stage[area_idx] if area_idx < 3 else -1 card_name = "メンバー" if cid >= 0: base_id = get_base_id(cid) m = get_from_db(member_db, base_id) if m: card_name = get_v(m, "name", "メンバー") choice_type, params = get_top_pending() choice_label = f"選択 {choice_idx}" if choice_type == "ORDER_DECK": cards = params.get("cards", []) if choice_idx < len(cards): choice_label = f"デッキトップ: {get_card_name(cards[choice_idx])}" else: choice_label = "[確定]" elif choice_type == "COLOR_SELECT": colors = ["赤", "青", "緑", "黄", "紫", "ピンク"] if choice_idx < len(colors): choice_label = f"色選択: {colors[choice_idx]}" elif choice_type == "SELECT_MODE": options = params.get("options", []) if choice_idx < len(options): choice_label = options[choice_idx] else: choice_label = f"モード {choice_idx + 1}" elif choice_type == "SELECT_FROM_LIST": cards = params.get("cards", []) if choice_idx < len(cards): choice_label = f"選択: {get_card_name(cards[choice_idx])}" return f"[{card_name}] {choice_label} ({area_name})" # 3. PHASE-SPECIFIC CORE ACTIONS (Main Range) # -------------------------------------------------------------------------- # 1-180: Playing Members (Main Phase) # Each hand index has 3 target slots: aid = 1 + (hand_idx * 3) + slot_idx if 1 <= a <= 180 and int(gs.phase) == int(Phase.MAIN): idx = (a - 1) // 3 area_idx = (a - 1) % 3 areas = ["左", "センター", "右"] area_name = areas[area_idx] card_name = f"カード[{idx}]" new_card_cost = 0 suffix = "" if idx < len(p.hand): cid = p.hand[idx] base_cid = get_base_id(int(cid)) m = get_from_db(member_db, base_cid) if m: card_name = get_v(m, "name", "メンバー") new_card_cost = get_v(m, "cost", 0) abilities = get_v(m, "abilities", []) if any(get_v(ab, "trigger", 0) == 1 for ab in abilities): suffix = " [登場]" stage_cid = p.stage[area_idx] if stage_cid >= 0: base_stage_cid = get_base_id(int(stage_cid)) old_card = get_from_db(member_db, base_stage_cid) if old_card: old_name = get_v(old_card, "name", "メンバー") old_cost = get_v(old_card, "cost", 0) actual_cost = max(0, new_card_cost - old_cost) return f"【{area_name}に置く】 {card_name}{suffix} (バトンタッチ: {old_name}退場, 支払:{actual_cost})" return f"【{area_name}に置く】 {card_name}{suffix} (コスト {new_card_cost})" # 100-159: Energy Charge Selection if 100 <= a <= 159 and int(gs.phase) == int(Phase.ENERGY): idx = a - 100 card_name = f"手札[{idx}]" if idx < len(p.hand): card_name = get_card_name(p.hand[idx]) return f"【エネルギー】 {card_name}をチャージ" # 300-359: Mulligan Selection # Toggles cards to be shuffled back during the mulligan phase. if 300 <= a <= 359: idx = a - 300 card_name = f"手札[{idx}]" if idx < len(p.hand): card_name = get_card_name(p.hand[idx]) return f"【マリガン】 {card_name}を選択/解除" # 400-459: Live Set Selection # Sets a Live card from hand to face-up or face-down zone. if 400 <= a <= 459: idx = a - 400 card_name = f"手札[{idx}]" score_text = "" if idx < len(p.hand): cid = p.hand[idx] card_name = get_card_name(cid) base_id = get_base_id(cid) live = get_from_db(live_db, base_id) return f"【ライブセット】 {card_name}{score_text}" # 200-299: Activated Ability on Stage # [起動] abilities triggered manually by the player. if 200 <= a <= 299: adj = a - 200 area_idx = adj // 10 ab_idx = adj % 10 areas = ["左", "センター", "右"] area_name = areas[area_idx] if area_idx < 3 else f"Slot {area_idx}" cid = p.stage[area_idx] if area_idx < 3 else -1 if cid >= 0: base_cid = get_base_id(int(cid)) member = get_from_db(member_db, base_cid) if member: card_name = get_v(member, "name", "メンバー") abilities = get_v(member, "abilities", []) summary = "アビリティ" if len(abilities) > ab_idx: ab = abilities[ab_idx] raw_text = get_v(ab, "raw_text", "").strip() if raw_text: summary = raw_text.split("\n")[0][:40] if len(raw_text) > 40: summary += "..." return f"【起動】{card_name}: {summary} ({area_name})" return f"Ability ({area_name})" # 2000-2999: Discard Pile Activation # Playing or activating effects of cards currently in the discard zone. if 2000 <= a <= 2999: adj = a - 2000 discard_idx = adj // 10 ab_idx = adj % 10 card_name = f"控え室[{discard_idx}]" if discard_idx < len(p.discard): cid = p.discard[discard_idx] card_name = get_card_name(cid) base_id = get_base_id(cid) member = get_from_db(member_db, base_id) if member: abilities = get_v(member, "abilities", []) if len(abilities) > ab_idx: ab = abilities[ab_idx] raw_text = get_v(ab, "raw_text", "").strip() summary = raw_text.split("\n")[0][:40] if raw_text else "効果" return f"【控え召喚】 {card_name}: {summary}" return f"【控え召喚】 {card_name}" # 1000-1999: OnPlay Sub-Choices # Options offered immediately during/after a member is played. if 1000 <= a <= 1999: adj = a - 1000 hand_idx = adj // 100 slot_idx = (adj % 100) // 10 choice_idx = adj % 10 choice_label = f"選択 {choice_idx}" choice_type, params = get_top_pending() if choice_type == "ORDER_DECK": cards = params.get("cards", []) if choice_idx < len(cards): choice_label = f"{get_card_name(cards[choice_idx])}をトップへ" else: choice_label = "[確定]" elif choice_type == "COLOR_SELECT": colors = ["赤", "青", "緑", "黄", "紫", "ピンク"] if choice_idx < len(colors): choice_label = f"{colors[choice_idx]}を選択" elif choice_type == "SELECT_MODE": options = params.get("options", []) if choice_idx < len(options): choice_label = options[choice_idx] else: choice_label = f"モード {choice_idx + 1}" elif choice_type == "SELECT_FROM_LIST": cards = params.get("cards", []) if choice_idx < len(cards): choice_label = f"{get_card_name(cards[choice_idx])}を選択" return choice_label # 4. FALLBACKS # -------------------------------------------------------------------------- # 510-559: Generic Hand Selection Fallback if 510 <= a <= 559: idx = a - 500 # Keep idx logic consistent with 500-509 card_name = f"手札[{idx}]" if idx < len(p.hand): card_name = get_card_name(p.hand[idx]) return f"【手札選択】 {card_name}" return f"Action {a}" return f"Action {a}"