Code viewer for World: AI Project
/*
    * Please note!
    * All of this work is mine EXCEPT for the fenToBoard function
    * The source is provided above the function declaration at line ~136
    * Otherwise, everything else is original to me and my partner
    * 
    * Authors -----------¬
    * - Rhodney Paraiso
    * - Evan Mackey
    * 
    * You may use a sample input for a FEN position, located below
    * r1bqkbnr/pp2pppp/2np4/8/3NP3/2N5/PPP2PPP/R1BQKB1R b KQkq - 2 5
*/

document.write(`
    <html>
        <header>
            <link rel="preconnect" href="https://fonts.googleapis.com">
            <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
            <link href="https://fonts.googleapis.com/css2?family=Geist+Mono:wght@100..900&display=swap" rel="stylesheet">
        </header>
    </html>

    <body>
        <div>
            <div class="title-title">
                <h1>
                    Welcome to the Chess App
                </h1>
            </div>
            <div class="title-title">
                <p1>
                    This is an Ancient Brain world that will analyse your chess FEN position and output the best moves to make from our API'S                    
                </p1>
            </div>
            <div class="title-title">
                <p1>
                    Please be sure to enter the Hugging Face API Key first! 
                </p1>
            </div>
            <div class="input-container">
                <input type="text" id="hf_api_key" placeholder="Enter HF API Key">
                <button onclick="setKey()">Enter Key</button>
            </div>
            <div class="input-container">
                <input type="text" id="fen_input" placeholder="Enter FEN position">
                <button onclick="processFen()">Show Board</button>
            </div>
            <br>
            <div id="output-title" style="width: 100vw; text-align: center"></div>
            <div id="output"></div>
            <div id="output-title"></div>
            <div id="output-bestmove"></div>
        </div>
    </body>

    <style>
        * {
            font-family: "Geist Mono", monospace;
            font-optical-sizing: auto;
            font-style: normal;
        }

        .title-title {
            display: flex;
            justify-content: center;
        }

        .input-container {
            display: flex;
            justify-content: center;
            gap: 10px;
            margin: 20px;
        }

        #output {
            display: flex;
            justify-content: center;
            margin-top: 20px;
            white-space: pre;
            font-family: monospace;
        }

        #output-title {
            display: flex;
            justify-content: center;
            margin-top: 20px;
            white-space: pre;
            font-family: monospace;
        }

        #output-bestmove {
            display: flex;
            justify-content: center;
            margin-top: 20px;
            white-space: pre;
            font-family: monospace;
        }
    </style>
`);

// VARIABLES
let api_key = "";

async function setKey() {
    const hf_api_key = document.getElementById('hf_api_key').value;
    api_key = hf_api_key;

    console.log("HF API Key set to:", api_key);
}

async function processFen() {
    const fen_input = document.getElementById('fen_input').value;
    const board     = fenToBoard(fen_input);

    // TESTING
    const STOCKFISH_RESULT = await stockfishAPI(fen_input);             // Stockfish API
    const top_moves        = await getTopFiveMoves(fen_input);          // To get the top 5 moves (for ranking)
    const LICHESS_API      = await lichessAPI(fen_input);              // Lichess API
    const CHESSAPI_RESULT  = await chessAPI({ fen: fen_input });       // Chess.com API
    const GITHUB_RESULT    = await githubAPI(fen_input);               // Github API
    const CHINESECN_RESULT = await chineseDBAPI(fen_input);            // Github API
    const HF_RESULT        = await huggingFaceAPI(fen_input);          // Hugging Face API

    console.log(HF_RESULT)

    console.log("RESULT IS BELOW");
    console.log(CHINESECN_RESULT);
    const formattedBoard   = board.map(row => row.join(' ')).join('\n'); // Formatting the board
    let output             = document.getElementById('output');
    let output_message     = "";                                        // Constructing the output message
    output.innerHTML       = `${formattedBoard}\n`;

    output_message = output_message + "Here are the best moves provided by our chess engines\n\n";
    output_message = output_message + `\nStockfish    API:    ${STOCKFISH_RESULT} Score: (${top_moves[STOCKFISH_RESULT] ||         "Not a good move"})\n`;
    output_message = output_message + `\nChess        API:    ${CHESSAPI_RESULT.move} Score: (${top_moves[CHESSAPI_RESULT.move] || "Not a good move"})\n`;
    output_message = output_message + `\nLichess      API:    ${LICHESS_API} Score: (${top_moves[LICHESS_API] ||                   "Not a good move"})\n`;
    output_message = output_message + `\nGitHub       API:    ${GITHUB_RESULT?.message} Score: (${top_moves[GITHUB_RESULT] ||      "Not a good move"})\n`;
    output_message = output_message + `\nChineseCN    API:    ${CHINESECN_RESULT.best_move} Score: (${top_moves[CHINESECN_RESULT.best_move] || "Not a good move"})\n`;
    output_message = output_message + `\nHugging Face API:    (Check the console!)\n`;

    output = document.getElementById('output-bestmove');
    output.innerHTML = output_message;
}

// Stockfish
// Inspired from https://stackoverflow.com/questions/66451525/how-to-convert-fen-id-onto-a-chess-board
// Function => Convert FEN to Board in text

function fenToBoard(fen) {
    const board = []
    const rows  = fen.split('/')

    for (const row of rows) {
        const brow = []
        for (const c of row) {
            if (c === ' ') break
            if ('12345678'.includes(c)) {
                brow.push(...Array(parseInt(c)).fill('--'))
            } else if (c === 'p') {
                brow.push('bp')
            } else if (c === 'P') {
                brow.push('wp')
            } else if (c > 'Z') {
                brow.push('b' + c.toUpperCase())
            } else {
                brow.push('w' + c)
            }
        }
        board.push(brow)
    }
    return board
}


/// Now using Stockfish API
// Creditt => https://lichess.org/api
const stockfishAPI = async (fenPosition) => {
    const options = {
        method: 'GET',
        headers: { 'Accept': 'application/json' }
    };

    const encodedFen = encodeURIComponent(fenPosition);
    const url = `https://lichess.org/api/cloud-eval?fen=${encodedFen}`;

    try {
        const response  = await fetch(url, options);
        const data      = await response.json();
        const best_move = data.pvs[0].moves.split(' ')[0];
        return best_move;
    } catch (err) {
        console.error('Error:', err);
        return 'Error analyzing position';
    }
};

// Credit => https://docs.github.com/en/rest/activity/events?apiVersion=2022-11-28
async function githubAPI() {
    try {
        const response = await fetch("https://api.github.com/events", {
            method: "GET",
            headers: {
                "Content-Type": "application/json"
            }
        });
        return await response.json();
    } catch (error) {
        console.error("Error querying GitHub API:", error);
        return { message: 'Error fetching data' };
    }
}

// Just getting the top 5 moves from Lichess API
// Creditt => https://lichess.org/api
async function getTopFiveMoves(fen) {
    const url = `https://lichess.org/api/cloud-eval?fen=${encodeURIComponent(fen)}`;
    try {
        const response        = await fetch(url);
        const data            = await response.json();
        const main_lines      = data?.pvs?.[0]?.moves?.split(' ') || [];
        const move_score_dict = {};
        main_lines.slice(0, 5).forEach((move, index) => {
            const baseScore       = data?.pvs?.[0]?.cp || 0;
            const scoreAdjustment = index * 5;
            const adjustedScore   = (baseScore - scoreAdjustment) / 100;
            move_score_dict[move] = adjustedScore.toFixed(2);
        });

        console.log(fen);
        return move_score_dict;
        
    } catch (error) {
        return { error: 'Error fetching moves' };
    }
}

// Credit => https://explorer.lichess.ovh
async function lichessAPI(fen) {
    const url = 'https://explorer.lichess.ovh/masters';

    try {
        const response = await fetch(`${url}?fen=${encodeURIComponent(fen)}`, {
            method: 'GET',
            headers: {
                'Accept': 'application/json'
            }
        });

        if (!response.ok) {
            return 'API response not ok';
        }

        const data = await response.json();
        const best_move = data?.moves?.[0]?.uci || 'No move available';
        return best_move;

    } catch (error) {
        console.log('An error occurred:', error);
        return 'Error fetching move';
    }
}


// Credit => https://chess-api.com/
async function chessAPI(data = {}) {
    const response = await fetch("https://chess-api.com/v1", {
        method: "POST",
        headers: {
            "Content-Type": "application/json"
        },
        body: JSON.stringify(data),
    });
    return response.json();
}

// Using a Chinese chess database API
// Credit => https://www.chessdb.cn/cloudbookc_api.html
const chineseDBAPI = async (fenPosition) => {
    const options = {
        method: 'GET',
        headers: {
            'Accept': 'application/json'
        }
    };
    const encodedFen = encodeURIComponent(fenPosition);
    const url        = `https://www.chessdb.cn/cdb.php?action=queryall&board=${encodedFen}&json=1`;
    try {
        const response = await fetch(url, options);
        
        if (!response.ok) {
            throw new Error(`HTTP error! status: ${response.status}`);
        }
        const data = await response.json();
        if (data && data.moves && data.moves.length > 0) {
            const best_move = data.moves[0].uci;
            const score     = data.moves[0].score;
            return { best_move, score };
        } else {
            console.log('No moves found in analysis');
            return null;
        }
    } catch (error) {
        console.log('Analysis request:', error.message);
        return null;
    }
};

// Credit => https://huggingface.co/openai-community/gpt2
async function huggingFaceAPI(fen) {
    const url = 'https://api-inference.huggingface.co/models/gpt2';
    try {
        const response = await fetch(url, {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
                'Authorization': `Bearer ${api_key}`
            },
            body: JSON.stringify({
                inputs: `Hugging face output ------ \nAnalyze this chess position: ${fen}. What's the best move?`,
            })
        });
        const data = await response.json();
        return data[0].generated_text;
    } catch (error) {
        return 'Analysis unavailable';
    }
}