最朴素的AI------用if-else模拟人类下棋直觉
设计截图如下:

代码如下:
typescript
/**
* AIPlayer.ets - 五子棋AI对手
* 支持三档难度:简单、普通、困难
*/
import { BOARD_SIZE, EMPTY, BLACK, WHITE, Difficulty, Move, getOpponent } from './GameConstants';
/** 评分常量 */
const SCORE_FIVE: number = 1000000; // 五连
const SCORE_OPEN_FOUR: number = 100000; // 活四
const SCORE_FOUR: number = 10000; // 冲四
const SCORE_OPEN_THREE: number = 8000; // 活三
const SCORE_THREE: number = 500; // 眠三
const SCORE_OPEN_TWO: number = 400; // 活二
const SCORE_TWO: number = 50; // 眠二
const SCORE_ONE: number = 10; // 单子
/** 带分数的落子候选 */
class ScoredMove {
move: Move;
score: number;
constructor(move: Move, score: number) {
this.move = move;
this.score = score;
}
}
export class AIPlayer {
private aiColor: number;
private humanColor: number;
private difficulty: Difficulty;
constructor(aiColor: number, difficulty: Difficulty) {
this.aiColor = aiColor;
this.humanColor = getOpponent(aiColor);
this.difficulty = difficulty;
}
/**
* 获取AI下一步落子位置
*/
getMove(board: number[][]): Move {
// 空棋盘直接下天元
let isEmpty = true;
for (let i = 0; i < BOARD_SIZE; i++) {
for (let j = 0; j < BOARD_SIZE; j++) {
if (board[i][j] !== EMPTY) {
isEmpty = false;
break;
}
}
if (!isEmpty) break;
}
if (isEmpty) {
return new Move(7, 7);
}
switch (this.difficulty) {
case Difficulty.EASY:
return this.getEasyMove(board);
case Difficulty.NORMAL:
return this.getNormalMove(board);
case Difficulty.HARD:
return this.getHardMove(board);
default:
return this.getNormalMove(board);
}
}
// ==================== 简单模式 ====================
private getEasyMove(board: number[][]): Move {
const candidates = this.getCandidates(board, 2);
// 1. AI能赢就赢
for (const move of candidates) {
board[move.row][move.col] = this.aiColor;
if (this.checkWin(board, move.row, move.col, this.aiColor)) {
board[move.row][move.col] = EMPTY;
return move;
}
board[move.row][move.col] = EMPTY;
}
// 2. 堵对手四连
for (const move of candidates) {
board[move.row][move.col] = this.humanColor;
if (this.checkWin(board, move.row, move.col, this.humanColor)) {
board[move.row][move.col] = EMPTY;
return move;
}
board[move.row][move.col] = EMPTY;
}
// 3. 堵对手活三
for (const move of candidates) {
board[move.row][move.col] = this.humanColor;
const score = this.evaluatePosition(board, move.row, move.col, this.humanColor);
board[move.row][move.col] = EMPTY;
if (score >= SCORE_OPEN_THREE) {
return move;
}
}
// 4. 随机选择(偏中心)
const centerDist = (m: Move): number => Math.abs(m.row - 7) + Math.abs(m.col - 7);
candidates.sort((a: Move, b: Move) => {
const diff = centerDist(a) - centerDist(b);
if (diff !== 0) return diff;
return Math.random() - 0.5;
});
const topN = Math.min(5, candidates.length);
const idx = Math.floor(Math.random() * topN);
return candidates[idx];
}
// ==================== 普通模式 ====================
private getNormalMove(board: number[][]): Move {
const candidates = this.getCandidates(board, 2);
let bestMove: Move = candidates[0];
let bestScore: number = -1;
for (const move of candidates) {
board[move.row][move.col] = this.aiColor;
const attackScore = this.evaluatePosition(board, move.row, move.col, this.aiColor);
board[move.row][move.col] = EMPTY;
board[move.row][move.col] = this.humanColor;
const defendScore = this.evaluatePosition(board, move.row, move.col, this.humanColor);
board[move.row][move.col] = EMPTY;
const totalScore = attackScore * 1.1 + defendScore;
if (totalScore > bestScore) {
bestScore = totalScore;
bestMove = move;
}
}
return bestMove;
}
// ==================== 困难模式 ====================
private getHardMove(board: number[][]): Move {
const candidates = this.getSortedCandidates(board);
const maxCandidates = Math.min(12, candidates.length);
let bestMove: Move = candidates[0];
let bestScore: number = -Infinity;
let alpha: number = -Infinity;
const beta: number = Infinity;
for (let i = 0; i < maxCandidates; i++) {
const move = candidates[i];
board[move.row][move.col] = this.aiColor;
if (this.checkWin(board, move.row, move.col, this.aiColor)) {
board[move.row][move.col] = EMPTY;
return move;
}
const score = this.minimax(board, 2, alpha, beta, false);
board[move.row][move.col] = EMPTY;
if (score > bestScore) {
bestScore = score;
bestMove = move;
}
alpha = Math.max(alpha, score);
}
return bestMove;
}
private minimax(board: number[][], depth: number, alpha: number, beta: number,
isMaximizing: boolean): number {
if (depth === 0) {
return this.evaluateBoard(board);
}
const candidates = this.getSortedCandidates(board);
const maxCandidates = Math.min(8, candidates.length);
if (isMaximizing) {
let maxEval: number = -Infinity;
for (let i = 0; i < maxCandidates; i++) {
const move = candidates[i];
board[move.row][move.col] = this.aiColor;
if (this.checkWin(board, move.row, move.col, this.aiColor)) {
board[move.row][move.col] = EMPTY;
return SCORE_FIVE;
}
const evalScore = this.minimax(board, depth - 1, alpha, beta, false);
board[move.row][move.col] = EMPTY;
maxEval = Math.max(maxEval, evalScore);
alpha = Math.max(alpha, evalScore);
if (beta <= alpha) break;
}
return maxEval;
} else {
let minEval: number = Infinity;
for (let i = 0; i < maxCandidates; i++) {
const move = candidates[i];
board[move.row][move.col] = this.humanColor;
if (this.checkWin(board, move.row, move.col, this.humanColor)) {
board[move.row][move.col] = EMPTY;
return -SCORE_FIVE;
}
const evalScore = this.minimax(board, depth - 1, alpha, beta, true);
board[move.row][move.col] = EMPTY;
minEval = Math.min(minEval, evalScore);
beta = Math.min(beta, evalScore);
if (beta <= alpha) break;
}
return minEval;
}
}
// ==================== 评估函数 ====================
private evaluateBoard(board: number[][]): number {
let aiScore: number = 0;
let humanScore: number = 0;
aiScore += this.evaluateLines(board, this.aiColor);
humanScore += this.evaluateLines(board, this.humanColor);
return aiScore - humanScore * 1.1;
}
private evaluateLines(board: number[][], player: number): number {
let total: number = 0;
const directions: number[][] = [[0, 1], [1, 0], [1, 1], [1, -1]];
for (const dir of directions) {
if (dir[0] === 0 && dir[1] === 1) {
for (let row = 0; row < BOARD_SIZE; row++) {
total += this.evaluateLine(board, row, 0, dir[0], dir[1], player);
}
} else if (dir[0] === 1 && dir[1] === 0) {
for (let col = 0; col < BOARD_SIZE; col++) {
total += this.evaluateLine(board, 0, col, dir[0], dir[1], player);
}
} else if (dir[0] === 1 && dir[1] === 1) {
for (let row = 0; row < BOARD_SIZE; row++) {
total += this.evaluateLine(board, row, 0, dir[0], dir[1], player);
}
for (let col = 1; col < BOARD_SIZE; col++) {
total += this.evaluateLine(board, 0, col, dir[0], dir[1], player);
}
} else {
for (let row = 0; row < BOARD_SIZE; row++) {
total += this.evaluateLine(board, row, 0, dir[0], dir[1], player);
}
for (let col = 1; col < BOARD_SIZE; col++) {
total += this.evaluateLine(board, 0, col, dir[0], dir[1], player);
}
}
}
return total;
}
private evaluateLine(board: number[][], startRow: number, startCol: number,
dr: number, dc: number, player: number): number {
let score: number = 0;
let row = startRow;
let col = startCol;
let consecutive: number = 0;
while (row >= 0 && row < BOARD_SIZE && col >= 0 && col < BOARD_SIZE) {
if (board[row][col] === player) {
consecutive++;
} else {
if (consecutive > 0) {
let openEnds: number = 0;
const prevR = row - dr * (consecutive + 1);
const prevC = col - dc * (consecutive + 1);
if (prevR >= 0 && prevR < BOARD_SIZE && prevC >= 0 && prevC < BOARD_SIZE &&
board[prevR][prevC] === EMPTY) {
openEnds++;
}
if (board[row][col] === EMPTY) {
openEnds++;
}
score += this.scoreForCount(consecutive, openEnds);
consecutive = 0;
}
}
row += dr;
col += dc;
}
if (consecutive > 0) {
let openEnds: number = 0;
const prevR = row - dr * (consecutive + 1);
const prevC = col - dc * (consecutive + 1);
if (prevR >= 0 && prevR < BOARD_SIZE && prevC >= 0 && prevC < BOARD_SIZE &&
board[prevR][prevC] === EMPTY) {
openEnds++;
}
score += this.scoreForCount(consecutive, openEnds);
}
return score;
}
private scoreForCount(count: number, openEnds: number): number {
if (count >= 5) return SCORE_FIVE;
if (count === 4) {
if (openEnds === 2) return SCORE_OPEN_FOUR;
if (openEnds === 1) return SCORE_FOUR;
return 0;
}
if (count === 3) {
if (openEnds === 2) return SCORE_OPEN_THREE;
if (openEnds === 1) return SCORE_THREE;
return 0;
}
if (count === 2) {
if (openEnds === 2) return SCORE_OPEN_TWO;
if (openEnds === 1) return SCORE_TWO;
return 0;
}
if (count === 1) {
if (openEnds === 2) return SCORE_ONE;
return 0;
}
return 0;
}
private evaluatePosition(board: number[][], row: number, col: number,
player: number): number {
const directions: number[][] = [[0, 1], [1, 0], [1, 1], [1, -1]];
let totalScore: number = 0;
for (const dir of directions) {
totalScore += this.evaluateDirection(board, row, col, dir[0], dir[1], player);
}
return totalScore;
}
private evaluateDirection(board: number[][], row: number, col: number,
dr: number, dc: number, player: number): number {
let count: number = 1;
let leftOpen: boolean = false;
let rightOpen: boolean = false;
let r = row + dr;
let c = col + dc;
while (r >= 0 && r < BOARD_SIZE && c >= 0 && c < BOARD_SIZE && board[r][c] === player) {
count++;
r += dr;
c += dc;
}
if (r >= 0 && r < BOARD_SIZE && c >= 0 && c < BOARD_SIZE && board[r][c] === EMPTY) {
rightOpen = true;
}
r = row - dr;
c = col - dc;
while (r >= 0 && r < BOARD_SIZE && c >= 0 && c < BOARD_SIZE && board[r][c] === player) {
count++;
r -= dr;
c -= dc;
}
if (r >= 0 && r < BOARD_SIZE && c >= 0 && c < BOARD_SIZE && board[r][c] === EMPTY) {
leftOpen = true;
}
const openCount = (leftOpen ? 1 : 0) + (rightOpen ? 1 : 0);
return this.scoreForCount(count, openCount);
}
// ==================== 辅助方法 ====================
private checkWin(board: number[][], row: number, col: number, player: number): boolean {
const directions: number[][] = [[0, 1], [1, 0], [1, 1], [1, -1]];
for (const dir of directions) {
let count = 1;
for (let i = 1; i < 5; i++) {
const r = row + dir[0] * i;
const c = col + dir[1] * i;
if (r < 0 || r >= BOARD_SIZE || c < 0 || c >= BOARD_SIZE) break;
if (board[r][c] === player) count++;
else break;
}
for (let i = 1; i < 5; i++) {
const r = row - dir[0] * i;
const c = col - dir[1] * i;
if (r < 0 || r >= BOARD_SIZE || c < 0 || c >= BOARD_SIZE) break;
if (board[r][c] === player) count++;
else break;
}
if (count >= 5) return true;
}
return false;
}
private getCandidates(board: number[][], range: number = 2): Move[] {
const candidates: Move[] = [];
const seen: Set<string> = new Set();
for (let i = 0; i < BOARD_SIZE; i++) {
for (let j = 0; j < BOARD_SIZE; j++) {
if (board[i][j] !== EMPTY) {
for (let dr = -range; dr <= range; dr++) {
for (let dc = -range; dc <= range; dc++) {
const r = i + dr;
const c = j + dc;
if (r >= 0 && r < BOARD_SIZE && c >= 0 && c < BOARD_SIZE &&
board[r][c] === EMPTY) {
const key = `${r},${c}`;
if (!seen.has(key)) {
seen.add(key);
candidates.push(new Move(r, c));
}
}
}
}
}
}
}
if (candidates.length === 0) {
candidates.push(new Move(7, 7));
}
return candidates;
}
private getSortedCandidates(board: number[][]): Move[] {
const candidates = this.getCandidates(board, 2);
const scored: ScoredMove[] = [];
for (const move of candidates) {
board[move.row][move.col] = this.aiColor;
const attack = this.evaluatePosition(board, move.row, move.col, this.aiColor);
board[move.row][move.col] = EMPTY;
board[move.row][move.col] = this.humanColor;
const defend = this.evaluatePosition(board, move.row, move.col, this.humanColor);
board[move.row][move.col] = EMPTY;
scored.push(new ScoredMove(move, attack + defend));
}
scored.sort((a: ScoredMove, b: ScoredMove) => b.score - a.score);
return scored.map((s: ScoredMove) => s.move);
}
}
简单AI的决策链
typescript
private getEasyMove(board: number[][]): Move {
const candidates = this.getCandidates(board, 2);
// 优先级1:AI能赢就赢
for (const move of candidates) {
board[move.row][move.col] = this.aiColor;
if (this.checkWin(board, move.row, move.col, this.aiColor)) {
board[move.row][move.col] = EMPTY;
return move;
}
board[move.row][move.col] = EMPTY;
}
// 优先级2:堵对手四连
for (const move of candidates) {
board[move.row][move.col] = this.humanColor;
if (this.checkWin(board, move.row, move.col, this.humanColor)) {
board[move.row][move.col] = EMPTY;
return move;
}
board[move.row][move.col] = EMPTY;
}
// 优先级3:堵对手活三
for (const move of candidates) {
board[move.row][move.col] = this.humanColor;
const score = this.evaluatePosition(board, move.row, move.col, this.humanColor);
board[move.row][move.col] = EMPTY;
if (score >= SCORE_OPEN_THREE) {
return move;
}
}
// 优先级4:随机选择(偏中心)
const centerDist = (m: Move): number => Math.abs(m.row - 7) + Math.abs(m.col - 7);
candidates.sort((a: Move, b: Move) => {
const diff = centerDist(a) - centerDist(b);
if (diff !== 0) return diff;
return Math.random() - 0.5;
});
const topN = Math.min(5, candidates.length);
const idx = Math.floor(Math.random() * topN);
return candidates[idx];
}
四级优先级详解
优先级1:AI能赢就赢
typescript
board[move.row][move.col] = this.aiColor;
if (this.checkWin(board, move.row, move.col, this.aiColor)) {
board[move.row][move.col] = EMPTY;
return move;
}
board[move.row][move.col] = EMPTY;
模拟落子→检查胜负→撤销:这是AI中最常用的"试探"模式。对每个候选位置,假设AI落子在此,检查是否形成五连。如果赢,立即返回。
优先级2:堵对手四连
同样的试探模式,但这次模拟的是人类落子。如果人类在此处落子能赢,AI必须堵住。
优先级3:堵对手活三
typescript
const score = this.evaluatePosition(board, move.row, move.col, this.humanColor);
if (score >= SCORE_OPEN_THREE) {
return move;
}
活三是威胁性很强的棋型(下一步可以变成活四),必须提前防御。
优先级4:随机偏中心
typescript
candidates.sort((a: Move, b: Move) => {
const diff = centerDist(a) - centerDist(b);
if (diff !== 0) return diff;
return Math.random() - 0.5; // 距离相同时随机
});
const topN = Math.min(5, candidates.length);
const idx = Math.floor(Math.random() * topN);
return candidates[idx];
中心偏置 :centerDist计算曼哈顿距离,距离中心近的排前面。
随机性:从前5个候选中随机选一个,增加AI的不可预测性。
"试探-撤销"模式
这是简单AI最核心的技术:
typescript
// 1. 在空位上模拟落子
board[move.row][move.col] = player;
// 2. 检查效果
const wins = this.checkWin(board, move.row, move.col, player);
// 3. 撤销模拟
board[move.row][move.col] = EMPTY;
为什么直接修改board而不创建副本?
- 创建15x15数组副本有内存开销
- 试探后立即撤销,board状态不变
- JavaScript是单线程的,不存在并发问题
简单AI的局限性
- 只看一步:不考虑对手的后续反应
- 不会主动进攻:除了"能赢就赢",没有进攻策略
- 可能被双活三击败:只堵第一个发现的活三
- 随机性可能下出臭棋:优先级4完全随机
但这些局限正是"简单"的含义------它是一个合格的入门级AI。
对比普通AI
typescript
// 简单AI:规则优先级,找到就返回
if (能赢) return 赢;
if (要堵) return 堵;
// 普通AI:评估所有候选,选最优
for (每个候选) {
score = 攻击分 × 1.1 + 防守分
}
return 最高分;
简单AI是"找到就做",普通AI是"比较后选最优"。
总结
简单AI展示了最基础的AI设计模式:
- 优先级链:从高到低依次检查
- 试探-撤销:模拟落子评估效果
- 中心偏置:符合棋类开局直觉
- 适度随机:增加不可预测性
这种规则驱动的AI虽然简单,但在实际游戏中能提供合理的对手体验。