feat(terrain-variation): 地形变异首批类型定稿批——四类型两轴×两关落地(台账 92)
- 首批四类型(2026-09-19/20 用户拍板):荆棘丛/藤蔓缠绕(L1 dungeon 平原密林)+ 岩浆/烬覆(L2 underdark 幽暗地域);伤害型=挖开轮末结算扣血(1/2 点),信息型=呈现层数字遮蔽 - 机制枚举化:effect/trigger/placement 三枚举 + 跨字段互锁(schema 重写),新增类型=加行不改代码 - 布置段移排战斗初始化第 4 步(生成棋盘→打开状态初始化→资源格→变异格,用户拍板顺序):_place_variants 数量=5%+每关+2、连通片≤4、安全格减展开区减雷格、每图单类型、variant_place 新封闭流 - biome_tags→board_types 更名(用户裁定对齐 board_type 词表);counter_items 字段位预留归 Alpha 反制批 - 冒烟 353 项全绿(+22 项变异断言);四场景探针全过;600 局×2 关模拟读数 L1 91.3/93.3、L2 78.3/81.7,承伤带全过 - 文档收口:地形变异 v0.3、棋盘生成四阶段化、战斗结算随机消费六处、registry variation_table/variant_pool_gate/rng_service、06 清单四条目收口+两条目改排 Alpha、00/02/03/05 过期提法清理
This commit is contained in:
1 parent
87fec0278a
commit
ce47064627
20 files changed
+448
-160
No files matched your search
@@ -150,6 +150,9 @@ func _install_new_board(during_round := false) -> bool:
|
||||
if not _reward_economy.roll_board(eligible):
|
||||
_fatal("地图战斗资源 roll_board 被拒")
|
||||
return false
|
||||
# 地形变异 布置段(2026-09-19 用户拍板棋盘设置顺序第 4 步:生成棋盘 → 打开状态初始化 →
|
||||
# 战斗资源格设置 → 变异格设置)——布置在初始展开与资源格之后,变异天然不落已开区
|
||||
_place_variants(board)
|
||||
_flag_consumed.clear() # 换图新雷 = 新实例,资格自然重置(行动经济 Rule 8)
|
||||
_flag_miss_seen.clear()
|
||||
_perfect_hit_true = 0 # 板级完美计数随 install 重置
|
||||
@@ -160,6 +163,58 @@ func _install_new_board(during_round := false) -> bool:
|
||||
return true
|
||||
|
||||
|
||||
# ---- 地形变异 布置段(首批类型定稿批 2026-09-19——棋盘设置顺序第 4 步)----
|
||||
# 候选池门控(unlock_level ≤ 关卡序号 + board_types 与本地形交集,∅ 通配)经 config_db.variant_pool;
|
||||
# 池空 = 合法态,静默零布置(框架 P0 口径不变)。零特判:本段只写格子变异数据快照,不改扫雷规则本体。
|
||||
# 候选格域 = 安全格 − 初始展开区(伤害型 placement=closed_only 的结构保障——布置发生在初始展开之后,
|
||||
# 已开格天然排除);雷格不吃变异(挖雷一层惩罚 + 变异伤害双层叠罚污染逐行归因,首版排除——provisional)。
|
||||
# 每图至多一种类型(候选池内均匀选一,混型布置归因复杂留待后续批);数量 = round(总格数×percent) +
|
||||
# (关卡序号−1)×per_level_bonus;连通片生长(8 邻),单片 ≤ pack_max(2×2 / 1×4 / 1×3+1 等形)。
|
||||
func _place_variants(board: BoardData) -> void:
|
||||
var pool: Array = db.variant_pool(int(encounter.level_index), String(board.board_type))
|
||||
if pool.is_empty():
|
||||
return
|
||||
var r0 := {}
|
||||
for c in board.compute_r0():
|
||||
r0[c] = true
|
||||
var candidates: Array = []
|
||||
for c in board.safe_cells():
|
||||
if not r0.has(c):
|
||||
candidates.append(c)
|
||||
if candidates.is_empty():
|
||||
return
|
||||
var stream: RandomNumberGenerator = rng.open_stream("variant_place") # 一板恰一 scope(换图/刷新图各取号一次)
|
||||
var vrow: Dictionary = pool[stream.randi_range(0, pool.size() - 1)]
|
||||
var total: int = int(round(float(board.width * board.height) * float(db.variation.percent_of_cells) / 100.0)) \
|
||||
+ (int(encounter.level_index) - 1) * int(db.variation.per_level_bonus)
|
||||
total = mini(total, candidates.size())
|
||||
var placed := {}
|
||||
while placed.size() < total:
|
||||
var pack_size: int = mini(total - placed.size(), stream.randi_range(2, int(db.variation.pack_max)))
|
||||
var free: Array = []
|
||||
for c in candidates:
|
||||
if not placed.has(c):
|
||||
free.append(c)
|
||||
if free.is_empty():
|
||||
break
|
||||
var seed_c: Vector2i = free[stream.randi_range(0, free.size() - 1)]
|
||||
placed[seed_c] = true
|
||||
var pack: Array = [seed_c]
|
||||
while pack.size() < pack_size:
|
||||
var fringe: Array = []
|
||||
for c in pack:
|
||||
for n in board.neighbors8(c):
|
||||
if candidates.has(n) and not placed.has(n) and not fringe.has(n):
|
||||
fringe.append(n)
|
||||
if fringe.is_empty():
|
||||
break # 生长域耗尽——该片就地收束(数量不足总目标为合法态,候选域碎片化时触发)
|
||||
var nxt: Vector2i = fringe[stream.randi_range(0, fringe.size() - 1)]
|
||||
placed[nxt] = true
|
||||
pack.append(nxt)
|
||||
for c in placed.keys():
|
||||
board.variants[c] = [String(vrow.variant_id)]
|
||||
|
||||
|
||||
# ---- 事件缓冲(轮 facts 收集;结算触发 = Waiting 相位编排点,不依赖到达序)----
|
||||
|
||||
func _on_cell_revealed(evt: Dictionary) -> void:
|
||||
@@ -219,6 +274,18 @@ func _settle() -> void:
|
||||
# 插旗不可逆四情况②:插雷格再挖开 = 只受挖雷一层惩罚(mine_hit 照常兑付——惊动巢穴),
|
||||
# 标中不发(旗已随挖掘擦除、不入轮末判定基线)。
|
||||
_grant_routed(snap, "negative", "mine_hit", evt.cell, ri, "")
|
||||
# 地形变异 兑付段(授予段尾——触发枚举 on_open:挖开即伤、chain 通道同口径,轮末结算逐行兑现;
|
||||
# 信息型 hide_number 不进结算——呈现层读取,见 board_view 数字遮蔽;雷格恒无变异——布置段排除,
|
||||
# 双层叠罚不存在;插旗格直挖照吃变异伤害——地形后果同 mine_hit 口径,非奖励不作废)
|
||||
for evt in _facts_reveals:
|
||||
var vids: Array = evt.get("variant_ids", [])
|
||||
for vid in vids:
|
||||
var vrow: Dictionary = db.variation.row_by_id(String(vid))
|
||||
if String(vrow.get("effect", "")) != "flat_damage":
|
||||
continue # 触发词表按 effect 路由:flat_damage=on_open 兑现;hide_number/on_open 外组合 = 数据行新词表未接线(显式忽略面)
|
||||
var dmg: int = int(vrow.get("effect_value", 0))
|
||||
_enemy_system.apply_damage(_enemy_system.hero_unit(), dmg)
|
||||
_emit_row("damage", "地形变异", evt.cell, ri, "%s → 主角" % String(vrow.get("display_name", vid)), dmg, {"target": "hero", "variant_id": String(vid)}, "")
|
||||
for evt in _facts_flags:
|
||||
_judge_flag(evt, ri)
|
||||
if _facts_path == "cleared":
|
||||
|
||||
@@ -301,14 +301,15 @@ func _ceil_key(sorted_keys: Array, request: int) -> int:
|
||||
return -1
|
||||
|
||||
|
||||
# ---- 地形变异 variant_pool_gate:候选池门控(∅ biome_tag = 通配)----
|
||||
# ---- 地形变异 variant_pool_gate:候选池门控(∅ board_types = 通配;2026-09-19 用户裁定字段名对齐
|
||||
# board_type 词表——biome_tags 系旧观念残留退役)----
|
||||
|
||||
func variant_pool(level_index: int, board_type: String) -> Array:
|
||||
var pool: Array = []
|
||||
for row in variation.rows:
|
||||
if int(row.get("unlock_level", 999)) > level_index:
|
||||
continue
|
||||
var tags: Array = row.get("biome_tags", [])
|
||||
var tags: Array = row.get("board_types", [])
|
||||
if tags.is_empty():
|
||||
pool.append(row)
|
||||
continue
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
# 取号语义:全新实例首个 open_stream = scope 0(地图战斗资源 预推进术语的登记口径)。
|
||||
extends Node
|
||||
|
||||
const STREAM_NAMES := ["board_gen", "reward_roll", "encounter", "armor_roll", "behavior_roll"] # 2026-09-12 增 encounter(遭遇随机化批);2026-09-16 增 armor_roll(护甲等级掷点批)+ behavior_roll(敌人技能池批——开轮摇号,每敌派生独立子流,registry rng_service 条目同步)
|
||||
const STREAM_NAMES := ["board_gen", "reward_roll", "encounter", "armor_roll", "behavior_roll", "variant_place"] # 2026-09-12 增 encounter(遭遇随机化批);2026-09-16 增 armor_roll(护甲等级掷点批)+ behavior_roll(敌人技能池批——开轮摇号,每敌派生独立子流,registry rng_service 条目同步);2026-09-19 增 variant_place(地形变异首批类型定稿批——变异布置流,一板恰一 scope)
|
||||
|
||||
# 分段表达式书写(数据配置 R2 实现级规约——禁 ≥2^63 完整字面量)
|
||||
const GOLDEN_RATIO := (0x9E3779B9 << 32) | 0x7F4A7C15 # 0x9E3779B97F4A7C15
|
||||
@@ -16,6 +16,7 @@ const SALT_REWARD_ROLL := (0xE7037ED1 << 32) | 0xA0B428DB # 0xE7037ED1A0B428DB
|
||||
const SALT_ENCOUNTER := (0x8EBC6AF0 << 32) | 0x9C88C6E3 # 0x8EBC6AF09C88C6E3(遭遇骰——registry stream_seed_derivation 权威值;旧值 0x5DEECE66D<<32 超 int64 被截断,黄金向量漂移)
|
||||
const SALT_ARMOR_ROLL := (0xC2B2AE3D << 32) | 0x27D4EB2F # 0xC2B2AE3D27D4EB2F(护甲掷点流——互异奇常数 ≠ 乘数;黄金向量见冒烟/registry 同批登记)
|
||||
const SALT_BEHAVIOR_ROLL := (0x85EBCA77 << 32) | 0x58C24E15 # 0x85EBCA7758C24E15(技能摇号流——互异奇常数 ≠ 乘数;黄金向量见冒烟/registry 同批登记)
|
||||
const SALT_VARIANT_PLACE := (0x9AE16A3F << 32) | 0x2FAD6C39 # 0x9AE16A3F2FAD6C39(变异布置流——互异奇常数 ≠ 乘数;地形变异首批类型定稿批 2026-09-19)
|
||||
# behavior_roll 派生向量(registry 同批登记):子流 scope = spawn_index × STRIDE + 单敌摇号序(0 起)。
|
||||
# 每敌消费自己的 scope 段 → 单敌出招轨迹可独立复放、敌间互不穿插(proposal 约束二)。
|
||||
const BEHAVIOR_SUBSTREAM_STRIDE := 1000000
|
||||
@@ -27,7 +28,7 @@ const INT64_MIN := -9223372036854775808
|
||||
var _master_seed: int = -1 # -1 = 未播种(取数即致命,EC7)
|
||||
var master_seed: int:
|
||||
get: return _master_seed
|
||||
var _counters := {"board_gen": 0, "reward_roll": 0, "encounter": 0, "armor_roll": 0, "behavior_roll": 0}
|
||||
var _counters := {"board_gen": 0, "reward_roll": 0, "encounter": 0, "armor_roll": 0, "behavior_roll": 0, "variant_place": 0}
|
||||
|
||||
|
||||
func _salt_for(stream: String) -> int:
|
||||
@@ -42,6 +43,8 @@ func _salt_for(stream: String) -> int:
|
||||
return SALT_ARMOR_ROLL
|
||||
"behavior_roll":
|
||||
return SALT_BEHAVIOR_ROLL
|
||||
"variant_place":
|
||||
return SALT_VARIANT_PLACE
|
||||
push_error("[RngService] 未知名流: %s" % stream)
|
||||
assert(false, "未知名流(流集合封闭,EC12)")
|
||||
return 0
|
||||
@@ -63,10 +66,11 @@ func set_master_seed(seed_val: int) -> void:
|
||||
if seed_val < 0:
|
||||
seed_val = -seed_val
|
||||
push_warning("[RngService] master_seed 负输入 abs 归一化: %d" % seed_val)
|
||||
assert(_counters["board_gen"] == 0 and _counters["reward_roll"] == 0 and _counters["encounter"] == 0 and _counters["armor_roll"] == 0 and _counters["behavior_roll"] == 0, "冒险中途重播种会击穿同冒险无碰撞前提(仅许全部流计数器全零时调用)")
|
||||
assert(_counters["board_gen"] == 0 and _counters["reward_roll"] == 0 and _counters["encounter"] == 0 and _counters["armor_roll"] == 0 and _counters["behavior_roll"] == 0 and _counters["variant_place"] == 0, "冒险中途重播种会击穿同冒险无碰撞前提(仅许全部流计数器全零时调用)")
|
||||
_master_seed = seed_val
|
||||
_counters["board_gen"] = 0
|
||||
_counters["reward_roll"] = 0
|
||||
_counters["variant_place"] = 0
|
||||
|
||||
|
||||
# 切片登记:冒险开始重播种入口(正式侧调用权归 冒险结构;切片主菜单 = 冒险起点)
|
||||
@@ -76,6 +80,7 @@ func new_adventure(seed_val: int) -> void:
|
||||
_counters["encounter"] = 0
|
||||
_counters["armor_roll"] = 0
|
||||
_counters["behavior_roll"] = 0
|
||||
_counters["variant_place"] = 0
|
||||
set_master_seed(seed_val)
|
||||
|
||||
|
||||
|
||||
@@ -1,16 +1,27 @@
|
||||
# VERTICAL SLICE - NOT FOR PRODUCTION
|
||||
# Validation Question: 玩家能否无指导在一场完整战斗中经历「开战→挖掘规划→轮末结算逐行兑现→胜/负」并逐行归因(P2)
|
||||
# Date: 2026-09-10
|
||||
# 数据配置 VariationTable schema(地形变异 回填值;行 = Dictionary,键 variant_id)。
|
||||
# 依据 design/archive/gdd-2026-09-14/a4-terrain-variation.md "VariationTable Schema 定稿"。
|
||||
# 数据配置 VariationTable schema(地形变异 首批类型定稿批 2026-09-19;行 = Dictionary,键 variant_id)。
|
||||
# 字段集(2026-09-19 用户裁定:枚举抽象,不写死类型):effect/effect_value/trigger/placement 四字段
|
||||
# 把"变异是什么、在哪出现、何时起作用"全部数据化——新增类型 = 清单加行 + 必要时扩充枚举词表,不改读取方。
|
||||
# biome_tags 更名 board_types(用户裁定:门控匹配对象即 board_type 词表,biome 系旧观念残留不沿用);
|
||||
# counter_items 字段位保留(反制道具绑定,归 Alpha 反制批回填,切片期恒空)。
|
||||
# info_mask/cell_constraint 两旧字段随枚举化退役(hide_number 并入 effect 词表,safe_nonzero 由
|
||||
# 布置域结构保障——初始展开区天然排除,见 battle_orchestrator._place_variants)。
|
||||
class_name VariationTable
|
||||
extends Resource
|
||||
|
||||
@export var rows: Array = [] # Array[Dictionary]
|
||||
# 布置参数(表头字段;数量公式 = round(总格数 × percent_of_cells / 100) + (关卡序号 − 1) × per_level_bonus,
|
||||
# 连通片生长、单片 ≤ pack_max——2026-09-19 用户拍板:图规模百分比 × 关卡递增,全连通、单片不超 4)
|
||||
@export var percent_of_cells: int = 5
|
||||
@export var per_level_bonus: int = 2
|
||||
@export var pack_max: int = 4
|
||||
|
||||
const SCOPES := ["region", "global"]
|
||||
const INFO_MASKS := ["none", "hide_numbers"]
|
||||
const CELL_CONSTRAINTS := ["any", "safe_nonzero"]
|
||||
const EFFECTS := ["none", "flat_damage", "hide_number"] # 效果词表:直伤 / 数字遮蔽(可扩充)
|
||||
const TRIGGERS := ["on_open", "on_number_shown"] # 触发词表:被打开即触发 / 打开且显示数字时触发(可扩充)
|
||||
const PLACEMENTS := ["closed_only", "any"] # 布置约束词表:只落布置时未开格 / 任意格(可扩充)
|
||||
|
||||
|
||||
func validate() -> Array:
|
||||
@@ -32,13 +43,27 @@ func validate() -> Array:
|
||||
errs.append({"category": "domain", "message": "%s unlock_level 越域 [1,999]: %d" % [vid, ul]})
|
||||
if not SCOPES.has(row.get("scope", "")):
|
||||
errs.append({"category": "enum", "message": "%s scope 出域: %s" % [vid, row.get("scope")]})
|
||||
if not INFO_MASKS.has(row.get("info_mask", "")):
|
||||
errs.append({"category": "enum", "message": "%s info_mask 出域: %s" % [vid, row.get("info_mask")]})
|
||||
if not CELL_CONSTRAINTS.has(row.get("cell_constraint", "")):
|
||||
errs.append({"category": "enum", "message": "%s cell_constraint 出域: %s" % [vid, row.get("cell_constraint")]})
|
||||
# 跨字段(地形变异 re-review 增补,加载期致命):hide_numbers ⇒ safe_nonzero
|
||||
if row.get("info_mask", "") == "hide_numbers" and row.get("cell_constraint", "") != "safe_nonzero":
|
||||
errs.append({"category": "cross_field", "message": "%s hide_numbers ⇒ cell_constraint 必为 safe_nonzero" % vid})
|
||||
if not EFFECTS.has(row.get("effect", "")):
|
||||
errs.append({"category": "enum", "message": "%s effect 出域: %s" % [vid, row.get("effect")]})
|
||||
if not TRIGGERS.has(row.get("trigger", "")):
|
||||
errs.append({"category": "enum", "message": "%s trigger 出域: %s" % [vid, row.get("trigger")]})
|
||||
if not PLACEMENTS.has(row.get("placement", "")):
|
||||
errs.append({"category": "enum", "message": "%s placement 出域: %s" % [vid, row.get("placement")]})
|
||||
# 跨字段互锁(加载期致命——枚举语义一致性随首批类型定稿批 2026-09-19 立闸):
|
||||
# 伤害型 = 挖开即伤 ⇒ 触发 on_open + 只落未开格(closed_only)+ 伤害量为加法核小整数
|
||||
if row.get("effect", "") == "flat_damage":
|
||||
if row.get("trigger", "") != "on_open":
|
||||
errs.append({"category": "cross_field", "message": "%s flat_damage ⇒ trigger 必为 on_open" % vid})
|
||||
if row.get("placement", "") != "closed_only":
|
||||
errs.append({"category": "cross_field", "message": "%s flat_damage ⇒ placement 必为 closed_only(只出现在未打开的格子)" % vid})
|
||||
var ev: int = int(row.get("effect_value", 0))
|
||||
if ev < 1 or ev > 9:
|
||||
errs.append({"category": "domain", "message": "%s effect_value 越域 [1,9](加法核小整数): %d" % [vid, ev]})
|
||||
# 信息型 = 被打开且显示数字时起作用 ⇒ 触发 on_number_shown
|
||||
if row.get("effect", "") == "hide_number" and row.get("trigger", "") != "on_number_shown":
|
||||
errs.append({"category": "cross_field", "message": "%s hide_number ⇒ trigger 必为 on_number_shown" % vid})
|
||||
# 信息遮蔽型只允许落安全格(遮蔽对象是数字;雷格无数字可遮)——placement=any 时由读取方
|
||||
# 呈现面按 kind=safe 自然短路,此处不设布置约束
|
||||
return errs
|
||||
|
||||
|
||||
|
||||
@@ -4,8 +4,60 @@
|
||||
|
||||
[resource]
|
||||
script = ExtResource("1")
|
||||
rows = []
|
||||
percent_of_cells = 5
|
||||
per_level_bonus = 2
|
||||
pack_max = 4
|
||||
rows = [{
|
||||
"variant_id": "thorn",
|
||||
"display_name": "荆棘丛",
|
||||
"unlock_level": 1,
|
||||
"scope": "region",
|
||||
"effect": "flat_damage",
|
||||
"effect_value": 1,
|
||||
"trigger": "on_open",
|
||||
"placement": "closed_only",
|
||||
"board_types": ["dungeon"],
|
||||
"counter_items": []
|
||||
}, {
|
||||
"variant_id": "vine",
|
||||
"display_name": "藤蔓缠绕",
|
||||
"unlock_level": 1,
|
||||
"scope": "region",
|
||||
"effect": "hide_number",
|
||||
"effect_value": 0,
|
||||
"trigger": "on_number_shown",
|
||||
"placement": "any",
|
||||
"board_types": ["dungeon"],
|
||||
"counter_items": []
|
||||
}, {
|
||||
"variant_id": "magma",
|
||||
"display_name": "岩浆",
|
||||
"unlock_level": 2,
|
||||
"scope": "region",
|
||||
"effect": "flat_damage",
|
||||
"effect_value": 2,
|
||||
"trigger": "on_open",
|
||||
"placement": "closed_only",
|
||||
"board_types": ["underdark"],
|
||||
"counter_items": []
|
||||
}, {
|
||||
"variant_id": "ash",
|
||||
"display_name": "烬覆",
|
||||
"unlock_level": 2,
|
||||
"scope": "region",
|
||||
"effect": "hide_number",
|
||||
"effect_value": 0,
|
||||
"trigger": "on_number_shown",
|
||||
"placement": "any",
|
||||
"board_types": ["underdark"],
|
||||
"counter_items": []
|
||||
}]
|
||||
|
||||
; 地形变异 清单为空是合法态(地形变异 P0):零变体行 → 棋盘生成零布置,行为与不存在本系统时一致。
|
||||
; 首批变异类型(岩浆/冰冻,机制未定)定稿时在本清单加一行声明即应生效,无需改代码。雾格已整体
|
||||
; 移除(见 design/gdd/05-不做清单.md)——本表不再有 fog 行。
|
||||
; 地形变异 首批类型定稿批(2026-09-19 用户拍板)——四行两轴×两关:
|
||||
; 第一关 dungeon(平原/密林基线):荆棘丛(伤害型:挖开扣 1 点生命,轮末结算兑现)+ 藤蔓缠绕
|
||||
; (信息型:挖开且显示数字时数字被遮蔽,呈现层读取);第二关 underdark(幽暗地域):岩浆
|
||||
; (伤害型 2 点,登记名沿用)+ 烬覆(信息型)。unlock_level 门控 + board_types 交集门控双闸,
|
||||
; 首关候选池恒 {荆棘丛, 藤蔓缠绕},第二关候选池恒 {岩浆, 烬覆}。
|
||||
; counter_items 恒空:反制道具配套义务随框架契约保留,兑现归 Alpha 反制批(2026-09-19 用户拍板
|
||||
; 反制道具改排 Alpha——切片期变异暂无反制手段,纯税状态已知悉接受)。
|
||||
; 全部行值 provisional 归 难度曲线与Boss 调参批(effect_value/布置参数均为加法核小整数轴)。
|
||||
@@ -99,6 +99,8 @@ func _build_ui() -> void:
|
||||
|
||||
# 奖励徽章数据源:地图战斗资源 身份解析只读回调(已开格身份 icon——挖掘期 Active 相位内有效)
|
||||
board_view.set_identity_resolver(_identity_lookup)
|
||||
# 数字遮蔽数据源:地形变异 hide_number 解析只读回调(信息型呈现——board_view 渲染面读取)
|
||||
board_view.set_variant_mask_resolver(_variant_mask_lookup)
|
||||
|
||||
logger = PlaytestLogger.new()
|
||||
logger.ap_per_round = int(db.action_economy.ap_per_round)
|
||||
@@ -329,6 +331,20 @@ func _identity_lookup(c: Vector2i):
|
||||
return settlement.query_reward_identity(c)
|
||||
|
||||
|
||||
# 地形变异 数字遮蔽解析(信息型 hide_number——格带该效果变异即遮蔽;读取方按格子变异数据行动)
|
||||
func _variant_mask_lookup(c: Vector2i) -> bool:
|
||||
if settlement == null:
|
||||
return false
|
||||
var b = settlement.minesweeper.current_board()
|
||||
if b == null:
|
||||
return false
|
||||
for vid in b.variant_ids(c):
|
||||
var vrow: Dictionary = settlement.db.variation.row_by_id(String(vid))
|
||||
if String(vrow.get("effect", "")) == "hide_number":
|
||||
return true
|
||||
return false
|
||||
|
||||
|
||||
func _show_board_fx_for_node(node_type: String = "") -> void:
|
||||
# Boss 遭遇变体:紫赤影子爬棋盘一次性占位演出(§2.6)
|
||||
if node_type == "boss":
|
||||
|
||||
@@ -40,6 +40,7 @@ var _marker := {} # Vector2i → {"dot": ColorRect, "badge": Label, "h
|
||||
var _board: BoardData = null
|
||||
var _minesweeper = null # 排雷玩法 只读查询引用(refresh 更新;null = 无战斗)
|
||||
var _identity_cb := Callable() # 地图战斗资源 身份解析回调(组合根注入只读面)——Vector2i → Dictionary/null
|
||||
var _mask_cb := Callable() # 地形变异 数字遮蔽回调(组合根注入只读面)——Vector2i → bool(true = 该格数字不显示)
|
||||
var _diggable := {} # 可挖域(8 向相邻未开格)——呼吸微光作用集
|
||||
var _breath_t := 0.0
|
||||
var _dig_glow := false # 挖掘期才允许唯一在动元素(art-bible §2.1)
|
||||
@@ -92,6 +93,12 @@ func set_identity_resolver(cb: Callable) -> void:
|
||||
_identity_cb = cb
|
||||
|
||||
|
||||
# 组合根注入 地形变异 数字遮蔽只读回调(信息型 hide_number 触发面——触发枚举 on_number_shown:
|
||||
# 已开且显示数字时起作用;读取方按格子变异数据行动,规则本体零特判)
|
||||
func set_variant_mask_resolver(cb: Callable) -> void:
|
||||
_mask_cb = cb
|
||||
|
||||
|
||||
# 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 格高。
|
||||
@@ -313,7 +320,10 @@ func _paint_cell(c: Vector2i, hero: Vector2i) -> void:
|
||||
bg = COL_FLOOR
|
||||
var d: int = rec.get("digit", 0)
|
||||
if d > 0:
|
||||
text = str(d)
|
||||
if _mask_cb.is_valid() and bool(_mask_cb.call(c)):
|
||||
text = "?" # 数字遮蔽(地形变异 hide_number——信息型:该格真实数字不显示)
|
||||
else:
|
||||
text = str(d)
|
||||
else:
|
||||
if flag:
|
||||
bg = COL_BRICK
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
uid://cc2yhv5d20gfk
|
||||
@@ -68,6 +68,8 @@ func _run_core_smoke(db, rng) -> void:
|
||||
_smoke_board_generator()
|
||||
_smoke_layout_cell_formula()
|
||||
_smoke_variation_framework()
|
||||
_smoke_variant_placement(1)
|
||||
_smoke_variant_placement(2)
|
||||
_smoke_full_battle(1, 0, "L1 · 地精")
|
||||
_smoke_full_battle(2, 0, "L2 · 兄弟精")
|
||||
_smoke_level()
|
||||
@@ -484,7 +486,7 @@ func _smoke_board_generator() -> void:
|
||||
# 但必须打印实测值(正常出口);accept_worst 由下方 illegal_product 断言承担。
|
||||
check(inf >= float(row.inferability_target) or mode == "accept_best",
|
||||
"L%d inferability %.3f %s target %.2f(accept_mode=%s)" % [L, inf, "≥" if inf >= float(row.inferability_target) else "<", float(row.inferability_target), mode])
|
||||
check(b1.variants.is_empty(), "L%d 零变体格(地形变异 清单为空 → 生成期零布置)" % L)
|
||||
check(b1.variants.is_empty(), "L%d 生成器产物零变体格(变异布置归战斗初始化第 4 步——生成器管线不布置,2026-09-19 用户拍板棋盘设置顺序)" % L)
|
||||
check(b1.metrics.has("degraded_product") and b1.metrics.has("target_mines"), "L%d 指标集输出降级标记与目标雷数(三分出口表)" % L)
|
||||
check(b1.degraded_product == (mode == "accept_best"), "L%d 降级产物标记 = (accept_mode==accept_best)(%s→%s)" % [L, mode, str(b1.degraded_product)])
|
||||
check(b1.illegal_product == (mode == "accept_worst"), "L%d 非法产物标记 = (accept_mode==accept_worst)(%s→%s)" % [L, mode, str(b1.illegal_product)])
|
||||
@@ -506,13 +508,131 @@ func _mk_stream(stream_name: String, scope: int) -> RandomNumberGenerator:
|
||||
return rg
|
||||
|
||||
|
||||
# ---- 地形变异 框架面(P0 空清单合法 + 候选池门控通道在位——框架惰性非废弃)----
|
||||
# ---- 地形变异 框架面(首批类型定稿批 2026-09-19:四行两轴×两关 + 门控 + 跨字段互锁)----
|
||||
|
||||
func _smoke_variation_framework() -> void:
|
||||
print("--- 地形变异 框架面(空清单合法 + 候选池门控通道)---")
|
||||
check(_db.variation.rows.is_empty(), "变异清单为空(P0 合法态——零变体格)")
|
||||
check(_db.variant_pool(1, "dungeon").is_empty(), "候选池为空——棋盘生成静默产出无变异图(不报错、不警告)")
|
||||
print("--- 地形变异 框架面(首批四类型 + 候选池门控 + 跨字段互锁)---")
|
||||
check(_db.variation.rows.size() == 4, "变异清单 4 行(荆棘丛/藤蔓缠绕/岩浆/烬覆——首批类型定稿批)")
|
||||
check(_db.variation.validate().is_empty(), "变异表校验零错误(4 行全过域与跨字段互锁)")
|
||||
var pool1_ids: Array = []
|
||||
for r in _db.variant_pool(1, "dungeon"):
|
||||
pool1_ids.append(String(r.variant_id))
|
||||
check(pool1_ids == ["thorn", "vine"], "L1 dungeon 候选池 = 荆棘丛+藤蔓缠绕(unlock + board_types 双闸)")
|
||||
var pool2_ids: Array = []
|
||||
for r in _db.variant_pool(2, "underdark"):
|
||||
pool2_ids.append(String(r.variant_id))
|
||||
check(pool2_ids == ["magma", "ash"], "L2 underdark 候选池 = 岩浆+烬覆(第二关解锁)")
|
||||
check(_db.variant_pool(1, "underdark").is_empty(), "unlock 门控:L1 不进 underdark 行(岩浆/烬覆第二关解锁)")
|
||||
var pool2d_ids: Array = []
|
||||
for r in _db.variant_pool(2, "dungeon"):
|
||||
pool2d_ids.append(String(r.variant_id))
|
||||
check(pool2d_ids == ["thorn", "vine"], "board_types 门控:L2 dungeon 板池 = 荆棘丛+藤蔓缠绕(解锁累积;岩浆/烬覆被 board_types ∅ 交集排除)")
|
||||
check(_db.variation.row_by_id("nonexistent").is_empty(), "清单外 variant_id 返回空(读取通道在位)")
|
||||
# 跨字段互锁负样本(加载期致命——枚举语义一致性闸)
|
||||
var vt = load("res://data/schemas/variation_table.gd").new()
|
||||
vt.rows = [{"variant_id": "bad1", "display_name": "坏行甲", "unlock_level": 1, "scope": "region",
|
||||
"effect": "flat_damage", "effect_value": 1, "trigger": "on_open", "placement": "any",
|
||||
"board_types": [], "counter_items": []}]
|
||||
check(_errs_has_cross_field(vt.validate()), "跨字段互锁负样本①:flat_damage + placement=any 被拒(只出现在未打开的格子)")
|
||||
vt.rows = [{"variant_id": "bad2", "display_name": "坏行乙", "unlock_level": 1, "scope": "region",
|
||||
"effect": "hide_number", "effect_value": 0, "trigger": "on_open", "placement": "any",
|
||||
"board_types": [], "counter_items": []}]
|
||||
check(_errs_has_cross_field(vt.validate()), "跨字段互锁负样本②:hide_number + trigger≠on_number_shown 被拒")
|
||||
|
||||
|
||||
func _errs_has_cross_field(errs: Array) -> bool:
|
||||
for e in errs:
|
||||
if String(e.get("category", "")) == "cross_field":
|
||||
return true
|
||||
return false
|
||||
|
||||
|
||||
# ---- 地形变异 布置段(首批类型定稿批 2026-09-19——棋盘设置顺序第 4 步断言面)----
|
||||
# 布置发生在编排器战斗初始化(install 初始展开 + roll_board 资源格之后),生成器产物零变异——
|
||||
# 本段经 begin_battle 走完整初始化序后断言:门控一致性 / 布置域 / 数量公式 / 连通片上限 / 确定性。
|
||||
|
||||
func _smoke_variant_placement(L: int) -> void:
|
||||
print("--- 地形变异 布置段(L%d)---" % L)
|
||||
var rng = load("res://core/rng_service.gd").new()
|
||||
rng.set_master_seed(42)
|
||||
var settlement = load("res://core/battle_orchestrator.gd").new(_db, rng)
|
||||
if not settlement.begin_battle({"level_index": L, "node_depth": 0, "hero_hp": _db.hero.max_hp}):
|
||||
check(false, "L%d 布置段战斗启动失败" % L)
|
||||
return
|
||||
var b = settlement.minesweeper.current_board()
|
||||
check(b != null, "L%d 布置段板非空" % L)
|
||||
if b == null:
|
||||
return
|
||||
var pool_ids: Array = []
|
||||
for r in _db.variant_pool(L, String(b.board_type)):
|
||||
pool_ids.append(String(r.variant_id))
|
||||
var vcells: Array = b.variants.keys()
|
||||
var vid0 := ""
|
||||
var single_type := true
|
||||
var bad_id := false
|
||||
for c in vcells:
|
||||
var ids: Array = b.variants[c]
|
||||
if ids.size() != 1:
|
||||
single_type = false
|
||||
for vid in ids:
|
||||
if vid0 == "":
|
||||
vid0 = String(vid)
|
||||
elif vid0 != String(vid):
|
||||
single_type = false
|
||||
if not pool_ids.has(String(vid)):
|
||||
bad_id = true
|
||||
check(not bad_id, "L%d 变异 id 全在候选池(门控一致性:%s)" % [L, str(pool_ids)])
|
||||
check(single_type, "L%d 每图至多一种变异类型(单格单 id + 全图同 id)" % L)
|
||||
var r0 := {}
|
||||
for c in b.compute_r0():
|
||||
r0[c] = true
|
||||
var domain_ok := true
|
||||
for c in vcells:
|
||||
if r0.has(c) or b.is_mine(c):
|
||||
domain_ok = false
|
||||
check(domain_ok, "L%d 变异格全在初始展开区外且不吃雷格(布置域结构保障——closed_only)" % L)
|
||||
var expect_cap: int = int(round(float(b.width * b.height) * float(_db.variation.percent_of_cells) / 100.0)) \
|
||||
+ (L - 1) * int(_db.variation.per_level_bonus)
|
||||
check(vcells.size() <= expect_cap, "L%d 变异格数 %d ≤ 公式上限 %d(图规模百分比 + 关卡递增)" % [L, vcells.size(), expect_cap])
|
||||
check(vcells.size() > 0, "L%d 种子 42 板非零布置(候选池非空下应有遭遇面)" % L)
|
||||
var pack_max: int = int(_db.variation.pack_max)
|
||||
var vset := {}
|
||||
for c in vcells:
|
||||
vset[c] = true
|
||||
var seen := {}
|
||||
var pack_ok := true
|
||||
for c in vcells:
|
||||
if seen.has(c):
|
||||
continue
|
||||
seen[c] = true
|
||||
var comp_size := 1
|
||||
var q: Array = [c]
|
||||
while q.size() > 0:
|
||||
var x: Vector2i = q.pop_front()
|
||||
for n in b.neighbors8(x):
|
||||
if vset.has(n) and not seen.has(n):
|
||||
seen[n] = true
|
||||
comp_size += 1
|
||||
q.append(n)
|
||||
if comp_size > pack_max:
|
||||
pack_ok = false
|
||||
check(pack_ok, "L%d 全部连通片 ≤ %d 格(全连通生长、2×2/1×4/1×3+1 等形)" % [L, pack_max])
|
||||
# 布置确定性:全新 RngService 同种子重开,变异布置逐格相同(variant_place 流确定性)
|
||||
var rng2 = load("res://core/rng_service.gd").new()
|
||||
rng2.set_master_seed(42)
|
||||
var s2 = load("res://core/battle_orchestrator.gd").new(_db, rng2)
|
||||
s2.begin_battle({"level_index": L, "node_depth": 0, "hero_hp": _db.hero.max_hp})
|
||||
var b2 = s2.minesweeper.current_board()
|
||||
check(b2 != null and _variant_dump(b2) == _variant_dump(b), "L%d 同种子变异布置逐格相同(variant_place 流确定性)" % L)
|
||||
|
||||
|
||||
func _variant_dump(b) -> String:
|
||||
var keys: Array = b.variants.keys()
|
||||
keys.sort_custom(func(a, b2): return a.y * 1000 + a.x < b2.y * 1000 + b2.x)
|
||||
var parts: Array = []
|
||||
for c in keys:
|
||||
parts.append("%d,%d:%s" % [c.x, c.y, str(b.variants[c])])
|
||||
return "|".join(parts)
|
||||
|
||||
|
||||
func _board_dump(b) -> String:
|
||||
|
||||
@@ -106,8 +106,17 @@ func _ready() -> void:
|
||||
await get_tree().process_frame
|
||||
if main.board_view.board() == null:
|
||||
problems.append("L2 单场未启动(board=null)")
|
||||
elif not main.board_view.board().variants.is_empty():
|
||||
problems.append("L2 板出现变体格(地形变异 清单为空 → 零布置)")
|
||||
else:
|
||||
# 地形变异 首批类型定稿批(2026-09-19):L2 板允许带变异(岩浆/烬覆)——布置域断言 =
|
||||
# 变异格全在初始展开区外(closed_only 结构保障)
|
||||
var b0 = main.board_view.board()
|
||||
var r0p := {}
|
||||
for c in b0.compute_r0():
|
||||
r0p[c] = true
|
||||
for c in b0.variants.keys():
|
||||
if r0p.has(c):
|
||||
problems.append("L2 板变异格落在初始展开区(地形变异 布置域违例)")
|
||||
break
|
||||
main.board_view.set_desaturated(true)
|
||||
main.board_view.set_desaturated(false)
|
||||
if problems.is_empty():
|
||||
|
||||
Reference in new issue
Block a user