Files
gplan-2/tools/analyze_playtest.py
T
DKChan 7876642122 feat(resource): 格面资源八型批——伏击退役换装警觉+新增行动点第8型+护甲型数值对齐(2026-09-20 用户拍板)——①动机=全字典镜像对账(系统设计分析):伏击的层数×出手敌数复利集中在主角单血条、与爆裂粉尘摊 N 条敌方血条双重不对称;护甲型惩罚 −2 比奖励 +1 反向偏重;敌方护甲等级轴全系统空缺;②伏击退役=伤害公式拆除伏击附加项(敌方技能攻击=max(damage_floor, floor(掷点档位技能基础分量)×乘区)+rider)、快照 ambush_layers 字段拆除、效果行/毒爆复算断言同步;③blast 负向换装=enemy_alert 警觉(敌方全体护甲等级 +1/层,1 结算,side-wide 逐颗叠加无上限)——消费=_defender_ac 敌方侧 ac 型状态读法本批接线(防守方 AC=表值+Σac 型效果,敌方路径从纯表值扩展);④第 8 型 action=ap_up 行动点(正向,下轮 AP+1,weight 6)/ap_drain 迟滞(负向,下轮 AP−1)——action_economy 排队面 queue_ap_delta/pending_ap_delta 新增,notify_round_start 兑现(基线 6+净值,下限 0,兑现清零),明细记 effect 行;⑤护甲型对齐=off_guard −2→−1;⑥schema=八型封闭枚举十六格、伏击加层行特例校验退役、effect_pool_map 删 ambush/flat_attach 键;⑦表现层=敌卡伏击池行拆除(警觉入敌方增益投影,percent|ac 双量纲)、状态槽图标 ambush→alert;⑧文档=敌人系统 v0.12(伏击节退役改写)/战斗结算 v0.20(公式与授予段)/地图战斗资源 v0.14(八型字典)/行动经济 v0.5(下轮行动点修正节+F1 条款3 改写)/难度曲线 v0.15/增益状态 v0.8/06 清单(伏击量纲条目随批解决+调参批数值点评登记)/registry 22 处(ambush_unit_definition 退役+继任 alert、clause3_supply 伏击暴露口径作废、reward_table 八型 74 权重、行动经济排队面)/ux battle/关卡节点地图;⑨工具=analyze_playtest M_AMB 归零保旧日志兼容、clear_dominance 伏击暴露模型转历史口径注记(新口径归调参批);⑩验证=冒烟 330 项 0 失败三闸全过(新增警觉在位+action 正负抵消排队断言)+gdd_lint 与 master 基线一致(8 文件存量);遗留=警觉/行动点数值 provisional 归曲线调参批(06 已登记),真人批试玩建议在终版规则上执行
2026-09-20 03:58:15 +08:00

683 lines
34 KiB
Python
Raw 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.
#!/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 公式逐行精确重算。
· 新效应词汇批(2026-09-19):敌方伤害行携带 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
· 【2026-09-20 八型批】伏击机制退役(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
# ---- 表值区(溯源见文件头) ----
POOL_CLAMP = (-100, 100)
CRIT_MULT = 200
M_AMB = 0 # 伏击已退役(2026-09-20 八型批)——反事实剥除项归零,旧日志兼容读
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":只统计该策略画像(清图支配性对账批 2026-09-19——
双画像同文件时分开聚合做配对对照;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))
# 弃图刷新与轮出口(清图支配性对账批 2026-09-19 增补读数——此前日志有行无分析)
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"))))
# 弃图刷新面(清图支配性对账批 2026-09-19:刷新板数÷总板数 = 弃图率)
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)) # 必中附加分量(新效应词汇批 2026-09-19)——敌方招式成分,非棋盘来源
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 = 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)
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()