Code viewer for World: AI Best Next Move
// Cloned by Conor Mc Quillan on 18 Nov 2024 from World "Chess" by Mathias Bazin 
// Please leave this clone trail here.

// Using Lichess cloud evaluation API: https://lichess.org/api
// Using Stockfish API (via Lichess Cloud Eval)

// Credits:

// Using Lichess Cloud Evaluation API: https://lichess.org/api
    // Note: Lichess API provides multiple evaluation options. Here, we specifically obtain moves from the Stockfish engine through Lichess.

// I compare two responses from the Lichess API to simulate different AI models (e.g., Stockfish at different evaluation depths).
// - Lichess Cloud Evaluation API: https://lichess.org/api (i used to evaluate board positions with Stockfish at different depths).
// - Comparison is based on Lichess Stockfish evaluations at two different settings.

// - Tutorials: General tutorial on using Fetch API in JavaScript: https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch
// - Chessboard concepts learned from Chess.js library documentation: https://github.com/jhlywa/chess.js
// - CSS gradient background inspiration: https://www.w3schools.com/css/css3_gradients.asp


// Conor's Code: Added setup control flag and GPT move cache
let setupCalled = false; // need to Prevent duplicate setup calls
let gptMoveCache = {}; // now need to Move cache
const fallbackMoves = {
    '8/8/8/8/3k4/3P4/4K3/8 w - - 0 1': 'e2d2', // Endgame position
    'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1': 'e2e4', // Starting position
    '8/8/8/8/4k3/3P4/4K3/8 b - - 0 1': 'd4e3', // Black to move in endgame
};

// Original Code: Declaring variables for chess game, square size, and offsets
let chess; // Chess object
let scl = 40;//swqaure size
let xoff = 250;// x 
let yoff = 300;// y

// Original Code: Images for chess pieces
let bk, bq, bn, br, bb, bp, wk, wq, wn, wr, wb, wp; // peice images 

// Original Code: Setting runReady to false
AB.runReady = false;

// Conor's Code/ original: Modified preload function
// Loading images for all the pieces
function preload() {
    bk = loadImage("/uploads/mathias/bk.png");
    bq = loadImage("/uploads/mathias/bq.png");
    bn = loadImage("/uploads/mathias/bn.png");
    br = loadImage("/uploads/mathias/br.png");
    bb = loadImage("/uploads/mathias/bb.png");
    bp = loadImage("/uploads/mathias/bp.png");
    wk = loadImage("/uploads/mathias/wk.png");
    wq = loadImage("/uploads/mathias/wq.png");
    wr = loadImage("/uploads/mathias/wr.png");
    wn = loadImage("/uploads/mathias/wn.png");
    wb = loadImage("/uploads/mathias/wb.png");
    wp = loadImage("/uploads/mathias/wp.png");
}


// Conor's Code / original: Function to wait for p5.js to load
// Added to handle asynchronous loading of p5.js library

function waitForP5(callback, interval = 100) {
    const intervalId = setInterval(() => {
        if (typeof createCanvas !== "undefined") {
            clearInterval(intervalId);
            callback();
        } else {
            console.log("Waiting for p5.js to be ready...");
        }
    }, interval);
}

// Conor's Code: Called waitForP5 to initiate setup
waitForP5(setup);

// Conor's Code: New setup function to initialize the board and create canvas other origina code didnt suit 
function setup() {
    if (setupCalled) return;
    setupCalled = true;

    console.log("setup called!");

    if (typeof createCanvas !== "undefined") {
        createCanvas(800, 800); // Create canvas for chessboard
    } else {
        console.error("p5.js is not ready, cannot create canvas.");
        return;
    }
    // Conor's Custom Chess Object
    // Simplified chess representation without using chess.js library
    chess = {
        board: 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1',
        fen: function () {
            return this.board;
        },
        makeMove: function (move) {
            console.log("Making move:", move);
            this.board = this.board.replace(' w ', ' b '); // Alternate turns
        },
    };
 
 // Conor's Code: Draw grid and initialize board with FEN input
    drawGrid();
    drawBoard(chess.fen());
    addFenInput(); // Add the FEN input field
    addButtons();  // Add the "Get AI Moves" button
}

// Conor's Code: New draw function for p5.js
// Clears and redraws the board every frame
function draw() {
    background(255); // clear the canvas 
    drawGrid();
    drawBoard(chess.fen());
}

// Conor's Code: Adding FEN input field for users to update the board
function addFenInput() {
    // Create a container for FEN input
    let fenContainer = document.createElement("div");
    fenContainer.style.position = "absolute";
    fenContainer.style.top = "20px";
    fenContainer.style.left = "50%";
    fenContainer.style.transform = "translateX(-50%)";
    fenContainer.style.zIndex = "9999";
    fenContainer.style.textAlign = "center";

    // Create a label for FEN input
    let fenLabel = document.createElement("label");
    fenLabel.innerHTML = "Enter FEN: ";
    fenLabel.style.fontSize = "16px";
    fenContainer.appendChild(fenLabel);

    // Create an input box for FEN
    let fenInput = document.createElement("input");
    fenInput.type = "text";
    fenInput.id = "fen-input";
    fenInput.style.padding = "5px";
    fenInput.style.fontSize = "14px";
    fenInput.style.marginRight = "10px";
    fenContainer.appendChild(fenInput);

    // Create a button to submit FEN
    let fenButton = document.createElement("button");
    fenButton.innerHTML = "Update Board";
    fenButton.style.padding = "5px 10px";
    fenButton.style.fontSize = "14px";
    fenContainer.appendChild(fenButton);

    document.body.appendChild(fenContainer);

    // Add an event listener to update the board based on the FEN input
    fenButton.addEventListener("click", function () {
        const fen = fenInput.value.trim();
        if (fen) {
            console.log("Updating board with FEN:", fen);
            chess.board = fen; // first Update the chess object with the new FEN
            drawBoard(fen); // now Redraw the board with the new FEN
        } else {
            console.error("FEN input is empty!");
        }
    });
}

// Conor's Code: Adding a button function to help to fetch AI moves
function addButtons() {
    console.log("addButtons called!");

    let moveButton = document.createElement("button");
    moveButton.innerHTML = "Get AI Moves";

    moveButton.style.position = "absolute";
    moveButton.style.bottom = "20px";
    moveButton.style.left = "50%";
    moveButton.style.transform = "translateX(-50%)";
    moveButton.style.padding = "10px 20px";
    moveButton.style.fontSize = "16px";
    moveButton.style.zIndex = "9999";

    document.body.appendChild(moveButton);

    moveButton.addEventListener("click", async function () {
        console.log("Button clicked!");
        if (chess) {
            const fen = chess.fen(); // Get the current chessboard position in FEN
            console.log("Current FEN:", fen);

            // Fetch moves trying to use fallback logic
            const stockfishMove = await getMoveWithFallback(getMoveFromStockfish, fen);
            console.log("Stockfish suggested move:", stockfishMove);

            const lichessMove = await getMoveWithFallback(getMoveFromLichess, fen);
            console.log("Lichess suggested move:", lichessMove);

            //now redraw the board
            drawBoard(chess.fen());

            // now display both moves
            displayComparison(stockfishMove, lichessMove);
        } else {
            console.error("Chess instance not available when button clicked.");
        }
    });
}

// oringinal code 
function drawGrid() {
    strokeWeight(2); // adding thicker lines here 
    for (let i = 0; i < 8; i++) {
        for (let j = 0; j < 8; j++) {
            let isLightSquare = (i + j) % 2 === 0;
            fill(isLightSquare ? "#f0d9b5" : "#b58863"); // Light and dark squares so it looks better visually kinda
            rect(xoff + j * scl, yoff + i * scl, scl, scl);
        }
    }
}


//Oringinal code 
function drawBoard(board) {
    let square = 0;

    for (let cur of board) {
        const x = xoff + scl * (square % 8);
        const y = yoff + scl * Math.floor(square / 8);

        
        // Draw pieces original code 
        switch (cur) {
            case "r":
                image(br, x, y);
                square++;
                break;
            case "n":
                image(bn, x, y);
                square++;
                break;
            case "b":
                image(bb, x, y);
                square++;
                break;
            case "q":
                image(bq, x, y);
                square++;
                break;
            case "k":
                image(bk, x, y);
                square++;
                break;
            case "p":
                image(bp, x, y);
                square++;
                break;
            case "R":
                image(wr, x, y);
                square++;
                break;
            case "N":
                image(wn, x, y);
                square++;
                break;
            case "B":
                image(wb, x, y);
                square++;
                break;
            case "Q":
                image(wq, x, y);
                square++;
                break;
            case "K":
                image(wk, x, y);
                square++;
                break;
            case "P":
                image(wp, x, y);
                square++;
                break
            case "/":
                break;
            case " ":
                return;
            case "1": case "2": case "3": case "4":
            case "5": case "6": case "7": case "8":
                square += +cur;
                break;
            default:
                console.log("Error in drawBoard");
        }
    }
}


// Original function with modifications 
async function getMoveFromStockfish(fen) {
    const apiUrl = "https://lichess.org/api/cloud-eval";
    try {
        const response = await fetch(`${apiUrl}?fen=${encodeURIComponent(fen)}`, { method: "GET" });

        if (!response.ok) {
            console.error(`Stockfish API Error: ${response.status} - ${response.statusText}`);
            return "No move found";
        }

        const data = await response.json();
        console.log("Stockfish API response:", data);

        return data.pvs && data.pvs[0] ? data.pvs[0].moves.split(" ")[0] : "No move found";
    } catch (error) {
        console.error("Error with Stockfish API:", error);
        return "Error";
    }
}

// Conor's Code: This Fetch move from Lichess using cloud evaluation API

async function getMoveFromLichess(fen) {
    const apiUrl = "https://lichess.org/api/cloud-eval";
    try {
        console.log(`Sending request to Lichess API for FEN: ${fen}`);

        // Perform GET request without API key (Conor's modification)
        const response = await fetch(`${apiUrl}?fen=${encodeURIComponent(fen)}`, { method: "GET" });

        // Handle unsuccessful response
        if (!response.ok) {
            console.error(`Lichess API Error: ${response.status} - ${response.statusText}`);
            if (response.status === 404) {
                console.warn("Position evaluation not found in Lichess cloud eval (404). Falling back to alternatives...");
            }
            return "No move found";
        }

      // Parse response (Conor's Code)
        const data = await response.json();
        console.log("Lichess API response:", data);

        return data.pvs && data.pvs[0] ? data.pvs[0].moves.split(" ")[0] : "No move found";
    } catch (error) {
        console.error("Error with Lichess API:", error);
        return "Error";
    }
}

// Conor's Code: Fallback function to handle move fetching from APIs 

async function getMoveWithFallback(apiCall, fen) {
    let move = await apiCall(fen);
    if (move === "No move found" || move === "Error") {
        console.warn(`Falling back for FEN: ${fen}`);

        // If Lichess fails, use fallback moves if available
        move = fallbackMoves[fen] || "No legal moves available";
    }
    return move;
}

// Conor's Code: Function to display comparison between Stockfish and Lichess moves

function displayComparison(stockfishMove, lichessMove) {
    let resultsSection = document.getElementById("results-section");
    if (!resultsSection) {
        resultsSection = document.createElement("div");
        resultsSection.id = "results-section";
        resultsSection.style.border = "1px solid #000"; // fix bug 
        resultsSection.style.padding = "10px";
        resultsSection.style.marginTop = "20px";
        document.body.appendChild(resultsSection);
    }

    // Clear previous results
    resultsSection.innerHTML = "";

    // Add Stockfish and Lichess results
    const stockfishDiv = document.createElement("div");
    stockfishDiv.innerHTML = `<strong>Stockfish Move:</strong> ${stockfishMove}`;
    resultsSection.appendChild(stockfishDiv);

    const lichessDiv = document.createElement("div");
    lichessDiv.innerHTML = `<strong>Lichess Move:</strong> ${lichessMove}`;
    resultsSection.appendChild(lichessDiv);

    // Now compare the two moves
    if (stockfishMove === lichessMove) {
        const agreementDiv = document.createElement("div");
        agreementDiv.innerHTML = `<strong>Result:</strong> Both engines agree on the move!`;
        agreementDiv.style.color = "Gree";
        resultsSection.appendChild(agreementDiv);
    } else {
        const disagreementDiv = document.createElement("div");
        disagreementDiv.innerHTML = `<strong>Result:</strong> Engines suggest different moves.`;
        disagreementDiv.style.color = "red";
        resultsSection.appendChild(disagreementDiv);
    }
}

// Original function with modifications 
function displayStockfishMove(stockfishMove) {
    let resultsSection = document.getElementById("results-section");
    if (!resultsSection) {
        resultsSection = document.createElement("div");
        resultsSection.id = "results-section";
        resultsSection.style.border = "1px solid #000";
        resultsSection.style.padding = "10px";
        resultsSection.style.marginTop = "20px";
        document.body.appendChild(resultsSection);
    }
    const stockfishMoveDiv = document.createElement("div");
    stockfishMoveDiv.innerHTML = `<strong>Stockfish Move:</strong> ${stockfishMove}`;
    resultsSection.appendChild(stockfishMoveDiv);
}

// Original function for world initialization with new modifications
function World() {
    this.newRun = function () {
        console.log("World initialized!");
    };
}


// now adding some css for fun and just to make it look nice :) (Conors Code)
const pageStyle = document.createElement("style");
pageStyle.innerHTML = `
    body {
        margin: 0;
        padding: 0;
        background: linear-gradient(to bottom, #1e3c72, #2a5298); /* Dark blue gradient */
        color: white;
        font-family: 'Roboto', sans-serif;
        display: flex;
        justify-content: center;
        align-items: center;
        height: 100vh; /* Full height */
        overflow: hidden; /* Prevent scrollbars */
    }

    canvas {
        display: block;
        margin: 0 auto; /* Center horizontally */
        box-shadow: 0 8px 16px rgba(0, 0, 0, 0.3); /* Add shadow to chessboard */
        border-radius: 10px; /* Rounded corners */
    }

    #fen-input {
        border: 2px solid #FFD700; /* Gold border */
        border-radius: 5px;
        padding: 10px;
        width: 300px;
        font-size: 14px;
        margin-bottom: 10px;
    }

    button {
        background-color: #FFD700; /* Gold button */
        color: black;
        border: none;
        border-radius: 5px;
        padding: 10px 20px;
        cursor: pointer;
        font-size: 16px;
        font-weight: bold;
        margin: 10px;
        box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
    }

    button:hover {
        background-color: #daa520; /* Darker gold on hover */
    }

    #results-section {
        font-family: 'Courier New', Courier, monospace;
        color: #333;
        background-color: rgba(255, 255, 255, 0.9); /* Slightly transparent white */
        border: 1px solid #FFD700;
        padding: 15px;
        border-radius: 5px;
        box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
        margin-top: 20px;
        text-align: center;
    }
`;
document.head.appendChild(pageStyle);