Files
gplan-2/game/core/board_generator.gd
T
DKChan ea322599f7 卡②(结构小件批p1-p7+V15): 七点定点修落地+运行期违约通道负面套件+wr10 偏运缝拆验收——①p1 状态角标判据去重(main_overlay_flow._sync_status_entry 一组重复赋值语句删除,battle_live 局部量随之退役=wr1 载体);②p2 确认窗开合归模态栈单口(main 两处 _on_confirm_* 改 _close_modal;存档删除入口改 root._open_modal(root.confirm_dialog) 后再 open,遮挡记账/挂起兜底/开侧同步同口);③p3 棋盘动作与 stop 两入口显式 settlement 空防护(锁谓词异常时不崩于空引用);④p4 会话更替即清队:_begin_session 首行 _row_pacer.flush()(待播行不住局态=机制保证非时序约定);⑤p5 缝拆:RewardEconomy 删跨域直持(_fx/_ctx/set_fx)改注入 luck_bias_query: Callable,reward_roll 发射与 bias_rewards 求和收回 BattleFxHost.luck_bias_for(cell_count)(三参形 dispatch,不带 facts——禁复用 fx_emit 的第四实参会打开 when 事实条件面),组装点 set_fx 一行换赋值一行 1 换 1;类变量 15−2+2 净 15,gdstyle 无第 16 枚;⑥p6/p7 RngService 未知用途标签与未播种取数两分支加 violation_handler 记录位+release 下 return null 空引用即刻崩(fail-fast 传导,合法路径不可达)——debug 下 assert 先中断函数本体两态不可判别,故记录位是唯一可观测面;⑦board_generator/reward_economy 各加 fatal_handler 注入位并由组装点接 orchestrator._fatal(release 剥 assert 后仍经 battle_result 封印真中止)。新件 tests/unit/core/run_time_fatal_test.gd(manifest 登记 expect=1 negative=true):wr5 协作件 _fatal 经组装点传导 p5 封印+缺省形态不外溢/wr6 未知标签通道/wr7 未播种通道(各用新建 RngService 实例,播种断言 _counter==0)。wr10 三条入 variant_placement_test=判定面夹具白盒装载(现网数据无 bias_rewards 行,沿 tree_rules_v11_test 先例)+接线同源+权重生效 77+4*2。验收:variant_placement 49 checks 全绿(wr10 ①②③ PASS:空订阅 0/命中 1 行/装载后 2;查询口有效且与宿主同源;偏运池实测 85);run_time_fatal_test 退出码 1 三点绿;pacing_queue 绿(wr4 前置夹具真入队 2 行→更替后清零);ui_click_probe 问题 0(wr2 遮挡账开/关两侧断言);round_flow_skill_pool 130 checks 绿;wr11 dump_battle 六组(L{1,2}×S{42,779401,779501})与改前 before_* 逐字节 cmp 全等;gdstyle 基线外新违规 0、恶化 1(level_map_screen 类变量 22>21=iter38 存量非本批触碰面)、基线文件零改动〔结构小件批 V15〕
2026-10-27 18:39:53 +08:00

405 lines
15 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
## 确定性棋盘生成器:生成-验证循环出图,不达标走接受最优/最不坏诚实降级(垂直切片验证:每张图可推理可归因)。
## 只消费传入的单条 rng 流(重试共用,attempt 边界非 scope 事件)——同流同参逐位复现。
## generate 的 db 参数鸭子型(不标类型)——冒烟测量面注入桩表依赖此形态,勿收紧签名。
class_name BoardGenerator
extends RefCounted
const METRIC_VERSION := 1
# 违约中止通道——缺省空 Callable 退化 push_error+assert(现行为逐字保持);组装层注入
# orchestrator._fatal,使 release 剥 assert 后仍经 battle_result 封印真中止。
var fatal_handler: Callable = Callable()
# ==================== 生成入口 ====================
# 目标雷数 N = round(d×(W×H−9))(params 快照键:density/w/h)——布雷定数与产物断言两处的单点公式
static func target_mine_count(params: Dictionary) -> int:
return int(round(float(params.density) * float(int(params.w) * int(params.h) - BoardData.SPAWN_AREA_CELLS)))
# rng_stream = 一次 open_stream("board_gen") 实例(一张图的全部取数;重试共用——attempt 边界非 scope 事件)。
# db 参数不标具体类型原因:冒烟工具注入 FakeDB 桩(experiment_density.gd 等)——typed 形参会拒收桩表
func generate(L: int, D: int, rng_stream: RandomNumberGenerator, db) -> BoardData: # type-hint: ignore(测量面 _BoardRowStub 冒烟夹具鸭子型注入——typed 参拒 stub)
var row: Dictionary[Variant, Variant] = db.resolve_board_gen(L, D)
if row.is_empty():
_fatal("BoardGen 行缺失 (%d,%d)" % [L, D])
return null
# 生成期表值单次读取快照(防热重载撕裂 attempt 语义)
var params := {
"L": L, "D": D,
"w": int(row.board_width), "h": int(row.board_height),
"density": float(row.mine_density),
"target": float(row.inferability_target),
"solver_rules": String(row.solver_rules),
"min_safe": int(row.min_safe_count),
"max_attempts": int(row.max_gen_attempts),
"board_type": String(row.board_type),
# 初始展开收窄批(迭代14① T1——两新参数;.get 缺省=legacy 调用方防御口径:shave_r=1 现行投值、
# r0_cap=0 闸关。正式表行全行投值,加载期校验在 BoardGenTable.validate)
"shave_r": int(row.get("shave_ring_radius", 1)),
"r0_cap": int(row.get("r0_max_cap", 0)),
}
var best_legal: BoardData = null
var best_legal_score := -1.0
var best_bad: BoardData = null
var best_badness := [999999, 999999, 999999]
var attempts_used := 0
var accepted: BoardData = null
var accept_mode := ""
var r0_cap_rejected := 0
for k in range(1, params.max_attempts + 1):
attempts_used = k
var b := _attempt(params, rng_stream, db)
var badness := _badness(b, params)
if best_bad == null or _lex_less(badness, best_badness):
best_bad = b
best_badness = badness
# R₀≤cap 重掷闸触发计数(迭代14① T3——尝试事实,与合法性判定独立记录;占比=T5 闸触发率读数)
if int(params.r0_cap) > 0 and b.compute_r0().size() > int(params.r0_cap):
r0_cap_rejected += 1
if legality(b, params):
var m: Dictionary[Variant, Variant] = measure_inferability(b) # 求解器每合法板恰一次(O3 收口——此前重复调用两次)
var inf: float = m["inferability"]
if best_legal == null or inf > best_legal_score:
best_legal = b
best_legal_score = inf
if inf >= params.target: # 单一判据(棋盘生成:现行规则已回归"可推理率 ≥ 及格线")
accepted = b
accept_mode = "first_pass"
break
if accepted == null:
if best_legal != null:
accepted = best_legal
accept_mode = "accept_best"
accepted.degraded_product = true # 降级标记(独立于 illegal_product——三分出口表要求标记随产物上抛)
push_warning("[棋盘生成] 重试耗尽无达标——接受最优出图(强告警埋点,target=%.2f best=%.3f)" % [params.target, best_legal_score])
else:
accepted = best_bad
accept_mode = "accept_worst"
accepted.illegal_product = true
push_error("[棋盘生成] attempts 耗尽零合法——接受最不坏出图 + 非法产物标记(玩家永不卡死)")
_finalize(accepted, params, attempts_used, accept_mode, db, r0_cap_rejected)
return accepted
# ==================== 五阶段:单次尝试 ====================
func _attempt(params: Dictionary, rng_stream: RandomNumberGenerator, db) -> BoardData: # type-hint: ignore(同 generate——测量面 stub 注入)
var b := BoardData.new()
b.width = params.w
b.height = params.h
b.spawn_center = Vector2i(int((params.w - 1) / 2.0), int((params.h - 1) / 2.0))
b.board_type = str(params.board_type)
b.level_index = params.L
b.node_depth = params.D
# ② 布雷:N = round(d×(W×H−9)) 先定数;域 = 全格 − 出生 3×3;均匀布雷后"先布后删"
# (−9 = 出生区格数,常量单点 BoardData.SPAWN_AREA_CELLS)
var spawn := {}
for c in b.spawn_cells():
spawn[c] = true
var domain: Array[Vector2i] = []
for c in b.all_cells():
if not spawn.has(c):
domain.append(c)
var n_mines: int = target_mine_count(params)
# 部分 Fisher-Yates(确定性,消费 board_gen 流)
for i in range(0, mini(n_mines, domain.size())):
var j := rng_stream.randi_range(i, domain.size() - 1)
var tmp = domain[i]
domain[i] = domain[j]
domain[j] = tmp
for i in range(0, mini(n_mines, domain.size())):
b.mines[domain[i]] = true
_compute_digits(b)
# 先布后删:剃雷域 = 距 spawn_center 切比雪夫距离 ≤1+r 的格(迭代14①收窄批 T2——原「R₀ flood 域∪
# 外扩 1 环」全域剃雷=开局开阔地巨大根因〔10×10 板 R₀ 均值 68.6=板面 68.6%〕;收窄后开阔地=
# 出生邻域外的自然 flood。r 读表 shave_ring_radius(r=1 → 5×5=25 格;r=0 即只剃出生 3×3)。
# 只减不增、无迭代语义不变;ring_removed 指标语义不变(现值变小=预期);零 RNG 消费变化——
# 同种子布雷序列逐位一致,仅移除域收窄。
var shave_r: int = int(params.get("shave_r", 1))
var ring := {}
for c in b.all_cells():
if maxi(absi(c.x - b.spawn_center.x), absi(c.y - b.spawn_center.y)) <= 1 + shave_r:
ring[c] = true
var removed := 0
for c in ring.keys():
if b.mines.has(c):
b.mines.erase(c)
removed += 1
_compute_digits(b)
# ③ 变异布置(地形变异 框架面):布置约束与算法随首批变异类型定稿(生成期布置接口结构位),
# 定稿前零布置——清单为空是合法态(P0),静默产出无变异图。
b.metrics = {
"metric_version": METRIC_VERSION,
"ring_removed": removed,
}
return b
func _compute_digits(b: BoardData) -> void:
b.digits.clear()
for c in b.all_cells():
if b.is_safe(c):
var d := 0
for n in b.neighbors8(c):
if b.is_mine(n):
d += 1
b.digits[c] = d
# ==================== F1 可推理率度量(独立入口——与内嵌调用同一实现) ====================
# 返回 {r0, r_inf_count, inferability}
func measure_inferability(b: BoardData) -> Dictionary:
var safe_set := {}
for c in b.safe_cells():
safe_set[c] = true
var r := {}
for c in b.compute_r0():
r[c] = true
var known_mines := {}
var changed := true
while changed:
changed = false
# 算子① 连锁:0 格的 8 邻安全格全入 R
var frontier: Array[Variant] = r.keys()
for x in frontier:
if safe_set.has(x) and b.digit_of(x) == 0:
for n in b.neighbors8(x):
if safe_set.has(n) and not r.has(n):
r[n] = true
changed = true
# 信息源:数字约束 single + subset(solver_rules=single_subset 冻结域)
var constraints := _build_constraints(b, r, known_mines, safe_set)
# single 规则
for con in constraints:
if con.mines == 0:
for c in con.unknown:
if not r.has(c):
r[c] = true
changed = true
elif con.mines == con.unknown.size():
for c in con.unknown:
if not known_mines.has(c):
known_mines[c] = true
changed = true
# subset 规则:U1 ⊂ U2 ⇒ (d2−d1, U2∖U1)
var derived := []
for i in constraints.size():
for j in range(i + 1, constraints.size()):
var a: Dictionary[Variant, Variant] = constraints[i]
var bb: Dictionary[Variant, Variant] = constraints[j]
var sub := _subset_of(a.unknown, bb.unknown)
if sub == 1:
derived.append(_diff_constraint(a, bb))
elif sub == 2:
derived.append(_diff_constraint(bb, a))
for con in derived:
if con.mines == 0:
for c in con.unknown:
if not r.has(c): # 同 no-peek 口径:推导结论不再用 safe_set 复核(约束本身可靠)
r[c] = true
changed = true
elif con.mines == con.unknown.size():
for c in con.unknown:
if not known_mines.has(c):
known_mines[c] = true
changed = true
var r_inf := r.keys()
var s_count := safe_set.size()
var inferability: float = float(r_inf.size()) / float(maxi(1, s_count))
return {
"r0": b.compute_r0().size(),
"r_inf_count": r_inf.size(),
"inferability": inferability,
}
func _build_constraints(b: BoardData, r: Dictionary, known_mines: Dictionary, safe_set: Dictionary) -> Array:
var out: Array[Dictionary] = []
for c in r.keys():
if not safe_set.has(c):
continue # 只对已开安全格的数字建约束
var d := b.digit_of(c)
var unknown := {}
var known_here := 0
for n in b.neighbors8(c):
if known_mines.has(n):
known_here += 1
elif not r.has(n):
unknown[n] = true # 未揭示格一律计入未知(含危险格)——求解器禁读 safe_set 真值(棋盘生成:雷位不许偷看)
var d_eff := d - known_here
if d_eff < 0 or (d_eff > 0 and unknown.is_empty()):
continue # 矛盾态(非法板)——不产约束,验收由 legality 闸承担
if not unknown.is_empty():
out.append({"mines": d_eff, "unknown": unknown})
return out
func _subset_of(a: Dictionary, b: Dictionary) -> int:
# 1: a ⊂ b;2: b ⊂ a;0: 互不包含
var a_in_b := true
var b_in_a := true
for k in a.keys():
if not b.has(k):
a_in_b = false
for k in b.keys():
if not a.has(k):
b_in_a = false
if a.size() < b.size() and a_in_b:
return 1
if b.size() < a.size() and b_in_a:
return 2
return 0
func _diff_constraint(big: Dictionary, small: Dictionary) -> Dictionary:
var diff := {}
for k in big.unknown.keys():
if not small.unknown.has(k):
diff[k] = true
return {"mines": big.mines - small.mines, "unknown": diff}
# ==================== 合法性闸(独立入口;F5 legality + R₀≠S 修订义务) ====================
func is_clearable(b: BoardData) -> bool:
var safe_set := {}
for c in b.safe_cells():
safe_set[c] = true
var seen := {}
var queue: Array[Variant] = b.spawn_cells().duplicate()
for c in queue:
seen[c] = true
while not queue.is_empty():
var c: Vector2i = queue.pop_front()
if not safe_set.has(c):
continue
for n in b.neighbors8(c):
if safe_set.has(n) and not seen.has(n):
seen[n] = true
queue.append(n)
for c in safe_set.keys():
if not seen.has(c):
return false
return true
func legality(b: BoardData, params: Dictionary) -> bool:
var reasons := _legality_faults(b, params)
return reasons.is_empty()
func _legality_faults(b: BoardData, params: Dictionary) -> Array:
var faults: Array[String] = []
# ① 出生区无雷 ② 出生区∪R₀ 无变异 ③ 数字恒真 ④ R₀ ≠ S(排雷玩法 修订义务)
for c in b.spawn_cells():
if b.is_mine(c):
faults.append("inv1_mine_in_spawn")
var r0 := {}
for c in b.compute_r0():
r0[c] = true
if b.variant_ids(c).size() > 0:
faults.append("inv2b_variant_in_r0")
for c in b.spawn_cells():
if b.variant_ids(c).size() > 0:
faults.append("inv2a_variant_in_spawn")
for c in b.all_cells():
if b.is_safe(c) and b.digit_of(c) != _neighbor_mine_count(b, c):
faults.append("inv3_digit_invariant")
if r0.size() == b.safe_cells().size():
faults.append("inv4_r0_equals_s")
# clearable(合法性闸——与质量度量两闸分离)
if not is_clearable(b):
faults.append("not_clearable")
# min_safe 产物侧(容错两种入参形态:generate 的 params 快照 / 原始表行)
var min_safe: int = int(params.get("min_safe", params.get("min_safe_count", 0)))
if b.safe_cells().size() < min_safe:
faults.append("min_safe_short")
# ⑤ R₀≤cap 重掷闸(迭代14①收窄批 T3——合法性先于质量评分,同 inv4 白给板先例;超阈整板作废
# 同流重试〔重试共用 board_gen 流,attempt 边界非 scope 事件〕;全部尝试超阈 → 既有三分出口
# accept_worst 诚实降级+非法产物标记(玩家永不卡死)。cap=0 = 闸关(legacy 调用方防御口径)
var r0_cap: int = int(params.get("r0_cap", 0))
if r0_cap > 0 and r0.size() > r0_cap:
faults.append("r0_cap_exceeded")
return faults
func _neighbor_mine_count(b: BoardData, c: Vector2i) -> int:
var d := 0
for n in b.neighbors8(c):
if b.is_mine(n):
d += 1
return d
# ==================== 产出上报(生成期指标集) ====================
func _finalize(b: BoardData, params: Dictionary, attempts_used: int, accept_mode: String, _db, r0_cap_rejected: int = 0) -> void: # type-hint: ignore(同 generate——测量面 stub 注入)
var m := measure_inferability(b)
# 产物断言(棋盘生成 出生区安全与反退化布雷):实际雷数 ≤ 目标雷数 N——先布后删只减不增,失败即致命
var n_target: int = target_mine_count(params)
if b.mines.size() > n_target:
_fatal("产物断言违例:实际雷数 %d > 目标雷数 N=%d(先布后删只减不增)" % [b.mines.size(), n_target])
var attempt_facts := b.metrics # 保留尝试期事实(ring_removed)
b.metrics = {
"metric_version": METRIC_VERSION,
"inferability": m.inferability,
"actual_mines": b.mines.size(),
"target_mines": n_target,
"actual_safe_count": b.safe_cells().size(),
"attempts_used": attempts_used,
"accept_mode": accept_mode,
"degraded_product": accept_mode == "accept_best",
"illegal_product": accept_mode == "accept_worst",
"ring_removed": attempt_facts.get("ring_removed", 0),
"r0_count": m["r0"],
# R₀≤cap 重掷闸尝试事实(迭代14① T3——占比=T5 闸触发率读数,过高=甲乙配比失衡信号)
"r0_cap_rejected": r0_cap_rejected,
}
b.illegal_product = accept_mode == "accept_worst"
b.degraded_product = accept_mode == "accept_best"
func _badness(b: BoardData, params: Dictionary) -> Array:
# F5 k*** 确定性劣度字典序:(不可达安全格数, min_safe 缺口, 序号)
var unreachable := 0
var safe_set := {}
for c in b.safe_cells():
safe_set[c] = true
var seen := {}
var queue: Array[Variant] = b.spawn_cells().duplicate()
for c in queue:
seen[c] = true
while not queue.is_empty():
var c: Vector2i = queue.pop_front()
if not safe_set.has(c):
continue
for n in b.neighbors8(c):
if safe_set.has(n) and not seen.has(n):
seen[n] = true
queue.append(n)
for c in safe_set.keys():
if not seen.has(c):
unreachable += 1
var gap: int = maxi(0, int(params.min_safe) - b.safe_cells().size())
return [unreachable, gap, 0]
func _lex_less(a: Array, b: Array) -> bool:
for i in a.size():
if a[i] < b[i]:
return true
if a[i] > b[i]:
return false
return false
func _fatal(msg: String) -> void:
push_error("[棋盘生成/fatal] " + msg)
if fatal_handler.is_valid():
fatal_handler.call(msg) # 组装层注入 orchestrator._fatal(封印传导,不绕第二中止通道)
assert(false, msg)