Files
website-collection/chess/public/js/ai.js
T

543 lines
9.5 KiB
JavaScript
Raw Normal View History

2026-08-22 17:36:51 +00:00
const PIECE_VALUE = {
P: 100,
N: 320,
B: 330,
R: 500,
Q: 900,
K: 20_000,
};
const LEVELS = {
easy: {
depth: 1,
randomness: 0.35,
},
intermediate: {
depth: 2,
randomness: 0.08,
},
hard: {
depth: 3,
randomness: 0.01,
},
expert: {
depth: 4,
randomness: 0,
},
};
const CENTER = new Set([
"d4",
"e4",
"d5",
"e5",
]);
const EXTENDED_CENTER = new Set([
"c3",
"d3",
"e3",
"f3",
"c4",
"d4",
"e4",
"f4",
"c5",
"d5",
"e5",
"f5",
"c6",
"d6",
"e6",
"f6",
]);
function cloneEngine(engine) {
return new engine.constructor(
engine.getSnapshot(),
);
}
function opposite(color) {
return color === "w"
? "b"
: "w";
}
function squareName(x, y) {
return String.fromCharCode(
97 + x,
) + (8 - y);
}
function getAllMoves(engine, color) {
const moves = [];
for (let y = 0; y < 8; y++) {
for (let x = 0; x < 8; x++) {
const square =
squareName(x, y);
const piece =
engine.state.board[y][x];
if (
!piece ||
piece.color !== color
) {
continue;
}
const pieceMoves =
engine.getMovesFrom(
square,
);
for (const move of pieceMoves) {
moves.push(move);
}
}
}
return moves;
}
function evaluateMaterial(engine, color) {
let score = 0;
for (const row of engine.state.board) {
for (const piece of row) {
if (!piece) {
continue;
}
const value =
PIECE_VALUE[piece.type] ||
0;
score +=
piece.color === color
? value
: -value;
}
}
return score;
}
function evaluatePosition(engine, color) {
let score = 0;
for (let y = 0; y < 8; y++) {
for (let x = 0; x < 8; x++) {
const piece =
engine.state.board[y][x];
if (!piece) {
continue;
}
const square =
squareName(x, y);
const sign =
piece.color === color
? 1
: -1;
if (
CENTER.has(square)
) {
score +=
sign *
(piece.type === "P"
? 30
: 18);
}
if (
EXTENDED_CENTER.has(
square,
)
) {
score += sign * 6;
}
if (
piece.type === "N"
) {
const edgePenalty =
Math.abs(3.5 - x) +
Math.abs(3.5 - y);
score +=
sign *
Math.max(
0,
18 - edgePenalty * 4,
);
}
if (
piece.type === "P"
) {
const advancement =
piece.color === "w"
? 6 - y
: y - 1;
score +=
sign *
advancement *
8;
}
if (
piece.type === "B"
) {
score += sign * 4;
}
if (
piece.type === "R" &&
(y === 0 ||
y === 7)
) {
score += sign * 5;
}
}
}
return score;
}
function evaluateMobility(
engine,
color,
) {
const own =
getAllMoves(
engine,
color,
).length;
const enemy =
getAllMoves(
engine,
opposite(color),
).length;
return (own - enemy) * 3;
}
function evaluateKings(engine, color) {
let score = 0;
const status =
engine.state.status;
if (!status) {
return 0;
}
if (status.check) {
const turn =
engine.state.turn;
if (turn === color) {
score -= 35;
} else {
score += 35;
}
}
return score;
}
function evaluate(engine, color) {
const status =
engine.state.status;
if (
status?.phase ===
"checkmate"
) {
if (
status.winner === color
) {
return 1_000_000;
}
return -1_000_000;
}
if (
status?.phase === "draw"
) {
return 0;
}
return (
evaluateMaterial(
engine,
color,
) +
evaluatePosition(
engine,
color,
) +
evaluateMobility(
engine,
color,
) +
evaluateKings(
engine,
color,
)
);
}
function orderMoves(
engine,
moves,
) {
return [...moves].sort(
(a, b) => {
const captureA =
a.capture ? 1 : 0;
const captureB =
b.capture ? 1 : 0;
const promotionA =
a.promotion ? 1 : 0;
const promotionB =
b.promotion ? 1 : 0;
return (
captureB -
captureA ||
promotionB -
promotionA
);
},
);
}
function minimax(
engine,
depth,
alpha,
beta,
maximizingColor,
) {
const status =
engine.state.status;
if (
depth <= 0 ||
status?.phase ===
"checkmate" ||
status?.phase === "draw"
) {
return {
score: evaluate(
engine,
maximizingColor,
),
move: null,
};
}
const turn =
engine.state.turn;
const moves =
orderMoves(
engine,
getAllMoves(
engine,
turn,
),
);
if (!moves.length) {
return {
score: evaluate(
engine,
maximizingColor,
),
move: null,
};
}
const maximizing =
turn === maximizingColor;
let bestScore = maximizing
? -Infinity
: Infinity;
let bestMove = null;
for (const move of moves) {
const child =
cloneEngine(engine);
const result =
child.makeMove({
from: move.from,
to: move.to,
promotion:
move.promotion ||
null,
});
if (!result.ok) {
continue;
}
const resultNode =
minimax(
child,
depth - 1,
alpha,
beta,
maximizingColor,
);
const score =
resultNode.score;
if (maximizing) {
if (
score >
bestScore
) {
bestScore = score;
bestMove = move;
}
alpha = Math.max(
alpha,
bestScore,
);
} else {
if (
score <
bestScore
) {
bestScore = score;
bestMove = move;
}
beta = Math.min(
beta,
bestScore,
);
}
if (beta <= alpha) {
break;
}
}
return {
score: bestScore,
move: bestMove,
};
}
export const AI_LEVELS = LEVELS;
export function chooseComputerMove(
engine,
level = "intermediate",
) {
const config =
LEVELS[level] ||
LEVELS.intermediate;
const color =
engine.state.turn;
const moves =
getAllMoves(
engine,
color,
);
if (!moves.length) {
return null;
}
const ranked = [];
for (const move of moves) {
const child =
cloneEngine(engine);
const result =
child.makeMove({
from: move.from,
to: move.to,
promotion:
move.promotion ||
null,
});
if (!result.ok) {
continue;
}
const score =
minimax(
child,
Math.max(
0,
config.depth - 1,
),
-Infinity,
Infinity,
color,
).score;
ranked.push({
move,
score,
});
}
ranked.sort(
(a, b) =>
b.score - a.score,
);
if (!ranked.length) {
return moves[0];
}
if (
Math.random() <
config.randomness
) {
const count =
Math.min(
3,
ranked.length,
);
return ranked[
Math.floor(
Math.random() *
count,
)
].move;
}
return ranked[0].move;
}