LovecaSim / engine /game /desc_utils.py
trioskosmos's picture
Upload folder using huggingface_hub
bb3fbf9 verified
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}"