AI Integration - RumenDamyanov/js-chess GitHub Wiki
Complete guide to implementing AI opponents and chess analysis features across all frameworks.
This guide covers:
- AI opponent implementation strategies
- Chess engine integration
- Move evaluation algorithms
- Difficulty levels and personality
- Analysis and hint systems
- Performance optimization for AI
class ChessAI {
constructor(difficulty = 'medium') {
this.difficulty = difficulty;
this.maxDepth = this.getDepthForDifficulty(difficulty);
this.evaluator = new PositionEvaluator();
this.transpositionTable = new Map();
this.killerMoves = [];
}
getDepthForDifficulty(difficulty) {
const depths = {
'beginner': 2,
'easy': 3,
'medium': 4,
'hard': 5,
'expert': 6,
'master': 7
};
return depths[difficulty] || 4;
}
async getBestMove(gameState) {
this.startTime = Date.now();
this.nodesSearched = 0;
const result = this.minimax(
gameState,
this.maxDepth,
-Infinity,
Infinity,
gameState.currentPlayer === 'white'
);
return result.move;
}
minimax(gameState, depth, alpha, beta, isMaximizing) {
this.nodesSearched++;
// Check time limit
if (Date.now() - this.startTime > 5000) {
return { evaluation: this.evaluator.evaluate(gameState), move: null };
}
// Terminal conditions
if (depth === 0 || gameState.gameStatus !== 'active') {
return {
evaluation: this.evaluator.evaluate(gameState),
move: null
};
}
// Check transposition table
const positionKey = this.getPositionKey(gameState);
if (this.transpositionTable.has(positionKey)) {
const entry = this.transpositionTable.get(positionKey);
if (entry.depth >= depth) {
return entry;
}
}
const moves = this.getAllLegalMoves(gameState);
if (moves.length === 0) {
const evaluation = gameState.gameStatus === 'checkmate'
? (isMaximizing ? -10000 : 10000)
: 0; // Stalemate
return { evaluation, move: null };
}
// Move ordering for better alpha-beta pruning
moves.sort((a, b) => this.scoreMoveForOrdering(b, gameState) -
this.scoreMoveForOrdering(a, gameState));
let bestMove = null;
let bestEvaluation = isMaximizing ? -Infinity : Infinity;
for (const move of moves) {
const newGameState = this.makeMove(gameState, move);
const result = this.minimax(newGameState, depth - 1, alpha, beta, !isMaximizing);
if (isMaximizing) {
if (result.evaluation > bestEvaluation) {
bestEvaluation = result.evaluation;
bestMove = move;
}
alpha = Math.max(alpha, result.evaluation);
} else {
if (result.evaluation < bestEvaluation) {
bestEvaluation = result.evaluation;
bestMove = move;
}
beta = Math.min(beta, result.evaluation);
}
// Alpha-beta pruning
if (beta <= alpha) {
break;
}
}
// Store in transposition table
const entry = { evaluation: bestEvaluation, move: bestMove, depth };
this.transpositionTable.set(positionKey, entry);
return { evaluation: bestEvaluation, move: bestMove };
}
}
class PositionEvaluator {
constructor() {
this.pieceValues = {
'P': 100, 'N': 320, 'B': 330, 'R': 500, 'Q': 900, 'K': 20000,
'p': -100, 'n': -320, 'b': -330, 'r': -500, 'q': -900, 'k': -20000
};
// Positional piece-square tables
this.pieceSquareTables = {
'P': this.createPawnTable(true),
'p': this.createPawnTable(false),
'N': this.createKnightTable(),
'n': this.createKnightTable(true),
// ... other pieces
};
}
evaluate(gameState) {
let evaluation = 0;
// Material evaluation
evaluation += this.evaluateMaterial(gameState.board);
// Positional evaluation
evaluation += this.evaluatePosition(gameState.board);
// King safety
evaluation += this.evaluateKingSafety(gameState);
// Pawn structure
evaluation += this.evaluatePawnStructure(gameState.board);
// Piece activity
evaluation += this.evaluatePieceActivity(gameState);
// Endgame considerations
if (this.isEndgame(gameState)) {
evaluation += this.evaluateEndgame(gameState);
}
return evaluation;
}
evaluateMaterial(board) {
let material = 0;
for (let row = 0; row < 8; row++) {
for (let col = 0; col < 8; col++) {
const piece = board[row][col];
if (piece) {
material += this.pieceValues[piece];
}
}
}
return material;
}
evaluatePosition(board) {
let positional = 0;
for (let row = 0; row < 8; row++) {
for (let col = 0; col < 8; col++) {
const piece = board[row][col];
if (piece && this.pieceSquareTables[piece]) {
positional += this.pieceSquareTables[piece][row][col];
}
}
}
return positional;
}
evaluateKingSafety(gameState) {
let safety = 0;
// Check if kings are castled
if (this.isKingCastled(gameState, 'white')) safety += 50;
if (this.isKingCastled(gameState, 'black')) safety -= 50;
// Pawn shield evaluation
safety += this.evaluatePawnShield(gameState, 'white');
safety -= this.evaluatePawnShield(gameState, 'black');
return safety;
}
createPawnTable(isWhite) {
const baseTable = [
[0, 0, 0, 0, 0, 0, 0, 0],
[50, 50, 50, 50, 50, 50, 50, 50],
[10, 10, 20, 30, 30, 20, 10, 10],
[5, 5, 10, 25, 25, 10, 5, 5],
[0, 0, 0, 20, 20, 0, 0, 0],
[5, -5,-10, 0, 0,-10, -5, 5],
[5, 10, 10,-20,-20, 10, 10, 5],
[0, 0, 0, 0, 0, 0, 0, 0]
];
if (isWhite) {
return baseTable.reverse();
} else {
return baseTable.map(row => row.map(val => -val));
}
}
}
class AdaptiveDifficulty {
constructor() {
this.playerSkillLevel = 1200; // Starting ELO-like rating
this.recentGames = [];
this.adaptionRate = 0.1;
}
adjustDifficulty(gameResult, movesPlayed) {
const gameData = {
result: gameResult, // 'win', 'loss', 'draw'
moves: movesPlayed,
timestamp: Date.now()
};
this.recentGames.push(gameData);
// Keep only last 10 games
if (this.recentGames.length > 10) {
this.recentGames.shift();
}
// Calculate win rate
const wins = this.recentGames.filter(g => g.result === 'win').length;
const winRate = wins / this.recentGames.length;
// Adjust skill level
if (winRate > 0.7) {
this.playerSkillLevel += 50 * this.adaptionRate;
} else if (winRate < 0.3) {
this.playerSkillLevel -= 50 * this.adaptionRate;
}
return this.getAIDifficultyForSkillLevel();
}
getAIDifficultyForSkillLevel() {
if (this.playerSkillLevel < 800) return 'beginner';
if (this.playerSkillLevel < 1000) return 'easy';
if (this.playerSkillLevel < 1400) return 'medium';
if (this.playerSkillLevel < 1800) return 'hard';
if (this.playerSkillLevel < 2200) return 'expert';
return 'master';
}
}
class AIPersonality {
static getPersonality(type) {
const personalities = {
'aggressive': {
name: 'The Attacker',
description: 'Loves tactical combinations and sacrifices',
evaluationBonus: {
attack: 1.5,
defense: 0.8,
development: 1.2
},
preferredOpenings: ['Kings Indian Attack', 'Sicilian Dragon'],
moveTimeVariation: 0.2
},
'positional': {
name: 'The Strategist',
description: 'Focuses on long-term planning and structure',
evaluationBonus: {
pawnStructure: 1.4,
pieceActivity: 1.3,
kingSafety: 1.2
},
preferredOpenings: ['Queens Gambit', 'English Opening'],
moveTimeVariation: 0.1
},
'defensive': {
name: 'The Fortress',
description: 'Solid play with emphasis on safety',
evaluationBonus: {
defense: 1.6,
kingSafety: 1.5,
pawnStructure: 1.3
},
preferredOpenings: ['French Defense', 'Caro-Kann'],
moveTimeVariation: 0.15
},
'tactical': {
name: 'The Calculator',
description: 'Excels at finding tactical shots',
searchDepthBonus: 1,
tacticalBonus: 1.8,
evaluationBonus: {
tactics: 2.0,
combinations: 1.7
},
moveTimeVariation: 0.3
}
};
return personalities[type] || personalities['positional'];
}
static applyPersonality(ai, personality) {
ai.personality = personality;
// Modify evaluation function
const originalEvaluate = ai.evaluator.evaluate.bind(ai.evaluator);
ai.evaluator.evaluate = (gameState) => {
let evaluation = originalEvaluate(gameState);
// Apply personality bonuses
if (personality.evaluationBonus) {
Object.keys(personality.evaluationBonus).forEach(factor => {
const bonus = ai.evaluator[`evaluate${factor.charAt(0).toUpperCase() + factor.slice(1)}`];
if (bonus) {
evaluation += bonus(gameState) * (personality.evaluationBonus[factor] - 1);
}
});
}
return evaluation;
};
// Modify search depth
if (personality.searchDepthBonus) {
ai.maxDepth += personality.searchDepthBonus;
}
return ai;
}
}
class OpeningBook {
constructor() {
this.openings = new Map();
this.loadOpeningDatabase();
}
loadOpeningDatabase() {
// Popular opening moves with their evaluations
const openingMoves = {
// Starting position
'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq -': [
{ move: 'e2e4', weight: 100, name: 'Kings Pawn' },
{ move: 'd2d4', weight: 95, name: 'Queens Pawn' },
{ move: 'g1f3', weight: 85, name: 'Reti Opening' },
{ move: 'c2c4', weight: 80, name: 'English Opening' }
],
// After 1.e4
'rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3': [
{ move: 'e7e5', weight: 100, name: 'Kings Pawn Game' },
{ move: 'c7c5', weight: 95, name: 'Sicilian Defense' },
{ move: 'e7e6', weight: 85, name: 'French Defense' },
{ move: 'c7c6', weight: 80, name: 'Caro-Kann Defense' }
],
// Add more opening positions...
};
Object.entries(openingMoves).forEach(([fen, moves]) => {
this.openings.set(fen, moves);
});
}
getOpeningMove(gameState, personality = null) {
const fen = this.gameStateToFEN(gameState);
const openingMoves = this.openings.get(fen);
if (!openingMoves) return null;
// Filter moves based on personality preferences
let availableMoves = openingMoves;
if (personality && personality.preferredOpenings) {
availableMoves = openingMoves.filter(move =>
personality.preferredOpenings.some(opening =>
move.name.includes(opening)
)
);
// Fall back to all moves if no preferred moves found
if (availableMoves.length === 0) {
availableMoves = openingMoves;
}
}
// Weighted random selection
const totalWeight = availableMoves.reduce((sum, move) => sum + move.weight, 0);
let random = Math.random() * totalWeight;
for (const move of availableMoves) {
random -= move.weight;
if (random <= 0) {
return move.move;
}
}
return availableMoves[0].move;
}
}
class EndgameTablebase {
constructor() {
this.cache = new Map();
this.supportedEndgames = [
'KQK', 'KRK', 'KBBK', 'KBNK', 'KNNK',
'KQKQ', 'KRKR', 'KQKR'
];
}
async queryTablebase(gameState) {
const pieces = this.countPieces(gameState.board);
const totalPieces = Object.values(pieces).reduce((a, b) => a + b, 0);
// Only use tablebase for endgames with <= 6 pieces
if (totalPieces > 6) return null;
const endgameKey = this.getEndgameKey(pieces);
if (!this.supportedEndgames.includes(endgameKey)) return null;
const fen = this.gameStateToFEN(gameState);
// Check cache first
if (this.cache.has(fen)) {
return this.cache.get(fen);
}
try {
// Query online tablebase (Syzygy format)
const response = await fetch(`https://tablebase.lichess.ovh/standard?fen=${encodeURIComponent(fen)}`);
const data = await response.json();
const result = {
evaluation: this.convertTablebaseEval(data),
bestMove: data.moves && data.moves[0] ? data.moves[0].uci : null,
dtm: data.dtm, // Distance to mate
dtz: data.dtz // Distance to zero (50-move rule)
};
this.cache.set(fen, result);
return result;
} catch (error) {
console.warn('Tablebase query failed:', error);
return null;
}
}
convertTablebaseEval(data) {
if (data.category === 'win') {
return 10000 - data.dtm;
} else if (data.category === 'loss') {
return -10000 + data.dtm;
} else {
return 0; // Draw
}
}
}
class AIThinkingSimulator {
constructor() {
this.thinkingPhrases = [
"Analyzing position...",
"Calculating variations...",
"Evaluating threats...",
"Considering sacrifices...",
"Looking for tactics...",
"Planning strategy...",
"Checking defenses...",
"Computing best move..."
];
}
async simulateThinking(depth, onThinkingUpdate) {
const baseTime = 1000; // Minimum thinking time
const depthTime = depth * 500; // Additional time per depth
const totalTime = baseTime + depthTime + Math.random() * 1000;
const phraseInterval = totalTime / 4;
let currentPhrase = 0;
const thinkingInterval = setInterval(() => {
if (currentPhrase < this.thinkingPhrases.length) {
onThinkingUpdate({
phrase: this.thinkingPhrases[currentPhrase],
progress: (currentPhrase + 1) / this.thinkingPhrases.length
});
currentPhrase++;
}
}, phraseInterval);
return new Promise(resolve => {
setTimeout(() => {
clearInterval(thinkingInterval);
onThinkingUpdate({ phrase: "Move ready!", progress: 1 });
resolve();
}, totalTime);
});
}
}
class MoveAnimator {
constructor(boardElement) {
this.boardElement = boardElement;
this.animationSpeed = 300; // ms
}
async animateAIMove(from, to, piece) {
const fromElement = this.getSquareElement(from);
const toElement = this.getSquareElement(to);
const pieceElement = fromElement.querySelector('.piece');
if (!pieceElement) return;
// Create moving piece element
const movingPiece = pieceElement.cloneNode(true);
movingPiece.classList.add('moving-piece');
// Calculate movement
const fromRect = fromElement.getBoundingClientRect();
const toRect = toElement.getBoundingClientRect();
const deltaX = toRect.left - fromRect.left;
const deltaY = toRect.top - fromRect.top;
// Start animation
this.boardElement.appendChild(movingPiece);
pieceElement.style.opacity = '0';
return new Promise(resolve => {
// Apply transform
movingPiece.style.transform = `translate(${deltaX}px, ${deltaY}px)`;
movingPiece.style.transition = `transform ${this.animationSpeed}ms ease-in-out`;
setTimeout(() => {
// Complete the move
toElement.appendChild(pieceElement);
pieceElement.style.opacity = '1';
movingPiece.remove();
fromElement.innerHTML = '';
resolve();
}, this.animationSpeed);
});
}
getSquareElement(square) {
return this.boardElement.querySelector(`[data-square="${square}"]`);
}
}
@Injectable({
providedIn: 'root'
})
export class ChessAIService {
private ai: ChessAI;
private openingBook: OpeningBook;
private tablebase: EndgameTablebase;
private thinking$ = new BehaviorSubject<string>('');
constructor() {
this.ai = new ChessAI('medium');
this.openingBook = new OpeningBook();
this.tablebase = new EndgameTablebase();
}
async getAIMove(gameState: GameState): Promise<string> {
this.thinking$.next('AI is thinking...');
try {
// Try opening book first
const openingMove = this.openingBook.getOpeningMove(gameState);
if (openingMove) {
this.thinking$.next('');
return openingMove;
}
// Try tablebase for endgames
const tablebaseResult = await this.tablebase.queryTablebase(gameState);
if (tablebaseResult && tablebaseResult.bestMove) {
this.thinking$.next('');
return tablebaseResult.bestMove;
}
// Use main AI engine
const bestMove = await this.ai.getBestMove(gameState);
this.thinking$.next('');
return bestMove;
} catch (error) {
this.thinking$.next('');
throw error;
}
}
getThinkingStatus(): Observable<string> {
return this.thinking$.asObservable();
}
setDifficulty(difficulty: string): void {
this.ai = new ChessAI(difficulty);
}
}
function useChessAI(difficulty = 'medium') {
const [ai] = useState(() => new ChessAI(difficulty));
const [isThinking, setIsThinking] = useState(false);
const [thinkingPhrase, setThinkingPhrase] = useState('');
const openingBookRef = useRef(new OpeningBook());
const tablebaseRef = useRef(new EndgameTablebase());
const getAIMove = useCallback(async (gameState) => {
setIsThinking(true);
try {
// Simulate thinking animation
const simulator = new AIThinkingSimulator();
const thinkingPromise = simulator.simulateThinking(
ai.maxDepth,
({ phrase, progress }) => {
setThinkingPhrase(phrase);
}
);
// Get the actual move
const movePromise = (async () => {
const openingMove = openingBookRef.current.getOpeningMove(gameState);
if (openingMove) return openingMove;
const tablebaseResult = await tablebaseRef.current.queryTablebase(gameState);
if (tablebaseResult?.bestMove) return tablebaseResult.bestMove;
return await ai.getBestMove(gameState);
})();
// Wait for both thinking animation and move calculation
const [move] = await Promise.all([movePromise, thinkingPromise]);
return move;
} finally {
setIsThinking(false);
setThinkingPhrase('');
}
}, [ai]);
const changeDifficulty = useCallback((newDifficulty) => {
ai.difficulty = newDifficulty;
ai.maxDepth = ai.getDepthForDifficulty(newDifficulty);
}, [ai]);
return {
getAIMove,
isThinking,
thinkingPhrase,
changeDifficulty
};
}
import { ref, computed } from 'vue';
export function useChessAI(difficulty = 'medium') {
const ai = ref(new ChessAI(difficulty));
const isThinking = ref(false);
const thinkingPhrase = ref('');
const openingBook = new OpeningBook();
const tablebase = new EndgameTablebase();
const getAIMove = async (gameState) => {
isThinking.value = true;
try {
const simulator = new AIThinkingSimulator();
const thinkingPromise = simulator.simulateThinking(
ai.value.maxDepth,
({ phrase }) => {
thinkingPhrase.value = phrase;
}
);
const movePromise = (async () => {
const openingMove = openingBook.getOpeningMove(gameState);
if (openingMove) return openingMove;
const tablebaseResult = await tablebase.queryTablebase(gameState);
if (tablebaseResult?.bestMove) return tablebaseResult.bestMove;
return await ai.value.getBestMove(gameState);
})();
const [move] = await Promise.all([movePromise, thinkingPromise]);
return move;
} finally {
isThinking.value = false;
thinkingPhrase.value = '';
}
};
const changeDifficulty = (newDifficulty) => {
ai.value = new ChessAI(newDifficulty);
};
return {
ai: computed(() => ai.value),
isThinking: computed(() => isThinking.value),
thinkingPhrase: computed(() => thinkingPhrase.value),
getAIMove,
changeDifficulty
};
}
class MoveAnalyzer {
constructor() {
this.ai = new ChessAI('expert');
}
async analyzePosition(gameState) {
const analysis = {
bestMove: null,
evaluation: 0,
principalVariation: [],
threats: [],
weaknesses: [],
suggestions: []
};
// Get best move and evaluation
const result = await this.ai.getBestMove(gameState);
analysis.bestMove = result.move;
analysis.evaluation = result.evaluation;
// Calculate principal variation
analysis.principalVariation = await this.calculatePV(gameState, 5);
// Identify threats
analysis.threats = this.identifyThreats(gameState);
// Find positional weaknesses
analysis.weaknesses = this.findWeaknesses(gameState);
// Generate suggestions
analysis.suggestions = this.generateSuggestions(gameState, analysis);
return analysis;
}
async getHint(gameState, playerLevel = 'intermediate') {
const analysis = await this.analyzePosition(gameState);
const hints = {
beginner: this.generateBeginnerHint(gameState, analysis),
intermediate: this.generateIntermediateHint(gameState, analysis),
advanced: this.generateAdvancedHint(gameState, analysis)
};
return hints[playerLevel] || hints.intermediate;
}
generateBeginnerHint(gameState, analysis) {
// Simple tactical hints
if (analysis.threats.length > 0) {
const threat = analysis.threats[0];
return {
type: 'threat',
message: `Watch out! Your ${threat.piece} on ${threat.square} is being attacked.`,
square: threat.square,
severity: 'high'
};
}
// Basic development hints
if (gameState.moves.length < 10) {
const undevelopedPieces = this.findUndevelopedPieces(gameState);
if (undevelopedPieces.length > 0) {
return {
type: 'development',
message: `Try developing your ${undevelopedPieces[0].piece}.`,
square: undevelopedPieces[0].square,
severity: 'medium'
};
}
}
return {
type: 'general',
message: 'Look for safe moves that improve your position.',
severity: 'low'
};
}
}
// ai-worker.js
class AIWorker {
constructor() {
this.ai = new ChessAI();
self.onmessage = (event) => {
const { type, data } = event.data;
switch (type) {
case 'getBestMove':
this.handleGetBestMove(data);
break;
case 'analyzePosition':
this.handleAnalyzePosition(data);
break;
case 'setDifficulty':
this.ai.difficulty = data.difficulty;
this.ai.maxDepth = this.ai.getDepthForDifficulty(data.difficulty);
break;
}
};
}
async handleGetBestMove(gameState) {
try {
const move = await this.ai.getBestMove(gameState);
self.postMessage({
type: 'bestMoveResult',
data: { move, success: true }
});
} catch (error) {
self.postMessage({
type: 'bestMoveResult',
data: { error: error.message, success: false }
});
}
}
}
new AIWorker();
class AIManager {
constructor() {
this.worker = new Worker('/ai-worker.js');
this.pendingRequests = new Map();
this.requestId = 0;
this.worker.onmessage = (event) => {
const { type, data, requestId } = event.data;
if (this.pendingRequests.has(requestId)) {
const { resolve, reject } = this.pendingRequests.get(requestId);
this.pendingRequests.delete(requestId);
if (data.success) {
resolve(data);
} else {
reject(new Error(data.error));
}
}
};
}
async getBestMove(gameState) {
return new Promise((resolve, reject) => {
const requestId = ++this.requestId;
this.pendingRequests.set(requestId, { resolve, reject });
this.worker.postMessage({
type: 'getBestMove',
data: gameState,
requestId
});
});
}
setDifficulty(difficulty) {
this.worker.postMessage({
type: 'setDifficulty',
data: { difficulty }
});
}
}
describe('Chess AI', () => {
let ai;
beforeEach(() => {
ai = new ChessAI('medium');
});
test('finds mate in one', async () => {
const mateInOnePosition = createMateInOnePosition();
const bestMove = await ai.getBestMove(mateInOnePosition);
// Verify the move leads to checkmate
const newGameState = makeMove(mateInOnePosition, bestMove);
expect(newGameState.gameStatus).toBe('checkmate');
});
test('avoids blunders', async () => {
const blunderPosition = createBlunderTestPosition();
const bestMove = await ai.getBestMove(blunderPosition);
// Verify AI doesn't hang pieces
const evaluation = ai.evaluator.evaluate(
makeMove(blunderPosition, bestMove)
);
expect(evaluation).toBeGreaterThan(-200); // Not losing significant material
});
test('respects difficulty settings', async () => {
const easyAI = new ChessAI('easy');
const hardAI = new ChessAI('hard');
expect(easyAI.maxDepth).toBeLessThan(hardAI.maxDepth);
});
});
- Game Logic - Understanding the underlying chess rules
- WebSocket Guide - Real-time multiplayer with AI
- Performance - Optimizing AI performance
- Testing Guide - Testing AI functionality