接水管:让每一张地图都一定有解
随机撒管道很容易做出无解地图。用随机生成树铺一遍,再把每格随机转九十度——可解性由构造保证,玩家只需要转回来。
接水管的规则一句话能说完:每格是一段管子,点一下转九十度,把水从左上角引到每一格。
难点在出题。随机给每格一个朝向,绝大多数地图是无解的——玩家转到手指发酸也接不通,这不是难度,这是 bug。
可解性应该由构造保证
我换了个思路:不去判定一张随机地图有没有解,而是先造一张一定可解的地图,再把它打乱。
做法是随机生成树。从左上角出发,随机挑一个还没访问的邻居打通,递归下去,直到所有格子都被访问过。这样得到的连接图天然是一棵树——没有环,而且每一格都和源点连通。

图:先连成一棵树,再把每格转乱;玩家要做的就是把旋转还原。
const stack: number[] = [0];
visited[0] = true;
while (stack.length) {
const current = stack[stack.length - 1];
const neighbours = freeNeighbours(current);
if (!neighbours.length) {
stack.pop();
continue;
}
const pick = neighbours[Math.floor(random() * neighbours.length)];
masks[current] |= pick.from;
masks[pick.index] |= pick.to;
visited[pick.index] = true;
stack.push(pick.index);
}
树的另一个好处是没有多余的管子:每一段都必须接上,玩家不会遇到「这段好像没用」的困惑。
打乱:旋转,不是重排
生成完之后,每格随机转 0–3 次。因为旋转是可逆的,解一定存在——把每格反向转回去就是答案。
这里有个细节:保存的是「转了几次」,不是最终朝向。重置时要能回到初始状态,而初始状态是打乱后的样子;如果只存朝向,重置就退化成「重新生成一张地图」。
连通判定:两边都得开口
判定水有没有通到每一格,用一次 BFS 就够了。关键是相邻两格必须互相开口:
if (!(mask & need)) continue; // 我这格朝它有口
if (!(masks[next] & back)) continue; // 它那格朝我也得有口
只判一边会得到什么结果?看起来「通了」,实际管子只从一侧伸出来,视觉上是断的。我第一版就漏了这个反向检查,测试时发现明明画面上是断的,代码却说通关了。
朝向用四位掩码表示:北 1、东 2、南 4、西 8。旋转九十度就是循环左移一位:
export function rotate(mask: number): number {
return ((mask << 1) | (mask >> 3)) & 15;
}
北→东→南→西→北,四次回到原样,正好对应「点四下转一圈」。
颜色即状态
界面上的反馈不需要额外的状态字段:BFS 返回一个布尔数组,通水的格子用强调色画管子,没通的用灰。玩家一眼就能看出水卡在哪一格。
这也顺带解决了「怎么算赢」:数组全是 true 就是通关。不用维护计数器,也不会出现计数器和画面不一致的情况。
一点手感
- 每次点击都立刻转,不等动画结束——转管子是高频操作,延迟会毁掉节奏
- 通关后禁止再转,避免玩家把已完成的图转坏
- 「换一张」重新生成,不保留进度:这类游戏一局就该几分钟
构造保证可解,判定保证反馈正确,剩下的就只是转。

评论
…