Files

235 lines
8.8 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from __future__ import annotations
import re
from copy import deepcopy
from typing import Any
DEFAULT_ROLE_BADGES: list[dict[str, Any]] = [
{
"id": "detective",
"name": "侦探徽章",
"description": "擅长抽丝剥茧,在悬疑/推理类剧本中可获得额外线索优势。",
"unlock_metric": "plays",
"unlock_threshold": 3,
"genre_keywords": ["悬疑", "推理", "调查", "mystery", "detective"],
"effect_item": "侦探工具包",
"effect_clue": "【侦探直觉】你提前注意到一处可疑细节。",
"special_choice": "侦探徽章:进行现场勘验",
},
{
"id": "negotiator",
"name": "交涉者徽章",
"description": "擅长沟通博弈,在社交/阵营类剧本中可获得额外交涉筹码。",
"unlock_metric": "wins",
"unlock_threshold": 2,
"genre_keywords": ["社交", "阵营", "心理", "博弈", "negotiation", "social"],
"effect_item": "交涉筹码",
"effect_clue": "【交涉洞察】你看穿了对方一句话中的破绽。",
"special_choice": "交涉者徽章:发起高压谈判",
},
{
"id": "infiltrator",
"name": "潜行者徽章",
"description": "擅长隐秘行动,在生存/探索类剧本中可获得额外渗透手段。",
"unlock_metric": "best_streak",
"unlock_threshold": 2,
"genre_keywords": ["潜行", "生存", "探索", "渗透", "stealth", "survival", "explore"],
"effect_item": "潜行工具",
"effect_clue": "【潜行本能】你发现一条更隐蔽的行动路径。",
"special_choice": "潜行者徽章:尝试隐秘渗透",
},
]
ALLOWED_UNLOCK_METRICS = {"plays", "wins", "best_streak"}
def _safe_int(value: Any, default: int = 0) -> int:
try:
return int(value)
except (TypeError, ValueError):
return int(default)
def _parse_text_list(raw_value: Any, *, limit: int = 12) -> list[str]:
if isinstance(raw_value, list):
rows = [str(item).strip() for item in raw_value if str(item).strip()]
else:
rows = [row.strip() for row in re.split(r"[\n,,]", str(raw_value or "")) if row.strip()]
return rows[:limit]
def default_role_badges() -> list[dict[str, Any]]:
return deepcopy(DEFAULT_ROLE_BADGES)
def normalize_badge_definitions(
raw_value: Any,
*,
default_to_builtin: bool = True,
limit: int = 12,
) -> list[dict[str, Any]]:
rows = raw_value if isinstance(raw_value, list) else []
normalized: list[dict[str, Any]] = []
seen_ids: set[str] = set()
for idx, row in enumerate(rows[: max(1, int(limit))]):
if not isinstance(row, dict):
continue
raw_id = str(row.get("id", "")).strip().lower()
seed_id = raw_id or f"badge_{idx+1}"
badge_id = re.sub(r"[^a-z0-9_-]+", "_", seed_id).strip("_")[:36]
if not badge_id or badge_id in seen_ids:
continue
seen_ids.add(badge_id)
name = str(row.get("name", "")).strip()[:24]
if not name:
continue
unlock_metric = str(row.get("unlock_metric", "plays")).strip().lower()
if unlock_metric not in ALLOWED_UNLOCK_METRICS:
unlock_metric = "plays"
threshold = max(1, _safe_int(row.get("unlock_threshold", 1), 1))
normalized.append(
{
"id": badge_id,
"name": name,
"description": str(row.get("description", "")).strip()[:180],
"unlock_metric": unlock_metric,
"unlock_threshold": threshold,
"genre_keywords": _parse_text_list(row.get("genre_keywords", []), limit=16),
"effect_item": str(row.get("effect_item", "")).strip()[:48],
"effect_clue": str(row.get("effect_clue", "")).strip()[:180],
"special_choice": str(row.get("special_choice", "")).strip()[:48],
}
)
if not normalized and default_to_builtin:
return default_role_badges()
return normalized
def _resolve_badges(badge_definitions: Any = None) -> list[dict[str, Any]]:
if badge_definitions is None:
return default_role_badges()
return normalize_badge_definitions(badge_definitions, default_to_builtin=False)
def _badge_map(badge_definitions: Any = None) -> dict[str, dict[str, Any]]:
rows = _resolve_badges(badge_definitions)
return {str(row.get("id", "")).strip(): row for row in rows if str(row.get("id", "")).strip()}
def normalize_badge_id(value: Any, badge_definitions: Any = None) -> str:
badge_id = str(value or "").strip().lower()
if not badge_id:
return ""
if badge_id in _badge_map(badge_definitions):
return badge_id
return ""
def build_badge_progress(
stats: dict[str, Any],
active_badge: Any = "",
badge_definitions: Any = None,
) -> dict[str, Any]:
role_badges = _resolve_badges(badge_definitions)
plays = max(0, _safe_int(stats.get("played_games", 0), 0))
wins = max(0, _safe_int(stats.get("wins", 0), 0))
best_streak = max(0, _safe_int(stats.get("best_streak", 0), 0))
metric_values = {
"plays": plays,
"wins": wins,
"best_streak": best_streak,
}
rows: list[dict[str, Any]] = []
unlocked_ids: list[str] = []
for badge in role_badges:
badge_id = str(badge["id"])
metric_key = str(badge.get("unlock_metric", "plays"))
threshold = max(1, _safe_int(badge.get("unlock_threshold", 1), 1))
current = max(0, _safe_int(metric_values.get(metric_key, 0), 0))
unlocked = current >= threshold
if unlocked:
unlocked_ids.append(badge_id)
progress_value = min(current, threshold)
progress_percent = min(100, int(round((progress_value / threshold) * 100)))
rows.append(
{
"id": badge_id,
"name": str(badge.get("name", "")).strip()[:24],
"description": str(badge.get("description", "")).strip()[:180],
"unlock_metric": metric_key,
"unlock_threshold": threshold,
"unlock_current": current,
"progress_value": progress_value,
"progress_percent": progress_percent,
"unlocked": unlocked,
"genre_keywords": _parse_text_list(badge.get("genre_keywords", []), limit=16),
"special_choice": str(badge.get("special_choice", "")).strip()[:48],
"effect_item": str(badge.get("effect_item", "")).strip()[:48],
"effect_clue": str(badge.get("effect_clue", "")).strip()[:180],
}
)
normalized_active = normalize_badge_id(active_badge, role_badges)
if normalized_active and normalized_active not in unlocked_ids:
normalized_active = ""
if not normalized_active and unlocked_ids:
normalized_active = unlocked_ids[0]
return {
"active_badge": normalized_active,
"badges": rows,
"unlocked_ids": unlocked_ids,
"stats_snapshot": metric_values,
}
def badge_matches_card(badge_id: Any, card: dict[str, Any] | None, badge_definitions: Any = None) -> bool:
badge_map = _badge_map(badge_definitions)
normalized = str(badge_id or "").strip().lower()
if normalized not in badge_map:
return False
badge = badge_map.get(normalized, {})
keywords = [str(row).strip().lower() for row in badge.get("genre_keywords", []) if str(row).strip()]
if not keywords:
return False
payload = card if isinstance(card, dict) else {}
content_pack = payload.get("content_pack", {})
if not isinstance(content_pack, dict):
content_pack = {}
genre_tags = content_pack.get("genre_tags", [])
if not isinstance(genre_tags, list):
genre_tags = []
corpus = []
corpus.extend(str(row).strip().lower() for row in genre_tags if str(row).strip())
corpus.append(str(payload.get("title", "")).strip().lower())
corpus.append(str(payload.get("subtitle", "")).strip().lower())
corpus.append(str(payload.get("summary", "")).strip().lower())
text = " ".join(row for row in corpus if row)
if not text:
return False
return any(keyword in text for keyword in keywords)
def badge_effect_payload(badge_id: Any, badge_definitions: Any = None) -> dict[str, str]:
badge_map = _badge_map(badge_definitions)
normalized = str(badge_id or "").strip().lower()
if normalized not in badge_map:
return {"id": "", "name": "", "item": "", "clue": "", "special_choice": ""}
badge = badge_map.get(normalized, {})
return {
"id": normalized,
"name": str(badge.get("name", "")).strip()[:24],
"item": str(badge.get("effect_item", "")).strip()[:48],
"clue": str(badge.get("effect_clue", "")).strip()[:180],
"special_choice": str(badge.get("special_choice", "")).strip()[:48],
}