Skip to content

AI.XO #13

@Lelo5356

Description

@Lelo5356
<title>Tic-Tac-Toe vs. Learning AI</title> <script src="https://cdn.tailwindcss.com"></script> <style> /* Custom styles for a better look and feel */ body { font-family: 'Inter', sans-serif; background-color: #1a202c; /* Dark gray/black background */ } /* Style for the game cells */ .cell { width: 100px; height: 100px; display: flex; justify-content: center; align-items: center; font-size: 3rem; font-weight: 900; /* Bolder font for X and O */ cursor: pointer; transition: all 0.2s ease-in-out; background-color: #2d3748; /* Darker gray for cells */ color: #fbd38d; /* Yellow-ish text */ } /* Hover effect for cells to give them a lift */ .cell:hover { transform: translateY(-2px); box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); } /* Mobile-first approach for responsive design */ @media (max-width: 640px) { .cell { width: 80px; height: 80px; font-size: 2.5rem; } } </style>

Tic-Tac-Toe vs. Learning AI

    <!-- The main game container -->
    <div id="game-container">
        <div id="status" class="text-center text-2xl font-semibold mb-6 h-8 text-yellow-400">It's your turn (X).</div>

        <!-- The game board using CSS Grid for a 3x3 layout -->
        <div id="board" class="grid grid-cols-3 gap-2 bg-yellow-400 p-2 rounded-xl">
            <!-- Each div represents a single cell on the board -->
            <div class="cell rounded-xl transition-all duration-200" data-cell-index="0"></div>
            <div class="cell rounded-xl transition-all duration-200" data-cell-index="1"></div>
            <div class="cell rounded-xl transition-all duration-200" data-cell-index="2"></div>
            <div class="cell rounded-xl transition-all duration-200" data-cell-index="3"></div>
            <div class="cell rounded-xl transition-all duration-200" data-cell-index="4"></div>
            <div class="cell rounded-xl transition-all duration-200" data-cell-index="5"></div>
            <div class="cell rounded-xl transition-all duration-200" data-cell-index="6"></div>
            <div class="cell rounded-xl transition-all duration-200" data-cell-index="7"></div>
            <div class="cell rounded-xl transition-all duration-200" data-cell-index="8"></div>
        </div>

        <!-- Game control button -->
        <div class="mt-6 text-center">
            <button id="restart-button" class="bg-yellow-400 text-black px-6 py-3 rounded-xl shadow-lg hover:bg-yellow-500 hover:shadow-xl transition-colors focus:outline-none">
                Restart Game
            </button>
        </div>
        <div id="ai-info" class="mt-4 text-center text-yellow-400 text-sm">
            AI's Memory: <span id="q-table-size">0</span> states
        </div>
    </div>
</div>

<script>
    // --- Game State Variables and DOM Elements ---
    let board = ['', '', '', '', '', '', '', '', ''];
    let currentPlayer = 'X';
    let isGameActive = true;
    // Game history to store AI's moves for learning.
    let gameHistory = [];

    // DOM Elements
    const statusDisplay = document.getElementById('status');
    const cells = document.querySelectorAll('.cell');
    const restartButton = document.getElementById('restart-button');
    const qTableSizeDisplay = document.getElementById('q-table-size');

    // Define all possible winning conditions for Tic-Tac-Toe
    const winningConditions = [
        [0, 1, 2], [3, 4, 5], [6, 7, 8],
        [0, 3, 6], [1, 4, 7], [2, 5, 8],
        [0, 4, 8], [2, 4, 6]
    ];

    // --- Q-learning AI Parameters ---
    // The Q-table is an object mapping board states to actions and their Q-values.
    // It acts as the AI's "memory."
    let qTable = {};
    const Q_TABLE_KEY = "tictactoe_q_table";

    // Hyperparameters for the Q-learning algorithm
    const LEARNING_RATE = 0.1;  // How much new information updates old knowledge.
    const DISCOUNT_FACTOR = 0.9; // How much future rewards are valued.
    let EPSILON = 0.2;          // The probability of the AI making a random (exploratory) move.
    const EPSILON_DECAY = 0.9999; // Rate at which epsilon decreases over time.

    // --- Q-learning Helper Functions ---

    /**
     * Converts the board array into a unique string to be used as a key in the Q-table.
     * @param {Array<string>} currentBoard The current state of the board.
     * @returns {string} A string representation of the board state.
     */
    const getBoardStateKey = (currentBoard) => {
        return currentBoard.join('');
    };

    /**
     * Saves the current Q-table to the browser's local storage.
     */
    const saveQTable = () => {
        try {
            localStorage.setItem(Q_TABLE_KEY, JSON.stringify(qTable));
            console.log("Q-table saved.");
        } catch (e) {
            console.error("Error saving Q-table to local storage:", e);
        }
    };

    /**
     * Loads the Q-table from local storage, if it exists.
     * Otherwise, it starts with an empty Q-table.
     */
    const loadQTable = () => {
        try {
            const storedQTable = localStorage.getItem(Q_TABLE_KEY);
            if (storedQTable) {
                qTable = JSON.parse(storedQTable);
                console.log("Q-table loaded.");
            } else {
                console.log("No existing Q-table found. Starting from scratch.");
            }
        } catch (e) {
            console.error("Error loading Q-table from local storage:", e);
            qTable = {};
        }
        // Update the display with the current size of the Q-table.
        qTableSizeDisplay.textContent = Object.keys(qTable).length;
    };

    /**
     * Updates the Q-table after a game has finished based on the final reward.
     * The reward is back-propagated through the AI's game history.
     * @param {Array<Object>} history An array of objects containing the state and action for each of the AI's moves.
     * @param {number} reward The final reward for the AI (1 for win, 0.5 for draw, -1 for loss).
     */
    const learn = (history, reward) => {
        if (history.length === 0) return;

        // Update the Q-value for the last move first.
        const { state, action } = history[history.length - 1];
        const oldQ = (qTable[state] && qTable[state][action]) || 0;
        // The last state has no future reward, so maxNextQ is 0.
        qTable[state] = qTable[state] || {};
        qTable[state][action] = oldQ + LEARNING_RATE * (reward - oldQ);

        // Back-propagate rewards for all previous moves.
        for (let i = history.length - 2; i >= 0; i--) {
            const { state: currentState, action: currentAction } = history[i];
            const { state: nextState } = history[i + 1];

            const oldQ = (qTable[currentState] && qTable[currentState][currentAction]) || 0;
            
            // Find the maximum Q-value for the next state, representing the best possible future reward.
            const maxNextQ = qTable[nextState] ? Math.max(...Object.values(qTable[nextState])) : 0;
            
            // Q-learning update formula.
            const newQ = oldQ + LEARNING_RATE * (0 + DISCOUNT_FACTOR * maxNextQ - oldQ);
            
            qTable[currentState] = qTable[currentState] || {};
            qTable[currentState][currentAction] = newQ;
        }
    };

    /**
     * Chooses the AI's next move using an epsilon-greedy strategy.
     * @returns {number} The index of the chosen move.
     */
    const chooseAIMove = () => {
        const stateKey = getBoardStateKey(board);
        const emptyCells = getEmptyCells(board);
        
        // Epsilon-greedy strategy: with probability epsilon, explore; otherwise, exploit.
        if (Math.random() < EPSILON) {
            // Explore: choose a random move.
            return emptyCells[Math.floor(Math.random() * emptyCells.length)];
        } else {
            // Exploit: choose the move with the highest Q-value.
            let bestMove = -1;
            let maxQValue = -Infinity;

            if (qTable[stateKey]) {
                // Check all available moves and find the one with the highest Q-value.
                for (const move of emptyCells) {
                    const qValue = qTable[stateKey][move] || 0;
                    if (qValue > maxQValue) {
                        maxQValue = qValue;
                        bestMove = move;
                    }
                }
            }
            
            // If no good move is found (no Q-values or all are 0), pick a random move.
            if (bestMove === -1) {
                return emptyCells[Math.floor(Math.random() * emptyCells.length)];
            }

            return bestMove;
        }
    };

    // --- Game Logic Functions ---

    /**
     * Updates the status message displayed to the user.
     * @param {string} message The message to display.
     */
    const updateStatus = (message) => {
        statusDisplay.textContent = message;
    };

    /**
     * Checks if a specific player has won the game.
     * @param {Array<string>} currentBoard The current state of the board.
     * @param {string} player The player's symbol ('X' or 'O').
     * @returns {boolean} True if the player has won, otherwise False.
     */
    const checkWin = (currentBoard, player) => {
        return winningConditions.some(combination => {
            return combination.every(index => {
                return currentBoard[index] === player;
            });
        });
    };

    /**
     * Checks if the game has ended in a draw.
     * @param {Array<string>} currentBoard The current state of the board.
     * @returns {boolean} True if the board is full, otherwise False.
     */
    const checkDraw = (currentBoard) => {
        return !currentBoard.includes('');
    };

    /**
     * Handles the user's click on a cell.
     * @param {Event} event The click event object.
     */
    const handleCellClick = (event) => {
        const clickedCell = event.target;
        const clickedCellIndex = parseInt(clickedCell.getAttribute('data-cell-index'));

        // Ignore the click if the cell is already taken or the game is over.
        if (board[clickedCellIndex] !== '' || !isGameActive || currentPlayer !== 'X') {
            return;
        }

        // Update the board state and the cell's content on the page.
        board[clickedCellIndex] = currentPlayer;
        clickedCell.textContent = currentPlayer;
        // 'X' gets a brighter yellow color.
        clickedCell.classList.add('text-yellow-300');

        // Check if the current move results in a win or a draw.
        if (checkWin(board, currentPlayer)) {
            isGameActive = false;
            updateStatus(`You have won!`);
            learn(gameHistory, -1); // AI loses, so it gets a negative reward.
            return;
        }
        if (checkDraw(board)) {
            isGameActive = false;
            updateStatus(`It's a draw!`);
            learn(gameHistory, 0.5); // AI gets a small positive reward for a draw.
            return;
        }

        // Switch to the AI's turn.
        currentPlayer = 'O';
        updateStatus(`AI is thinking...`);
        // Use a small delay to make the AI's "thinking" visible.
        setTimeout(aiMove, 500); 
    };

    /**
     * Resets the game board and state for a new game.
     */
    const resetGame = () => {
        // Decay epsilon slightly to make the AI more confident over time.
        EPSILON *= EPSILON_DECAY;
        
        isGameActive = true;
        currentPlayer = 'X';
        gameHistory = [];
        board = ['', '', '', '', '', '', '', '', ''];
        for (let i = 0; i < 9; i++) {
            cells[i].textContent = '';
            cells[i].classList.remove('text-yellow-300', 'text-yellow-400');
        }
        updateStatus("It's your turn (X).");
        qTableSizeDisplay.textContent = Object.keys(qTable).length;
        
        // Save the Q-table after each game.
        saveQTable();
    };

    /**
     * Returns a list of indices of all empty cells.
     * @param {Array<string>} currentBoard The current board state.
     * @returns {Array<number>} An array of empty cell indices.
     */
    const getEmptyCells = (currentBoard) => {
        const emptyCells = [];
        currentBoard.forEach((cell, index) => {
            if (cell === '') {
                emptyCells.push(index);
            }
        });
        return emptyCells;
    };

    /**
     * Executes the AI's turn by finding a move and placing its mark.
     */
    const aiMove = () => {
        const stateBeforeMove = getBoardStateKey(board);
        const move = chooseAIMove();
        
        if (move !== undefined) {
            // Store the AI's move and the state before it for learning later.
            gameHistory.push({ state: stateBeforeMove, action: move });
            
            const clickedCell = cells[move];
            board[move] = 'O';
            clickedCell.textContent = 'O';
            clickedCell.classList.add('text-yellow-400');

            // Check for game end after the AI's move
            if (checkWin(board, 'O')) {
                isGameActive = false;
                updateStatus("AI has won!");
                learn(gameHistory, 1); // AI wins, so it gets a positive reward.
                return;
            }
            if (checkDraw(board)) {
                isGameActive = false;
                updateStatus("It's a draw!");
                learn(gameHistory, 0.5); // AI gets a small positive reward for a draw.
                return;
            }

            // Switch back to the human player
            currentPlayer = 'X';
            updateStatus("It's your turn (X).");
        }
    };

    // --- Event Listeners and Initialization ---

    // Add a click event listener to each cell on the board
    cells.forEach(cell => cell.addEventListener('click', handleCellClick));

    // Add an event listener for the restart button
    restartButton.addEventListener('click', resetGame);
    
    // Load the AI's Q-table from local storage when the page loads.
    window.addEventListener('load', loadQTable);
</script>

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions