feat(slice): 分支节点图批收编——关卡2「幽暗地域」试玩批(图版行进器+图合法性六检+L2 内容行+选路 UI+首页直入口;冒烟 296 全绿+四探针 PASS;模态覆盖层 0×0 与 route_overlay 未入树两缺陷修复;试玩日志新格式+analyze_playtest 统计工具;五文档回填)

This commit is contained in:
DKChan committed 2026-09-17 21:38:10 +08:00
1 parent 31f4966d3e
commit 072dc43777
29 files changed
+1958 -177

No files matched your search

+642
View File
@@ -0,0 +1,642 @@
#!/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()