Files
gplan-2/game/presentation/playtest_logger.gd
T

178 lines
6.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
## 试玩日志器:订阅核心信号逐行落 JSONL(user://logs/playtest_<seed>_<时间戳>.jsonl)供数值分析,卡死诊断转储 diag_*.json。
## ux_reject/ux_refresh_attempt/ux_hint_shown 无信号源——依赖组合根显式上报,漏调即缺行。
## ap_per_round 须由组合根注入——ux_round_input_ratio 的 AP 消耗口径分母。
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 钱包行(到账/购买/清零事件;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 护栏观测口径 = 原始输入)
# 逐操作落盘(操作记录须完整持久化,禁只入环形缓冲)
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)) # 进店时点余额
# reward 节点:零载荷(节点内容未在呈现面定义)
_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)})
# 结算明细行逐行落盘(五段结算完整回放数据面);
# 公共七字段之外的全部行字段(payload:roll/ac/tier/crit/normal/pierce/magic/baseline/rider/target/
# effect_id/pool_value 等)随行平铺落盘(无 payload 时反事实只能
# 比例近似,带 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()