ginka-generator/docs/special-door-clean-design.md
2026-05-27 22:29:44 +08:00

360 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 机关门语义与数据集清洗修正文档
## 背景
当前数据集清洗已经能处理连续门团、连续怪团、无用分支和一部分闲置分支,但还有一类常见且重要的结构没有被正确表达:**机关门驱动的战斗房间**。
这类结构的共同点是:
1. 地图中存在机关门。
2. 门前或门附近会摆放一组怪物。
3. 这些怪物在局部上未必直接守护资源,也未必在拓扑上形成“打掉以后暴露新区域”的普通分支。
4. 但它们仍然有明确设计意义,因为它们承担了“开机关门”的推进语义。
一个典型例子可以概括为:上方是一扇机关门,下方是一个小房间,房间里放若干怪物和空地。若只按当前“邻居数”或“后侧资源收益”来判断,这些怪物很容易被视为闲置怪;但从实际玩法上看,它们显然不是噪声结构。
因此,这个问题本质上不是“过滤阈值太松或太紧”,而是**当前标签空间里没有把机关门作为独立语义类保留下来**,导致后续清洗算法看不到这层结构信息。
---
## 问题概述
### 当前异常现象
在当前规则下,存在一批地图会因为“闲置怪”而被过滤,但人工复核时会发现这些怪物实际上属于机关门房间的一部分。它们看起来像是:
1. 站在一片空地边缘。
2. 不直接守护资源。
3. 拓扑上可能只连接到一个邻接节点。
4. 但所在区域同时存在机关门,击败这些怪物本身就是推进条件的一部分。
也就是说,这些怪物虽然满足了“局部无影响分支”的几何特征,却**不满足“没有设计意义”这一真正的清洗目标**。
### 误判根因
这个问题目前至少有三层原因:
1. **标签层压扁**:机关门在原始塔数据中可被识别,但在当前主配置里没有变成独立 tile而是与普通门共用同一标签。
2. **拓扑层压扁**:拓扑图只区分 `Door``Enemy` 两种分支,不区分普通门和机关门。
3. **过滤层失语义**:闲置怪、重复守卫、无用分支等规则只能看到“门/怪/空地/资源”的局部拓扑,看不到“这个怪物与机关门联动”的语义。
只要这三层没有拆开,即使继续微调 `allowIdleBranch` 一类阈值,也很难稳定解决问题。
---
## 当前实现现状
### 转换层已经能识别机关门,但没有保留独立标签
当前实现中,原始塔数据里的 `specialDoor` 已经能在转换阶段被识别出来;这一点说明问题不是“完全不知道机关门存在”,而是**识别到了,但在标签化输出时被折叠掉了**。
现状可以概括为:
1. 转换阶段会把原始 `specialDoor` 映射到 `tiles.specialDoors[0]`
2. 但当前主配置里,`specialDoors` 与 `commonDoors` 实际共用了同一个标签值。
3. 共享常量里,`specialDoorTiles` 也与普通门使用同一个集合值。
这带来两个直接后果:
1. 训练数据里的地图矩阵无法区分“普通门”和“机关门”。
2. 当前的 `specialDoorCount` 统计并不是真正意义上的“机关门数量”,而只是复用了普通门的标签统计,观测值没有独立语义。
换句话说,当前代码里虽然存在 `specialDoors` 这一字段,但它还没有真正成为**独立的数据集类别**。
### 拓扑层只有 Door/Enemy 两类分支
当前拓扑图中的分支类型只有两种:
1. `Door`
2. `Enemy`
这意味着:
1. 连续门检测会把普通门和机关门都看成同一类门分支。
2. 闲置门检测也无法区分普通门和机关门。
3. 更重要的是,怪物侧根本无法知道“我所在的这个房间是否与机关门有关”。
对于本问题来说,真正缺失的并不是“门是不是门”,而是“这个门是不是机关门”。
### 当前闲置筛选为什么会误杀
现有闲置规则里,最敏感的是这两类:
1. `neighbors.size === 1` 的闲置分支规则。
2. 基于后侧收益缺失的无用分支规则。
它们的问题并不是逻辑错误,而是默认假设了:
1. 分支的价值主要来自局部守护资源。
2. 分支的价值主要来自是否暴露新区域。
但机关门关联怪的价值来自第三种来源:**全局或半全局的开门触发语义**。如果标签空间里没有 `Special Door`,这类语义就无法进入筛选逻辑,于是就会被误判成“看起来没用的怪”。
---
## 目标
本轮文档希望固定以下目标:
1. 在数据集标签层中,为机关门新增一个独立 tile 类别 `Special Door`
2. 在拓扑和清洗阶段,能够识别“机关门关联怪”,避免其被简单当作闲置怪过滤。
3. 在不大幅重写现有拓扑框架的前提下,尽量复用已有的区域合并、邻接和入口连通逻辑。
4. 让过滤结果继续保持可解释,至少能回答“为什么这个怪没有再被判成闲置怪”。
5. 第一版实现优先保守,宁可少杀一些,也不要把常见机关门房间结构大量误删。
---
## 非目标与边界
本轮文档暂时不做以下承诺:
1. **不要求第一版就精确恢复脚本级联动关系**。也就是说,暂不要求从原始事件脚本中严格求出“哪一只怪控制哪一扇机关门”。
2. **不要求立即重写所有门/怪规则**。本次重点是修正与机关门相关的误杀,而不是重构整套清洗体系。
3. **不要求在本轮文档里完成模型改造细节**。但需要明确指出:新增 tile 会影响训练数据词表和 mask token 编号。
4. **不改变机关门在大类上的门属性**。例如门密度、门类分支连通块等统计,默认仍然把机关门视为门的一种。
---
## 设计一:在标签层新增 `Special Door`
### 设计原则
这里需要强调一个重要约束:当前训练数据里 `MASK_ID = 6`,因此**不能直接把新语义无脑塞到现有编号上而不处理 mask token**。
第一版比较稳妥的编号方案是:
| 语义 | 当前主要编号 | 建议编号 |
| ------------ | ------------ | -------- |
| Empty | 0 | 0 |
| Wall | 1 | 1 |
| Common Door | 2 | 2 |
| Resource | 3 | 3 |
| Enemy | 4 | 4 |
| Entry | 5 | 5 |
| Special Door | 无独立编号 | 6 |
| Mask | 6 | 7 |
这个方案的优点是:
1. 现有 0 到 5 的主语义尽量不动。
2. 只为 `Special Door` 新增一个实际 tile。
3. `MASK_ID` 顺延到 7语义清晰不与真实地图 tile 混用。
### 对数据预处理侧的影响
新增独立标签后,数据预处理侧至少需要满足:
1. `commonDoorTiles``specialDoorTiles` 不再共享同一标签值。
2. `doorTiles` 仍然是二者并集,用于总门密度等大类统计。
3. `specialDoorCount` 才能真正表示机关门数量,而不是“普通门标签的重复统计”。
4. 主配置中的 `specialDoors` 需要改成独立编号,而不是继续与普通门共用标签。
### 对训练侧的影响
虽然本轮不直接修改训练代码,但文档必须明确:新增 `Special Door` 之后,训练侧至少要同步处理以下问题:
1. 数据集的 tile 词表大小会加 1。
2. `MASK_ID` 需要后移。
3. Stage 1 / Stage 2 / Stage 3 的退化逻辑需要把 `Special Door` 视作门类的一部分处理。
4. 统计目标里的门密度若仍按“大类门”处理,则需要把普通门和机关门一起计入。
也就是说,`Special Door` 虽然是一个新 tile但它在训练目标上仍然应属于“门超类”中的细分子类而不是完全独立的结构域。
---
## 设计二:在拓扑层保留机关门子语义
### 为什么不能只改 tile不改拓扑
如果只是把地图矩阵里的机关门编号拆出来,但拓扑层仍然把它和普通门都压成同一个 `Door` 分支,那么清洗算法依然无法利用这条信息。
因此,除了标签层扩展以外,拓扑层也需要补上“门子类型”这一维语义。
### 建议的数据结构方向
当前更合适的方向,不是把 `BranchType` 直接从两类扩成三类,而是保留现有大类,再补一个门子类型字段。原因是:
1. 现有很多逻辑只关心“门 vs 怪”,并不希望被额外分支复杂化。
2. 连续门团、门密度等统计,仍然需要把普通门和机关门归到门大类里。
3. 但闲置修正又确实需要知道“这个门是不是机关门”。
因此更推荐的方向是:
1. `branch` 仍然保持 `Door | Enemy`
2. 对门分支额外补一个 `doorKind = Common | Special`
这样可以同时满足:
1. 老规则不需要大改。
2. 新规则在需要的时候可以单独读取机关门语义。
### 机关门在拓扑统计中的默认归属
当前版本建议固定以下口径:
1. **大类统计**:机关门计入门密度、门连通块、门热力图。
2. **子类统计**:另外单独记录机关门数量,必要时可追加机关门密度。
3. **异常清洗**:仅在与机关门关联怪的识别相关时,读取 `doorKind = Special` 这一子语义。
这意味着:新增 `Special Door` 不是为了把机关门彻底从“门”里面剥出去,而是为了给清洗逻辑一个足够可靠的判别信号。
---
## 设计三:为“机关门关联怪”增加保守豁免
### 问题本质
需要修正的并不是“所有机关门附近的怪都要保留”,而是:
> 如果某只怪所在的局部区域明显属于机关门房间的一部分,那么它不能仅仅因为 `neighbors.size === 1` 或“背后无资源”就被直接视为闲置怪或无用怪。
换句话说,当前需要补的是一条**保守豁免规则**,而不是另一条更强的删除规则。
### 为什么第一版不建议直接解析脚本联动
理论上,最精确的做法是从原始塔事件里直接解析:
1. 哪扇机关门存在。
2. 哪些怪物被击败后会触发开门。
3. 一只怪是否同时参与多个门的触发。
但这个方案的工程代价很高,而且不同塔版本、脚本写法、自定义事件逻辑都可能造成兼容成本。因此,第一版不建议把问题拉到脚本层求精确真值,而是先使用当前拓扑图已经具备的信息,做一个偏保守的结构性修正。
### 建议主规则:基于“机关门关联区域”的豁免
当前版本更合适的主规则是:
1. 先沿用现有的非分支区域合并逻辑,把 `Empty``Resource` 节点合并成更大的可进入区域。
2. 若某个合并区域与至少一个 `Special Door` 分支相邻,则把这个区域记为“机关门关联区域”。
3. 若某个怪物分支与某个机关门关联区域相邻,则把该怪记为“机关门关联怪候选”。
4. 对机关门关联怪候选,不再仅凭 `neighbors.size === 1` 就直接计入 `idleEnemyBranchCount`
5. 对机关门关联怪候选,也不再仅凭“局部无资源收益”就直接计入无用分支命中。
这条规则的核心思想是:
1. 当前并不试图证明“这只怪一定会开门”。
2. 当前只要求证明“这只怪所在区域与机关门强相关”。
3. 一旦存在这种强相关,第一版就保守地不把它当作典型闲置怪删除。
### 为什么这个规则适合作为第一版
这条规则有几个优点:
1. 它只依赖现有拓扑图与门子类型,不依赖脚本解析。
2. 它可以复用当前重复守卫规则里已经存在的“合并非分支区域”思路。
3. 它解释性很强,可以直接回答“这只怪因为与机关门处在同一关联区域,所以没有被判成闲置怪”。
4. 它天然偏保守,符合当前“宁可少杀,不要误杀重要结构”的工程目标。
### 已知代价
这条豁免规则也有明确代价:
1. 它可能会保留少量“恰好和机关门在同一区域、但其实也不太重要”的怪物。
2. 它并不能恢复真正的脚本级触发关系,只是利用局部结构做代理。
但当前版本更需要避免的是**高频、显眼、结构性明确的误杀**。与之相比,这种偏保守的漏删代价是可以接受的。
---
## 对现有筛选规则的具体影响
### 规则一:`idleEnemyBranchCount`
这是当前最需要优先修正的规则。
建议口径:
1. 怪物若命中 `neighbors.size === 1`,先作为普通闲置怪候选。
2. 再检查它是否属于机关门关联怪。
3. 若是,则不计入 `idleEnemyBranchCount`
4. 若不是,则维持原判。
### 规则二:`hasUselessBranch`
这条规则也可能误伤机关门关联怪,因为这类怪物往往不守局部资源。
建议口径:
1. 对普通分支,仍按现有“删除该分支后,后侧是否失去入口连通且没有资源收益”的规则处理。
2. 对机关门关联怪,第一版默认不参与这条规则的硬过滤。
原因是:机关门关联怪的价值本来就未必体现为“守护后侧资源”,继续套用这条规则容易再次把它们误杀。
### 规则三:`hasRepeatedGuardIdleBranch`
这条规则的风险相对次要,但仍需要关注。因为一个机关门房间里可能存在多个同类怪,它们可能被错误理解成“重复守同一连通区域”。
当前建议是:
1. 第一版先增加观测统计,不急着强行改判。
2. 如果抽样发现机关门房间经常命中这条规则,再补“机关门关联怪不参与 repeated guard 统计”这一豁免。
也就是说,这条规则暂时列为**重点观察项**,不一定与 `idleEnemyBranchCount` 同步收紧。
### 规则四:连续门团 / 连续怪团
这两条规则当前不建议因机关门而修改。
原因是:
1. 机关门仍然是门的一种,继续计入门类连通块是合理的。
2. 机关门关联怪即使有特殊语义,也不意味着它们应当不受连续怪团规则约束。
换句话说,当前修的是“局部价值误判”,不是“所有机关门怪都特殊”。
---
## 统计与可观测性要求
为了让后续人工复核和规则调整有抓手,新增机关门语义后,至少应补以下统计:
1. `specialDoorCount`:真正意义上的机关门数量。
2. `specialDoorFloorCount`:包含机关门的楼层数量。
3. `specialDoorLinkedEnemyCount`:被识别为机关门关联怪的数量。
4. `ignoredIdleEnemyBySpecialDoorCount`:原本会命中闲置怪,但因机关门关联而被豁免的数量。
5. `ignoredUselessBranchBySpecialDoorCount`:原本会命中无用分支,但因机关门关联而被豁免的数量。
当前 filtered 数据集与图片标注体系也应支持这一点,至少能在抽样时回答:
1. 某张图为什么被保留。
2. 某张图为什么仍被过滤。
3. 如果某个闲置怪没有被过滤,是不是因为它属于机关门关联怪。
---
## 验收方向
本问题后续落地时,至少应满足以下验收方向:
1. 数据集标签中,机关门拥有独立 tile 编号,不再与普通门共用同一个语义值。
2. `specialDoorCount` 能真正反映机关门数量,而不是普通门的重复统计。
3. 典型机关门房间中的怪物,不会再仅因 `neighbors.size === 1` 被直接判成闲置怪。
4. 对包含机关门的地图进行抽样复核时,能明显看到误杀减少。
5. 连续门团、连续怪团等其他清洗规则在大方向上保持不变,不因为这次修正而整体失真。
---
## 当前建议的实施顺序
从工程风险看,当前更稳妥的顺序是:
1. 先把 `Special Door` 从标签层独立出来。
2. 再在拓扑层补上门子类型。
3. 优先修正 `idleEnemyBranchCount` 的误判。
4. 然后观察 `hasUselessBranch``hasRepeatedGuardIdleBranch` 的命中样本,决定是否追加豁免。
这个顺序的原因是:如果连独立 tile 都没有,后面的筛选修正只能停留在模糊的局部几何启发式上,无法稳定描述“机关门关联怪”这一目标对象。
---
## 本文档暂不回答的问题
以下问题在当前版本里先不定稿,留待后续实现与抽样验证后再讨论:
1. 是否有必要进一步解析原始脚本,恢复精确的“怪 -> 机关门”触发关系。
2. 机关门关联怪是否应该影响资源密度、怪物密度等统计解释方式。
3. 是否需要为“机关门房间”本身增加一个独立结构标签,供后续训练条件使用。
4. 机关门是否需要从普通门的连续门团统计中拆出去单独观测。
当前文档只固定一件事:**机关门必须先成为独立语义类,闲置筛选才有可能在这个问题上做对。**