Merge branch 'master' into m3-iter10-20261001

# Conflicts:
#	game/smoke/level_autoplay.gd
#	game/smoke/smoke_all.gd
This commit is contained in:
DKChan committed 2026-10-01 18:18:03 +08:00
commit bc737fb0cb
24 files changed
+1471 -112

No files matched your search

+32 -24
View File
@@ -12,9 +12,9 @@ rows = [{
"unit_count": 1,
"unit_count_max": 2,
"hp": 8,
"atk": 2,
"atk_min": 1,
"atk_max": 3,
"atk": 1,
"atk_min": 0,
"atk_max": 2,
"pool": [{
"skill_id": "attack",
"weight": 9
@@ -32,9 +32,9 @@ rows = [{
"unit_count": 2,
"unit_count_max": 3,
"hp": 4,
"atk": 2,
"atk_min": 1,
"atk_max": 3,
"atk": 1,
"atk_min": 0,
"atk_max": 2,
"pool": [{
"skill_id": "attack",
"weight": 9
@@ -51,9 +51,9 @@ rows = [{
"display_name": "熊地精",
"unit_count": 1,
"hp": 50,
"atk": 2,
"atk_min": 1,
"atk_max": 3,
"atk": 1,
"atk_min": 0,
"atk_max": 2,
"pool": [{
"skill_id": "attack",
"weight": 6
@@ -74,9 +74,9 @@ rows = [{
"unit_count": 2,
"unit_count_max": 3,
"hp": 14,
"atk": 2,
"atk_min": 1,
"atk_max": 3,
"atk": 1,
"atk_min": 0,
"atk_max": 2,
"pool": [{
"skill_id": "attack",
"weight": 6
@@ -96,9 +96,9 @@ rows = [{
"display_name": "食人魔酋长",
"unit_count": 1,
"hp": 72,
"atk": 2,
"atk_min": 1,
"atk_max": 3,
"atk": 1,
"atk_min": 0,
"atk_max": 2,
"pool": [{
"skill_id": "attack",
"weight": 6
@@ -118,9 +118,9 @@ rows = [{
"display_name": "熊地精",
"unit_count": 1,
"hp": 50,
"atk": 2,
"atk_min": 1,
"atk_max": 3,
"atk": 1,
"atk_min": 0,
"atk_max": 2,
"pool": [{
"skill_id": "attack",
"weight": 6
@@ -141,9 +141,9 @@ rows = [{
"unit_count": 2,
"unit_count_max": 3,
"hp": 26,
"atk": 2,
"atk_min": 1,
"atk_max": 3,
"atk": 1,
"atk_min": 0,
"atk_max": 2,
"pool": [{
"skill_id": "attack",
"weight": 6
@@ -163,9 +163,9 @@ rows = [{
"display_name": "巨蜘蛛",
"unit_count": 2,
"hp": 28,
"atk": 2,
"atk_min": 1,
"atk_max": 3,
"atk": 1,
"atk_min": 0,
"atk_max": 2,
"pool": [{
"skill_id": "attack",
"weight": 6
@@ -564,3 +564,11 @@ rows = [{
; design/qa/m3-iter5-lever-scan-2026-09-28.md)。
; M-3 迭代 5 T4 AC 投值批·终版 Boss-only(2026-09-28:v1 硬梯度普通 L2 14.3% 越带/v2 软梯度 15.7% 仍越带——杂鱼税对普通画像〔普通分量为主力输出〕系统性超压;终版=杂鱼全 0〔带 0~5 下缘,掷点戏剧性集中 Boss 战〕+Boss 台阶 8/9/12 严格递增;三版演化链读数见 design/qa/m3-iter5-t4-ac-2026-09-28.md,随出口人闸追认)。
; M-3 迭代 5 返工批·U2 裁 v1 硬梯度重投(2026-09-29 用户推翻 Boss-only:重击手 8/重装手 9〔逐行手设值,非精英编码位——解读 A 追认,精英 P2 承载〕+Boss 台阶 8/10/12;杂鱼行按 T4 报告 §一 软梯度恢复〔v1 中间态未入 git,v1↔v2 唯一差异=重击手/重装手,代拟面记台账 247〕;卓尔 hp 100→130 随 C3 回退复原;组合读数 C1+v1 无先测,全量确认归返工批——裁断全录 design/qa/m3-iter5-u2-gate-2026-09-29.md)。
;
; 数值修复批·调参二(2026-10-01 新尺 MC 驱动):L1 系八行 atk 2→1(行 15/35/54/77/99/121/144/166;
; 行 234 巨蜘蛛巢群主体=L2 域维持)——新尺读数:普通档 v3.4 死因=L1 四连战无治疗夹缝(雷伤 6/8/8
; + 杂兵垫刀双压,L1 条件通关 1-2%,0/100 出第一关);完美档战役 50%(L1 93%)。刀位语义=
; 「危险来自雷,不来自杂兵」:杂兵垫刀减半,雷直伤 6/8/8 维持痛感;L2/L3(atk 3 系)本刀不动。
; 全旧带读数随新尺作废重测归本批。
; 调参二补充:上述八行 atk 区间 1-3→0-2(中点恒等期望当量锚不变式——atk=1 需区间中点同步 1;0=掷点空刀,保随机手感)。
+6 -1
View File
@@ -4,7 +4,7 @@
[resource]
script = ExtResource("1")
max_hp = 20
max_hp = 26
atk = 2
armor_class = 10
armor_type = "light"
@@ -30,3 +30,8 @@ attack_type = "normal"
; 触发 §6.2(L1 精英承伤 43-55% vs 靶 ≤20%、L1 净消耗 17.7/17=104% 破产、normal L1 通过率
; 8.3% vs 靶 70-78%)的生存侧修复。单变量归因原则:同轮第二变量(heal_minor 加强)在 reward.tres。
; 血线 17/27/37→20/30/40(关间大奖励 +10 不变)。provisional 未提交。
; 数值修复批(2026-10-01——模拟档位重构后新尺首调):max_hp 20→26(调参二再 +2:普通档 L1 7% 差 2-4 血量级,+8% 池把 b3/b4 压线死改写险胜;完美档 62% 上界余量健康)。依据=新尺 MC 读数:普通档 v3.4
; (完美+失误注入,踩雷 ~2.5/战)L1 四连战无治疗夹缝全灭(L1 条件通关 2%,0/100 出第一关);
; 完美档战役 36-41%(L2 53% 弱环)。+20% 血池抬夹缝生存;配套直伤雷当量同步上调(reward.tres
; 4/5/5→6/8/8,雷痛回归用户口径)双向对冲。全旧带读数随新尺作废重测归本批。
+6 -3
View File
@@ -82,7 +82,7 @@ rows = [{
"polarity": "negative",
"effect_id": "",
"display_name": "反噬震荡",
"power": 4,
"power": 6,
"weight": 0,
"pool_tag": "cell",
"tier": 1
@@ -146,7 +146,7 @@ rows = [{
"polarity": "negative",
"effect_id": "",
"display_name": "穿刺陷阱",
"power": 5,
"power": 8,
"weight": 0,
"pool_tag": "cell",
"tier": 1
@@ -156,7 +156,7 @@ rows = [{
"polarity": "negative",
"effect_id": "",
"display_name": "魔法反噬",
"power": 5,
"power": 8,
"weight": 0,
"pool_tag": "cell",
"tier": 1
@@ -300,3 +300,6 @@ rows = [{
;(区间 [18,35]→[24,40],(24+40)/2=32 恰为整中点锚)。boss 面三缺同解(normal 胜率 62% 贴下沿 /
; 清图率 49% vs ≥80 / 完成度 99 vs >130)——清图尖峰加大让"先清一图"成为 boss 战最优路径。
; 副作用已知:perfect L1 boss 胜率 97% 越 75% 上界加剧。provisional 未提交。
; 数值修复批(2026-10-01):直伤雷三行 4/5/5→6/8/8(迭代 8 雷当量 provisional 的再抬档)——用户口径
; 「踩雷必须痛」;配套 hero max_hp 20→24 双向对冲(见 hero.tres 批注)。新尺 MC 复测归本批。
+2 -2
View File
@@ -73,7 +73,7 @@ var review_overlay: SettlementReviewOverlay
var node_overlay: NodeOverlay
var shop_overlay: ShopOverlay # 商店覆盖层(GP+商店房实施批——商店房节点兑付呈现)
var _adventure_gp_view := 0 # GP 冒险钱包呈现侧影子(迭代 6 T7——终局面板 GP 行取数面,
# wallet_changed gain/buy 载荷=入账后总额 level_runner.gd:482/539)
# wallet_changed gain/buy 载荷=入账后总额 level_runner.gd:482/539)
var _adventure_gp_final := 0 # 收束清零拍暂存的最终合计("clear" 到达时自影子转存)
var _aux_use_selection := 0 # 魔法道具选中句柄(迭代 7 T4——0=未选中;选中期棋盘点击=使用目标拾取)
var _aux_unlocked := {} # aux 解锁门控集(迭代 7 T5——工坊解锁×活行交集,冒险开始组装)
@@ -610,7 +610,7 @@ func _on_battle_end_continue() -> void:
func _on_end_review() -> void:
review_overlay.open(log_view.parsed_text())
_open_modal(review_overlay) # 迭代 6 T7——走模态栈:终局层在册上方的可见遮挡被显式隐藏,
# 修复「回看层被终局层全屏 STOP 遮罩压死」(直开绕栈缺陷)
# 修复「回看层被终局层全屏 STOP 遮罩压死」(直开绕栈缺陷)
# ==================== 调试模式(连续两层——编排归 debug_session.gd;
+91 -10
View File
@@ -130,6 +130,10 @@ var _floor_tiles: Array[Texture2D] = [] # 地面瓦片五变体(下标 0..4
var _tiles_ok := false # false = 素材缺位/未装载——bg_sb 程序化平色原路径
var _flag_tex: Texture2D = null # 封印旗纹理(缺盘=null=字形「旗」回退——独立可选件不连瓦片整批)
var _tiles_board_type := "dungeon" # 当前装载瓦片套的 board_type(rebuild 按板切换——T6 换色批)
var _digit_dark_ink := true # 数字墨族按板自适应(true=深墨三档)。缺盘平色回退态 #B39A78 裸土
# 为中明度底——深墨族 CR 4.96 达标、亮墨族仅 2.2,故缺盘默认深墨;瓦片装载后按实测分位数覆写
var _digit_plate := false # 数字色牌垫底模式(双峰纹理板——直画两族皆不过门槛时落牌,
# 素牌+深墨 12:1 纹理免疫;2026-10-01 渲染帧取证 underdark 系深墨直画隐形后启用)
var cell_size: float: # 只读属性(ui_layout_probe 断言面——旧私有名 _cell 的契约化出口)
get:
@@ -647,9 +651,16 @@ func _badge_glyph(category: String) -> String:
# ---- 格态渲染(灰度截图可辨:完整覆尘 / 碎裂裸土 / 已开数字) ----
# 数字亮墨三档色温(圣经 v3.5 冻结④ 2026-09-30 批 B2——直画已开格底:1-3 冷亮/4-5 暖白/6-8 金亮,
# 色温承袭密度语义;实测对比 6.0–6.3:1。旧深墨三档转色牌后备路径专用〔牌上深墨族 12:1〕)
# 数字三档色温(按板自适应双族——v3.5 冻结④ 修订 2026-10-01):亮墨族 1-3 冷亮/4-5 暖白/6-8 金亮
#(dungeon 瓦片实测达标维持);深墨族 1-3 青墨/4-5 赭墨/6-8 锈墨(色温语义同构——underdark/
# deep_underdark 亮白筋纹瓦片上亮墨对亮斑 CR 1.0–1.9 融字,深墨族 5.7–11.1;选族判据见 _digit_ink_mode)
func _digit_ink(d: int) -> Color:
if _digit_dark_ink:
if d <= 3:
return UiKit.INK_COLD
if d <= 5:
return UiKit.INK_OCHRE
return UiKit.INK_RUST
if d <= 3:
return UiKit.INK_L_COLD
if d <= 5:
@@ -697,16 +708,20 @@ func _paint_cell(c: Vector2i, hero: Vector2i) -> void:
# 瓦片模式(高墙接线批 2026-09-26):tex_base = 查表瓦片(未开=墙瓦片/已开=地面瓦片),
# bg_sb 降级为透明描边载体——只承载呼吸微光/主角蓝框(CellUnit 瓦片态画在瓦片之上),
# 程序化平色退役;瓦片分配=格坐标哈希,初始化落定后逐拍稳定(禁逐轮重掷——圣经原则三)
var sb: StyleBoxFlat
# draw calls 批优化(2026-10-01):透明载体 sb 视觉贡献=零(透明底+零描边——不可见),但
# StyleBoxFlat 多边形命令不可合批,每格打断棋盘批——实测 100 格满负荷帧 ~402 DC(A/B 探针
# 阶梯:纹理统一 0 增益/去 glyph −23/去叠层 −23/去隐形 sb −402)。非英雄格 sb 直接置 null
#(绘制路径零像素变化),仅英雄格保留蓝框载体。
var sb: StyleBoxFlat = null
var tile: Texture2D = null
if _tiles_ok:
# Color.TRANSPARENT 载体(引擎常量——视图禁内联色字面量纪律;透明+零描边=瓦片上不画任何东西)
sb = UiKit.panel_flat(Color.TRANSPARENT, Color.TRANSPARENT, 3, 0)
tile = _floor_tile_for(c) if revealed else _wall_tile_for(c)
else:
# 素材整批缺位回退 = 现行程序化平色路径(fresh checkout 冒烟不炸——任务书 §2.2)
sb = UiKit.panel_flat(bg, bg.darkened(0.15), 3)
if is_hero:
if sb == null:
sb = UiKit.panel_flat(Color.TRANSPARENT, Color.TRANSPARENT, 3, 0)
sb.set_border_width_all(3)
sb.border_color = COL_HERO
if glyph == "" and _hero_tex_idle == null:
@@ -717,7 +732,13 @@ func _paint_cell(c: Vector2i, hero: Vector2i) -> void:
u.bg_sb = sb
u.glyph = glyph
u.glyph_color = digit_col
u.plate = revealed and (glyph == "雷" or glyph == "?") # 色牌后备(v3.5 批 B2——数字直画退牌;雷/「?」维持牌载保读性;复杂格底〔物件纹样/装饰变体格〕判定随装饰瓦片自产批扩)
# 色牌后备(v3.5 批 B2——数字直画退牌;雷/「?」维持牌载保读性)+ 2026-10-01 v3.4:双峰纹理板
#(_digit_plate——underdark 系亮白筋+近黑斑并存的直画不可救纹理)数字落牌,牌上深墨 12:1
var plate_digit := _digit_plate and glyph != "" and glyph != "雷" and glyph != "?" and glyph != "旗" and glyph != "人"
if plate_digit:
digit_col = COL_DIGIT # 牌上深墨族(素牌 12:1——双峰纹理免疫)
u.glyph_color = digit_col
u.plate = revealed and (glyph == "雷" or glyph == "?" or plate_digit)
u.outline_sides.assign([]) # typed Array[int](代码审查批 3 类型收紧——assign 保持引用语义)
u.facade_south = false
u.proj_band = 0.0
@@ -867,9 +888,14 @@ func _load_board_tiles(board_type: String = "dungeon") -> void:
return
# 地板归一(圣经 v3.5 冻结①/§1.4 样张 v3 记录——已开格底归一世界底层档 L≈0.10,带 0.08–0.14):
# 方向稿 _v3_darken_floors 同式移植;现行瓦片 L≈0.036 低于带下沿故实为提亮;素材重产归资产批,
# 本归一为程序代役(逐批装载时对 freshly-loaded 原图测光,幂等)
# 本归一为程序代役(逐批装载时对 freshly-loaded 原图测光,幂等)。归一后逐像素明度顺路采集,
# 驱动数字墨族按板自适应(_digit_ink_mode——均值分位同源同像素,禁二次解码)
var lumas: Array = []
for i in range(floors.size()):
floors[i] = _normalize_floor_luma(floors[i])
floors[i] = _normalize_floor_luma(floors[i], 0.10, lumas)
var ink_mode := _digit_ink_mode(lumas)
_digit_dark_ink = bool(ink_mode.dark)
_digit_plate = bool(ink_mode.plate)
# 六镜像件(词表 §二:wall_e←wall_w / wall_ne←wall_nw / wall_se←wall_sw /
# wall_end_e←wall_end_w / wall_in_ne←wall_in_nw / wall_in_se←wall_in_sw;只水平翻转禁旋转
# ——北受光不受损)
@@ -907,7 +933,7 @@ static func _flip_tex(tex: Texture2D) -> Texture2D:
# 地板瓦片明度归一(v3.5 批 B1——方向稿 _v3_darken_floors 同式移植):8×8 双线性重采样均值测光
# (sRGB→线性域,Rec.709 系数),增益 k=(目标/实测)^(1/2.2) 钳 [0.6,1.8],逐通道 sRGB 域线性拉伸。
# 输入恒为 freshly-loaded 原图(load_tex 每次盘读)——幂等可重入;32×32 五变体开销可忽略
static func _normalize_floor_luma(tex: Texture2D, target_l: float = 0.10) -> Texture2D:
static func _normalize_floor_luma(tex: Texture2D, target_l: float = 0.10, lumas_out = null) -> Texture2D:
var img: Image = tex.get_image()
if img.is_compressed():
img.decompress()
@@ -924,7 +950,12 @@ static func _normalize_floor_luma(tex: Texture2D, target_l: float = 0.10) -> Tex
for y in range(img.get_height()):
for x in range(img.get_width()):
var c := img.get_pixel(x, y)
img.set_pixel(x, y, Color(minf(c.r * k, 1.0), minf(c.g * k, 1.0), minf(c.b * k, 1.0), c.a))
var nr := minf(c.r * k, 1.0)
var ng := minf(c.g * k, 1.0)
var nb := minf(c.b * k, 1.0)
img.set_pixel(x, y, Color(nr, ng, nb, c.a))
if lumas_out != null:
lumas_out.append(0.2126 * _srgb_lin(nr) + 0.7152 * _srgb_lin(ng) + 0.0722 * _srgb_lin(nb))
return ImageTexture.create_from_image(img)
@@ -932,6 +963,56 @@ static func _srgb_lin(c: float) -> float:
return c / 12.92 if c <= 0.04045 else pow((c + 0.055) / 1.055, 2.4)
# ---- 数字墨族按板自适应(L2/L3 亮瓦融字缺陷修复 2026-10-01——甲案:墨色按关自适应+三关对比断言)----
const DIGIT_INK_CR_P90 := 4.5 # 墨族对地板 p90 亮斑对比度门槛(失效模式=亮白筋纹冲字,WCAG 数字体同源)
const DIGIT_INK_CR_MEAN_BRIGHT := 2.5 # 亮族对地板均值门槛(中调防吃字)
const DIGIT_INK_CR_MEAN_DARK := 3.0 # 深族对地板均值门槛(大字 AA 级——2.4 的实测观感=隐形,v3.4 收紧)
# 归一后地板逐像素明度 → 选数字呈现模式。三态:亮族直画(暗纹理板)/深族直画(中暗板)/
# 色牌垫底(双峰纹理板——近黑斑与近白筋并存时任何墨族直画必融一峰,2026-10-01 渲染帧取证
# 实证 underdark 系深墨隐形,牌上深墨 12:1 纹理免疫)。家族最弱成员(亮族=金亮/深族=锈墨)
# 对 p90 与均值双门槛;亮族过选亮族,否则深族(均值门 3.0),两族皆不过落牌。
# 实测(2026-10-01):dungeon p90=0.121 亮族 4.79/5.80 ✓ 直画;underdark p90=0.406 亮族 ✗、
# 深族 5.34/**2.41**(均值 <3.0 ✗)→ 落牌;deep_underdark 同→落牌
static func _digit_ink_mode(lumas: Array) -> Dictionary:
var sorted := lumas.duplicate()
sorted.sort()
var n := sorted.size()
if n == 0:
return {"dark": false, "plate": false, "cr_p90_bright": 0.0, "cr_mean_bright": 0.0, "cr_p90_dark": 0.0, "cr_mean_dark": 0.0}
var p90: float = sorted[int(n * 0.9)]
var mean := 0.0
for v in sorted:
mean += v
mean /= n
var cr_p90_b := _contrast_y(UiKit.INK_L_GOLD, p90)
var cr_mean_b := _contrast_y(UiKit.INK_L_GOLD, mean)
var cr_p90_d := _contrast_y(UiKit.INK_RUST, p90)
var cr_mean_d := _contrast_y(UiKit.INK_RUST, mean)
var bright_ok := cr_p90_b >= DIGIT_INK_CR_P90 and cr_mean_b >= DIGIT_INK_CR_MEAN_BRIGHT
var dark_ok := cr_p90_d >= DIGIT_INK_CR_P90 and cr_mean_d >= DIGIT_INK_CR_MEAN_DARK
var plate := false
var dark := false
if bright_ok:
dark = false
elif dark_ok:
dark = true
else:
plate = true # 双峰纹理:直画不可救,色牌兜底(素牌+深墨 12:1)
return {"dark": dark, "plate": plate, "cr_p90_bright": cr_p90_b, "cr_mean_bright": cr_mean_b,
"cr_p90_dark": cr_p90_d, "cr_mean_dark": cr_mean_d}
static func _contrast_y(ink: Color, bg: float) -> float:
var y := _ink_y(ink)
return (maxf(y, bg) + 0.05) / (minf(y, bg) + 0.05)
static func _ink_y(c: Color) -> float:
return 0.2126 * _srgb_lin(c.r) + 0.7152 * _srgb_lin(c.g) + 0.0722 * _srgb_lin(c.b)
# 格坐标确定性散列(禁 RNG/禁 randomize 纪律——「初始化随机落定禁逐轮重掷」圣经原则三落点:
# 同格坐标恒同值,refresh/重绘/逐轮不变)。乘加混合 + 移位雪崩;64 位整型乘法回绕即哈希语义
#(GDScript int 溢出静默回绕),板域 10×10/12×12 内 mod 10/mod 5 分布均匀
+5 -4
View File
@@ -7,11 +7,12 @@ extends RefCounted
# ── 色板(语义族;值 = 2026-09-20 收敛时现值) ──
const INK := Color8(51, 38, 27) # 墨族——正文/数字(对格底 ≥4.5:1)
const INK_COLD := Color8(19, 46, 54) # 青墨 1-3 #132E36(数字墨三档·冷=稀疏——样张 v2 定稿 2026-09-28;v3.5 起转色牌后备路径专用)
const INK_OCHRE := Color8(60, 37, 12) # 赭墨 4-5 #3C250C(数字墨三档·中;v3.5 起转色牌后备路径专用)
const INK_RUST := Color8(74, 30, 20) # 锈墨 6-8 #4A1E14(数字墨三档·暖=密集;v3.5 起转色牌后备路径专用)
const INK_COLD := Color8(19, 46, 54) # 青墨 1-3 #132E36(数字墨三档·冷=稀疏——样张 v2 定稿 2026-09-28;亮瓦自适应深墨族档兼色牌后备路径)
const INK_OCHRE := Color8(60, 37, 12) # 赭墨 4-5 #3C250C(数字墨三档·中;亮瓦自适应深墨族档兼色牌后备路径)
const INK_RUST := Color8(74, 30, 20) # 锈墨 6-8 #4A1E14(数字墨三档·暖=密集;亮瓦自适应深墨族档兼色牌后备路径)
# ── 亮墨三档(圣经 v3.5 冻结④ 2026-09-30——数字直画已开格底:色温承袭密度语义,冷亮/暖白/金亮;
# 实测对比 6.0–6.3:1(A3a);金亮低饱和避奖励金语义;色牌后备组用深墨族上牌〔12:1〕)──
# 实测对比 6.0–6.3:1(A3a);金亮低饱和避奖励金语义;色牌后备组用深墨族上牌〔12:1〕。
# 2026-10-01 修订:亮墨族限暗纹理板(dungeon),亮筋纹板按 _digit_ink_mode 自适应切深墨三档)──
const INK_L_COLD := Color8(216, 236, 240) # 冷亮 1-3 #D8ECF0
const INK_L_WARM := Color8(240, 226, 200) # 暖白 4-5 #F0E2C8(关卡屏 WARM 同族)
const INK_L_GOLD := Color8(248, 227, 168) # 金亮 6-8 #F8E3A8
+42
View File
@@ -208,3 +208,45 @@ static func _row_major(cells: Dictionary) -> Array:
var out: Array = cells.keys()
out.sort_custom(func(a, b): return a.y * 1000 + a.x < b.y * 1000 + b.x)
return out
# 卡死赌挖选格(普通档 v3.1 2026-10-01——模拟档位重构):概率启发近似真人「挑看起来最安全的一格」。
# 危险度 = 该格对每个邻接已证数字约束取(余雷数/未知数)比值中的最小值(= 任一邻数字视角下的
# 最低雷占比——single 已证安全格在进入本函数前已被 perfect_action 消化,故卡死态比值恒 >0);
# 同危险度行主序先到先得。纯查询面零 RNG(确定性保回放);寻路同 choose_dig(目标已开 8 邻
# 首个可达者为 hero_cell)。
static func gamble_pick(minesweeper) -> Dictionary:
var cons := _constraints(minesweeper, minesweeper.current_board(), {})
var best_h := 2.0
var best := Vector2i(-1, -1)
var revealed: Array = minesweeper.revealed_cells()
var revealed_set := {}
for c in revealed:
revealed_set[c] = true
var board = minesweeper.current_board()
for y in board.height:
for x in board.width:
var c := Vector2i(x, y)
if minesweeper.is_revealed(c) or minesweeper.has_flag(c):
continue
var adj := false
for n in board.neighbors8(c):
if revealed_set.has(n):
adj = true
break
if not adj:
continue
var h := 1.0
for con in cons:
if (con["unknown"] as Dictionary).has(c):
var ratio := float(con["mines"]) / float((con["unknown"] as Dictionary).size())
h = minf(h, ratio)
if h < best_h - 0.0001:
best_h = h
best = c
if best.x < 0:
return {}
for n in board.neighbors8(best):
if minesweeper.is_revealed(n) and (n == minesweeper.hero_pos() or not minesweeper.compute_move_path(n).is_empty()):
return {"dig_cell": best, "hero_cell": n}
return {}
+62
View File
@@ -0,0 +1,62 @@
# VERTICAL SLICE - NOT FOR PRODUCTION
# 棋盘渲染帧取证探针(2026-10-01——数字墨族按板自适应修复的目视验证面:机检≠看得清)。
# 必须**窗口化**运行(headless 无渲染面):
# Godot_console.exe --path . res://smoke/board_shot_probe.tscn -- <输出目录>
# 流程:L1/L2/L3 各起一板(真实 LevelRunner+settle_batch 驱动 ~18 步 novice——自然揭示数字/旗),
# BoardView 真渲染,frame_post_draw 同步后抓视口 PNG。零存档触碰(无 save_manager 引用)。
extends Node
const AutoPlayer := preload("res://smoke/auto_player.gd")
func _ready() -> void:
var args := OS.get_cmdline_user_args()
var out := args[0] if args.size() >= 1 and args[0] != "" else "user://board-shots"
DirAccess.make_dir_recursive_absolute(out)
await get_tree().process_frame
var db = load("res://core/config_db.gd").new()
var ok: bool = db.load_tables("res://data/tables", true)
if not ok:
push_error("[board_shot] 表加载失败")
get_tree().quit(1)
return
var ui_font: Font = load("res://presentation/views/ui_kit.gd").load_font(load("res://presentation/views/ui_kit.gd").FONT_UI)
var targets := [[1, "L1_dungeon"], [2, "L2_underdark"], [3, "L3_deep_underdark"]]
for t in targets:
var level_index: int = t[0]
var tag: String = t[1]
var rng = load("res://core/rng_service.gd").new()
rng.set_master_seed(420000 + level_index)
var runner = load("res://core/level_runner.gd").new(db, rng)
if not runner.start_level(level_index, 420000 + level_index, {}, 0):
push_error("[board_shot] 开关失败 level=%d" % level_index)
get_tree().quit(1)
return
var settlement = runner.current_battle()
for i in range(18):
var act: Dictionary = AutoPlayer.novice_action(settlement.minesweeper)
var kind := String(act.get("kind", "stop"))
if kind == "stop":
break
var op := kind
if kind == "flag":
op = "flag_place"
settlement.minesweeper.settle_batch([{"action": op, "cell": act.get("cell")}])
var view = load("res://presentation/views/board_view.gd").new()
view.size = Vector2(900, 900)
view.position = Vector2(40, 40)
if ui_font != null:
view.set_world_font(ui_font)
add_child(view)
view.rebuild(settlement.minesweeper.current_board(), settlement.minesweeper)
for i in range(6):
await get_tree().process_frame
await RenderingServer.frame_post_draw
var img := get_viewport().get_texture().get_image()
var path := "%s/board_%s.png" % [out, tag]
img.save_png(path)
print("[board_shot] saved %s (%dx%d)" % [path, img.get_width(), img.get_height()])
view.queue_free()
await get_tree().process_frame
print("[board_shot] done")
get_tree().quit(0)
+6
View File
@@ -0,0 +1,6 @@
[gd_scene load_steps=2 format=3]
[ext_resource type="Script" path="res://smoke/board_shot_probe.gd" id="1"]
[node name="BoardShotProbe" type="Node"]
script = ExtResource("1")
+155 -12
View File
@@ -15,15 +15,31 @@
# 驱动循环按 route 策略显式 pick_next_node;route = "min"/"max"(候选节点编号最小/最大——
# 确定性路径轴,零随机流自开;默认 "min" 既有调用方签名兼容)。
# 2026-09-21 冒险结构实现批:run_adventure 三关连探驱动(AdventureRunner——主种子恰一次、
# 2026-09-21 冒险结构实现批:run_adventure 三关连探驱动(AdventureRunner——主种子恰一次、
# 通关兑付停点确认、adventure_finished 置 result/summary;外层每关重取 runner 实例经 _drive_level
# 复用单关驱动)。run_level 驱动段抽成 _drive_level——签名与返回结构零变化,行为逐位不变。
# 2026-10-01 迭代 10 奖励重构批:大奖励立即确认改三选一停点驱动(候选取首件/替换放弃新件)。
# 2026-10-01 模拟档位重构(master 数值修复批——普通档 v2 被真人实证推翻,尺子比真人弱 20 倍):
# ①普通档 v3 = 完美策略 + 受控失误注入(失误流走独立策略 RNG——种子派生自局种子,
# 逐局确定性/可回放保持,核心七流零消费零扰动);novice_action 留库不删(历史读数对账面)。
# ②完美档修上界:选路目标价值评分(rest 伤重优先/shop 有钱优先/boss 缓冲)+ 商店贪心买齐。
# ③route 轴保持:评分同分 tie-break 仍按 min/max 编号方向——verifier 双窗轴不塌。
# 运行前提:db 已 load_tables;run_level/run_adventure 均须全新 RngService(同种子 ⇒ 全程逐步可回放)。
# 实现注:信号处理体全部为静态方法 + bind 传状态——GDScript 多行 lambda 内嵌 match/控制流的解析不可靠。
extends RefCounted
const AutoPlayer := preload("res://smoke/auto_player.gd") # O4 收口:自动玩家策略单一实现
# ── 普通档 v3 失误注入参数(2026-10-01 用户规格:踩雷期望=完美+2/板、每板少挖几个正确格)──
# 标雷→误挖转化率:v3.2 实证 0.22 时普通档 0/100 全灭(首轮大量已证旗=保底命中,非真人失误
# 形态——真人几乎不挖自己插旗的格);降 0.10 = 偶发真手滑,主踩雷源=卡死赌挖(不确定性概率命中)
const NORMAL_MINE_SLIP_P := 0.10
# 安全挖跳过率:每板安全格 ~70-90 × p ≈ 3-5 次漏挖(信息与推进双损失)
const NORMAL_SKIP_DIG_P := 0.05
# 空闲提前赌挖率(v3.4):已证安全格在手时小概率先赌一格——完美系把不确定性攒到残局
# (残局 hazard→1=扫雷),真人早赌滚信息(低 hazard 期吃掉不确定性);p×~40 安全 AP/板 ≈ 4 次
const NORMAL_IDLE_GAMBLE_P := 0.10
static func run_level(db, seed_val: int, level_index: int, atk_pct: int, hp_pct: int, collect_rows: bool = false, logger = null, policy: String = "normal", route: String = "min", template_pick: int = -1) -> Dictionary:
var own_session := logger == null
@@ -42,6 +58,7 @@ static func run_level(db, seed_val: int, level_index: int, atk_pct: int, hp_pct:
"boss_depth": -1, "hp_at_boss": -1, "rows": [], "timeout": false,
"log_path": "", "policy": policy, "refreshes": 0, "mine_hits": 0,
"visited_ids": [], "choices": 0, "last_battle_enemies": [],
"prng": _policy_rng(seed_val), "mine_slips": 0, "skip_digs": 0, "shop_buys": 0,
}
runner.step_row.connect(_on_step_row.bind(runner, st, collect_rows))
runner.node_entered.connect(_on_node_entered.bind(runner, st, logger))
@@ -80,7 +97,7 @@ static func _drive_level(runner, st: Dictionary, logger, policy: String, route:
# 选路态(入口停点 / 兑付确认 / 战斗胜利后的零自动推进停点)——route 策略显式选下一跳
if runner.node_index >= 0:
st.choices = int(st.choices) + 1
runner.pick_next_node(_route_pick(runner, route))
runner.pick_next_node(_route_pick(runner, route, st))
continue
guard_battles -= 1
var this_settlement = runner.current_battle()
@@ -118,6 +135,8 @@ static func run_adventure(db, seed_val: int, atk_pct: int, hp_pct: int, collect_
"visited_ids": [], "choices": 0, "last_battle_enemies": [],
"summary": {}, "wallet_events": [], "encounter_max_hp_seq": [], "reward_events": [],
"shop_gp_seq": [], # 商店房到达余额序(逐关 1 次——GP 跨关延续验收取数面)
"prng": _policy_rng(seed_val), "mine_slips": 0, "skip_digs": 0, "shop_buys": 0,
"adv": true, # v3 档位行为开关(选路评分/失误注入/商店购买只挂冒险测量面——单关夹具保持旧语义零扰动)
"level_templates": [], # 逐关图模板名(关卡节点图生成 v0.4 分层出数)
}
@@ -192,7 +211,8 @@ static func _on_adventure_node_entered(node: Dictionary, info: Dictionary, adr,
r.continue_after_node()
elif t == "shop":
st.shop_gp_seq.append(int(info.get("gp", -1)))
r.continue_after_node() # 商店房零购离开(自动路径直通 Boss——购买行为归冒烟专项/真人试玩)
_autoplay_shop(r, st) # 贪心买齐(2026-10-01——旧零购离开退役:GP 局内死钱,真人实证两店全购)
r.continue_after_node()
if logger != null:
logger.log_node(node, info, r.node_index)
@@ -233,6 +253,28 @@ static func _on_adventure_bonus_resolved(result: Dictionary, st: Dictionary, adr
st.result = "advance_failed"
# 商店贪心买齐(2026-10-01 模拟档位重构):GP 局内死钱——买得起就买( affordable 集里挑最便宜的
# 反复买到达不上任何商品为止,guard 防死循环)。确定性(零随机流);购买出账/加算区入账全走
# runner.buy_product 核心编排(与呈现层购买同一出账路径,wallet_changed 信号照常采集)。
static func _autoplay_shop(r, st: Dictionary) -> void:
var guard := 30
while guard > 0:
guard -= 1
var affordable: Array = []
for p in r.shop_products():
if r.gp >= int(p.get("price", 1 << 30)):
affordable.append(p)
if affordable.is_empty():
return
var pick: Dictionary = affordable[0]
for p in affordable:
if int(p.get("price", 1 << 30)) < int(pick.get("price", 1 << 30)):
pick = p
if not r.buy_product(String(pick.get("product_id", ""))):
return
st.shop_buys = int(st.get("shop_buys", 0)) + 1
# 冒险终局(completed/failed/abandoned——result 载 summary.result,外层驱动循环随之收敛)
static func _on_adventure_finished(summary: Dictionary, st: Dictionary, logger) -> void:
st.summary = summary.duplicate(true)
@@ -241,14 +283,58 @@ static func _on_adventure_finished(summary: Dictionary, st: Dictionary, logger)
logger.log_level_end(summary)
# 路径轴策略(确定性——候选编号最小/最大;min 侧 = 岔路恒取小编号支)。
# 编号取 runner.next_candidate_ids()(编号空间解析单一出口——单链下标 / 图 node_depth)
static func _route_pick(runner, route: String) -> int:
# 策略流 RNG(2026-10-01 模拟档位重构):种子派生自局种子 × 黄金比率常数 + 偏移——与核心统一流
# (rng_service 单盐派生)零共享零扰动;逐局确定性保持(同种子 ⇒ 同决策序列 ⇒ 同回放)
static func _policy_rng(seed_val: int) -> RandomNumberGenerator:
var prng := RandomNumberGenerator.new()
prng.seed = seed_val * 2654435761 + 912931
return prng
# 路径轴选路(2026-10-01 目标价值评分制——机械最小编号退役):候选按节点类型评分取最高,
# 同分 tie-break 沿 route 方向(min=小编号/max=大编号——verifier 双窗轴保持)。
# 评分语义(确定性,零随机流):rest=伤重高值(缺口比例×100)/shop=买得起高值(GP 局内死钱,
# 真人实证两店全购)/reward=占位低值(MC 夹具解锁空集=零兑付直通)/boss=缓冲低值(有店有_rest
# 先补给再打)/battle=基线。编号取 runner.next_candidate_ids()(编号空间解析单一出口)。
static func _route_pick(runner, route: String, st: Dictionary) -> int:
var ids: Array = runner.next_candidate_ids()
if ids.is_empty():
return -1
ids.sort()
return ids[0] if route != "max" else ids[ids.size() - 1]
if not bool(st.get("adv", false)): # 单关机制夹具:旧机械选路保持(smoke 断言零扰动)
ids.sort()
return int(ids[0]) if route != "max" else int(ids[ids.size() - 1])
var nodes: Array = runner.next_candidates()
var scored: Array = []
for i in nodes.size():
var node: Dictionary = nodes[i]
var t := String(node.get("type", ""))
var s := 30.0
if t == "rest":
var max_hp := float(runner.effective_max_hp())
s = 5.0 + (1.0 - float(runner.hero_hp) / maxf(max_hp, 1.0)) * 100.0 # 满血 rest=5 低于 battle(白 rest 不去);伤重可到 105
elif t == "shop":
s = 10.0
var cheapest := 1 << 30
for p in runner.shop_products():
cheapest = mini(cheapest, int(p.get("price", 1 << 30)))
if cheapest < (1 << 30) and runner.gp >= cheapest:
s = 80.0
elif t == "reward":
s = 5.0
elif t == "boss":
s = 20.0
scored.append({"id": int(ids[i]), "s": s})
var best: Dictionary = scored[0]
for i in range(1, scored.size()):
var c: Dictionary = scored[i]
var better: bool
if absf(c.s - best.s) > 0.0001:
better = c.s > best.s
else:
better = (c.id < best.id) if route != "max" else (c.id > best.id)
if better:
best = c
return int(best.id)
static func _on_step_row(row: Dictionary, _runner, st: Dictionary, collect_rows: bool) -> void:
@@ -293,7 +379,8 @@ static func _on_node_entered(node: Dictionary, info: Dictionary, runner, st: Dic
st.hp_before_rest = int(info.get("hp", 0)) - st.rest_healed
runner.continue_after_node()
elif t == "shop":
runner.continue_after_node() # 商店房零购离开(GP+商店房批——自动路径直通 Boss;购买行为归冒烟商店专项断言/真人试玩)
runner.continue_after_node() # 单关机制夹具(run_level)零购离开保持——smoke 商店/战斗断言面
# 不掺购买行为;冒险口径(run_adventure)已在 _on_adventure_node_entered 改贪心买齐
if logger != null:
logger.log_node(node, info, runner.node_index)
@@ -312,9 +399,10 @@ static func _autoplay_battle(runner, settlement, st: Dictionary, logger) -> void
if String(st.get("policy", "normal")) == "perfect":
_autoplay_battle_perfect(runner, settlement, st, logger)
return
# 普通策略档 v2(2026-09-19 用户拍板):基础定式画像——单步决策循环(move/dig/flag 经 settle_batch 提交,
# 推理分级见 auto_player.gd 普通策略档节:新手级单数字满足规则 + 盲挖边界兜底、无 refresh);
# 旧"无推理挖最近格"口径随批退役。连续拒绝 ≥3 = 编排违约防御(同完美档 fail-fast 语义)。
# 普通策略档 v3(2026-10-01 用户重定义):完美策略 + 受控失误注入——v2「新手级单步推理+盲挖
# 兜底」画像被真人实证推翻(零加成真人首通三关踩 23 雷 vs v2 普通档 L2 条件通关率 10.6%)。
# v3 语义 = 会玩的普通玩家:推理与完美档同源(subset 解算/标旗/卡死刷新),失误只注入两手——
# 标雷误挖(NORMAL_MINE_SLIP_P)+ 安全挖跳过(NORMAL_SKIP_DIG_P)。连续拒绝 ≥3 = 编排违约防御。
var round_guard := 60
while st.result == "" and runner.current_battle() == settlement and settlement.battle_result == "" and round_guard > 0:
round_guard -= 1
@@ -325,7 +413,11 @@ static func _autoplay_battle(runner, settlement, st: Dictionary, logger) -> void
while runner.current_battle() == settlement and settlement.battle_result == "" and settlement.action_economy.phase == "Active" \
and settlement.round_index == start_round and action_guard > 0 and rejects < 3:
action_guard -= 1
var act := AutoPlayer.novice_action(settlement.minesweeper)
var act: Dictionary
if bool(st.get("adv", false)):
act = _apply_normal_slips(AutoPlayer.perfect_action(settlement.minesweeper, settlement), settlement, st) # v3:完美+失误
else:
act = AutoPlayer.novice_action(settlement.minesweeper) # 单关机制夹具:v2 新手画像保持
var kind := String(act.get("kind", "stop"))
if kind == "stop":
break
@@ -336,7 +428,13 @@ static func _autoplay_battle(runner, settlement, st: Dictionary, logger) -> void
ok = _submit_intent(settlement, "dig", act.get("cell"), logger, start_round)
elif kind == "flag":
ok = _submit_intent(settlement, "flag_place", act.get("cell"), logger, start_round)
elif kind == "refresh":
ok = bool(settlement.player_request_refresh())
if logger != null:
logger.log_action("refresh", Vector2i(-1, -1), ok, {"round": start_round})
if ok:
if kind == "refresh":
st.refreshes = int(st.refreshes) + 1
rejects = 0
else:
rejects += 1
@@ -346,9 +444,54 @@ static func _autoplay_battle(runner, settlement, st: Dictionary, logger) -> void
logger.log_action("stop", Vector2i(-1, -1), _accepted(out_s), {"round": start_round})
# 普通档 v3 失误注入(2026-10-01 用户规格):三类——
# ①标雷误挖:把「标旗已证雷」动作直接转成「挖该格」(真人手滑原型:知道是雷、位置点错,
# 踩雷吃伤且该雷奖励报废);期望踩雷增量 ≈ 每板已证雷数 × NORMAL_MINE_SLIP_P(目标完美+2/板)
# ②安全挖跳过:把「挖已证安全格」动作转成本轮停手(少挖几个正确的雷——信息与推进双损失)
# ③卡死赌挖:完美档无已证动作时刷新/停手拖轮次吃敌方垫刀(实证=完美档 41% 通关全败于慢玩
# 垫刀非踩雷)——真人会按概率赌一个边界格(用户实证口径「有些地方因为我看不到数字,也是盲挖
# 的」);普通档不刷新(用户自报全程没用刷新),赌挖替代刷新/停手
# 失误流 = st.prng(策略流 RNG,种子派生自局种子——同种子逐位可回放;核心七流零消费零扰动)
static func _apply_normal_slips(act: Dictionary, settlement, st: Dictionary) -> Dictionary:
var prng: RandomNumberGenerator = st.get("prng")
if prng == null:
return act
var kind := String(act.get("kind", "stop"))
if kind == "flag" and prng.randf() < NORMAL_MINE_SLIP_P:
st.mine_slips = int(st.get("mine_slips", 0)) + 1
return {"kind": "dig", "cell": act.get("cell")}
if kind == "dig":
var roll := prng.randf()
if roll < NORMAL_SKIP_DIG_P:
st.skip_digs = int(st.get("skip_digs", 0)) + 1
return {"kind": "stop"}
if roll < NORMAL_SKIP_DIG_P + NORMAL_IDLE_GAMBLE_P:
var idle := AutoPlayer.gamble_pick(settlement.minesweeper)
if not idle.is_empty():
return _gamble_action(idle, settlement)
if kind == "refresh" or kind == "stop":
var plan := AutoPlayer.gamble_pick(settlement.minesweeper) # v3.2:概率启发赌挖(choose_dig 行主序=概率最差格,实证 199/200 死第二战——退役)
if not plan.is_empty():
return _gamble_action(plan, settlement)
return act
# 赌挖动作组装(v3.4 抽出):目标邻接=直接挖,否则先移到目标的已开邻格(下一拍再挖)
static func _gamble_action(plan: Dictionary, settlement) -> Dictionary:
var ms = settlement.minesweeper
var hero: Vector2i = ms.hero_pos()
var target: Vector2i = plan.dig_cell
var d := Vector2i(absi(target.x - hero.x), absi(target.y - hero.y))
if maxi(d.x, d.y) == 1:
return {"kind": "dig", "cell": target}
return {"kind": "move", "cell": plan.hero_cell}
# 完美策略档战斗驱动(2026-09-14):单步决策循环——move/dig/flag 经 排雷玩法 settle_batch 提交,
# refresh 经 settlement.player_request_refresh(战斗结算 核心编排:行动经济 扣 RAP + 退役 + 当场换新板),
# stop 出内环走统一停手提交。连续拒绝 ≥3 = 编排违约防御(fail-fast 语义,停手防死循环)。
# 2026-10-01 上界修正:选路改目标价值评分(_route_pick)+ 商店贪心买齐(_autoplay_shop)——
# 「完美」须是元游戏意义上的上界(会购物会补给),不再只是扫雷子游戏上界。
static func _autoplay_battle_perfect(runner, settlement, st: Dictionary, logger) -> void:
var round_guard := 60
while st.result == "" and runner.current_battle() == settlement and settlement.battle_result == "" and round_guard > 0:
+18 -16
View File
@@ -331,8 +331,8 @@ func _smoke_adventure() -> void:
var resolve_ok := level_resolves.size() == 2
var max_flat := int(_db.adventure.bonus_max_hp_flat)
var heal_pct := int(_db.adventure.clear_heal_pct)
var hp_acc := 0 # max_hp 件累加(回血封顶断言的生效上限推导)
var ks_prefix := 0
hp_acc := 0 # max_hp 件累加(回血封顶断言的生效上限推导)
ks_prefix := 0
for i in picks.size():
var p: Dictionary = picks[i]
var cands: Array = p.get("candidates", []) as Array
@@ -346,37 +346,39 @@ func _smoke_adventure() -> void:
var heal: int = int(p.get("heal_gained", -1))
var hp_before: int = int(p.get("hp_before", -1))
var hp_after: int = int(p.get("hp_after", -1))
var max_eff := int(_db.hero.max_hp) + hp_acc # 回血口径 = 表值+加算区(无 TunePanel 百分比)
if heal != mini(int(floor(float(max_eff) * float(heal_pct) / 100.0)), max_eff - hp_before) \
or hp_after != hp_before + heal or hp_after > max_eff:
# 模拟档 v3.4 商店贪心购血合法抬升(L2/L3 店在 Boss 前)——回血口径窗口 = 加算区件 + 购血余量 0..15
var heal_ok_i := false
for extra in 16:
var m := int(_db.hero.max_hp) + hp_acc + extra
if heal == mini(int(floor(float(m) * float(heal_pct) / 100.0)), m - hp_before):
heal_ok_i = true
break
if not heal_ok_i or hp_after != hp_before + heal or hp_after > int(_db.hero.max_hp) + hp_acc + 15:
heal_ok = false
ks_prefix += int(_db.meta_boss_keystone_row(int(p.get("level_index", -1))).get("first_kill", 0))
if int(p.get("keystone_gained", -1)) != int(_db.meta_boss_keystone_row(int(p.get("level_index", -1))).get("first_kill", 0)) \
or int(p.get("keystone_total", -1)) != ks_prefix:
if int(p.get("keystone_gained", -1)) != int(_db.meta_boss_keystone_row(int(p.get("level_index", -1))).get("first_kill", 0)) or int(p.get("keystone_total", -1)) != ks_prefix:
ks_gain_ok = false
if resolve_ok:
var r: Dictionary = level_resolves[i]
if not (p.get("candidates", []) as Array).has(String(r.get("picked", ""))) \
or int(r.get("value", -1)) != int(_db.adventure.bonus_value(String(r.get("picked", "")))):
if not (p.get("candidates", []) as Array).has(String(r.get("picked", ""))) or int(r.get("value", -1)) != int(_db.adventure.bonus_value(String(r.get("picked", "")))):
resolve_ok = false
if String(r.get("picked", "")) == "max_hp":
hp_acc += max_flat
check(cand_ok, "三选一候选 2–3 件且零重复(加权洗牌不放回)")
check(heal_ok, "通关部分回血封顶(floor(上限×比例) 封顶缺口——血回满退役)")
check(heal_ok, "通关部分回血封顶(floor(上限×比例) 封顶缺口——血回满退役;购血容差窗口)")
check(ks_gain_ok, "钥石逐杀入账(首杀档位 + 累计逐位)")
check(resolve_ok, "点选恰兑付选中一件(值=表值;未选项零生效)")
var seq: Array = st["encounter_max_hp_seq"]
var k := 5.0 # hp+400 ⇒ TunePanel 百分比后乘 ×5(加算先加、百分比后乘——生效次序口径)
var exp_base := maxi(1, int(round(float(int(_db.hero.max_hp)) * k)))
var valid_hp := {}
for acc in 4:
valid_hp[maxi(1, int(round(float(int(_db.hero.max_hp) + acc * max_flat) * k)))] = true
# 模拟档 v3.4 商店贪心购血(数值修复批)合法抬升——上限窗口 = 基础 + 加算区 3 件 + 购血余量(hp_up flat 3×5 次宽松界)
var hp_ceiling := maxi(1, int(round(float(int(_db.hero.max_hp) + 3 * max_flat + 15) * k)))
var seq_ok := seq.size() > 0 and int(seq[0]) == exp_base
for v in (seq as Array):
if not valid_hp.has(int(v)):
if int(v) < exp_base or int(v) > hp_ceiling:
seq_ok = false
check(seq_ok,
"各战上限入参 ∈ {基础..基础+3×件值}(覆盖值通道——通关+奖励节点双渠道共享上限 3;首战=基础 %d)" % exp_base)
"各战上限入参 ∈ [基础, 基础+3×件值+购血余量](覆盖值通道——双渠道共享上限 3;模拟档贪心购血合法叠加〔数值修复批 v3.4〕;首战=基础 %d)" % exp_base)
# ③ 种子域可复现(同种子两局:summary + 上限序列 + 行进序列 + 三选一停点载荷逐位一致)
var st2: Dictionary = autoplay.run_adventure(_db, 42, 900, 400)
check(st2["summary"] == sum and st2["encounter_max_hp_seq"] == seq
@@ -3391,7 +3393,7 @@ func _smoke_nine_types_gp_stream() -> void:
gp_neg = int(row.weight)
check(cell_rows == 22, "奖励字典 22 行(九型×正负 18 格 + aux 正三投放/负一占位——魔法道具内容批 2026-09-29;清图槽独立批尾不计)")
# 雷格负向直伤当量(迭代 8 卡⑧——真人反馈「三雷 3/4 血无后果感」投值:三行各抬 +3 档,负向≈正向 2 倍定价)
var mine_power := {"heal_backlash": 4, "pierce_snare": 5, "magic_recoil": 5}
var mine_power := {"heal_backlash": 6, "pierce_snare": 8, "magic_recoil": 8} # 数值修复批 2026-10-01 再抬档(旧 4/5/5,见 numfix 报告)
for row in _db.reward.rows:
var rid := String(row.get("reward_id", ""))
if mine_power.has(rid):
@@ -36,6 +36,7 @@ func _init() -> void:
_check("前置:board_gen 词表非空(%d 型)" % types.size(), types.size() > 0)
_test_set_completeness(board_dir, types)
_test_hue_domains(board_dir)
_test_digit_ink_modes(board_dir, types)
if _failures > 0:
push_error("[terrain_tile_sets_test] %d/%d 项失败" % [_failures, _checks])
quit(1)
@@ -128,3 +129,47 @@ func _load_stats(board_dir: String, fname: String, top_half: bool) -> Dictionary
if n_v == 0 or n_h == 0:
return {}
return {"h": h_sum / n_h, "v": v_sum / n_v}
# 数字呈现模式对比断言(L2/L3 亮瓦融字缺陷修复 2026-10-01——甲案机检面,v3.4 扩三态):
# 每个图型走 BoardView 同一归一+测光+选模式管线(禁测试侧另写算法),断言:
# ①dungeon=亮墨族直画(对 p90 ≥4.5 且对均值 ≥2.5);②underdark/deep_underdark=色牌垫底
# (双峰纹理直画不可救——渲染帧取证深墨隐形实证,牌上深墨 12:1);③直画模式的双门槛值照抄
# BoardView 判据常量(防判据漂移)
func _test_digit_ink_modes(board_dir: String, types: Array) -> void:
var expect_plate := {"dungeon": false, "underdark": true, "deep_underdark": true}
var variants: int = int(BoardViewScript.TILE_FLOOR_VARIANTS)
for bt in types:
var lumas: Array = []
var readable := true
for i in range(1, variants + 1):
var p := board_dir.path_join("terrain_floor_%s_%d.png" % [String(bt), i])
if not FileAccess.file_exists(p):
readable = false
break
var tex: Texture2D = ImageTexture.create_from_image(Image.load_from_file(p))
BoardViewScript._normalize_floor_luma(tex, 0.10, lumas)
if not readable:
_check("图型 %s 数字墨族断言样本可读" % String(bt), false)
continue
var mode: Dictionary = BoardViewScript._digit_ink_mode(lumas)
var plate: bool = bool(mode["plate"])
var dark: bool = bool(mode["dark"])
var mode_name := "色牌垫底" if plate else ("深墨族直画" if dark else "亮墨族直画")
if plate:
_check("图型 %s 双峰纹理落牌(亮族 p90 %.2f/均值 %.2f,深族 p90 %.2f/均值 %.2f 均未过门)" % [
String(bt), float(mode["cr_p90_bright"]), float(mode["cr_mean_bright"]),
float(mode["cr_p90_dark"]), float(mode["cr_mean_dark"])],
float(mode["cr_mean_dark"]) < float(BoardViewScript.DIGIT_INK_CR_MEAN_DARK))
else:
var cr_p90 := float(mode["cr_p90_bright"]) if not dark else float(mode["cr_p90_dark"])
var cr_mean := float(mode["cr_mean_bright"]) if not dark else float(mode["cr_mean_dark"])
_check("图型 %s 所选%s 对 p90 ≥%.1f(实测 %.2f)" % [String(bt), mode_name,
float(BoardViewScript.DIGIT_INK_CR_P90), cr_p90],
cr_p90 >= float(BoardViewScript.DIGIT_INK_CR_P90))
var mean_gate := float(BoardViewScript.DIGIT_INK_CR_MEAN_BRIGHT) if not dark else float(BoardViewScript.DIGIT_INK_CR_MEAN_DARK)
_check("图型 %s 所选%s 对均值 ≥%.1f(实测 %.2f)" % [String(bt), mode_name, mean_gate, cr_mean],
cr_mean >= mean_gate)
if expect_plate.has(bt):
_check("图型 %s 呈现模式期望=%s(实测=%s)" % [String(bt),
"色牌" if bool(expect_plate[bt]) else "直画", mode_name], plate == bool(expect_plate[bt]))