Code viewer for World: 2Flannel
// Cloned by Killian Daly on 23 Nov 2024 from World "Deep Flannel" by Killian Daly
// Please leave this clone trail here.

// Cloned by Killian Daly on 25 May 2024 from World "One Cube World (P5)" by Starter user 
// Please leave this clone trail here.

// Test cases from Alisson Gichette

const boardSize = 600;
const tileSize = boardSize / 8;
let canvasWidth, canvasHeight;

let chess = null;
let pieceImages = {};

// Create dictionary of pieces and corresponding images to display
// Wikimedia Commons. (2022). Wikimedia.org. https://commons.wikimedia.org/wiki/Category:PNG_chess_pieces/Standard_transparent#
const pieceImageUrls = {
    w: {
        p: "https://upload.wikimedia.org/wikipedia/commons/0/04/Chess_plt60.png",
        r: "https://upload.wikimedia.org/wikipedia/commons/5/5c/Chess_rlt60.png",
        n: "https://upload.wikimedia.org/wikipedia/commons/2/28/Chess_nlt60.png",
        b: "https://upload.wikimedia.org/wikipedia/commons/9/9b/Chess_blt60.png",
        q: "https://upload.wikimedia.org/wikipedia/commons/4/49/Chess_qlt60.png",
        k: "https://upload.wikimedia.org/wikipedia/commons/3/3b/Chess_klt60.png",
    },
    b: {
        p: "https://upload.wikimedia.org/wikipedia/commons/c/cd/Chess_pdt60.png",
        r: "https://upload.wikimedia.org/wikipedia/commons/a/a0/Chess_rdt60.png",
        n: "https://upload.wikimedia.org/wikipedia/commons/f/f1/Chess_ndt60.png",
        b: "https://upload.wikimedia.org/wikipedia/commons/8/81/Chess_bdt60.png",
        q: "https://upload.wikimedia.org/wikipedia/commons/a/af/Chess_qdt60.png",
        k: "https://upload.wikimedia.org/wikipedia/commons/e/e3/Chess_kdt60.png",
    },
};

// chess.js a chess library for chess move generation/validation, piece placement/movement and check/checkmate/draw detection
// Jeff Hlywa
// https://github.com/jhlywa/chess.js/tree/master
// code sourced from https://www.npmjs.com/package/chess.js/v/0.13.0?activeTab=readme
$.getScript("uploads/dalyk34/chess-min.js", function () {
    // Create chess object to use chess.js functionality
    chess = new Chess();
});

function preload() {
    // Audio for moves from http://images.chesscomfiles.com/chess-themes/sounds/_MP3_/default/notify.mp3
    moveSound = new Audio("uploads/dalyk34/notify.mp3");
    moveSound.volume = 0.2;
    
    // Music from Valve Corporation for non-commercial use in line with copyright policy
    const MUSICFILE = 'https://ia801404.us.archive.org/33/items/fight-songs-the-music-of-team-fortress-2-flac/14.%20Valve%20Studio%20Orchestra%20-%20Archimedes.mp3';
    AB.backgroundMusic ( MUSICFILE );
    
    // Load piece images
    for (let color in pieceImageUrls) {
        pieceImages[color] = {};
        for (let type in pieceImageUrls[color]) {
            pieceImages[color][type] = loadImage(pieceImageUrls[color][type]);
        }
    }
}

function setup() {
    canvasWidth = windowWidth;
    canvasHeight = windowHeight;

    createCanvas(canvasWidth, canvasHeight);
    
    // Chess Stockfish 16 gives a broken move when castling, so we disable castling by moving the pieces at start
    chess.load(chess.fen().replace(/ [KQkq]+ /, " - "));

    drawChessboard();
    drawPieces();
    
    makeWhiteMove();
}

function draw() {
    background("LightBlue");
    drawChessboard();
    drawPieces();
    drawWinnerBox();
}

function drawChessboard() {
    const startX = (canvasWidth - boardSize) / 2;
    const startY = (canvasHeight - boardSize) / 2;

    for (let row = 0; row < 8; row++) {
        for (let col = 0; col < 8; col++) {
            // Draw chessboard squares
            fill((row + col) % 2 === 0 ? 240 : 181, (row + col) % 2 === 0 ? 217 : 136, (row + col) % 2 === 0 ? 181 : 68);
            noStroke();
            rect(startX + col * tileSize, startY + row * tileSize, tileSize, tileSize);
        }
    }
}

function drawPieces() {
    if (!chess) return;

    const board = chess.board();
    const startX = (canvasWidth - boardSize) / 2;
    const startY = (canvasHeight - boardSize) / 2;

    for (let row = 0; row < 8; row++) {
        for (let col = 0; col < 8; col++) {
            const piece = board[row][col];
            if (piece) {
                const img = pieceImages[piece.color][piece.type];
                if (img) {
                    image(img, startX + col * tileSize, startY + row * tileSize, tileSize, tileSize);
                }
            }
        }
    }
}

function updatePiecesFromChess() {
    const board = chess.board();
    pieces = board.map((row) =>
        row.map((square) => (square ? square.type.toUpperCase() : " "))
    );
}

function redrawBoard() {
    background("beige");
    drawChessboard();
    drawPieces();
}

function drawWinnerBox(whiteWin) {
    const boxWidth = 260;
    const boxHeight = 100;
    const padding = 20;

    fill(181, 136, 68);
    rect(padding, padding, boxWidth, boxHeight, 10);

    // Black for stockfish 17
    fill(0);
    textSize(boxHeight / 4);
    textAlign(LEFT, CENTER);
    textStyle(BOLD);

    const textX = padding + 10;
    const textY = padding + boxHeight / 2;

    if (chess.in_checkmate()) {
        text(`Winner: Stockfish ${chess.turn() === "w" ? "17" : "16"}`, textX, textY);
    } else if (chess.in_stalemate() || chess.in_draw()) {
        text("No winner!", textX, textY);
    } else {
        text("Stockfish 17 vs.", textX, textY - 10);
        // WHite text for white player stockfish 16
        fill(255)
        text("Stockfish 16", textX, textY + 20);
    }
    
    // We check a boolean value because Stockfish 16 doesn't give its final winning move
    if (whiteWin) {
        text(`Winner: Stockfish 16"}`, textX, textY);
    }
}

function windowResized() {
    resizeCanvas(windowWidth, windowHeight);
}

// Play sound whenever a move is made
function playMoveSound() {
    if (moveSound) {
        // Reset to the start of the sound
        moveSound.currentTime = 0;
        moveSound.play();
    }
}

// Get move from Chess StockFish 16 API @ https://rapidapi.com/cinnamon17/api/chess-stockfish-16-api 
async function makeWhiteMove() {
    // End game if checkmate, stalemate, or draw
    if (checkGameState()) return;

    const form = new FormData();
    
     // Get current board state in fen format for White's move
    form.append("fen", chess.fen());

    const settings = {
        async: true,
        crossDomain: true,
        url: "https://chess-stockfish-16-api.p.rapidapi.com/chess/api",
        method: "POST",
        headers: {
            "x-rapidapi-key": "291a0dfb38mshf850128e2ea20d3p1a81cejsn8348250cd9d2", // API key
            "x-rapidapi-host": "chess-stockfish-16-api.p.rapidapi.com",
        },
        processData: false,
        contentType: false,
        mimeType: "multipart/form-data",
        data: form,
    };

    $.ajax(settings).done(function (response) {
        const data = JSON.parse(response);
        
        // Stockfish 16 does not give winning moves, for some reason, but that means bestmove (none) is a winning move
        if (data.result === "bestmove (none)" && data.message === "There is no available moves from this position") {
            console.log("Checkmate! White wins");
            // Let draw winner box know the winning move happened
            drawWinnerBox(true);
            return;
        }
        
        const bestMove = data.bestmove;

        // Apply move and trigger response if move is valid
        if (bestMove) {
            applyMove(bestMove);
        }
    });
    
    // Provide a turn delay so we don't hit API rate limits
    await turnDelay();
    makeBlackMove();
}

// Get move from Chess-API @ https://chess-api.com/ powered by Stockfish 17 
async function makeBlackMove() {
    // End game if checkmate, stalemate, or draw
    if (checkGameState()) return;

    const fenData = {
        // Stockfish 17 recognises a different boardstate format than chess.js, so we process it
        fen: await preprocessFenForBlack(chess.fen()),
        // Max settings
        variants: 5,
        depth: 18,
        maxThinkingTime: 100,
    };

    try {
        const response = await fetch("https://chess-api.com/v1", {
            method: "POST",
            headers: {
                "Content-Type": "application/json",
            },
            body: JSON.stringify(fenData),
        });

        // Parse the response as JSON
        const data = await response.json();
        const bestMove = data.move;

        // Apply move and trigger response if move is valid
        if (bestMove) {
            applyMove(bestMove);
        }
    } catch (error) {
        console.error("Error in Black's move:", error);
    }

    // Provide a turn delay so we don't hit API rate limits or become desynced
    await turnDelay();
    makeWhiteMove();
}

// Function to preprocess FEN using the correct turn from chess.js
async function preprocessFenForBlack(fen) {
    let parts = fen.split(" ");
    // Modify the turn to 'b' for black's turn, and update the halfmove/fullmove counters if needed
    parts[1] = 'b';  // Force black's turn (if needed)
    return parts.join(" ");
}

// We can slice the string e5e6 and make sense of it as e5 to e6 for example
function applyMove(bestMove) {
    const from = bestMove.slice(0, 2);
    const to = bestMove.slice(2, 4);
    const move = { from, to };

    // Stockfish 16 can't handle pawn promotion choice so they're always promoted to queen
    const piece = chess.get(from);
    if (piece && piece.type === "p" && (to[1] === "1" || to[1] === "8")) {
        // Promote to queen
        move.promotion = "q";
    }

    const result = chess.move(move);
    if (result) {
        console.log(move);
        updatePiecesFromChess();
        redrawBoard();
        playMoveSound();
    }
}

// A turn delay to avoid hitting API rate limits
async function turnDelay() {
    await new Promise((resolve) => { setTimeout(resolve, 2000); });
}

// Checking whether game has ended using chess.js library
function checkGameState() {
    if (chess.in_checkmate()) {
        console.log(`Checkmate! ${chess.turn() === "w" ? "Black" : "White"} wins.`);
        drawWinnerBox(false);
        return true;
    } else if (chess.in_stalemate() || chess.in_draw()) {
        console.log("It's a draw.");
        drawWinnerBox(false);
        return true;
    }
    return false;
}