feat: add puzzle clear template runtime

This commit is contained in:
2026-06-03 22:11:46 +08:00
parent 6e74cf5add
commit 1b5e098225
148 changed files with 19588 additions and 241 deletions

View File

@@ -0,0 +1,115 @@
# 拼消消玩法模板技术方案
日期:`2026-05-30`
## 总体边界
拼消消使用独立工程域 `puzzle-clear`,不复用拼图运行态规则本体。实现按 DDD 分层:
- `module-puzzle-clear`:纯领域规则,覆盖图案组规划、棋盘、交换、半锁定、消除、补牌、防死局、关卡状态。
- `shared-contracts` / `packages/shared`工作台输入、生成素材、结果页、作品摘要、runtime snapshot 与 action DTO。
- `spacetime-module`session、work profile、runtime run、事件 / 统计、公开 source view。
- `spacetime-client`typed facade 与 row mapper。
- `api-server`Axum 路由、鉴权、入口熔断、生成编排、资产持久化、BFF。
- `platform-image` / OSS / asset object图片生成、切图、上传、换签和失败审计。
- 前端:轻表单、生成页、结果页与 runtime 动画,不承接正式业务真相。
## 资产生成方案
素材目标从“单张超大 atlas 生图”收敛为 4 张素材工作表,再由服务端合成最终 atlas
- image2 调用4 次,每次生成 1 张 `1024x1536` 竖版素材工作表。
- sheet 裁切:每张按 `4 列 x 6 行` 裁切,每个 1x1 单元为 `256x256`
- 最终 atlas服务端把 95 个切片按领域坐标合成 `10x10 / 2560x2560` PNG空单元保留浅色背景。
- 运行态素材:最终写回 `35` 个复合图案组和 `95` 个 1x1 卡牌切片。
服务端固定布局如下:
| 形状 | 数量 | 单组单元数 | 解锁 |
| --- | ---: | ---: | --- |
| 1x2 | 23 | 2 | 第 1 关 |
| 1x3 | 5 | 3 | 第 1 关 |
| 2x2 | 4 | 4 | 第 1 关 |
| 2x3 | 3 | 6 | 第 1 关 |
流程:
```text
主题词 / 场地底图主题词 / 用户底图 -> 4 张 sheet 坐标规划 -> gpt-image-2 生成素材工作表 -> 按 4x6 裁切 1x1 -> 合成最终 atlas -> atlas 与卡牌切片持久化 -> OSS / asset_object / bind -> session draft 回写
```
中央场地底图的 prompt 来源固定为:若用户填写 `boardBackgroundPrompt`AI 生成底图只读取该字段;若该字段为空,才回退读取 `themePrompt`。用户直接上传底图资产时不再用主题词重写该资产,只执行平台资产持久化与换签。中央场地底图在运行态不是普通棋盘衬底,而是玩家逐渐消除卡牌后露出的主题目标图;生成请求使用与中央棋盘一致的 1:1 正方形尺寸prompt 必须强调探索、揭开全貌、追求完成目标、精致主题主视觉和强主题表现,不写“画面干净”或“适合作为卡牌棋盘底图”。
### 素材工作表风险与切片验证
风险4x6 工作表 prompt 仍需要告诉 provider 编号布局;如果模型把布局理解成 UI 海报、说明图或卡牌模板,可能画出文字、编号、边框、切分线、贴纸外框或重复主体。若 provider 无法严格按布局输出,切片后可能出现跨格、主体贴边、重复图案、文字或图案错位。
验证策略:
- 生图 prompt 明确禁止文字、水印、UI、边框标签、切分线、网格线、裁切参考线和跨格主体。
- 复合图案组本身不画任何可见分割辅助线,但 prompt 必须说明每个 `1x2``1x3``2x2``2x3` 图案都能被服务端按等大的 1x1 方形单元切分;纵向 `1x2` 按横向切线分成两个 1x1小图案内不显示切线横向 `1x2` 按纵向切线分成两个 1x1小图案内不显示切线其他形状同理。
- 服务端保留 `PuzzleClearPatternGroup` 坐标清单,切片前校验每个 sheet 编号出现次数等于领域图案组 `width * height`,并要求同编号区域是完整连续矩形;切片后还应对尺寸、非空像素比例和重复 hash 做校验。
- 首版若当前 provider 无法稳定产出可切 atlas生成任务进入 `failed`,错误写入审计;不得退回前端假素材或绕过平台资产底座。
- 草稿编译和作品发布都必须拒绝缺失 atlas、缺失卡牌切片、空 `assetObjectId` / `imageObjectKey``placeholder` 占位资产;`spacetime-client` 不再为编译请求合成默认 atlas / card assets。
- 技术回退需要用户确认后才能改成更多 sheet、降低切片规格或改为逐图生成当前需求固定为 4 张 `1024x1536` sheet 与最终 `2560x2560` atlas。
## 领域规则
`module-puzzle-clear` 已固定以下规则:
- 关卡配置:单关 `6x6/35`600 秒。
- 图案组配比:`1x2=23``1x3=5``2x2=4``2x3=3`
- 开局随机铺满并保证至少一步可解。
- 补牌按列重力下落;补牌后仍保证至少一步可解。
- 完整图案组消除并清空对应格。
- 半锁定拼接组只由玩家主动交换 / 撞入打散,补牌不破坏。
- 超时失败只作用于当前单关,可重试;完成 35 次消除目标并清空棋盘后整局完成。
## API 命名空间
- `POST /api/creation/puzzle-clear/sessions`
- `GET /api/creation/puzzle-clear/sessions/{sessionId}`
- `POST /api/creation/puzzle-clear/sessions/{sessionId}/actions`
- `GET /api/creation/puzzle-clear/works`
- `GET /api/creation/puzzle-clear/works/{profileId}`
- `POST /api/creation/puzzle-clear/works/{profileId}/publish`
- `GET /api/runtime/puzzle-clear/works/{profileId}`
- `POST /api/runtime/puzzle-clear/runs`
- `POST /api/runtime/puzzle-clear/runs/{runId}/swap`
- `POST /api/runtime/puzzle-clear/runs/{runId}/retry-level`
- `POST /api/runtime/puzzle-clear/runs/{runId}/next-level`
- `POST /api/runtime/puzzle-clear/runs/{runId}/time-up`
api-server 路由熔断使用 SpacetimeDB 创作入口配置 `puzzle-clear`,不得新增前端硬编码事实源。
## Runtime 事件与统计载荷
正式 `published` run 记录开局、全局完成、当前关失败、耗时和消除统计。runtime action 返回的终态事件包括:
- `run-finished`:第 1 关完成并结束整局,结果 JSON 至少包含 `status``level``clears``clearDelta``elapsedMs`
- `level-failed`:当前关超时失败,结果 JSON 至少包含 `status``level``clears``clearDelta``elapsedMs`
草稿试玩只消费同一份 snapshot/action 结果做表现,不写正式统计。
## 前端阶段
新增阶段:
- `puzzle-clear-workspace` -> `/creation/puzzle-clear`
- `puzzle-clear-generating` -> `/creation/puzzle-clear/generating`
- `puzzle-clear-result` -> `/creation/puzzle-clear/result`
- `puzzle-clear-runtime` -> `/runtime/puzzle-clear`
runtime 移动端优先,首屏结构为顶部倒计时 / 单关铭牌、顶部列准备区、棋盘、失败 / 完成弹层。棋盘主网格、半锁定组覆盖层和消除 / 掉落覆盖层统一使用 3px 格间距。动画包括开场翻转、列补牌下落和消除表现,不再有下一关切换。消除和补牌动画只能作为当前后端 snapshot 的表现层覆盖;已有场上卡片因重力下沉后的最终格不得被旧消除坐标或掉落覆盖层隐藏,避免出现“下方空位但上方卡片未下落”的视觉假象。
## 验证计划
- `cargo test -p module-puzzle-clear --manifest-path server-rs/Cargo.toml`
- `cargo test -p api-server puzzle_clear --manifest-path server-rs/Cargo.toml -- --nocapture`
- `cargo test -p spacetime-client --manifest-path server-rs/Cargo.toml puzzle_clear_compile_requires_real_atlas_assets_from_api_server`
- `npm run test -- src/services/puzzle-clear/puzzleClearLocalRuntime.test.ts`
- `npm run test -- src/components/puzzle-clear-result/PuzzleClearResultView.test.tsx src/components/puzzle-clear-runtime/PuzzleClearRuntimeShell.test.tsx`
- `npm run test -- src/routing/appPageRoutes.test.ts src/services/publicWorkCode.test.ts`
- `npm run check:encoding`
- `npm run typecheck`
- 接入 SpacetimeDB schema 后:`npm run spacetime:generate``npm run check:spacetime-runtime-access``npm run check:spacetime-schema``npm run check:server-rs-ddd`