ginka-generator/docs/resource-compress-design.md
unanmed 3e273c5a9d feat: 资源压缩为一种 tile
Co-authored-by: Copilot <copilot@github.com>
2026-05-05 21:46:38 +08:00

8.1 KiB
Raw Blame History

资源压缩与后续规划设计文档

背景与问题诊断

现有图块定义

当前地图生成模型共有以下图块类别NUM_CLASSES = 16掩码 token 占位 15

ID 语义 阶段
0 空地 阶段一
1 墙壁 阶段一
2 阶段二
3 钥匙 阶段三
4 红宝石 阶段三
5 蓝宝石 阶段三
6 绿宝石 阶段三
7 血瓶 阶段三
8 道具 阶段三
9 怪物 阶段二
10 入口 阶段二
15 掩码 token

问题分析

三阶段生成的任务量严重不均衡:

  • 阶段一(空地 + 墙壁2 类,决定地图基本骨架,结构约束强。
  • 阶段二(门 + 怪物 + 入口3 类,决定关卡通路与挑战点,结构约束强。
  • 阶段三(钥匙 + 红宝石 + 蓝宝石 + 绿宝石 + 血瓶 + 道具):6 类,资源种类几乎与前两阶段总和相同,但资源放置对地图结构的约束极弱——同一位置放红宝石还是蓝宝石,对地图整体结构几乎没有影响。

这种任务不均衡导致:

  1. 模型在阶段三花费大量参数容量学习细粒度资源分类,而这一分类对结构生成没有实质贡献。
  2. 掩码预测任务的类别分布偏斜(大量位置都是资源,其类别却彼此高度相似),训练信号稀疏,模型难以稳定收敛。

方案一:资源类别压缩(核心改进)

核心思路

将阶段三的所有资源种类(钥匙、红宝石、蓝宝石、绿宝石、血瓶、道具)统一压缩为单一的 Resource 类别,地图生成模型不再区分具体资源类型。资源的具体种类与数值由后续独立模型负责(见方案二规划)。

新图块定义

压缩后NUM_CLASSES 从 16 降至 7(含掩码 token图块重新编号如下

新 ID 语义 原 ID
0 空地 0
1 墙壁 1
2 2
3 资源(统一) 3/4/5/6/7/8
4 怪物 9
5 入口 10
6 掩码 token 15

三阶段任务调整为:

阶段 包含类别 类别数
阶段一 空地0、墙壁1 2
阶段二 2、怪物4、入口5 3
阶段三 资源3 1

阶段三现在退化为"在已知位置上填入资源"的简单任务,模型只需判断哪些空位应当放资源,而无需区分资源种类,任务难度大幅降低,信号更加清晰。

需要修改的位置

实施策略:优先在 Python 训练侧完成验证,确认效果后再同步修改 TypeScript 数据管线。各图块的原始数字编号在此阶段保持不变(如 entry 仍为 10最小化改动范围后续与 TS 侧统一调整时再重新编号。

  • 立即执行:第 3、4 项训练脚本、dataset 重映射)
  • 与 TS 同步执行:第 1、2 项(数据管线重编号)
  • 无需修改:第 5 项(可视化模块)

1. data/src/shared.ts(后续,与 TS 同步执行)

resourceTiles 的各子类别合并计入统一映射逻辑,图块 ID 重新编号:

// 新图块 ID 常量
export const TILE_EMPTY = 0;
export const TILE_WALL = 1;
export const TILE_DOOR = 2;
export const TILE_RESOURCE = 3; // 统一资源(原 3~8
export const TILE_ENEMY = 4; // 原 9
export const TILE_ENTRY = 5; // 原 10
export const TILE_MASK = 6; // 原 15掩码 token

2. data/src/auto/converter.ts(后续,与 TS 同步执行)

convertTile() 方法中,将原 key / redGem / blueGem / greenGem / potion / item 的输出统一映射为 TILE_RESOURCE = 3。同时保留原始图块 ID 至 ResourceType 的映射,供后续数值模型使用(不丢弃语义信息,只是在地图 token 层面合并)。

3. ginka/train_joint.py(及其他训练脚本)

NUM_CLASSES = 7   # 原来是 16
MASK_TOKEN  = 6   # 原来是 15

当前尚未实现阶段分离,阶段划分常量待后续引入多阶段生成时再补充。

4. ginka/dataset.py

__getitem__ 中,加载 map 数据后做一次原地重映射,将各类资源统一压缩为 3其余图块保持原始编号不变与 TS 侧尚未同步重编号保持一致):

REMAP = {
    0: 0,   # 空地
    1: 1,   # 墙壁
    2: 2,   # 门
    3: 3, 4: 3, 5: 3, 6: 3, 7: 3, 8: 3,  # 各类资源 → 统一资源
    9: 9,   # 怪物(保持原始编号)
    10: 10, # 入口(保持原始编号)
}

target_np = np.vectorize(REMAP.get)(target_np)

5. shared/image.pyshared/visual.py(无需修改)

shared/image.py 使用图片素材渲染,不依赖颜色调色板;shared/visual.py 仅用于数据集可视化查看,两者均不受此次图块合并影响,无需改动。

预期收益

指标 改动前 改动后
NUM_CLASSES 16 7
阶段三任务复杂度 6 类细粒度分类 1 类二元(放/不放)
嵌入表大小 16 × d_model 7 × d_model
分类头输出维度 16 7
训练信号质量 阶段三信号弱、偏斜 三阶段均衡、信号清晰

模型参数量略有下降,但更重要的是任务难度降低、各阶段学习目标更清晰,预期可显著改善阶段三(及整体)的收敛稳定性。


方案二:资源数值模型与怪物数值模型(后续规划)

此部分为后续计划,暂不细化实现,待方案一验证稳定后推进。

方案一的地图生成模型只负责"在哪里放资源/怪物",而"放什么资源/什么强度的怪物"由一组独立模型负责。

整体流程

地图生成模型(方案一)
    │ 输出:含统一 Resource/Enemy 的地图骨架
    ▼
资源数值模型 / 怪物数值模型
    │ 输入:地图骨架 + 当前关卡强度条件
    │ 输出:每个资源/怪物位置的 { type, value } 分布
    ▼
分类模型
    │ 输入:{ type, value } 分布
    │ 输出:具体图块 ID如 redGem_lv2、potion_lv1
    ▼
完整地图(含具体资源与怪物种类)

两类子模型结构

每类模型(资源 / 怪物)均分为两个独立子模型:

a. 数值模型Value Model

  • 输入:地图骨架(含资源/怪物占位标记)+ 关卡强度向量
  • 输出:每个位置的类型与归一化数值,例如 { type: "potion", value: 0.8 }
  • 数值在 [0, 1] 范围内归一化,推理时线性映射到目标区间(如等级 1~5

b. 分类模型Classifier Model

  • 输入:数值模型的输出分布
  • 输出:具体图块 ID离散
  • 职责:将连续数值量化为游戏中实际存在的有限种类,防止模型输出连续值导致种类爆炸(一般地图上资源/怪物只有少数几种反复复用)

设计动机

  • 解耦地图结构与关卡数值,使二者可独立调优。
  • 分类模型的引入是关键:直接让数值模型输出离散 ID 会导致种类碎片化;分类模型将"数值相近的资源/怪物聚类到同一具体图块",符合游戏设计中资源重用的实际规律。
  • 两类模型结构一致,可共用框架代码,仅在训练数据与输出头上有差异。

实施顺序

  1. 完成方案一修改数据管线TypeScript 侧、数据集类Python 侧)和训练脚本,重新生成数据集,以新图块体系从头训练地图生成模型,验证收敛效果。
  2. 稳定后推进方案二:在地图生成模型可以稳定生成结构合理的骨架图之后,再设计并实现数值模型与分类模型,最终串联为完整的地图生成管线。