一个按天轮换的纸笔谜题站点,当前已接入:
NurikabeFillominoYajilin
pnpm install
pnpm dev常用检查:
pnpm build
pnpm lint和新增题型最相关的目录如下:
src/
App.tsx
hooks/
useDailyPuzzleSession.ts
components/
RulesSection.tsx
examples/
NurikabeExample.tsx
FillominoExample.tsx
YajilinExample.tsx
puzzles/
database.ts
registry.tsx
types.ts
Nurikabe/
Fillomino/
Yajilin/
关键职责:
src/puzzles/types.ts维护题型的数据结构定义。src/puzzles/registry.tsx维护题型注册中心:parsePuzzLink、template、renderBoard、renderExample。src/puzzles/database.ts维护每日题列表allPuzzles,并通过注册中心解析题目。src/components/RulesSection.tsx渲染规则区;具体 example 内容由注册中心提供。src/hooks/useDailyPuzzleSession.ts管理每日题会话,不需要为新增题型单独修改。
下面以假设新增题型 Slitherlink 为例。
你至少需要补这几类:
- 题面数据类型
- 规则区 example 数据类型
PuzzleData联合类型PuzzleTypePuzzleEntry如果这个题型有特殊来源格式,也要补进去
示意:
export interface SlitherlinkPuzzleData {
type: 'slitherlink';
width: number;
height: number;
clues: (number | null)[][];
}
export type PuzzleData =
| NurikabePuzzleData
| FillominoPuzzleData
| YajilinPuzzleData
| SlitherlinkPuzzleData;如果规则示例的答案展示方式和现有题型都不一样,也要给 PuzzleExample 新增一个分支。
建议目录:
src/puzzles/Slitherlink/
Slitherlink.tsx
utils.ts
最低要求通常有两部分:
Slitherlink.tsx主棋盘组件,接收:puzzlestartTimeonComplete
utils.ts解析、判题、辅助函数
当前项目中可以直接参考:
src/puzzles/Nurikabe/src/puzzles/Fillomino/src/puzzles/Yajilin/
如果你的题目来源是 pzprxs 链接或其他编码字符串,需要在该题型的 utils.ts 中提供:
export function parseSlitherlinkLink(link: string): SlitherlinkPuzzleData | null然后在注册中心中接入它。
如果你的题型不走链接,而是只写本地静态题面,也可以先不写 parser,但一般还是建议补上,这样题库维护会轻松很多。
这是现在接入新题型最核心的一步。
你需要做四件事:
- 引入主棋盘和 example 组件
- 引入
parsePuzzLink - 提供
template - 在 entry 上实现
renderBoard和renderExample
示意:
slitherlink: {
parsePuzzLink: parseSlitherlinkLink,
template: {
type: 'slitherlink',
name: 'Slitherlink',
nameCn: '数回',
rulesTitle: '游戏规则',
rules: [...],
exampleTitle: '例题(5×5)',
playableLabel: '可游玩例题',
answerLabel: '正确答案',
example: ...
},
renderBoard: ({ puzzle, startTime, onComplete, key }) => (
<SlitherlinkBoard key={key} puzzle={puzzle} startTime={startTime} onComplete={onComplete} />
),
renderExample: (template) => (
<SlitherlinkExample ... />
),
}注册完成后,这几个能力会自动生效:
database.ts可以通过resolvePuzzleEntry()解析题目App.tsx可以通过renderPuzzleBoard()渲染棋盘- 规则文案和题型名称可以通过
template获取 RulesSection.tsx可以通过注册中心渲染 example
把题目加入 allPuzzles。
当前支持两种写法:
{
type: 'slitherlink',
puzzLink: 'https://...'
}{
type: 'slitherlink',
puzzle: {
type: 'slitherlink',
width: 5,
height: 5,
clues: [...]
}
}如果你想让新题型进入每日轮换,只需要把条目加入 allPuzzles。
你仍然需要新建 example 组件,例如:
src/components/examples/SlitherlinkExample.tsx
但现在不需要再去改 RulesSection.tsx,只需要在 registry.tsx 中把它接进去即可。
至少跑这两个命令:
pnpm build
pnpm exec eslint src/puzzles/registry.tsx src/puzzles/database.ts src/puzzles/types.ts如果你刚新增了题型组件,也建议一起检查:
pnpm exec eslint src/puzzles/Slitherlink/ src/components/examples/SlitherlinkExample.tsx如果你只想快速照着做,可以按这个 checklist:
- 在
src/puzzles/types.ts加类型 - 新建
src/puzzles/<Type>/目录和主棋盘组件 - 补
parse<Type>Link() - 新建
src/components/examples/<Type>Example.tsx - 在
src/puzzles/registry.tsx注册parser + template + renderBoard + renderExample - 在
src/puzzles/database.ts的allPuzzles里加题目 - 运行
pnpm build
如果以后还想继续整理结构,优先级大概是:
- 把各题型的 example 渲染抽成统一接口
- 拆分较大的交互文件,例如
Fillomino.tsx、Yajilin/utils.ts - 补一层题型接入自检文档或测试样例
下面是目前仓库里“从 0 到可上线”新增一个题型的完整执行顺序。
建议严格按顺序做,避免出现“能渲染但不能入库”或“能玩但不能保存进度”这类断层。
必须补齐三类类型:
- 题面数据类型(例如
KurarinPuzzleData) - 题目元素类型(例如
KurarinClue、KurarinClueColor) - 规则示例联合类型(
PuzzleExample新增kurarin分支)
同时把新题型加入:
PuzzleData联合类型PuzzleType(自动来自PuzzleData['type'])- 所有依赖题型联合的地方(例如
CompletionModal的puzzleType)
这一步至少要有:
parseKurarinLink(link):把p?kurarin/...链接转成KurarinPuzzleData- 网格状态初始化(如
createEmptyKurarinGrid) - 边 key 工具(
getEdgeKey/parseEdgeKey/createEdgeSet) - 命中检测(
detectKurarinHitTarget,用于区分点到的是格子还是边) - 校验函数(
validateKurarin)
Kurarin 的校验建议拆成三层:
- 回路合法性:不分叉、不自交、所有未涂黑格都在同一回路中
- 黑格与边冲突:黑格不能带线段
- 圆圈约束:黑/白/灰三色分别满足“黑多/白多/相等”
推荐直接复用现有题型(如 Yajilin)的历史与试填框架:
usePuzzleHistory快照(含grid、loopEdges、crossedEdges、trial level)startBatch/finishBatch保证拖动过程合并历史- 统一
onSnapshotChange让每日进度可持久化
Kurarin 交互规范(本项目约定):
- 桌面端
- 左键拖动格子:连续涂黑或取消涂黑
- 右键拖动/点击格子:放置或取消叉格
- 右键点击边:在边上放置或取消叉号
- 手机端
- 空白格点击/拖动 -> 涂黑
- 涂黑格点击/拖动 -> 叉格
- 叉格点击/拖动 -> 空白
不要在题型文件里硬编码重复样式,优先使用 src/puzzles/boardTheme.ts:
- 棋盘外框:
getBoardFrameStyle - 格子底色:
getBoardCellColors - 叉号字号/样式:
getBoardCrossFontSize+getCrossMarkStyle - 响应式格宽:
getResponsiveCellSize
Kurarin 的三色圆圈建议用 SVG 叠加绘制(便于后续高亮、动画、invalid 态扩展)。
示例区需要两块:
- 左侧可游玩示例(直接复用真实
KurarinBoard) - 右侧答案展示(带遮罩、点击确认后揭晓)
并确保示例答案里可同时显示:
- 黑格
- 回路线
- 边叉
- 三色圆圈
这是最关键的接线步骤,必须一次接全:
- import 新题型 Board / Example / parser
PuzzleRegistry类型新增kurarinpuzzleRegistry.kurarin新增完整 entrytemplate填题型名、规则、示例题面renderBoard/renderExample正确返回组件
只要这里接通,RulesSection、首页渲染、历史回放都会自动走通。
在 allPuzzles 里加一条 p?kurarin/... 即可参与每日轮换。
若 parser 返回 null,这一天会无法出题,所以加库前要先本地验证 parser。
至少跑:
pnpm build
pnpm exec eslint src/puzzles/Kurarin src/components/examples/KurarinExample.tsx src/puzzles/registry.tsx src/puzzles/types.ts src/puzzles/database.ts并手测以下场景:
- 新题型当天题面是否可正常打开
- Undo / Redo / 试填存档是否正常
- 刷新页面后进度是否恢复
- 完成后是否正确触发计时结束与完成弹窗
- 历史记录中加载该题型是否正常
以下流程是按当前仓库结构整理的标准 SOP,建议每次新增题型都完整执行一遍,避免出现“能打开但规则/示例/存档不一致”的隐性问题。
- 新增题型基础数据接口(如
KurarinPuzzleData),至少包含type、width、height、线索结构。 - 若线索有多种形态(颜色、方向、数字、点阵坐标),单独定义
Clue类型与枚举/联合类型。 - 将新题型加入
PuzzleData联合类型。 - 将新题型加入
PuzzleType。 - 将题型加入
PuzzleTemplate的example联合类型。 - 若有完成弹窗或统计面板的类型约束(如
CompletionModal),同步扩展puzzleType。
- 实现
parse<Type>Link(link),兼容p?type/...与纯数据串。 - 如果是 pzpr 系链接,优先按官方源码或编码文档实现,不要靠样例反推。
- 补齐核心规则校验函数(如
validate<Type>),保证返回可用于 UI 高亮的详细错误信息(坏格、坏线索索引等)。 - 若题型涉及线段/边状态,统一实现边 key 工具:
getEdgeKey、parseEdgeKey、createEdgeSet、getIncidentEdgeKeys。 - 若题型需要复杂命中判定(格子/边/点),抽离
detect<Type>HitTarget,避免组件里写大量坐标分支。
- 使用
usePuzzleHistory接入 Undo/Redo 与试填。 - 统一支持
initialSnapshot和onSnapshotChange,保证日历与会话恢复可用。 - 操作与样式优先复用共用函数(
boardTheme.ts):getBoardFrameStyle、getBoardCellColors、getCrossMarkStyle、getResponsiveCellSize。 - 鼠标与触屏交互分开设计并保持一致语义(点击、拖动、右键、边上打叉)。
- 校验结果用于局部高亮,而不是只给 toast 文案。
- 完成条件必须以规则校验通过为准,再触发
onComplete。
- 左侧可玩示例复用主棋盘组件,避免双实现逻辑漂移。
- 右侧答案面板按题型真实元素绘制(格、边、点、圈、叉),不要简化成近似图。
- 示例答案坐标系统必须与解析器一致(例如点阵题型不能按 cell 坐标画线索)。
- 保留遮罩和确认弹窗(防剧透)。
- import 新增的
Board、Example、parse<Type>Link。 - 在
PuzzleRegistry类型里加入新题型 entry。 - 在
puzzleRegistry新增完整配置:parsePuzzLink、template、renderBoard、renderExample。 template.example的数据要可直接驱动示例组件,不要依赖运行时推导。
- 在
allPuzzles增加至少一条新题型数据(优先使用官方可验证链接)。 - 若有多来源数据(链接/内联 puzzle),都要跑通
resolvePuzzleEntry()。
template.rules中英文同时补齐。- 若题型有特殊交互(如边上打叉、移动端三态循环),写进规则说明或帮助文案。
pnpm buildpnpm exec eslint src/puzzles/<Type> src/components/examples/<Type>Example.tsx src/puzzles/registry.tsx src/puzzles/database.ts src/puzzles/types.ts- 手工验证:左键/右键/拖动、触屏操作、撤销重做、试填、重置、完成判定、示例展示、链接解析、会话恢复。
- Kurarin 圆圈坐标是点阵坐标,不是格子坐标。
- 点阵尺寸为
(2*height-1) x (2*width-1)。 - 圆圈可位于:格中心(覆盖 1 格)、边中心(覆盖 2 格)、顶点(覆盖 4 格)。
- 颜色映射按官方实现:
1=black, 2=gray, 3=white。 - 圆圈判定应按覆盖区域内黑白格数量比较,而不是固定邻域。
- 绘制时应使用独立 SVG 覆盖层,按点阵坐标换算到像素位置,不要塞在单元格内部渲染。