Files
gplan-2/tools/analyze_playtest.py
T
DKChan e66d1561bc feat(slice): 充能退役+board_type 词表定稿+06 待办两轮清理批(2026-09-19 用户逐项拍板)——含并行会话未提交工作一并入库
①充能退役批(用户终裁删除,结构批):
- 主角攻击回归单分量:battle_orchestrator 攻击段单分量化(payload 携带实际攻击类型键 + roll/ac/tier 掷点读数)、_charge_points/_charge_counts/_charge_component 与 round_facts 充能键全删
- DamageMath.charge_component → counter_component 改名(唯一消费方 = 奖励直伤道具的克制直伤,棋盘火力不受影响)
- settlement.tres charge_grants 键与供给注释删除;settlement_table schema 字段与校验删除
- 冒烟改形 298→293(净减 5 = 充能断言组退役):乘区接线探针单分量基线 100(原充电注入 102)、三分量断言改"类型键+掷点读数"、②′充能定义+②″改投桶探针整组退役、标中旗断言改正向兑付行、恢复点探针换种子 4242→42(原种子在新输出模型下恢复点前败北——探针目的 = 验回复公式,换种注释在案)
- 文档:战斗结算 v0.16(挖格/旗判定批充能条款删、退役注记替原供给表、乘区公式改攻击合计、调参表行删)、07 清单速览与③④观测项、README 三行退役注记、registry 四处(charge_grants 行删/ac_roll 范围/effect_pool_map Σ三分量→攻击合计/mechanics F6 改型)、难度曲线与Boss 联动表三行、01 核心循环数值语言句去充能
- 裁决依据:充能 ≠ 改变攻击类型(是在普攻外叠加穿刺/魔法分量);删除代价 = 混合甲战斗主角常态输出只吃一种克制(用户知情拍板);克制策略归冒险前配置(hero.attack_type 营地换型 + 未来冒险前参数面)

②board_type 词表定稿批(用户拍板:图级单值字段命名 board_type、玩家侧显示名「地图类型」、词表 {dungeon, underdark}):
- board_gen_table 域校验升级(非空 → 词表成员检查,BOARD_TYPE_VOCABULARY 常量);board_gen.tres 12 行单值化(L1 五行 dungeon、L2 七行 underdark——「第二关=幽暗地域」从文案变数据);board_generator 单值化;board_data.biome → board_type;config_db variant_pool 签名 Array → String(休眠无调用方)
- 棋盘生成.md 图类型节/调参表行/下游行、地图战斗资源.md 两处"词表已定稿"注记、registry 三处、experiment_density.gd 旧键同步;零行为变化(消费方全部休眠)

③board_data compute_r0 变异死分支删除(用户拍板——「带变异的 0 格不参与初始展开传播」从未触发,与排雷玩法展开实现口径归一;变异定稿批按正式语义重接)

④06 待办两轮清理(解决即移除新口径 + 用户逐项拍板):
- 第一轮 7 项:图版措辞对齐(battle.md"节点链"×6→"节点图"+冒险结构句+数据配置行)、轮内刷新读数核销(实现已在:编排器计数+AP 条"剩 N 次"文案;行动经济.md 补只读观测量句)、敌种线索键集核销(映射已类型级)、敌方攻击区间落文核销(字段表已在)、坍塌显形对位批次=表现层升级批(音频×2+战斗表现写入)、音量分工收口(入口归设置/生效归音频)、快照域进 Alpha 存档首批
- 第一轮改写:关卡表半边核销(level.tres 齐备)剩冒险表半边;变异条目死分支半边清偿
- 第一轮拍板登记:未开格类型微光方案(魔法紫/穿刺蓝/群伤红/治疗绿/增益黄,服务护甲克制规划;连带:不做清单 owner 修订合法/art-bible §4.6.3 对齐归美术批/探宝罗盘价值清空/色板检查归样张批/实施=表现层升级批切片默认不开)
- 第二轮:连锁溢出核销(满层拒绝+「溢出未生效」行即正式语义,零代码;并行会话同裁)、标错惩罚核销(极性矩阵已取代——标空=该安全格资源负向兑付)、监控阈值定案(07 新⑥⑦节:速杀预警 provisional 40% + 刷新监控日志先行阈值等基线——纯日志侧不影响玩法)、两关序列列入 Alpha 验收条件(切片期直入口可接受)、设置持久化独立存储(用户两分制:系统级设置一安装一份、删游戏才消失、不进存档;存档只存该档生涯数据——设置与本地化三处+存档序列化边界表补行+条目删除)、未开格微光并入预读边界+探宝罗盘两方案新立项(色光永久化路线 vs 黑帝斯塔罗牌式可选天赋路线,两案不互斥,归魔法道具内容重定批)、第二关"解锁新元素"删除、Alpha 全怪物技能池数值验证新立项(范围含池构成/权重/倍率/冷却/保底/EV 台阶/克制覆盖+倍率取整抖动复核+卓尔增益技与敌人系统文档 L2 行两缺漏复核;时点 Alpha 随验收重测同窗口)
- 过程更正:6-10 曾越权执行被用户指出,已全部回滚(四条目按 HEAD 原文恢复/伪"用户裁决"注记抹除/设置与存档两文档还原)后按真实拍板重落地;台账 77 立规——决策项必须逐项明确批准,推荐≠批准,严禁把建议写成"用户裁决"落文

⑤并行会话未提交工作一并入库(归属注明,均已通过冒烟):甲效矩阵用户调参批(settlement.tres 加法核矩阵 0/+1/−1、smoke 矩阵断言改形 300→298、enemy/hero.tres 批注)、普通策略档 v2 novice 推理画像(auto_player.gd +46 行/level_autoplay.gd——2026-09-19 用户拍板替代旧"无推理最近格"口径)、roadmap 收口纪律五步(+loop_dashboard 软纪律)、元进度定稿批文档、analyze_playtest.py 微调、并行会话 06 新增两提案条目(插旗不可逆/GP+商店房)

⑥验证:冒烟 293 项 0 失败(提交前新鲜复跑与在飞态一致);ui/click/layout/tune 四探针 exit=0;冒烟基线 300→298→293 的减项全部为断言组合并/退役,零失败记录

⑦未入库标旗:未跟踪文件维持未跟踪(美术清单×2、格面差异化提案、loop_dashboard 系列、config_fatal_probe.gd.uid);第 7 项 L2 池对比两候选缺漏(卓尔池增益技去留/敌人系统文档补 L2 行)待用户二轮定夺,条目在册

active.md 快照节 + 台账 72-79
2026-09-19 03:15:26 +08:00

643 lines
31 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 公式逐行精确重算。
表值硬编码溯源(表值变更须同步——单一事实源仍在 .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):
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(","))
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()