feat(slice): 插旗不可逆全套落地批+棋盘布局修复批(台账 87,2026-09-19 真人试玩两 bug 质询触发;bug⑤ 经冲突呈报后用户 AskUserQuestion 拍板「全套落地」)——①bug⑤ 不能挖旗格=插旗不可逆提案(2026-09-18 台账 72)转正:禁拔旗(flag_pull 出目录+flag_pull_ap_cost 删除+SUB_REASONS 删 flag_absent)+挖旗格放行(_board_face_dig 删 flag_present 拒挖;eff_flags 副本剔本格算链闭包=本格无旗其余旗照挡、request_dig 拒则旗原地保留事务性;cell_revealed+dug_under_flag 标记仅首格)+四情况兑付改判(战斗结算 v0.18:②插旗雷格直挖只吃挖雷惩罚标中不发/④插旗安全格直挖零惩罚零奖励=正向作废+reward_ineffective 归因行恰一;①③留判照常零代码);②bug⑥ L2 棋盘下缘裁切≈1/6 格高=格边长公式漏算 (N-1)×2px 网格间距(旧公式 N×cell≤可用高-12,12×12 板 460px 可用高下格边 37 网格实高 466 溢出 6px 被 clip_contents 裁切与用户读数吻合)——修复=floor((min(宽-12,高-24)-(N-1)*2)/N)+垂直预留 24px(格边 34 网格高 430 底部留白 15px);③表现层:board_view 右键旗格零动作+悬停语义重排(相邻未开优先挖光标)、hud_presenter flag_present 文案改不能插旗、legend_overlay 旗行改一次性承诺、教学引导+旗承诺首遇提示新条;④验证:冒烟 317→331 项 0 失败(_smoke_flag_irreversible_quadrants 三场景 20 断言+_smoke_layout_cell_formula 布局回归四断言)+四探针 PASS;80 局 MC 双画像回归读数与批前逐位一致(L1 65%/85%、L2 80%/80%——模拟画像不拔旗不挖旗格零行为变更,旗行为归 07⑧ 真人采集);过程登记=board_view 缩进解析错误曾致三 UI 探针静默挂死(冒烟纯脚本模式不加载表现层抓不到,--check-only 定位——测试基建盲区第二例);⑤文档:排雷玩法 v0.5(动作三件事/挖旗格放行/旗生命周期改写)+行动经济 v0.4(六处拔旗退役)+战斗结算 v0.18(判定基线+四情况表)+地图战斗资源 v0.12(极性矩阵删撤旗行增直挖行)+07 速览行+⑧重写(评估使命终结转行为监控)+registry 五处;⑥本提交 active.md 台账 84-89 各会话登记行随本批一并入库(各批代码已各自提交、登记行遗留未收口——一并落账防撞号延续);docs/playnote.md 并行在飞改动未碰
This commit is contained in:
1 parent
0b015dcb5d
commit
87aa89d68d
17 files changed
+221
-134
No files matched your search
@@ -43,8 +43,6 @@ func _cost(kind: String) -> int:
|
||||
return ae.dig_ap_cost
|
||||
"flag_place":
|
||||
return ae.flag_place_ap_cost
|
||||
"flag_pull":
|
||||
return ae.flag_pull_ap_cost
|
||||
"move":
|
||||
return ae.move_ap_cost
|
||||
"chain":
|
||||
|
||||
@@ -200,7 +200,8 @@ func _settle() -> void:
|
||||
_settling = true
|
||||
var ri := round_index
|
||||
# ① 授予段:结算队列(开格/旗判定/清图)→ 极性路由逐批兑付。
|
||||
# 抵消过滤 = 旗判定批以轮末在场旗为基线(被拔不判——发射面已过滤,标旗后撤旗零兑付);开格事实不可撤。
|
||||
# 判定基线 = 轮末在场旗(发射面已过滤——被挖开的旗已随挖掘擦除零兑付,插旗不可逆批 2026-09-19;
|
||||
# 开格事实不可撤)。
|
||||
for evt in _facts_reveals:
|
||||
if not bool(evt.get("resource", false)):
|
||||
continue # 封锁格(initial 通道)恒无资源——不入账(战斗结算 义务②)
|
||||
@@ -208,8 +209,15 @@ func _settle() -> void:
|
||||
if not (snap is Dictionary):
|
||||
continue
|
||||
if evt.kind == "safe":
|
||||
if bool(evt.get("dug_under_flag", false)):
|
||||
# 插旗不可逆四情况④(2026-09-19 用户拍板):插安全格再挖开 = 零惩罚零奖励——正向兑付
|
||||
# 作废(归因行在案);标空判定已随旗擦除自然缺席(被挖的旗不在轮末在场基线)。
|
||||
_emit_row("reward_ineffective", "safe_cell", evt.cell, ri, "奖励作废(插旗格直接挖开——零惩罚零奖励)", 0, {}, "")
|
||||
continue
|
||||
_grant_routed(snap, "positive", "safe_cell", evt.cell, ri, "")
|
||||
elif evt.kind == "mine":
|
||||
# 插旗不可逆四情况②:插雷格再挖开 = 只受挖雷一层惩罚(mine_hit 照常兑付——惊动巢穴),
|
||||
# 标中不发(旗已随挖掘擦除、不入轮末判定基线)。
|
||||
_grant_routed(snap, "negative", "mine_hit", evt.cell, ri, "")
|
||||
for evt in _facts_flags:
|
||||
_judge_flag(evt, ri)
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
# Date: 2026-09-10
|
||||
# 排雷玩法规则引擎:零随机/零表读取/零 UI——输入 = 棋盘生成 产物 + 意图序列,输出 = 三事件流。
|
||||
# 核心契约:install 四不变式 · 初始展开 = R₀ 恰等 · 连锁不越旗 · 批次语义序仲裁(cleared > stop > 其余)。
|
||||
# 旗语义(插旗不可逆批 2026-09-19 用户拍板全套落地):旗 = 一次性承诺——挖旗格放行(本格旗事务内
|
||||
# 隐式退役、轮末不判),拔旗动作退役(flag_pull 出目录)。
|
||||
# 实现 GDD:design/archive/gdd-2026-09-14/a2-minesweeper-rules.md(系统 ID A2·排雷玩法——registry 追溯锚,命名规范见 CLAUDE.md)
|
||||
class_name MinesweeperRules
|
||||
extends RefCounted
|
||||
@@ -11,7 +13,7 @@ signal cell_revealed(evt: Dictionary)
|
||||
signal flag_judged(evt: Dictionary)
|
||||
signal round_end_board(payload: Dictionary) # 双发射源之棋盘路径(排雷玩法 外发;stopped 由 行动经济 直发)
|
||||
|
||||
const SUB_REASONS := ["not_adjacent", "target_revealed", "target_concealed", "flag_present", "flag_absent", "no_path", "out_of_catalog"]
|
||||
const SUB_REASONS := ["not_adjacent", "target_revealed", "target_concealed", "flag_present", "no_path", "out_of_catalog"] # flag_absent 随 flag_pull 退役(2026-09-19)
|
||||
|
||||
var db
|
||||
var action_economy
|
||||
@@ -201,8 +203,10 @@ func _classify(intent: Dictionary) -> int:
|
||||
if intent.get("action", "") == "dig":
|
||||
var c: Vector2i = intent.get("cell", Vector2i(-9, -9))
|
||||
if _board_face_dig(c) == "" and not _board.is_mine(c):
|
||||
# 模拟 Chain(O, c) 后清图判定
|
||||
var chain := _chain_closure(c, _flags) # 与真实执行同口径——空旗集捷径会在旗挡连锁时误判 C_dig(排雷玩法:真模拟)
|
||||
# 模拟 Chain(O, c) 后清图判定——挖旗格同口径:本格旗视为已隐式退役(其余旗照挡)
|
||||
var sim_flags: Dictionary = _flags.duplicate()
|
||||
sim_flags.erase(c)
|
||||
var chain := _chain_closure(c, sim_flags) # 与真实执行同口径——空旗集捷径会在旗挡连锁时误判 C_dig(排雷玩法:真模拟)
|
||||
var remaining := 0
|
||||
for sc in _board.safe_cells():
|
||||
if not _revealed.has(sc) and not chain.has(sc):
|
||||
@@ -221,8 +225,6 @@ func _execute(intent: Dictionary) -> Dictionary:
|
||||
return _do_dig(intent.get("cell", Vector2i(-9, -9)))
|
||||
"flag_place":
|
||||
return _do_flag_place(intent.get("cell", Vector2i(-9, -9)))
|
||||
"flag_pull":
|
||||
return _do_flag_pull(intent.get("cell", Vector2i(-9, -9)))
|
||||
"move":
|
||||
return _do_move(intent.get("cell", Vector2i(-9, -9)))
|
||||
"stop":
|
||||
@@ -237,8 +239,16 @@ func _do_dig(c: Vector2i) -> Dictionary:
|
||||
var sub := _board_face_dig(c)
|
||||
if sub != "":
|
||||
return _outcome(false, "rejected", "board_face", sub)
|
||||
# 挖旗格(插旗不可逆批 2026-09-19):本格旗事务内隐式退役——链闭包按「本格无旗、其余旗照挡」
|
||||
# 计算(旗格为 0 时连锁照常外扩);request_dig 被拒则旗原地保留(事务性),受理后该旗轮末不判
|
||||
# (发射面基线 = 在场旗——标中/标空兑付让位于挖掘本体结局,四情况②④)。
|
||||
var was_flagged := has_flag(c)
|
||||
var eff_flags: Dictionary = _flags
|
||||
if was_flagged:
|
||||
eff_flags = _flags.duplicate()
|
||||
eff_flags.erase(c)
|
||||
# 真模拟连锁与清图(F2/F1——单一实现两名字)
|
||||
var chain := _chain_closure(c, _flags)
|
||||
var chain := _chain_closure(c, eff_flags)
|
||||
var clears: bool = _cleared_after(chain)
|
||||
# F2 口径:chain_size = |O′ ∖ (O ∪ {c})|——闭包含已开集 O,只数新开格(不含首格)
|
||||
var chain_size := 0
|
||||
@@ -249,7 +259,9 @@ func _do_dig(c: Vector2i) -> Dictionary:
|
||||
if not action_economy.request_dig(chain_size, clears):
|
||||
return _outcome(false, "rejected", "resource_face", "")
|
||||
# 事务内执行:首格 + 连锁新格揭示(BFS 规范序,零特判);事件 = O′ ∖ O
|
||||
var reveal_order: Array = _chain_reveal_order(c, _flags)
|
||||
if was_flagged:
|
||||
_flags.erase(c)
|
||||
var reveal_order: Array = _chain_reveal_order(c, eff_flags)
|
||||
for rc in reveal_order:
|
||||
if _revealed.has(rc):
|
||||
continue # 闭包可穿过已开区——已开格零事件(cell_revealed × |O′ ∖ O|)
|
||||
@@ -257,17 +269,20 @@ func _do_dig(c: Vector2i) -> Dictionary:
|
||||
var channel := "dig" if rc == c else "chain"
|
||||
# 格面资源资格(2026-09-15 格面资源批):chain/dig 恒 true——安全格与雷格一视同仁(雷格亦携带,
|
||||
# 负向兑付归 战斗结算 极性路由);initial(封锁格)恒 false。资源本体与极性不在本系统。
|
||||
# dug_under_flag = 首格带旗直挖标记(战斗结算 情况④兑付改判消费;连锁格恒 false——连锁不越旗)
|
||||
if is_mine:
|
||||
_revealed[rc] = {"kind": "mine", "digit": -1}
|
||||
cell_revealed.emit({
|
||||
"cell": rc, "kind": "mine", "digit": -1,
|
||||
"variant_ids": [], "channel": channel, "resource": true,
|
||||
"dug_under_flag": was_flagged and rc == c,
|
||||
})
|
||||
else:
|
||||
_revealed[rc] = {"kind": "safe", "digit": _board.digit_of(rc)}
|
||||
cell_revealed.emit({
|
||||
"cell": rc, "kind": "safe", "digit": _board.digit_of(rc),
|
||||
"variant_ids": _board.variant_ids(rc), "channel": channel, "resource": true,
|
||||
"dug_under_flag": was_flagged and rc == c,
|
||||
})
|
||||
if action_economy.phase != action_economy.PHASE_ACTIVE:
|
||||
_emit_round_end_flags_and_payload()
|
||||
@@ -281,8 +296,7 @@ func _board_face_dig(c: Vector2i) -> String:
|
||||
return "not_adjacent"
|
||||
if is_revealed(c):
|
||||
return "target_revealed"
|
||||
if has_flag(c):
|
||||
return "flag_present"
|
||||
# 旗不拦挖掘(插旗不可逆批 2026-09-19):有旗格直接挖 = 对承诺的兑现,兑付改判归 战斗结算。
|
||||
return ""
|
||||
|
||||
|
||||
@@ -347,24 +361,6 @@ func _do_flag_place(c: Vector2i) -> Dictionary:
|
||||
return _outcome(true, "executed", "", "")
|
||||
|
||||
|
||||
func _do_flag_pull(c: Vector2i) -> Dictionary:
|
||||
var sub := ""
|
||||
if not _board.in_bounds(c):
|
||||
sub = "out_of_catalog"
|
||||
elif not _adjacent_to_hero(c):
|
||||
sub = "not_adjacent"
|
||||
elif not has_flag(c):
|
||||
sub = "flag_absent"
|
||||
if sub != "":
|
||||
return _outcome(false, "rejected", "board_face", sub)
|
||||
if not action_economy.try_spend("flag_pull"):
|
||||
return _outcome(false, "rejected", "resource_face", "")
|
||||
_flags.erase(c)
|
||||
if action_economy.phase != action_economy.PHASE_ACTIVE:
|
||||
_emit_round_end_flags_and_payload() # 同 flag_place——末点 AP 轮末发射(playtest 缺陷②)
|
||||
return _outcome(true, "executed", "", "")
|
||||
|
||||
|
||||
# ---- 移动(瞬移;免费不经账本;BFS 限已探明区含已引爆雷格)----
|
||||
|
||||
func _do_move(c: Vector2i) -> Dictionary:
|
||||
|
||||
@@ -8,8 +8,7 @@ extends Resource
|
||||
|
||||
@export var ap_per_round: int = 6 # [1,32] knob(默认值 = 现行表值基线,2026-09-16 AP 压速批 8→6 随动)
|
||||
@export var dig_ap_cost: int = 1 # [1,8] knob(域下限 1 = 零预算系统失效拒绝)
|
||||
@export var flag_place_ap_cost: int = 1 # [0,8] knob
|
||||
@export var flag_pull_ap_cost: int = 1 # [0,8] knob
|
||||
@export var flag_place_ap_cost: int = 1 # [0,8] knob(flag_pull_ap_cost 随拔旗退役批 2026-09-19 删除)
|
||||
@export var rap_refresh_cost: int = 6 # [1,64] knob(默认值随 AP 压速批 8→6 随动)
|
||||
@export var refresh_cap_per_round: int = 2 # [1,8] knob(2026-09-12 轮内刷新批——每轮刷新兜底上限,Active/Waiting 合计)
|
||||
@export var move_ap_cost: int = 0 # 冻结 0(P4 氛围承载)
|
||||
@@ -24,8 +23,6 @@ func validate() -> Array:
|
||||
errs.append({"category": "domain", "message": "dig_ap_cost 越域 [1,8]: %d" % dig_ap_cost})
|
||||
if flag_place_ap_cost < 0 or flag_place_ap_cost > 8:
|
||||
errs.append({"category": "domain", "message": "flag_place_ap_cost 越域 [0,8]: %d" % flag_place_ap_cost})
|
||||
if flag_pull_ap_cost < 0 or flag_pull_ap_cost > 8:
|
||||
errs.append({"category": "domain", "message": "flag_pull_ap_cost 越域 [0,8]: %d" % flag_pull_ap_cost})
|
||||
if rap_refresh_cost < 1 or rap_refresh_cost > 64:
|
||||
errs.append({"category": "domain", "message": "rap_refresh_cost 越域 [1,64]: %d" % rap_refresh_cost})
|
||||
if refresh_cap_per_round < 1 or refresh_cap_per_round > 8:
|
||||
@@ -35,7 +32,7 @@ func validate() -> Array:
|
||||
if chain_ap_cost != 0:
|
||||
errs.append({"category": "frozen", "message": "chain_ap_cost 结构冻结为 0: %d" % chain_ap_cost})
|
||||
# 跨字段(行动经济 三轮评审增补):任一动作成本 ≤ ap_per_round,违规加载期致命
|
||||
for pair in [["dig_ap_cost", dig_ap_cost], ["flag_place_ap_cost", flag_place_ap_cost], ["flag_pull_ap_cost", flag_pull_ap_cost]]:
|
||||
for pair in [["dig_ap_cost", dig_ap_cost], ["flag_place_ap_cost", flag_place_ap_cost]]:
|
||||
if pair[1] > ap_per_round:
|
||||
errs.append({"category": "cross_field", "message": "%s(%d) > ap_per_round(%d)——挖掘/旗动作永久不可用软锁" % [pair[0], pair[1], ap_per_round]})
|
||||
return errs
|
||||
@@ -7,7 +7,6 @@ script = ExtResource("1")
|
||||
ap_per_round = 6
|
||||
dig_ap_cost = 1
|
||||
flag_place_ap_cost = 1
|
||||
flag_pull_ap_cost = 1
|
||||
rap_refresh_cost = 6
|
||||
refresh_cap_per_round = 2
|
||||
move_ap_cost = 0
|
||||
|
||||
@@ -96,8 +96,7 @@ static func outcome_text(outcome: Dictionary) -> String:
|
||||
"not_adjacent": "不能执行:目标与小人不相邻",
|
||||
"target_revealed": "不能执行:目标已开",
|
||||
"target_concealed": "不能移动:目标未开",
|
||||
"flag_present": "不能挖:此格有旗——先右键拔旗",
|
||||
"flag_absent": "不能拔旗:此格无旗",
|
||||
"flag_present": "不能插旗:此格已有旗(旗为一次性承诺——相邻时左键直接挖开)",
|
||||
"no_path": "不能移动:已探明区不连通",
|
||||
"out_of_catalog": "无效目标",
|
||||
}
|
||||
|
||||
@@ -92,12 +92,16 @@ func set_identity_resolver(cb: Callable) -> void:
|
||||
_identity_cb = cb
|
||||
|
||||
|
||||
# D 区可用面积(AP 条之外)→ 格边长 = floor(min(宽, 高)/N)(battle.md 格尺寸公式,36–45px 档)
|
||||
# D 区可用面积(AP 条之外)→ 格边长 = floor((min(宽, 高) − 预留 − (N−1)×格间距) / N)(battle.md 格尺寸公式)
|
||||
# 2026-09-19 修复:格边长公式补算 (N−1)×2px 网格间距 + 垂直预留 24px——旧公式只算 N×cell,
|
||||
# L2 12×12 板网格实高溢出可用区 ~6px,下缘被 clip_contents 裁掉约 1/6 格高。
|
||||
func set_cell_area(avail: Vector2) -> void:
|
||||
if _board == null or _board.width == 0:
|
||||
return
|
||||
var n: int = maxi(_board.width, _board.height)
|
||||
var cell := int(floor(minf(avail.x - 12.0, avail.y - 12.0) / float(n)))
|
||||
var sep := 2 # GridContainer v/h_separation(_init 同值)
|
||||
var reserve := 24.0 # 垂直总预留(上 12 + 下 12:棋盘底部不贴 D 区边框)
|
||||
var cell := int(floor((minf(avail.x - 12.0, avail.y - reserve) - float((n - 1) * sep)) / float(n)))
|
||||
_cell = float(clampi(cell, 26, 46))
|
||||
for c in _cells:
|
||||
(_cells[c] as Button).custom_minimum_size = Vector2(_cell, _cell)
|
||||
@@ -168,10 +172,10 @@ func _on_cell_input(c: Vector2i, ev: InputEvent) -> void:
|
||||
action_requested.emit(c, "dig")
|
||||
_detect_chord(c)
|
||||
elif ev.button_index == MOUSE_BUTTON_RIGHT:
|
||||
if _minesweeper.has_flag(c):
|
||||
action_requested.emit(c, "flag_pull")
|
||||
else:
|
||||
if not _minesweeper.has_flag(c):
|
||||
action_requested.emit(c, "flag_place")
|
||||
# 旗 = 一次性承诺(插旗不可逆批 2026-09-19):拔旗退役——旗格右键零动作;
|
||||
# 左键相邻旗格直接挖(四情况②④兑付改判,规则引擎放行)。
|
||||
|
||||
|
||||
# chord 肌肉记忆首遇锚:同格双左击快开尝试(350ms 窗)——每 run 一次提示由组合根去重
|
||||
@@ -216,14 +220,14 @@ func _apply_hover_visuals(c: Vector2i) -> void:
|
||||
_set_hl_color(hl, COL_HL_TARGET) # 移动目标格边框(金)
|
||||
_set_cursor_shape(c, CURSOR_MOVE_SEM)
|
||||
_show_path_preview(c)
|
||||
elif flag:
|
||||
hl.visible = true
|
||||
_set_hl_color(hl, COL_FLAG) # 拔旗目标(旗朱)
|
||||
_set_cursor_shape(c, CURSOR_FLAG)
|
||||
elif adjacent:
|
||||
hl.visible = true
|
||||
_set_hl_color(hl, COL_HL_TARGET) # 挖掘目标格边框
|
||||
_set_hl_color(hl, COL_HL_TARGET) # 挖掘目标格边框(相邻旗格同此——左键直接挖,插旗不可逆批)
|
||||
_set_cursor_shape(c, CURSOR_DIG)
|
||||
elif flag:
|
||||
hl.visible = true
|
||||
_set_hl_color(hl, COL_FLAG) # 已承诺旗标记(旗朱;无交互可供——拔旗已退役)
|
||||
_set_cursor_shape(c, Input.CURSOR_FORBIDDEN)
|
||||
else:
|
||||
hl.visible = true
|
||||
_set_hl_color(hl, Color8(120, 60, 50)) # 不可及域灰禁框(禁止光标+首遇 toast 双通道)
|
||||
|
||||
@@ -12,7 +12,7 @@ signal closed
|
||||
const ENTRIES := [
|
||||
["#C9AF83", "完整覆尘地砖", "未开格——左键挖开(1AP,须相邻)· 右键插旗(1AP)"],
|
||||
["#B39A78", "碎裂裸土", "已开 0 格——可通行,左键移动(免费)"],
|
||||
["#C63D22", "旗", "插旗标记——轮末自动判定标中"],
|
||||
["#C63D22", "旗", "一次性承诺(不能拔)——轮末自动判定标中;插错可左键直接挖开(安全格零奖励、雷格吃挖雷惩罚)"],
|
||||
["#2E6E8E", "小人", "探险者——8 向相邻未开格常驻微光 = 可挖域"],
|
||||
["#F0A828", "金徽章", "已开格奖励身份 icon(旗之下、数值之上,不进数字区)"],
|
||||
["#33261B", "状态槽空态", "无状态效果时槽位隐藏,数据框上移补位——非异常"],
|
||||
|
||||
@@ -66,6 +66,7 @@ func _run_core_smoke(db, rng) -> void:
|
||||
_smoke_armor_roll()
|
||||
_smoke_pool_wiring()
|
||||
_smoke_board_generator()
|
||||
_smoke_layout_cell_formula()
|
||||
_smoke_variation_framework()
|
||||
_smoke_full_battle(1, 0, "L1 · 地精")
|
||||
_smoke_full_battle(2, 0, "L2 · 兄弟精")
|
||||
@@ -82,7 +83,7 @@ func _run_core_smoke(db, rng) -> void:
|
||||
_smoke_clear_reward_chain()
|
||||
_smoke_refresh_provenance_guard()
|
||||
_smoke_polarity_routing()
|
||||
_smoke_flag_misfire_dig_after_pull()
|
||||
_smoke_flag_irreversible_quadrants()
|
||||
_smoke_negative_dictionary()
|
||||
_smoke_resource_surface()
|
||||
_smoke_settlement_segments()
|
||||
@@ -90,6 +91,26 @@ func _run_core_smoke(db, rng) -> void:
|
||||
_smoke_level_graph()
|
||||
|
||||
|
||||
# ---- 战斗屏棋盘布局·格边长公式回归(2026-09-19 真人试玩 bug⑥:L2 12×12 板下缘被裁约 1/6 格高——
|
||||
# 旧公式 N×cell ≤ 可用高−12 未计 (N−1)×2px 网格间距,网格实高溢出被 clip_contents 裁掉;
|
||||
# 修复 = 公式计入间距 + 垂直预留 24px〔上 12/下 12——底部不贴 D 区边框〕)----
|
||||
func _smoke_layout_cell_formula() -> void:
|
||||
print("--- 棋盘布局格边长公式(底部预留回归)---")
|
||||
var board_generator = load("res://core/board_generator.gd").new()
|
||||
var board_view = load("res://presentation/views/board_view.gd").new()
|
||||
for L in [1, 2]:
|
||||
var b = board_generator.generate(L, 0, _mk_stream("board_gen", 555), _db)
|
||||
board_view.rebuild(b, null)
|
||||
var avail := Vector2(1036, 460) # 受约束 D 区可用面积样本(1280×800 档量级——用户实测裁切场景)
|
||||
board_view.set_cell_area(avail)
|
||||
var n: int = maxi(b.width, b.height)
|
||||
var cell: float = board_view.get("_cell")
|
||||
var grid_h := n * int(cell) + (n - 1) * 2
|
||||
check(grid_h <= int(avail.y) - 24, "L%d %dx%d 板网格高 %d ≤ 可用高 %d − 预留 24(格边 %dpx——下缘不贴边不裁切)" % [L, b.width, b.height, grid_h, int(avail.y), int(cell)])
|
||||
check(cell >= 26.0 and cell <= 46.0, "L%d 格边在 26–46 档内(%dpx)" % [L, int(cell)])
|
||||
board_view.free()
|
||||
|
||||
|
||||
# ---- 地图战斗资源·身份面单元冒烟(格面资源批:roll 恰一次 / 雷安同池 / 幂等 / 清图批尾)----
|
||||
|
||||
func _smoke_reward_identity_unit() -> void:
|
||||
@@ -864,75 +885,116 @@ func _find_flag_target(minesweeper, want_mine: bool) -> Dictionary:
|
||||
return {}
|
||||
|
||||
|
||||
# ---- 旗奖励语义实证(2026-09-19 观测项⑧语义锚——用户质询「误旗判定后拔旗再挖是否仍有奖励」)----
|
||||
# 场景 A(跨轮):标安全格 → 收轮判定(flag_miss 负向兑付)→ 拔旗 → 挖开 → 收轮。
|
||||
# 语义锚 = 惩罚与奖励并行:判定走消费型 grant、挖开走查询型 query_full_snapshot,两通道互不吞并。
|
||||
# 场景 B(同轮插撤):标安全格 → 同轮拔旗 → 挖开 → 收轮。被拔不判(抵消过滤)——零惩罚零影响。
|
||||
func _smoke_flag_misfire_dig_after_pull() -> void:
|
||||
print("--- 旗奖励语义(误旗判定后拔旗再挖)---")
|
||||
# 场景 A:跨轮(判定已走)
|
||||
# ---- 插旗不可逆四情况冒烟(2026-09-19 用户拍板全套落地:禁拔旗 + 挖旗格兑付改判)----
|
||||
# 四情况:① 插雷格不动 = 标中奖励照常;② 插雷格再挖开 = 只受挖雷一层惩罚(mine_hit 照常)标中不发;
|
||||
# ③ 插安全格不动 = 标空惩罚照常;④ 插安全格再挖开 = 零惩罚零奖励(正向兑付作废、标空不判)。
|
||||
# 退役面:flag_pull 出目录(out_of_catalog);重复插旗仍 flag_present 拒绝。
|
||||
# 场景 A(④ + 退役面):标安全格 → flag_pull 拒绝 → 重复插旗拒绝 → 直接挖开 → 收轮。
|
||||
# 场景 B(②):标雷格 → 直接挖开 → 收轮。
|
||||
# 场景 C(①③):标安全格 + 标雷格 → 不挖收轮 → 标空/标中判定照常。
|
||||
func _smoke_flag_irreversible_quadrants() -> void:
|
||||
print("--- 插旗不可逆四情况(禁拔旗 + 挖旗格兑付改判)---")
|
||||
# 场景 A:安全格直挖(情况④)+ flag_pull/重复插旗退役面
|
||||
var rng_a = load("res://core/rng_service.gd").new()
|
||||
rng_a.set_master_seed(20260919)
|
||||
var settle_a = load("res://core/battle_orchestrator.gd").new(_db, rng_a)
|
||||
var rows_a: Array = []
|
||||
settle_a.step_row.connect(func(row): rows_a.append(row))
|
||||
check(settle_a.begin_battle({"level_index": 1, "node_depth": 4, "hero_hp": int(_db.hero.max_hp)}), "旗语义 A:开局受理")
|
||||
check(settle_a.begin_battle({"level_index": 1, "node_depth": 4, "hero_hp": int(_db.hero.max_hp)}), "四情况 A:开局受理")
|
||||
var ms_a = settle_a.minesweeper
|
||||
var t_a := _find_flag_target(ms_a, false)
|
||||
check(not t_a.is_empty(), "旗语义 A:找到安全格旗目标")
|
||||
check(not t_a.is_empty(), "四情况 A:找到安全格旗目标")
|
||||
if not t_a.is_empty():
|
||||
if t_a.hero_cell != ms_a.hero_pos():
|
||||
ms_a.settle_batch([{"action": "move", "cell": t_a.hero_cell}])
|
||||
ms_a.settle_batch([{"action": "flag_place", "cell": t_a.flag_cell}])
|
||||
ms_a.settle_batch([{"action": "stop"}]) # 收轮 → 判定批:flag_miss + 负向兑付
|
||||
ms_a.settle_batch([{"action": "flag_pull", "cell": t_a.flag_cell}])
|
||||
check(ms_a.settle_batch([{"action": "flag_place", "cell": t_a.flag_cell}])[0].accepted, "四情况 A:插旗受理")
|
||||
var pull_out: Array = ms_a.settle_batch([{"action": "flag_pull", "cell": t_a.flag_cell}])
|
||||
check(not pull_out.is_empty() and not bool(pull_out[0].get("accepted", true)) and String(pull_out[0].get("sub_reason", "")) == "out_of_catalog",
|
||||
"四情况 A:flag_pull 出目录拒绝(拔旗退役)")
|
||||
var dup_place: Array = ms_a.settle_batch([{"action": "flag_place", "cell": t_a.flag_cell}])
|
||||
check(not dup_place.is_empty() and not bool(dup_place[0].get("accepted", true)) and String(dup_place[0].get("sub_reason", "")) == "flag_present",
|
||||
"四情况 A:重复插旗仍被拒(flag_present)")
|
||||
var dig_a: Array = ms_a.settle_batch([{"action": "dig", "cell": t_a.flag_cell}])
|
||||
check(not dig_a.is_empty() and bool(dig_a[0].get("accepted", false)), "旗语义 A:拔旗后挖开被受理")
|
||||
check(not dig_a.is_empty() and bool(dig_a[0].get("accepted", false)), "四情况 A:旗格直接挖开被受理(不再须先拔旗)")
|
||||
check(not ms_a.has_flag(t_a.flag_cell), "四情况 A:旗随挖掘隐式退役(has_flag = false)")
|
||||
ms_a.settle_batch([{"action": "stop"}])
|
||||
var target_a: Array = [t_a.flag_cell.x, t_a.flag_cell.y]
|
||||
var miss_attr := 0
|
||||
var miss_neg := 0
|
||||
var dig_reward := 0
|
||||
var miss_a := 0
|
||||
var grant_a := 0
|
||||
var void_a := 0
|
||||
for row in rows_a:
|
||||
if row.get("origin_cell", [0, 0]) != target_a:
|
||||
continue
|
||||
if row.get("type", "") == "flag_miss":
|
||||
miss_attr += 1
|
||||
if String(row.get("source_class", "")) == "flag_miss":
|
||||
miss_neg += 1
|
||||
if String(row.get("source_class", "")) == "safe_cell":
|
||||
dig_reward += 1
|
||||
check(miss_attr == 1, "旗语义 A:标空归因行恰一次(×%d——每格实例一次去重)" % miss_attr)
|
||||
check(miss_neg >= 1, "旗语义 A:标空负向兑付行在案(×%d)" % miss_neg)
|
||||
check(dig_reward >= 1, "旗语义 A:判定后拔旗再挖仍获正向奖励(×%d——惩罚与奖励并行)" % dig_reward)
|
||||
# 场景 B:同轮插撤(被拔不判)
|
||||
if row.get("type", "") == "flag_miss" or String(row.get("source_class", "")) == "flag_miss":
|
||||
miss_a += 1
|
||||
if String(row.get("source_class", "")) == "safe_cell" and row.get("type", "") != "reward_ineffective":
|
||||
grant_a += 1
|
||||
if row.get("type", "") == "reward_ineffective":
|
||||
void_a += 1
|
||||
check(miss_a == 0, "四情况 A:标空判定零兑付(×%d——旗已擦除不入轮末基线)" % miss_a)
|
||||
check(grant_a == 0, "四情况 A:安全格正向兑付作废(×%d——零奖励)" % grant_a)
|
||||
check(void_a == 1, "四情况 A:兑付作废归因行恰一次(×%d——零惩罚零奖励留痕)" % void_a)
|
||||
# 场景 B:雷格直挖(情况②)
|
||||
var rng_b = load("res://core/rng_service.gd").new()
|
||||
rng_b.set_master_seed(20260919)
|
||||
var settle_b = load("res://core/battle_orchestrator.gd").new(_db, rng_b)
|
||||
var rows_b: Array = []
|
||||
settle_b.step_row.connect(func(row): rows_b.append(row))
|
||||
check(settle_b.begin_battle({"level_index": 1, "node_depth": 4, "hero_hp": int(_db.hero.max_hp)}), "旗语义 B:开局受理")
|
||||
check(settle_b.begin_battle({"level_index": 1, "node_depth": 4, "hero_hp": int(_db.hero.max_hp)}), "四情况 B:开局受理")
|
||||
var ms_b = settle_b.minesweeper
|
||||
var t_b := _find_flag_target(ms_b, false)
|
||||
check(not t_b.is_empty(), "旗语义 B:找到安全格旗目标")
|
||||
var t_b := _find_flag_target(ms_b, true)
|
||||
check(not t_b.is_empty(), "四情况 B:找到雷格旗目标")
|
||||
if not t_b.is_empty():
|
||||
if t_b.hero_cell != ms_b.hero_pos():
|
||||
ms_b.settle_batch([{"action": "move", "cell": t_b.hero_cell}])
|
||||
ms_b.settle_batch([{"action": "flag_place", "cell": t_b.flag_cell}])
|
||||
ms_b.settle_batch([{"action": "flag_pull", "cell": t_b.flag_cell}]) # 同轮拔——发射面已过滤
|
||||
check(ms_b.settle_batch([{"action": "flag_place", "cell": t_b.flag_cell}])[0].accepted, "四情况 B:插旗受理")
|
||||
var dig_b: Array = ms_b.settle_batch([{"action": "dig", "cell": t_b.flag_cell}])
|
||||
check(not dig_b.is_empty() and bool(dig_b[0].get("accepted", false)), "旗语义 B:同轮插撤后挖开被受理")
|
||||
check(not dig_b.is_empty() and bool(dig_b[0].get("accepted", false)), "四情况 B:旗雷格直接挖开被受理")
|
||||
check(ms_b.is_revealed(t_b.flag_cell) and ms_b.current_board().is_mine(t_b.flag_cell), "四情况 B:雷格已揭示(挖雷本体照常)")
|
||||
ms_b.settle_batch([{"action": "stop"}])
|
||||
var target_b: Array = [t_b.flag_cell.x, t_b.flag_cell.y]
|
||||
var miss_b := 0
|
||||
var reward_b := 0
|
||||
var mine_hit_b := 0
|
||||
var hit_b := 0
|
||||
for row in rows_b:
|
||||
if row.get("type", "") == "flag_miss" or String(row.get("source_class", "")) == "flag_miss":
|
||||
miss_b += 1
|
||||
if String(row.get("source_class", "")) == "safe_cell" and row.get("origin_cell", [0, 0]) == target_b:
|
||||
reward_b += 1
|
||||
check(miss_b == 0, "旗语义 B:同轮插撤零判定零惩罚(×%d——被拔不判)" % miss_b)
|
||||
check(reward_b >= 1, "旗语义 B:同轮插撤后挖开奖励不受影响(×%d)" % reward_b)
|
||||
if row.get("origin_cell", [0, 0]) != target_b:
|
||||
continue
|
||||
if String(row.get("source_class", "")) == "mine_hit":
|
||||
mine_hit_b += 1
|
||||
if row.get("type", "") == "flag_hit" or String(row.get("source_class", "")) == "flag_hit":
|
||||
hit_b += 1
|
||||
check(mine_hit_b >= 1, "四情况 B:挖雷惩罚照常兑付(×%d——只受挖雷一层惩罚)" % mine_hit_b)
|
||||
check(hit_b == 0, "四情况 B:标中不发(×%d——旗已擦除不判)" % hit_b)
|
||||
# 场景 C:不挖收轮(①③判定照常)
|
||||
var rng_c = load("res://core/rng_service.gd").new()
|
||||
rng_c.set_master_seed(20260919)
|
||||
var settle_c = load("res://core/battle_orchestrator.gd").new(_db, rng_c)
|
||||
var rows_c: Array = []
|
||||
settle_c.step_row.connect(func(row): rows_c.append(row))
|
||||
check(settle_c.begin_battle({"level_index": 1, "node_depth": 4, "hero_hp": int(_db.hero.max_hp)}), "四情况 C:开局受理")
|
||||
var ms_c = settle_c.minesweeper
|
||||
var t_safe := _find_flag_target(ms_c, false)
|
||||
var t_mine := _find_flag_target(ms_c, true)
|
||||
check(not t_safe.is_empty() and not t_mine.is_empty(), "四情况 C:安全格+雷格旗目标齐备")
|
||||
if not t_safe.is_empty() and not t_mine.is_empty():
|
||||
for t in [t_safe, t_mine]:
|
||||
if t.hero_cell != ms_c.hero_pos():
|
||||
ms_c.settle_batch([{"action": "move", "cell": t.hero_cell}])
|
||||
ms_c.settle_batch([{"action": "flag_place", "cell": t.flag_cell}])
|
||||
ms_c.settle_batch([{"action": "stop"}]) # 不挖收轮——两旗均在场基线
|
||||
var safe_c: Array = [t_safe.flag_cell.x, t_safe.flag_cell.y]
|
||||
var mine_c: Array = [t_mine.flag_cell.x, t_mine.flag_cell.y]
|
||||
var miss_c := 0
|
||||
var miss_neg_c := 0
|
||||
var hit_c := 0
|
||||
for row in rows_c:
|
||||
if row.get("origin_cell", [0, 0]) == safe_c and row.get("type", "") == "flag_miss":
|
||||
miss_c += 1
|
||||
if row.get("origin_cell", [0, 0]) == safe_c and String(row.get("source_class", "")) == "flag_miss" and row.get("type", "") != "flag_miss":
|
||||
miss_neg_c += 1
|
||||
if row.get("origin_cell", [0, 0]) == mine_c and (row.get("type", "") == "flag_hit" or String(row.get("source_class", "")) == "flag_hit"):
|
||||
hit_c += 1
|
||||
check(miss_c >= 1 and miss_neg_c >= 1, "四情况 C:标空惩罚照常(归因×%d 负向兑付×%d)" % [miss_c, miss_neg_c])
|
||||
check(hit_c >= 1, "四情况 C:标中奖励照常(×%d)" % hit_c)
|
||||
|
||||
|
||||
# ---- 试玩日志器冒烟(结算快照 JSONL + 诊断转储)----
|
||||
|
||||
Reference in new issue
Block a user