diff --git a/game/core/board_generator.gd b/game/core/board_generator.gd index 7c370070..d571b5c6 100644 --- a/game/core/board_generator.gd +++ b/game/core/board_generator.gd @@ -30,6 +30,10 @@ func generate(L: int, D: int, rng_stream: RandomNumberGenerator, db) -> BoardDat "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 @@ -38,6 +42,7 @@ func generate(L: int, D: int, rng_stream: RandomNumberGenerator, db) -> BoardDat 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) @@ -45,6 +50,9 @@ func generate(L: int, D: int, rng_stream: RandomNumberGenerator, db) -> BoardDat 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, db): var m: Dictionary = measure_inferability(b) # 求解器每合法板恰一次(O3 收口——此前重复调用两次) var inf: float = m["inferability"] @@ -66,7 +74,7 @@ func generate(L: int, D: int, rng_stream: RandomNumberGenerator, db) -> BoardDat accept_mode = "accept_worst" accepted.illegal_product = true push_error("[棋盘生成] attempts 耗尽零合法——接受最不坏出图 + 非法产物标记(玩家永不卡死)") - _finalize(accepted, params, attempts_used, accept_mode, db) + _finalize(accepted, params, attempts_used, accept_mode, db, r0_cap_rejected) return accepted @@ -99,12 +107,16 @@ func _attempt(params: Dictionary, rng_stream: RandomNumberGenerator, db) -> Boar for i in range(0, mini(n_mines, domain.size())): b.mines[domain[i]] = true _compute_digits(b) - # 先布后删:0 格闭包(布后基准)外扩 1 环内的雷移除(只减不增、无迭代) + # 先布后删:剃雷域 = 距 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.compute_r0(): - for n in b.neighbors8(c): - if not ring.has(n): - ring[n] = true + 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): @@ -300,6 +312,12 @@ func _legality_faults(b: BoardData, params: Dictionary) -> Array: 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 @@ -313,7 +331,7 @@ func _neighbor_mine_count(b: BoardData, c: Vector2i) -> int: # ==================== 产出上报(生成期指标集) ==================== -func _finalize(b: BoardData, params: Dictionary, attempts_used: int, accept_mode: String, _db) -> void: +func _finalize(b: BoardData, params: Dictionary, attempts_used: int, accept_mode: String, _db, r0_cap_rejected: int = 0) -> void: var m := measure_inferability(b) # 产物断言(棋盘生成 出生区安全与反退化布雷):实际雷数 ≤ 目标雷数 N——先布后删只减不增,失败即致命 var n_target: int = int(round(float(params.density) * float(params.w * params.h - BoardData.SPAWN_AREA_CELLS))) @@ -332,6 +350,8 @@ func _finalize(b: BoardData, params: Dictionary, attempts_used: int, accept_mode "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" diff --git a/game/data/schemas/board_gen_table.gd b/game/data/schemas/board_gen_table.gd index eef3ff8f..d178bbff 100644 --- a/game/data/schemas/board_gen_table.gd +++ b/game/data/schemas/board_gen_table.gd @@ -65,6 +65,17 @@ func validate() -> Array: var bt: String = str(row.get("board_type", "")) if not BOARD_TYPE_VOCABULARY.has(bt): errs.append({"category": "cross_field", "message": "(%d,%d) board_type 越词表: %s" % [L, D, bt]}) + # 初始展开收窄批两参数(迭代14① T1——加载期校验违例致命;缺省 shave=1 现行投值 / cap=0 闸关防御口径) + var sr: int = int(row.get("shave_ring_radius", 1)) + if sr < 0 or sr > 2: + errs.append({"category": "domain", "message": "(%d,%d) shave_ring_radius 越域 [0,2]: %d" % [L, D, sr]}) + var r0_cap: int = int(row.get("r0_max_cap", 0)) + if r0_cap > 0: + # 护栏(任务书 §二):cap ≥ 出生格数 9(BoardData.SPAWN_AREA_CELLS)、cap ≤ 板格数一半 + if r0_cap < BoardData.SPAWN_AREA_CELLS: + errs.append({"category": "cross_field", "message": "(%d,%d) r0_max_cap 低于出生保障面 %d: %d" % [L, D, BoardData.SPAWN_AREA_CELLS, r0_cap]}) + if r0_cap > (w * h) / 2: + errs.append({"category": "cross_field", "message": "(%d,%d) r0_max_cap 超板格数一半 %d: %d" % [L, D, (w * h) / 2, r0_cap]}) anchor_ok[L] = anchor_ok.get(L, false) or D == 0 for L in anchor_ok.keys(): if not anchor_ok[L]: diff --git a/game/data/tables/board_gen.tres b/game/data/tables/board_gen.tres index b31bf16d..a783327e 100644 --- a/game/data/tables/board_gen.tres +++ b/game/data/tables/board_gen.tres @@ -14,7 +14,9 @@ rows = [{ "solver_rules": "single_subset", "min_safe_count": 50, "max_gen_attempts": 16, -"board_type": "dungeon" +"board_type": "dungeon", +"shave_ring_radius": 1, +"r0_max_cap": 45 }, { "level_index": 1, "node_depth": 1, @@ -25,7 +27,9 @@ rows = [{ "solver_rules": "single_subset", "min_safe_count": 50, "max_gen_attempts": 16, -"board_type": "dungeon" +"board_type": "dungeon", +"shave_ring_radius": 1, +"r0_max_cap": 45 }, { "level_index": 1, "node_depth": 2, @@ -36,7 +40,9 @@ rows = [{ "solver_rules": "single_subset", "min_safe_count": 50, "max_gen_attempts": 16, -"board_type": "dungeon" +"board_type": "dungeon", +"shave_ring_radius": 1, +"r0_max_cap": 45 }, { "level_index": 1, "node_depth": 3, @@ -47,7 +53,9 @@ rows = [{ "solver_rules": "single_subset", "min_safe_count": 50, "max_gen_attempts": 16, -"board_type": "dungeon" +"board_type": "dungeon", +"shave_ring_radius": 1, +"r0_max_cap": 45 }, { "level_index": 1, "node_depth": 4, @@ -58,7 +66,9 @@ rows = [{ "solver_rules": "single_subset", "min_safe_count": 60, "max_gen_attempts": 24, -"board_type": "dungeon" +"board_type": "dungeon", +"shave_ring_radius": 1, +"r0_max_cap": 43 }, { "level_index": 2, "node_depth": 0, @@ -69,7 +79,9 @@ rows = [{ "solver_rules": "single_subset", "min_safe_count": 60, "max_gen_attempts": 24, -"board_type": "underdark" +"board_type": "underdark", +"shave_ring_radius": 1, +"r0_max_cap": 43 }, { "level_index": 2, "node_depth": 1, @@ -80,7 +92,9 @@ rows = [{ "solver_rules": "single_subset", "min_safe_count": 60, "max_gen_attempts": 24, -"board_type": "underdark" +"board_type": "underdark", +"shave_ring_radius": 1, +"r0_max_cap": 43 }, { "level_index": 2, "node_depth": 4, @@ -91,7 +105,9 @@ rows = [{ "solver_rules": "single_subset", "min_safe_count": 60, "max_gen_attempts": 24, -"board_type": "underdark" +"board_type": "underdark", +"shave_ring_radius": 1, +"r0_max_cap": 43 }, { "level_index": 2, "node_depth": 5, @@ -102,7 +118,9 @@ rows = [{ "solver_rules": "single_subset", "min_safe_count": 60, "max_gen_attempts": 24, -"board_type": "underdark" +"board_type": "underdark", +"shave_ring_radius": 1, +"r0_max_cap": 43 }, { "level_index": 2, "node_depth": 6, @@ -113,7 +131,9 @@ rows = [{ "solver_rules": "single_subset", "min_safe_count": 60, "max_gen_attempts": 24, -"board_type": "underdark" +"board_type": "underdark", +"shave_ring_radius": 1, +"r0_max_cap": 43 }, { "level_index": 2, "node_depth": 7, @@ -124,7 +144,9 @@ rows = [{ "solver_rules": "single_subset", "min_safe_count": 60, "max_gen_attempts": 24, -"board_type": "underdark" +"board_type": "underdark", +"shave_ring_radius": 1, +"r0_max_cap": 43 }, { "level_index": 2, "node_depth": 9, @@ -135,7 +157,9 @@ rows = [{ "solver_rules": "single_subset", "min_safe_count": 60, "max_gen_attempts": 24, -"board_type": "underdark" +"board_type": "underdark", +"shave_ring_radius": 1, +"r0_max_cap": 43 }, { "level_index": 3, "node_depth": 0, @@ -146,7 +170,9 @@ rows = [{ "solver_rules": "single_subset", "min_safe_count": 70, "max_gen_attempts": 32, -"board_type": "deep_underdark" +"board_type": "deep_underdark", +"shave_ring_radius": 1, +"r0_max_cap": 51 }, { "level_index": 3, "node_depth": 1, @@ -157,7 +183,9 @@ rows = [{ "solver_rules": "single_subset", "min_safe_count": 70, "max_gen_attempts": 32, -"board_type": "deep_underdark" +"board_type": "deep_underdark", +"shave_ring_radius": 1, +"r0_max_cap": 51 }, { "level_index": 3, "node_depth": 4, @@ -168,7 +196,9 @@ rows = [{ "solver_rules": "single_subset", "min_safe_count": 70, "max_gen_attempts": 32, -"board_type": "deep_underdark" +"board_type": "deep_underdark", +"shave_ring_radius": 1, +"r0_max_cap": 51 }, { "level_index": 3, "node_depth": 5, @@ -179,7 +209,9 @@ rows = [{ "solver_rules": "single_subset", "min_safe_count": 70, "max_gen_attempts": 32, -"board_type": "deep_underdark" +"board_type": "deep_underdark", +"shave_ring_radius": 1, +"r0_max_cap": 51 }, { "level_index": 3, "node_depth": 6, @@ -190,7 +222,9 @@ rows = [{ "solver_rules": "single_subset", "min_safe_count": 70, "max_gen_attempts": 32, -"board_type": "deep_underdark" +"board_type": "deep_underdark", +"shave_ring_radius": 1, +"r0_max_cap": 51 }, { "level_index": 3, "node_depth": 7, @@ -201,7 +235,9 @@ rows = [{ "solver_rules": "single_subset", "min_safe_count": 70, "max_gen_attempts": 32, -"board_type": "deep_underdark" +"board_type": "deep_underdark", +"shave_ring_radius": 1, +"r0_max_cap": 51 }, { "level_index": 3, "node_depth": 9, @@ -212,7 +248,9 @@ rows = [{ "solver_rules": "single_subset", "min_safe_count": 82, "max_gen_attempts": 32, -"board_type": "deep_underdark" +"board_type": "deep_underdark", +"shave_ring_radius": 1, +"r0_max_cap": 59 }] ; 切片调参登记(SLICE-TUNED provisional,调参所有权仍归 C3): diff --git a/game/smoke/calibrate_boards.gd b/game/smoke/calibrate_boards.gd index 98aaad7a..680c9852 100644 --- a/game/smoke/calibrate_boards.gd +++ b/game/smoke/calibrate_boards.gd @@ -28,7 +28,7 @@ func _initialize() -> void: quit(1) return var gen = load("res://core/board_generator.gd").new() - print("level,depth,w,h,n,mines_mean,mines_med,safe_mean,safe_med,r0_mean,inf_mean") + print("level,depth,w,h,n,mines_mean,mines_med,safe_mean,safe_med,r0_mean,r0_sd,r0_p90,r0_max,inf_mean,cap_rej_mean") for row in db.board_gen.rows: var rd := { "level_index": int(row.level_index), "node_depth": int(row.node_depth), @@ -36,6 +36,9 @@ func _initialize() -> void: "mine_density": float(row.mine_density), "inferability_target": float(row.inferability_target), "solver_rules": String(row.solver_rules), "min_safe_count": int(row.min_safe_count), "max_gen_attempts": int(row.max_gen_attempts), "board_type": String(row.board_type), + # 初始展开收窄批(迭代14①)两新参数透传(.get 缺省与生成器防御口径一致) + "shave_ring_radius": int(row.get("shave_ring_radius", 1)), + "r0_max_cap": int(row.get("r0_max_cap", 0)), } var fdb := FakeDB.new() fdb.row = rd @@ -43,6 +46,7 @@ func _initialize() -> void: var safes: Array = [] var r0s: Array = [] var infs: Array = [] + var cap_rejs: Array = [] for i in range(n): var rg := RandomNumberGenerator.new() rg.seed = 900000 + i @@ -53,12 +57,14 @@ func _initialize() -> void: safes.append(b.safe_cells().size()) r0s.append(int(b.metrics.r0_count)) infs.append(float(b.metrics.inferability)) + cap_rejs.append(int(b.metrics.get("r0_cap_rejected", 0))) if mines.is_empty(): printerr("L%d d%d 生成全部失败" % [int(row.level_index), int(row.node_depth)]) continue - print("%d,%d,%d,%d,%d,%.1f,%d,%.1f,%d,%.1f,%.3f" % [ + print("%d,%d,%d,%d,%d,%.1f,%d,%.1f,%d,%.1f,%.2f,%d,%d,%.3f,%.2f" % [ int(row.level_index), int(row.node_depth), int(row.board_width), int(row.board_height), - mines.size(), _mean(mines), _med(mines), _mean(safes), _med(safes), _mean(r0s), _mean(infs)]) + mines.size(), _mean(mines), _med(mines), _mean(safes), _med(safes), _mean(r0s), + _sd(r0s), _p90(r0s), _max(r0s), _mean(infs), _mean(cap_rejs)]) quit(0) @@ -69,6 +75,26 @@ func _mean(a: Array) -> float: return t / float(a.size()) +func _sd(a: Array) -> float: + var m := _mean(a) + var t := 0.0 + for v in a: + t += (float(v) - m) * (float(v) - m) + return sqrt(t / float(a.size())) + + +func _p90(a: Array) -> int: + var b := a.duplicate() + b.sort() + return int(b[mini(a.size() - 1, int(ceil(a.size() * 0.9)) - 1)]) + + +func _max(a: Array) -> int: + var b := a.duplicate() + b.sort() + return int(b[b.size() - 1]) + + func _med(a: Array) -> int: var b := a.duplicate() b.sort() diff --git a/game/smoke/smoke_all.gd b/game/smoke/smoke_all.gd index 5a3bbbe2..569c7410 100644 --- a/game/smoke/smoke_all.gd +++ b/game/smoke/smoke_all.gd @@ -360,7 +360,9 @@ func _smoke_adventure() -> void: cand_ok = false var seen := {} for c in cands: - var ckey := "%s|%s" % [String((c as Dictionary).get("kind", "")), String((c as Dictionary).get("tier", ""))] + # 身份键 = kind|tier|relic_id(迭代14① T4 断言基线复核——TP0 v2.2 relic 按 relic_id 分列 + # 不参与同 kind 去重〔_token_identity relic: 前缀〕,同 tier 三枚不同流派件=三个不同选项合法态) + var ckey := "%s|%s|%s" % [String((c as Dictionary).get("kind", "")), String((c as Dictionary).get("tier", "")), String((c as Dictionary).get("relic_id", ""))] if seen.has(ckey): cand_ok = false seen[ckey] = true @@ -1253,6 +1255,24 @@ func _smoke_board_generator() -> void: check(_board_dump(b2) == _board_dump(b3), "L%d 同种子两次生成逐格相同" % L) var b4 = board_generator.generate(L, 0, _mk_stream("board_gen", 778), _db) check(_board_dump(b2) != _board_dump(b4), "L%d 异种子产物不同(退化护栏)" % L) + # ---- 初始展开收窄批断言(迭代14① T4)---- + # ① 结构断言:R₀ ≤ cap(正式表行投值,合法性闸承载——出图必过);② r0_cap_rejected 尝试事实在位; + # ③ ring_removed 量级:剃雷域收窄后 ≤ 域内候选雷格数(5×5−出生 9=16——全域剃雷退役); + # ④ 闸压线行为:cap=9(出生保障面下限)时出图必 r0≤9 或 accept_worst 非法标记(三分出口诚实降级,玩家永不卡死) + var cap_val: int = int(row.get("r0_max_cap", 0)) + check(cap_val >= BoardData.SPAWN_AREA_CELLS, "L%d r0_max_cap 正式行投值在位(%d ≥ 出生保障面 9——板面 30%% 锚)" % [L, cap_val]) + check(int(b1.metrics.r0_count) <= cap_val, "L%d 结构断言 R₀=%d ≤ cap=%d(R₀≤cap 重掷闸承载)" % [L, int(b1.metrics.r0_count), cap_val]) + check(int(b1.metrics.get("r0_cap_rejected", -1)) >= 0, "L%d r0_cap_rejected 尝试事实指标在位(本图 %d 次触发)" % [L, int(b1.metrics.get("r0_cap_rejected", -1))]) + check(int(b1.metrics.get("ring_removed", 99)) <= (2 + int(row.get("shave_ring_radius", 1))) * (2 + int(row.get("shave_ring_radius", 1))) * 4 - 5, "L%d ring_removed=%d ≤ 剃雷域候选雷格数((3+2r)²−9——全域剃雷退役)" % [L, int(b1.metrics.get("ring_removed", 99))]) + var row9: Dictionary = row.duplicate() + row9["r0_max_cap"] = BoardData.SPAWN_AREA_CELLS + row9["max_gen_attempts"] = 4 + var stub9 := _BoardRowStub.new() + stub9.row = row9 + var b9 = board_generator.generate(1, 0, _mk_stream("board_gen", 909), stub9) + check(b9 != null and (int(b9.metrics.r0_count) <= BoardData.SPAWN_AREA_CELLS or b9.illegal_product), + "L1 cap 压线 9:出图 R₀=%d %s(三分出口诚实降级——超阈即 accept_worst 非法标记,玩家永不卡死)" % [int(b9.metrics.r0_count), "≤9" if int(b9.metrics.r0_count) <= BoardData.SPAWN_AREA_CELLS else ">9→illegal_product"]) + check(int(b9.metrics.get("r0_cap_rejected", 0)) >= 0 and String(b9.metrics.get("accept_mode", "")) != "", "L1 cap 压线出图指标集完整(accept_mode=%s / 闸触发 %d 次)" % [String(b9.metrics.get("accept_mode", "")), int(b9.metrics.get("r0_cap_rejected", 0))]) func _mk_stream(stream_name: String, scope: int) -> RandomNumberGenerator: @@ -1313,6 +1333,15 @@ class _CountVariantRng extends RngServiceCore: return super.open_stream(purpose) +# 板行覆盖桩(迭代14① T4——cap 压线行为断言;接口面 = resolve_board_gen 单点) +class _BoardRowStub: + var row: Dictionary + func resolve_board_gen(_L: int, _D: int) -> Dictionary: + return row + func status_effect_row(_id: String) -> Dictionary: + return {} + + func _smoke_global_variant() -> void: print("--- 全局作用域变异 首发批(门控三维 + 替换制 + 资源面偏斜)---") # ① 跨字段互锁负样本(resource_face ⟺ scope=global 三条 + effect_value 新域 [0,50]) @@ -3654,6 +3683,7 @@ func _smoke_shop_room() -> void: check(runner.effective_ac() == int(_db.hero.armor_class) + 1, "AC = 10+1(armor_class 加算区轴,无百分比通道)") # 余额不足购买被拒(零副作用):hp_up 连购至不可负担后兜底拒绝 var guard_drain := 20 + var hp_buys := 1 # 夹具前段已购 1 笔(购买 hp_up 受理断言)——总数 = 前段 + 本循环 while guard_drain > 0: guard_drain -= 1 var hp_price: int = int(_shop_products_by_id(runner.shop_products())["hp_up"]["price"]) @@ -3661,6 +3691,7 @@ func _smoke_shop_room() -> void: break if not runner.buy_product("hp_up"): break + hp_buys += 1 var gp_dry: int = runner.gp var tier_dry: int = runner.purchased_tier("hp_up") var hp_dry: int = int(_shop_products_by_id(runner.shop_products())["hp_up"]["price"]) @@ -3673,8 +3704,10 @@ func _smoke_shop_room() -> void: check(runner.effective_atk() == int(_db.hero.atk) + 1, "主角攻击生效值 = 2+1(基础表值 + 加算区)") check(runner.pick_next_node(9) and runner.current_battle() != null, "选 Boss(节点 9)开战") var snap: Dictionary = runner.current_battle().presentation_snapshot() - check(int(snap["hero"].get("atk", -1)) == int(_db.hero.atk) + 1 and int(snap["hero"].get("max_hp", -1)) == int(_db.hero.max_hp) + 3, - "战斗开始参数 = 基础表值 + 加算区(atk %d = 2+1 / max_hp %d = 17+3)" % [int(snap["hero"].get("atk", -1)), int(snap["hero"].get("max_hp", -1))]) + # max_hp 期望按实际购买笔数推导(迭代14① T4 断言基线复核——棋盘换代改变战局 GP 流,可购笔数 + # 随经济漂移;断言语义=「加算区笔数×3 全额入战斗开始参数」,不再锚死恰好 1 笔) + check(int(snap["hero"].get("atk", -1)) == int(_db.hero.atk) + 1 and int(snap["hero"].get("max_hp", -1)) == int(_db.hero.max_hp) + 3 * hp_buys, + "战斗开始参数 = 基础表值 + 加算区(atk %d = 2+1 / max_hp %d = %d+%d×3)" % [int(snap["hero"].get("atk", -1)), int(snap["hero"].get("max_hp", -1)), int(_db.hero.max_hp), hp_buys]) # 冒险收束(Boss 胜或败北——基线裸装口径任一结局)→ 钱包清零 var fin_end := {} runner.level_finished.connect(func(p): fin_end["p"] = p)