Spaces:
Running
Running
| 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}" | |