Files
gplan-2/game/presentation/playtest_logger.gd
T
DKChan 6c2dbc37c1 docs: ADR 体系废除 + architecture.md 确立为架构唯一事实源(2026-10-15 用户令:决策记录归 git commit log,不另存决策文档)
- 删除 design/dev/architecture/adr-0001~0007 共 7 份文件(内容留 git 历史可查)
- architecture.md:删「ADR 审计」「ADR 立项清单」两节;清除全文 ADR-000X 注记、QQ 表/S 表 ADR 引用;未决表关闭已落定项(QQ-01/02/03),立项清单在途事项转登记 QQ-11~14;补 A1 生成姿态事实、autoload 四件清单、board_generator 性能姿态;文档状态立「事实源地位」条
- CLAUDE.md:恢复 RNG 单点硬性红线(去 ADR-0007 引用);新增「架构事实唯一源」红线;规范导航架构行标注唯一事实源
- doc-conventions:§5 增「架构事实唯一源」行;§11 活文档清单补架构文档
- registry:architecture.yaml 删 18 处 adr 字段、autoload_set 补 SaveManager/MetaProgression、board_rending 更新为 Node2D 已落地事实;entities.yaml 12 处引用清除
- game/ 代码注释约 60 处清 ADR-000X 引用(事实与日期保留);豁免标记 ADR-0007-exempt → rng-seed-exempt(main.gd/static_scan.sh/architecture.md/06/iter27 五处同步)
- GDD/qa 实现报告/research 卡片/roadmap/tests README 引用全清;死链校验 1206 链接 0 死链、gdd_lint 0 不通过
- 代码中 adr/adr5 等为 adventure_runner 既有局部变量名(非 ADR 引用),未动
2026-10-15 23:52:30 +08:00

185 lines
7.8 KiB
GDScript
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.
# VERTICAL SLICE - NOT FOR PRODUCTION
# 实现 GDD:design/ux/battle.md(§Events Fired 遥测义务——ux 行族)。
# Date: 2026-09-12(战斗屏 ux 遥测)
# 试玩日志器(呈现层组件——订阅核心信号持久化 JSONL,供数值分析;卡死诊断转储)。
# 职责边界:核心层只发 settlement_snapshot 信号(战斗结算)/既有信号(battle_finished/node_entered/
# level_finished/step_row);本组件负责订阅、缓冲与写文件——核心零日志感知(零 UI 依赖不变)。
# 文件 = user://logs/playtest_<seed>_<时间戳>.jsonl(每行一次结算/终局/节点的全量状态 JSON)。
# ux 行族(呈现层观测,不改 core):ux_round_input_ratio(原始输入动作数∶AP 消耗)、
# ux_round_exit{path_tag}(快照 round_facts.path——呈现层只订 战斗结算 的接线纪律下唯一合法来源)、
# ux_refresh_attempt / ux_reject / ux_hint_shown(组合根显式上报;无 ux_window_choice——道具窗口已下线)。
# 诊断转储 = 全量核心状态 + 最近结算/操作环形缓冲 → user://logs/diag_<时间戳>.json(卡死时点按钮)。
class_name PlaytestLogger
extends RefCounted
const RING_SIZE := 200
var _file: FileAccess = null
var _path := ""
var _seed := 0
var _recent_steps: Array[Dictionary] = [] # 环形缓冲:最近结算行
var _recent_actions: Array[Dictionary] = [] # 环形缓冲:最近玩家动作
var _snap_count := 0
var ap_per_round := 8 # 组合根注入(AP 消耗口径分母——行动经济 无推送信号)
var _ux_inputs := 0 # 本结算轮原始输入动作数(log_action 计数,快照后归零)
# 开启会话(每次 start_level/start_battle 调用——按种子分文件)。返回日志路径。
func start_session(seed_val: int) -> String:
_seed = seed_val
_recent_steps.clear()
_recent_actions.clear()
_snap_count = 0
DirAccess.make_dir_recursive_absolute("user://logs")
var stamp: String = Time.get_datetime_string_from_system().replace(":", "").replace("-", "").replace(" ", "_")
_path = "user://logs/playtest_%d_%s.jsonl" % [seed_val, stamp]
if _file != null:
_file.close()
_file = FileAccess.open(_path, FileAccess.WRITE)
_append({"kind": "session", "seed": seed_val, "ts": Time.get_datetime_string_from_system()})
return _path
func close_session() -> void:
if _file != null:
_file.close()
_file = null
func log_path() -> String:
return _path
func attach_battle(settlement: BattleOrchestrator) -> void:
settlement.step_row.connect(_on_step_row)
settlement.settlement_snapshot.connect(_on_settlement_snapshot)
settlement.battle_finished.connect(_on_battle_finished_log)
func _on_battle_finished_log(p: Dictionary) -> void:
_append({
"kind": "battle_end", "result": String(p.get("result", "")),
"hero_hp": int(p.get("hero_hp", 0)),
"gp_dropped": int(p.get("gp_dropped", 0))}) # 战斗掉落列(GP+商店房实施批 2026-09-21——验收取数面)
# ---- GP 钱包行(GP+商店房实施批——到账/购买/清零事件;runner wallet_changed 投影经组合根转发)----
func log_wallet(event: String, gp: int, extra: Dictionary = {}) -> void:
var line := {"kind": "wallet", "event": event, "gp": gp}
for k in extra.keys():
line[k] = extra[k]
_append(line)
# ---- ux 遥测(battle.md §Events Fired——呈现层观测行族,不改 core) ----
func log_ux(ux_kind: String, payload: Dictionary) -> void:
var line := {"kind": "ux", "ux": ux_kind}
for k in payload.keys():
line[k] = payload[k]
_append(line)
func log_action(kind: String, cell: Vector2i, accepted: bool, extra: Dictionary = {}) -> void:
_ux_inputs += 1 # 原始输入动作数(含被拒——P4 ≤2 护栏观测口径 = 原始输入)
# 逐操作落盘(2026-09-12 用户要求:操作记录完整持久化——此前仅入环形缓冲、卡死诊断才可见)
var line := {"kind": "action", "op": kind, "cell": [cell.x, cell.y], "accepted": accepted}
for k in extra.keys():
line[k] = extra[k]
_append(line)
_ring(_recent_actions, {"kind": kind, "cell": [cell.x, cell.y], "accepted": accepted}.merged(extra))
func _on_settlement_snapshot(snap: Dictionary) -> void:
_append(snap)
# ux_round_exit{path_tag}:对齐上游封闭枚举 {stopped, ap_exhausted, cleared}
var facts: Dictionary[Variant, Variant] = snap.get("round_facts", {})
log_ux("ux_round_exit", {"path_tag": String(facts.get("path", ""))})
# ux_round_input_ratio:原始输入动作数∶AP 消耗(本轮 AP 配额 − 快照时剩余 = 消耗)
var economy: Dictionary[Variant, Variant] = snap.get("economy", {})
var ap_left: int = int(economy.get("ap", 0))
var consumed: int = clampi(ap_per_round - ap_left, 0, ap_per_round)
log_ux("ux_round_input_ratio", {"inputs": _ux_inputs, "ap_consumed": consumed,
"ratio": float(_ux_inputs) / float(maxi(consumed, 1))})
_ux_inputs = 0
func log_node(node: Dictionary, info: Dictionary, node_index: int) -> void:
var t := String(node.get("type", ""))
var line := {"kind": "node", "type": t, "index": node_index}
if t == "battle" or t == "boss":
line["depth"] = int(node.get("node_depth", 0))
elif t == "rest":
line["healed"] = int(info.get("healed", 0))
elif t == "shop":
line["gp"] = int(info.get("gp", 0)) # 进店时点余额(GP+商店房实施批)
# reward 节点:内容待拍板(格面资源批 2026-09-15 道具删除)——零载荷
_append(line)
# 批量模拟会话的局次标记(一文件多局——run_start 行携带该局种子/档位,供逐局切分)
func log_run(meta: Dictionary) -> void:
var line := {"kind": "run"}
for k in meta.keys():
line[k] = meta[k]
_append(line)
func log_level_end(payload: Dictionary) -> void:
_append({"kind": "level_end", "result": String(payload.get("result", "")),
"node_index": int(payload.get("node_index", -1)), "hp": int(payload.get("hp", 0))})
func _on_step_row(row: Dictionary) -> void:
_ring(_recent_steps, {"t": row.type, "src": row.source_class, "v": int(row.value), "r": int(row.round_index)})
# 结算明细行逐行落盘(2026-09-12 用户要求——五段结算完整回放数据面);
# 公共七字段之外的全部行字段(payload:roll/ac/tier/crit/normal/pierce/magic/baseline/rider/target/
# effect_id/pool_value 等)随行平铺落盘(2026-09-17 数值分析工具批——无 payload 的旧日志反事实只能
# 比例近似,新日志可精确重算)。
var line := {"kind": "step", "type": row.type, "source": row.source_class,
"cell": row.origin_cell, "round": int(row.round_index),
"text": String(row.display_name), "value": int(row.value),
"highlight": String(row.get("highlight", ""))}
for k in row.keys():
if not line.has(k):
line[k] = row[k]
_append(line)
# ---- 诊断转储(卡死时点击——全量状态 + 环形缓冲)----
func dump_diagnosis(context: Dictionary) -> String:
DirAccess.make_dir_recursive_absolute("user://logs")
var stamp: String = Time.get_datetime_string_from_system().replace(":", "").replace("-", "").replace(" ", "_")
var path := "user://logs/diag_%s.json" % stamp
var dump := {
"kind": "diagnosis", "ts": Time.get_datetime_string_from_system(), "seed": _seed,
"session_log": _path, "snap_count": _snap_count,
"recent_actions": _recent_actions.duplicate(),
"recent_steps": _recent_steps.duplicate(),
"core": context,
}
var f := FileAccess.open(path, FileAccess.WRITE)
if f != null:
f.store_string(JSON.stringify(dump, " "))
f.close()
print("[playtest_logger] 诊断转储已写入: %s(项目日志目录见输出)" % path)
print(" [diag] %s" % JSON.stringify(context))
return path
func _ring(buf: Array, item: Dictionary) -> void:
buf.append(item)
while buf.size() > RING_SIZE:
buf.pop_front()
func _append(dict: Dictionary) -> void:
if dict.get("kind", "") == "settlement":
_snap_count += 1
if _file == null:
return
_file.store_line(JSON.stringify(dict))
_file.flush()