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>
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
> 创建日期:2026-03-28
|
||||
> 项目仓库:https://github.com/manpengan/wechat-minigame
|
||||
> 标准流程:~/pro/kb/workflows/standard-dev-process/SKILL.md
|
||||
> 当前阶段:Phase 1 立项(待开始)
|
||||
> 当前阶段:Phase 7 开发实现(进行中)
|
||||
|
||||
---
|
||||
|
||||
@@ -38,13 +38,13 @@ Phase 6 ──→ Phase 7 ──→ Phase 8 ──→ Phase 9 ──→ Phase 10
|
||||
|
||||
| 阶段 | 状态 | 产出文档 | 备注 |
|
||||
|------|------|----------|------|
|
||||
| Phase 1 立项 | ⏳ 待开始 | docs/01-project-charter.md | |
|
||||
| Phase 1 立项 | ✅ 已完成 | docs/01-project-charter.md | 方向确定为城市抓猫猫,5 项决策已锁定 |
|
||||
| Phase 2 市场调研 | 🔒 未开始 | docs/02-market-research.md | |
|
||||
| Phase 3 需求分析 | 🔒 未开始 | docs/03-prd.md | |
|
||||
| Phase 4 技术方案 | 🔒 未开始 | docs/04-technical-design.md | |
|
||||
| Phase 4 技术方案 | ✅ 已完成 | docs/04-technical-design.md | 04-city-content-system + 05-difficulty-generator |
|
||||
| Phase 5 项目计划 | 🔒 未开始 | docs/05-project-plan.md | |
|
||||
| Phase 6 设计阶段 | 🔒 未开始 | docs/06-design-spec.md | |
|
||||
| Phase 7 开发实现 | 🔒 未开始 | docs/07-dev-notes.md | |
|
||||
| Phase 7 开发实现 | 🔧 进行中 | docs/07-dev-notes.md | Codex 落代码骨架 js/content/* |
|
||||
| Phase 8 测试验收 | 🔒 未开始 | docs/08-test-report.md | |
|
||||
| Phase 9 发布上线 | 🔒 未开始 | docs/09-release-checklist.md | |
|
||||
| Phase 10 运营迭代 | 🔒 未开始 | docs/10-operations.md | |
|
||||
|
||||
@@ -178,11 +178,11 @@
|
||||
|
||||
| # | 决策项 | 建议方向 | 决策依据 | 状态 |
|
||||
|---|--------|---------|---------|------|
|
||||
| 1 | 消除机制 | 沿用抓大鹅堆叠三消,不做大变体 | 机制成熟、用户认知成本低、开发风险小 | 待确认 |
|
||||
| 2 | 首页层级 | MVP 简化为"洲 → 城市"二级(去掉国家层) | 减少导航深度,MVP 只有 1 洲 6 城市,三级无意义 | 待确认 |
|
||||
| 3 | 首批 6 城市 | 北京、东京、曼谷、首尔、新加坡、伊斯坦布尔 | 文化辨识度高、素材丰富、地理分布覆盖亚洲主要区域 | 待确认 |
|
||||
| 4 | 美术风格 | 扁平冰箱贴风 | 辨识度高、AI 生成友好、适合小尺寸 Canvas 渲染 | 待确认 |
|
||||
| 5 | 素材生产 | AI 出图 + 人工修正 | 单城市 2-3 天产出,成本可控,质量可接受 | 待确认 |
|
||||
| 1 | 消除机制 | 沿用抓大鹅堆叠三消,不做大变体 | 机制成熟、用户认知成本低、开发风险小 | ✅ 已锁定 |
|
||||
| 2 | 首页层级 | MVP 简化为"洲 → 城市"二级(去掉国家层) | 减少导航深度,MVP 只有 1 洲 6 城市,三级无意义 | ✅ 已锁定 |
|
||||
| 3 | 首批 6 城市 | 北京、东京、曼谷、首尔、新加坡、伊斯坦布尔 | 文化辨识度高、素材丰富、地理分布覆盖亚洲主要区域 | ✅ 已锁定 |
|
||||
| 4 | 美术风格 | 扁平冰箱贴风 | 辨识度高、AI 生成友好、适合小尺寸 Canvas 渲染 | ✅ 已锁定 |
|
||||
| 5 | 素材生产 | AI 出图 + 人工修正 | 单城市 2-3 天产出,成本可控,质量可接受 | ✅ 已锁定 |
|
||||
|
||||
## 13. 下一步产出
|
||||
|
||||
|
||||
411
docs/04-city-content-system.md
Normal file
411
docs/04-city-content-system.md
Normal file
@@ -0,0 +1,411 @@
|
||||
# 04 City Content System — 城市抓猫猫
|
||||
|
||||
## 1. 目标
|
||||
|
||||
城市内容系统负责把"北京/东京/曼谷"这类城市内容变成稳定、可校验、可扩展、可分包加载的数据资产。
|
||||
|
||||
它解决 4 个问题:
|
||||
|
||||
- 新增一个城市时,尽量只新增配置和资源,不改玩法代码
|
||||
- 运行时能快速拿到城市列表、封面、元素、猫猫、关卡参数
|
||||
- 内容出错时能在入库前被校验出来,而不是上线后炸在运行时
|
||||
- 内容包和玩家进度解耦,后续扩城市、扩洲、做活动都不改存档结构
|
||||
|
||||
## 2. 设计原则
|
||||
|
||||
### 2.1 配置驱动
|
||||
|
||||
- 玩法代码不写死城市名、元素名、解锁顺序
|
||||
- 城市、洲、猫猫、护照、分享卡片都从 JS module 配置读取
|
||||
- 运行时只依赖稳定 id,不依赖中文展示名
|
||||
|
||||
### 2.2 稳定标识优先
|
||||
|
||||
- `continentId`、`cityId`、`elementId`、`catId` 一旦上线不可重命名
|
||||
- 展示文案、美术资源可迭代,稳定 id 不变
|
||||
- 玩家存档只记录 id 和进度,不直接记录资源路径
|
||||
|
||||
### 2.3 内容与进度解耦
|
||||
|
||||
- 内容是只读配置
|
||||
- 进度来自 `playerState`
|
||||
- UI 展示由 `content + playerState` 合成,不把内容字段写回存档
|
||||
|
||||
### 2.4 可校验优先于可编辑
|
||||
|
||||
- 城市配置必须显式、冗余、容易校验
|
||||
- 不追求“最短配置”,追求“新手也不容易配错”
|
||||
|
||||
## 3. 逻辑模块
|
||||
|
||||
| 模块 | 职责 |
|
||||
|------|------|
|
||||
| `ContinentRegistry` | 管理洲索引、城市顺序、预下载策略 |
|
||||
| `CityRegistry` | 加载单个城市配置、提供城市查询接口 |
|
||||
| `ContentResolver` | 根据 id 解析封面、猫猫、元素、护照、分享资源 |
|
||||
| `ContentValidator` | 在入库前做结构校验、语义校验、跨文件校验 |
|
||||
| `ProgressProjector` | 用 `playerState` 投影出解锁态、通关态、图鉴态 |
|
||||
| `BundleResolver` | 决定某个城市资源在主包还是分包,何时预加载 |
|
||||
|
||||
## 4. 数据层级
|
||||
|
||||
运行时的数据层级固定为:
|
||||
|
||||
```text
|
||||
ContinentManifest
|
||||
-> CityManifest
|
||||
-> ElementSpec[]
|
||||
-> CatSpec
|
||||
-> LevelPreset[]
|
||||
-> PassportSpec
|
||||
-> ShareCardSpec
|
||||
```
|
||||
|
||||
### 4.1 ContinentManifest
|
||||
|
||||
洲配置只描述“入口”和“顺序”,不重复城市详情。
|
||||
|
||||
```javascript
|
||||
module.exports = {
|
||||
id: 'asia',
|
||||
name: '亚洲',
|
||||
sortOrder: 1,
|
||||
themeColor: '#FF6B6B',
|
||||
cityIds: ['beijing', 'tokyo', 'bangkok', 'seoul', 'singapore', 'istanbul'],
|
||||
unlockOrder: ['beijing', 'tokyo', 'bangkok', 'seoul', 'singapore', 'istanbul'],
|
||||
bundle: {
|
||||
packId: 'asia-rest',
|
||||
preloadOnEnter: true,
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
约束:
|
||||
|
||||
- `cityIds` 和 `unlockOrder` 必须一一对应
|
||||
- MVP 只启用 `asia`
|
||||
- 后续新增洲时,`sortOrder` 决定大地图排序
|
||||
|
||||
### 4.2 CityManifest
|
||||
|
||||
城市配置是运行时的主实体。
|
||||
|
||||
```javascript
|
||||
module.exports = {
|
||||
id: 'beijing',
|
||||
contentVersion: 1,
|
||||
continentId: 'asia',
|
||||
sortOrder: 1,
|
||||
unlockAfterCityId: null,
|
||||
bundle: {
|
||||
packId: 'main',
|
||||
preload: 'startup',
|
||||
},
|
||||
display: {
|
||||
name: '北京',
|
||||
nameEn: 'Beijing',
|
||||
bgColor: '#CC2936',
|
||||
tagline: '天安门前看猫猫',
|
||||
funFact: '北京是世界上拥有最多宫殿的城市',
|
||||
},
|
||||
cover: {
|
||||
catImage: 'images/cats/cat_beijing.png',
|
||||
catThumb: 'images/cats/cat_beijing_thumb.png',
|
||||
shareAccent: '#F6D365',
|
||||
},
|
||||
cat: {
|
||||
id: 'cat_beijing',
|
||||
name: '京京',
|
||||
baseColor: '#F28C28',
|
||||
pattern: 'tabby',
|
||||
accessory: '虎头帽',
|
||||
},
|
||||
passport: {
|
||||
stampId: 'stamp_beijing',
|
||||
stampLabel: '北京',
|
||||
},
|
||||
elements: [
|
||||
// 12-15 个
|
||||
],
|
||||
levelPresets: [
|
||||
// 6 个基础关卡
|
||||
],
|
||||
shareCard: {
|
||||
titleUnlocked: '我解锁了北京猫!',
|
||||
titlePassport: '北京护照盖章完成',
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
### 4.3 ElementSpec
|
||||
|
||||
元素是关卡和图鉴的最小单位。
|
||||
|
||||
```javascript
|
||||
{
|
||||
id: 'beijing_01',
|
||||
name: '糖葫芦',
|
||||
category: 'food',
|
||||
rarity: 'core',
|
||||
image: 'images/elements/beijing/beijing_01.png',
|
||||
tags: ['sweet', 'street-food', 'red'],
|
||||
}
|
||||
```
|
||||
|
||||
字段约束:
|
||||
|
||||
- `id` 全局唯一
|
||||
- `category` 只能来自固定枚举:`landmark` / `food` / `culture` / `item` / `nature`
|
||||
- `rarity` 先保留 `core` / `accent` 两档,MVP 默认都可用 `core`
|
||||
- `tags` 给后续活动筛选、图鉴展示、搜索扩展留接口
|
||||
|
||||
### 4.4 LevelPreset
|
||||
|
||||
关卡不直接保存所有物件坐标,只保存生成输入。
|
||||
|
||||
```javascript
|
||||
{
|
||||
id: 1,
|
||||
difficultyTier: 'intro',
|
||||
seedBase: 11001,
|
||||
elementCount: 6,
|
||||
piecesPerElement: 3,
|
||||
layers: 2,
|
||||
density: 'low',
|
||||
targetPassRate: 0.95,
|
||||
targetDurationSec: [90, 150],
|
||||
}
|
||||
```
|
||||
|
||||
设计原则:
|
||||
|
||||
- 关卡配置是“生成器输入 + 目标指标”
|
||||
- 真正布局由 `difficulty generator` 按 `seed` 生成
|
||||
- 同一关卡可复现实例,不手工存静态坐标表
|
||||
|
||||
## 5. 推荐目录结构
|
||||
|
||||
系统层推荐的逻辑目录如下:
|
||||
|
||||
```text
|
||||
js/content/
|
||||
continents/
|
||||
asia.js
|
||||
index.js
|
||||
cities/
|
||||
beijing.js
|
||||
tokyo.js
|
||||
...
|
||||
registry/
|
||||
continent-registry.js
|
||||
city-registry.js
|
||||
bundle-resolver.js
|
||||
progress-projector.js
|
||||
validation/
|
||||
validate-continent.js
|
||||
validate-city.js
|
||||
validate-assets.js
|
||||
```
|
||||
|
||||
说明:
|
||||
|
||||
- `docs/03` 里的 `cities/*.js`、`continents/*.js` 是逻辑格式,不要求现在就按这个目录实现
|
||||
- 真的写代码时,建议统一挂到 `js/content/` 下,避免根目录膨胀
|
||||
|
||||
## 6. 运行时读取流程
|
||||
|
||||
### 6.1 启动阶段
|
||||
|
||||
1. 读取 `continents/index`
|
||||
2. 初始化 `ContinentRegistry`
|
||||
3. 读取主包内首城市 `beijing`
|
||||
4. 生成城市页卡片数据
|
||||
5. 用 `playerState` 投影出解锁态和通关态
|
||||
|
||||
### 6.2 进入城市页
|
||||
|
||||
1. 取亚洲 `unlockOrder`
|
||||
2. 读取所有已在本地可用的 `CityManifest` 摘要
|
||||
3. 对未下载但已可见的城市展示灰态卡片
|
||||
4. 对“下一目标城市”预触发分包预下载
|
||||
|
||||
### 6.3 进入单城市
|
||||
|
||||
1. 检查 `bundle.packId`
|
||||
2. 若分包未加载,则先下载/加载
|
||||
3. 加载 `CityManifest`
|
||||
4. 加载该城市封面猫猫、元素图标、分享卡资源
|
||||
5. 生成 6 个关卡入口及对应种子
|
||||
|
||||
### 6.4 完成城市
|
||||
|
||||
1. `levelProgress` 全部满 6 关
|
||||
2. 写入 `collectedCats`
|
||||
3. 写入 `passportStamps`
|
||||
4. 解锁 `unlockAfterCityId` 的后续城市
|
||||
5. 预下载下一个城市内容包
|
||||
|
||||
## 7. 内容与存档的映射关系
|
||||
|
||||
内容系统不直接写存档,它只定义映射规则。
|
||||
|
||||
| 内容字段 | 存档字段 | 用途 |
|
||||
|---------|---------|------|
|
||||
| `city.id` | `unlockedCities[]` | 城市解锁 |
|
||||
| `levelPresets[].id` | `levelProgress[cityId][levelId]` | 关卡进度 |
|
||||
| `city.id` | `collectedCats[]` | 猫猫图鉴完成态 |
|
||||
| `city.id` | `passportStamps[]` | 护照盖章完成态 |
|
||||
| `bundle.packId` | 不入存档 | 运行时加载决策 |
|
||||
| `contentVersion` | 不入存档 | 内容升级和兼容判定 |
|
||||
|
||||
规则:
|
||||
|
||||
- 存档永远记录 `cityId`,不记录中文名
|
||||
- `cat.id` 和 `passport.stampId` 只作为配置内标识,MVP 存档阶段继续只记 `cityId`
|
||||
- 这样后面猫猫重命名、护照表现样式调整、城市文案微调都不影响旧存档
|
||||
|
||||
## 8. 分包与资源边界
|
||||
|
||||
### 8.1 包划分原则
|
||||
|
||||
- 主包只放启动必需内容
|
||||
- 分包按“玩家最可能连续访问的内容”组织,而不是按文件类型组织
|
||||
- 对 MVP 来说,推荐:
|
||||
- 主包:框架、通用 UI、北京城市包
|
||||
- 亚洲分包:其余 5 个城市
|
||||
|
||||
### 8.2 Bundle 元数据
|
||||
|
||||
每个城市都带自己的包信息:
|
||||
|
||||
```javascript
|
||||
bundle: {
|
||||
packId: 'asia-rest',
|
||||
preload: 'on-city-page',
|
||||
}
|
||||
```
|
||||
|
||||
`preload` 枚举建议:
|
||||
|
||||
- `startup`:随启动主包一起可用
|
||||
- `on-city-page`:进入城市页即预下载
|
||||
- `on-demand`:用户点击城市时再拉取
|
||||
|
||||
### 8.3 资源命名规则
|
||||
|
||||
- 猫猫封面:`images/cats/cat_{cityId}.png`
|
||||
- 猫猫缩略图:`images/cats/cat_{cityId}_thumb.png`
|
||||
- 元素图标:`images/elements/{cityId}/{cityId}_{seq}.png`
|
||||
- 分享资源如需额外图层:`images/share/{cityId}_*.png`
|
||||
|
||||
## 9. 校验体系
|
||||
|
||||
### 9.1 结构校验
|
||||
|
||||
结构校验回答“字段有没有、类型对不对”。
|
||||
|
||||
校验项:
|
||||
|
||||
- 必填字段完整
|
||||
- 枚举值合法
|
||||
- `levelPresets` 固定为 6 个
|
||||
- `elements.length` 在 12-15 之间
|
||||
- `piecesPerElement` 为 3 的倍数
|
||||
|
||||
### 9.2 语义校验
|
||||
|
||||
语义校验回答“内容合理不合理”。
|
||||
|
||||
校验项:
|
||||
|
||||
- 5 类目配比满足:`landmark >= 2`、`food >= 3`、`culture >= 2`、`item >= 2`、`nature >= 2`
|
||||
- 同城市元素名称不能重复
|
||||
- `unlockAfterCityId` 必须和洲内顺序一致
|
||||
- `display.bgColor` 与封面资源不能缺失
|
||||
- `cat.id` 与 `city.id` 一一对应
|
||||
|
||||
### 9.3 跨文件校验
|
||||
|
||||
跨文件校验回答“城市、洲、资源之间有没有断链”。
|
||||
|
||||
校验项:
|
||||
|
||||
- `continent.cityIds` 里的每个城市文件都存在
|
||||
- `city.continentId` 必须回指到合法洲
|
||||
- `cover.catImage`、`cover.catThumb`、`elements[].image` 必须存在
|
||||
- `unlockOrder` 和 `sortOrder` 不冲突
|
||||
|
||||
### 9.4 校验输出
|
||||
|
||||
校验输出分两类:
|
||||
|
||||
- `error`:阻止入库,比如资源缺失、id 重复、类目不达标
|
||||
- `warning`:允许入库,但需要人工确认,比如 `funFact` 过长、色彩接近、标签过少
|
||||
|
||||
推荐接口:
|
||||
|
||||
```javascript
|
||||
validateContinent(continentConfig)
|
||||
validateCity(cityConfig, allContinentConfigs)
|
||||
validateAssetPaths(cityConfig, assetIndex)
|
||||
```
|
||||
|
||||
## 10. 内容扩展流程
|
||||
|
||||
新增一个城市时,流程固定为:
|
||||
|
||||
1. 创建 `CityManifest`
|
||||
2. 准备 12-15 个元素图标
|
||||
3. 准备猫猫封面和缩略图
|
||||
4. 补齐护照和分享文案
|
||||
5. 跑内容校验
|
||||
6. 跑难度生成器验证 6 个基础关卡
|
||||
7. 把城市 id 挂到对应洲的 `cityIds` 和 `unlockOrder`
|
||||
8. 再跑一次跨文件校验
|
||||
|
||||
只有第 7 步会影响洲索引,其余步骤都不该动玩法代码。
|
||||
|
||||
## 11. 版本与兼容策略
|
||||
|
||||
### 11.1 contentVersion
|
||||
|
||||
- `contentVersion` 用来标记城市配置版本
|
||||
- 仅当字段结构变化或默认行为变化时递增
|
||||
- 纯文案或美术替换不必改版本
|
||||
|
||||
### 11.2 删除与下线
|
||||
|
||||
上线后不建议真正删除城市 id。
|
||||
|
||||
若后续城市下线:
|
||||
|
||||
- 内容层标为 `disabled: true`
|
||||
- 入口层不再展示
|
||||
- 存档里旧 `cityId` 仍然保留,避免脏存档
|
||||
|
||||
## 12. 对工程实现的要求
|
||||
|
||||
内容系统落地时需要优先保证这些接口稳定:
|
||||
|
||||
```javascript
|
||||
getContinentList()
|
||||
getContinent(continentId)
|
||||
getCity(cityId)
|
||||
getCityCardView(cityId, playerState)
|
||||
getLevelPreset(cityId, levelId)
|
||||
ensureCityBundle(cityId)
|
||||
validateContent()
|
||||
```
|
||||
|
||||
这批接口一旦稳定,UI、地图、图鉴、护照、分享页都能复用同一层内容数据。
|
||||
|
||||
## 13. 本文档不负责的事情
|
||||
|
||||
以下内容不在 04 文档内定义:
|
||||
|
||||
- 具体关卡布局算法
|
||||
- overlap graph 结构
|
||||
- solver / validator 的实现
|
||||
- 道具状态机细节
|
||||
|
||||
这些交给 [05-difficulty-generator.md](./05-difficulty-generator.md)。
|
||||
457
docs/05-difficulty-generator.md
Normal file
457
docs/05-difficulty-generator.md
Normal file
@@ -0,0 +1,457 @@
|
||||
# 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
|
||||
|
||||
```javascript
|
||||
{
|
||||
cityId: 'beijing',
|
||||
levelId: 1,
|
||||
seed: 11001,
|
||||
elementCount: 6,
|
||||
piecesPerElement: 3,
|
||||
layers: 2,
|
||||
density: 'low',
|
||||
targetPassRate: 0.95,
|
||||
targetDurationSec: [90, 150],
|
||||
}
|
||||
```
|
||||
|
||||
### 4.2 PieceInstance
|
||||
|
||||
```javascript
|
||||
{
|
||||
id: 'piece_0001',
|
||||
elementId: 'beijing_01',
|
||||
layer: 1,
|
||||
x: 220,
|
||||
y: 340,
|
||||
width: 64,
|
||||
height: 64,
|
||||
rotation: -4,
|
||||
removed: false,
|
||||
}
|
||||
```
|
||||
|
||||
### 4.3 BoardState
|
||||
|
||||
```javascript
|
||||
{
|
||||
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
|
||||
|
||||
```javascript
|
||||
{
|
||||
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 图结构接口
|
||||
|
||||
```javascript
|
||||
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 流程
|
||||
|
||||
```text
|
||||
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 单次模拟流程
|
||||
|
||||
```text
|
||||
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
|
||||
|
||||
满足以下条件时,判为软死局:
|
||||
|
||||
- 当前直接继续大概率失败
|
||||
- 但使用 `Undo`、`Remove` 或 `Shuffle` 可恢复可玩态
|
||||
|
||||
设计原则:
|
||||
|
||||
- 生成器允许出现软死局
|
||||
- 生成器不允许把基础通关路径设计成必经硬死局
|
||||
|
||||
## 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`
|
||||
- `result`(win / fail / revive / quit)
|
||||
- `peakSlotUsage`
|
||||
- `toolUsage`
|
||||
- `timeSpentSec`
|
||||
- `firstFailStep`
|
||||
|
||||
这些数据用来做三件事:
|
||||
|
||||
- 验证离线模拟是否接近真实玩家
|
||||
- 找出异常 seed
|
||||
- 调整不同关卡的目标通关率
|
||||
|
||||
## 18. 推荐接口
|
||||
|
||||
```javascript
|
||||
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](./04-city-content-system.md) 提供的这些输入:
|
||||
|
||||
- `CityManifest.elements`
|
||||
- `CityManifest.levelPresets`
|
||||
- `cityId / levelId / revision`
|
||||
|
||||
05 文档输出给运行时的是:
|
||||
|
||||
- 单局布局
|
||||
- 评估指标
|
||||
- 可复现 seed
|
||||
|
||||
内容系统负责“有什么内容”,难度生成器负责“这些内容如何排成一盘可玩的局”。
|
||||
Reference in New Issue
Block a user