接水管:让每一张地图都一定有解

随机撒管道很容易做出无解地图。用随机生成树铺一遍,再把每格随机转九十度——可解性由构造保证,玩家只需要转回来。

接水管的规则一句话能说完:每格是一段管子,点一下转九十度,把水从左上角引到每一格。

难点在出题。随机给每格一个朝向,绝大多数地图是无解的——玩家转到手指发酸也接不通,这不是难度,这是 bug。

可解性应该由构造保证

我换了个思路:不去判定一张随机地图有没有解,而是先造一张一定可解的地图,再把它打乱。

做法是随机生成树。从左上角出发,随机挑一个还没访问的邻居打通,递归下去,直到所有格子都被访问过。这样得到的连接图天然是一棵树——没有环,而且每一格都和源点连通。

左:随机生成树铺满每一格;右:每格随机旋转 0 到 3 次后的题面,可解性由构造保证

图:先连成一棵树,再把每格转乱;玩家要做的就是把旋转还原。

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 就是通关。不用维护计数器,也不会出现计数器和画面不一致的情况。

一点手感

  • 每次点击都立刻转,不等动画结束——转管子是高频操作,延迟会毁掉节奏
  • 通关后禁止再转,避免玩家把已完成的图转坏
  • 「换一张」重新生成,不保留进度:这类游戏一局就该几分钟

构造保证可解,判定保证反馈正确,剩下的就只是转。

← 返回文章列表

评论

…