#!/usr/bin/env python3 # -*- coding: utf-8 -*- """试玩日志统计分析工具(消费 playtest_logger 的 JSONL——user://logs/playtest_*.jsonl)。 统计口径: · 每轮获得的奖励(效果/治疗/奖励伤害/未生效)与充能点数 · 实际造成伤害 / 实际受到伤害 / 恢复总量 · 增伤贡献(attack_up + 充能穿刺/魔法)、减伤贡献(hero_defense×enemy_output 池挡下的承伤) · 反事实一「无奖励造伤」:不算增伤和奖励伤害的主角输出(普通分量按无 attack_up 重算) · 反事实二「无减伤承伤」:不算减伤池(hero_defense/enemy_output 归零)的承伤 · 反事实三「无奖励承伤」:再扣伏击附加与 enrage(池归零已含) 两种日志格式: · 旧格式(step 行无 payload):反事实按比例近似(误差每行 ±1~2,多 floor 链无法逆精确); 掷点折扣与 ac_up 对敌方判定的影响不可恢复,忽略(已标注)。 · 新格式(step 行携带 roll/ac/tier/crit/normal/pierce/magic/baseline/attach): 反事实按 DamageMath 公式逐行精确重算。 · 敌方伤害行携带 rider(必中附加分量)——计为敌方招式成分, 无减伤世界(cf_tn)含 rider、无奖励世界(cf_tr)保留 rider 只剥伏击附加。 表值硬编码溯源(表值变更须同步——单一事实源仍在 .tres): · status_effects.tres:attack_up 50 / weaken −65 / enrage 25 / off_guard 25 / sapped −25(percent) · settlement.tres:pool_clamp [-100,100] · crit_multiplier 200 · 伏击机制已退役(blast 负向=警觉,敌方 AC+1,掷点分布型,无 flat 附加项)—— M_AMB 置 0 保持反事实管道兼容旧日志;新日志不携带 ambush 效果行 · skills.tres:突刺/撕咬 1.5 · 重击 2.0 · 毒爆 2.5 · 普攻 1.0 · armor_effectiveness:pierce 克 ward 150 / heavy 50;magic 克 heavy 150 / ward 50;normal 恒 100 """ import glob import json import os import sys from pathlib import Path from godot_user_dir import resolve_user_dir # ---- 表值区(溯源见文件头) ---- POOL_CLAMP = (-100, 100) CRIT_MULT = 200 M_AMB = 0 # 伏击已退役——反事实剥除项归零,旧日志兼容读 SKILL_MULT = {"普通攻击": 1.0, "突刺": 1.5, "重击": 2.0, "撕咬": 1.5, "毒爆": 2.5} EFFECT_TEXT_MAP = { # 旧日志无 effect_id——按明细行文本反查 "增伤": "attack_up", "护甲强化": "ac_up", "削弱": "weaken", "暴走": "enrage", "失衡": "off_guard", "乏力": "sapped", "警觉": "alert", "伏击": "ambush", "蓄力": "empower", "硬化": "fortify", } EFFECT_CN = {v: k for k, v in EFFECT_TEXT_MAP.items()} # effect_id → 中文显示名 # 正负分类(奖励件 / 惩罚件的口径——数据分析的三维度共用) POS_NAMES = {"增伤", "护甲强化", "削弱", "群伤", "回复", "清障炸药"} NEG_NAMES = {"暴走", "失衡", "乏力", "伏击", "警觉", "踩雷直伤"} POOL_OF = {"attack_up": "hero_offense", "sapped": "hero_offense", "off_guard": "hero_defense", "weaken": "enemy_output", "enrage": "enemy_output", "empower": "enemy_output", "fortify": "enemy_defense"} ARMOR_EFF = {"pierce": {"light": 100, "ward": 150, "heavy": 50}, "magic": {"light": 100, "ward": 50, "heavy": 150}} def clamp_pool(v): return max(POOL_CLAMP[0], min(POOL_CLAMP[1], v)) def charge_comp(points, eff): return int(points * eff / 100.0) # floor def rolled_component(base, discount, crit, eff): v = base * discount / 100.0 if crit: v *= CRIT_MULT / 100.0 return int(v * eff / 100.0) # floor def disp_w(s): # 中文显示宽度(终端对齐用) return sum(2 if ord(c) > 0x2E7F else 1 for c in str(s)) def pad(s, w, right=False): s = str(s) gap = w - disp_w(s) return (" " * gap + s) if right else (s + " " * gap) def effect_id_of(row): if row.get("effect_id"): return row["effect_id"] for name, eid in EFFECT_TEXT_MAP.items(): if name in row.get("text", ""): return eid return None def parse_log(path): rows = [] with open(path, encoding="utf-8") as f: for ln in f: ln = ln.strip() if ln: rows.append(json.loads(ln)) return rows # ---- 多局聚合模式(monte carlo 共享日志:run 行切分局次,按通关/失败分组对比) ---- def analyze_runs(rows, label, tier=None, policy=None): """tier = (atk_pct, hp_pct):只统计该天赋档(None = 全档)。 policy = "normal"/"perfect":只统计该策略画像 (双画像同文件时分开聚合做配对对照;None = 全画像混算)。""" runs = [] # [{meta, rows}] cur = None for r in rows: k = r.get("kind") if k == "run": cur = {"meta": r, "rows": []} runs.append(cur) elif cur is not None: cur["rows"].append(r) if tier is not None: runs = [run for run in runs if int(run["meta"].get("atk_pct", -999)) == tier[0] and int(run["meta"].get("hp_pct", -999)) == tier[1]] if policy is not None: runs = [run for run in runs if run["meta"].get("policy") == policy] stats = [] for run in runs: res = analyze_run_core(run["rows"], run["meta"]) if res: stats.append(res) print_run_report(label, stats) def analyze_run_core(rows, meta): """单局聚合:返回该局的总账(结果/死亡节点/造伤/承伤/回复/奖励/轮数 + 战斗级明细)。""" out = dict(meta=meta, result="", death_node=-1, hp_left=-1, hero_dmg=0, reward_dmg=0, taken=0, healed=0, rest_heal=0, rounds=0, rewards_n=0, eff={}, battles_won=0, battles=0, battle_list=[], refresh_ok=0, refresh_rej=0, exits={}) eff_counts = {} cur_b = None for r in rows: k = r.get("kind") if k == "node" and r.get("type") in ("battle", "boss"): out["battles"] += 1 cur_b = dict(btype=r.get("type"), depth=int(r.get("node_depth", r.get("depth", -1))), rounds=0, hero_dmg=0, reward_dmg=0, taken=0, healed=0, pos_n=0, neg_n=0, result="") out["battle_list"].append(cur_b) elif k == "battle_end": if r.get("result") == "victory": out["battles_won"] += 1 if cur_b is not None: cur_b["result"] = r.get("result", "") cur_b["end_hp"] = int(r.get("hero_hp", -1)) elif k == "level_end": out["result"] = r.get("result", "") out["death_node"] = int(r.get("node_index", -1)) out["hp_left"] = int(r.get("hp", -1)) elif k == "settlement": out["rounds"] += 1 if cur_b is not None: cur_b["rounds"] += 1 elif k == "step": t, src = r["type"], r.get("source", "") if t == "effect": eid = effect_id_of(r) if eid: name = "伏击" if eid == "ambush" else EFFECT_CN.get(eid, eid) eff_counts[name] = eff_counts.get(name, 0) + 1 if eid != "ambush": out["rewards_n"] += 1 if cur_b is not None: if name in NEG_NAMES: cur_b["neg_n"] += 1 elif name in POS_NAMES: cur_b["pos_n"] += 1 elif t == "heal": out["healed"] += int(r["value"]) out["rewards_n"] += 1 eff_counts["回复"] = eff_counts.get("回复", 0) + 1 if cur_b is not None: cur_b["healed"] += int(r["value"]) cur_b["pos_n"] += 1 elif t == "damage": v = int(r["value"]) if src == "hero_attack": out["hero_dmg"] += v if cur_b is not None: cur_b["hero_dmg"] += v elif src in ("safe_cell", "clear_reward", "flag_hit") or ( src == "mine_hit" and "全体敌人" in r.get("text", "")): out["reward_dmg"] += v out["rewards_n"] += 1 name = "清障炸药" if src == "clear_reward" else "群伤" eff_counts[name] = eff_counts.get(name, 0) + 1 if cur_b is not None: cur_b["reward_dmg"] += v cur_b["pos_n"] += 1 elif src == "mine_hit": out["taken"] += v eff_counts["踩雷直伤"] = eff_counts.get("踩雷直伤", 0) + 1 if cur_b is not None: cur_b["taken"] += v cur_b["neg_n"] += 1 elif src == "enemy_action": out["taken"] += v if cur_b is not None: cur_b["taken"] += v if k == "node" and r.get("type") == "rest": out["rest_heal"] += int(r.get("healed", 0)) # 弃图刷新与轮出口(常设读数) elif k == "action" and r.get("op") == "refresh": if r.get("accepted", False): out["refresh_ok"] += 1 else: out["refresh_rej"] += 1 elif k == "ux" and r.get("ux") == "ux_round_exit": tag = r.get("path_tag", "?") out["exits"][tag] = out["exits"].get(tag, 0) + 1 out["eff"] = eff_counts return out if out["rounds"] > 0 else None def print_run_report(label, stats): print("=" * 104) print("多局聚合:%s (共 %d 局)" % (label, len(stats))) print("=" * 104) wins = [s for s in stats if s["result"] == "victory"] loss = [s for s in stats if s["result"] != "victory"] print("\n通关 %d 局 · 失败 %d 局(通关率 %.0f%%)" % (len(wins), len(loss), 100.0 * len(wins) / max(1, len(stats)))) def avg(group, key): return sum(s[key] for s in group) / max(1, len(group)) def eff_line(group): total_rounds = sum(s["rounds"] for s in group) or 1 parts = [] names = ["增伤", "护甲强化", "削弱", "群伤", "回复", "暴走", "失衡", "乏力", "伏击", "清障炸药", "踩雷直伤"] for n in names: c = sum(s["eff"].get(n, 0) for s in group) if c: parts.append("%s %.2f件/轮" % (n, c / total_rounds)) return "、".join(parts) if parts else "—" def group_block(name, group): print("\n【%s · %d 局】" % (name, len(group))) if not group: return print(" 平均总造成伤害(主角攻击) :%.1f" % avg(group, "hero_dmg")) print(" 平均奖励伤害(群伤/炸药) :%.1f" % avg(group, "reward_dmg")) print(" 平均总受到伤害 :%.1f" % avg(group, "taken")) print(" 平均战斗内回复 :%.1f" % avg(group, "healed")) print(" 平均恢复节点回复 :%.1f" % avg(group, "rest_heal")) print(" 平均结算轮数 :%.1f" % avg(group, "rounds")) print(" 平均每局奖励件数 :%.1f(%.2f 件/轮)" % ( avg(group, "rewards_n"), avg(group, "rewards_n") / max(1.0, avg(group, "rounds")))) # 弃图刷新面:刷新板数÷总板数 = 弃图率 boards = sum(s["battles"] + s["refresh_ok"] for s in group) refreshes = sum(s["refresh_ok"] for s in group) print(" 平均每局弃图刷新 :%.2f 次(被拒 %.2f 次) 弃图率:%.0f%%" % ( refreshes / max(1, len(group)), sum(s["refresh_rej"] for s in group) / max(1, len(group)), 100.0 * refreshes / max(1, boards))) exits = {} for s in group: for k, v in s["exits"].items(): exits[k] = exits.get(k, 0) + v exit_total = sum(exits.values()) or 1 exit_cn = {"cleared": "清图换板", "stopped": "主动停手", "ap_exhausted": "行动点耗尽"} print(" 轮出口分布 :%s" % "、".join( "%s %d(%.0f%%)" % (exit_cn.get(k, k), v, 100.0 * v / exit_total) for k, v in sorted(exits.items(), key=lambda x: -x[1]))) print(" 地图战斗资源构成(件/轮) :%s" % eff_line(group)) if name.startswith("失败"): dn = {} for s in group: dn[s["death_node"]] = dn.get(s["death_node"], 0) + 1 print(" 死亡节点分布 :%s" % "、".join( "节点%s ×%d" % (k, v) for k, v in sorted(dn.items(), key=lambda x: -x[1]))) group_block("通关局", wins) group_block("失败局", loss) # ---- 战斗状态对照(普通战斗 vs Boss——跨局聚合) ---- def battle_block(name, group): rows = [b for s in group for b in s.get("battle_list", [])] if not rows: return print("\n【战斗状态对照 · %s】" % name) hdr = ["战斗类型", "场次", "场均轮数", "场均造成(主角+奖励)", "场均受到", "场均回复", "场均奖励件", "场均惩罚件", "胜率"] widths = [10, 6, 10, 20, 10, 10, 11, 11, 8] print(" " + " | ".join(pad(h, w) for h, w in zip(hdr, widths))) for btype, label_cn in (("battle", "普通战斗"), ("boss", "Boss")): sub = [b for b in rows if b.get("btype") == btype] if not sub: continue n = len(sub) won = sum(1 for b in sub if b.get("result") == "victory") cells = [label_cn, n, "%.1f" % (sum(b["rounds"] for b in sub) / n), "%.1f" % (sum(b["hero_dmg"] + b["reward_dmg"] for b in sub) / n), "%.1f" % (sum(b["taken"] for b in sub) / n), "%.1f" % (sum(b["healed"] for b in sub) / n), "%.1f" % (sum(b["pos_n"] for b in sub) / n), "%.1f" % (sum(b["neg_n"] for b in sub) / n), "%.0f%%" % (100.0 * won / n)] print(" " + " | ".join(pad(c, w) for c, w in zip(cells, widths))) battle_block("通关局", wins) battle_block("失败局", loss) print() def analyze(rows, label): # 战斗开始生命预扫描:running_hp 从初始满血起步,battle_end 落账、rest 节点回血中途累加 running_hp = None for r in rows: k = r.get("kind") if k == "settlement" and running_hp is None: running_hp = int(r.get("hero", {}).get("max_hp", 0)) elif k == "node" and r.get("type") == "rest" and running_hp is not None: running_hp += int(r.get("healed", 0)) elif k == "battle_end": r["_start_hp"] = running_hp running_hp = int(r.get("hero_hp", -1)) battles = [] # [{name, rounds: {round: {"sett": snap, "steps": [rows]}}, prev_ambush}] cur = None for r in rows: k = r.get("kind") if k == "node" and r.get("type") in ("battle", "boss"): cur = {"name": "%s D%d" % (r["type"], r.get("depth", -1)), "btype": r.get("type"), "rounds": {}, "seq": len(battles)} battles.append(cur) elif k == "settlement": if cur is None: cur = {"name": "独立战斗", "btype": "battle", "rounds": {}, "seq": 0} battles.append(cur) cur["rounds"].setdefault(r["round"], {"sett": None, "steps": []})["sett"] = r elif k == "step": if cur is None: continue cur["rounds"].setdefault(r["round"], {"sett": None, "steps": []})["steps"].append(r) elif k == "battle_end" and cur is not None: cur["result"] = r.get("result") cur["end_hp"] = int(r.get("hero_hp", -1)) cur["start_hp"] = int(r.get("_start_hp", -1)) # ---- 每战斗逐轮分解 ---- per_battle = [] totals = dict(hero_dmg=0, reward_dmg=0, taken=0, healed=0, cf_hero_dmg=0, cf_taken_nored=0, cf_taken_norw=0, rest_heal=0, ineffective=0, pos_n=0, neg_n=0, pierce=0, magic=0, mines=0, back_sum=0) exact_mode = None for b in battles: rounds = sorted(b["rounds"].keys()) prev_ambush = 0 bstat = dict(hero_dmg=0, reward_dmg=0, taken=0, healed=0, cf_hero_dmg=0, cf_taken_nored=0, cf_taken_norw=0, rounds=[], pos_n=0, neg_n=0, pierce=0, magic=0, mines=0, back_sum=0, start_hp=int(b.get("start_hp", -1)), end_hp=int(b.get("end_hp", -1)), btype=b.get("btype", "battle")) for rd in rounds: srow = b["rounds"][rd] sett = srow["sett"] or {} steps = srow["steps"] pool = {} # effect_id -> pool_value(挂层行的当轮池值) ambush_gain = 0 for s in steps: if s["type"] == "effect": eid = effect_id_of(s) if eid == "ambush": ambush_gain += int(s["value"]) elif eid: pool[eid] = int(s.get("pool_value", s["value"])) up = pool.get("attack_up", 0) # 现行实现:hero_offense 走 attack_up 单效果直乘 hero_def = clamp_pool(pool.get("off_guard", 0)) enemy_out = clamp_pool(pool.get("weaken", 0) + pool.get("enrage", 0) + pool.get("empower", 0)) enemy_def = clamp_pool(pool.get("fortify", 0)) ambush_layers = prev_ambush + ambush_gain attach = ambush_layers * M_AMB k_taken = (100 + hero_def) * (100 + enemy_out) / 10000.0 enemies = sett.get("enemies", []) armor0 = enemies[0].get("armor", "light") if enemies else "light" hero_atk = sett.get("hero", {}).get("atk", 0) chg = sett.get("round_facts", {}).get("charge_points", {}) if sett else {} facts = sett.get("round_facts", {}) if sett else {} rowout = dict(rd=rd, safe=facts.get("safe", 0), mines=facts.get("mines", 0), flags=facts.get("flag_events", 0), pierce=chg.get("pierce", 0), magic=chg.get("magic", 0), hero=0, rw=0, taken=0, heal=0, cf_hero=0, cf_tn=0, cf_tr=0, eff_counts={}, heal_n=0, heal_sum=0, aoe_n=0, aoe_sum=0, back_n=0, back_sum=0, clear_n=0, clear_sum=0, ineff_counts={}, pos_n=0, neg_n=0) for s in steps: t, src = s["type"], s.get("source", "") if t == "effect": eid = effect_id_of(s) if eid == "ambush": rowout["eff_counts"]["伏击"] = rowout["eff_counts"].get("伏击", 0) + 1 rowout["neg_n"] += 1 elif eid: name = EFFECT_CN.get(eid, eid) rowout["eff_counts"][name] = rowout["eff_counts"].get(name, 0) + 1 if name in NEG_NAMES: rowout["neg_n"] += 1 elif name in POS_NAMES: rowout["pos_n"] += 1 elif t == "heal": rowout["heal"] += int(s["value"]) rowout["heal_n"] += 1 rowout["heal_sum"] += int(s["value"]) rowout["pos_n"] += 1 elif t == "damage": if src == "hero_attack": v = int(s["value"]) rowout["hero"] += v if exact_mode is None: exact_mode = "normal" in s if exact_mode: comp = {a: int(s.get(a, 0)) for a in ("normal", "pierce", "magic")} cf_n = rolled_component(hero_atk, int(s.get("tier", 100)), bool(s.get("crit")), 100) cf_n = int(cf_n * (100 + enemy_def) / 100.0) cf = max(1, cf_n) if (comp["normal"] + comp["pierce"] + comp["magic"]) >= 1 else 0 rowout["cf_hero"] += cf_n if cf else v else: p = charge_comp(chg.get("pierce", 0), ARMOR_EFF["pierce"].get(armor0, 100)) m = charge_comp(chg.get("magic", 0), ARMOR_EFF["magic"].get(armor0, 100)) n = max(0, v - p - m) cf_n = int(round(n * 100.0 / (100 + up))) rowout["cf_hero"] += cf_n elif src in ("safe_cell", "clear_reward", "flag_hit") or ( src == "mine_hit" and "全体敌人" in s.get("text", "")): # 奖励伤害 = 打敌人的棋盘来源伤害;踩雷负向打主角的(反噬震荡)归承伤侧 rowout["rw"] += int(s["value"]) rowout["pos_n"] += 1 if src == "clear_reward": rowout["clear_n"] += 1 rowout["clear_sum"] += int(s["value"]) else: rowout["aoe_n"] += 1 rowout["aoe_sum"] += int(s["value"]) elif src == "mine_hit": v = int(s["value"]) rowout["taken"] += v rowout["back_n"] += 1 rowout["back_sum"] += v rowout["neg_n"] += 1 # 踩雷直伤照吃(不算减伤≠去掉它),但无奖励世界没有这套负向兑付 rowout["cf_tn"] += v elif src == "enemy_action": v = int(s["value"]) rowout["taken"] += v if exact_mode: base = float(s.get("baseline", v)) att = int(s.get("attach", 0)) rider = int(s.get("rider", 0)) # 必中附加分量——敌方招式成分,非棋盘来源 rolled = rolled_component(base, int(s.get("tier", 100)), bool(s.get("crit")), 100) pre = rolled + att + rider cf_nored = max(1, pre) cf_norw = max(1, rolled + rider) # 无奖励世界只剥棋盘来源的伏击附加,rider 保留 else: pre = int(round(v / k_taken)) if k_taken > 0 else v cf_nored = max(1, pre) cf_norw = max(1, pre - attach) rowout["cf_tn"] += cf_nored rowout["cf_tr"] += cf_norw for s in steps: if s["type"] == "reward_ineffective": txt = s.get("text", "") if "满层" in txt: key = "增益满层" elif "满血" in txt: key = "回血满血" elif "无存活敌人" in txt: key = "无存活敌人" elif "主角已倒下" in txt: key = "主角已倒下" else: key = "其他" rowout["ineff_counts"][key] = rowout["ineff_counts"].get(key, 0) + 1 bstat["hero_dmg"] += rowout["hero"] bstat["reward_dmg"] += rowout["rw"] bstat["taken"] += rowout["taken"] bstat["healed"] += rowout["heal"] bstat["cf_hero_dmg"] += rowout["cf_hero"] bstat["cf_taken_nored"] += rowout["cf_tn"] bstat["cf_taken_norw"] += rowout["cf_tr"] bstat["pos_n"] += rowout["pos_n"] bstat["neg_n"] += rowout["neg_n"] bstat["pierce"] += rowout["pierce"] bstat["magic"] += rowout["magic"] bstat["mines"] += rowout["mines"] bstat["back_sum"] += rowout["back_sum"] bstat["rounds"].append(rowout) prev_ambush = sett.get("ambush_layers", ambush_layers) bstat["name"] = b.get("name", "?") bstat["result"] = b.get("result", "?") per_battle.append(bstat) for kk in ("hero_dmg", "reward_dmg", "taken", "healed", "cf_hero_dmg", "cf_taken_nored", "cf_taken_norw", "pos_n", "neg_n", "pierce", "magic", "mines", "back_sum"): totals[kk] += bstat[kk] # ---- 关卡恢复节点(rest) ---- for r in rows: if r.get("kind") == "node" and r.get("type") == "rest": totals["rest_heal"] += int(r.get("healed", 0)) print_report(label, per_battle, totals, exact_mode) return totals def _reward_parts(r): """单轮/单战斗的奖励与惩罚构成文本(聚合计数,不逐件枚举)。""" pos, neg = [], [] for name, n in r.get("eff_counts", {}).items(): (neg if name in NEG_NAMES else pos).append("%s×%d" % (name, n)) for name, cnt_key, sum_key in (("群伤", "aoe_n", "aoe_sum"), ("清障炸药", "clear_n", "clear_sum")): if r.get(cnt_key): pos.append("%s×%d(%d点)" % (name, r[cnt_key], r[sum_key])) if r.get("heal_n"): pos.append("回复×%d(+%d)" % (r["heal_n"], r["heal_sum"])) if r.get("back_n"): neg.append("踩雷直伤×%d(%d点)" % (r["back_n"], r["back_sum"])) return pos, neg def print_report(label, per_battle, t, exact_mode): W = 22 mode_txt = ("精确重算(payload 日志)" if exact_mode else "比例近似(旧格式日志,每行误差 ±1~2;掷点折扣与护甲等级对敌方判定的影响不可恢复,忽略)") print("=" * 104) print("试玩统计:%s (反事实口径:%s)" % (label, mode_txt)) print("=" * 104) # ---- 维度一:整个关卡的状态 ---- print("\n" + "=" * 104) print("【维度一 · 整个关卡的状态】") print("=" * 104) rounds_total = sum(len(b["rounds"]) for b in per_battle) won = sum(1 for b in per_battle if b["result"] == "victory") print(" 战斗数 / 总轮数 / 通关战斗:%d 场 / %d 轮 / %d 胜" % (len(per_battle), rounds_total, won)) print(" 造成伤害:%d(主角攻击 %d + 奖励伤害 %d)" % ( t["hero_dmg"] + t["reward_dmg"], t["hero_dmg"], t["reward_dmg"])) print(" 受到伤害:%d(敌方行动 %d + 踩雷直伤 %d)" % ( t["taken"], t["taken"] - t["back_sum"], t["back_sum"])) print(" 回复总量:%d(战斗内 %d + 恢复节点 %d)" % ( t["healed"] + t["rest_heal"], t["healed"], t["rest_heal"])) print(" 充能总量:穿刺 %d 点 / 魔法 %d 点" % (t["pierce"], t["magic"])) print(" 获得奖励件数:%d 件 获得惩罚件数:%d 件 踩危险格数:%d" % ( t["pos_n"], t["neg_n"], t["mines"])) hp_trail = " → ".join("%d" % b["end_hp"] for b in per_battle if b["end_hp"] >= 0) print(" 生命轨迹(各战斗结束 HP):%s" % hp_trail) print(" 不算增伤和奖励伤害的造成伤害:%d(增伤+充能贡献 %d)" % ( t["cf_hero_dmg"], t["hero_dmg"] - t["cf_hero_dmg"])) print(" 不算减伤的受到伤害:%d(减伤池挡下 %d)" % ( t["cf_taken_nored"], t["cf_taken_nored"] - t["taken"])) print(" 不算奖励的受到伤害:%d" % t["cf_taken_norw"]) # ---- 维度二:一场战斗的状态 ---- print("\n" + "=" * 104) print("【维度二 · 一场战斗的状态】") print("=" * 104) hdr = ["战斗", "结果", "轮数", "生命(开始→结束)", "造成(主角+奖励)", "受到", "回复", "奖励件", "惩罚件", "踩雷", "充能(穿/魔)", "获得奖励 / 获得惩罚"] widths = [10, 6, 5, 14, 16, 6, 5, 7, 7, 5, 11, 46] print(" " + " | ".join(pad(h, w) for h, w in zip(hdr, widths))) for b in per_battle: agg = dict(eff_counts={}, aoe_n=0, aoe_sum=0, clear_n=0, clear_sum=0, heal_n=0, heal_sum=0, back_n=0, back_sum=0) for r in b["rounds"]: for k, v in r["eff_counts"].items(): agg["eff_counts"][k] = agg["eff_counts"].get(k, 0) + v for k in ("aoe_n", "aoe_sum", "clear_n", "clear_sum", "heal_n", "heal_sum", "back_n", "back_sum"): agg[k] += r[k] pos, neg = _reward_parts(agg) res_txt = "胜利" if b["result"] == "victory" else str(b["result"]) hp_txt = ("%d→%d" % (b["start_hp"], b["end_hp"])) if b["start_hp"] >= 0 else "—" cells = [b["name"], res_txt, len(b["rounds"]), hp_txt, "%d+%d" % (b["hero_dmg"], b["reward_dmg"]), b["taken"], b["healed"], "%d件" % b["pos_n"], "%d件" % b["neg_n"], b["mines"], "%d/%d" % (b["pierce"], b["magic"]), ("奖:" + "、".join(pos) if pos else "奖:—") + (" 罚:" + "、".join(neg) if neg else "")] print(" " + " | ".join(pad(c, w) for c, w in zip(cells, widths))) # ---- 维度三:轮次的状态 ---- print("\n" + "=" * 104) print("【维度三 · 轮次的状态】") print("=" * 104) for b in per_battle: print("\n— %s · %s —" % (b["name"], "胜利" if b["result"] == "victory" else b["result"])) hdr = ["轮", "开格(安/雷/旗)", "充能(穿/魔)", "获得奖励 / 获得惩罚", "造成伤害", "受到伤害", "回复量", "不算增伤和奖励伤害的造成伤害", "不算减伤的受到伤害", "不算奖励的受到伤害"] widths = [4, 14, 12, W + 20, 8, 8, 6, 20, 16, 16] print(" " + " | ".join(pad(h, w) for h, w in zip(hdr, widths))) all_ineff = {} for r in b["rounds"]: pos, neg = _reward_parts(r) reward_txt = ("奖:" + "、".join(pos) if pos else "奖:—") + \ (" 罚:" + "、".join(neg) if neg else "") cells = [r["rd"], "%d/%d/%d" % (r["safe"], r["mines"], r["flags"]), "%d/%d" % (r["pierce"], r["magic"]), reward_txt, r["hero"], r["taken"], r["heal"], r["cf_hero"], r["cf_tn"], r["cf_tr"]] print(" " + " | ".join(pad(c, w) for c, w in zip(cells, widths))) for k, n in r["ineff_counts"].items(): all_ineff[k] = all_ineff.get(k, 0) + n if all_ineff: print(" 奖励未生效统计:" + "、".join("%s×%d" % (k, n) for k, n in all_ineff.items())) print() def main(): log_dir = str(resolve_user_dir(Path(__file__).resolve().parent.parent) / "logs") path = None args = sys.argv[1:] if args and not args[0].startswith("-"): path = args[0] else: cands = sorted(glob.glob(os.path.join(log_dir, "playtest_*.jsonl")), key=os.path.getmtime) if not cands: print("未找到日志(%s)" % log_dir) sys.exit(1) path = cands[-1] rows = parse_log(path) # 命令行第二参 = runs:多局聚合模式(monte carlo 共享日志按 run 行切分,通关/失败分组对比); # --tier ATK,HP:只统计该天赋档(默认 0,0 基线档——0.5 传作 50)。 if "--runs" in args: tier = (0, 0) policy = None for i, a in enumerate(args): if a == "--tier" and i + 1 < len(args): tier = tuple(int(x) for x in args[i + 1].split(",")) elif a.startswith("--tier="): tier = tuple(int(x) for x in a.split("=")[1].split(",")) elif a == "--policy" and i + 1 < len(args): policy = args[i + 1] elif a.startswith("--policy="): policy = a.split("=")[1] analyze_runs(rows, os.path.basename(path), tier, policy) elif "--run" in args: # 抽单局(大日志里按 run 序号或种子挑一局)走三层维度报告 sel = None for i, a in enumerate(args): if a == "--run" and i + 1 < len(args): sel = args[i + 1] elif a.startswith("--run="): sel = a.split("=")[1] runs, cur = [], None for r in rows: if r.get("kind") == "run": cur = {"meta": r, "rows": []} runs.append(cur) elif cur is not None: cur["rows"].append(r) pick = None if sel is not None: for x in runs: # 先按种子精确匹配(模拟局 seed 唯一) if str(x["meta"].get("seed")) == sel: pick = x break if pick is None and sel.isdigit(): # 再按基线档(0,0)序号 base = [x for x in runs if int(x["meta"].get("atk_pct", -1)) == 0 and int(x["meta"].get("hp_pct", -1)) == 0] if int(sel) < len(base): pick = base[int(sel)] if pick is None: print("未找到该局(--run <序号|种子>)") sys.exit(1) sub = [dict(r, kind="session")] + pick["rows"] analyze(sub, "%s · seed=%s · 档位 atk%s%%/hp%s%%" % ( os.path.basename(path), pick["meta"].get("seed"), pick["meta"].get("atk_pct"), pick["meta"].get("hp_pct"))) else: analyze(rows, os.path.basename(path)) if __name__ == "__main__": main()