Files
gplan-2/game/core/board_generator.gd
T
DKChan 1b9736f065 fix(iter37①): T1 core 域缺陷修——单据归属判定/数值入表/公式单点/信号契约载体统一
- d1 伤害控制器来源敌作废判定 `==`→`is_same`(同内容异实例不再误废;夹具双敌两断言〔wd1〕)
- d11 shop 表新增 relic_base_price/spend_discount 两 knob+域检+ConfigDB 访问器+level_runner 读表;
  level_shop_economy_test 三表值断言+relic 现价读表与迁移前字面量逐位等价〔wd11〕
- d16a HeroTable.display_name+域检+访问器+unit_container 读表;spawn 真实链断言〔wd16〕
- d14 board_generator 目标雷数公式单点 target_mine_count(两调用点改引)+legality 去 db 参数(两调用点同批改)〔wd14〕
- d15 蓄力层上限 CHARGE_LAYER_CAP 常量(两处字面量+注释随动)
- d16b unit_container/status_container 六处 Dictionary 取值显式 int() 包裹(读侧包)
- d10 minesweeper 两信号按 §2.1 三要素补契约(载荷键集按发射位并集实查)+全库 28 处信号注释载体 #→##

撤销证明:回退 d1 hunk → 同容异实例断言红(A 单据被 B 死亡误废),还原复绿;
回退 d10 契约 → 契约在位扫描 undocumented=2 红,还原归零。等价类申报(d11/d14/d15/d16)见实现报告。
代拍板:#1 target_mine_count 签名(照接口块)、#2 契约措辞与载荷键并集(实查发射位)、#4 int() 读侧包写法。
2026-10-25 03:35:55 +08:00

399 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
# ==================== 生成入口 ====================
# 目标雷数 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)
assert(false, msg)