From 202193b0ac877dcf9cf741b194e5806ef36fc54f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Max=20M=C3=B6nch?= Date: Sun, 25 Jan 2026 23:02:20 +0100 Subject: [PATCH 1/2] add tests --- package-lock.json | 197 ++++++++++++++++++++++++++ package.json | 3 +- src/models/gamestate.ts | 12 +- test/models/gamestate.test.ts | 254 ++++++++++++++++++++++++++++++++++ tsconfig.json | 4 +- 5 files changed, 458 insertions(+), 12 deletions(-) create mode 100644 test/models/gamestate.test.ts diff --git a/package-lock.json b/package-lock.json index 6db03e2..23e7587 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "devDependencies": { "@types/node": "^22.19.7", "0x": "^6.0.0", + "rimraf": "^6.1.2", "tsx": "^4.7.0" } }, @@ -460,6 +461,29 @@ "node": ">=18" } }, + "node_modules/@isaacs/balanced-match": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", + "integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@isaacs/brace-expansion": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz", + "integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@isaacs/balanced-match": "^4.0.1" + }, + "engines": { + "node": "20 || >=22" + } + }, "node_modules/@types/node": { "version": "22.19.7", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.7.tgz", @@ -2532,6 +2556,16 @@ "dev": true, "license": "MIT" }, + "node_modules/lru-cache": { + "version": "11.2.4", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.4.tgz", + "integrity": "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, "node_modules/magic-string": { "version": "0.23.2", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.23.2.tgz", @@ -2658,6 +2692,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/mkdirp-classic": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", @@ -2922,6 +2966,13 @@ "dev": true, "license": "MIT" }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, "node_modules/pako": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", @@ -2990,6 +3041,23 @@ "node": ">= 0.8.0" } }, + "node_modules/path-scurry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.1.tgz", + "integrity": "sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/pbkdf2": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.5.tgz", @@ -3239,6 +3307,60 @@ "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" } }, + "node_modules/rimraf": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.1.2.tgz", + "integrity": "sha512-cFCkPslJv7BAXJsYlK1dZsbP8/ZNLkCAQ0bi1hf5EKX2QHegmDFEFA6QhuYJlk7UDdc+02JjO80YSOrWPpw06g==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "glob": "^13.0.0", + "package-json-from-dist": "^1.0.1" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/glob": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.0.tgz", + "integrity": "sha512-tvZgpqk6fz4BaNZ66ZsRaZnbHvP/jG3uKJvAZOwEVUL4RTA5nJeeLYfyN9/VA8NX/V3IBG+hkeuGpKjvELkVhA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "minimatch": "^10.1.1", + "minipass": "^7.1.2", + "path-scurry": "^2.0.0" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/minimatch": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz", + "integrity": "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/brace-expansion": "^5.0.0" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/ripemd160": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.3.tgz", @@ -4212,6 +4334,21 @@ "dev": true, "optional": true }, + "@isaacs/balanced-match": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", + "integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==", + "dev": true + }, + "@isaacs/brace-expansion": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz", + "integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==", + "dev": true, + "requires": { + "@isaacs/balanced-match": "^4.0.1" + } + }, "@types/node": { "version": "22.19.7", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.7.tgz", @@ -5815,6 +5952,12 @@ "integrity": "sha512-2Fgx1Ycm599x+WGpIYwJOvsjmXFzTSc34IwDWALRA/8AopUKAVPwfJ+h5+f85BCp0PWmmJcWzEpxOpoXycMpdA==", "dev": true }, + "lru-cache": { + "version": "11.2.4", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.4.tgz", + "integrity": "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==", + "dev": true + }, "magic-string": { "version": "0.23.2", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.23.2.tgz", @@ -5912,6 +6055,12 @@ "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", "dev": true }, + "minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true + }, "mkdirp-classic": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", @@ -6118,6 +6267,12 @@ "integrity": "sha512-gjcpUc3clBf9+210TRaDWbf+rZZZEshZ+DlXMRCeAjp0xhTrnQsKHypIy1J3d5hKdUzj69t708EHtU8P6bUn0A==", "dev": true }, + "package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true + }, "pako": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", @@ -6170,6 +6325,16 @@ "integrity": "sha512-Y30dB6rab1A/nfEKsZxmr01nUotHX0c/ZiIAsCTatEe1CmS5Pm5He7fZ195bPT7RdquoaL8lLxFCMQi/bS7IJg==", "dev": true }, + "path-scurry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.1.tgz", + "integrity": "sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==", + "dev": true, + "requires": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + } + }, "pbkdf2": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.5.tgz", @@ -6366,6 +6531,38 @@ "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", "dev": true }, + "rimraf": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.1.2.tgz", + "integrity": "sha512-cFCkPslJv7BAXJsYlK1dZsbP8/ZNLkCAQ0bi1hf5EKX2QHegmDFEFA6QhuYJlk7UDdc+02JjO80YSOrWPpw06g==", + "dev": true, + "requires": { + "glob": "^13.0.0", + "package-json-from-dist": "^1.0.1" + }, + "dependencies": { + "glob": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.0.tgz", + "integrity": "sha512-tvZgpqk6fz4BaNZ66ZsRaZnbHvP/jG3uKJvAZOwEVUL4RTA5nJeeLYfyN9/VA8NX/V3IBG+hkeuGpKjvELkVhA==", + "dev": true, + "requires": { + "minimatch": "^10.1.1", + "minipass": "^7.1.2", + "path-scurry": "^2.0.0" + } + }, + "minimatch": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz", + "integrity": "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==", + "dev": true, + "requires": { + "@isaacs/brace-expansion": "^5.0.0" + } + } + } + }, "ripemd160": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.3.tgz", diff --git a/package.json b/package.json index 7dcf511..01b9157 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "cli": "node dist/cli/index.js", "cli:dev": "npx tsx src/cli/index.ts", "test": "node --import tsx --test 'test/**/*.test.ts'", - "build": "npx tsc", + "build": "rimraf dist && tsc", "build:c": "gcc -Wall -Wextra -std=c99 -lm performance/single-run.c -o performance/single-run", "build:wasm": "emcc -Wall -Wextra -std=c99 -O3 -s WASM=1 -s MODULARIZE=1 -s EXPORT_ES6=1 -s EXPORTED_RUNTIME_METHODS='[\"UTF8ToString\",\"HEAP32\",\"_malloc\",\"_free\"]' -s EXPORT_NAME='createModule' performance/single-run-wasm.c -o performance/single-run-wasm.js -lm", "test:performance": "npx tsx performance/single-run.ts", @@ -28,6 +28,7 @@ "devDependencies": { "@types/node": "^22.19.7", "0x": "^6.0.0", + "rimraf": "^6.1.2", "tsx": "^4.7.0" }, "repository": { diff --git a/src/models/gamestate.ts b/src/models/gamestate.ts index a24900a..c4a9dc2 100644 --- a/src/models/gamestate.ts +++ b/src/models/gamestate.ts @@ -155,12 +155,6 @@ export class GameState { const id = arr[step].id; if (this.getById(id).state === 0) { - console.table( - this.gameField.map((row) => row.map((field) => field.state)), - ); - console.table( - this.gameField.map((row) => row.map((field) => field.id)), - ); const arr = this.getFromSide(i, orientation); throw new Error(`stone with id ${id} is not empty`); } @@ -252,9 +246,9 @@ export class GameState { // TODO: check if move is allowed if (!this.isValidMove(arr, takeNumber)) { - console.table( - this.gameField.map((row) => row.map((field) => field.state)), - ); + // console.table( + // this.gameField.map((row) => row.map((field) => field.state)), + // ); throw Error( `invalid move with id ${arr[takeNumber].id}, row ${row}, column ${column}, orientation ${convertOrientation(orientation)}`, ); diff --git a/test/models/gamestate.test.ts b/test/models/gamestate.test.ts new file mode 100644 index 0000000..6992e2a --- /dev/null +++ b/test/models/gamestate.test.ts @@ -0,0 +1,254 @@ +import assert from "node:assert"; +import { describe, it } from "node:test"; +import { GameState } from "../../dist/models/gamestate"; +import { Orientation } from "../../dist/models/interfaces"; + +const orientations = [ + Orientation.TOP, + Orientation.BOTTOM, + Orientation.LEFT, + Orientation.RIGHT, +]; + +describe("GameState", () => { + describe("initialization", () => { + it("should be able to create a new game state", () => { + const gameState = new GameState(); + assert.ok(gameState, "gameState should be defined"); + }); + + describe("invalid initialization", () => { + it("should throw an error if the size is 0", () => { + const gameState = new GameState(); + assert.throws( + () => gameState.init({ size: [0, 0] }), + "should throw an error if the size is 0x0", + ); + }); + }); + }); + + it("should be able to initialize a game state with different sizes", () => { + const gameState = new GameState(); + for (let row = 0; row < 10; row++) { + for (let col = 0; col < 10; col++) { + gameState.init({ size: [row, col] }); + assert.strictEqual( + gameState.rows.length, + row, + `gameState should have ${row} rows`, + ); + assert.strictEqual( + gameState.columns.length, + col, + `gameState should have ${col} columns`, + ); + } + } + }); +}); + +describe("making moves", () => { + describe("invalid moves", () => { + it("should not be able to take a stone from different sides by row, column indices and orientation in an finished game", () => { + for (let orientation of orientations) { + const gameState = new GameState(); + gameState.init({ size: [1, 1] }); + assert.throws( + () => gameState.take(0, 0, orientation), + "should throw an error", + ); + } + }); + + it("should not be able to take a stone from different sides by id and orientation in a finished game", () => { + for (let orientation of orientations) { + const gameState = new GameState(); + gameState.init({ size: [1, 1] }); + assert.throws( + () => gameState.takeById(0, orientation), + "should throw an error", + ); + } + }); + }); + + describe("valid moves", () => { + it("player should change after a valid move", () => { + const gameState = new GameState(); + gameState.init({ size: [2, 1] }); + assert.strictEqual(gameState.getCurrentPlayer(), 0); + gameState.take(0, 0, Orientation.LEFT); + assert.strictEqual(gameState.getCurrentPlayer(), 1); + }); + + it("player should not change after an invalid move", () => { + const gameState = new GameState(); + gameState.init({ size: [1, 1] }); + assert.strictEqual(gameState.getCurrentPlayer(), 0); + assert.throws( + () => gameState.take(0, 0, Orientation.LEFT), + "should throw an error", + ); + assert.strictEqual(gameState.getCurrentPlayer(), 0); + }); + + it("finishing the game should change the current player to the losing player and the game should be finished", () => { + let gameState = new GameState(); + gameState.init({ size: [2, 1] }); + + const validMoves = gameState.getAllValidMoves(); + assert.strictEqual(validMoves.length, 6, "should have 6 valid moves"); + + for (let move of validMoves) { + // reset game state + gameState = new GameState(); + gameState.init({ size: [2, 1] }); + + assert.strictEqual( + gameState.getCurrentPlayer(), + 0, + "player 0 should be the current player", + ); + + assert.strictEqual( + gameState.isFinished(), + false, + "game should not be finished", + ); + + gameState.takeById(move.id, move.orientation); + + assert.strictEqual( + gameState.getCurrentPlayer(), + 1, + "player 1 should be the current player", + ); + + assert.strictEqual( + gameState.isFinished(), + true, + "game should be finished", + ); + } + }); + }); + + describe("validating moves", () => { + describe("invalid moves", () => { + it("should be able to validate a move by row, column indices and orientation", () => { + const gameState = new GameState(); + gameState.init({ size: [1, 1] }); + + for (let orientation of orientations) { + assert.strictEqual( + gameState.isValidMove(0, 0, orientation), + false, + `${Orientation[orientation]} should be invalid`, + ); + } + }); + + it("should be able to validate a move by id and orientation", () => { + const gameState = new GameState(); + gameState.init({ size: [1, 1] }); + + for (let orientation of orientations) { + assert.strictEqual( + gameState.isValidMoveByIndex(0, orientation), + false, + `${Orientation[orientation]} should be invalid`, + ); + } + }); + }); + }); + + describe("utility functions", () => { + describe(".getAllValidMoves", () => { + it("should not return any valid moves in a finished game", () => { + const gameState = new GameState(); + gameState.init({ size: [1, 1] }); + assert.strictEqual( + gameState.getAllValidMoves().length, + 0, + "should have 0 valid moves", + ); + }); + + it("should return all valid moves in an brand new game", () => { + const gameState = new GameState(); + gameState.init({ size: [2, 2] }); + + assert.strictEqual( + gameState.getAllValidMoves().length, + 16, + "should have 16 valid moves", + ); + }); + }); + + describe(".getValidMoves", () => { + it("should not return any valid moves in a finished game", () => { + const gameState = new GameState(); + gameState.init({ size: [1, 1] }); + + for (let orientation of orientations) { + assert.strictEqual( + gameState.getValidMoves(orientation).length, + 0, + `${Orientation[orientation]} should have 0 valid moves`, + ); + } + }); + + it("should return all valid moves in an brand new game", () => { + const gameState = new GameState(); + gameState.init({ size: [2, 2] }); + + for (let orientation of orientations) { + assert.strictEqual( + gameState.getValidMoves(orientation).length, + 4, + `${Orientation[orientation]} should have 4 valid moves`, + ); + } + }); + }); + + describe(".clone", () => { + it("should create a deep copy of the game state", () => { + const gameState = new GameState(); + + // make it easy to test by initializing with a small size + gameState.init({ size: [1, 1] }); + + // clone the game state + const clone = gameState.clone(); + + assert.notStrictEqual( + gameState, + clone, + "clone should create a new game state object", + ); + + // check if the clone has the same attributes as the game state + const attributes = ["rows", "columns"]; + for (let attribute of attributes) { + assert.notStrictEqual( + gameState[attribute], + clone[attribute], + `clone should create a new [object] or [array] ${attribute} `, + ); + } + + // check if fields are deep copied + assert.notStrictEqual( + gameState.getById(0), + clone.getById(0), + "clone should create a new field object", + ); + }); + }); + }); +}); diff --git a/tsconfig.json b/tsconfig.json index 40c2d8c..5491789 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -65,6 +65,6 @@ "skipLibCheck": true /* Skip type checking of declaration files. */, "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ }, - "include": ["src/**/*.ts", "test/solver/mcts.test.ts"], - "exclude": ["performance/**/*", "playground.ts", "playground.js", "dist/**/*"] + "include": ["src/**/*.ts"], + "exclude": ["performance/**/*", "playground.ts", "playground.js", "dist/**/*", "test/**/*"] } From 94d7053be5827142ef8c5f019312308cc363040f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Max=20M=C3=B6nch?= Date: Sun, 25 Jan 2026 23:30:49 +0100 Subject: [PATCH 2/2] adding debug mode --- src/ai/solver/mcts.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/ai/solver/mcts.ts b/src/ai/solver/mcts.ts index 5865e2c..60ac543 100644 --- a/src/ai/solver/mcts.ts +++ b/src/ai/solver/mcts.ts @@ -1,5 +1,6 @@ import { Move, Solver } from './solver.js'; import { GameState } from '../../models/gamestate.js'; +import { convertOrientation } from '../utils/utils.js'; export class MCTSSolver implements Solver { readonly name = 'MCTS Solver'; @@ -240,6 +241,17 @@ export class MCTS { return null; } + // helps to understand the decision making process + if(process.env.ALGO_DEBUG){ + console.log("MCTS: uct values"); + console.table(this.root.children.map((c) => ({ + move: `${c.move.id} ${convertOrientation(c.move.orientation)}`, + uct: c.uctValue, + visits: c.visits, + wins: c.wins, + })).sort((a, b) => b.uct - a.uct)); + } + // find the child with the most visits const best = this.root.children.reduce((a, b) => a.visits > b.visits ? a : b,