# VERTICAL SLICE - NOT FOR PRODUCTION # Validation Question: 玩家能否无指导在一场完整战斗中经历「开战→挖掘规划→轮末结算逐行兑现→胜/负」并逐行归因(P2) # Date: 2026-09-10 # 确定性棋盘生成器:五阶段管线(参数→布雷→变异布置→生成-验证→产出)+ F1 可推理率度量 # (single_subset 求解器)+ F5 验收算子(达标即出/接受最优/接受最不坏)。 # 切片裁剪登记:黄金向量回归套件与 F4 批量工具不做(难度曲线与Boss 调参期工具域)。 # 实现 GDD:design/archive/gdd-2026-09-14/a1-board-generation.md(系统 ID A1·棋盘生成——registry 追溯锚,命名规范见 CLAUDE.md) class_name BoardGenerator extends RefCounted const METRIC_VERSION := 1 # ==================== 生成入口 ==================== # rng_stream = 一次 open_stream("board_gen") 实例(一张图的全部取数;重试共用——attempt 边界非 scope 事件) func generate(L: int, D: int, rng_stream: RandomNumberGenerator, db) -> BoardData: var row: Dictionary = 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), } 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 := "" 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 if legality(b, params, db): var m: Dictionary = 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) return accepted # ==================== 五阶段:单次尝试 ==================== func _attempt(params: Dictionary, rng_stream: RandomNumberGenerator, db) -> BoardData: 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;均匀布雷后"先布后删" var spawn := {} for c in b.spawn_cells(): spawn[c] = true var domain: Array = [] for c in b.all_cells(): if not spawn.has(c): domain.append(c) var n_mines: int = int(round(params.density * float(params.w * params.h - 9))) # 部分 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) # 先布后删:0 格闭包(布后基准)外扩 1 环内的雷移除(只减不增、无迭代) var ring := {} for c in b.compute_r0(): for n in b.neighbors8(c): if not ring.has(n): ring[n] = 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 = 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 = constraints[i] var bb: Dictionary = 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 = [] 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 = 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, db) -> bool: var reasons := _legality_faults(b, params) return reasons.is_empty() func _legality_faults(b: BoardData, params: Dictionary) -> Array: var faults: Array = [] # ① 出生区无雷 ② 出生区∪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") 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) -> void: var m := measure_inferability(b) # 产物断言(棋盘生成 出生区安全与反退化布雷):实际雷数 ≤ 目标雷数 N——先布后删只减不增,失败即致命 var n_target: int = int(round(float(params.density) * float(params.w * params.h - 9))) 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"], } 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 = 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)