18 KiB
拼图玩法创作与运行态最小落地技术方案
日期:2026-04-22
1. 文档目的
本文件承接 PRD《AI 原生拼图玩法创作工具与玩法系统》,冻结本轮拼图玩法在当前平台内的最小完整闭环。
本轮目标不是抽象一个通用拼图编辑器,也不是额外搭建一个脱离平台的新小游戏站点,而是在现有平台壳层内新增独立 puzzle 玩法域,跑通下面这条主链:
- 平台创作入口选择拼图玩法
- Agent-first 对话收束 5 个高杠杆视觉锚点
- 编译结果页草稿
- 在结果页编辑关卡名、标签并生成候选拼图图片
- 发布作品进入拼图广场
- 玩家从广场进入第 1 关
- 后端初始化
3x3 / 4x4棋盘 - 后端裁决交换、合并、拖动、拆分与通关
- 通关后根据“标签相似度
70%+ 同作者30%”推荐下一关
2. 本轮明确不做
- 不做异形拼块、旋转拼块、道具、体力、倒计时。
- 不做新的平台站点或新的全局导航系统。
- 不做前端本地推荐、前端本地裁决、前端本地持久化真相。
- 不做复杂图片模型编排;首版图片生成沿用
api-server的占位资产生成方式,保证完整链路可跑通。 - 不把拼图玩法继续挂在
customWorld、rpgWorld或 RPG runtime 旧语义下。 - 不扩到拼图排行榜、社交评论、收藏、复盘系统。
3. 分层边界
3.1 前端
前端继续使用当前 React + TypeScript + Vite 平台壳层,只负责:
- 展示拼图创作中心、Agent 工作区、结果页、广场、运行时画布。
- 发起聊天、结果页编辑、发布、开始游戏、交换与拖动请求。
- 基于后端快照渲染棋盘、HUD、选中态与合并反馈。
前端禁止:
- 自行判断下一关推荐。
- 自行判断拼块是否应当合并。
- 自行判断合并块是否应当拆分。
- 自行判断通关。
3.2 Axum
server-rs/crates/api-server 负责:
- 对外暴露
/api/runtime/puzzle-*HTTP 接口。 - 鉴权、请求上下文、错误 envelope。
- 结果页占位图片生成与静态资产落盘。
- 调用
spacetime-client读写拼图玩法真相态。
3.3 SpacetimeDB
server-rs/crates/spacetime-module 负责:
- 存储拼图 Agent session / message。
- 存储已发布拼图作品 profile。
- 存储拼图运行态 run snapshot。
- 通过 procedure 同步返回 session / works / gallery / runtime 快照。
3.4 纯领域 crate
新增 server-rs/crates/module-puzzle,承载:
- 5 个锚点与会话阶段的纯领域模型。
- 草稿编译、标签规范化、发布校验。
3x3 / 4x4棋盘初始化。- 交换、合并、拖动、拆分、通关与下一关推荐算法。
4. 共享契约
4.1 TypeScript shared contracts
在 packages/shared/src/contracts/ 新增:
puzzleAgentSession.tspuzzleAgentDraft.tspuzzleAgentActions.tspuzzleResultPreview.tspuzzleWorkSummary.tspuzzleRuntimeSession.ts
这些文件分别承载:
- Agent session / message / anchor pack
- 结果页草稿与候选图片
- Agent actions 与 works/gallery mutation request
- 结果页 publish gate / preview
- owner-only works 与 gallery card
- runtime run / board / swap / drag / next-level contract
4.2 Rust shared contracts
在 server-rs/crates/shared-contracts/src/ 新增:
puzzle_agent.rspuzzle_works.rspuzzle_gallery.rspuzzle_runtime.rs
Rust DTO 只承载对前端公开的 HTTP contract,不直接泄露 module-puzzle 内部实现细节。
5. Spacetime 表与 procedure
本轮保持“最小闭环优先”,作品与运行时仍以结构化字段 + snapshot_json 组合持久化,不额外拆出更多高耦合表。
5.1 puzzle_agent_session
字段:
session_idowner_user_idseed_textcurrent_turnprogress_percentstageanchor_pack_jsondraft_jsonlast_assistant_replypublished_profile_idcreated_atupdated_at
5.2 puzzle_agent_message
字段:
message_idsession_idrolekindtextcreated_at
5.3 puzzle_work_profile
字段:
profile_idowner_user_idsource_session_idauthor_display_namelevel_namesummary_texttheme_tags_jsoncover_image_srccover_asset_idanchor_pack_jsonpublication_statusplay_countupdated_atpublished_at
5.4 puzzle_runtime_run
字段:
run_idowner_user_identry_profile_idcurrent_profile_idcleared_level_countcurrent_level_indexcurrent_grid_sizeplayed_profile_ids_jsonprevious_level_tags_jsonsnapshot_jsonupdated_atcreated_at
5.5 Procedure
本轮全部使用 procedure 同步返回快照,避免 Axum 再次读 private table:
create_puzzle_agent_sessionget_puzzle_agent_sessionsubmit_puzzle_agent_messagecompile_puzzle_agent_draftsave_puzzle_generated_imagesselect_puzzle_cover_imagepublish_puzzle_worklist_puzzle_worksget_puzzle_work_detailupdate_puzzle_worklist_puzzle_galleryget_puzzle_gallery_detailstart_puzzle_runget_puzzle_runswap_puzzle_piecesdrag_puzzle_piece_or_groupadvance_puzzle_next_level
6. 结果页图片生成策略
本轮后续已经接入 api-server 统一资产链路:拼图候选图由 api-server 调用图像服务生成,再以 OSS 对象作为持久化真值,SpacetimeDB 只保存候选图 URL、assetId 与 prompt snapshot。
- 每次生成 2 张候选图。
- 候选图通过
api-server写入 OSS,兼容展示路径统一为/generated-puzzle-assets/...,禁止再落到仓库public/目录。 - Axum 把候选图 URL、assetId、prompt snapshot 回写到 Spacetime session draft。
- 创作者在结果页选择其中 1 张作为正式图。
这样可以保证:
- 结果页图片生成、重生、应用正式图完整可用。
- 发布链有正式图片可校验。
- 不再依赖本地
public/占位目录,避免开发工作区混入运行时生成文件。
6.1 发布前编辑真相补充
结果页允许创作者在发布前直接编辑:
关卡名摘要题材标签
这 3 个字段不能只停留在前端临时态。
本轮冻结为:
publish_puzzle_work允许直接携带levelName / summary / themeTagsspacetime-module在发布事务内先把这些字段覆盖回 session draft 真相- 覆盖后的 draft 再参与发布校验与 profile 持久化
这样可以避免额外新增一条“草稿轻量编辑 procedure”,同时确保结果页编辑内容会真实进入广场作品与后续运行时 HUD。
7. 运行态规则冻结
7.1 难度推进
function resolvePuzzleGridSize(clearedLevelCount: number): 3 | 4 {
return clearedLevelCount >= 3 ? 4 : 3;
}
7.2 棋盘初始化
- 根据正式图片与网格规格生成
pieceId -> correctRow/correctCol。 - 随机打乱到非完成态。
- 生成初始
mergedGroups = [],再执行一次正确连接检查。
7.3 正确连接
若两个拼块在当前棋盘中四向相邻,且它们在原图上的正确位置也以同方向相邻,则视为正确连接。
所有正确连接链通过并查集合并为 mergedGroup。
7.4 拖动与拆分
- 单块拖到单块位置:执行交换。
- 合并块拖到任意目标锚点:保持内部相对布局整体重排。
- 若合并块整体平移后覆盖到多个单块,被覆盖单块必须与合并块腾出的原格子做一对一交换,禁止把多个单块回填到同一个源格。
- 一对一交换必须满足:
- 每个被覆盖单块只移动一次。
- 每个被腾出的源格只接收一个被覆盖单块。
- 若腾出的源格数量与被覆盖单块数量不一致,本次拖动视为非法,不更新棋盘。
- 单块拖到合并块占据位置:先拆分目标合并块,再执行交换,最后重算合并。
7.5 通关
当所有拼块回到正确位置,或全盘只剩一个覆盖全部拼块的合并组时,标记当前关卡 cleared。
7.6 下一关推荐
固定公式:
finalScore = tagSimilarityScore * 0.7 + sameAuthorScore * 0.3;
标签相似度首版使用规范化标签集合的 Jaccard。
同分裁决顺序:
tagSimilarityScore更高- 当前 run 未出现过
play_count更低updated_at更近
8. 前端接入
8.1 平台入口
只改现有平台壳层:
- 在创作类型弹层新增“拼图玩法”。
- 新增拼图专属 stage,不改 RPG runtime 主链。
8.2 组件目录
新增:
src/components/puzzle-agent/src/components/puzzle-result/src/components/puzzle-gallery/src/components/puzzle-runtime/
8.3 服务目录
新增:
src/services/puzzle-agent/src/services/puzzle-works/src/services/puzzle-gallery/src/services/puzzle-runtime/
本轮全部走 HTTP facade,不引入新的前端 Spacetime 直连。
8.4 当前前端最小落地补充
当前实现固定走下面这条最小链路:
PlatformEntryCreationTypeModal选择puzzlePuzzleAgentWorkspace收束锚点并触发compile_puzzle_draftPuzzleResultView编辑levelName / summary / themeTags- 图片生成通过独立
PuzzleImageStudioModal触发,不在结果页内联堆叠 - 发布后跳转
PuzzleGalleryDetailView - 从详情进入
PuzzleRuntimeShell
创作中心作品展示冻结为:
- 拼图作品也是平台作品,和其他创作作品共用同一套列表项样式。
- 创作中心不再保留独立“拼图玩法作品模块”。
- 拼图作品仅通过
拼图标签与题材标签区分,不额外拆出第二块作品区。 - 创作中心仍保留统一“新建作品”入口,由创建类型弹层继续分流到 RPG / 拼图玩法。
运行时前端表现冻结为:
- 使用正式封面图按
correctRow / correctCol做真实网格切片渲染 - 点击两块时仅前端维护轻量选中态,真正交换以后端返回快照为准
- 拖动统一采用 pointer 事件,兼顾网页端与移动端
- 不在前端计算合并、拆分、通关与下一关推荐
9. 验收与检查
完成后至少执行:
npm run check:encodingnpm run typechecknpm run testcargo check -p module-puzzlecargo check -p shared-contractscargo check -p spacetime-modulenpm run spacetime:generatecargo check -p spacetime-clientcargo check -p api-server
如果检查中发现拼图主链缺口,继续补齐;如果已经满足 PRD 主链和上述检查,不再追加额外玩法能力。
10. 2026-04-22 最终验收记录
本轮已按“最小完整闭环、禁止超出需求过度实现”完成拼图玩法主链落地,并补齐收尾检查。
10.1 已落地主链
- 平台创作中心可选择
puzzle玩法入口。 PuzzleAgentWorkspace已接入 Agent-first 锚点收束与草稿编译。PuzzleResultView已支持最小结果页编辑与独立图片生成弹层。- 发布后作品可进入拼图广场与详情页。
PuzzleRuntimeShell已按正式封面图真实切片渲染3x3 / 4x4关卡。- 交换、拖动、拆分、合并、通关、下一关推荐真相全部以后端快照为准。
10.2 本轮额外修复的验收阻塞
在最终验收阶段,补齐了与拼图主链无直接业务耦合、但会阻塞仓库整体检查的基线问题:
typecheck基线类型不兼容。AccountModal测试 mock 字段落后于最新鉴权契约。customWorld存档归一化中场景连接方向未收敛到强类型。- 结果页生成资源在签名 URL 尚未返回时会短暂空白,已调整为先展示原路径占位,再异步替换签名读地址。
10.3 实际通过的检查
npm run check:encodingnpm run typechecknpm run testcargo check -p module-puzzlecargo check -p shared-contractscargo check -p spacetime-modulecargo check -p spacetime-clientcargo check -p api-server
10.4 冻结说明
截至本次验收,拼图玩法已满足 PRD 要求的最小产品闭环;未继续扩展排行榜、提示、体力、异形拼块、倒计时、前端本地裁决等超出本轮需求的能力。
11. 2026-04-26 运行态机制补齐记录
本次按 PRD 第 9 章补齐拼图运行态的未完成机制,落点保持在 server-rs/crates/module-puzzle 领域层;前端本地兜底只同步表现和离线闭环,不改变后端真相源。
11.1 棋盘初始化
build_initial_board_with_seed使用种子化洗牌生成初始棋盘,不再固定左移一格。- 正式 run 的种子由
runId + profileId + levelIndex + gridSize派生;由于每次进入都会创建新的runId,同一作品多次进入也会得到不同打乱样式。 - 洗牌后若极端情况下仍为完成态,强制旋转一次,保证新关卡不是已完成局面。
- 初始棋盘不得存在任何原图相邻块互相贴边;初始化会多次洗牌筛选,若极端情况下未命中,则使用确定性约束搜索兜底,避免开局出现局部连续结构。
module-puzzle与本地 fallback 的测试都必须直接断言初始棋盘不存在原图相邻贴边对,不能只检查mergedGroups = []。
11.2 局部重算与合并
- 交换后只把源格、目标格和四向邻格纳入重算范围。
- 拖动后把源格、目标格、被移动合并块边界格、被拆分目标合并块格子纳入重算范围。
- 对受影响范围内的旧合并组先拆回单块,再按正确四向相邻关系重新生成合并组;未受影响的旧合并组保留。
- 每次生成快照时统一重编号合并组,避免保留组与新组出现重复
groupId。
11.3 拖动与拆分
- 单块拖到单块位置时执行交换。
- 合并块拖动时保持内部相对布局,以被拖动块作为锚点整体平移。
- 单块拖入合并块占据位置时,先拆分目标合并块,再完成本次交换,最后按受影响范围重新合并。
11.4 本次新增验证
cargo test -p module-puzzle覆盖:每次 run 不同打乱、正确相邻自动合并、单块拖入合并块拆分目标组。npm run test -- src/services/puzzle-runtime/puzzleLocalRuntime.test.ts覆盖:本地兜底每次启动不同打乱、交换后正确相邻自动合并、通关后推进下一关。npm run check:encoding已通过,确认中文文档未被编码写坏。
12. 2026-04-26 二次运行态缺口补齐
本次继续按 PRD 第 9.12 与第 14.4 节收敛两个剩余缺口:
- 通关判定必须同时支持“所有拼块回到正确位置”和“所有拼块汇成一个覆盖全盘的大合并块”。领域层以
all_tiles_resolved作为唯一对外真相,但其计算来源必须包含这两个条件。 - 运行态底部不再常驻玩法说明文字,只保留短状态反馈、错误反馈和下一关动作;点击/拖动规则不写成长期 UI 文案。
12.1 合并块可见性修正
用户反馈“正确连接的块自动合并没有看到”后,确认原实现只把已合并格子染成绿色,仍按单块逐格渲染,视觉上无法形成“合并块”。本次运行态画布改为:
- 根据
mergedGroups计算合并块外接矩形。 - 原单格位置让位为透明占位。
- 在棋盘上叠加一个跨格整体层,内部仍按原图切片拼接,但外边框、阴影和拖动事件都属于同一个合并块。
- 合并块整体层以组内第一块作为拖动锚点,继续沿用后端/本地运行态的合并块拖动裁决。
12.2 拖动可用性修正
用户反馈“没有办法拖动拼图块”后,确认原交互只在 pointer move 超过阈值后记录 dragging = true,没有持续记录当前指针位置,也没有把拖动中的块做视觉平移;移动端还可能被浏览器默认触控手势抢占。修正如下:
dragState持续记录currentX/currentY,拖动中按指针偏移对单块或合并块做translate3d跟手反馈。- 棋盘与合并块交互层增加
touch-none select-none,避免移动端滚动、选中文本等默认行为打断拖动。 - 松手后仍只提交
pieceId + targetRow + targetCol,最终交换、合并、拆分和通关继续以后端/本地运行态快照为准。
12.3 合并块外轮廓描边修正
用户反馈“合并的块的边界显示要描边自己的块的边界,不要搞一个正方形或者矩形的边界”后,移除合并块外接矩形 ring 层。运行态现在按合并组真实占据格逐格判断四向邻居:某一边没有同组合并格时才画该边描边,同组内部相邻边不画线。这样 L 形、长条或其他非矩形合并块只显示自身外轮廓,拖动热区仍只覆盖真实拼块格。
后续反馈要求合并块边界也要圆角后,外轮廓描边补充按四个角判断:只有相邻两条外露边同时存在的真实外轮廓角才应用圆角,内部拼接角保持直角且不显示分界线。
12.4 第二关后打乱规则旁路修正
用户反馈“从第二关开始打乱规则像是完全相同”后,检查发现 api-server 的本地下一关 fallback 仍使用旧版 build_local_puzzle_board 固定左移一格,没有复用 module-puzzle 的种子化初始化规则。该路径会在图库/正式推荐不可用、由 API 临时构造下一关时触发。
修正后 api-server 本地下一关构造改为调用 module_puzzle::build_initial_board_with_seed,种子由 runId + profileId + levelIndex + gridSize 派生;因此第二关、第三关以及后续 fallback 关卡也会得到不同布局,并继续满足“开局没有原图相邻块贴边”的约束。