您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
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.