
源代码里有搜索 AI
othello-web/lib/ai.ts 中的 AI 用 alpha-beta 搜索向前读几步。空格多时读 4 步,剩 12 格以下时读 6 步,剩 8 格以下时读到终局。盘面通过位置权重、可下位置的数量、角,以及角的斜对角相邻格(X 位)来评估。Qt 应用的 aiplayer.cpp 也是同样的方式。
让这个 AI 按 13–0 的着法顺序来下,第 6 步它会下 B5 而不是 D6,不会走上记录中的着法顺序。
部署的是另一个 AI
我们把 othello.mariago.kr 当前下发的页面代码中的 AI 部分解开来看,里面没有搜索。它在每个可下的位置各试放一次,然后选得分最高的位置。
- 得分 = 位置权重×4 + 盘上棋子数 + (己方棋子 − 对方棋子)×3 − 对方可下位置数×2
- 如果是角,再加 40 分。
让这个 AI 按 13–0 的着法顺序来下,白方的四步与记录完全相同。
网页的最后一次构建是在保存搜索 AI 的 13 分钟之前完成的。之后再也没有重新构建,所以网站上留下的是最初的 AI。
第 8 步
上图是第 8 步白方可以下的六个位置。其中四个位置一旦落子,黑方下一步就能把白子全部翻过来。只看一步的 AI 把其中的 F4 以 -12 分评为最高并选择了它。因为它不看黑方的下一步,所以无法察觉全灭。搜索 AI 把这四个位置都视为必败的着法,选择了 B6。
让两个 AI 对战
我们让三个 AI 两两对战各 100 局。双方轮流执黑执白,为了避免出现完全相同的对局,前 4 步随机落子。
- 搜索 AI 对 只看一步的 AI:100 比 0
- 搜索 AI 对 随机:99 比 1
- 只看一步的 AI 对 随机:86 比 10,平局 4
11 步之内让白方全灭的黑方着法顺序,对只看一步的 AI 有 2 个,对搜索 AI 有 0 个。

这一阶段的实际代码
这是从已部署的页面代码中解开改写的只看一步的 AI。只把缩写的变量名改成了易读的名字。
已部署构建中的 AI
const WEIGHTS = [
[100, -20, 10, 5, 5, 10, -20, 100],
[-20, -50, -2, -2, -2, -2, -50, -20],
[10, -2, 1, 1, 1, 1, -2, 10],
[5, -2, 1, 0, 0, 1, -2, 5],
[5, -2, 1, 0, 0, 1, -2, 5],
[10, -2, 1, 1, 1, 1, -2, 10],
[-20, -50, -2, -2, -2, -2, -50, -20],
[100, -20, 10, 5, 5, 10, -20, 100],
];
function suggestMove(board, who) {
const moves = legalMoves(board, who);
if (moves.length === 0) return null;
const opp = opponent(who);
let best = moves[0];
let bestScore = -1e9;
for (const mv of moves) {
const res = playAt(board, mv.r, mv.c, who, "playing");
if (!res.ok) continue;
const mine = who === BLACK ? res.black : res.white;
const theirs = who === BLACK ? res.white : res.black;
let score = 4 * WEIGHTS[mv.r][mv.c];
score += res.black + res.white;
score += (mine - theirs) * 3;
score -= 2 * legalMoves(res.board, opp).length;
if ((mv.r === 0 || mv.r === 7) && (mv.c === 0 || mv.c === 7)) score += 40;
if (score > bestScore) {
bestScore = score;
best = mv;
}
}
return best;
}这是源代码中的搜索 AI。
othello-web/lib/ai.ts
function searchDepth(empties: number): number {
if (empties <= 8) return empties;
if (empties <= 12) return 6;
if (empties <= 20) return 5;
return 4;
}
// ...
function negamax(
board: Stone[],
who: Stone,
depth: number,
alpha: number,
beta: number,
): SearchResult {
const moves = legalMoves(board, who);
const opp = opponent(who);
if (moves.length === 0) {
if (legalMoves(board, opp).length === 0) {
const { black, white } = counts(board);
const mine = who === BLACK ? black : white;
const theirs = who === BLACK ? white : black;
return { score: (mine - theirs) * 100_000, move: null };
}
const child = negamax(board, opp, depth - 1, -beta, -alpha);
return { score: -child.score, move: null };
}
if (depth <= 0) {
return { score: evaluate(board, who), move: null };
}
let bestScore = -Infinity;
let bestMove: Point | null = null;
for (const mv of orderMoves(moves)) {
const res = playAt(board, mv.r, mv.c, who, "playing");
if (!res.ok) continue;
const child = negamax(res.board, opp, depth - 1, -beta, -alpha);
const score = -child.score;
if (score > bestScore) {
bestScore = score;
bestMove = mv;
}
if (score > alpha) alpha = score;
if (alpha >= beta) break;
}
return { score: bestScore, move: bestMove };
}
/** 알파·베타 탐색. 초반은 위치·기동력, 종반은 돌 수를 중시. */
export function suggestMove(board: Stone[], who: Stone): Point | null {
const moves = legalMoves(board, who);
if (moves.length === 0) return null;
if (moves.length === 1) return moves[0]!;
const depth = searchDepth(emptyCount(board));
const { move } = negamax(board, who, depth, -Infinity, Infinity);
return move ?? moves[0]!;
}下一篇是“3. VLM 一次也没下过:Gemma 4 加载失败”。
COMMENTS · CHAPTER 02
只看一步的 AI:已部署的 AI 与源代码不同 评论 0
还没有评论。阅读时有疑问或想补充的内容,欢迎留言。
发表评论前,请用 Google 开始并登记昵称和邮箱。
用 Google 开始