import React, { useState, useEffect, useCallback } from 'react'; const TicTacToe = () => { // Game state const [board, setBoard] = useState(Array(9).fill(null)); const [currentPlayer, setCurrentPlayer] = useState('X'); const [gameStatus, setGameStatus] = useState('playing'); // 'playing', 'won', 'draw' const [winner, setWinner] = useState(null); const [winningLine, setWinningLine] = useState(null); // New state for AI features const [playAgainstAI, setPlayAgainstAI] = useState(true); const [aiDifficulty, setAiDifficulty] = useState('medium'); // 'easy', 'medium', 'hard' const [dynamicDifficulty, setDynamicDifficulty] = useState(false); const [gameHistory, setGameHistory] = useState([]); const [playerStats, setPlayerStats] = useState(() => { const savedStats = localStorage.getItem('ticTacToePlayerStats'); return savedStats ? JSON.parse(savedStats) : { wins: 0, losses: 0, draws: 0 }; }); // Q-learning model state const [qTable, setQTable] = useState(() => { const savedQTable = localStorage.getItem('ticTacToeQTable'); return savedQTable ? JSON.parse(savedQTable) : {}; }); // Add new state for AI move visualization const [aiConsideredMoves, setAiConsideredMoves] = useState([]); const [showingAiMoves, setShowingAiMoves] = useState(false); // Win conditions - all possible winning combinations const winConditions = [ [0, 1, 2], [3, 4, 5], [6, 7, 8], // Rows [0, 3, 6], [1, 4, 7], [2, 5, 8], // Columns [0, 4, 8], [2, 4, 6] // Diagonals ]; // Check for win condition const checkWinner = (currentBoard) => { for (let i = 0; i < winConditions.length; i++) { const [a, b, c] = winConditions[i]; if (currentBoard[a] && currentBoard[a] === currentBoard[b] && currentBoard[a] === currentBoard[c]) { return { winner: currentBoard[a], line: winConditions[i] }; } } return null; }; // Check for draw condition const checkDraw = (currentBoard) => { return currentBoard.every(cell => cell !== null); }; // Save Q-table to localStorage whenever it changes useEffect(() => { localStorage.setItem('ticTacToeQTable', JSON.stringify(qTable)); }, [qTable]); // Save player stats to localStorage whenever they change useEffect(() => { localStorage.setItem('ticTacToePlayerStats', JSON.stringify(playerStats)); }, [playerStats]); // AI move handler - called after player makes a move useEffect(() => { if (playAgainstAI && currentPlayer === 'O' && gameStatus === 'playing') { // Small delay to make AI move feel more natural const aiMoveTimeout = setTimeout(() => { makeAIMove(); }, 600); return () => clearTimeout(aiMoveTimeout); } }, [currentPlayer, gameStatus, playAgainstAI, makeAIMove]); // Minimax algorithm with alpha-beta pruning const minimax = useCallback((board, depth, isMaximizing, alpha = -Infinity, beta = Infinity) => { // Check for terminal states const winResult = checkWinner(board); if (winResult) { return winResult.winner === 'O' ? 10 - depth : depth - 10; } if (checkDraw(board)) { return 0; } // Depth limit for different difficulty levels const maxDepth = { easy: 1, medium: 3, hard: 9 }[aiDifficulty]; if (depth >= maxDepth) { return 0; } if (isMaximizing) { let bestScore = -Infinity; for (let i = 0; i < board.length; i++) { if (!board[i]) { const newBoard = [...board]; newBoard[i] = 'O'; const score = minimax(newBoard, depth + 1, false, alpha, beta); bestScore = Math.max(score, bestScore); alpha = Math.max(alpha, bestScore); if (beta <= alpha) break; } } return bestScore; } else { let bestScore = Infinity; for (let i = 0; i < board.length; i++) { if (!board[i]) { const newBoard = [...board]; newBoard[i] = 'X'; const score = minimax(newBoard, depth + 1, true, alpha, beta); bestScore = Math.min(score, bestScore); beta = Math.min(beta, bestScore); if (beta <= alpha) break; } } return bestScore; } }, [aiDifficulty]); // Get Q-value for a given state-action pair const getQValue = useCallback((boardState, action) => { const stateKey = boardState.join(''); if (qTable[stateKey] && qTable[stateKey][action] !== undefined) { return qTable[stateKey][action]; } return 0; }, [qTable]); // Update Q-value for a state-action pair const updateQValue = useCallback((boardState, action, reward, nextBoardState) => { const stateKey = boardState.join(''); const nextStateKey = nextBoardState.join(''); // Q-learning parameters const learningRate = 0.1; const discountFactor = 0.9; // Initialize state in Q-table if it doesn't exist if (!qTable[stateKey]) { qTable[stateKey] = {}; } // Initialize action in state if it doesn't exist if (qTable[stateKey][action] === undefined) { qTable[stateKey][action] = 0; } // Get maximum Q-value for next state let maxNextQ = -Infinity; if (qTable[nextStateKey]) { maxNextQ = Math.max(...Object.values(qTable[nextStateKey]), 0); } else { maxNextQ = 0; } // Q-learning update formula: Q(s,a) = Q(s,a) + α * [r + γ * max(Q(s',a')) - Q(s,a)] const newQValue = qTable[stateKey][action] + learningRate * (reward + discountFactor * maxNextQ - qTable[stateKey][action]); // Update Q-table setQTable(prevQTable => { const newQTable = { ...prevQTable }; if (!newQTable[stateKey]) newQTable[stateKey] = {}; newQTable[stateKey][action] = newQValue; return newQTable; }); }, [qTable]); // Make AI move using minimax and Q-learning const makeAIMove = useCallback(() => { // Clone current board const currentBoard = [...board]; let bestScore = -Infinity; let bestMoves = []; let allMoveScores = []; // Exploration rate (epsilon) - decreases as AI learns const epsilon = { easy: 0.4, medium: 0.2, hard: 0.05 }[aiDifficulty]; // Random move (exploration) based on epsilon if (Math.random() < epsilon) { const emptyIndices = currentBoard.map((cell, index) => cell === null ? index : null).filter(index => index !== null); if (emptyIndices.length > 0) { const randomIndex = emptyIndices[Math.floor(Math.random() * emptyIndices.length)]; // Show that this is a random exploratory move setAiConsideredMoves([{ index: randomIndex, minimaxScore: 'N/A', qValue: 'N/A', combinedScore: 'Random', isSelected: true }]); setShowingAiMoves(true); // Delay the actual move setTimeout(() => { makeMove(randomIndex); setShowingAiMoves(false); setAiConsideredMoves([]); }, 1000); return; } } // Find best move using minimax and Q-learning for (let i = 0; i < currentBoard.length; i++) { if (!currentBoard[i]) { const newBoard = [...currentBoard]; newBoard[i] = 'O'; // Combine minimax score with Q-value const minimaxScore = minimax(newBoard, 0, false); const qValue = getQValue(currentBoard, i); const combinedScore = minimaxScore + qValue; // Store all move scores for visualization allMoveScores.push({ index: i, minimaxScore, qValue, combinedScore }); if (combinedScore > bestScore) { bestScore = combinedScore; bestMoves = [i]; } else if (combinedScore === bestScore) { bestMoves.push(i); } } } // Sort all moves by score (descending) allMoveScores.sort((a, b) => b.combinedScore - a.combinedScore); // Take top 3 moves (or fewer if less are available) const topMoves = allMoveScores.slice(0, 3).map(move => ({ ...move, isSelected: bestMoves.includes(move.index) })); // Set the considered moves for visualization setAiConsideredMoves(topMoves); setShowingAiMoves(true); // Choose randomly among best moves if (bestMoves.length > 0) { const moveIndex = bestMoves[Math.floor(Math.random() * bestMoves.length)]; // Delay the actual move to allow visualization setTimeout(() => { makeMove(moveIndex); setShowingAiMoves(false); setAiConsideredMoves([]); // Record move for history const historyEntry = { board: [...currentBoard], move: moveIndex, player: 'O' }; setGameHistory(prev => [...prev, historyEntry]); }, 1500); } }, [board, minimax, getQValue, aiDifficulty]); // Make a move (used by both player and AI) const makeMove = (index) => { // Prevent moves if game is over or cell is already filled if (gameStatus !== 'playing' || board[index]) { return; } // Create new board with the move const newBoard = [...board]; newBoard[index] = currentPlayer; setBoard(newBoard); // Record move for history if it's player's move if (currentPlayer === 'X') { const historyEntry = { board: [...board], move: index, player: 'X' }; setGameHistory(prev => [...prev, historyEntry]); } // Check for winner const winResult = checkWinner(newBoard); if (winResult) { setGameStatus('won'); setWinner(winResult.winner); setWinningLine(winResult.line); // Update player stats if (winResult.winner === 'X') { setPlayerStats(prev => ({ ...prev, wins: prev.wins + 1 })); // Update Q-values with positive reward for AI losing (to learn from mistakes) updateGameHistory(-1); } else { setPlayerStats(prev => ({ ...prev, losses: prev.losses + 1 })); // Update Q-values with positive reward for AI winning updateGameHistory(1); } // Adjust difficulty if dynamic difficulty is enabled if (dynamicDifficulty) { adjustDynamicDifficulty(); } return; } // Check for draw if (checkDraw(newBoard)) { setGameStatus('draw'); setPlayerStats(prev => ({ ...prev, draws: prev.draws + 1 })); // Update Q-values with small positive reward for draw updateGameHistory(0.5); return; } // Switch players setCurrentPlayer(currentPlayer === 'X' ? 'O' : 'X'); }; // Update Q-values based on game history and outcome const updateGameHistory = (finalReward) => { // Skip if no history or not playing against AI if (gameHistory.length === 0 || !playAgainstAI) return; // Process each AI move in the history const aiMoves = gameHistory.filter(entry => entry.player === 'O'); aiMoves.forEach((historyEntry, index) => { const { board: boardState, move } = historyEntry; // Get next state after this move let nextBoardState; if (index < aiMoves.length - 1) { // If not the last move, use the board state after the next player move const nextPlayerMove = gameHistory.find(entry => entry.player === 'X' && gameHistory.indexOf(entry) > gameHistory.indexOf(historyEntry) ); nextBoardState = nextPlayerMove ? nextPlayerMove.board : [...boardState]; nextBoardState[move] = 'O'; // Apply the AI move } else { // If last move, use final board state nextBoardState = [...board]; } // Calculate reward - higher reward for moves closer to the end const moveReward = index === aiMoves.length - 1 ? finalReward : finalReward * 0.5 / (aiMoves.length - index); // Update Q-value for this state-action pair updateQValue(boardState, move, moveReward, nextBoardState); }); // Clear history after processing setGameHistory([]); }; // Adjust difficulty based on player performance const adjustDynamicDifficulty = () => { const { wins, losses } = playerStats; const totalGames = wins + losses; if (totalGames >= 5) { const winRate = wins / totalGames; if (winRate > 0.7 && aiDifficulty !== 'hard') { // Player is doing well, increase difficulty setAiDifficulty('hard'); } else if (winRate < 0.3 && aiDifficulty !== 'easy') { // Player is struggling, decrease difficulty setAiDifficulty('easy'); } else if (winRate >= 0.3 && winRate <= 0.7 && aiDifficulty !== 'medium') { // Player is doing average, set medium difficulty setAiDifficulty('medium'); } } }; // Handle cell click (player move) const handleCellClick = (index) => { // Only allow player X moves (human player) if (currentPlayer === 'X') { makeMove(index); } }; // Reset game const resetGame = () => { setBoard(Array(9).fill(null)); setCurrentPlayer('X'); setGameStatus('playing'); setWinner(null); setWinningLine(null); setGameHistory([]); }; // Get game status message const getStatusMessage = () => { if (gameStatus === 'won') { return `Player ${winner} Wins!`; } else if (gameStatus === 'draw') { return "It's a Draw!"; } else { return playAgainstAI ? (currentPlayer === 'X' ? "Your Turn" : "AI's Turn") : `Player ${currentPlayer}'s Turn`; } }; // Get status message color const getStatusColor = () => { if (gameStatus === 'won') { return winner === 'X' ? 'text-blue-800' : 'text-red-600'; } return 'text-gray-700'; }; return (
{playAgainstAI ? "Play against the AI! The AI learns from your moves and gets smarter over time." : "Get three in a row, column, or diagonal to win!"}