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() 读侧包写法。
This commit is contained in:
DKChan committed 2026-10-25 03:35:55 +08:00
1 parent a0acdf5c43
commit 1b9736f065
25 files changed
+186 -59

No files matched your search

+5 -4
View File
@@ -6,11 +6,12 @@
class_name ActionEconomy
extends RefCounted
signal round_end_stopped(payload: Dictionary) # 双发射源之停手路径(行动经济 直发;payload 与 排雷玩法 棋盘路径同构)
signal round_end_stopped(payload: Dictionary) ## 双发射源之停手路径(行动经济 直发;payload 与 排雷玩法 棋盘路径同构)
const PHASE_IDLE := "Idle"
const PHASE_ACTIVE := "Active"
const PHASE_WAITING := "Waiting"
const CHARGE_LAYER_CAP := 3 # 蓄力层上限(机制结构常量,非玩法表值——整备军备面)
# 相位轴:内部 enum + match,外部 phase getter 保持
# String 面(presentation_snapshot 与冒烟链的 "Idle"/"Active"/"Waiting" 对照契约)。
@@ -182,14 +183,14 @@ func grant_reserve_ap(n: int) -> void:
# 轮末停手转存决策(round_end 发射点调用——编排器授予段尾):transfer=true=剩余 AP 转蓄力层
# (上限 3,整备军备面);false=按转存率转存 RAP(transfer_pct 百分比口径缺省 100=1:1;
# (上限 CHARGE_LAYER_CAP,整备军备面);false=按转存率转存 RAP(transfer_pct 百分比口径缺省 100=1:1;
# 储备罐 rap_transfer_mult magnitude 125=1.25:1,整数除法向下取整=每 4 AP 多转 1)。
# 非停手轮(cleared/耗尽)_last_stop_ap=0 两分支均零操作。转蓄后达上限的沿发 charge_full(R6 框架先行——现无订阅者)。
func finish_round_stop(transfer: bool, transfer_pct: int = 100) -> void:
if transfer:
var prev_layers := _charge_layers
_charge_layers = mini(3, _charge_layers + _last_stop_ap)
if _fx != null and _charge_layers >= 3 and prev_layers < 3:
_charge_layers = mini(CHARGE_LAYER_CAP, _charge_layers + _last_stop_ap)
if _fx != null and _charge_layers >= CHARGE_LAYER_CAP and prev_layers < CHARGE_LAYER_CAP:
_fx.dispatch(_ctx, "charge_full", {"layers": _charge_layers})
else:
_rap += _last_stop_ap * maxi(1, transfer_pct) / 100
+4 -4
View File
@@ -6,10 +6,10 @@
class_name AdventureRunner
extends RefCounted
signal level_started(level_index: int, display_name: String) # 每关开始(含冒险首关)——呈现层重取 runner()、刷新图条与层名行
signal curio_choice_presented(payload: Dictionary) # 专长三选一停点(非末关通关恰一次;通道白名单后候选恒专长+回血在载荷)——{level_index, candidates, heal_gained, hp_before, hp_after, keystone_gained, keystone_total, gp, aux_cleared}
signal curio_resolved(result: Dictionary) # 三选一兑付完成(pick_curio/替换受理后恰一次)——{picked, value, hp_before, hp_after, level_index}
signal adventure_finished(summary: Dictionary) # 冒险结算(completed/failed/abandoned)——summary 五件事:打穿没有/通关几关/止步第几关/钥石合计/主种子(+additive keystone_drops)
signal level_started(level_index: int, display_name: String) ## 每关开始(含冒险首关)——呈现层重取 runner()、刷新图条与层名行
signal curio_choice_presented(payload: Dictionary) ## 专长三选一停点(非末关通关恰一次;通道白名单后候选恒专长+回血在载荷)——{level_index, candidates, heal_gained, hp_before, hp_after, keystone_gained, keystone_total, gp, aux_cleared}
signal curio_resolved(result: Dictionary) ## 三选一兑付完成(pick_curio/替换受理后恰一次)——{picked, value, hp_before, hp_after, level_index}
signal adventure_finished(summary: Dictionary) ## 冒险结算(completed/failed/abandoned)——summary 五件事:打穿没有/通关几关/止步第几关/钥石合计/主种子(+additive keystone_drops)
# ---- 转发族(签名与 LevelRunner 同款——呈现层对接透明;S6 经 _forward_* 方法)----
signal node_entered(node: Dictionary, info: Dictionary)
signal choices_presented(candidates: Array)
+2 -2
View File
@@ -7,10 +7,10 @@ class_name BattleOrchestrator
extends RefCounted
signal step_row(row: Dictionary)
signal battle_finished(payload: Dictionary) # {result, hero_hp, gp_dropped}——hero 在 despawn 前捕获(呈现层销毁后读状态的时序防护);gp_dropped = 战斗掉落合计
signal battle_finished(payload: Dictionary) ## {result, hero_hp, gp_dropped}——hero 在 despawn 前捕获(呈现层销毁后读状态的时序防护);gp_dropped = 战斗掉落合计
signal round_started(round_index: int)
signal board_changed()
signal settlement_snapshot(snap: Dictionary) # 每结算末尾的全量状态快照(playtest_logger 消费——JSONL 数值分析)
signal settlement_snapshot(snap: Dictionary) ## 每结算末尾的全量状态快照(playtest_logger 消费——JSONL 数值分析)
# 明细流水行型协议(战斗结算 Core Rule;9 类;
# 清单改动须同步 battle_log_view 的 _format_row 与本文档头注):
+10 -5
View File
@@ -1,7 +1,7 @@
## VERTICAL SLICE - NOT FOR PRODUCTION
## 确定性棋盘生成器:生成-验证循环出图,不达标走接受最优/最不坏诚实降级(垂直切片验证:每张图可推理可归因)。
## 只消费传入的单条 rng 流(重试共用,attempt 边界非 scope 事件)——同流同参逐位复现。
## generate/legality 的 db 参数鸭子型(不标类型)——冒烟测量面注入桩表依赖此形态,勿收紧签名。
## generate 的 db 参数鸭子型(不标类型)——冒烟测量面注入桩表依赖此形态,勿收紧签名。
class_name BoardGenerator
extends RefCounted
@@ -10,6 +10,11 @@ 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)
@@ -50,7 +55,7 @@ func generate(L: int, D: int, rng_stream: RandomNumberGenerator, db) -> BoardDat
# 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):
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:
@@ -94,7 +99,7 @@ func _attempt(params: Dictionary, rng_stream: RandomNumberGenerator, db) -> Boar
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 - BoardData.SPAWN_AREA_CELLS)))
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)
@@ -278,7 +283,7 @@ func is_clearable(b: BoardData) -> bool:
return true
func legality(b: BoardData, params: Dictionary, db) -> bool: # type-hint: ignore(同 generate——测量面 stub 注入)
func legality(b: BoardData, params: Dictionary) -> bool:
var reasons := _legality_faults(b, params)
return reasons.is_empty()
@@ -331,7 +336,7 @@ func _neighbor_mine_count(b: BoardData, c: Vector2i) -> int:
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 = int(round(float(params.density) * float(params.w * params.h - BoardData.SPAWN_AREA_CELLS)))
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)
+14
View File
@@ -342,6 +342,20 @@ func shop_price_b() -> float:
return float(shop.price_b) if shop != null else 0.0
# relic 保底卡基价 / 散折(economy_spend 激活时购买价折扣——表未入库 = 表 schema 缺省值)
func shop_relic_base_price() -> float:
return float(shop.relic_base_price) if shop != null else 0.0
func shop_spend_discount() -> float:
return float(shop.spend_discount) if shop != null else 0.0
# 英雄显示名(单位快照读取面单源——与敌人行 display_name 同轴)
func hero_display_name() -> String:
return String(hero.display_name) if hero != null else ""
# 行族数组读取(ShopTable 属性名 = 行族名;表未已入库/属性缺失 = 空集非错误态)
func _shop_rows(family: String) -> Array:
if shop == null:
+1 -1
View File
@@ -212,7 +212,7 @@ func _on_unit_died(u: Dictionary) -> void:
_halted = true # 主角倒下——flush 余单作废;Game Over 归判定段既有 hp 读数路径
return
for t in _queue:
if t.source_unit == u:
if is_same(t.source_unit, u):
t["cancelled"] = true # 死亡即不动手——该敌未结单据作废(静默零行零 commit)
+9 -9
View File
@@ -7,13 +7,13 @@
class_name LevelRunner
extends RefCounted
signal node_entered(node: Dictionary, info: Dictionary) # info 按 type:battle/boss={} · reward={}(内容待定) · rest={healed,hp,max_hp} · shop={gp}
signal choices_presented(candidates: Array) # 选路口(兑付确认后——候选节点引用数组,零自动推进)
signal step_row(row: Dictionary) # 转发当前 战斗结算 结算行
signal node_entered(node: Dictionary, info: Dictionary) ## info 按 type:battle/boss={} · reward={}(内容待定) · rest={healed,hp,max_hp} · shop={gp}
signal choices_presented(candidates: Array) ## 选路口(兑付确认后——候选节点引用数组,零自动推进)
signal step_row(row: Dictionary) ## 转发当前 战斗结算 结算行
signal round_started(round_index: int)
signal board_changed()
signal level_finished(payload: Dictionary) # {result, hp, node_index, node_id, node_type}
signal wallet_changed(event: String, gp: int, extra: Dictionary) # GP 钱包事件投影:gain{amount}/buy{product,price}/clear——试玩日志与商店覆盖层刷新源(GP+商店房实施批)
signal level_finished(payload: Dictionary) ## {result, hp, node_index, node_id, node_type}
signal wallet_changed(event: String, gp: int, extra: Dictionary) ## GP 钱包事件投影:gain{amount}/buy{product,price}/clear——试玩日志与商店覆盖层刷新源(GP+商店房实施批)
var db: ConfigDb
var rng: RngServiceCore
@@ -758,12 +758,12 @@ func current_price(prow: Dictionary) -> int:
+ int(prow.get("price_increment", 0)) * purchased_tier(String(prow.get("product_id", ""))))
# 散折价单点(财富之道·散 economy_spend 激活时商品购买价 ×80% provisional,
# 散折价单点(财富之道·散 economy_spend 激活时商品购买价 ×spend_discount〔表 knob,缺省 0.8〕,
# floor 取整偏向玩家、下限 1 防 1 元商品折成 0;范围=商品标价定义边界〔legacy 商品面 current_price
# + 池货架面 pool_price 同折〕——商店刷新价 shop_refresh_cost 与三选一付费刷新不折)
func _spend_discounted(price: int) -> int:
if _tree_rules.has("economy_spend"):
return maxi(1, int(floor(float(price) * 0.8)))
return maxi(1, int(floor(float(price) * db.shop_spend_discount())))
return price
@@ -874,12 +874,12 @@ func pool_stock(kind: String) -> int:
# 现价 = ceil(基价 × b^(关序−1) × A^(本次进店已购次数))(P4 甲 ceil 向上单一取整——玩家不利向;
# A/b 表 knob provisional;核心现算,呈现层禁复算——契约 5)。relic 卡(保底占位——shop 词表外)
# 基价 provisional 15 固定锚(台账披露待追认——shop.tres 词表扩列归数值批)。
# 基价 provisional 15(表 knob relic_base_price——shop.tres 词表扩列归数值批)。
func pool_price(kind: String) -> int:
var row: Dictionary[Variant, Variant] = db.shop_pool_row(kind)
if row.is_empty():
if kind == "relic":
return _spend_discounted(int(ceil(15.0 * pow(db.shop_price_b(), float(maxi(0, int(level_row.level_index) - 1))) * pow(db.shop_price_a(), float(_shop_visit_buys)))))
return _spend_discounted(int(ceil(db.shop_relic_base_price() * pow(db.shop_price_b(), float(maxi(0, int(level_row.level_index) - 1))) * pow(db.shop_price_a(), float(_shop_visit_buys)))))
return 999999
var base := float(maxi(1, int(row.get("base_price", 1))))
var price_f: float = base * pow(db.shop_price_b(), float(maxi(0, int(level_row.level_index) - 1))) \
+15 -1
View File
@@ -5,9 +5,23 @@
class_name MinesweeperRules
extends RefCounted
## 揭示事实流:每格首次进入已开集恰发一次(已开格零事件)。
## 载荷键集(四处发射位并集):cell: Vector2i/kind: "safe"|"mine"/digit: int(安全格邻雷数;雷格 -1)/
## variant_ids: Array(地形变异身份,雷格空集)/channel: "initial"|"dig"|"chain"|"fx_reveal"/
## resource: bool(格面资源兑付资格:initial=false,其余 true)/dug_under_seal: bool(首格带封印直挖标记——
## initial 通道不发此键)。
## 发送时点:initial=install_board 初始展开逐格;dig/chain=_do_dig 揭示闭包内(本格 AP 扣费在其后);
## fx_reveal=fx_reveal 闭包内(零 AP 消耗零相位要求)。
## 消费方依赖:战斗结算 _on_cell_revealed 以 resource 分流缓冲(fx_reveal 由兑付点直入账,禁二次缓冲)、
## 以 kind 驱动积点计数、以 cell 取奖励快照——禁改键名与语义;事件序 cell_revealed 批先于 seal_judged 批。
signal cell_revealed(evt: Dictionary)
## 封印判定事实流:轮末事务开始时的封印位逐格判定(行主序确定性枚举,被拔的不判)。
## 载荷键集:cell: Vector2i(封印格)/hit: bool(该格是否为雷)。
## 发送时点:停手路径 _do_stop 委托行动经济之前;棋盘路径揭示闭包收尾(phase 非 Active 时)——
## 全路径统一先于 round_end 发射(事件序契约 seal_judged < round_end)。
## 消费方依赖:战斗结算 _on_seal_judged 收批后统一开封印命中单——载荷缺键即错判,禁改键名与语义。
signal seal_judged(evt: Dictionary)
signal round_end_board(payload: Dictionary) # 双发射源之棋盘路径(排雷玩法 外发;stopped 由 行动经济 直发)
signal round_end_board(payload: Dictionary) ## 双发射源之棋盘路径(排雷玩法 外发;stopped 由 行动经济 直发)
var db: ConfigDb
var action_economy: ActionEconomy
+4 -4
View File
@@ -56,9 +56,9 @@ func apply_effect(effect_id: String, source: Dictionary, duration_override: int
"stack":
for inst in _instances:
if inst.effect_id == effect_id:
if inst.layers >= max_stack:
if int(inst.layers) >= max_stack:
return false # 满层拒绝原子——容器零写入(拒收由战斗结算记"奖励未生效"行)
inst.layers += 1 # stack 授予不触碰 R(spec 决议)
inst.layers = int(inst.layers) + 1 # stack 授予不触碰 R(spec 决议)
return true
_instances.append(_new_instance(effect_id, 1, dur, source, snap_m, snap_unit, snap_display))
return true
@@ -99,7 +99,7 @@ func query_active(side: String) -> Array:
"display_name": inst.snap_display,
"snap_magnitude": inst.snap_m,
"snap_unit": inst.snap_unit,
"effective_value": inst.layers * inst.snap_m, # L×m 加算(percent 越域不 clamp——收口归 战斗结算)
"effective_value": int(inst.layers) * int(inst.snap_m), # L×m 加算(percent 越域不 clamp——收口归 战斗结算)
})
return out
@@ -111,7 +111,7 @@ func decay_round() -> int:
var k := 0
var survivors: Array[Dictionary] = []
for inst in _instances:
inst.remaining -= 1
inst.remaining = int(inst.remaining) - 1
if inst.remaining <= 0:
k += 1
else:
-8
View File
@@ -52,14 +52,6 @@ func inventory_count() -> int:
return _items.size()
# 持有数(同调位上限判定读数:只计未消耗件;Used 不可逆即不再持有)
# 同调位守卫谓词(读取源 transitional_sync_slots 过渡键已随表退役;
# 容量闸现行事实源 = 营地装配面 Attunement.slots 满位禁选。函数体保留空壳防四散引用误删遗漏——
# 冒烟「过渡键读取点归零」断言覆盖)
# 持有件记录(奖励节点替换子交互数据源:只列未消耗件,handle 定位丢弃目标)
# 丢弃(同调位替换交互节点渠道——腾位收新;与 Used 消费分轨的一次性移除,快照不再含)
# 整图零信息可用性预判(图标置灰/入口禁用判据——GDD「整张图都没东西可看」级):
# 真视恒可用(零雷窗口=信息非空);AP 类恒可用(组合判据取 OR——任一效果有信息即可用);
# 纯预读行 = 板上还有未开格才可用。(积点件不入使用面——自动兑付无玩家入口)
+3 -3
View File
@@ -52,7 +52,7 @@ func spawn(level_index: int, node_depth: int, hero_hp: int, unit_count_overrides
return false
# max_hp = 生效上限(非入参 hero_hp)——跨战斗携带 HP 时上限不得压扁
# (恢复点/治疗包的满血守卫与治疗上限随之失效)
_hero = {"hp": hero_hp, "max_hp": eff_max_hp, "atk": eff_atk, "is_hero": true, "alive": true, "display_name": "探险者", "enemy_id": "hero", "armor_type": hero_armor_type if hero_armor_type != "" else db.forge_default_armor_type(), "attack_type": hero_attack_type if hero_attack_type != "" else db.forge_default_attack_type()} # 护甲/攻击类型轴快照(读注入值,空串回退锻造工坊配置默认值 normal/light;甲效矩阵消费面)
_hero = {"hp": hero_hp, "max_hp": eff_max_hp, "atk": eff_atk, "is_hero": true, "alive": true, "display_name": db.hero_display_name(), "enemy_id": "hero", "armor_type": hero_armor_type if hero_armor_type != "" else db.forge_default_armor_type(), "attack_type": hero_attack_type if hero_attack_type != "" else db.forge_default_attack_type()} # 护甲/攻击类型轴快照(读注入值,空串回退锻造工坊配置默认值 normal/light;甲效矩阵消费面)
_enemies.clear() # 难度曲线与Boss DifficultyTable 修饰:施加于行值、行值快照之前——MVP 空表 = 恒 1.0 无行为(AC-10);混编行集内全部敌种同乘((L,D) 精确匹配一次取值——F9 同乘不改变异,敌人系统 v0.15)
var mod: Dictionary[Variant, Variant] = db.resolve_difficulty_modifiers(level_index, node_depth)
var hp_mult: float = float(mod.get("hp_mult", 1.0))
@@ -254,7 +254,7 @@ func apply_damage(u: Dictionary, n: int) -> bool:
if u.is_empty() or _despawned or not u.alive:
_diag("apply_damage 失效/死亡句柄")
return false
u.hp = maxi(0, u.hp - n)
u.hp = maxi(0, int(u.hp) - n)
if u.hp == 0:
u.alive = false # Dead 不可逆(死亡判定时机归 战斗结算)
unit_died.emit(u) # 死亡事件——单位自行宣布
@@ -267,7 +267,7 @@ func heal(u: Dictionary, n: int) -> int:
_diag("heal 非法/失效句柄")
return 0
var actual: int = mini(n, int(u.max_hp) - int(u.hp))
u.hp += actual
u.hp = int(u.hp) + actual
return actual
+3
View File
@@ -8,6 +8,7 @@ extends Resource
@export var max_hp: int = 50 # [1,999] knob(provisional,溯源 = 原型锚)
@export var atk: int = 6 # [1,999] knob——基础攻击(溯源 = 概念原型 HERO_BASE_ATK,provisional 归 难度曲线与Boss);消费面 = 战斗结算·战斗段先手攻击
@export var armor_class: int = 10 # [0,999] knob——防守方护甲等级基准(暂定 10 归 难度曲线与Boss);消费面 = 战斗结算掷点判定阈值基数(ac_up 效果加算其上)
@export var display_name: String = "探险者" # 英雄单位显示名(与敌人行 display_name 同轴;单位快照读取面单源)
func validate() -> Array:
@@ -18,4 +19,6 @@ func validate() -> Array:
errs.append({"category": "domain", "message": "atk 越域 [1,999]: %d" % atk})
if armor_class < 0 or armor_class > 999:
errs.append({"category": "domain", "message": "armor_class 越域 [0,999]: %d" % armor_class})
if display_name.strip_edges() == "":
errs.append({"category": "domain", "message": "display_name 为空"})
return errs
+6
View File
@@ -12,6 +12,8 @@ extends Resource
@export var pool_shelf: Array[Dictionary] = [] # Array[Dictionary] 键 kind_id
@export var price_a: float = 0.0 # 进店已购次数爬价因子(provisional——0 = 未投值即致命域)
@export var price_b: float = 0.0 # 跨关递增因子(provisional)
@export var relic_base_price: float = 15.0 # relic 保底卡基价(provisional——池令牌非货架种,词表外专枝)
@export var spend_discount: float = 0.8 # 财富之道·散 economy_spend 激活时的购买折扣(provisional)
const GP_TIERS := ["normal", "elite", "boss"]
const PRODUCT_ATTRS := ["atk", "max_hp", "armor_class"]
@@ -102,4 +104,8 @@ func validate() -> Array:
errs.append({"category": "domain", "message": "price_a 越域 [1.0,3.0]: %f" % price_a})
if price_b < 1.0 or price_b > 2.0:
errs.append({"category": "domain", "message": "price_b 越域 [1.0,2.0]: %f" % price_b})
if relic_base_price < 1.0 or relic_base_price > 99.0:
errs.append({"category": "domain", "message": "relic_base_price 越域 [1.0,99.0]: %f" % relic_base_price})
if spend_discount <= 0.0 or spend_discount > 1.0:
errs.append({"category": "domain", "message": "spend_discount 越域 (0.0,1.0]: %f" % spend_discount})
return errs
+1
View File
@@ -7,6 +7,7 @@ script = ExtResource("1")
max_hp = 26
atk = 2
armor_class = 10
display_name = "探险者"
; 〔2026-10-12 迭代 23 锻造工坊实装批 T2——两根类型轴删除(契约 2 所有权移交):armor_type/
; attack_type 两字段随本批从静态表删除,默认值归锻造工坊配置常驻持有(forge.tres
+2
View File
@@ -92,6 +92,8 @@ pool_shelf = [{
}]
price_a = 1.8
price_b = 1.1
relic_base_price = 15.0
spend_discount = 0.8
; GP+商店房实施批(2026-09-20 用户拍板,设计权威 design/research/proposal-gp-shop-room-2026-09-20.md 裁决记录):
; 掉落=均值±方差掷定(gp_drop 封闭流)+关级加值;精英行为预留行(当前无消费方,随精英节点内容批启用);
+2 -2
View File
@@ -4,8 +4,8 @@
## 加成生效值经注入 Callable 现算、随 encounter 显式传参——零表写。
extends RefCounted
signal battle_created(settlement, level: int, carry_hp: int) # begin_battle 前——组合根完成信号接线
signal battle_ready(settlement, level: int, carry_hp: int) # begin_battle 完成后——组合根 rebuild 棋盘 + 刷新呈现
signal battle_created(settlement, level: int, carry_hp: int) ## begin_battle 前——组合根完成信号接线
signal battle_ready(settlement, level: int, carry_hp: int) ## begin_battle 完成后——组合根 rebuild 棋盘 + 刷新呈现
var db
var rng # /root/RngService(组合根注入)
+3 -3
View File
@@ -4,9 +4,9 @@
class_name BoardView
extends Control
signal action_requested(cell: Vector2i, kind: String) # kind ∈ {dig, move, seal_place, seal_pull}
signal hover_forbidden(cell: Vector2i) # 非相邻未开格悬停(首遇 toast 归组合根去重)
signal chord_attempted(cell: Vector2i) # 双击快开尝试(chord 肌肉记忆提示锚)
signal action_requested(cell: Vector2i, kind: String) ## kind ∈ {dig, move, seal_place, seal_pull}
signal hover_forbidden(cell: Vector2i) ## 非相邻未开格悬停(首遇 toast 归组合根去重)
signal chord_attempted(cell: Vector2i) ## 双击快开尝试(chord 肌肉记忆提示锚)
const _CELL_UNIT := preload("res://presentation/views/board_cell_unit.gd")
const _BOARD_WORLD := preload("res://presentation/views/board_world.gd")
+6 -6
View File
@@ -7,12 +7,12 @@ extends Control
signal buy_requested(attr: String)
signal closed
signal attune_toggled(item_id: String) # 同调装配切换意图(写口唯一归 MetaProgression.toggle_attunement)
signal tree_buy_requested(node_id: String) # 天赋树购买意图(写口唯一归 MetaProgression.tree_buy)
signal tree_fork_toggled(group: String, side: String) # 双态切换意图(一节点双态——零币切态)
signal forge_buy_requested(route_id: String) # 锻造路线购买意图(写口唯一归 MetaProgression.forge_buy_level)
signal forge_unlock_requested(route_id: String) # 锻造路线解锁意图(写口唯一归 MetaProgression.forge_buy_unlock)
signal forge_select_requested(route_id: String) # 锻造路线选中意图(写口唯一归 MetaProgression.forge_select_weapon/armor)
signal attune_toggled(item_id: String) ## 同调装配切换意图(写口唯一归 MetaProgression.toggle_attunement)
signal tree_buy_requested(node_id: String) ## 天赋树购买意图(写口唯一归 MetaProgression.tree_buy)
signal tree_fork_toggled(group: String, side: String) ## 双态切换意图(一节点双态——零币切态)
signal forge_buy_requested(route_id: String) ## 锻造路线购买意图(写口唯一归 MetaProgression.forge_buy_level)
signal forge_unlock_requested(route_id: String) ## 锻造路线解锁意图(写口唯一归 MetaProgression.forge_buy_unlock)
signal forge_select_requested(route_id: String) ## 锻造路线选中意图(写口唯一归 MetaProgression.forge_select_weapon/armor)
# 方向稿取色(柜台头带深棕/行板暖米;双色墨=亮板配深墨)——值单点收 UiKit
const HEADER_BAND_COL := UiKit.CAMP_HEADER_BAND
+2 -2
View File
@@ -4,8 +4,8 @@
class_name DebugOverlay
extends Control
signal apply_requested(atk_pct: int, hp_pct: int) # 应用并关闭——加成落表归组合根单一应用路径
signal simulate_requested(count: int) # 批量模拟编排归组合根(表基线复位 + 单路径施加)
signal apply_requested(atk_pct: int, hp_pct: int) ## 应用并关闭——加成落表归组合根单一应用路径
signal simulate_requested(count: int) ## 批量模拟编排归组合根(表基线复位 + 单路径施加)
signal debug_levels_requested # 两层调试对局入口(编排归 debug_session.gd)
signal diagnose_requested # 诊断转储入口(卡死取证编排归组合根)
signal closed
@@ -8,7 +8,7 @@ const COL_INK := UiKit.INK
var _atk_label: Label
var _armor_label: Label
signal treasure_slot_clicked(handle: int) # 槽点击(组合根消费:AP 类即用/空间类进选中态)
signal treasure_slot_clicked(handle: int) ## 槽点击(组合根消费:AP 类即用/空间类进选中态)
var item_slots: HBoxContainer # 秘宝槽(库存条常读+两步交互第一步)
var _slot_panels: Array[Variant] = [] # 槽 PanelContainer 引用(选中高亮/置灰重刷)
var _selected_handle := 0 # 选中态(0=无;组合根回写维持单源)
+1 -1
View File
@@ -4,7 +4,7 @@
class_name ShopOverlay
extends Control
signal buy_requested(product_id: String) # legacy=product_id / pool=kind_id(组合根按模式分发)
signal buy_requested(product_id: String) ## legacy=product_id / pool=kind_id(组合根按模式分发)
signal leave_pressed
const ATTR_LABELS := {"atk": "攻击", "max_hp": "生命上限", "armor_class": "护甲"} # 档位文案轴(数据契约 attr 词表)
@@ -29,6 +29,7 @@ func _init() -> void:
_test_five_recipes_and_replay(db)
_test_policy_immediate_mode(db)
_test_source_death_void(db)
_test_identical_content_dual_enemy(db)
_test_requires_gate_and_zero(db)
_test_hero_death_halt(db)
_test_ac_merge(db)
@@ -211,6 +212,46 @@ func _test_source_death_void(db) -> void:
Kit.check("作废单零行零扣血", int(h.units.hero_unit().hp) == hero_before and _rows.is_empty())
# ---- 同内容双敌:单据归属按实例身份判定(生产 handle_id 使内容全同不可达——夹具显式钉住语义面) ----
# 单位字典键集复刻(剥离 handle_id:生产键集含句柄,`==` 误命的可达条件被破坏,夹具刻意不携句柄)
func _unit_fixture() -> Dictionary:
return {
"enemy_id": "fixture_enemy",
"display_name": "哥布林",
"hp": 5, "max_hp": 5, "atk": 1,
"pool": [], "pending_skill": "", "roll_seq": 0, "basic_streak": 0,
"armor_type": "light", "armor_class": 0, "attack_type": "normal",
"alive": true, "is_hero": false,
}
func _test_identical_content_dual_enemy(db) -> void:
var a := _unit_fixture()
var b := _unit_fixture()
Kit.check("前置:双敌字典内容全同且非同一实例", a == b and not is_same(a, b))
var h = _harness(db)
if not h.spawn_ok:
return
var ticket: Dictionary = h.ctrl.open_ticket(DamageControllerScript.KIND_BOARD_FLAT_HERO, "mine_hit",
"同容双敌单", "棋盘兑付", 1, DamageControllerScript.TARGET_HERO, 1)
ticket["source_unit"] = a
var t: int = h.ctrl.submit(ticket)
h.units.unit_died.emit(b) # 真实死亡事件(单位自行宣布)——同容异实例
h.ctrl.flush()
Kit.check("同内容异实例敌死亡不误废他人单据〔wd1〕", h.ctrl.outcome(t) == "redeemed")
var h2 = _harness(db)
if not h2.spawn_ok:
return
var ticket2: Dictionary = h2.ctrl.open_ticket(DamageControllerScript.KIND_BOARD_FLAT_HERO, "mine_hit",
"同容双敌单·自亡", "棋盘兑付", 1, DamageControllerScript.TARGET_HERO, 1)
ticket2["source_unit"] = a
var t2: int = h2.ctrl.submit(ticket2)
h2.units.unit_died.emit(a)
h2.ctrl.flush()
Kit.check("来源敌自身死亡单据作废(正向对照)〔wd1〕", h2.ctrl.outcome(t2) == "voided")
# ---- requires 前置门:前置单未实际有效兑付 → 后继单静默作废;无存活敌 blast = 合法零兑付 ----
func _test_requires_gate_and_zero(db) -> void:
@@ -157,7 +157,7 @@ func _smoke_board_generator() -> void:
if b1 == null:
continue
Kit.check("L%d clearable(安全格 8 邻连通)" % L, board_generator.is_clearable(b1))
Kit.check("L%d legality 全过" % L, board_generator.legality(b1, row, _db))
Kit.check("L%d legality 全过" % L, board_generator.legality(b1, row))
var inf := float(b1.metrics.get("inferability", 0.0))
var mode := String(b1.metrics.get("accept_mode", ""))
print(" L%d 实测:inferability=%.3f / attempts_used=%d / accept_mode=%s / target=%.2f" % [
@@ -18,6 +18,7 @@ func _init() -> void:
quit(Kit.finish("board_generator_test"))
return
_test_generate_shape_and_mines()
_test_target_mine_count_single_source()
_test_digits_equal_neighbor_mine_count()
_test_same_seed_bit_identical()
_test_clearable_and_inferability_selfconsistent()
@@ -54,6 +55,34 @@ func _test_generate_shape_and_mines() -> void:
Kit.check("生成期指标集已落盘(metrics 非空)", not b.metrics.is_empty())
# 目标雷数公式单点(d14):同参同值对拍原式 + 真实产图指标读数同源
func _test_target_mine_count_single_source() -> void:
var bad := 0
var checked := 0
for L in [1, 2, 3]:
var row: Dictionary = _db.resolve_board_gen(L, 0)
if row.is_empty():
continue
var params := {
"w": int(row.board_width), "h": int(row.board_height),
"density": float(row.mine_density),
}
var from_fn: int = BoardGeneratorScript.target_mine_count(params)
var from_expr: int = int(round(float(params.density) * float(params.w * params.h - BoardData.SPAWN_AREA_CELLS)))
checked += 1
if from_fn != from_expr:
bad += 1
Kit.check("target_mine_count 与原式同参同值(%d 档对拍全等)〔wd14〕" % checked, checked > 0 and bad == 0)
var b := _generate(1, 0, 74000)
if b == null:
Kit.check("生成返回棋盘(雷数单点段)", false)
return
var row1: Dictionary = _db.resolve_board_gen(1, 0)
Kit.check("产物断言读数 = target_mine_count 同源(metrics.target_mines)〔wd14〕",
int(b.metrics.get("target_mines", -1)) == BoardGeneratorScript.target_mine_count({
"w": int(row1.board_width), "h": int(row1.board_height), "density": float(row1.mine_density)}))
func _test_digits_equal_neighbor_mine_count() -> void:
var b := _generate(1, 1, 72000)
if b == null:
@@ -242,6 +242,7 @@ func _init() -> void:
if d.severity == "error" or d.severity == "fatal":
push_error(" [%s] %s: %s" % [d.severity, d.category, d.message])
# 套执行序沿源编排(基点 _run_core_smoke 调用序——本组套的相对序)
_smoke_table_migration()
_smoke_level()
_smoke_level_graph()
_smoke_gp_wallet()
@@ -254,6 +255,20 @@ func _init() -> void:
quit(Kit.finish("level_shop_economy_test"))
# ---- 数值入表等价证明(d11/d16a——原字面量迁移:表值 == 迁移前字面量,消费端读表)----
func _smoke_table_migration() -> void:
print("--- 数值入表(shop 两 knob + hero 显示名 · 原值迁移)---")
Kit.check("shop.relic_base_price 表值 == 迁移前字面量(15.0)〔wd11〕", _db.shop_relic_base_price() == 15.0)
Kit.check("shop.spend_discount 表值 == 迁移前字面量(0.8)〔wd11〕", _db.shop_spend_discount() == 0.8)
Kit.check("hero.display_name 表值 == 迁移前字面量(探险者)〔wd16〕", _db.hero_display_name() == "探险者")
# 消费端读表:spawn 真实构造链的英雄单位显示名与表值同
var units = load("res://core/unit_container.gd").new(_db)
var spawned: bool = units.spawn(1, 0, int(_db.hero.max_hp))
Kit.check("spawn 后英雄单位 display_name 读表同值(探险者)〔wd16〕",
spawned and String(units.hero_unit().get("display_name", "")) == _db.hero_display_name())
# ---- 生成图深度扩面覆盖断言(迭代 35 卡② S5/V11——节点源=卡① 生成器产图,
# 敌侧/板侧解析一律走 ConfigDB 真实消费链,禁直读行集自算)----
@@ -1274,15 +1289,19 @@ func _smoke_shop_pool() -> void:
var r8 = scen["r"]
# ② 货架投影形制:kind 词表封闭 + 现价公式推导(ceil(base×b^(关序−1)×A^0)——核心现算,呈现零复算)
var l2 := int(r8.level_row.level_index)
# 现价等价证明(d11——消费端读表推导 vs 迁移前硬编码字面量推导:同输入逐位一致)
var relic_price_now: int = r8.pool_price("relic")
var relic_price_pre: int = int(ceil(15.0 * pow(_db.shop_price_b(), float(l2 - 1)) * 1.0))
Kit.check("relic 现价 = ceil(15.0×b^(关序−1))(读表与迁移前字面量逐位一致,%d)〔wd11〕" % relic_price_now, relic_price_now == relic_price_pre)
for c in r8.shop_shelf():
# T6 商店保底后 relic 卡合法入架(保底占位)——词表断言放行 relic;价格断言分叉
# (数值卡走 shop_pool_row 推导 / relic 卡走 15 固定锚——同公式不同基价)
# (数值卡走 shop_pool_row 推导 / relic 卡走表 knob relic_base_price——同公式不同基价)
var ckind := String(c.get("kind_id", ""))
var is_relic := ckind == "relic"
Kit.check("货架 kind 在封闭词表或保底 relic 卡(%s)" % ckind, is_relic or shelf_kinds.has(ckind))
var expect_p: int
if is_relic:
expect_p = int(ceil(15.0 * pow(_db.shop_price_b(), float(l2 - 1)) * 1.0))
expect_p = int(ceil(_db.shop_relic_base_price() * pow(_db.shop_price_b(), float(l2 - 1)) * 1.0))
else:
var prow: Dictionary = _db.shop_pool_row(ckind)
expect_p = int(ceil(float(int(prow.get("base_price", 0))) * pow(_db.shop_price_b(), float(l2 - 1)) * 1.0))