# 冒险全流程零加成 600 局三维分析(2026-09-21 采集批 → 分析会话) # 输入:fulllog_normal_300.jsonl / fulllog_perfect_300.jsonl(playtest_logger 全量 JSONL) # runsum_600.jsonl(逐局摘要) # 输出:stdout 报告 + analysis_*.csv # 口径: # 局切分 = run 行;战斗切分 = node(battle/boss) 开、battle_end FIFO 收(延迟发出——同 log_analysis.py 口径) # 棋盘完成度% = Σ_各板(该板累计安全格翻开数 ÷ 该板安全格总数) × 100(跨板简单相加,用户 2026-09-21 口径) # 安全格总数校准 = 该 (level,depth) 清图板样本的累计 safe 数中位数(清图时恰等于总数);无样本回退 # w*h − round(0.25*(w*h−9))(board_gen 表口径,生成期删雷修正会造成小幅高估) # 承伤 = step 行 source=enemy_action × type=damage 合计;占生效 max_hp 比 = ÷ 该场首快照 hero.max_hp # 档位映射(tier):L1 normal=d0,d1 elite=d2,d3 boss=d4;L2/L3 normal=d0,d1,d6,d7 elite=d4,d5 boss=d9 import json, csv, statistics as st, collections, sys, io, os sys.stdout.reconfigure(encoding='utf-8', errors='replace') BASE = r'D:/code/g-plan2-adventure-mc/qa-data/' RUNSUM = os.environ.get('RUNSUM', 'runsum_600.jsonl') OUT_SUFFIX = os.environ.get('OUT_SUFFIX', '') TIER = { (1, 0): 'normal', (1, 1): 'normal', (1, 2): 'elite', (1, 3): 'elite', (1, 4): 'boss', (2, 0): 'normal', (2, 1): 'normal', (2, 4): 'elite', (2, 5): 'elite', (2, 6): 'normal', (2, 7): 'normal', (2, 9): 'boss', (3, 0): 'normal', (3, 1): 'normal', (3, 4): 'elite', (3, 5): 'elite', (3, 6): 'normal', (3, 7): 'normal', (3, 9): 'boss', } FALLBACK_SAFE = {100: 77, 144: 110, 169: 129, 196: 149} # w*h → 表口径 safe 数 def parse_log(path): """返回 battles 列表 + runs 列表(逐局)""" battles = [] # 完成的战斗 open_q = [] # FIFO 未收 battle_end 的战斗 cur = None # 当前 node 上下文(battle/boss 开启) run = None runs = [] last_level = 0 def new_battle(node): return { 'seed': run['seed'], 'policy': run['policy'], 'level': None, 'depth': node.get('depth'), 'node_type': node['type'], 'snaps': [], 'dmg_taken': 0, 'dmg_var': 0, 'dmg_mine': 0, 'dmg_flagmiss': 0, 'dmg_by_round': collections.Counter(), 'heal': 0, 'dmg_out': collections.Counter(), 'refreshes': 0, 'result': '', 'hero_hp_end': -1, 'gp': 0, } with open(path, encoding='utf-8') as fh: for line in fh: d = json.loads(line) k = d.get('kind') if k == 'run': run = {'seed': d['seed'], 'policy': d['policy'], 'rests': []} runs.append(run) elif k == 'node': if d['type'] in ('battle', 'boss'): cur = new_battle(d) open_q.append(cur) else: cur = None if d['type'] == 'rest' and run is not None: run.setdefault('rests', []).append({'healed': int(d.get('healed', 0)), 'level': last_level}) elif k == 'settlement' and open_q: b = open_q[-1] b['snaps'].append(d) b['level'] = d.get('level') last_level = d.get('level') elif k == 'step': tgt = open_q[-1] if open_q else None if tgt is None: continue t, s, v = d.get('type'), d.get('source', ''), int(d.get('value', 0)) if t == 'damage': if s == 'enemy_action': tgt['dmg_taken'] += v tgt['dmg_by_round'][int(d.get('round', 0))] += v elif s in ('safe_cell', 'flag_hit', 'hero_attack', 'clear_reward'): tgt['dmg_out'][s] += v elif s == 'mine_hit': tgt['dmg_mine'] += v elif s == 'flag_miss': tgt['dmg_flagmiss'] += v else: tgt['dmg_var'] += v # 地形变异(荆棘/岩浆等 display_name 作 source) elif t == 'heal': tgt['heal'] += v elif k == 'action': if open_q and d.get('op') == 'refresh': open_q[-1]['refreshes'] += 1 elif k == 'battle_end': if open_q: b = open_q.pop(0) # FIFO 对账(battle_end 延迟发出) b['result'] = d.get('result', '') b['hero_hp_end'] = int(d.get('hero_hp', -1)) b['gp'] = int(d.get('gp_dropped', 0)) battles.append(b) elif k == 'level_end': pass return battles, runs def board_progress(snaps): """逐板累计口径:seq → {revealed(末值), mines(累计触雷), w, h, cleared_at(触雷修正后安全翻开)} revealed = board.revealed(累计翻开,含触雷格);round_facts.mines = 该轮触雷数(R₀ 永不触雷,无归属歧义); round_facts.safe 不含初始展开 R₀(首板 install 事件不入轮 facts——battle_orchestrator L121),不可作完成度分子""" prog = {} for s in snaps: bd = s.get('board', {}) seq = int(bd.get('seq', 0)) rf = s.get('round_facts', {}) p = prog.setdefault(seq, {'revealed': 0, 'mines': 0, 'w': int(bd.get('w', 0)), 'h': int(bd.get('h', 0)), 'cleared_safe': -1}) p['revealed'] = max(p['revealed'], int(bd.get('revealed', 0))) p['mines'] += int(rf.get('mines', 0)) if rf.get('path') == 'cleared': p['cleared_safe'] = p['revealed'] - p['mines'] return prog def battle_stats(b, safe_total_map, safe_by_wh, r0_map=None): """单场派生指标""" snaps = b['snaps'] if not snaps: return None first = snaps[0] lv, dp = b['level'], b['depth'] tier = TIER.get((lv, dp), '?') rounds = max(int(s.get('round', 0)) for s in snaps) max_hp = int(first['hero']['max_hp']) atk = int(first['hero']['atk']) foes = first.get('enemies', []) enemy_hp = sum(int(e['max_hp']) for e in foes) enemy_ids = '+'.join(sorted({e['id'] for e in foes})) or '-' prog = board_progress(snaps) cleared = sum(1 for p in prog.values() if p['cleared_safe'] >= 0) mines = sum(p['mines'] for p in prog.values()) flags = sum(int(s.get('round_facts', {}).get('flag_events', 0)) for s in snaps) paths = collections.Counter(s.get('round_facts', {}).get('path', '') for s in snaps) # 完成度 = Σ_各板 min(安全翻开, 安全总数)/安全总数 × 100(跨板简单相加,用户口径) comp = 0.0 dug_safe = 0.0 r0_base = (r0_map or {}).get((lv, dp), 0.0) for seq, p in prog.items(): tot = safe_total_map.get((lv, dp, p['w'], p['h'])) or safe_by_wh.get((p['w'], p['h'])) or FALLBACK_SAFE.get(p['w'] * p['h'], max(1, p['w'] * p['h'])) rs = max(p['revealed'] - p['mines'], 0) comp += 100.0 * min(rs, tot) / tot dug_safe += max(rs - r0_base, 0) # 主动挖掘翻开的安全格(扣 R₀ 免费展开均值) dmg_pct = 100.0 * b['dmg_taken'] / max(1, max_hp) hero_out = sum(b['dmg_out'].values()) entry_hp = int(first['hero']['hp']) + b['dmg_by_round'].get(int(first.get('round', 1)), 0) # 快照反推法(首快照=首回合末) return { 'policy': b['policy'], 'seed': b['seed'], 'level': lv, 'depth': dp, 'tier': tier, 'result': b['result'], 'rounds': rounds, 'comp': comp, 'cleared': cleared, 'dug_safe': dug_safe, 'boards': len(prog), 'dmg_taken': b['dmg_taken'], 'dmg_pct': dmg_pct, 'dmg_var': b['dmg_var'], 'dmg_mine': b['dmg_mine'], 'dmg_flagmiss': b['dmg_flagmiss'], 'heal': b['heal'], 'hero_out': hero_out, 'out_per_round': hero_out / max(1, rounds), 'dmg_out': dict(b['dmg_out']), 'mines': mines, 'flags': flags, 'refreshes': b['refreshes'], 'max_hp': max_hp, 'atk': atk, 'enemy_hp': enemy_hp, 'enemy_ids': enemy_ids, 'paths': dict(paths), 'entry_hp': entry_hp, 'hero_hp_end': b['hero_hp_end'], } def main(): files = sys.argv[1:] or ['fulllog_normal_300.jsonl', 'fulllog_perfect_300.jsonl'] # 第一遍:清图板校准 safe_total(清图时点 revealed−mines 恰=安全格总数;生成期删雷修正 ⇒ 表口径不可用) all_battles = {} for fn in files: pol = 'perfect' if 'perfect' in fn else 'normal' key = fn.replace('.jsonl', '') bs, runs = parse_log(BASE + fn) all_battles[key] = (bs, runs, pol) calib = collections.defaultdict(list) # (level,depth,w,h) → 清图样本 calib_wh = collections.defaultdict(list) # (w,h) → 清图样本(跨深度回退池) for key, (bs, _, _) in all_battles.items(): for b in bs: for seq, p in board_progress(b['snaps']).items(): if p['cleared_safe'] >= 0: calib[(b['level'], b['depth'], p['w'], p['h'])].append(p['cleared_safe']) calib_wh[(p['w'], p['h'])].append(p['cleared_safe']) safe_total_map = {k: int(st.median(v)) for k, v in calib.items()} safe_by_wh = {k: int(st.median(v)) for k, v in calib_wh.items()} # 生成器真值分母(smoke/calibrate_boards.gd 200 板/行——权威口径;日志清图校准仅作交叉核对) gen_safe = {} gen_r0 = {} with open(BASE + 'board_calibration.csv', encoding='utf-8') as f: for rec in csv.DictReader(f): k = (int(rec['level']), int(rec['depth'])) gen_safe[(k[0], k[1], int(rec['w']), int(rec['h']))] = int(rec['safe_med']) gen_r0[k] = (float(rec['r0_mean']), int(rec['safe_med'])) safe_total_map.update(gen_safe) print('== safe_total 分母(生成器真值 calibrate_boards 200板/行;R₀ 免费展开基线)==') for k in sorted(gen_r0): r0, sm = gen_r0[k] print(f" L{k[0]} d{k[1]}: safe={sm} R₀均={r0:.1f} ({100*r0/sm:.0f}% 免费展开)") for k in sorted(calib): print(f" [日志清图校准对照] L{k[0]} d{k[1]} {k[2]}x{k[3]}: safe={int(st.median(calib[k]))} (样本{len(calib[k])}, min={min(calib[k])}, max={max(calib[k])})") rows = [] r0_map = {k: v[0] for k, v in gen_r0.items()} for key, (bs, _, _) in all_battles.items(): for b in bs: r = battle_stats(b, safe_total_map, safe_by_wh, r0_map) if r: rows.append(r) # ---- CSV 落盘 ---- with open(BASE + 'analysis_battles' + OUT_SUFFIX + '.csv', 'w', newline='', encoding='utf-8') as f: w = csv.DictWriter(f, fieldnames=list(rows[0].keys())) w.writeheader() for r in rows: w.writerow(r) # ---- R1 局级 ---- runsums = [json.loads(l) for l in open(BASE + RUNSUM, encoding='utf-8')] print('\n== R1 局级通关率(N=300/策略,零加成)==') for pol in ('normal', 'perfect'): rs = [r for r in runsums if r['policy'] == pol] n = len(rs) if n == 0: continue comp = sum(1 for r in rs if r['cleared']) stop = collections.Counter(r['stopped_at'] for r in rs if not r['cleared']) pass1 = n - stop.get(1, 0) pass2 = n - stop.get(1, 0) - stop.get(2, 0) print(f" {pol}: 通关3关 {comp}/{n} = {100*comp/n:.1f}% | 止步L1 {stop.get(1,0)} ({100*stop.get(1,0)/n:.1f}%) " f"止步L2 {stop.get(2,0)} ({100*stop.get(2,0)/n:.1f}%) 止步L3 {stop.get(3,0)} ({100*stop.get(3,0)/n:.1f}%)") print(f" 逐关通过率: L1 {100*pass1/n:.1f}% · L2(过L1后累计) {100*pass2/n:.1f}% · 条件通过 L1→L2 {100*pass2/max(1,pass1):.1f}% · L2→L3 {100*comp/max(1,pass2):.1f}%") # ---- R2 战斗级 ---- def q(a, p): a = sorted(a) return a[min(len(a) - 1, int(p * len(a)))] if a else 0 print('\n== R2 战斗级(按 策略×关卡×档位)==') hdr = f" {'策略':<8}{'L':>2} {'档位':<7}{'场数':>5}{'胜率':>7}{'轮数均/中':>10}{'完成度均/中/p90':>16}{'挖安全格/场':>10}{'踩雷/场':>8}{'清图≥1':>7}{'承伤%均':>8}{'输出/轮':>8}" print(hdr) for pol in ('normal', 'perfect'): for lv in (1, 2, 3): for tier in ('normal', 'elite', 'boss'): g = [r for r in rows if r['policy'] == pol and r['level'] == lv and r['tier'] == tier] if not g: continue wins = sum(1 for r in g if r['result'] == 'victory') comps = [r['comp'] for r in g] rds = [r['rounds'] for r in g] dps = [r['dmg_pct'] for r in g] clr = sum(1 for r in g if r['cleared'] >= 1) opr = [r['out_per_round'] for r in g] print(f" {pol:<8}{lv:>2} {tier:<7}{len(g):>5}{100*wins/len(g):>6.0f}%" f"{st.mean(rds):>6.1f}/{q(rds,0.5):<4.0f}{st.mean(comps):>8.0f}/{q(comps,0.5):<4.0f}/{q(comps,0.9):<5.0f}" f"{st.mean([r['dug_safe'] for r in g]):>10.1f}{st.mean([r['mines'] for r in g]):>8.2f}" f"{100*clr/len(g):>6.0f}%{st.mean(dps):>7.1f}%{st.mean(opr):>8.1f}") print('\n== R2b 完成度分布桶(占该组场数%)==') buckets = [(0, 70), (70, 90), (90, 100), (100, 130), (130, 10**9)] labels = ['<70', '70-90', '90-100', '100-130', '>130'] print(f" {'组':<24}" + ''.join(f"{l:>9}" for l in labels)) for pol in ('normal', 'perfect'): for lv in (1, 2, 3): for tier in ('normal', 'elite', 'boss'): g = [r for r in rows if r['policy'] == pol and r['level'] == lv and r['tier'] == tier] if not g: continue cnt = [sum(1 for r in g if lo <= r['comp'] < hi) for lo, hi in buckets] print(f" {pol+' L'+str(lv)+' '+tier:<24}" + ''.join(f"{100*c/len(g):>8.0f}%" for c in cnt)) # ---- R3 成长数值 ---- print('\n== R3 战斗成长数值(按关卡)==') def mean_or(g, key, fmt='{:.1f}'): v = [r[key] for r in g] return fmt.format(st.mean(v)) if v else '—' for pol in ('normal', 'perfect'): for lv in (1, 2, 3): g = [r for r in rows if r['policy'] == pol and r['level'] == lv] if not g: continue mhp = {r['max_hp'] for r in g} atks = {r['atk'] for r in g} parts = [] for tier in ('normal', 'elite', 'boss'): gt = [r for r in g if r['tier'] == tier] parts.append(f"{tier}敌HP均={mean_or(gt, 'enemy_hp')}") print(f" {pol} L{lv}: 英雄max_hp={sorted(mhp)} atk={sorted(atks)} | " + ' '.join(parts) + f" | 英雄输出/轮均={mean_or(g, 'out_per_round', '{:.2f}')} 敌输出/场均={mean_or(g, 'dmg_taken')}" f" 变异伤/场均={mean_or(g, 'dmg_var')} 雷伤/场均={mean_or(g, 'dmg_mine')}") print('\n== R3b 伤害输出构成(全战斗合计占比)==') for pol in ('normal', 'perfect'): tot = collections.Counter() for r in rows: if r['policy'] == pol: tot.update(r['dmg_out']) s = sum(tot.values()) print(f" {pol}: " + ' '.join(f"{k}={100*v/s:.1f}%" for k, v in tot.most_common())) # 逐深度敌HP与轮数(成长曲线细目) print('\n== R3c 逐深度明细(normal 策略)==') print(f" {'L.d':<6}{'敌HP均':>7}{'轮均':>6}{'完成度均':>9}{'承伤%均':>8}{'胜率':>7}") seen = set() for r in rows: seen.add((r['level'], r['depth'])) for lv, dp in sorted(seen): g = [r for r in rows if r['policy'] == 'normal' and r['level'] == lv and r['depth'] == dp] if not g: continue wins = sum(1 for r in g if r['result'] == 'victory') print(f" {lv}.{dp:<4}{st.mean([r['enemy_hp'] for r in g]):>7.1f}{st.mean([r['rounds'] for r in g]):>6.1f}" f"{st.mean([r['comp'] for r in g]):>9.0f}{st.mean([r['dmg_pct'] for r in g]):>7.1f}%{100*wins/len(g):>6.0f}%") # 败北战斗画像 print('\n== R4 败北战斗画像(result=defeat 的战斗)==') for pol in ('normal', 'perfect'): g = [r for r in rows if r['policy'] == pol and r['result'] == 'defeat'] if not g: continue by = collections.Counter((r['level'], r['tier']) for r in g) print(f" {pol}: 败北场数={len(g)} 分布=" + ' '.join(f"L{k[0]}{k[1]}×{v}" for k, v in sorted(by.items()))) ent = [r['entry_hp'] / max(1, r['max_hp']) for r in g] print(f" 进场HP占比 均={100*st.mean(ent):.0f}% 中位={100*q(ent,0.5):.0f}% | 败北场承伤%均={st.mean([r['dmg_pct'] for r in g]):.0f}%") # ---- R5 逐关 HP 经济(成长系统的账本面)---- print('\n== R5 逐关 HP 经济(每局每关:承伤合计 vs 治疗+休息;开局回满,关间大奖励+10上限回满)==') bstat_by_run = collections.defaultdict(list) for r in rows: bstat_by_run[(r['policy'], r['seed'])].append(r) print(f" {'策略':<9}{'L':>2} {'局数':>4} {'战斗/局':>7} {'敌伤/局':>8} {'变异+雷伤/局':>10} {'战斗治疗/局':>9} {'休息恢复/局':>9} {'净消耗/局':>9} {'期初上限':>7}") for key, (bs, runs, pol) in all_battles.items(): for lv in (1, 2, 3): n = 0 acc = collections.Counter() for run in runs: rl = [r for r in bstat_by_run.get((pol, run['seed']), []) if r['level'] == lv] if not rl: continue n += 1 acc['battles'] += len(rl) acc['edmg'] += sum(r['dmg_taken'] for r in rl) acc['var'] += sum(r['dmg_var'] + r['dmg_mine'] + r['dmg_flagmiss'] for r in rl) acc['heal'] += sum(r['heal'] for r in rl) acc['rest'] += sum(x['healed'] for x in run['rests'] if x['level'] == lv) acc['maxhp'] = rl[0]['max_hp'] if not n: continue print(f" {pol:<9}{lv:>2} {n:>4} {acc['battles']/n:>7.1f} {acc['edmg']/n:>8.1f} {acc['var']/n:>10.1f}" f" {acc['heal']/n:>9.1f} {acc['rest']/n:>9.1f} {(acc['edmg']+acc['var']-acc['heal']-acc['rest'])/n:>9.1f} {acc['maxhp']:>7}") if __name__ == '__main__': main()