改动面 112 文件 572+/572-:game/tests/** 与 game/probe/** 共 106 件 545 行 `# oracle:`→`# 判据出处:`;tools/capability_audit.py 正则字面量换中文+ORACLE_*→CRITERION_*/check_oracle_markers→check_criterion_markers/oracle_marked(flagged)→criterion_* 共 11 行;test-conventions.md:119 格式句+:155 措辞;待办清单与需求卡片-QA覆盖余量收口批各 1 处字面量;coverage_enum.py:21 与 camp_scene_layout_test.gd:101 旁支措辞;ui_click_probe.gd 对拍局部变量 oracle→expected 7 处;auto_player.gd 三处 safe_set 真值措辞。不动:历史审计报告两份、test-oracle-anchor 分支名、git 史、生成日志;指针格式/扫描面/判红条件/验收义务全不变。 读数(区内实跑):改前 capability_audit --strict「417 注记函数·红项 0」→改后「417 判据出处注记函数·红项 0」exit=0;新标记 545 行/106 文件,现行义务面 oracle 残留 0(在册沿革除外);diff 自检 game/ 非注释新增行仅 6 行且全在 ui_click_probe.gd 的 expected 变量段;112 改动文件 CRLF=0;py_compile tools/capability_audit.py OK;--docs exit=0【验收通过】无新增红;--gates exit=0(静态扫描绿、闸壳 bash -n ×13 失败 0)。
503 lines
13 KiB
GDScript
503 lines
13 KiB
GDScript
# tests/unit/battle_settlement/settlement_wiring_snapshot_test.gd — 结算接线同种子逐项 golden 对拍宿主(架构审查 5.1 封装修复批 R1 主证)
|
||
# 断言面 = 同种子定死驱动序下 step_row 全量行流 + settlement_snapshot 全量载荷 + battle_finished
|
||
# 载荷 + 收束 presentation_snapshot 关键键(round/battle_result/hero.hp/enemies.size()),
|
||
# 与 GOLDEN 常量逐位相等〔封装5.1 V1〕——段类束注入改造的零行为变更主证:
|
||
# 改前基线在未改树上固化(GOLDEN 为空时走转储通道取数),改后全程保持绿。
|
||
# 驱动序(卡面逐字定死):begin_battle(voucher_rap=20) → 轮1 choose_dig 移动/挖/封邻格/停 →
|
||
# player_request_refresh(换图刷新链入流)→ 轮2 一挖一停 → 轮3 径直停手。
|
||
# 协议照单元侧惯例:extends SceneTree+Kit.check 主通道;--script 模式无 autoload,
|
||
# ConfigDB 显式实例化构造注入+自建 RngService(掷点隔离形态)。
|
||
# 取数通道:GOLDEN 为空时打印捕获序列 JSON(两格缩进)后按专用码 3 退出——非判定码族(0 绿/1 红/2 空跑)。
|
||
# 运行:godot --headless --path game --script res://tests/unit/battle_settlement/settlement_wiring_snapshot_test.gd
|
||
extends SceneTree
|
||
|
||
const Kit := preload("res://tests/unit/test_kit.gd")
|
||
const ConfigDBScript := preload("res://core/config_db.gd")
|
||
const AutoPlayer := preload("res://sim/auto_player.gd")
|
||
|
||
const TEST_SEED := 4242 # golden 固化种子(卡面定死——改前改后同种子对拍前提)
|
||
const DUMP_EXIT := 3 # 空跑转储专用退出码(GOLDEN 未固化态取数用)
|
||
|
||
# 改前基线(JSON 两格缩进单文件固化——与捕获序列 JSON.stringify 逐字相等即绿;空串=未固化→转储模式)
|
||
const GOLDEN := r"""
|
||
{
|
||
"final": {
|
||
"battle_result": "",
|
||
"enemies_n": 2,
|
||
"hero_hp": 21,
|
||
"round": 4
|
||
},
|
||
"finished": [],
|
||
"rows": [
|
||
{
|
||
"display_name": "迟滞(下轮行动点 -1)",
|
||
"effect_id": "ap_delta",
|
||
"origin_cell": [
|
||
1,
|
||
0
|
||
],
|
||
"pool_value": -1,
|
||
"round_index": 1,
|
||
"source_class": "mine_hit",
|
||
"type": "effect",
|
||
"value": -1
|
||
},
|
||
{
|
||
"display_name": "行动点(下轮行动点 +1)",
|
||
"effect_id": "ap_delta",
|
||
"highlight": "seal_hit",
|
||
"origin_cell": [
|
||
1,
|
||
1
|
||
],
|
||
"pool_value": 0,
|
||
"round_index": 1,
|
||
"source_class": "seal_hit",
|
||
"type": "effect",
|
||
"value": 1
|
||
},
|
||
{
|
||
"ac": 1,
|
||
"display_name": "主角攻击",
|
||
"killed": false,
|
||
"normal": 2,
|
||
"origin_cell": [
|
||
-1,
|
||
-1
|
||
],
|
||
"roll": 12,
|
||
"round_index": 1,
|
||
"source_class": "hero_attack",
|
||
"target": "地精一伙",
|
||
"target_id": "goblin",
|
||
"tier": 100,
|
||
"type": "damage",
|
||
"value": 2
|
||
},
|
||
{
|
||
"ac": 10,
|
||
"attack_type": "normal",
|
||
"baseline": 1,
|
||
"crit": true,
|
||
"display_name": "地精一伙 普通攻击(暴击)",
|
||
"origin_cell": [
|
||
-1,
|
||
-1
|
||
],
|
||
"roll": 20,
|
||
"round_index": 1,
|
||
"skill_id": "attack",
|
||
"source_class": "enemy_action",
|
||
"target": "hero",
|
||
"tier": 100,
|
||
"type": "damage",
|
||
"value": 2
|
||
},
|
||
{
|
||
"ac": 10,
|
||
"attack_type": "normal",
|
||
"baseline": 1,
|
||
"display_name": "地精一伙 普通攻击(被压制)",
|
||
"origin_cell": [
|
||
-1,
|
||
-1
|
||
],
|
||
"roll": 2,
|
||
"round_index": 1,
|
||
"skill_id": "attack",
|
||
"source_class": "enemy_action",
|
||
"target": "hero",
|
||
"tier": 25,
|
||
"type": "damage",
|
||
"value": 1
|
||
},
|
||
{
|
||
"display_name": "状态衰减",
|
||
"origin_cell": [
|
||
-1,
|
||
-1
|
||
],
|
||
"round_index": 1,
|
||
"source_class": "",
|
||
"type": "decay",
|
||
"value": 0
|
||
},
|
||
{
|
||
"display_name": "下一轮次",
|
||
"origin_cell": [
|
||
-1,
|
||
-1
|
||
],
|
||
"round_index": 1,
|
||
"source_class": "",
|
||
"type": "verdict",
|
||
"value": 0
|
||
},
|
||
{
|
||
"display_name": "暴走",
|
||
"effect_id": "enrage",
|
||
"origin_cell": [
|
||
0,
|
||
0
|
||
],
|
||
"pool_value": 25,
|
||
"round_index": 2,
|
||
"source_class": "mine_hit",
|
||
"type": "effect",
|
||
"value": 25
|
||
},
|
||
{
|
||
"ac": 1,
|
||
"display_name": "主角攻击",
|
||
"killed": true,
|
||
"normal": 2,
|
||
"origin_cell": [
|
||
-1,
|
||
-1
|
||
],
|
||
"roll": 2,
|
||
"round_index": 2,
|
||
"source_class": "hero_attack",
|
||
"target": "地精一伙",
|
||
"target_id": "goblin",
|
||
"tier": 100,
|
||
"type": "damage",
|
||
"value": 2
|
||
},
|
||
{
|
||
"ac": 10,
|
||
"attack_type": "normal",
|
||
"baseline": 1,
|
||
"display_name": "地精一伙 普通攻击(被压制)",
|
||
"origin_cell": [
|
||
-1,
|
||
-1
|
||
],
|
||
"roll": 1,
|
||
"round_index": 2,
|
||
"skill_id": "attack",
|
||
"source_class": "enemy_action",
|
||
"target": "hero",
|
||
"tier": 25,
|
||
"type": "damage",
|
||
"value": 1
|
||
},
|
||
{
|
||
"display_name": "状态衰减",
|
||
"origin_cell": [
|
||
-1,
|
||
-1
|
||
],
|
||
"round_index": 2,
|
||
"source_class": "",
|
||
"type": "decay",
|
||
"value": 1
|
||
},
|
||
{
|
||
"display_name": "下一轮次",
|
||
"origin_cell": [
|
||
-1,
|
||
-1
|
||
],
|
||
"round_index": 2,
|
||
"source_class": "",
|
||
"type": "verdict",
|
||
"value": 0
|
||
},
|
||
{
|
||
"ac": 1,
|
||
"display_name": "主角攻击",
|
||
"killed": false,
|
||
"normal": 2,
|
||
"origin_cell": [
|
||
-1,
|
||
-1
|
||
],
|
||
"roll": 8,
|
||
"round_index": 3,
|
||
"source_class": "hero_attack",
|
||
"target": "地精一伙",
|
||
"target_id": "goblin",
|
||
"tier": 100,
|
||
"type": "damage",
|
||
"value": 2
|
||
},
|
||
{
|
||
"ac": 10,
|
||
"attack_type": "normal",
|
||
"baseline": 1,
|
||
"display_name": "地精一伙 普通攻击(被压制)",
|
||
"origin_cell": [
|
||
-1,
|
||
-1
|
||
],
|
||
"roll": 6,
|
||
"round_index": 3,
|
||
"skill_id": "attack",
|
||
"source_class": "enemy_action",
|
||
"target": "hero",
|
||
"tier": 50,
|
||
"type": "damage",
|
||
"value": 1
|
||
},
|
||
{
|
||
"display_name": "状态衰减",
|
||
"origin_cell": [
|
||
-1,
|
||
-1
|
||
],
|
||
"round_index": 3,
|
||
"source_class": "",
|
||
"type": "decay",
|
||
"value": 0
|
||
},
|
||
{
|
||
"display_name": "下一轮次",
|
||
"origin_cell": [
|
||
-1,
|
||
-1
|
||
],
|
||
"round_index": 3,
|
||
"source_class": "",
|
||
"type": "verdict",
|
||
"value": 0
|
||
}
|
||
],
|
||
"snaps": [
|
||
{
|
||
"board": {
|
||
"h": 10,
|
||
"revealed": 42,
|
||
"seals": 1,
|
||
"seq": 1,
|
||
"w": 10
|
||
},
|
||
"buffs": [],
|
||
"depth": 1,
|
||
"economy": {
|
||
"ap": 0,
|
||
"phase": "Waiting",
|
||
"rap": 26,
|
||
"refresh_used": 0
|
||
},
|
||
"enemies": [
|
||
{
|
||
"armor": "light",
|
||
"armor_class": 1,
|
||
"atk": 1,
|
||
"hp": 2,
|
||
"id": "goblin",
|
||
"max_hp": 4,
|
||
"pending_skill": ""
|
||
},
|
||
{
|
||
"armor": "light",
|
||
"armor_class": 1,
|
||
"atk": 1,
|
||
"hp": 4,
|
||
"id": "goblin",
|
||
"max_hp": 4,
|
||
"pending_skill": ""
|
||
}
|
||
],
|
||
"hero": {
|
||
"armor_class": 10,
|
||
"atk": 2,
|
||
"hp": 23,
|
||
"max_hp": 26
|
||
},
|
||
"kind": "settlement",
|
||
"level": 1,
|
||
"round": 1,
|
||
"round_facts": {
|
||
"mines": 1,
|
||
"path": "stopped",
|
||
"safe": 0,
|
||
"seal_events": 1
|
||
},
|
||
"spawn_count": 2,
|
||
"unit_count": 2,
|
||
"verdict": "continue"
|
||
},
|
||
{
|
||
"board": {
|
||
"h": 10,
|
||
"revealed": 43,
|
||
"seals": 0,
|
||
"seq": 2,
|
||
"w": 10
|
||
},
|
||
"buffs": [],
|
||
"depth": 1,
|
||
"economy": {
|
||
"ap": 0,
|
||
"phase": "Waiting",
|
||
"rap": 25,
|
||
"refresh_used": 1
|
||
},
|
||
"enemies": [
|
||
{
|
||
"armor": "light",
|
||
"armor_class": 1,
|
||
"atk": 1,
|
||
"hp": 4,
|
||
"id": "goblin",
|
||
"max_hp": 4,
|
||
"pending_skill": ""
|
||
}
|
||
],
|
||
"hero": {
|
||
"armor_class": 10,
|
||
"atk": 2,
|
||
"hp": 22,
|
||
"max_hp": 26
|
||
},
|
||
"kind": "settlement",
|
||
"level": 1,
|
||
"round": 2,
|
||
"round_facts": {
|
||
"mines": 1,
|
||
"path": "stopped",
|
||
"safe": 42,
|
||
"seal_events": 0
|
||
},
|
||
"spawn_count": 2,
|
||
"unit_count": 1,
|
||
"verdict": "continue"
|
||
},
|
||
{
|
||
"board": {
|
||
"h": 10,
|
||
"revealed": 43,
|
||
"seals": 0,
|
||
"seq": 2,
|
||
"w": 10
|
||
},
|
||
"buffs": [],
|
||
"depth": 1,
|
||
"economy": {
|
||
"ap": 0,
|
||
"phase": "Waiting",
|
||
"rap": 33,
|
||
"refresh_used": 0
|
||
},
|
||
"enemies": [
|
||
{
|
||
"armor": "light",
|
||
"armor_class": 1,
|
||
"atk": 1,
|
||
"hp": 2,
|
||
"id": "goblin",
|
||
"max_hp": 4,
|
||
"pending_skill": ""
|
||
}
|
||
],
|
||
"hero": {
|
||
"armor_class": 10,
|
||
"atk": 2,
|
||
"hp": 21,
|
||
"max_hp": 26
|
||
},
|
||
"kind": "settlement",
|
||
"level": 1,
|
||
"round": 3,
|
||
"round_facts": {
|
||
"mines": 0,
|
||
"path": "stopped",
|
||
"safe": 0,
|
||
"seal_events": 0
|
||
},
|
||
"spawn_count": 2,
|
||
"unit_count": 1,
|
||
"verdict": "continue"
|
||
}
|
||
]
|
||
}
|
||
"""
|
||
|
||
var _db
|
||
|
||
|
||
func _init() -> void:
|
||
# 判据出处: design/gdd/systems/战斗循环/战斗结算.md:362 (结算接线同种子逐项 golden 对拍组)
|
||
_db = ConfigDBScript.new()
|
||
var load_ok: bool = _db.load_tables("res://data/tables", true)
|
||
Kit.check("前置:表加载零 error", load_ok and _db.diagnostics_count("error") == 0)
|
||
var orch = load("res://core/battle_orchestrator.gd").new(_db, _make_rng())
|
||
var rows: Array = []
|
||
var snaps: Array = []
|
||
var finished: Array = []
|
||
orch.step_row.connect(func(row: Dictionary) -> void: rows.append(row))
|
||
orch.settlement_snapshot.connect(func(s: Dictionary) -> void: snaps.append(s))
|
||
orch.battle_finished.connect(func(p: Dictionary) -> void: finished.append(p))
|
||
var accepted: bool = orch.begin_battle({"level_index": 1, "node_depth": 1,
|
||
"hero_hp": int(_db.combat.hero.max_hp), "voucher_rap": 20})
|
||
Kit.check("前置:begin_battle 受理", accepted)
|
||
if accepted:
|
||
_drive(orch)
|
||
var final_snap: Dictionary = orch.presentation_snapshot()
|
||
var captured := {"rows": rows, "snaps": snaps, "finished": finished,
|
||
"final": {"round": int(final_snap.get("round", -1)),
|
||
"battle_result": String(final_snap.get("battle_result", "")),
|
||
"hero_hp": int((final_snap.get("hero", {}) as Dictionary).get("hp", -1)),
|
||
"enemies_n": (final_snap.get("enemies", []) as Array).size()}}
|
||
if GOLDEN == "":
|
||
print("[golden-dump] begin")
|
||
print(JSON.stringify(captured, " "))
|
||
print("[golden-dump] end")
|
||
quit(DUMP_EXIT)
|
||
return
|
||
# GOLDEN 三引号字面量首尾各带一个换行(多行字符串语法载体)——对拍两侧 strip 首尾空白后逐位比
|
||
var captured_json := JSON.stringify(captured, " ").strip_edges()
|
||
var golden_json := GOLDEN.strip_edges()
|
||
if captured_json != golden_json:
|
||
print("[captured-dump] begin")
|
||
print(JSON.stringify(captured, " "))
|
||
print("[captured-dump] end")
|
||
Kit.check("封装5.1 V1 同种子逐项对拍:行流/快照/终局载荷/presentation 逐位同 golden",
|
||
captured_json == golden_json)
|
||
quit(Kit.finish("settlement_wiring_snapshot_test"))
|
||
|
||
|
||
func _make_rng():
|
||
var rng = load("res://core/rng_service.gd").new()
|
||
rng.set_master_seed(TEST_SEED)
|
||
return rng
|
||
|
||
|
||
# 驱动序(卡面定死):轮1 挖+封+停 → 刷新 → 轮2 一挖一停 → 轮3 停手
|
||
func _drive(orch) -> void:
|
||
var ms = orch.minesweeper
|
||
# 轮 1:choose_dig 非空则 move→dig→封首个未揭示主角邻格→stop
|
||
var pick: Dictionary = AutoPlayer.choose_dig(ms)
|
||
if not pick.is_empty() and pick.get("dig_cell", null) != null:
|
||
ms.settle_batch([{"action": "move", "cell": pick["hero_cell"]}])
|
||
ms.settle_batch([{"action": "dig", "cell": pick["dig_cell"]}])
|
||
var c := _first_unrevealed_adjacent(orch)
|
||
if c.x >= 0:
|
||
ms.settle_batch([{"action": "seal_place", "cell": c}])
|
||
ms.settle_batch([{"action": "stop"}])
|
||
# 轮 1 结算后刷新(voucher_rap=20 保证 RAP 抵折后价——覆盖换图刷新链)
|
||
orch.player_request_refresh()
|
||
# 轮 2:一挖一停
|
||
var pick2: Dictionary = AutoPlayer.choose_dig(ms)
|
||
if not pick2.is_empty() and pick2.get("dig_cell", null) != null:
|
||
ms.settle_batch([{"action": "move", "cell": pick2["hero_cell"]}])
|
||
ms.settle_batch([{"action": "dig", "cell": pick2["dig_cell"]}])
|
||
ms.settle_batch([{"action": "stop"}])
|
||
# 轮 3:径直停手
|
||
ms.settle_batch([{"action": "stop"}])
|
||
|
||
|
||
# 首个未揭示且与主角 8 邻接的格(卡面定死封印目标选取;无候选=返回 (-1,-1) 跳过封印)
|
||
func _first_unrevealed_adjacent(orch) -> Vector2i:
|
||
var ms = orch.minesweeper
|
||
var hero: Vector2i = ms.hero_pos()
|
||
var board = ms.current_board()
|
||
for y in board.height:
|
||
for x in board.width:
|
||
var c := Vector2i(x, y)
|
||
if not ms.is_revealed(c) and board.neighbors8(hero).has(c):
|
||
return c
|
||
return Vector2i(-1, -1)
|