Checkers Script

Checkers Komputer - a checkers playing AI, built as a warm up for other games.

// ==UserScript==
// @name         Checkers Script
// @namespace    http://tampermonkey.net/
// @version      2024-05-05
// @description  Checkers Komputer - a checkers playing AI, built as a warm up for other games.
// @author       Stephen Wilbo Montague
// @match        http://gamesbyemail.com/Games/Checkers
// @icon         https://www.google.com/s2/favicons?sz=64&domain=gamesbyemail.com
// ==/UserScript==

(function() {
    // Begin main script.
    console.log("Hello Checkers.");

    // Wait for the page to load.
    waitForKeyElements("#Foundation_Elemental_7_savePlayerNotes", () => {

        // Add functions for simulating mouse input.
        Foundation.$registry[7].simulateMouseDown = function (valueIndex)
        {
            var boardPoint=this.boardPointFromValueIndex(valueIndex);
            var piece=this.pieces.findAtPoint(boardPoint);
            if (piece && piece.color==this.player.team.color)
            {
                if (this.madeMove && (!boardPoint.equals(this.lastMovePiece.boardPoint) || this.readyToSend))
                {
                    if (boardPoint.equals(this.lastMovePiece.boardPoint))
                        boardPoint=this.originalMoveFromPoint;
                    this.undo();
                    piece=this.pieces.findAtPoint(boardPoint);
                }
                this.clearHilites();
                this.pieces.cancelFlashes();
                this.lastMoveFromPoint=boardPoint;
                this.onLeftMouseUp="mouseUp";
                this.onDragByClicks="dragByClicks";
                this.lastMovePiece=piece;
                this.lastCheckedPoint=boardPoint.clone();
                this.lastCheckedLegal=false;
            }
        }; // End simulateMouseDown
        Foundation.$registry[7].simulateMouseUp = function(valueIndex)
        {
            this.onMouseMove=null;
            this.onLeftMouseUp=null;
            var boardPoint=this.boardPointFromValueIndex(valueIndex);
            var moveData=this.checkMove(boardPoint,this.lastMoveFromPoint);
            if (moveData)
            {
                this.setMoveData(moveData);
                this.lastMovePiece=this.pieces.findAtPoint(boardPoint);
                if (!this.madeMove)
                    this.originalMoveFromPoint=this.lastMoveFromPoint.clone();
                this.madeMove=true;
                if (moveData.canContinueJumping)
                {
                    this.pieces.flash(3,null,moveData.canContinueJumping);
                    this.readyToSend=false;
                }
                else
                    this.readyToSend=true;
                this.update();
            }
            else
            {
                this.lastMovePiece.reset();
                if (!this.madeMove)
                {
                    this.lastMovePiece=null;
                    this.lastMoveFromPoint=null;
                }
            }
        }; // End simulateMouseUp

        // Add button to page.
        console.log("Adding AI agent control button.");
        addButton("Run Checkers Komputer", runKomputer);
        window.IS_KOMPUTER_READY = true;
   }); // End wait for page load

})(); // End main script.

function addButton(text, onclick, cssObj) {
    let style = cssObj || {position: 'absolute', top: '574px', left:'24px', 'z-index': '9999', "-webkit-transition-duration": "0.6s", "transition-duration": "0.6s", overflow: 'hidden'}
    let button = document.createElement('button');
    button.setAttribute("class", "button_runKomputer");
    button.innerHTML = text;
    button.onclick = onclick;
    let btnStyle = button.style;
    Object.keys(style).forEach(key => btnStyle[key] = style[key]);
    document.body.appendChild(button); // For now, this works well enough.

    // Add Button Press Transition 1
    const cssButtonClassString1 = `.button_runKomputer:after{content: ""; background: #90EE90; display: block; position: absolute; padding-top: 300%; padding-left: 350%; margin-left: -20px!important; margin-top: -120%; opacity: 0; transition: all 1.0s}`;
    const styleTag1 = document.createElement("style");
    styleTag1.innerHTML = cssButtonClassString1;
    document.head.insertAdjacentElement('beforeend', styleTag1);

    // Add Button Press Transition 2
    const cssButtonClassString2 = `.button_runKomputer:active:after{padding: 0; margin: 0; opacity: 1; transition: 0s}`;
    const styleTag2 = document.createElement("style");
    styleTag2.innerHTML = cssButtonClassString2;
    document.head.insertAdjacentElement('beforeend', styleTag2);
}

async function inputMovements(origin, movements)
{
    let GBE_Origin = convertToGBE_Index(origin);
    let GBE_Destination = null;
    let firstMove = true;
    while (movements.length > 0)
    {
        if (firstMove)
        {
            await Foundation.$registry[7].simulateMouseDown(GBE_Origin);
            firstMove = false;
        }
        else
        {
            await Foundation.$registry[7].simulateMouseDown(GBE_Destination);
        }
        GBE_Destination = convertToGBE_Index(movements.shift());
        await Foundation.$registry[7].simulateMouseUp(GBE_Destination);
    }
    window.G_interval = await setInterval(sendMove, 100);
}


function convertToGBE_Index(localIndex)
{
    let GBE_Index = Math.abs(localIndex-31)
    if (GBE_Index % 4 === 3)
    {
        GBE_Index -= 3;
    }
    else if (GBE_Index % 4 === 2)
    {
        GBE_Index -= 1;
    }
     else if (GBE_Index % 4 === 1)
    {
        GBE_Index += 1;
    }
    else
    {
        GBE_Index += 3;
    }
    return GBE_Index;
}

async function sendMove()
{
    if (Foundation.$registry[7].readyToSend)
    {
        await Foundation.$registry[7].sendMove();
        if (typeof window.G_interval !== 'undefined' && window.G_interval)
        {
            clearInterval(window.G_interval);
        }
        window.IS_KOMPUTER_READY = true;
    }
}

async function maybeSendLastUserMove()
{
    if (Foundation.$registry[7].readyToSend)
    {
        await Foundation.$registry[7].sendMove();
        return 100;
    }
    return 1;
}

/**
     * Greasemonkey Wrench by CoeJoder, for public use.
     * Source: https://github.com/CoeJoder/GM_wrench/blob/master/src/GM_wrench.js
	 * Detect and handle AJAXed content.  Can force each element to be processed one or more times.
	 *
	 * @example
	 * GM_wrench.waitForKeyElements('div.comments', (element) => {
	 *   element.innerHTML = 'This text inserted by waitForKeyElements().';
	 * });
	 *
	 * GM_wrench.waitForKeyElements(() => {
	 *   const iframe = document.querySelector('iframe');
	 *   if (iframe) {
	 *     const iframeDoc = iframe.contentDocument || iframe.contentWindow.document;
	 *     return iframeDoc.querySelectorAll('div.comments');
	 *   }
	 *   return null;
	 * }, callbackFunc);
	 *
	 * @param {(string|function)} selectorOrFunction The selector string or function.
	 * @param {function}          callback           The callback function; takes a single DOM element as parameter.  If
	 *                                               returns true, element will be processed again on subsequent iterations.
	 * @param {boolean}           [waitOnce=true]    Whether to stop after the first elements are found.
	 * @param {number}            [interval=300]     The time (ms) to wait between iterations.
	 * @param {number}            [maxIntervals=-1]  The max number of intervals to run (negative number for unlimited).
*/
function waitForKeyElements (selectorOrFunction, callback, waitOnce, interval, maxIntervals) {
    if (typeof waitOnce === "undefined") {
        waitOnce = true;
    }
    if (typeof interval === "undefined") {
        interval = 300;
    }
    if (typeof maxIntervals === "undefined") {
        maxIntervals = -1;
    }
    var targetNodes =
        typeof selectorOrFunction === "function"
    ? selectorOrFunction()
    : document.querySelectorAll(selectorOrFunction);

    var targetsFound = targetNodes && targetNodes.length > 0;
    if (targetsFound) {
        targetNodes.forEach(function (targetNode) {
            var attrAlreadyFound = "data-userscript-alreadyFound";
            var alreadyFound = targetNode.getAttribute(attrAlreadyFound) || false;
            if (!alreadyFound) {
                var cancelFound = callback(targetNode);
                if (cancelFound) {
                    targetsFound = false;
                } else {
                    targetNode.setAttribute(attrAlreadyFound, true);
                }
            }
        });
    }

    if (maxIntervals !== 0 && !(targetsFound && waitOnce)) {
        maxIntervals -= 1;
        setTimeout(function () {
            waitForKeyElements(selectorOrFunction, callback, waitOnce, interval, maxIntervals);
        }, interval);
    }
};


async function runKomputer()
{
// Below is the Komputer AI script, via webpack without minification, as per the rules of Greasy Fork.
// Will create an organized, compact version of this soon. Time spent thinking per turn can be changed here.
// It's also possible to change AI versions - search for "const SETUP" and modify agent_0 to "Random", "MCTS-UCT", "MCTS-UCT-ENHANCED", etc.
// MCTS-PUCT-NET is currently not available, as importing the neural network into script managers and Greasy Fork is rather complicated.
// Instead, the AI is using a strong (sometimes better) alternative - a crafted hueristic based MCTS-PUCT algorithm.

// There's quite a few extra SETUP parameters for a standalone demo version on GitHub that allows tournaments.
// Most of these should be left as is.

if (window.IS_KOMPUTER_READY){
window.IS_KOMPUTER_READY = false;

// It's easy for a user to forget to send a move, so if a move is ready, send it first.
let delay = await maybeSendLastUserMove();

const WEBPACK_SCRIPT_OBJ = { f: function f(gameInfo) {

/******/ (() => { // webpackBootstrap
/******/ 	"use strict";
/******/ 	var __webpack_modules__ = ({

/***/ 442:
/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {


// EXPORTS
__webpack_require__.d(__webpack_exports__, {
  g: () => (/* binding */ Agent)
});

// EXTERNAL MODULE: ./src/setup.js
var setup = __webpack_require__(560);
;// CONCATENATED MODULE: ./src/mcts-puct/children.js


/*
This is an LRU cache for Node children.
The Least Recently Used child is deleted upon reaching max capacity.
This data structure, combined with a depth limit, is a simple way to cap memory use.

Based on Sean Welsh Brown's implementation.
Source: https://dev.to/seanwelshbrown/implement-a-simple-lru-cache-in-javascript-4o92
*/

class Children
{
    constructor(depth, childrenToClone = null)
    {
      this.cache = childrenToClone ? new Set(childrenToClone.cache) : new Set();
      this.capacity = initCapacity(depth);
    }

    get(key)
    {
        if (!this.cache.has(key)) return undefined;

        this.cache.delete(key);
        this.cache.add(key);
        return key;
    }

    put(key)
    {
        this.cache.delete(key);

        if (this.cache.size === this.capacity) {
          this.cache.delete(this.cache.keys().next().value);
          this.cache.add(key);
        } else {
          this.cache.add(key);
        }
    }
}

function initCapacity (depth)
{
  if (depth === 1)
  {
    return setup/* SETUP */.K.PUCT_ROOT_DEPTH_1_CHILD_CAPACITY;
  }
  else if (depth === 2)
  {
    return setup/* SETUP */.K.PUCT_NODE_DEPTH_2_CHILD_CAPACITY;
  }
  else
  {
    return setup/* SETUP */.K.PUCT_NODE_GENERAL_CHILD_CAPACITY;
  }
}

;// CONCATENATED MODULE: ./src/mcts-puct/node.js


class Node
{
    constructor(board, isPlayer1, visitCount = 0, sumValue = 0, parent = null, childrenToClone = null, isProvenWinner = false)
    {
        this.board = board;
        this.isPlayer1 = isPlayer1;
        this.visitCount = visitCount;
        this.sumValue = sumValue;
        this.parent = parent;
        this.depth = (parent === null) ? 0 : parent.depth + 1;
        this.children = new Children(this.depth + 1, childrenToClone);
        this.isProvenWinner = isProvenWinner;
    }

    clone()
    {
        return (new Node(this.board, this.isPlayer1, this.visitCount, this.sumValue, this.parent, this.children, this.isProvenWinner));
    }
}

;// CONCATENATED MODULE: ./src/mcts-puct/select.js


const DEPTH_LIMIT = setup/* SETUP */.K.PUCT_TREE_DEPTH_LIMIT;
const UCB_C = setup/* SETUP */.K.UCB_FORMULA_CONSTANT;

function SelectNode(root, rules)
{
    let bestUCB = 0;
    let bestChild = null;
    let selectedNode = root.clone();
    let depth = selectedNode.depth;

    // If the selected node has children, find the best descendant.
    while (depth < DEPTH_LIMIT && selectedNode.children.cache.size > 0)
    {
        for (let child of selectedNode.children.cache.keys())
        {
            if (child.visitCount > 0)
            {
                // Use PUCT formula, adjusted for adversarial play
                const P = ( 1 - rules.getPrediction(child.board, child.isPlayer1) ) ;
                const UCB_SCORE = (
                    (child.sumValue / child.visitCount) + (P * UCB_C * Math.sqrt( Math.log(child.parent.visitCount) / child.visitCount ) )
                    );
                if (UCB_SCORE > bestUCB)
                {
                    bestUCB = UCB_SCORE;
                    bestChild = child;
                }
            }
        }
        // Continue search under best child, and use the LRU children getter, to record using this child.
        selectedNode = bestChild? selectedNode.children.get(bestChild) : selectedNode.children.cache.keys().next().value;
        bestUCB = 0;
        bestChild = null;
        depth++;
    }
    return selectedNode;
}

/*

UCB1 formula: avgValue + ( 2 * sqrt( ln N / n ) )

---

avgValue:  node.sumValue / node.visitCount

ln: natural log

N: parent.visitCount

n: node.visitCount

---

Note: to avoid division by 0 error, visitCount > zero is required.
It also helps the formula work, to get at least some data from each node.

*/

;// CONCATENATED MODULE: ./src/mcts-puct/backpropagate.js

function Backpropagate(node, result)
{
    const simulatedNode = node;

    // Update all ancestors who match the simulated node by player, ending after root is updated.
    while(node.parent !== null)
    {
        if (node.parent.isPlayer1 === simulatedNode.isPlayer1)
        {
            node.parent.sumValue += result;
        }
        node.parent.visitCount++;
        node = node.parent;
    }
}

;// CONCATENATED MODULE: ./src/mcts-puct/expand.js




function Expand(node, rules)
{
    if (hasNextState(node, rules))
    {
        for (const NEXT_BOARD of rules.nextPossibleBoards)
        {
            node.children.put(new Node(NEXT_BOARD, !node.isPlayer1, 0, 0, node))
        }
    }
    else
    {
        handleLeaf(node, rules);
    }
}

function handleLeaf(node, rules)
{
    let result = 0;
    if (isTie(rules))
    {
        result = setup/* SETUP */.K.REWARD.TIE;
    }
    else if (isWin(node, rules))
    {
        result = calculateProvenWinReward(node.depth);
        node.isProvenWinner = true;
    }
    node.sumValue += result;
    node.visitCount++;
    Backpropagate(node, result);
}

function hasNextState(node, rules)
{
    return (rules.hasGeneratedNextPossibleStates(node.board, node.isPlayer1));
}

function isTie(rules)
{
    return (rules.winner.isPlayer1 === null);
}

function isWin(node, rules)
{
    return (rules.winner.isPlayer1 && node.parent.isPlayer1 || !rules.winner.isPlayer1 && !node.parent.isPlayer1);
}

function calculateProvenWinReward(depth) {
    switch(depth)
    {
        // An exponential decay function for rewards by depth, from Depth 1: million to Depth > 9.
        // These large rewards may help to differentiate between a winning game vs a won game.
        // Otherwise, a winning depth-limited agent may be happy to push pieces in a circle.
        // Basically, a proven win should be worth more than a win guess after simulation.
        // Likewise, a near win should be worth more than a distant win.

        // case 1:  Depth 1 doesn't need a reward, since proven winners are always chosen.
        //     return 1E6;
        case 2:
            return 400000;
        case 3:
            return 150000;
        case 4:
            return 60000;
        case 5:
            return 20000;
        case 6:
            return 6000;
        case 7:
            return 1500;
        case 8:
            return 400;
        case 9:
            return 120;
        default:
            return 30;
    }
}

;// CONCATENATED MODULE: ./src/random.js

/// Functions for random behavior.

function GetRandomNextBoard(nextPossibleBoards)
{
    const MAX = nextPossibleBoards.length;
    const RANDOM_INDEX = getRandomIndexExclusive(MAX);
    return nextPossibleBoards[RANDOM_INDEX];
}

// Returns random key from Set or Map.
// Source: https://stackoverflow.com/questions/42739256/how-get-random-item-from-es6-map-or-set
// This is slow O(n), so if there's a lot of keys, better take one of the first few keys,
// Or use a data structure that supports random access.
function GetRandomKey(collection) {
    let counter = 0;
    const RANDOM_INDEX = getRandomIndexExclusive(collection.size);
    for (let key of collection.keys()) {
        if (counter++ === RANDOM_INDEX) {
            return key;
        }
    }
}

// Returns random integer between [zero, max).
function getRandomIndexExclusive(max)
{
    return Math.floor(Math.random() * max);
}

;// CONCATENATED MODULE: ./src/mcts-puct/simulate.js



const simulate_DEPTH_LIMIT = setup/* SETUP */.K.PUCT_SIMULATION_DEPTH_LIMIT;
const TURN_LIMIT = setup/* SETUP */.K.MAX_TURNS_PER_GAME;

function Simulate(child, rules)
{
    let result = 0;
    const IS_PLAYER1_WINNER = getIsPlayer1Winner(child.board, child.isPlayer1, rules);
    if (IS_PLAYER1_WINNER === null)
    {
        result = setup/* SETUP */.K.REWARD.TIE;
    }
    else if (IS_PLAYER1_WINNER && child.parent.isPlayer1 || !IS_PLAYER1_WINNER && !child.parent.isPlayer1)
    {
        result = setup/* SETUP */.K.REWARD.WIN;
    }
    child.sumValue += result;
    child.visitCount++;
    return result;
}

///  Returns true for player1 win or false for loss, null if none.
function getIsPlayer1Winner(board, isPlayer1, rules)
{
    // Game loop for sim
    let turn = 0;
    let depth = 0;
    while(true)
    {
        const HAS_NEXT_STATE = rules.hasGeneratedNextPossibleStates(board, isPlayer1);

        // Check for a winner.
        if (rules.winner.isPlayer1 !== null)
        {
            return rules.winner.isPlayer1;
        }
        // Check for a tie.
        else if(!HAS_NEXT_STATE)
        {
            return null;
        }
        // Maybe continue.
        else if (depth < simulate_DEPTH_LIMIT && turn < TURN_LIMIT )
        {
            board = GetPredictedNextBoard(isPlayer1, rules);
            isPlayer1 = !isPlayer1;
            depth++;
        }
        // Guess result.
        else
        {
            return rules.willPlayer1Win(board, isPlayer1);
        }
    }
}

function GetPredictedNextBoard(currentIsPlayer1, rules)
{
    // Choose the board that has the least predicted chance for the opponent to win.
    let bestPrediction = Number.MAX_VALUE;
    let bestBoards = [];
    for (const NEXT_POSSIBLE_BOARD of rules.nextPossibleBoards)
    {
        const PREDICTION = rules.getPrediction(NEXT_POSSIBLE_BOARD, !currentIsPlayer1);
        if (PREDICTION < bestPrediction)
        {
            bestPrediction = PREDICTION;
            bestBoards = [];
            bestBoards.push(NEXT_POSSIBLE_BOARD);
        }
        else if (PREDICTION === bestPrediction)
        {
            bestBoards.push(NEXT_POSSIBLE_BOARD);
        }
    }
    return (bestBoards.length > 1) ? GetRandomNextBoard(bestBoards) : bestBoards[0];
}

// EXTERNAL MODULE: ./src/game-rules/tictactoe.js
var tictactoe = __webpack_require__(706);
// EXTERNAL MODULE: ./src/game-rules/checkers.js
var checkers = __webpack_require__(512);
;// CONCATENATED MODULE: ./src/mcts-puct/mcts_putc.js









const SEARCH_TIME = setup/* SETUP */.K.SEARCH_TIME;
const MAX_ITERATIONS = setup/* SETUP */.K.MAX_ITERATIONS;
const mcts_putc_DEPTH_LIMIT = setup/* SETUP */.K.PUCT_TREE_DEPTH_LIMIT;

class MCTS_PUCT_Logic
{
    constructor()
    {
        this.endSearchTime = null;
        this.rootNode = null;
        this.rules = null;
    }

    init(game, isPlayer1)
    {
        this.endSearchTime = (Date.now() + SEARCH_TIME);
        this.rootNode = new Node(game.board, isPlayer1);
        if (this.rules === null)
        {
            this.rules = this.getSimulationRules(game);
        }
        this.expandRoot(game.rules.nextPossibleBoards);
        for (const CHILD of this.rootNode.children.cache.keys())
        {
            const RESULT = Simulate(CHILD, this.rules);
            Backpropagate(CHILD, RESULT);
        }
    }

    getNextState()
    {
        while (this.hasTimeToThink() && this.hasMoreIterations())
        {
            const NODE_TO_VISIT = SelectNode(this.rootNode, this.rules);
            if (NODE_TO_VISIT.depth < mcts_putc_DEPTH_LIMIT)
            {
                Expand(NODE_TO_VISIT, this.rules);
                // Get first child via LRU children getter, which moves child to end, cycling who gets simulated next visit.
                for (let child of NODE_TO_VISIT.children.cache.keys())
                {
                    const CHILD_TO_SIM = NODE_TO_VISIT.children.get(child);
                    const RESULT = Simulate(CHILD_TO_SIM, this.rules);
                    Backpropagate(CHILD_TO_SIM, RESULT);
                    break;
                }
            }
            else
            {
                // At tree depth limit, just simulate the node.
                const RESULT = Simulate(NODE_TO_VISIT, this.rules);
                Backpropagate(NODE_TO_VISIT, RESULT);
            }
        }
        return this.getBest();
    }

    hasTimeToThink()
    {
        return (Date.now() < this.endSearchTime);
    }

    hasMoreIterations()
    {
        return (this.rootNode.visitCount < MAX_ITERATIONS);
    }

    getBest()
    {
        let bestChild = null;
        let bestVisitCount = 0;

        for (const CHILD of this.rootNode.children.cache.keys())
        {
            if (CHILD.isProvenWinner === true)
            {
                bestChild = CHILD;
                break;
            }
            if (CHILD.visitCount > bestVisitCount)
            {
                bestVisitCount = CHILD.visitCount;
                bestChild = CHILD;
            }
        }
        return bestChild.board;
    }

    getSimulationRules(game)
    {
        let rules = null;
        switch(game.name)
        {
            case "tictactoe":
                rules = new tictactoe/* TicTacToeRules */.u();
                break;
            case "checkers":
                rules = new checkers/* CheckersRules */.Y();
                break;
            default:
                console.error("Error: invalid game passed to MCTS for simulation.")
                break;
        }
        return rules;
    }

    expandRoot(nextPossibleBoards)
    {
        for (const BOARD of nextPossibleBoards)
        {
            this.rootNode.children.put(new Node(BOARD, !this.rootNode.isPlayer1, 0, 0, this.rootNode));
        }
    }
}

;// CONCATENATED MODULE: ./src/mcts-uct-enhanced/children.js


/*
This is an LRU cache for Node children.
The Least Recently Used child is deleted upon reaching max capacity.
This data structure, combined with a depth limit, is a simple way to cap memory use.

Based on Sean Welsh Brown's implementation.
Source: https://dev.to/seanwelshbrown/implement-a-simple-lru-cache-in-javascript-4o92
*/

class children_Children
{
    constructor(depth, childrenToClone = null)
    {
      this.cache = childrenToClone ? new Set(childrenToClone.cache) : new Set();
      this.capacity = children_initCapacity(depth);
    }

    get(key)
    {
        if (!this.cache.has(key)) return undefined;

        this.cache.delete(key);
        this.cache.add(key);
        return key;
    }

    put(key)
    {
        this.cache.delete(key);

        if (this.cache.size === this.capacity) {
          this.cache.delete(this.cache.keys().next().value);
          this.cache.add(key);
        } else {
          this.cache.add(key);
        }
    }
}

function children_initCapacity (depth)
{
  if (depth === 1)
  {
    return setup/* SETUP */.K.ROOT_DEPTH_1_CHILD_CAPACITY;
  }
  else if (depth === 2)
  {
    return setup/* SETUP */.K.NODE_DEPTH_2_CHILD_CAPACITY;
  }
  else
  {
    return setup/* SETUP */.K.NODE_GENERAL_CHILD_CAPACITY;
  }
}

;// CONCATENATED MODULE: ./src/mcts-uct-enhanced/node.js


class node_Node
{
    constructor(board, isPlayer1, visitCount = 0, sumValue = 0, parent = null, childrenToClone = null, isProvenWinner = false)
    {
        this.board = board;
        this.isPlayer1 = isPlayer1;
        this.visitCount = visitCount;
        this.sumValue = sumValue;
        this.parent = parent;
        this.depth = (parent === null) ? 0 : parent.depth + 1;
        this.children = new children_Children(this.depth + 1, childrenToClone);
        this.isProvenWinner = isProvenWinner;
    }

    clone()
    {
        return (new node_Node(this.board, this.isPlayer1, this.visitCount, this.sumValue, this.parent, this.children, this.isProvenWinner));
    }
}

;// CONCATENATED MODULE: ./src/mcts-uct-enhanced/select.js


const select_DEPTH_LIMIT = setup/* SETUP */.K.TREE_DEPTH_LIMIT;
const select_UCB_C = setup/* SETUP */.K.UCB_FORMULA_CONSTANT;

function select_SelectNode(root)
{
    let bestUCB = 0;
    let bestChild = null;
    let selectedNode = root.clone();

    // If the selected node has children, find the best descendant.
    while (selectedNode.depth < select_DEPTH_LIMIT && selectedNode.children.cache.size > 0)
    {
        for (let child of selectedNode.children.cache.keys())
        {
            if (child.visitCount > 0)
            {
                // Use UCB1 formula to find best node.
                const UCB_SCORE = (
                    (child.sumValue / child.visitCount) + (select_UCB_C * Math.sqrt( Math.log(child.parent.visitCount) / child.visitCount ) )
                    );
                if (UCB_SCORE > bestUCB)
                {
                    bestUCB = UCB_SCORE;
                    bestChild = child;
                }
            }
        }
        // Continue search under best child, and use the LRU children getter, to record using this child.
        selectedNode = bestChild? selectedNode.children.get(bestChild) : selectedNode.children.cache.keys().next().value;
        bestUCB = 0;
        bestChild = null;
    }
    return selectedNode;
}

/*

UCB1 formula: avgValue + ( 2 * sqrt( ln N / n ) )

---

avgValue:  node.sumValue / node.visitCount

ln: natural log

N: parent.visitCount

n: node.visitCount

---

Note: to avoid division by 0 error, visitCount > zero is required.
It also helps the formula work, to get at least some data from each node.

*/

;// CONCATENATED MODULE: ./src/mcts-uct-enhanced/backpropagate.js

function backpropagate_Backpropagate(node, result)
{
    const simulatedNode = node;

    // Update all ancestors who match the simulated node by player, ending after root is updated.
    while(node.parent !== null)
    {
        if (node.parent.isPlayer1 === simulatedNode.isPlayer1)
        {
            node.parent.sumValue += result;
        }
        node.parent.visitCount++;
        node = node.parent;
    }
}

;// CONCATENATED MODULE: ./src/mcts-uct-enhanced/expand.js




function expand_Expand(node, rules)
{
    if (expand_hasNextState(node, rules))
    {
        for (const NEXT_BOARD of rules.nextPossibleBoards)
        {
            node.children.put(new node_Node(NEXT_BOARD, !node.isPlayer1, 0, 0, node))
        }
    }
    else
    {
        expand_handleLeaf(node, rules);
    }
}

function expand_handleLeaf(node, rules)
{
    let result = 0;
    if (expand_isTie(rules))
    {
        result = setup/* SETUP */.K.REWARD.TIE;
    }
    else if (expand_isWin(node, rules))
    {
        result = expand_calculateProvenWinReward(node.depth);
        node.isProvenWinner = true;
    }
    node.sumValue += result;
    node.visitCount++;
    backpropagate_Backpropagate(node, result);
}

function expand_hasNextState(node, rules)
{
    return (rules.hasGeneratedNextPossibleStates(node.board, node.isPlayer1));
}

function expand_isTie(rules)
{
    return (rules.winner.isPlayer1 === null);
}

function expand_isWin(node, rules)
{
    return (rules.winner.isPlayer1 && node.parent.isPlayer1 || !rules.winner.isPlayer1 && !node.parent.isPlayer1);
}

function expand_calculateProvenWinReward(depth) {
    switch(depth)
    {
        // An exponential decay function for rewards by depth, from Depth 1: million to Depth > 9.
        // These large rewards may help to differentiate between a winning game vs a won game.
        // Otherwise, a winning depth-limited agent may be happy to push pieces in a circle.
        // Basically, a proven win should be worth more than a win guess after simulation.
        // Likewise, a near win should be worth more than a distant win.

        // case 1:  Depth 1 doesn't need a reward, since proven winners are always chosen.
        //     return 1E6;
        case 2:
            return 400000;
        case 3:
            return 150000;
        case 4:
            return 60000;
        case 5:
            return 20000;
        case 6:
            return 6000;
        case 7:
            return 1500;
        case 8:
            return 400;
        case 9:
            return 120;
        default:
            return 30;
    }
}

;// CONCATENATED MODULE: ./src/mcts-uct-enhanced/simulate.js



const mcts_uct_enhanced_simulate_DEPTH_LIMIT = setup/* SETUP */.K.SIMULATION_DEPTH_LIMIT;

function simulate_Simulate(child, rules)
{
    let result = 0;
    const IS_PLAYER1_WINNER = simulate_getIsPlayer1Winner(child.board, child.isPlayer1, rules);
    if (IS_PLAYER1_WINNER === null)
    {
        result = setup/* SETUP */.K.REWARD.TIE;
    }
    else if (IS_PLAYER1_WINNER && child.parent.isPlayer1 || !IS_PLAYER1_WINNER && !child.parent.isPlayer1)
    {
        result = setup/* SETUP */.K.REWARD.WIN;
    }
    child.sumValue += result;
    child.visitCount++;
    return result;
}

///  Returns true for player1 win or false for loss, null if none.
function simulate_getIsPlayer1Winner(board, isPlayer1, rules)
{
    // Game loop for sim
    let depth = 0;
    while(true)
    {
        const HAS_NEXT_STATE = rules.hasGeneratedNextPossibleStates(board, isPlayer1);

        // Check for a winner.
        if (rules.winner.isPlayer1 !== null)
        {
            return rules.winner.isPlayer1;
        }
        // Check for a tie.
        else if(!HAS_NEXT_STATE)
        {
            return null;
        }
        // Maybe continue.
        else if (depth < mcts_uct_enhanced_simulate_DEPTH_LIMIT)
        {
            board = GetRandomNextBoard(rules.nextPossibleBoards);
            isPlayer1 = !isPlayer1;
            depth++;
        }
        // Guess result.
        else
        {
            return rules.willPlayer1Win(board, isPlayer1);
        }
    }
}

;// CONCATENATED MODULE: ./src/mcts-uct-enhanced/mcts_utc_enhanced.js









const mcts_utc_enhanced_SEARCH_TIME = setup/* SETUP */.K.SEARCH_TIME;
const mcts_utc_enhanced_MAX_ITERATIONS = setup/* SETUP */.K.MAX_ITERATIONS;
const mcts_utc_enhanced_DEPTH_LIMIT = setup/* SETUP */.K.TREE_DEPTH_LIMIT;

class MCTS_UCT_Enhanced_Logic
{
    constructor()
    {
        this.endSearchTime = null;
        this.rootNode = null;
        this.rules = null;
    }

    init(game, isPlayer1)
    {
        this.endSearchTime = (Date.now() + mcts_utc_enhanced_SEARCH_TIME);
        this.rootNode = new node_Node(game.board, isPlayer1);
        this.rules = this.getSimulationRules(game);

        this.expandRoot(game.rules.nextPossibleBoards);
        for (const CHILD of this.rootNode.children.cache.keys())
        {
            const RESULT = simulate_Simulate(CHILD, this.rules);
            backpropagate_Backpropagate(CHILD, RESULT);
        }
    }

    getNextState()
    {
        while (this.hasTimeToThink() && this.hasMoreIterations())
        {
            const NODE_TO_VISIT = select_SelectNode(this.rootNode);
            if (NODE_TO_VISIT.depth < mcts_utc_enhanced_DEPTH_LIMIT)
            {
                expand_Expand(NODE_TO_VISIT, this.rules);
                // Get first child via LRU children getter, which moves child to end, cycling who gets simulated next visit.
                for (let child of NODE_TO_VISIT.children.cache.keys())
                {
                    const CHILD_TO_SIM = NODE_TO_VISIT.children.get(child);
                    const RESULT = simulate_Simulate(CHILD_TO_SIM, this.rules);
                    backpropagate_Backpropagate(CHILD_TO_SIM, RESULT);
                    break;
                }
            }
            else
            {
                // At tree depth limit, just simulate the node.
                const RESULT = simulate_Simulate(NODE_TO_VISIT, this.rules);
                backpropagate_Backpropagate(NODE_TO_VISIT, RESULT);
            }
        }
        return this.getBest();
    }

    hasTimeToThink()
    {
        return (Date.now() < this.endSearchTime);
    }

    hasMoreIterations()
    {
        return (this.rootNode.visitCount < mcts_utc_enhanced_MAX_ITERATIONS);
    }

    getBest()
    {
        let bestChild = null;
        let bestVisitCount = 0;

        for (const CHILD of this.rootNode.children.cache.keys())
        {
            if (CHILD.isProvenWinner === true)
            {
                bestChild = CHILD;
                break;
            }
            if (CHILD.visitCount > bestVisitCount)
            {
                bestVisitCount = CHILD.visitCount;
                bestChild = CHILD;
            }
        }
        return bestChild.board;
    }

    getSimulationRules(game)
    {
        let rules = null;
        switch(game.name)
        {
            case "tictactoe":
                rules = new tictactoe/* TicTacToeRules */.u();
                break;
            case "checkers":
                rules = new checkers/* CheckersRules */.Y();
                break;
            default:
                console.error("Error: invalid game passed to MCTS for simulation.")
                break;
        }
        return rules;
    }

    expandRoot(nextPossibleBoards)
    {
        for (const BOARD of nextPossibleBoards)
        {
            this.rootNode.children.put(new node_Node(BOARD, !this.rootNode.isPlayer1, 0, 0, this.rootNode));
        }
    }
}

;// CONCATENATED MODULE: ./src/mcts-uct/node.js

class mcts_uct_node_Node
{
    constructor(board, isPlayer1, visitCount = 0, sumValue = 0, parent = null, children = null)
    {
        this.board = board;
        this.isPlayer1 = isPlayer1;
        this.visitCount = visitCount;
        this.sumValue = sumValue;
        this.parent = parent;
        this.children = new Set(children);
    }

    clone()
    {
        return (new mcts_uct_node_Node(this.board, this.isPlayer1, this.visitCount, this.sumValue, this.parent, this.children));
    }
}

;// CONCATENATED MODULE: ./src/mcts-uct/select.js



const mcts_uct_select_UCB_C = setup/* SETUP */.K.UCB_FORMULA_CONSTANT;

/// Return descendent child key with max UCB value.
function mcts_uct_select_SelectNode(root)
{
    let bestUCB = 0;
    let bestChild = null;
    let selectedNode = root.clone();

    // If the selected node has a map of children, find the best descendant.
    while (selectedNode.children.size > 0)
    {
        for (let child of selectedNode.children.keys())
        {
            if (child.visitCount > 0)
            {
                const UCB_SCORE = (
                    (child.sumValue / child.visitCount) + ( mcts_uct_select_UCB_C * Math.sqrt( Math.log(child.parent.visitCount) / child.visitCount ) )
                    );
                if (UCB_SCORE > bestUCB)
                {
                    bestUCB = UCB_SCORE;
                    bestChild = child;
                }
            }
        }
        // Continue search under best child.
        selectedNode = bestChild? bestChild: GetRandomKey(selectedNode.children); // Use this, or maybe use selectedNode.children.keys().next().value;
        bestUCB = 0;
        bestChild = null;
    }
    return selectedNode;
}

/*

UCB1 formula: avgValue + ( 2 * sqrt( ln N / n ) )

---

avgValue:  node.sumValue / node.visitCount

ln: natural log

N: parent.visitCount

n: node.visitCount

---

Note: to avoid division by 0 error, visitCount > zero is required.
It also helps the formula work, to get at least some data from each node.

*/

;// CONCATENATED MODULE: ./src/mcts-uct/backpropagate.js

function mcts_uct_backpropagate_Backpropagate(node, result)
{
    const simulatedNode = node;

    // Update all ancestors who match the simulated node by player, ending after root is updated.
    while(node.parent !== null)
    {
        if (node.parent.isPlayer1 === simulatedNode.isPlayer1)
        {
            node.parent.sumValue += result;
        }
        node.parent.visitCount++;
        node = node.parent;
    }
}

;// CONCATENATED MODULE: ./src/mcts-uct/expand.js




/// Add new nodes to given node as children, if able, or if terminal, update tree.
function mcts_uct_expand_Expand(node, rules)
{
    // Generate nextPossibleBoards / states. For each board, add to children, as an opponent.
    const HAS_NEXT_STATE = rules.hasGeneratedNextPossibleStates(node.board, node.isPlayer1);
    if (HAS_NEXT_STATE)
    {
        for (const NEXT_BOARD of rules.nextPossibleBoards)
        {
            node.children.add(new mcts_uct_node_Node(NEXT_BOARD, !node.isPlayer1, 0, 0, node, null))
        }
    }
    // When node is a leaf (game in terminal state), check result and update tree.
    else
    {
        mcts_uct_expand_handleLeaf(node, rules);
    }
}

function mcts_uct_expand_handleLeaf(node, rules)
{
    let result = 0;
    if (rules.winner.isPlayer1 === null)
    {
        result = setup/* SETUP */.K.REWARD.TIE;
    }
    else if (rules.winner.isPlayer1 && node.parent.isPlayer1 || !rules.winner.isPlayer1 && !node.parent.isPlayer1)
    {
        result = setup/* SETUP */.K.REWARD.WIN;
    }
    node.sumValue += result;
    node.visitCount++;
    mcts_uct_backpropagate_Backpropagate(node, result);
}

;// CONCATENATED MODULE: ./src/mcts-uct/simulate.js



const simulate_TURN_LIMIT = setup/* SETUP */.K.MAX_TURNS_PER_GAME;

function mcts_uct_simulate_Simulate(child, rules)
{
    let result = 0;
    const IS_PLAYER1_WINNER = getisPlayer1Winner(child.board, child.isPlayer1, rules);
    if (IS_PLAYER1_WINNER === null)
    {
        result = setup/* SETUP */.K.REWARD.TIE;
    }
    else if (IS_PLAYER1_WINNER && child.parent.isPlayer1 || !IS_PLAYER1_WINNER && !child.parent.isPlayer1)
    {
        result = setup/* SETUP */.K.REWARD.WIN;
    }
    child.sumValue += result;
    child.visitCount++;
    return result;
}

///  Returns true for player1 win or false for loss, null if none.
function getisPlayer1Winner(board, isPlayer1, rules)
{
    // Game loop for sim
    let turn = 0;
    while(true)
    {
        const HAS_NEXT_STATE = rules.hasGeneratedNextPossibleStates(board, isPlayer1);

        // Check for a winner.
        if (rules.winner.isPlayer1 !== null)
        {
            return rules.winner.isPlayer1;
        }
        // Check for a tie.
        else if( !HAS_NEXT_STATE || !(turn < simulate_TURN_LIMIT) )
        {
            return null;
        }
        // Continue game.
        else
        {
            board = GetRandomNextBoard(rules.nextPossibleBoards);
            isPlayer1 = !isPlayer1;
            turn++;
        }
    }
}

;// CONCATENATED MODULE: ./src/mcts-uct/mcts_utc.js










const mcts_utc_SEARCH_TIME = setup/* SETUP */.K.SEARCH_TIME;
const mcts_utc_MAX_ITERATIONS = setup/* SETUP */.K.MAX_ITERATIONS;

class MCTS_UCT_Logic
{
    constructor()
    {
        this.endSearchTime = null;
        this.rootNode = null;
        this.rules = null;
    }

    init(game, isPlayer1)
    {
        this.endSearchTime = (Date.now() + mcts_utc_SEARCH_TIME);
        this.rootNode = new mcts_uct_node_Node(game.board, isPlayer1);
        this.rules = this.getSimulationRules(game);

        this.fullyExpand(game.rules.nextPossibleBoards);
        for (let child of this.rootNode.children.keys())
        {
            const RESULT = mcts_uct_simulate_Simulate(child, this.rules);
            mcts_uct_backpropagate_Backpropagate(child, RESULT);
        }
    }

    getNextState()
    {
        while (this.hasTimeToThink() && (this.hasMoreIterations()))
        {
            const NODE_TO_VISIT = mcts_uct_select_SelectNode(this.rootNode);
            mcts_uct_expand_Expand(NODE_TO_VISIT, this.rules);
            for (let child of NODE_TO_VISIT.children.keys())
            {
                if (child.visitCount === 0)
                {
                    const RESULT = mcts_uct_simulate_Simulate(child, this.rules);
                    mcts_uct_backpropagate_Backpropagate(child, RESULT);
                    break;
                }
            }
        }
        return this.getBest();
    }

    hasTimeToThink()
    {
        return (Date.now() < this.endSearchTime);
    }

    hasMoreIterations()
    {
        return this.rootNode.visitCount < mcts_utc_MAX_ITERATIONS
    }

    getBest()
    {
        let bestChild = null;
        let bestVisitCount = 0;

        for (let child of this.rootNode.children.keys())
        {
            if (child.visitCount > bestVisitCount)
            {
                bestVisitCount = child.visitCount;
                bestChild = child;
            }
        }
        return bestChild.board;
    }

    getSimulationRules(game)
    {
        let rules = null;
        switch(game.name)
        {
            case "tictactoe":
                rules = new tictactoe/* TicTacToeRules */.u();
                break;
            case "checkers":
                rules = new checkers/* CheckersRules */.Y();
                break;
            default:
                console.error("Error: invalid game passed to MCTS for simulation.")
                break;
        }
        return rules;
    }

    fullyExpand(nextPossibleBoards)
    {
        for (const BOARD of nextPossibleBoards)
        {
            this.rootNode.children.add(new mcts_uct_node_Node(BOARD, !this.rootNode.isPlayer1, 0, 0, this.rootNode, null));
        }
    }
}

;// CONCATENATED MODULE: ./src/agent.js




// import { MCTS_PUCT_NET_Logic } from "./mcts-puct-net/mcts_putc_net.js";
// import { NeuralNet } from "./setup.js";

class Agent
{
    constructor(name, network = null)
    {
        this.logName = name;
        this.name = name.toLowerCase();
        switch (this.name)
        {
            case "random":
                this.logic = null;
                break;
            case "mcts-uct":
                this.logic = new MCTS_UCT_Logic();
                break;
            case "mcts-uct-enhanced":
                this.logic = new MCTS_UCT_Enhanced_Logic();
                break;
            case "mcts-puct":
                this.logic = new MCTS_PUCT_Logic();
                break;
            // case "mcts-puct-net":
            //     this.logic = new MCTS_PUCT_NET_Logic();
            //     this.requiresNetwork = true;
            //     break;
            default:
                console.error("Error: invalid agent name passed to Agent constructor.");
                break;
        }
        this.game = null;
        this.isPlayer1 = null;
        this.winCount = 0;
        console.log(this.logName + " Agent constructed.");
    }

    async begin(game, isPlayer1 = true)
    {
        this.game = game;
        this.isPlayer1 = isPlayer1;
        // if (this.requiresNetwork && !this.logic.network)
        // {
        //     this.logic.network = await NeuralNet();
        // }
        if (isPlayer1)
        {
            console.log("= = = = =");
            game.logBoard();
            console.log(`${this.logName} begins.`);
            console.log("= = = = =");
        }
    }

    async continue()
    {
        this.game.hasNextState = this.game.rules.hasGeneratedNextPossibleStates(this.game.board, this.isPlayer1);

        if (this.game.hasWinner())
        {
            this.game.isDone = true;
            console.log('Game won by Player %s.', this.game.rules.winner.logName);
        }
        else if(this.game.isOver()) // Only checks if game over, but here, this implies a tie.
        {
            this.game.isDone = true;
            console.log("Game over.");
        }
        else
        {
            await this.chooseNextState();
            this.game.logBoard();
            console.log(`Turn played by: ${this.logName}`);
            console.log("= = = = =");
        }
    }

    async chooseNextState()
    {
        if (this.name === "random")
        {
            this.game.board = GetRandomNextBoard(this.game.rules.nextPossibleBoards);
        }
        else
        {
            // Cache current board.
            this.game.lastBoard = this.game.board;
            console.log(`${this.logName} is thinking.`);
            // Set next board.
            this.logic.init(this.game, this.isPlayer1);
            this.game.board = this.logic.getNextState();
            // Derive piece movements.
            let movements = [];
            const ORIGIN = this.game.rules.deriveMovements(this.game.lastBoard, this.game.board, this.isPlayer1, movements);
            console.log("Next move origin: " + ORIGIN);
            console.log(`Next movements: [${movements.join()}]`);
            // From web worker to main thread, send move origin and movements.
            postMessage([ORIGIN, movements]);
        }
    } // End function chooseNextState()
} // End class Agent


/***/ }),

/***/ 512:
/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {

/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */   Y: () => (/* binding */ CheckersRules)
/* harmony export */ });
/* harmony import */ var _winner_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(710);

/// Board is a grid of 32 cells,
/// Usually as a string of characters,
/// From index 0 (top) to 31 (bottom),
/// Where player1 begins on the bottom.
///
///  Board vizualization:
///
///      0      1       2      3
///   4      5      6       7
///      8      9      10     11
///  12     13     14      15
///     16     17      18     19
///  20     21     22      23
///     24     25      26     27
///  28     29     30      31



const EMPTY = '+'  // Empty cell.
const MAN = 'M'  // Player1 pawn.
const KING = 'K'  // Player1 royal.
const WOMAN = 'W' // Player2 pawn.
const QUEEN = 'Q'  // Player2 royal.
const BOARD_WIDTH = 4;
const BOARD_HEIGHT = 8;
const BOARD_CELL_COUNT = 32;
const HAS_CHECKERS_PATTERN = true;  // For board logs.
const KING_PROMOTION_MAX = 4; // Max index, exclusive, of the top row.
const QUEEN_PROMOTION_MIN = 27; // Min index, exclusive, of the bottom row.

class CheckersRules
{
    constructor()
    {
        this.winner = new _winner_js__WEBPACK_IMPORTED_MODULE_0__/* .Winner */ .k();
        this.nextPossibleBoards = [];
        this.possibleTurnMovements = [];
        this.transitionMoves = [];
    }

    getNewBoard(initialBoard)
    {
        const BOARD = (initialBoard === null)? "WWWWWWWWWWWW++++++++MMMMMMMMMMMM" : initialBoard;
        return [BOARD, BOARD_HEIGHT, BOARD_WIDTH, HAS_CHECKERS_PATTERN];
    }

    hasGeneratedNextPossibleStates(board, isPlayer1)
     {
        // Clear data from any prior game / simulation.
        this.nextPossibleBoards = [];
        this.possibleTurnMovements = [];
        this.winner.isPlayer1 = null;
        this.winner.logName = null;

        let playerPawn = isPlayer1 ? MAN : WOMAN;
        let playerRoyal = isPlayer1 ? KING : QUEEN;
        let opponentPawn = isPlayer1 ? WOMAN : MAN;
        let opponentRoyal = isPlayer1 ? QUEEN : KING;

        if (this.isJumpPossibleOnBoard(board, isPlayer1, playerPawn, playerRoyal, opponentPawn, opponentRoyal))
        {
           this.pushAllJumps(board, isPlayer1, playerPawn, playerRoyal, opponentPawn, opponentRoyal);
        }
        else
        {
           this.pushAllAdjacentMoves(board, isPlayer1, playerPawn, playerRoyal);
        }
        if (this.nextPossibleBoards.length > 0)
        {
            return true;
        }
        else
        {
            // Whoever played last won.
            // So the winner is the opposite.
            this.winner.isPlayer1 = !isPlayer1;
            this.winner.logName = isPlayer1? "2" : "1";
            return false;
        }
    }

    isJumpPossibleOnBoard(board, isPlayer1, playerPawn, playerRoyal, opponentPawn, opponentRoyal)
    {
       for (let i = 0; i < BOARD_CELL_COUNT; i++)
       {
          // Check if the cell contains a piece of the current player
          if (board[i] === playerPawn || board[i] === playerRoyal)
          {
             // Calculate forward indexes near piece
             const fwdLeftIndex = isPlayer1 ? this.northWestGet(i) : this.southEastGet(i);
             const fwdRightIndex = isPlayer1 ? this.northEastGet(i) : this.southWestGet(i);
             const fwdLeftJumpIndex = isPlayer1 ? this.northWestJumpGet(i) : this.southEastJumpGet(i);
             const fwdRightJumpIndex = isPlayer1 ? this.northEastJumpGet(i) : this.southWestJumpGet(i);
             // Check for forward left jumps
             if (fwdLeftIndex !== null && fwdLeftJumpIndex !== null &&
                board[fwdLeftJumpIndex] === EMPTY &&
                (board[fwdLeftIndex] === opponentPawn ||
                board[fwdLeftIndex] === opponentRoyal))
             {
                return true;
             }
             // Check for forward right jumps
             if (fwdRightIndex !== null && fwdRightJumpIndex !== null &&
                board[fwdRightJumpIndex] === EMPTY &&
                (board[fwdRightIndex] === opponentPawn ||
                board[fwdRightIndex] === opponentRoyal))
             {
                return true;
             }
             if (board[i] === playerRoyal)
             {
                // Calculate backward cells near the piece
                const backLeftIndex = isPlayer1 ? this.southWestGet(i) : this.northEastGet(i);
                const backRightIndex = isPlayer1 ? this.southEastGet(i) : this.northWestGet(i);
                const backLeftJumpIndex = isPlayer1 ? this.southWestJumpGet(i) : this.northEastJumpGet(i);
                const backRightJumpIndex = isPlayer1 ? this.southEastJumpGet(i) : this.northWestJumpGet(i);
                // Check for back left jumps
                if (backLeftIndex !== null && backLeftJumpIndex !== null &&
                   board[backLeftJumpIndex] === EMPTY &&
                   (board[backLeftIndex] === opponentPawn ||
                   board[backLeftIndex] === opponentRoyal))
                {
                   return true;
                }
                // Check for back right jumps
                if (backRightIndex !== null && backRightJumpIndex !== null &&
                   board[backRightJumpIndex] === EMPTY &&
                   (board[backRightIndex] === opponentPawn ||
                   board[backRightIndex] === opponentRoyal))
                {
                   return true;
                }
             }
          }
       }
       return false;
    }

    pushAllJumps(board, isPlayer1, playerPawn, playerRoyal, opponentPawn, opponentRoyal) {
        for (let index = 0; index < BOARD_CELL_COUNT; index++) {
            if (board[index] === playerPawn || board[index] === playerRoyal) {
                this.generateNextJumpBoards(board, index, isPlayer1, playerPawn, playerRoyal, opponentPawn, opponentRoyal);
            }
        }
    }

    pushAllAdjacentMoves(board, isPlayer1, playerPawn, playerRoyal) {
        for (let i = 0; i < BOARD_CELL_COUNT; i++) {
            if (board[i] === playerPawn || board[i] === playerRoyal) {
                // Calculate forward indexes near piece
                const FWD_LEFT_INDEX = isPlayer1 ? this.northWestGet(i) : this.southEastGet(i);
                const FWD_RIGHT_INDEX = isPlayer1 ? this.northEastGet(i) : this.southWestGet(i);

                // Check if piece can move to adjacent cell
                if (FWD_LEFT_INDEX !== null && board[FWD_LEFT_INDEX] === EMPTY) {
                    // Make the move on a new board.
                    const [NEW_BOARD, _] = this.getNewBoardFromMove(board, i, FWD_LEFT_INDEX);
                    // Add new board to next possible boards
                    this.possibleTurnMovements.push([FWD_LEFT_INDEX]);
                    this.nextPossibleBoards.push(NEW_BOARD);
                }
                if (FWD_RIGHT_INDEX !== null && board[FWD_RIGHT_INDEX] === EMPTY) {
                    const [NEW_BOARD, _] = this.getNewBoardFromMove(board, i, FWD_RIGHT_INDEX);
                    this.possibleTurnMovements.push([FWD_RIGHT_INDEX]);
                    this.nextPossibleBoards.push(NEW_BOARD);
                }
                // Check for king moves
                if (board[i] === playerRoyal) {
                    const BACK_LEFT_INDEX = isPlayer1 ? this.southWestGet(i) : this.northEastGet(i);
                    const BACK_RIGHT_INDEX = isPlayer1 ? this.southEastGet(i) : this.northWestGet(i);
                    if (BACK_LEFT_INDEX !== null && board[BACK_LEFT_INDEX] === EMPTY) {
                        const [NEW_BOARD, _] = this.getNewBoardFromMove(board, i, BACK_LEFT_INDEX);
                        this.possibleTurnMovements.push([BACK_LEFT_INDEX]);
                        this.nextPossibleBoards.push(NEW_BOARD);
                    }
                    if (BACK_RIGHT_INDEX !== null && board[BACK_RIGHT_INDEX] === EMPTY) {
                        const [NEW_BOARD, _] = this.getNewBoardFromMove(board, i, BACK_RIGHT_INDEX);
                        this.possibleTurnMovements.push([BACK_RIGHT_INDEX]);
                        this.nextPossibleBoards.push(NEW_BOARD);
                    }
                }
            }
        }
    }

    generateNextJumpBoards(board, piece, isPlayer1, playerPawn, playerRoyal, opponentPawn, opponentRoyal)
    {
        // Calculate forward indexes near piece
        const FWD_LEFT_INDEX = isPlayer1 ? this.northWestGet(piece) : this.southEastGet(piece);
        const FWD_RIGHT_INDEX = isPlayer1 ? this.northEastGet(piece) : this.southWestGet(piece);
        const FWD_LEFT_JUMP_INDEX = isPlayer1 ? this.northWestJumpGet(piece) : this.southEastJumpGet(piece);
        const FWD_RIGHT_JUMP_INDEX = isPlayer1 ? this.northEastJumpGet(piece) : this.southWestJumpGet(piece);
        // Check for a forward left jump
        if (FWD_LEFT_INDEX !== null && FWD_LEFT_JUMP_INDEX !== null &&
            board[FWD_LEFT_JUMP_INDEX] === EMPTY &&
            (board[FWD_LEFT_INDEX] === opponentPawn ||
            board[FWD_LEFT_INDEX] === opponentRoyal))
        {
            // Make move on a new board
            let [newBoard, wasPromoted] = this.getNewBoardFromMove(board, piece, FWD_LEFT_JUMP_INDEX, FWD_LEFT_INDEX);
            // Continue jumping if possible, or if terminal, add the new board
            if (!wasPromoted && this.isJumpPossibleForPiece(newBoard, isPlayer1, FWD_LEFT_JUMP_INDEX, playerRoyal, opponentPawn, opponentRoyal))
            {
                this.transitionMoves.push(FWD_LEFT_JUMP_INDEX);
                this.generateNextJumpBoards(newBoard, FWD_LEFT_JUMP_INDEX, isPlayer1, playerPawn, playerRoyal, opponentPawn, opponentRoyal);
            }
            else
            {
                this.transitionMoves.push(FWD_LEFT_JUMP_INDEX);
                this.possibleTurnMovements.push(Array.from(this.transitionMoves));
                this.nextPossibleBoards.push(newBoard);
                this.transitionMoves = [];
            }
        }
        // Check for a forward right jump
        if (FWD_RIGHT_INDEX !== null && FWD_RIGHT_JUMP_INDEX !== null &&
            board[FWD_RIGHT_JUMP_INDEX] === EMPTY &&
            (board[FWD_RIGHT_INDEX] === opponentPawn ||
            board[FWD_RIGHT_INDEX] === opponentRoyal))
        {
            // Make move on a new board
            let [newBoard, wasPromoted] = this.getNewBoardFromMove(board, piece, FWD_RIGHT_JUMP_INDEX, FWD_RIGHT_INDEX);
            // Continue jumping if possible, or if terminal, add the new board
            if (!wasPromoted && this.isJumpPossibleForPiece(newBoard, isPlayer1, FWD_RIGHT_JUMP_INDEX, playerRoyal, opponentPawn, opponentRoyal))
            {
                this.transitionMoves.push(FWD_RIGHT_JUMP_INDEX);
                this.generateNextJumpBoards(newBoard, FWD_RIGHT_JUMP_INDEX, isPlayer1, playerPawn, playerRoyal, opponentPawn, opponentRoyal);
            }
            else
            {
                this.transitionMoves.push(FWD_RIGHT_JUMP_INDEX);
                this.possibleTurnMovements.push(Array.from(this.transitionMoves));
                this.nextPossibleBoards.push(newBoard);
                this.transitionMoves = [];
            }
        }
        // Check if the piece is a king
        if (board[piece] === playerRoyal)
        {
            // Calculate backward cells near the piece
            const BACK_LEFT_INDEX = isPlayer1 ? this.southWestGet(piece) : this.northEastGet(piece);
            const BACK_RIGHT_INDEX = isPlayer1 ? this.southEastGet(piece) : this.northWestGet(piece);
            const BACK_LEFT_JUMP_INDEX = isPlayer1 ? this.southWestJumpGet(piece) : this.northEastJumpGet(piece);
            const BACK_RIGHT_JUMP_INDEX = isPlayer1 ? this.southEastJumpGet(piece) : this.northWestJumpGet(piece);
            // Check for a back left jump
            if (BACK_LEFT_INDEX !== null && BACK_LEFT_JUMP_INDEX !== null &&
                board[BACK_LEFT_JUMP_INDEX] === EMPTY &&
                (board[BACK_LEFT_INDEX] === opponentPawn ||
                board[BACK_LEFT_INDEX] === opponentRoyal))
            {
                // Make move on a new board
                let [newBoard, wasPromoted] = this.getNewBoardFromMove(board, piece, BACK_LEFT_JUMP_INDEX, BACK_LEFT_INDEX);
                // Continue jumping if possible, or if terminal, add the new board
                if (!wasPromoted && this.isJumpPossibleForPiece(newBoard, isPlayer1, BACK_LEFT_JUMP_INDEX, playerRoyal, opponentPawn, opponentRoyal))
                {
                    this.transitionMoves.push(BACK_LEFT_JUMP_INDEX);
                    this.generateNextJumpBoards(newBoard, BACK_LEFT_JUMP_INDEX, isPlayer1, playerPawn, playerRoyal, opponentPawn, opponentRoyal);
                }
                else
                {
                    this.transitionMoves.push(BACK_LEFT_JUMP_INDEX);
                    this.possibleTurnMovements.push(Array.from(this.transitionMoves));
                    this.nextPossibleBoards.push(newBoard);
                    this.transitionMoves = [];
                }
            }
            // Check for a back right jump
            if (BACK_RIGHT_INDEX !== null && BACK_RIGHT_JUMP_INDEX !== null &&
                board[BACK_RIGHT_JUMP_INDEX] === EMPTY &&
                (board[BACK_RIGHT_INDEX] === opponentPawn ||
                board[BACK_RIGHT_INDEX] === opponentRoyal))
            {
                // Make move on a new board
                let [newBoard, wasPromoted] = this.getNewBoardFromMove(board, piece, BACK_RIGHT_JUMP_INDEX, BACK_RIGHT_INDEX);
                // Continue jumping if possible, or if terminal, add the new board
                if (!wasPromoted && this.isJumpPossibleForPiece(newBoard, isPlayer1, BACK_RIGHT_JUMP_INDEX, playerRoyal, opponentPawn, opponentRoyal))
                {
                    this.transitionMoves.push(BACK_RIGHT_JUMP_INDEX);
                    this.generateNextJumpBoards(newBoard, BACK_RIGHT_JUMP_INDEX, isPlayer1, playerPawn, playerRoyal, opponentPawn, opponentRoyal);
                }
                else
                {
                    this.transitionMoves.push(BACK_RIGHT_JUMP_INDEX);
                    this.possibleTurnMovements.push(Array.from(this.transitionMoves));
                    this.nextPossibleBoards.push(newBoard);
                    this.transitionMoves = [];
                }
            }
        }
    }

    isJumpPossibleForPiece(board, isPlayer1, pieceIndex, playerRoyal, opponentPawn, opponentRoyal)
    {
        // Calculate forward indexes near piece
        let fwdLeftIndex = isPlayer1 ? this.northWestGet(pieceIndex) : this.southEastGet(pieceIndex);
        let fwdRightIndex = isPlayer1 ? this.northEastGet(pieceIndex) : this.southWestGet(pieceIndex);
        let fwdLeftJumpIndex = isPlayer1 ? this.northWestJumpGet(pieceIndex) : this.southEastJumpGet(pieceIndex);
        let fwdRightJumpIndex = isPlayer1 ? this.northEastJumpGet(pieceIndex) : this.southWestJumpGet(pieceIndex);
        // Check for forward left jumps
        if (fwdLeftIndex !== null && fwdLeftJumpIndex !== null &&
            board[fwdLeftJumpIndex] === EMPTY &&
            (board[fwdLeftIndex] === opponentPawn ||
            board[fwdLeftIndex] === opponentRoyal))
        {
            return true;
        }
        // Check for forward right jumps
        if (fwdRightIndex !== null && fwdRightJumpIndex !== null &&
            board[fwdRightJumpIndex] === EMPTY &&
            (board[fwdRightIndex] === opponentPawn ||
            board[fwdRightIndex] === opponentRoyal))
        {
            return true;
        }
        if (board[pieceIndex] === playerRoyal)
        {
            // Calculate backward cells near the piece
            let backLeftIndex = isPlayer1 ? this.southWestGet(pieceIndex) : this.northEastGet(pieceIndex);
            let backRightIndex = isPlayer1 ? this.southEastGet(pieceIndex) : this.northWestGet(pieceIndex);
            let backLeftJumpIndex = isPlayer1 ? this.southWestJumpGet(pieceIndex) : this.northEastJumpGet(pieceIndex);
            let backRightJumpIndex = isPlayer1 ? this.southEastJumpGet(pieceIndex) : this.northWestJumpGet(pieceIndex);
            // Check for back left jumps
            if (backLeftIndex !== null && backLeftJumpIndex !== null &&
                board[backLeftJumpIndex] === EMPTY &&
                (board[backLeftIndex] === opponentPawn ||
                board[backLeftIndex] === opponentRoyal))
            {
                return true;
            }
            // Check for back right jumps
            if (backRightIndex !== null && backRightJumpIndex !== null &&
                board[backRightJumpIndex] === EMPTY &&
                (board[backRightIndex] === opponentPawn ||
                board[backRightIndex] === opponentRoyal))
            {
                return true;
            }
        }
        return false;
    }

    getNewBoardFromMove (board, originIndex, destinationIndex, opponentIndex = null)
    {
        // Move piece to destination.
        let newBoard = board.split("");
        newBoard[destinationIndex] = board[originIndex];
        newBoard[originIndex] = EMPTY;

        // If an opponent was jumped, remove it.
        if (opponentIndex !== null)
            newBoard[opponentIndex] = EMPTY;

        // Check if a pawn reached the last row and promote if necessary.
        let wasPromoted = false;
        if (newBoard[destinationIndex] === MAN && destinationIndex < KING_PROMOTION_MAX)
        {
            newBoard[destinationIndex] = KING;
            wasPromoted = true;
        }
        if (newBoard[destinationIndex] === WOMAN && destinationIndex > QUEEN_PROMOTION_MIN)
        {
            newBoard[destinationIndex] = QUEEN;
            wasPromoted = true;
        }
        return [newBoard.join(""), wasPromoted]
    }

    /// ---
    /// Functions to help find what's near any given piece.
    /// Returns some board index nearby a given piece index.
    /// Returns null if off the board.
    /// ---

    northWestGet(index) {
        // Index visual of the checker board to confirm below.
        //   +  0   +  1   +   2   +  3
        //   4  +   5  +   6   +   7  +
        //   +  8   +  9   +  10   + 11
        //  12  +  13  +  14   +  15  +
        //   + 16   + 17   +  18   + 19
        //  20  +  21  +  22   +  23  +
        //   + 24   + 25   +  26   + 27
        //  28  +  29  +  30   +  31  +
        // Check for topmost cells that have no northWest.
        if (index < 5)
            return null;
        // Check rows that offset toward East, which all have a northWest.
        if ((index % 8) < 4)
        {
            return index - 4;
        }
        // Row is not offset, so after each remaining row beginning, calculate the northWest.
        else
        {
            // Check if row beginning.
            if (index % 4 == 0)
                return null;
            else
                return index - 5;
        }
    }

    northEastGet(index) {
        // Index visual of the checker board to confirm below.
        //   +  0   +  1   +   2   +  3
        //   4  +   5  +   6   +   7  +
        //   +  8   +  9   +  10   + 11
        //  12  +  13  +  14   +  15  +
        //   + 16   + 17   +  18   + 19
        //  20  +  21  +  22   +  23  +
        //   + 24   + 25   +  26   + 27
        //  28  +  29  +  30   +  31  +
        // Check the top row, which has no northEast.
        if (index < 4)
            return null;
        // Check rows that offset toward East, which all, except for end cells, have a northEast.
        if ((index % 8) < 4)
        {
            if (index % 4 == 3)
                return null;
            else
                return index - 3;
        }
        // Row is not offset, so for all cells calculate the northEast.
        else
        {
            return index - 4;
        }
    }

    southWestGet(index) {
        // Index visual of the checker board to confirm below.
        //   +  0   +  1   +   2   +  3
        //   4  +   5  +   6   +   7  +
        //   +  8   +  9   +  10   + 11
        //  12  +  13  +  14   +  15  +
        //   + 16   + 17   +  18   + 19
        //  20  +  21  +  22   +  23  +
        //   + 24   + 25   +  26   + 27
        //  28  +  29  +  30   +  31  +
        // Check for lowest row cells that have no southWest.
        if (index > 27)
            return null;
        // Check rows that offset toward East, which all have a southWest.
        if ((index % 8) < 4)
        {
            return index + 4;
        }
        // Row is not offset, so after each remaining row beginning, calculate the southWest.
        else
        {
            // Check if row beginning.
            if (index % 4 == 0)
                return null;
            else
                return index + 3;
        }
    }

    southEastGet(index) {
        // Index visual of the checker board to confirm below.
        //   +  0   +  1   +   2   +  3
        //   4  +   5  +   6   +   7  +
        //   +  8   +  9   +  10   + 11
        //  12  +  13  +  14   +  15  +
        //   + 16   + 17   +  18   + 19
        //  20  +  21  +  22   +  23  +
        //   + 24   + 25   +  26   + 27
        //  28  +  29  +  30   +  31  +
        // Check near bottom row cells, which have no southEast.
        if (index > 26)
            return null;
        // Check rows that offset toward East, which all, except for end cells, have a southEast.
        if ((index % 8) < 4)
        {
            if (index % 4 == 3)
                return null;
            else
                return index + 5;
        }
        // Row is not offset, so for all cells calculate the southEast.
        else
        {
            return index + 4;
        }
    }

    northWestJumpGet(index) {
        // Index visual of the checker board to confirm below.
        //   +  0   +  1   +   2   +  3
        //   4  +   5  +   6   +   7  +
        //   +  8   +  9   +  10   + 11
        //  12  +  13  +  14   +  15  +
        //   + 16   + 17   +  18   + 19
        //  20  +  21  +  22   +  23  +
        //   + 24   + 25   +  26   + 27
        //  28  +  29  +  30   +  31  +
        // Check for topmost cells or row beginnings, all which have no northWest jump.
        if (index < 9 || index % 4 == 0)
            return null;
        else
            return index - 9;
    }

    northEastJumpGet(index) {
        // Index visual of the checker board to confirm below.
        //   +  0   +  1   +   2   +  3
        //   4  +   5  +   6   +   7  +
        //   +  8   +  9   +  10   + 11
        //  12  +  13  +  14   +  15  +
        //   + 16   + 17   +  18   + 19
        //  20  +  21  +  22   +  23  +
        //   + 24   + 25   +  26   + 27
        //  28  +  29  +  30   +  31  +
        // Check for topmost cells or row ends, all which have no northEast jump.
        if (index < 8 || index % 4 == 3)
            return null;
        else
            return index - 7;
    }

    southWestJumpGet(index) {
        // Index visual of the checker board to confirm below.
        //   +  0   +  1   +   2   +  3
        //   4  +   5  +   6   +   7  +
        //   +  8   +  9   +  10   + 11
        //  12  +  13  +  14   +  15  +
        //   + 16   + 17   +  18   + 19
        //  20  +  21  +  22   +  23  +
        //   + 24   + 25   +  26   + 27
        //  28  +  29  +  30   +  31  +
        // Check for bottom cells or row beginnings, which all have no southWest jump.
        if (index > 23 || index % 4 == 0)
            return null;
        else
            return index + 7;
    }

    southEastJumpGet(index) {
        // Index visual of the checker board to confirm below.
        //   +  0   +  1   +   2   +  3
        //   4  +   5  +   6   +   7  +
        //   +  8   +  9   +  10   + 11
        //  12  +  13  +  14   +  15  +
        //   + 16   + 17   +  18   + 19
        //  20  +  21  +  22   +  23  +
        //   + 24   + 25   +  26   + 27
        //  28  +  29  +  30   +  31  +
        // Check for bottom cells or row ends, which all have no southEast jump.
        if (index > 23 || index % 4 == 3)
            return null;
        else
            return index + 9;
    }

    ///  Helper to console log boards in a checkers pattern.
    getSpecialPattern(board, textRow, x, y)
    {
        let cellIndex = (y * BOARD_WIDTH) + x;
        if (y % 2 == 1)
        {
            textRow.push(board[cellIndex]);
            textRow.push(" ")
        }
        else if (x % BOARD_WIDTH == 0)
        {
            textRow.push(" ");
            textRow.push(board[cellIndex])
            textRow.push(" ");
        }
        else
        {
            textRow.push(board[cellIndex])
            textRow.push(" ");
        }
        return textRow;
    }

    /// An eval function for simulations at a depth limit to guess the game result.
    /// An advantage of more than a pawn predicts a Win, otherwise a Tie.
    /// Returns true for player1 win, false for loss, or null for tie.
    willPlayer1Win(board, isPlayer1)
    {
        const ACTIVE_PAWN = isPlayer1? MAN : WOMAN;
        const ACTIVE_ROYAL = isPlayer1? KING : QUEEN;
        const OPPONENT_PAWN = isPlayer1? WOMAN : MAN;
        const OPPONENT_ROYAL = isPlayer1? QUEEN : KING;

        let activePawnCount = 0;
        let activeRoyalCount = 0;
        let opponentPawnCount = 0;
        let opponentRoyalCount = 0;

        // Count each piece type on board.
        for (let i = 0; i < BOARD_CELL_COUNT; i++)
        {
            if (board[i] === ACTIVE_PAWN)
                activePawnCount++;
            else if (board[i] === ACTIVE_ROYAL)
                activeRoyalCount++;
            else if (board[i] === OPPONENT_PAWN)
                opponentPawnCount++;
            else if (board[i] === OPPONENT_ROYAL)
                opponentRoyalCount++;
        }

        const PAWN_VALUE = 2;
        const ROYAL_VALUE = 3;
        const ACTIVE_PLAYER_SCORE = (PAWN_VALUE * activePawnCount) + (ROYAL_VALUE * activeRoyalCount);
        const OPPONENT_SCORE = (PAWN_VALUE * opponentPawnCount) + (ROYAL_VALUE * opponentRoyalCount);
        if (ACTIVE_PLAYER_SCORE > OPPONENT_SCORE + PAWN_VALUE)
            return ( isPlayer1? true : false );
        else if (OPPONENT_SCORE > ACTIVE_PLAYER_SCORE + PAWN_VALUE)
            return ( isPlayer1? false : true);
        return null;
    }

    /// Returns a prediction between (0,1) for the chance to win on a given board by a given player.
    getPrediction(board, isPlayer1)
    {
        const ACTIVE_PAWN = isPlayer1? MAN : WOMAN;
        const ACTIVE_ROYAL = isPlayer1? KING : QUEEN;
        const OPPONENT_PAWN = isPlayer1? WOMAN : MAN;
        const OPPONENT_ROYAL = isPlayer1? QUEEN : KING;

        let activePawnCount = 0;
        let activeRoyalCount = 0;
        let opponentPawnCount = 0;
        let opponentRoyalCount = 0;

        // Count each piece type on the board.
        for (let i = 0; i < BOARD_CELL_COUNT; i++)
        {
            if (board[i] === ACTIVE_PAWN)
                activePawnCount++;
            else if (board[i] === ACTIVE_ROYAL)
                activeRoyalCount++;
            else if (board[i] === OPPONENT_PAWN)
                opponentPawnCount++;
            else if (board[i] === OPPONENT_ROYAL)
                opponentRoyalCount++;
        }

        /*
            Predictions are based on the idea that a tie game has a 50% chance to win, and any advantage is added to this base.
            Pawns are worth 50% less than royals, and the exact value of each was set by experimentation.

            Under the values given below, winning by 2 pawns results in a prediction of a 66% chance to win, from 50 + (8 * 2).
            Likewise, winning by 1 pawn and 1 royal gives a 70% chance to win, from 50 + 8 + 12.
            Values are clamped between (0,1), so a huge advantage and a grossly huge advantage get the same prediction.
        */

        const PAWN_VALUE = 8;
        const ROYAL_VALUE = 12;
        const ACTIVE_PLAYER_SCORE = (PAWN_VALUE * activePawnCount) + (ROYAL_VALUE * activeRoyalCount);
        const OPPONENT_SCORE = (PAWN_VALUE * opponentPawnCount) + (ROYAL_VALUE * opponentRoyalCount);

        let advantage = 0;
        let prediction = 0;
        if (ACTIVE_PLAYER_SCORE > OPPONENT_SCORE)
        {
            advantage = (ACTIVE_PLAYER_SCORE - OPPONENT_SCORE) + 50;
            prediction = (advantage >= 100) ? 1 - Number.EPSILON : ( advantage / 100);
        }
        else if (OPPONENT_SCORE > ACTIVE_PLAYER_SCORE)
        {
            advantage = (OPPONENT_SCORE - ACTIVE_PLAYER_SCORE) + 50;
            prediction = (advantage >= 100) ? Number.EPSILON : ( 1 - (advantage / 100));
        }
        else
            prediction = 0.5;

        return prediction;
    }

    // Returns the board index of move origin and fills an out-parameter array of movements.
    deriveMovements(lastBoard, nextBoard, isPlayer1, movements)
    {
        const ACTIVE_PAWN = isPlayer1? MAN : WOMAN;
        const ACTIVE_ROYAL = isPlayer1? KING : QUEEN;

        let origin = null;

        // Find each piece on the board belonging to the active player.
        // If any piece is not on the next board (at the same index), it's the origin.
        // If all pieces are in the same place, then a piece jumped in a circle.
        // In this case, the origin is the same as the final destination.

        // Try to find the origin.
        for (let i = 0; i < lastBoard.length; i++)
        {
            if (lastBoard[i] === ACTIVE_PAWN || lastBoard[i] === ACTIVE_ROYAL)
            {
                if (lastBoard[i] !== nextBoard[i])
                {
                    origin = i;
                    break;
                }
            }
        }

        // Find all movements, and if moves were circular, the last move is also the origin.
        for (const [INDEX, BOARD] of this.nextPossibleBoards.entries())
        {
            if (BOARD === nextBoard)
            {
                if (origin === null)
                {
                    origin = this.possibleTurnMovements[INDEX][this.possibleTurnMovements.length-1];  // .at(-1); is cool, but it's only supported JS since 2022.
                }
                // Note that this.nextPossibleBoards and this.nextPossibleTurnMovements are in the same order,
                // so the index of next boards matches the index of next moves.
                // Shallow copy possible movements into the movements array.
                // Since this is an out-parameter, iterate and push each move, rather than assign (point) to a new array.
                for (const MOVE of this.possibleTurnMovements[INDEX])
                {
                    movements.push(MOVE);
                }
                break;
            }
        }
        return origin;
    }

} // End class


/***/ }),

/***/ 706:
/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {

/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */   u: () => (/* binding */ TicTacToeRules)
/* harmony export */ });
/* harmony import */ var _winner_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(710);

/// Tic Tac Toe



const player1Mark = 'X'
const player2Mark = 'O'
const EMPTY = '-'
const BOARD_WIDTH = 3;
const BOARD_HEIGHT = 3;
const BOARD_CELL_COUNT = 9;
const HAS_SPECIAL_PATTERN = false;  // For board logs. False defaults to a grid.

class TicTacToeRules
{
    constructor()
    {
        this.nextPossibleBoards = []
        this.winner = new _winner_js__WEBPACK_IMPORTED_MODULE_0__/* .Winner */ .k();
    }

    getNewBoard(initialBoard)
    {
        const BOARD = (initialBoard === null)? "---------" : initialBoard;
        return [BOARD, BOARD_HEIGHT, BOARD_WIDTH, HAS_SPECIAL_PATTERN];
    }

    hasGeneratedNextPossibleStates(board, isPlayer1)
    {
        // Clear winner from any previous game / simulation.
        this.winner.logName = null;
        this.winner.isPlayer1 = null;

        if (this.isWon(board, isPlayer1))
        {
            // Whoever played last won.
            // So the winner is the opposite.
            this.winner.isPlayer1 = !isPlayer1;
            this.winner.logName = isPlayer1? player2Mark: player1Mark;
            return false;
        }
        else
        {
            if (this.isMovePossible(board))
            {
                this.nextPossibleBoards = [];
                this.pushAllPossibleBoards(board, isPlayer1);
                return true;
            }
            return false;
        }
    }

    isMovePossible(board)
    {
        for (const cell of board)
        {
            if (cell === EMPTY)
                return true;
        }
        return false;
    }

    pushAllPossibleBoards(board, isPlayer1)
    {
        let playerMark = isPlayer1? player1Mark : player2Mark;
        for (let index = 0; index < BOARD_CELL_COUNT; index++)
        {
            if (board[index] === EMPTY)
            {
                let newBoard = board.split("");
                newBoard[index] = playerMark;
                newBoard = newBoard.join("");
                this.nextPossibleBoards.push(newBoard);
            }
        }
    }

    // Check if whoever played last won.
    isWon(board, isPlayer1)
    {
        let opponentMark = isPlayer1? player2Mark : player1Mark;
        return (
            ((board[0] ===   opponentMark) && (board[1] ===   opponentMark) && (board[2] ===   opponentMark)) || // Top row
            ((board[3] ===   opponentMark) && (board[4] ===   opponentMark) && (board[5] ===   opponentMark)) || // Center row
            ((board[6] ===   opponentMark) && (board[7] ===   opponentMark) && (board[8] ===   opponentMark)) || // Bottom row
            ((board[0] ===   opponentMark) && (board[3] ===   opponentMark) && (board[6] ===   opponentMark)) || // Left column
            ((board[1] ===   opponentMark) && (board[4] ===   opponentMark) && (board[7] ===   opponentMark)) || // Center column
            ((board[2] ===   opponentMark) && (board[5] ===   opponentMark) && (board[8] ===   opponentMark)) || // Right column
            ((board[0] ===   opponentMark) && (board[4] ===   opponentMark) && (board[8] ===   opponentMark)) || // Diagonal down
            ((board[2] ===   opponentMark) && (board[4] ===   opponentMark) && (board[6] ===   opponentMark)));  // Diagonal up
    }

    /// Don't expect to use these for TicTacToe, but if a PUCT agent calls, it'll cause no harm.

    willPlayer1Win(board, isPlayer1)
    {
        return null;
    }

    getPrediction(board, isPlayer1)
    {
        return 0;
    }
}


/***/ }),

/***/ 707:
/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {

/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */   Z: () => (/* binding */ Game)
/* harmony export */ });
/* harmony import */ var _game_rules_checkers_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(512);
/* harmony import */ var _game_rules_tictactoe_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(706);



class Game
{
    constructor(gameName, initialBoard = null)
    {
        this.logName = gameName;
        this.name = gameName.toLowerCase();
        this.hasNextState = true;
        this.isDone = false;
        this.lastBoard = null;
        switch (this.name)
        {
            case "tictactoe":
                console.log("Constructing Tic Tac Toe.");
                this.rules = new _game_rules_tictactoe_js__WEBPACK_IMPORTED_MODULE_0__/* .TicTacToeRules */ .u();
                [this.board, this.boardHeight, this.boardWidth,
                 this.hasSpecialPattern] = this.rules.getNewBoard(initialBoard);
                break;
            case "checkers":
                console.log("Constructing Checkers.");
                this.rules = new _game_rules_checkers_js__WEBPACK_IMPORTED_MODULE_1__/* .CheckersRules */ .Y();
                [this.board, this.boardHeight, this.boardWidth,
                 this.hasSpecialPattern] = this.rules.getNewBoard(initialBoard);
                break;
            default:
                console.error("Error: invalid game.")
                break;
        }
    }

    /// Console log board from top (index 0) to bottom.
    logBoard()
    {
        let textRow = [];
        for (let y = 0; y < this.boardHeight; y++)
        {
            for (let x = 0; x < this.boardWidth; x++)
            {
                if (this.hasSpecialPattern)
                {
                    textRow = this.rules.getSpecialPattern(this.board, textRow, x, y);
                }
                else
                {
                    let cellIndex = (y * this.boardWidth) + x;
                    textRow.push(" ");
                    textRow.push(this.board[cellIndex])
                    textRow.push(" ");
                }
            }
            console.log(textRow.join(""));
            textRow = [];
        }
    }

    hasWinner()
    {
        return this.rules.winner.isPlayer1 !== null;
    }

    isOver()
    {
        return !this.hasNextState;
    }
}


/***/ }),

/***/ 173:
/***/ ((__webpack_module__, __unused_webpack___webpack_exports__, __webpack_require__) => {

__webpack_require__.a(__webpack_module__, async (__webpack_handle_async_dependencies__, __webpack_async_result__) => { try {
/* harmony import */ var _setup_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(560);
/* harmony import */ var _agent_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(442);
/* harmony import */ var _game_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(707);


const INITIAL_BOARD = convertGBE_BoardToLocalBoard(gameInfo.data[0]); // await Foundation.$registry[7].info.board
let game = new _game_js__WEBPACK_IMPORTED_MODULE_2__/* .Game */ .Z(_setup_js__WEBPACK_IMPORTED_MODULE_0__/* .SETUP */ .K.GAME_TO_PLAY, INITIAL_BOARD);
let agent = new _agent_js__WEBPACK_IMPORTED_MODULE_1__/* .Agent */ .g(_setup_js__WEBPACK_IMPORTED_MODULE_0__/* .SETUP */ .K.AGENT_0);
let isPlayer1 = (gameInfo.data[1])? true : false;  // Foundation.$registry[7].perspectiveColor === 0
await agent.begin(game, isPlayer1);
await agent.continue();
console.log("Turn complete.");
close();

/// Helper functions below.

function convertGBE_BoardToLocalBoard(board) {
    let localBoard = []
    for (let i = 0; i < 32; i++)
    {
        let index = Math.abs(i-31)
        if (index % 4 === 3)
        {
            index -= 3;
        }
        else if (index % 4 === 2)
        {
            index -= 1;
        }
         else if (index % 4 === 1)
        {
            index += 1;
        }
        else
        {
            index += 3;
        }
        localBoard.push(convertGBE_SymbolToLocalSymbol(board[index]));
    }
    return localBoard.join("");
}

function convertGBE_SymbolToLocalSymbol(symbol)
{
    switch(symbol)
    {
        case ' ':
            return '+'
        case 'r':
            return 'M'
        case 'w':
            return 'W'
        case 'R':
            return 'K'
        case 'W':
            return 'Q'
    }
}

__webpack_async_result__();
} catch(e) { __webpack_async_result__(e); } }, 1);

/***/ }),

/***/ 560:
/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {

/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */   K: () => (/* binding */ SETUP)
/* harmony export */ });
/// Setup file for tournaments and agents.
const SETUP = {

    // Tournament
    AGENT_0 : "MCTS-PUCT", // Use Random, MCTS-UCT, MCTS-UCT-ENHANCED, MCTS-PUCT, or MCTS-PUCT-NET.
    AGENT_1 : "MCTS-PUCT",
    GAME_TO_PLAY : "Checkers", // Expects Checkers or TicTacToe.
    SPECIFY_INITIAL_BOARD: null, // Expects null (default) or board string. Example TicTacToe string: "X--OO-X--"
    SHOULD_ALTERNATE_PLAY_ORDER : true, // Per game, switch sides.
    MAX_TURNS_PER_GAME : 1, // Should be >=1 turn.
    TOURNAMENT_LENGTH : 1, // Should be >= 1 game.

    // For all (non-random) agents
    SEARCH_TIME : 1800, // In milliseconds: 1000 == 1 second. If debugging with break points, set to NUMBER.MAX_VALUE.
    MAX_ITERATIONS : Number.MAX_VALUE, // If using break points, set this, not time.
    UCB_FORMULA_CONSTANT : 2, // Controls exploit-explore ratio, where 0 is greedy.

    // MCTS-UCT Enhanced: Depth-Limited Evaluation & Tree Size Limits
    TREE_DEPTH_LIMIT: 18, // For no limit, use Number.MAX_VALUE;
    SIMULATION_DEPTH_LIMIT: 4, // Research says for many games, 4-8 is ideal.
    ROOT_DEPTH_1_CHILD_CAPACITY: 64,
    NODE_DEPTH_2_CHILD_CAPACITY: 8,
    NODE_GENERAL_CHILD_CAPACITY: 8,

    // MCTS-PUCT: same controls
    PUCT_TREE_DEPTH_LIMIT: 18,
    PUCT_SIMULATION_DEPTH_LIMIT: 4,
    PUCT_ROOT_DEPTH_1_CHILD_CAPACITY: 64,
    PUCT_NODE_DEPTH_2_CHILD_CAPACITY: 12,
    PUCT_NODE_GENERAL_CHILD_CAPACITY: 8,

    // MCTS-PUCT-NET
    // NETWORK_PATH: "./network/checkers_net_sim-based_4-5-2024_1601-10m.json",
    // HIDDEN_LAYERS: [36, 16, 4],
    // PUCT_NET_TREE_DEPTH_LIMIT: 18,
    // PUCT_NET_SIMULATION_DEPTH_LIMIT: 5,
    // PUCT_NET_ROOT_DEPTH_1_CHILD_CAPACITY: 64,
    // PUCT_NET_NODE_DEPTH_2_CHILD_CAPACITY: 12,
    // PUCT_NET_NODE_GENERAL_CHILD_CAPACITY: 8,

    // Rewards: expects positive numbers
    REWARD : {
        TIE :  1,
        WIN :  2
    }
}

// export async function NeuralNet()
// {
//     return await (fetch(SETUP.NETWORK_PATH)
//         .then(response => response.json())
//             .then(async data =>  {
//                 await import("./network/browser.js"); // To import from the web, use: import("https://cdn.rawgit.com/BrainJS/brain.js/45ce6ffc/browser.js");
//                 let net = new brain.NeuralNetwork({ inputSize: 33, hiddenLayers: SETUP.HIDDEN_LAYERS, outputSize: 1, activation: 'relu' });
//                 net.fromJSON(data);
//                 return net;
//         }).catch(error => console.error(error))
//     );
// }


/***/ }),

/***/ 710:
/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {

/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */   k: () => (/* binding */ Winner)
/* harmony export */ });
class Winner
{
    constructor()
    {
        this.logName = null;
        this.isPlayer1 = null;
    }
}

/***/ })

/******/ 	});
/************************************************************************/
/******/ 	// The module cache
/******/ 	var __webpack_module_cache__ = {};
/******/
/******/ 	// The require function
/******/ 	function __webpack_require__(moduleId) {
/******/ 		// Check if module is in cache
/******/ 		var cachedModule = __webpack_module_cache__[moduleId];
/******/ 		if (cachedModule !== undefined) {
/******/ 			return cachedModule.exports;
/******/ 		}
/******/ 		// Create a new module (and put it into the cache)
/******/ 		var module = __webpack_module_cache__[moduleId] = {
/******/ 			// no module.id needed
/******/ 			// no module.loaded needed
/******/ 			exports: {}
/******/ 		};
/******/
/******/ 		// Execute the module function
/******/ 		__webpack_modules__[moduleId](module, module.exports, __webpack_require__);
/******/
/******/ 		// Return the exports of the module
/******/ 		return module.exports;
/******/ 	}
/******/
/************************************************************************/
/******/ 	/* webpack/runtime/async module */
/******/ 	(() => {
/******/ 		var webpackQueues = typeof Symbol === "function" ? Symbol("webpack queues") : "__webpack_queues__";
/******/ 		var webpackExports = typeof Symbol === "function" ? Symbol("webpack exports") : "__webpack_exports__";
/******/ 		var webpackError = typeof Symbol === "function" ? Symbol("webpack error") : "__webpack_error__";
/******/ 		var resolveQueue = (queue) => {
/******/ 			if(queue && queue.d < 1) {
/******/ 				queue.d = 1;
/******/ 				queue.forEach((fn) => (fn.r--));
/******/ 				queue.forEach((fn) => (fn.r-- ? fn.r++ : fn()));
/******/ 			}
/******/ 		}
/******/ 		var wrapDeps = (deps) => (deps.map((dep) => {
/******/ 			if(dep !== null && typeof dep === "object") {
/******/ 				if(dep[webpackQueues]) return dep;
/******/ 				if(dep.then) {
/******/ 					var queue = [];
/******/ 					queue.d = 0;
/******/ 					dep.then((r) => {
/******/ 						obj[webpackExports] = r;
/******/ 						resolveQueue(queue);
/******/ 					}, (e) => {
/******/ 						obj[webpackError] = e;
/******/ 						resolveQueue(queue);
/******/ 					});
/******/ 					var obj = {};
/******/ 					obj[webpackQueues] = (fn) => (fn(queue));
/******/ 					return obj;
/******/ 				}
/******/ 			}
/******/ 			var ret = {};
/******/ 			ret[webpackQueues] = x => {};
/******/ 			ret[webpackExports] = dep;
/******/ 			return ret;
/******/ 		}));
/******/ 		__webpack_require__.a = (module, body, hasAwait) => {
/******/ 			var queue;
/******/ 			hasAwait && ((queue = []).d = -1);
/******/ 			var depQueues = new Set();
/******/ 			var exports = module.exports;
/******/ 			var currentDeps;
/******/ 			var outerResolve;
/******/ 			var reject;
/******/ 			var promise = new Promise((resolve, rej) => {
/******/ 				reject = rej;
/******/ 				outerResolve = resolve;
/******/ 			});
/******/ 			promise[webpackExports] = exports;
/******/ 			promise[webpackQueues] = (fn) => (queue && fn(queue), depQueues.forEach(fn), promise["catch"](x => {}));
/******/ 			module.exports = promise;
/******/ 			body((deps) => {
/******/ 				currentDeps = wrapDeps(deps);
/******/ 				var fn;
/******/ 				var getResult = () => (currentDeps.map((d) => {
/******/ 					if(d[webpackError]) throw d[webpackError];
/******/ 					return d[webpackExports];
/******/ 				}))
/******/ 				var promise = new Promise((resolve) => {
/******/ 					fn = () => (resolve(getResult));
/******/ 					fn.r = 0;
/******/ 					var fnQueue = (q) => (q !== queue && !depQueues.has(q) && (depQueues.add(q), q && !q.d && (fn.r++, q.push(fn))));
/******/ 					currentDeps.map((dep) => (dep[webpackQueues](fnQueue)));
/******/ 				});
/******/ 				return fn.r ? promise : getResult();
/******/ 			}, (err) => ((err ? reject(promise[webpackError] = err) : outerResolve(exports)), resolveQueue(queue)));
/******/ 			queue && queue.d < 0 && (queue.d = 0);
/******/ 		};
/******/ 	})();
/******/
/******/ 	/* webpack/runtime/define property getters */
/******/ 	(() => {
/******/ 		// define getter functions for harmony exports
/******/ 		__webpack_require__.d = (exports, definition) => {
/******/ 			for(var key in definition) {
/******/ 				if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
/******/ 					Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });
/******/ 				}
/******/ 			}
/******/ 		};
/******/ 	})();
/******/
/******/ 	/* webpack/runtime/hasOwnProperty shorthand */
/******/ 	(() => {
/******/ 		__webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))
/******/ 	})();
/******/
/************************************************************************/
/******/
/******/ 	// startup
/******/ 	// Load entry module and return exports
/******/ 	// This entry module used 'module' so it can't be inlined
/******/ 	var __webpack_exports__ = __webpack_require__(173);
/******/
/******/ })()
}} // End WEBPACK_SCRIPT_OBJ

// Run web-pack script in a worker thread.
let blob = new Blob(["onmessage = " + WEBPACK_SCRIPT_OBJ.f], {type: 'text/javascript'});
let blobURL = URL.createObjectURL(blob);
let worker = new unsafeWindow.Worker(blobURL);
URL.revokeObjectURL(blobURL);

// Add handler to receive worker messages.
worker.onmessage = (chosenMoveInputs) => {
  inputMovements(chosenMoveInputs.data[0], chosenMoveInputs.data[1]);
};

// Send message to worker: info on board and player.
setTimeout(function () {worker.postMessage([Foundation.$registry[7].info.board, (Foundation.$registry[7].perspectiveColor === 0)])}, delay);
} // End condition: isKomputerReady
} // End function runKomputer.