AI Integration - RumenDamyanov/js-chess GitHub Wiki

AI Integration

Complete guide to implementing AI opponents and chess analysis features across all frameworks.

Overview

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

AI Architecture

Core AI Components

Move Evaluation Engine

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 };
  }
}

Position Evaluation

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));
    }
  }
}

AI Difficulty Levels

Adaptive Difficulty System

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';
  }
}

AI Personalities

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;
  }
}

Opening Book Integration

Opening Database

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;
  }
}

Endgame Tablebase Integration

Basic Tablebase Support

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
    }
  }
}

AI Move Animation and Thinking

AI Thinking Simulation

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);
    });
  }
}

Move Animation System

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}"]`);
  }
}

Framework Integration

Angular AI Service

@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);
  }
}

React AI Hook

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
  };
}

Vue AI Composable

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
  };
}

Analysis and Hints

Move Analysis System

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'
    };
  }
}

Performance Optimization

Web Workers for AI

// 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();

AI Manager with Web Workers

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 }
    });
  }
}

Testing AI Components

AI Unit Tests

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);
  });
});

Next Steps

⚠️ **GitHub.com Fallback** ⚠️