获胜的棋形

中间空一格的○_○○○,只要填上那个空格就成了五连。用五子棋术语来说,这叫跳四。如果一手棋同时形成这个跳四和另一条线上的连三(○○○),就能获胜。对方必须先挡四,而在这期间把三变成活四,就再也挡不住了。这就是四三。

人很容易找到这个点。可是现在的AI即使能用自己的棋子形成这个棋形,也不会去下。

AI错过的四三。下在虚线位置就会形成横向的跳四和纵向的连三,但AI下在了这条线的左端
AI错过的四三。下在虚线位置就会形成横向的跳四和纵向的连三,但AI下在了这条线的左端

在实际对局中

我们重新回放了24局网页对局,寻找轮到AI(白)时能一手形成四三的时刻。这样的时刻有7次,而AI下在那个位置的只有1次。能靠跳四获胜的2次全都错过了。

相反,人却用同样的棋形赢了。在人获胜的22局中,有11局是以四三结束的。

人获胜的22局的终局棋形。活四9,四三(连三)6,四三(跳三)5,四四2
人获胜的22局的终局棋形。活四9,四三(连三)6,四三(跳三)5,四四2

下面是其中的一局。黑棋下在虚线位置后,同时形成了斜向的跳四(●●_●●)和横向的跳三(●●_●)。

实际对局中的四三。黑棋第23手同时形成了四和跳三
实际对局中的四三。黑棋第23手同时形成了四和跳三

本地AI测试

我们把网页上的本地AI代码原样移植过来,在15×15棋盘上随机改变方向进行测试。找到自己四三点的比例,在连四(○○○○)加三的情况下是29%,在跳四(○_○○○)加三的情况下是27%。提前挡住对方下一手能形成四三的位置的比例也只有33%。只挡一个三的情况,连三是67%,跳三是100%。

本地AI测试。找到自己的四三:连四29%,跳四27%;挡住对方的四三点33%;挡连三67%;挡跳三100%
本地AI测试。找到自己的四三:连四29%,跳四27%;挡住对方的四三点33%;挡连三67%;挡跳三100%

为什么看不到

本地AI会对每个空格分别给四个方向打分再相加。但每个方向都只从该格往前数五格,比如往右或往下,而不看后面。像上图那样,如果构成四和三的棋子位于四三点的左边和上边,从那个位置就一颗棋子也看不到。反倒是从这条线的左端往右看能看到三颗棋子,于是它就选了那里。

而且它只是逐条线地累加分数,并不会单独判断两条线在一点相交形成四三。寻找获胜着法的代码也只确认能立即形成五连的着法。所以只有在方向碰巧对上时,它才会下出四三。

禁手判定由引擎另外进行,引擎也会把包含跳三的三三判为禁手。规则是看对了的,不足的是下棋一方的判断。

这一步的实际代码

这是本地AI给一个空格打分的部分。可以看到它只数前面的五格。

omook-web/lib/ai.tstypescript · 38 行
function linePotential(
  board: Stone[],
  r: number,
  c: number,
  dr: number,
  dc: number,
  color: Stone,
  size: number,
): number {
  let streak = 0;
  let blockedNear = false;
  const rr0 = r - dr;
  const cc0 = c - dc;
  if (inBounds(rr0, cc0, size)) {
    const oc = board[idx(rr0, cc0, size)];
    if (oc !== EMPTY && oc !== color) blockedNear = true;
  }

  let rr = r;
  let cc = c;
  for (let k = 0; k < 5 && inBounds(rr, cc, size); k++, rr += dr, cc += dc) {
    const v = board[idx(rr, cc, size)];
    if (v === color) streak++;
    else if (v === EMPTY) {
      /* open */
    } else break;
  }
  let score = streak * streak * 120;
  if (streak >= 4) score += 50000;
  if (blockedNear && streak >= 4) score = Math.floor(score / 8);
  return score;
}

function scorePlacement(board: Stone[], r: number, c: number, who: Stone, size: number): number {
  let s = 0;
  for (const [dr, dc] of DIRS) s += linePotential(board, r, c, dr, dc, who, size);
  return s;
}

下一篇是“4. 让VLM学习启发式、再让启发式重新成为老师的计划”。