# 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)