Files
gplan-2/game/tests/unit/presentation/player_copy_sweep_test.gd
T

195 lines
10 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.
# tests/unit/presentation/player_copy_sweep_test.gd — 玩家面文案清扫直建断言宿主(迭代36 卡A)
# 断言面 = 玩家面字符串字面量换代钉值(u1–u13 钉值表——分类词/渠道旧话/「雷」族禁字清零):
# ① hero 面板两段头(专长/属性值提升——同屏分类撞车消除)
# ② 奖励节点三选一头部(冒险牌库/渠道分流三机制事实,零「同权重/同上限/提升池」旧话)
# ③ 专长卡兜底(feat_card_text 空行兜底「专长\n???」)
# ④ 图例封印行(标空/受挖掘惩罚,零「插错/挖雷」)
# ⑤ 状态槽来源名(mine_hit→危险格——tooltip 消费半)
# ⑥ 结算来源名(settlement 表值经 ConfigDB 真装载+battle_log 行消费半)
# ⑦ 已开危险格字形(glyph「危」+plate+RED_MINE——BoardView rebuild 真实刷新路径)
# ⑧ 源 token 扫描一:board_view.gd 零 "雷" 字面量(哨兵族统一钉住)
# ⑩ 源 token 扫描二:battle_orchestrator.gd 玩家面字符串字面量零「挖雷」(注释剥离=行首 # 判)
# 扫域口径:玩家面字符串字面量(契约 2 保留面——注释/内部常量名与注记/测试消息串不入域)。
# 协议照单元侧惯例:extends SceneTree+Kit.check 主通道;--script 模式无 autoload,
# ConfigDB 显式实例化构造注入(统一流 master 种子 42 沿源)。
# 运行:godot --headless --path game --script res://tests/unit/presentation/player_copy_sweep_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") # 自动玩家策略单一实现(夹具开域驱动)
var _db
func _init() -> void:
_db = ConfigDBScript.new()
var load_ok: bool = _db.load_tables("res://data/tables", true)
if not load_ok:
push_error("[player_copy_sweep_test] 表加载返回失败——诊断如下(红由具体断言落)")
for d in _db.diagnostics():
if d.severity == "error" or d.severity == "fatal":
push_error(" [%s] %s: %s" % [d.severity, d.category, d.message])
_sweep_hero_panel()
_sweep_narrator()
_sweep_legend()
_sweep_source_names()
_sweep_board_glyph()
_sweep_token_scans()
_sweep_refresh_toast_no_literals()
quit(Kit.finish("player_copy_sweep_test"))
# ① C 区两段头(u1/u2——直建 hero 面板,adventure_feats_test ⑦c 族同构)
func _sweep_hero_panel() -> void:
var panel = load("res://presentation/views/hero_info_panel_view.gd").new(_db.presentation.battle_layout)
var feat_head := _first_label(panel._feat_section)
var curio_head := _first_label(panel._curio_section)
Kit.check("C 区专长段头==「专长」(u1 换代——段头文案钉值)〔wa1〕",
feat_head != null and feat_head.text == "专长")
Kit.check("C 区属性值提升段头==「属性值提升」(u2 换代——同屏两段同标撞车消除)〔wa1〕",
curio_head != null and curio_head.text == "属性值提升")
# ②③ 奖励节点头部+专长卡兜底(u3/u11——静态直调)
func _sweep_narrator() -> void:
var narrator = load("res://presentation/battle_narrator.gd")
var head := String(narrator.node_curio_header())
Kit.check("奖励点头部含「冒险牌库」(u3 机制事实①同库)〔wa4〕", head.contains("冒险牌库"))
Kit.check("奖励点头部含「属性值提升与补给」(u3 机制事实②渠道分流)〔wa4〕", head.contains("属性值提升与补给"))
for w in ["同权重", "同上限", "提升池"]:
Kit.check("奖励点头部零「%s」旧话(u3 清零)〔wa4〕" % w, not head.contains(w))
Kit.check("专长卡兜底==「专长\n???」(u11 换代——数据缺行真实兜底路径)〔wa3〕",
String(narrator.feat_card_text({})) == "专长\n???")
# ④ 图例封印行(u4——直建 LegendOverlay 读 ENTRIES 常量)
func _sweep_legend() -> void:
load("res://presentation/views/legend_overlay.gd").new() # 直建(_init(rap_cost) 现签名)
var seal_row: Array = []
for e in load("res://presentation/views/legend_overlay.gd").ENTRIES:
if String(e[1]) == "封印":
seal_row = e
break
Kit.check("图例封印行含「标空」(u4——与「标中」成对)〔wa5〕", String(seal_row[2] if seal_row.size() > 2 else "").contains("标空"))
Kit.check("图例封印行含「受挖掘惩罚」(u4 雷字清零换代)〔wa5〕", String(seal_row[2] if seal_row.size() > 2 else "").contains("受挖掘惩罚"))
for w in ["插错", "挖雷"]:
Kit.check("图例封印行零「%s」〔wa5〕" % w,
not String(seal_row[2] if seal_row.size() > 2 else "").contains(w))
# ⑤⑥ 来源名双源(u5/u6——状态槽消费半+表值 ConfigDB 真装载+battle_log 行消费半)
func _sweep_source_names() -> void:
var row_view = load("res://presentation/views/status_slot_row.gd").new()
row_view.set_effects([{"effect_id": "alert", "source_class": "mine_hit",
"display_name": "危险格受击", "remaining_settlements": 2, "round_index": 1}])
var slot: PanelContainer = null
for child in row_view.get_children():
if child is PanelContainer and child.has_meta("eff"):
slot = child
break
Kit.check("状态槽 mine_hit 效果行在座(wa6 夹具前提)〔wa6〕", slot != null)
if slot != null:
row_view._on_slot_hover(slot)
Kit.check("状态槽来源注记==「危险格」(u5 换代——meta tooltip 消费半)〔wa6〕",
row_view._tip_label.text.contains("来源:危险格"))
row_view._hide_tip()
Kit.check("结算表 source_label_map[mine_hit]==「危险格」(u6 换代——ConfigDB 真装载)〔wa7〕",
String(_db.combat.settlement.core.source_label_map.get("mine_hit", "")) == "危险格")
var log_view = load("res://presentation/views/battle_log_view.gd").new(_db, _db.presentation.battle_layout)
var row_txt := String(log_view._format_row({"type": "effect", "source_class": "mine_hit",
"display_name": "挖掘反噬", "value": -5, "pool_value": -5, "round_index": 1}))
Kit.check("战斗日志 mine_hit 来源行含「(危险格)」(u6 消费半——_format_row 生产路径)〔wa7〕",
row_txt.contains("(危险格)"))
# ⑦⑧ 已开危险格字形族(u7/u8/u9——BoardView rebuild 真实刷新路径+源 token 扫描)
func _sweep_board_glyph() -> void:
var board_generator = load("res://core/board_generator.gd").new()
var b = board_generator.generate(1, 0, _mk_stream("board_gen", 4242), _db)
var ae = load("res://core/action_economy.gd").new(_db)
var ms = load("res://core/minesweeper_rules.gd").new(_db, ae)
ms.install_board(b)
ae.notify_round_start()
# 邻接雷定位(挖掘面规则=须邻接英雄——novice 驱动开域至危险格入挖掘射程;上限 60 步防冻态)
var mine_cell := Vector2i(-1, -1)
var glow_steps := 0
while glow_steps < 60:
for c in b.neighbors8(ms.hero_pos()):
if b.is_mine(c) and not ms.is_revealed(c):
mine_cell = c
break
if mine_cell.x >= 0:
break
var act: Dictionary = AutoPlayer.novice_action(ms)
if String(act.get("kind", "stop")) == "stop":
break
var op := String(act.get("kind", ""))
if op == "seal":
op = "seal_place"
ms.settle_batch([{"action": op, "cell": act.get("cell")}])
glow_steps += 1
Kit.check("棋盘夹具含挖掘射程内未开危险格(wa8 前提——实测 %d 步)〔wa8〕" % glow_steps, mine_cell.x >= 0)
if mine_cell.x >= 0:
ms.settle_batch([{"action": "dig", "cell": mine_cell}]) # 挖开危险格(真实规则路径——受击开格)
var view = load("res://presentation/views/board_view.gd").new()
view.rebuild(b, ms) # theme_projection_test.gd:200 先例式直建(缺素材程序化回退态——fresh checkout 冒烟同纪律)
var u: Node2D = view._cells[mine_cell]
Kit.check("已开危险格 glyph==「危」(u7 换代)〔wa8〕", u.glyph == "危")
Kit.check("已开危险格 plate==true(u9 牌载哨兵包含项随动)〔wa8〕", u.plate == true)
Kit.check("已开危险格 glyph_color==RED_MINE(u7 墨色不随字形换代漂移)〔wa8〕",
u.glyph_color == UiKit.RED_MINE)
# ⑧⑩ 源 token 扫描(注释剥离=行首 # 判——契约 2 保留面不入域)
func _sweep_token_scans() -> void:
var board_src := FileAccess.get_file_as_string("res://presentation/views/board_view.gd")
Kit.check("board_view.gd 源零 \"雷\" 字面量(u7/u8/u9 哨兵族统一钉住)〔wa8〕",
board_src.count("\"雷\"") == 0)
var orch_src := FileAccess.get_file_as_string("res://core/battle_orchestrator.gd")
var orch_hits := 0
for line in orch_src.split("\n"):
if line.strip_edges().begins_with("#"):
continue
if line.contains("挖雷"):
orch_hits += 1
Kit.check("battle_orchestrator.gd 玩家面字符串字面量零「挖雷」(u13——注释保留面不入域)〔wa12〕",
orch_hits == 0)
# 刷新两 toast 文案零数字字面量(架构整改 R4——格式串零数字、插值位与表字段一一对应;
# 静态判据+评审注记观测面局限:真机文案随表值由实机目检兜——T10 清单两项)
func _sweep_refresh_toast_no_literals() -> void:
var main_src := FileAccess.get_file_as_string("res://presentation/main.gd")
var rap_toast := ""
var cap_toast := ""
for line in main_src.split("
"):
if "弃图刷新需" in line:
rap_toast = line
if "本轮刷新次数已用完" in line:
cap_toast = line
Kit.check("V4 前置:两 toast 行在场(源锚)", rap_toast != "" and cap_toast != "")
# RAP 不足 toast:格式串零数字字面量(「需 %d」插值读 rap_refresh_cost 表值)
var rap_fmt_bad := "(弃图刷新需 8)" in rap_toast or "%d 点(弃图刷新需 %d)" not in rap_toast
Kit.check("RAP 不足 toast:格式串零数字字面量、插值位=cost 表值拼接〔架构整改 V4〕", not rap_fmt_bad)
# 轮上限 toast:文案零数字字面量(「每轮 %d 次」插值读 refresh_cap_per_round 表值)
Kit.check("轮上限 toast:零数字字面量、插值位=refresh_cap_per_round 表值拼接〔架构整改 V4〕",
"(每轮 2 次)" not in cap_toast and "每轮 %d 次" in cap_toast)
func _first_label(node: Node) -> Label:
for child in node.get_children():
if child is Label:
return child
return null
func _mk_stream(stream_name: String, scope: int) -> RandomNumberGenerator:
# stream_name = 调用点自述标签(不参与派生),保留参数零改调用方(theme_projection_test 同构)
var rg := RandomNumberGenerator.new()
var svc = load("res://core/rng_service.gd").new()
svc.set_master_seed(42)
rg.seed = svc.derive_at(scope)
return rg