643 lines
31 KiB
Python
643 lines
31 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
"""试玩日志统计分析工具(消费 playtest_logger 的 JSONL——user://logs/playtest_*.jsonl)。
|
||
|
||
统计口径(2026-09-17 用户定义):
|
||
· 每轮获得的奖励(效果/治疗/奖励伤害/未生效)与充能点数
|
||
· 实际造成伤害 / 实际受到伤害 / 恢复总量
|
||
· 增伤贡献(attack_up + 充能穿刺/魔法)、减伤贡献(hero_defense×enemy_output 池挡下的承伤)
|
||
· 反事实一「无奖励造伤」:不算增伤和奖励伤害的主角输出(普通分量按无 attack_up 重算)
|
||
· 反事实二「无减伤承伤」:不算减伤池(hero_defense/enemy_output 归零)的承伤
|
||
· 反事实三「无奖励承伤」:再扣伏击附加与 enrage(池归零已含)
|
||
|
||
两种日志格式:
|
||
· 旧格式(step 行无 payload):反事实按比例近似(误差每行 ±1~2,多 floor 链无法逆精确);
|
||
掷点折扣与 ac_up 对敌方判定的影响不可恢复,忽略(已标注)。
|
||
· 新格式(2026-09-17 起 step 行携带 roll/ac/tier/crit/normal/pierce/magic/baseline/attach):
|
||
反事实按 DamageMath 公式逐行精确重算。
|
||
|
||
表值硬编码溯源(表值变更须同步——单一事实源仍在 .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 · 伏击附加 = ambush 层数×3(magnitude)
|
||
· 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
|
||
|
||
# ---- 表值区(溯源见文件头) ----
|
||
POOL_CLAMP = (-100, 100)
|
||
CRIT_MULT = 200
|
||
M_AMB = 3 # ambush.magnitude:每层伏击附加伤害
|
||
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", "伏击": "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):
|
||
"""tier = (atk_pct, hp_pct):只统计该天赋档(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]]
|
||
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=[])
|
||
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))
|
||
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"))))
|
||
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))
|
||
rolled = rolled_component(base, int(s.get("tier", 100)), bool(s.get("crit")), 100)
|
||
pre = rolled + att
|
||
cf_nored = max(1, pre)
|
||
cf_norw = max(1, rolled)
|
||
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 = os.path.join(os.environ.get("APPDATA", ""), "Godot", "app_userdata",
|
||
"DungeonSweeperVerticalSlice", "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)
|
||
for i, a in enumerate(args):
|
||
if a == "--tier" and i + 1 < len(args):
|
||
atk_s, hp_s = args[i + 1].split(",")
|
||
elif a.startswith("--tier="):
|
||
atk_s, hp_s = a.split("=")[1].split(",")
|
||
analyze_runs(rows, os.path.basename(path), tier)
|
||
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()
|