Files
wechat-minigame/docs/05-difficulty-generator.md
manpengan b32ba6d595 content: 6 城市数据 + 决策锁定 + 流程更新
- js/content/cities/: 北京/东京/曼谷/首尔/新加坡/伊斯坦布尔完整 CityManifest
- 01-charter: 5 项设计决策标记  已锁定
- 00-process: Phase 1/4 已完成,Phase 7 进行中
- 收录 Codex 产出的 04/05 技术设计文档

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 23:10:24 +08:00

10 KiB
Raw Blame History

05 Difficulty Generator — 城市抓猫猫

1. 目标

可控难度生成器负责把 CityManifest.levelPresets 变成可玩的单局棋盘,并保证:

  • 同一关卡能通过 seed 复现
  • 关卡默认可解,不能依赖广告道具救不可能局
  • 不同城市和关卡能被放进统一的难度带
  • 生成器输出不仅是“布局”,还是“布局质量评估结果”

2. 非目标

这套系统暂时不做这些事:

  • 不做人类级最优解搜索
  • 不做手工逐关摆盘编辑器
  • 不把难度完全交给在线热更新动态生成
  • 不在 MVP 阶段做实时个性化难度

MVP 的目标是:离线可复现 + 可校验 + 能调参

3. 设计原则

3.1 关卡必须先“无道具可过”

道具是纠错和商业化入口,不是不可解关卡的补丁。

要求:

  • 所有基础关卡必须存在无道具通关路径
  • 关 1-4 的目标通关率以无道具为准
  • 关 5-6 可以允许少量玩家依赖道具,但不能依赖道具才有解

3.2 同一 seed 必须得到同一布局

  • 便于复现 bug
  • 便于分享每日挑战
  • 便于离线验证和埋点对齐

3.3 “难”来自信息压力,不来自脏规则

  • 难度主要来自种类数、可见物件密度、遮挡深度、误点代价
  • 不做肉眼不可识别的遮挡判定
  • 不做首屏几乎无可操作空间的恶意布局

4. 核心数据模型

4.1 DifficultyProfile

{
  cityId: 'beijing',
  levelId: 1,
  seed: 11001,
  elementCount: 6,
  piecesPerElement: 3,
  layers: 2,
  density: 'low',
  targetPassRate: 0.95,
  targetDurationSec: [90, 150],
}

4.2 PieceInstance

{
  id: 'piece_0001',
  elementId: 'beijing_01',
  layer: 1,
  x: 220,
  y: 340,
  width: 64,
  height: 64,
  rotation: -4,
  removed: false,
}

4.3 BoardState

{
  boardId: 'beijing-1-11001',
  cityId: 'beijing',
  levelId: 1,
  seed: 11001,
  pieces: [],
  overlapGraph: {},
  metrics: {
    totalPieces: 18,
    initialClickableRatio: 0.39,
    simulatedPassRate: 0.97,
    avgTurnsToFinish: 15.2,
    toolFreePassRate: 0.97,
    toolAssistPassRate: 1.0,
  },
}

4.4 RuntimeState

{
  slot: [],       // 最多 7 个
  bypass: [],     // Remove 使用,最多 3 个
  removedPieceIds: [],
}

5. 坐标系与布局空间

5.1 画布布局区

棋盘不直接自由散点,而是在“布局安全区”中放置:

  • 上方预留标题和城市信息区
  • 下方预留 7 格槽位和道具按钮区
  • 中间主区域作为堆叠区

建议生成器使用一套固定的 anchor grid

  • 列数6-8
  • 行数7-9
  • 每个 anchor 可带随机偏移
  • 每层共享同一批 anchor但偏移不同

这样做的目的:

  • 让视觉上看起来是自然堆叠
  • 实现上仍然可控、可校验

5.2 顶层回退落点

Undo 不恢复原位,而是回到“场景顶层空闲位置”。这个位置必须由生成器提前预留安全落点:

  • returnLanes 是一组不与标题区、按钮区重叠的顶层 anchor
  • 回退件只落在 returnLanes
  • 回退后不能让“初始唯一可点击物件”全部被堵死

6. Overlap Graph

Overlap Graph 是生成器和运行时共同使用的核心结构。

6.1 定义

  • 每个物件是一个节点
  • 若 A 遮挡 B则存在有向边 A -> B
  • 节点入度为 0 表示当前可点击

6.2 遮挡判定

两个物件存在遮挡关系,需同时满足:

  • A.layer > B.layer
  • 包围盒重叠面积超过 B 面积的阈值
  • 且重叠区域命中了 B 的中心活跃区

推荐阈值:

  • overlapRatio >= 0.2
  • 中心活跃区为物件中心 50% x 50%

这样可以避免“边角轻微擦到就算完全遮挡”的脏判定。

6.3 图结构接口

buildOverlapGraph(pieces)
getClickablePieces(boardState)
rebuildGraphAfterShuffle(boardState)
rebuildGraphAfterUndo(boardState)

7. 可点击判定

运行时的点击条件统一为:

  1. 物件未被移除
  2. 物件不在暂存槽和旁路寄存区
  3. overlapGraph 中入度为 0

不引入额外“半可点”状态,避免玩家心智混乱。

8. 生成流水线

8.1 输入

  • CityManifest
  • LevelPreset
  • seed

8.2 输出

  • BoardState
  • 评估指标 metrics
  • 若生成失败则返回错误原因和重试次数

8.3 流程

1. 依据 level preset 生成元素池
2. 根据 seed 洗牌元素池
3. 计算 layerDistribution
4. 在 anchor grid 中逐层放置物件
5. 构建 overlap graph
6. 计算初始可点击集合
7. 跑 solver / validator
8. 若指标不达标则调参重试
9. 输出最终 board + metrics

9. 元素池生成

9.1 规则

  • elementCount 决定本关启用多少种元素
  • piecesPerElement 可以是单值,也可以是数组
  • 所有值必须是 3 的倍数

9.2 采样原则

  • 优先从城市 elements 中选取高辨识度元素
  • 尽量覆盖多类目,不让一关全是美食或全是建筑
  • 同一城市 6 关的元素组合需要有节奏变化,不应完全重复

推荐策略:

  • 先抽每个类目至少 1 个
  • 剩余名额按权重补齐

10. 分层与密度模型

10.1 layerDistribution

density 决定每层占比。

建议默认分布:

density 分层建议
low [0.40, 0.35, 0.25]
medium [0.30, 0.30, 0.25, 0.15]
medium_high [0.28, 0.27, 0.25, 0.20]
high [0.25, 0.25, 0.25, 0.25]

注意:

  • 实际层数少于数组长度时,截断并重新归一化
  • 最顶层占比不能低到“首屏没有明显可点物件”

10.2 初始可见性约束

基础约束:

  • 初始可点击物件占比 >= 0.30
  • 初始可点击集合中,至少存在 2 组可形成潜在三消的元素链
  • 首关至少有 1 组直接三消机会

这三条是“避免开局恶心人”的底线。

11. 布局放置规则

11.1 放置策略

  • 从底层到顶层放置
  • 每层物件先选 anchor再施加轻微随机偏移
  • 同类元素默认不要 3 个全堆在同一小区域

11.2 同类分散规则

同类元素放置时遵循:

  • 至少 1 个在首屏可点击层附近
  • 不允许 3 个完全同层紧邻,避免白给三消过多
  • 也不允许 3 个全部深埋,避免“直到残局才第一次看到”

这是影响手感最明显的隐藏规则之一。

11.3 冲突规避

放置时要避开:

  • 顶部标题安全区
  • 底部槽位和道具按钮区
  • Undo 回退通道 returnLanes

12. Seed 机制

12.1 来源

  • 常规关:hash(cityId + ':' + levelId + ':' + revision)
  • 每日挑战:hash(utcDate + ':daily:' + cityId)
  • 调试复现:可直接手输 seed

12.2 用途

  • 复现线上问题
  • 让同一日挑战全量玩家拿到同一盘
  • 把埋点和具体布局绑定起来

12.3 revision

如果某关布局难度整体偏离目标,不直接改 levelId,而是增加 revision

这样可以区分:

  • levelId:设计上的第几关
  • revision:该关当前采用的生成版本

13. Solver 设计

生成器不能只靠一条贪心策略,否则评估噪音太大。

13.1 Solver 策略集

至少跑 3 类策略:

  • match-first 优先选择能立刻三消的物件
  • slot-build 优先补齐槽里已有 1-2 个的类型
  • risk-averse 避免把槽位铺得过散,优先减少槽中类型数

13.2 单次模拟流程

1. 初始化 board / slot / bypass
2. 获取 clickable pieces
3. 按当前策略给每个可点物件打分
4. 选择得分最高的候选
5. 放入槽位并执行三消
6. 若触发 bypass 回归,则先回归再重新判定槽位
7. 重建可点击集合
8. 直到清盘成功或失败

13.3 输出指标

每批模拟需要统计:

  • toolFreePassRate
  • avgTurnsToFinish
  • avgPeakSlotUsage
  • openingStability 定义为前 5 步中失败样本占比
  • stuckRate 定义为进入硬死局的比例

14. 死局分类

14.1 Hard Deadlock

满足以下条件时,判为硬死局:

  • 暂存槽已满 7 格
  • 当前无任何可形成三消的点击路径
  • 旁路寄存区为空或回归后仍失败
  • 无道具情况下不可能继续

14.2 Soft Deadlock

满足以下条件时,判为软死局:

  • 当前直接继续大概率失败
  • 但使用 UndoRemoveShuffle 可恢复可玩态

设计原则:

  • 生成器允许出现软死局
  • 生成器不允许把基础通关路径设计成必经硬死局

15. 验收门槛

15.1 关卡级门槛

关卡 toolFreePassRate toolAssistPassRate initialClickableRatio
1 >= 0.95 1.00 >= 0.38
2 >= 0.90 >= 0.98 >= 0.35
3 >= 0.80 >= 0.92 >= 0.33
4 >= 0.70 >= 0.88 >= 0.32
5 >= 0.60 >= 0.82 >= 0.30
6 0.50 - 0.60 >= 0.75 >= 0.30

说明:

  • toolFreePassRate 是核心门槛
  • toolAssistPassRate 用来衡量道具价值,不是救火指标

15.2 批量生成门槛

如果某个 LevelPreset 连续重试 50 次仍无法达到目标区间,应判为参数设计有问题,而不是继续无限重试。

16. 调参顺序

当某关过难时,按这个顺序调:

  1. 降低 density
  2. 降低 layers
  3. 降低 elementCount
  4. 调整 layerDistribution
  5. 调整同类元素分散规则

当某关过易时,反向调整。

不要优先改槽位大小,不要动基础规则。

17. 埋点与闭环

线上埋点至少记录:

  • cityId
  • levelId
  • revision
  • seed
  • resultwin / fail / revive / quit
  • peakSlotUsage
  • toolUsage
  • timeSpentSec
  • firstFailStep

这些数据用来做三件事:

  • 验证离线模拟是否接近真实玩家
  • 找出异常 seed
  • 调整不同关卡的目标通关率

18. 推荐接口

generateBoard(cityId, levelId, seed)
buildOverlapGraph(pieces)
getClickablePieces(boardState)
simulateBoard(boardState, policy)
evaluateBoard(boardState, levelPreset)
classifyDeadlock(runtimeState, boardState)
regenerateWithTweaks(levelPreset, failureReason)

19. 与 04 文档的边界

05 文档消费 04-city-content-system.md 提供的这些输入:

  • CityManifest.elements
  • CityManifest.levelPresets
  • cityId / levelId / revision

05 文档输出给运行时的是:

  • 单局布局
  • 评估指标
  • 可复现 seed

内容系统负责“有什么内容”,难度生成器负责“这些内容如何排成一盘可玩的局”。