diff --git a/.eslintrc.js b/.eslintrc.js index a1c9ab6b..b8a3ca97 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -94,7 +94,7 @@ module.exports = { rules: { "file-header": [ true, - "([Cc]opyright ([(][Cc][)])?\\s*[Oo]nline-[gG]o.com)|(bin/env)", // cspell: disable-line + "([Cc]opyright ([(][Cc][)]))|(bin/env)", // cspell: disable-line ], "import-spacing": true, "whitespace": [ diff --git a/.vscode/cspell.json b/.vscode/cspell.json index 051fea6d..67c68ab8 100644 --- a/.vscode/cspell.json +++ b/.vscode/cspell.json @@ -1,5 +1,6 @@ { "words": [ + "aabc", "abcdefghjklmnopqrstuvwxyz", "AILR", "aireview", @@ -61,6 +62,7 @@ "goban", "Gobans", "goquest", + "goscorer", "groupify", "Gulpfile", "hane", @@ -89,6 +91,7 @@ "kyus", "Leela", "lerp", + "lightvector", "linebreak", "localstorage", "malk", @@ -140,6 +143,7 @@ "shownotesindicator", "Sitewide", "slowstrobe", + "snapbacks", "sodos", "stdev", "styl", @@ -170,6 +174,7 @@ "unhighlight", "unitify", "Unranked", + "unscorable", "unstarted", "uservoice", "usgc", @@ -188,6 +193,8 @@ ], "language": "en,en-GB", "ignorePaths": [ - "test/autoscore_test_files" + "test/autoscore_test_files", + "src/goscorer", + "*.d.ts" ] } diff --git a/Makefile b/Makefile index 62f5f0c4..3bda5ec0 100644 --- a/Makefile +++ b/Makefile @@ -3,26 +3,44 @@ SLACK_WEBHOOK=$(shell cat ../ogs/.slack-webhook) all dev: yarn run dev + +build: lib types + +lib: build-debug build-production types + +build-debug: + yarn run build-debug + +build-production: + yarn run build-production + +types: + yarn run dts + lint: yarn run lint test: yarn run test + +detect-duplicate-code duplicate-code-detection: + yarn run detect-duplicate-code doc docs typedoc: yarn run typedoc +publish_docs: typedoc + cd docs && git add docs && git commit -m "Update docs" && git push + clean: rm -Rf lib node -publish push: publish_npm upload_to_cdn notify +publish push: publish_npm publish_docs upload_to_cdn notify beta: beta_npm upload_to_cdn -beta_npm: - yarn run build-debug - yarn run build-production +beta_npm: build yarn publish --tag beta ./ notify: @@ -30,9 +48,7 @@ notify: VERSION=`git describe --long`; \ curl -X POST -H 'Content-type: application/json' --data '{"text":"'"[GOBAN] $$VERSION $$MSG"'"}' $(SLACK_WEBHOOK) -publish_npm: - yarn run build-debug - yarn run build-production +publish_npm: build yarn publish ./ upload_to_cdn: @@ -40,8 +56,6 @@ upload_to_cdn: mkdir deployment-staging-area; cp lib/goban.js* deployment-staging-area cp lib/goban.min.js* deployment-staging-area - cp lib/engine.js* deployment-staging-area - cp lib/engine.min.js* deployment-staging-area gsutil -m rsync -r deployment-staging-area/ gs://ogs-site-files/goban/`node -pe 'JSON.parse(require("fs").readFileSync("package.json")).version'`/ -.PHONY: doc docs test clean all dev typedoc publich push +.PHONY: doc build docs test clean all dev typedoc publich push lib types diff --git a/jest.config.ts b/jest.config.ts index d68c4e62..425d624c 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -1,209 +1,225 @@ +/* + * Copyright (C) Online-Go.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + /* * For a detailed explanation regarding each configuration property and type check, visit: * https://jestjs.io/docs/configuration */ export default { - // All imported modules in your tests should be mocked automatically - // automock: false, - - // Stop running tests after `n` failures - // bail: 0, - - // The directory where Jest should store its cached dependency information - // cacheDirectory: "/private/var/folders/zf/nn3t1vpn1g12gm_gw4gbhnv40000gn/T/jest_dx", - - // Automatically clear mock calls, instances, contexts and results before every test - clearMocks: true, - - // Indicates whether the coverage information should be collected while executing the test - collectCoverage: true, - - // An array of glob patterns indicating a set of files for which coverage information should be collected - collectCoverageFrom: ["/src/**"], + // All imported modules in your tests should be mocked automatically + // automock: false, + + // Stop running tests after `n` failures + // bail: 0, + + // The directory where Jest should store its cached dependency information + // cacheDirectory: "/private/var/folders/zf/nn3t1vpn1g12gm_gw4gbhnv40000gn/T/jest_dx", + + // Automatically clear mock calls, instances, contexts and results before every test + clearMocks: true, + + // Indicates whether the coverage information should be collected while executing the test + collectCoverage: true, - // The directory where Jest should output its coverage files - coverageDirectory: "coverage", + // An array of glob patterns indicating a set of files for which coverage information should be collected + collectCoverageFrom: ["/src/**"], - // An array of regexp pattern strings used to skip coverage collection - // coveragePathIgnorePatterns: [ - // "/node_modules/" - // ], - coveragePathIgnorePatterns: [ - "/src/test.tsx", - "/src/goban.ts", - "/src/engine.ts", - ], + // The directory where Jest should output its coverage files + coverageDirectory: "coverage", - // Indicates which provider should be used to instrument code for coverage - coverageProvider: "v8", + // An array of regexp pattern strings used to skip coverage collection + // coveragePathIgnorePatterns: [ + // "/node_modules/" + // ], + coveragePathIgnorePatterns: [ + "/src/test.tsx", + "/src/renderer/index.ts", + "/src/engine/index.ts", + ".d.ts", + "wasm_estimator.ts", + ], - // A list of reporter names that Jest uses when writing coverage reports - // coverageReporters: [ - // "json", - // "text", - // "lcov", - // "clover" - // ], + // Indicates which provider should be used to instrument code for coverage + coverageProvider: "v8", - // An object that configures minimum threshold enforcement for coverage results - coverageThreshold: { - "global": { - "lines": 60 - } - }, + // A list of reporter names that Jest uses when writing coverage reports + // coverageReporters: [ + // "json", + // "text", + // "lcov", + // "clover" + // ], - // A path to a custom dependency extractor - // dependencyExtractor: undefined, + // An object that configures minimum threshold enforcement for coverage results + coverageThreshold: { + global: { + lines: 60, + }, + }, - // Make calling deprecated APIs throw helpful error messages - // errorOnDeprecated: false, + // A path to a custom dependency extractor + // dependencyExtractor: undefined, - // The default configuration for fake timers - // fakeTimers: { - // "enableGlobally": false - // }, + // Make calling deprecated APIs throw helpful error messages + // errorOnDeprecated: false, - // Force coverage collection from ignored files using an array of glob patterns - // forceCoverageMatch: [], + // The default configuration for fake timers + // fakeTimers: { + // "enableGlobally": false + // }, - // A path to a module which exports an async function that is triggered once before all test suites - // globalSetup: undefined, + // Force coverage collection from ignored files using an array of glob patterns + // forceCoverageMatch: [], - // A path to a module which exports an async function that is triggered once after all test suites - // globalTeardown: undefined, + // A path to a module which exports an async function that is triggered once before all test suites + // globalSetup: undefined, - // A set of global variables that need to be available in all test environments - // globals: {}, + // A path to a module which exports an async function that is triggered once after all test suites + // globalTeardown: undefined, - // The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers. - // maxWorkers: "50%", + // A set of global variables that need to be available in all test environments + // globals: {}, - // An array of directory names to be searched recursively up from the requiring module's location - // moduleDirectories: [ - // "node_modules" - // ], + // The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers. + // maxWorkers: "50%", - // An array of file extensions your modules use - // moduleFileExtensions: [ - // "js", - // "mjs", - // "cjs", - // "jsx", - // "ts", - // "tsx", - // "json", - // "node" - // ], + // An array of directory names to be searched recursively up from the requiring module's location + moduleDirectories: ["src", "src/third_party", "src/third_party/goscorer", "node_modules"], - // A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module - // moduleNameMapper: {}, + // An array of file extensions your modules use + // moduleFileExtensions: [ + // "js", + // "mjs", + // "cjs", + // "jsx", + // "ts", + // "tsx", + // "json", + // "node" + // ], - // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader - // modulePathIgnorePatterns: [], + // A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module + // moduleNameMapper: {}, - // Activates notifications for test results - // notify: false, + // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader + // modulePathIgnorePatterns: [], - // An enum that specifies notification mode. Requires { notify: true } - // notifyMode: "failure-change", + // Activates notifications for test results + // notify: false, - // A preset that is used as a base for Jest's configuration - preset: 'ts-jest', + // An enum that specifies notification mode. Requires { notify: true } + // notifyMode: "failure-change", - // Run tests from one or more projects - // projects: undefined, + // A preset that is used as a base for Jest's configuration + preset: "ts-jest", - // Use this configuration option to add custom reporters to Jest - // reporters: undefined, + // Run tests from one or more projects + // projects: undefined, - // Automatically reset mock state before every test - // resetMocks: false, + // Use this configuration option to add custom reporters to Jest + // reporters: undefined, - // Reset the module registry before running each individual test - // resetModules: false, + // Automatically reset mock state before every test + // resetMocks: false, - // A path to a custom resolver - // resolver: undefined, + // Reset the module registry before running each individual test + // resetModules: false, - // Automatically restore mock state and implementation before every test - // restoreMocks: false, + // A path to a custom resolver + // resolver: undefined, - // The root directory that Jest should scan for tests and modules within - // rootDir: undefined, + // Automatically restore mock state and implementation before every test + // restoreMocks: false, - // A list of paths to directories that Jest should use to search for files in - // roots: [ - // "" - // ], + // The root directory that Jest should scan for tests and modules within + // rootDir: undefined, - // Allows you to use a custom runner instead of Jest's default test runner - // runner: "jest-runner", + // A list of paths to directories that Jest should use to search for files in + // roots: [ + // "" + // ], - // The paths to modules that run some code to configure or set up the testing environment before each test - // setupFiles: [], + // Allows you to use a custom runner instead of Jest's default test runner + // runner: "jest-runner", - // A list of paths to modules that run some code to configure or set up the testing framework before each test - // setupFilesAfterEnv: [], + // The paths to modules that run some code to configure or set up the testing environment before each test + // setupFiles: [], - // The number of seconds after which a test is considered as slow and reported as such in the results. - // slowTestThreshold: 5, + // A list of paths to modules that run some code to configure or set up the testing framework before each test + // setupFilesAfterEnv: [], - // A list of paths to snapshot serializer modules Jest should use for snapshot testing - // snapshotSerializers: [], + // The number of seconds after which a test is considered as slow and reported as such in the results. + // slowTestThreshold: 5, - // The test environment that will be used for testing - // testEnvironment: "jest-environment-node", + // A list of paths to snapshot serializer modules Jest should use for snapshot testing + // snapshotSerializers: [], - // Options that will be passed to the testEnvironment - // testEnvironmentOptions: {}, + // The test environment that will be used for testing + testEnvironment: "jsdom", - // Adds a location field to test results - // testLocationInResults: false, + // Options that will be passed to the testEnvironment + // testEnvironmentOptions: {}, - // The glob patterns Jest uses to detect test files - testMatch: [ - "/src/**/__tests__/**/*.[jt]s?(x)", - "/src/**/?(*.)+(spec|test).ts" - ], + // Adds a location field to test results + // testLocationInResults: false, - // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped - // testPathIgnorePatterns: [ - // "/node_modules/" - // ], + // The glob patterns Jest uses to detect test files + testMatch: [ + "/src/**/unit_tests/**/*.[jt]s?(x)", + "/test/**/unit_tests/**/*.[jt]s?(x)", + "/src/**/?(*.)+(spec|test).ts", + "/test/**/?(*.)+(spec|test).ts", + ], - // The regexp pattern or array of patterns that Jest uses to detect test files - // testRegex: [], + // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped + // testPathIgnorePatterns: [ + // "/node_modules/" + // ], - // This option allows the use of a custom results processor - // testResultsProcessor: undefined, + // The regexp pattern or array of patterns that Jest uses to detect test files + // testRegex: [], - // This option allows use of a custom test runner - // testRunner: "jest-circus/runner", + // This option allows the use of a custom results processor + // testResultsProcessor: undefined, - // A map from regular expressions to paths to transformers - transform: { - '^.+\\.ts?$': 'ts-jest', - "^.+\\.svg$": "jest-transform-stub", - }, + // This option allows use of a custom test runner + // testRunner: "jest-circus/runner", - // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation - transformIgnorePatterns: [ - "/node_modules/", - ], + // A map from regular expressions to paths to transformers + transform: { + "^.+\\.ts?$": "ts-jest", + "^.+goscorer.js$": "ts-jest", + "^.+\\.svg$": "jest-transform-stub", + }, - // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them - // unmockedModulePathPatterns: undefined, + // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation + transformIgnorePatterns: ["/node_modules/"], - // Indicates whether each individual test should be reported during the run - // verbose: undefined, + // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them + // unmockedModulePathPatterns: undefined, - // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode - // watchPathIgnorePatterns: [], + // Indicates whether each individual test should be reported during the run + // verbose: undefined, - // Whether to use watchman for file crawling - // watchman: true, + // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode + // watchPathIgnorePatterns: [], + // Whether to use watchman for file crawling + // watchman: true, - "testTimeout": 200 + testTimeout: 200, }; diff --git a/package.json b/package.json index 28312cbc..ecdfa6dd 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,11 @@ { "name": "goban", - "version": "0.7.50", + "version": "0.8.0-beta.3", "description": "", - "main": "lib/goban.js", + "main": "node/goban-engine.js", + "browser": { + "goban": "lib/goban.js" + }, "types": "lib/goban.d.ts", "files": [ "lib/", @@ -22,10 +25,12 @@ "dev": "webpack-cli serve", "build-debug": "webpack", "build-production": "webpack --mode production", + "dts": "dts-bundle-generator -o lib/goban.d.ts src/index.ts", + "detect-duplicate-code": "jscpd --ignore '**/board_woods.ts' --ignore-pattern '.*place.*StoneSVG.*' --min-tokens 50 src/", "lint": "eslint src/ --ext=.ts,.tsx", "lint:fix": "eslint --fix src/ --ext=.ts,.tsx", - "typedoc": "typedoc src/goban.ts", - "typedoc:watch": "typedoc --watch src/goban.ts", + "typedoc": "typedoc --plugin typedoc-plugin-missing-exports src/index.ts ", + "typedoc:watch": "typedoc --watch src/index.ts", "prettier": "prettier --write \"src/**/*.{ts,tsx}\"", "prettier:check": "prettier --check \"src/**/*.{ts,tsx}\"", "checks": "npm run lint && npm run prettier:check", @@ -45,7 +50,7 @@ "homepage": "https://github.com/online-go/goban#readme", "devDependencies": { "@types/cli-color": "^2.0.6", - "@types/jest": "^29.5.0", + "@types/jest": "^29.5.12", "@types/node": "^18.15.5", "@types/react": "^18.0.28", "@types/react-dom": "^18.0.11", @@ -56,6 +61,7 @@ "canvas": "^2.10.2", "cli-color": "^2.0.4", "cspell": "^8.3.2", + "dts-bundle-generator": "^9.5.1", "eslint": "^8.56.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-jsdoc": "^46.9.1", @@ -69,6 +75,7 @@ "jest-environment-jsdom": "^29.7.0", "jest-transform-stub": "^2.0.0", "jest-websocket-mock": "^2.4.0", + "jscpd": "^4.0.1", "lint-staged": "^15.0.1", "prettier": "^3.1.1", "prettier-eslint": "^16.1.2", @@ -76,12 +83,14 @@ "react-dom": "^18.2.0", "svg-inline-loader": "0.8.2", "thread-loader": "^3.0.4", - "ts-jest": "^29.1.1", + "ts-jest": "^29.1.4", "ts-loader": "^9.5.0", "ts-node": "^10.9.1", + "tsconfig-paths": "^4.2.0", "tslint": "^6.1.3", - "typedoc": "^0.25.6", - "typescript": "=5.3.3", + "typedoc": "^0.25.13", + "typedoc-plugin-missing-exports": "^2.3.0", + "typescript": "=5.4.5", "utf-8-validate": "^6.0.3", "webpack": "^5.89.0", "webpack-cli": "^5.1.4", diff --git a/scripts/fetch_game_for_autoscore_testing.ts b/scripts/fetch_game_for_autoscore_testing.ts index 3280179b..93d0b98a 100755 --- a/scripts/fetch_game_for_autoscore_testing.ts +++ b/scripts/fetch_game_for_autoscore_testing.ts @@ -1,6 +1,6 @@ #!/usr/bin/env ts-node -/* +/* This script fetches a game, scores it, and writes it to a test file. @@ -12,7 +12,7 @@ Note: this script requires a JWT to be provided in the file "user.jwt" */ import { readFileSync, writeFileSync } from "fs"; -import { ScoreEstimateRequest } from "../src/ScoreEstimator"; +import { ScoreEstimateRequest } from "engine"; const jwt = readFileSync("user.jwt").toString().replace(/"/g, "").trim(); const game_id = process.argv[2]; @@ -30,9 +30,12 @@ if (!game_id) { } console.log(`Fetching game ${game_id}...`); +const TERM_SERVER = process.env.TERM_SERVER || "https://online-go.com"; +const AI_SERVER = process.env.AI_SERVER || "https://ai.online-go.com"; + (async () => { //fetch(`https://online-go.com/termination-api/game/${game_id}/score`, { - const res = await fetch(`https://online-go.com/termination-api/game/${game_id}/state`); + const res = await fetch(`${TERM_SERVER}/termination-api/game/${game_id}/state`); const json = await res.json(); const board_state = json.board; @@ -49,7 +52,7 @@ console.log(`Fetching game ${game_id}...`); const estimate_responses = await Promise.all([ // post to https://ai.online-go.com/api/score - fetch("https://ai.online-go.com/api/score", { + fetch(`${AI_SERVER}/api/score`, { method: "POST", headers: { "Content-Type": "application/json", @@ -57,7 +60,7 @@ console.log(`Fetching game ${game_id}...`); body: JSON.stringify(ser_black), }), - fetch("https://ai.online-go.com/api/score", { + fetch(`${AI_SERVER}/api/score`, { method: "POST", headers: { "Content-Type": "application/json", diff --git a/src/GoMath.ts b/src/GoMath.ts deleted file mode 100644 index 88097fb2..00000000 --- a/src/GoMath.ts +++ /dev/null @@ -1,460 +0,0 @@ -/* - * Copyright (C) Online-Go.com - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { JGOFIntersection, JGOFMove, JGOFNumericPlayerColor } from "./JGOF"; -import { AdHocPackedMove } from "./AdHocFormat"; - -export type Move = JGOFMove; -export type Intersection = JGOFIntersection; -export type NumberMatrix = Array>; -export type StringMatrix = Array>; - -export function makeMatrix(width: number, height: number, initialValue: number = 0): NumberMatrix { - const ret: NumberMatrix = []; - for (let y = 0; y < height; ++y) { - ret.push([]); - for (let x = 0; x < width; ++x) { - ret[y].push(initialValue); - } - } - return ret; -} -export function makeStringMatrix( - width: number, - height: number, - initialValue: string = "", -): StringMatrix { - const ret: StringMatrix = []; - for (let y = 0; y < height; ++y) { - ret.push([]); - for (let x = 0; x < width; ++x) { - ret[y].push(initialValue); - } - } - return ret; -} -export function makeObjectMatrix(width: number, height: number): Array> { - const ret = new Array>(height); - for (let y = 0; y < height; ++y) { - const row = new Array(width); - for (let x = 0; x < width; ++x) { - row[x] = {} as T; - } - ret[y] = row; - } - return ret; -} -export function makeEmptyObjectMatrix(width: number, height: number): Array> { - const ret = new Array>(height); - for (let y = 0; y < height; ++y) { - const row = new Array(width); - ret[y] = row; - } - return ret; -} -const COOR_SEQ = "abcdefghijklmnopqrstuvwxyz"; - -export function coor_ch2num(ch: string): number { - return COOR_SEQ.indexOf(ch?.toLowerCase()); -} - -export function coor_num2ch(coor: number): string { - return COOR_SEQ[coor]; -} - -const PRETTY_COOR_SEQ = "ABCDEFGHJKLMNOPQRSTUVWXYZ"; - -export function pretty_coor_ch2num(ch: string): number { - return PRETTY_COOR_SEQ.indexOf(ch?.toUpperCase()); -} - -export function pretty_coor_num2ch(coor: number): string { - return PRETTY_COOR_SEQ[coor]; -} - -export function prettyCoords(x: number, y: number, board_height: number): string { - if (x >= 0) { - return pretty_coor_num2ch(x) + ("" + (board_height - y)); - } - return "pass"; -} -export function decodeGTPCoordinate(move: string, width: number, height: number): JGOFMove { - if (move === ".." || move.toLowerCase() === "pass") { - return { x: -1, y: -1 }; - } - let y = height - parseInt(move.substr(1)); - const x = pretty_coor_ch2num(move[0]); - if (x === -1) { - y = -1; - } - return { x, y }; -} - -// TBD: A description of the scope, intent, and even known use-cases of this would be very helpful. -// (My head spins trying to understand what this takes care of, and how not to break that) -export function decodeMoves( - move_obj: - | AdHocPackedMove - | string - | Array - | [object] - | Array - | JGOFMove - | undefined, - width: number, - height: number, -): Array { - const ret: Array = []; - - if (!move_obj) { - return []; - } - - function decodeSingleMoveArray(arr: [number, number, number, number?, object?]): Move { - const obj: Move = { - x: arr[0], - y: arr[1], - timedelta: arr.length > 2 ? arr[2] : -1, - color: (arr.length > 3 ? arr[3] : 0) as JGOFNumericPlayerColor, - }; - const extra: any = arr.length > 4 ? arr[4] : {}; - for (const k in extra) { - (obj as any)[k] = extra[k]; - } - return obj; - } - - if (move_obj instanceof Array) { - if (move_obj.length === 0) { - return []; - } - if (typeof move_obj[0] === "number") { - ret.push(decodeSingleMoveArray(move_obj as [number, number, number, number])); - } else { - if ( - typeof move_obj[0] === "object" && - "x" in move_obj[0] && - typeof move_obj[0].x === "number" - ) { - return move_obj as Array; - } - - for (let i = 0; i < move_obj.length; ++i) { - const mv: any = move_obj[i]; - if (mv instanceof Array && typeof mv[0] === "number") { - ret.push(decodeSingleMoveArray(mv as [number, number, number, number])); - } else { - throw new Error(`Unrecognized move format: ${mv}`); - } - } - } - } else if (typeof move_obj === "string") { - if (!height || !width) { - throw new Error( - `decodeMoves requires a height and width to be set when decoding a string coordinate`, - ); - } - - if (/[a-zA-Z][0-9]/.test(move_obj)) { - /* coordinate form, used from human input. */ - const move_string = move_obj; - - const moves = move_string.split(/([a-zA-Z][0-9]+|pass|[.][.])/); - for (let i = 0; i < moves.length; ++i) { - if (i % 2) { - /* even are the 'splits', which should always be blank unless there is an error */ - let x = pretty_char2num(moves[i][0]); - let y = height - parseInt(moves[i].substring(1)); - if ((width && x >= width) || x < 0) { - x = y = -1; - } - if ((height && y >= height) || y < 0) { - x = y = -1; - } - ret.push({ x: x, y: y, edited: false, color: 0 }); - } else { - if (moves[i] !== "") { - throw "Unparsed move input: " + moves[i]; - } - } - } - } else { - /* Pure letter encoded form, used for all records */ - const move_string = move_obj; - - for (let i = 0; i < move_string.length - 1; i += 2) { - let edited = false; - let color: JGOFNumericPlayerColor = 0; - if (move_string[i + 0] === "!") { - edited = true; - if (move_string.substr(i, 10) === "!undefined") { - /* bad data */ - color = 0; - i += 10; - } else { - color = parseInt(move_string[i + 1]) as JGOFNumericPlayerColor; - i += 2; - } - } - - let x = char2num(move_string[i]); - let y = char2num(move_string[i + 1]); - if (width && x >= width) { - x = y = -1; - } - if (height && y >= height) { - x = y = -1; - } - ret.push({ x: x, y: y, edited: edited, color: color }); - } - } - } else if (typeof move_obj === "object" && "x" in move_obj && typeof move_obj.x === "number") { - return [move_obj] as Array; - } else { - throw new Error("Invalid move format: " + JSON.stringify(move_obj)); - } - - return ret; -} -export function char2num(ch: string): number { - if (ch === ".") { - return -1; - } - return coor_ch2num(ch); -} -function pretty_char2num(ch: string): number { - if (ch === ".") { - return -1; - } - return pretty_coor_ch2num(ch); -} -export function num2char(num: number): string { - if (num === -1) { - return "."; - } - return coor_num2ch(num); -} - -export function encodeMove(x: number | Move, y?: number): string { - if (typeof x === "number") { - if (typeof y !== "number") { - throw new Error(`Invalid y parameter to encodeMove y = ${y}`); - } - return num2char(x) + num2char(y); - } else { - const mv: Move = x; - - if (!mv.edited) { - return num2char(mv.x) + num2char(mv.y); - } else { - return "!" + mv.color + num2char(mv.x) + num2char(mv.y); - } - } -} - -export function encodeMoves(lst: Array): string { - let ret = ""; - for (let i = 0; i < lst.length; ++i) { - ret += encodeMove(lst[i]); - } - return ret; -} - -export function encodePrettyCoord(coord: string, height: number): string { - // "C12" with no "I". TBD: give these different `string`s proper type names. - const x = num2char(pretty_char2num(coord.charAt(0).toLowerCase())); - const y = num2char(height - parseInt(coord.substring(1))); - return x + y; -} - -export function encodeMoveToArray(mv: Move): AdHocPackedMove { - // Note: despite the name here, AdHocPackedMove became a tuple at some point! - let extra: any = {}; - if (mv.blur) { - extra.blur = mv.blur; - } - if (mv.sgf_downloaded_by) { - extra.sgf_downloaded_by = mv.sgf_downloaded_by; - } - if (mv.played_by) { - extra.played_by = mv.played_by; - } - if (mv.player_update) { - extra.player_update = mv.player_update; - } - - // don't add an extra if there is nothing extra... - if (Object.keys(extra).length === 0) { - extra = undefined; - } - - const arr: AdHocPackedMove = [mv.x, mv.y, mv.timedelta ? mv.timedelta : -1, undefined, extra]; - if (mv.edited) { - arr[3] = mv.color; - if (!extra) { - arr.pop(); - } - } else { - if (!extra) { - arr.pop(); // extra - arr.pop(); // edited - } - } - return arr; -} -export function encodeMovesToArray(moves: Array): Array { - const ret: Array = []; - for (let i = 0; i < moves.length; ++i) { - ret.push(encodeMoveToArray(moves[i])); - } - return ret; -} - -export function stripModeratorOnlyExtraInformation(move: AdHocPackedMove): AdHocPackedMove { - const moderator_only_extra_info = ["blur", "sgf_downloaded_by"]; - - if (move.length === 5 && move[4]) { - // the packed move has a defined `extra` field that we have to filter - let filtered_extra: any = { ...move[4] }; - for (const field of moderator_only_extra_info) { - delete filtered_extra[field]; - } - if (Object.keys(filtered_extra).length === 0) { - filtered_extra = undefined; - } - - //filtered_extra.stripped = true; // this is how you can tell by looking at a move structure in flight whether it went through here. - const filtered_move = [...move.slice(0, 4), filtered_extra]; - while (filtered_move.length > 3 && !filtered_move[filtered_move.length - 1]) { - filtered_move.pop(); - } - return filtered_move as AdHocPackedMove; - } - return move; -} - -/** - * Removes superfluous fields from the JGOFMove objects, such as - * edited=false and color=0. This does not modify the original array. - */ -export function trimJGOFMoves(arr: Array): Array { - return arr.map((o) => { - const r: JGOFMove = { - x: o.x, - y: o.y, - }; - if (o.edited) { - r.edited = o.edited; - } - if (o.color) { - r.color = o.color; - } - if (o.timedelta) { - r.timedelta = o.timedelta; - } - return r; - }); -} - -/** Returns a sorted move string, this is used in our stone removal logic */ -export function sortMoves(move_string: string, width: number, height: number): string { - const moves = decodeMoves(move_string, width, height); - moves.sort((a, b) => { - const av = (a.edited ? 1 : 0) * 10000 + a.x + a.y * 100; - const bv = (b.edited ? 1 : 0) * 10000 + b.x + b.y * 100; - return av - bv; - }); - return encodeMoves(moves); -} - -// OJE Sequence format is '.root.K10.Q1' ... -export function ojeSequenceToMoves(sequence: string): Array { - const plays = sequence.split("."); - - if (plays.shift() !== "" || plays.shift() !== "root") { - throw new Error("Sequence passed to sequenceToMoves does not start with .root"); - } - - const moves = plays.map((play) => { - if (play === "pass") { - return { x: -1, y: -1 }; - } - return decodeGTPCoordinate(play, 19, 19); - }); - - return moves; -} - -// This is intended to be an "easy to understand" method of generating a unique id -// for a board position. - -// The "id" is the list of all the positions of the stones, black first then white, -// separated by a colon. - -// There are in fact 8 possible ways to list the positions (all the rotations and -// reflections of the position). The id is the lowest (alpha-numerically) of these. - -// Colour independence for the position is achieved by takeing the lexically lower -// of the ids of the position with black and white reversed. - -// The "easy to understand" part is that the id can be compared visually to the -// board position - -// The downside is that the id string can be moderately long for boards with lots of stones - -type BoardTransform = (x: number, y: number) => { x: number; y: number }; - -export function positionId( - position: Array>, - height: number, - width: number, -): string { - // The basic algorithm is to list where each of the stones are, in a long string. - // We do this once for each transform, selecting the lowest (lexically) as we go. - - const transforms: Array = [ - (x, y) => ({ x, y }), - (x, y) => ({ x, y: height - y - 1 }), - (x, y) => ({ x: y, y: x }), - (x, y) => ({ x: y, y: width - x - 1 }), - (x, y) => ({ x: height - y - 1, y: x }), - (x, y) => ({ x: height - y - 1, y: width - x - 1 }), - (x, y) => ({ x: width - x - 1, y }), - (x, y) => ({ x: width - x - 1, y: height - y - 1 }), - ]; - - const ids = []; - - for (const transform of transforms) { - let black_state = ""; - let white_state = ""; - for (let x = 0; x < width; x++) { - for (let y = 0; y < height; y++) { - const c = transform(x, y); - if (position[x][y] === JGOFNumericPlayerColor.BLACK) { - black_state += encodeMove(c.x, c.y); - } - if (position[x][y] === JGOFNumericPlayerColor.WHITE) { - white_state += encodeMove(c.x, c.y); - } - } - } - - ids.push(`${black_state}.${white_state}`); - } - - return ids.reduce((prev, current) => (current < prev ? current : prev)); -} diff --git a/src/GoStoneGroup.ts b/src/GoStoneGroup.ts deleted file mode 100644 index daad7f42..00000000 --- a/src/GoStoneGroup.ts +++ /dev/null @@ -1,219 +0,0 @@ -/* - * Copyright (C) Online-Go.com - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { Intersection } from "./GoMath"; -import { JGOFNumericPlayerColor, JGOFIntersection } from "./JGOF"; - -export type Group = Array; - -export interface BoardState { - width: number; - height: number; - board: Array>; - removal: Array>; -} - -export class GoStoneGroup { - corner_groups: { [y: string]: { [x: string]: GoStoneGroup } }; - points: Array; - neighbors: Array; - is_territory: boolean = false; - color: JGOFNumericPlayerColor; - board_state: BoardState; - id: number; - is_strong_eye: boolean; - is_eye: boolean = false; - is_strong_string: boolean = false; - territory_color: JGOFNumericPlayerColor = 0; - is_territory_in_seki: boolean = false; - - private __added_neighbors: { [group_id: number]: boolean }; - private neighboring_space: GoStoneGroup[]; - private neighboring_enemy: GoStoneGroup[]; - - constructor(board_state: BoardState, id: number, color: JGOFNumericPlayerColor) { - this.board_state = board_state; - this.points = []; - this.neighbors = []; - this.neighboring_space = []; - this.neighboring_enemy = []; - this.id = id; - this.color = color; - this.is_strong_eye = false; - - this.__added_neighbors = {}; - this.corner_groups = {}; - } - addStone(x: number, y: number): void { - this.points.push({ x: x, y: y }); - } - addNeighborGroup(group: GoStoneGroup): void { - if (!(group.id in this.__added_neighbors)) { - this.neighbors.push(group); - this.__added_neighbors[group.id] = true; - - if (group.color !== this.color) { - if (group.color === JGOFNumericPlayerColor.EMPTY) { - this.neighboring_space.push(group); - } else { - this.neighboring_enemy.push(group); - } - } - } - } - addCornerGroup(x: number, y: number, group: GoStoneGroup): void { - if (!(y in this.corner_groups)) { - this.corner_groups[y] = {}; - } - this.corner_groups[y][x] = group; - } - foreachStone(fn: (pt: Intersection) => void): void { - for (let i = 0; i < this.points.length; ++i) { - fn(this.points[i]); - } - } - foreachNeighborGroup(fn: (group: GoStoneGroup) => void): void { - for (let i = 0; i < this.neighbors.length; ++i) { - fn(this.neighbors[i]); - } - } - foreachNeighborSpaceGroup(fn: (group: GoStoneGroup) => void): void { - for (let i = 0; i < this.neighboring_space.length; ++i) { - fn(this.neighboring_space[i]); - } - } - foreachNeighborEnemyGroup(fn: (group: GoStoneGroup) => void): void { - for (let i = 0; i < this.neighbors.length; ++i) { - fn(this.neighboring_enemy[i]); - } - } - computeIsEye(): void { - this.is_eye = false; - - if (this.points.length > 1) { - return; - } - - this.is_eye = this.is_territory; - } - size(): number { - return this.points.length; - } - computeIsStrongEye(): void { - /* If a single eye is surrounded by 7+ stones of the same color, 5 stones - * for edges, and 3 stones for corners, or if any of those spots are - * territory owned by the same color, it is considered strong. */ - this.is_strong_eye = false; - let color: JGOFNumericPlayerColor; - const board_state = this.board_state; - if (this.is_eye) { - const x = this.points[0].x; - const y = this.points[0].y; - color = board_state.board[y][x === 0 ? x + 1 : x - 1]; - let not_color = 0; - - const chk = (x: number, y: number): 0 | 1 => { - /* If there is a stone on the board and it's not our color, - * or if the spot is part of some territory which is not our color, - * then return true, else false. */ - return color !== board_state.board[y][x] && - (!this.corner_groups[y][x].is_territory || - this.corner_groups[y][x].territory_color !== color) - ? 1 - : 0; - }; - - not_color = - (x - 1 >= 0 && y - 1 >= 0 ? chk(x - 1, y - 1) : 0) + - (x + 1 < board_state.width && y - 1 >= 0 ? chk(x + 1, y - 1) : 0) + - (x - 1 >= 0 && y + 1 < board_state.height ? chk(x - 1, y + 1) : 0) + - (x + 1 < board_state.width && y + 1 < board_state.height ? chk(x + 1, y + 1) : 0); - - if ( - x - 1 >= 0 && - x + 1 < board_state.width && - y - 1 >= 0 && - y + 1 < board_state.height - ) { - this.is_strong_eye = not_color <= 1; - } else { - this.is_strong_eye = not_color === 0; - } - } - } - computeIsStrongString(): void { - /* A group is considered a strong string if it is adjacent to two strong eyes */ - let strong_eye_count = 0; - this.foreachNeighborGroup((gr) => { - strong_eye_count += gr.is_strong_eye ? 1 : 0; - }); - this.is_strong_string = strong_eye_count >= 2; - } - computeIsTerritory(): void { - /* An empty group is considered territory if all of it's neighbors are of - * the same color */ - this.is_territory = false; - this.territory_color = 0; - if (this.color) { - return; - } - - let color: JGOFNumericPlayerColor = 0; - for (let i = 0; i < this.neighbors.length; ++i) { - if (this.neighbors[i].color !== 0) { - color = this.neighbors[i].color; - break; - } - } - - this.foreachNeighborGroup((gr) => { - if (gr.color !== 0 && color !== gr.color) { - color = 0; - } - }); - - if (color) { - this.is_territory = true; - this.territory_color = color; - } - } - computeIsTerritoryInSeki(): void { - /* An empty group is considered territory if all of it's neighbors are of - * the same color */ - this.is_territory_in_seki = false; - if (this.is_territory) { - this.foreachNeighborGroup((border_stones) => { - border_stones.foreachNeighborGroup((border_of_border) => { - if (border_of_border.color === 0 && !border_of_border.is_territory) { - /* only mark in seki if the neighboring would-be-blocking - * territory hasn't been negated. */ - let is_not_negated = true; - for (let i = 0; i < border_of_border.points.length; ++i) { - const x = border_of_border.points[i].x; - const y = border_of_border.points[i].y; - if (!this.board_state.removal[y][x]) { - is_not_negated = false; - } - } - if (!is_not_negated) { - this.is_territory_in_seki = true; - } - } - }); - }); - } - } -} diff --git a/src/GoThemes.ts b/src/GoThemes.ts deleted file mode 100644 index 2685a539..00000000 --- a/src/GoThemes.ts +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright (C) Online-Go.com - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { GoTheme } from "./GoTheme"; - -export interface GoThemesInterface { - white: { [name: string]: typeof GoTheme }; - black: { [name: string]: typeof GoTheme }; - board: { [name: string]: typeof GoTheme }; - - // this exists so we can easily do GoThemes[what] - [_: string]: { [name: string]: typeof GoTheme }; -} - -export const GoThemes: GoThemesInterface = { - white: {}, - black: {}, - board: {}, -}; -export const GoThemesSorted: { [n: string]: Array } = { - white: [], - black: [], - board: [], -}; - -import init_board_plain from "./themes/board_plain"; -import init_board_woods from "./themes/board_woods"; -import init_plain_stones from "./themes/plain_stones"; -import init_rendered from "./themes/rendered_stones"; -import init_image_stones from "./themes/image_stones"; - -init_board_plain(GoThemes); -init_board_woods(GoThemes); -init_plain_stones(GoThemes); -init_rendered(GoThemes); -init_image_stones(GoThemes); - -function theme_sort(a: GoTheme, b: GoTheme) { - return a.sort() - b.sort(); -} - -for (const k in GoThemes) { - GoThemesSorted[k] = Object.keys(GoThemes[k]).map((n) => { - return new GoThemes[k][n](); - }); - GoThemesSorted[k].sort(theme_sort); -} diff --git a/src/GoUtil.ts b/src/GoUtil.ts deleted file mode 100644 index 956b6894..00000000 --- a/src/GoUtil.ts +++ /dev/null @@ -1,166 +0,0 @@ -/* - * Copyright (C) Online-Go.com - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { _, interpolate } from "./translate"; -import type { JGOFTimeControl } from "./JGOF"; - -export function getRandomInt(min: number, max: number) { - return Math.floor(Math.random() * (max - min)) + min; -} -export function shortDurationString(seconds: number) { - const weeks = Math.floor(seconds / (86400 * 7)); - seconds -= weeks * 86400 * 7; - const days = Math.floor(seconds / 86400); - seconds -= days * 86400; - const hours = Math.floor(seconds / 3600); - seconds -= hours * 3600; - const minutes = Math.floor(seconds / 60); - seconds -= minutes * 60; - return ( - "" + - (weeks ? " " + interpolate(_("%swk"), [weeks]) : "") + - (days ? " " + interpolate(_("%sd"), [days]) : "") + - (hours ? " " + interpolate(_("%sh"), [hours]) : "") + - (minutes ? " " + interpolate(_("%sm"), [minutes]) : "") + - (seconds ? " " + interpolate(_("%ss"), [seconds]) : "") - ); -} -export function dup(obj: any): any { - let ret: any; - if (typeof obj === "object") { - if (Array.isArray(obj)) { - ret = []; - for (let i = 0; i < obj.length; ++i) { - ret.push(dup(obj[i])); - } - } else { - ret = {}; - for (const i in obj) { - ret[i] = dup(obj[i]); - } - } - } else { - return obj; - } - return ret; -} -export function deepEqual(a: any, b: any) { - if (typeof a !== typeof b) { - return false; - } - - if (typeof a === "object") { - if (Array.isArray(a)) { - if (Array.isArray(b)) { - if (a.length !== b.length) { - return false; - } - for (let i = 0; i < a.length; ++i) { - if (!deepEqual(a[i], b[i])) { - return false; - } - } - } else { - return false; - } - } else { - for (const i in a) { - if (!(i in b)) { - return false; - } - if (!deepEqual(a[i], b[i])) { - return false; - } - } - for (const i in b) { - if (!(i in a)) { - return false; - } - } - } - return true; - } else { - return a === b; - } -} - -function averageMovesPerGame(w: number, h: number): number { - // Rough estimate based on discussion at https://forums.online-go.com/t/average-game-length-on-different-board-sizes/35042/11 - return Math.round(0.7 * w * h); -} - -export function computeAverageMoveTime( - time_control: JGOFTimeControl, - w?: number, - h?: number, -): number { - if (typeof time_control !== "object" || time_control === null) { - console.error( - `computeAverageMoveTime passed ${time_control} instead of a time_control object`, - ); - return time_control; - } - const moves = w && h ? averageMovesPerGame(w, h) / 2 : 90; - - try { - let t: number; - switch (time_control.system) { - case "fischer": - t = time_control.initial_time / moves + time_control.time_increment; - break; - case "byoyomi": - t = time_control.main_time / moves + time_control.period_time; - break; - case "simple": - t = time_control.per_move; - break; - case "canadian": - t = - time_control.main_time / moves + - time_control.period_time / time_control.stones_per_period; - break; - case "absolute": - t = time_control.total_time / moves; - break; - case "none": - t = 0; - break; - } - return Math.round(t); - } catch (err) { - console.error("Error computing average move time for time control: ", time_control); - console.error(err); - return 60; - } -} - -/* Like setInterval, but debounces catchups that happen - * when tabs wake up on some browsers. Cleared with - * the standard clearInterval. */ -export function niceInterval( - callback: () => void, - interval: number, -): ReturnType { - let last = performance.now(); - return setInterval(() => { - const now = performance.now(); - const diff = now - last; - if (diff >= interval * 0.9) { - last = now; - callback(); - } - }, interval); -} diff --git a/src/GobanCanvas.ts b/src/Goban/CanvasRenderer.ts similarity index 89% rename from src/GobanCanvas.ts rename to src/Goban/CanvasRenderer.ts index 5f938f01..ab29b42d 100644 --- a/src/GobanCanvas.ts +++ b/src/Goban/CanvasRenderer.ts @@ -14,27 +14,33 @@ * limitations under the License. */ -import { JGOF, JGOFIntersection, JGOFNumericPlayerColor } from "./JGOF"; - -import { AdHocFormat } from "./AdHocFormat"; - -import { GobanCore, GobanConfig, GobanSelectedThemes, GobanMetrics, GOBAN_FONT } from "./GobanCore"; -import { GoEngine } from "./GoEngine"; -import * as GoMath from "./GoMath"; -import { Group } from "./GoStoneGroup"; -import { MoveTree } from "./MoveTree"; -import { GoTheme } from "./GoTheme"; -import { GoThemes } from "./GoThemes"; -import { MoveTreePenMarks } from "./MoveTree"; +import { JGOF, JGOFIntersection, JGOFNumericPlayerColor } from "engine/formats/JGOF"; + +import { AdHocFormat } from "engine/formats/AdHocFormat"; + +import { GobanConfig } from "../GobanBase"; +import { GobanEngine } from "engine"; +import { MoveTree } from "engine/MoveTree"; +import { GobanTheme, THEMES } from "./themes"; +import { MoveTreePenMarks } from "engine/MoveTree"; import { createDeviceScaledCanvas, resizeDeviceScaledCanvas, allocateCanvasOrError, getRelativeEventPosition, } from "./canvas_utils"; -import { getRandomInt } from "./GoUtil"; -import { _ } from "./translate"; -import { formatMessage, MessageID } from "./messages"; +import { _ } from "engine/translate"; +import { formatMessage, MessageID } from "engine/messages"; +import { + color_blend, + encodeMove, + encodeMoves, + encodePrettyXCoordinate, + getRandomInt, + makeMatrix, +} from "engine/util"; +import { callbacks } from "./callbacks"; +import { Goban, GobanMetrics, GobanSelectedThemes, GOBAN_FONT } from "./Goban"; const __theme_cache: { [bw: string]: { [name: string]: { [size: string]: any } }; @@ -45,7 +51,7 @@ const __theme_cache: { declare let ResizeObserver: any; -export interface GobanCanvasConfig extends GobanConfig { +export interface CanvasRendererGobanConfig extends GobanConfig { board_div?: HTMLElement; title_div?: HTMLElement; move_tree_container?: HTMLElement; @@ -64,7 +70,7 @@ interface ViewPortInterface { const HOT_PINK = "#ff69b4"; export interface GobanCanvasInterface { - engine: GoEngine; + engine: GobanEngine; move_tree_container?: HTMLElement; clearAnalysisDrawing(): void; @@ -90,9 +96,8 @@ export interface GobanCanvasInterface { destroy(): void; } -export class GobanCanvas extends GobanCore implements GobanCanvasInterface { - public engine: GoEngine; - private parent: HTMLElement; +export class GobanCanvas extends Goban implements GobanCanvasInterface { + public engine: GobanEngine; //private board_div: HTMLElement; private board: HTMLCanvasElement; private __set_board_height: number = -1; @@ -131,25 +136,24 @@ export class GobanCanvas extends GobanCore implements GobanCanvasInterface { black: "Plain", white: "Plain", }; - private theme_black!: GoTheme; + private theme_black!: GobanTheme; private theme_black_stones: Array = []; private theme_black_text_color: string = HOT_PINK; private theme_blank_text_color: string = HOT_PINK; - private theme_board!: GoTheme; + private theme_board!: GobanTheme; private theme_faded_line_color: string = HOT_PINK; private theme_faded_star_color: string = HOT_PINK; //private theme_faded_text_color:string; private theme_line_color: string = ""; private theme_star_color: string = ""; private theme_stone_radius: number = 10; - private theme_white!: GoTheme; + private theme_white!: GobanTheme; private theme_white_stones: Array = []; private theme_white_text_color: string = HOT_PINK; - constructor(config: GobanCanvasConfig, preloaded_data?: AdHocFormat | JGOF) { + constructor(config: CanvasRendererGobanConfig, preloaded_data?: AdHocFormat | JGOF) { /* TODO: Need to reconcile the clock fields before we can get rid of this `any` cast */ super(config, preloaded_data as any); - console.info("GobanCanvas created"); // console.log("Goban canvas v 0.5.74.debug 5"); // GaJ: I use this to be sure I have linked & loaded the updates if (config.board_div) { @@ -198,7 +202,7 @@ export class GobanCanvas extends GobanCore implements GobanCanvasInterface { // this.theme_board // this.theme_white // this.theme_black - this.setThemes(this.getSelectedThemes(), true); + this.setTheme(this.getSelectedThemes(), true); let first_pass = true; const watcher = this.watchSelectedThemes((themes: GobanSelectedThemes) => { if ( @@ -211,7 +215,7 @@ export class GobanCanvas extends GobanCore implements GobanCanvasInterface { delete __theme_cache.black?.["Custom"]; delete __theme_cache.white?.["Custom"]; delete __theme_cache.board?.["Custom"]; - this.setThemes(themes, first_pass ? true : false); + this.setTheme(themes, first_pass ? true : false); first_pass = false; }); this.on("destroy", () => watcher.remove()); @@ -233,7 +237,7 @@ export class GobanCanvas extends GobanCore implements GobanCanvasInterface { this.detachPenCanvas(); } - public destroy(): void { + public override destroy(): void { super.destroy(); if (this.board && this.board.parentNode) { @@ -343,8 +347,10 @@ export class GobanCanvas extends GobanCore implements GobanCanvasInterface { let dragging = false; let last_click_square = this.xy2ij(0, 0); + let pointer_down_timestamp = 0; const pointerUp = (ev: MouseEvent | TouchEvent, double_clicked: boolean): void => { + const press_duration_ms = performance.now() - pointer_down_timestamp; try { if (!dragging) { /* if we didn't start the click in the canvas, don't respond to it */ @@ -364,11 +370,12 @@ export class GobanCanvas extends GobanCore implements GobanCanvasInterface { const pos = getRelativeEventPosition(ev); const pt = this.xy2ij(pos.x, pos.y); if (pt.i >= 0 && pt.i < this.width && pt.j >= 0 && pt.j < this.height) { - if (this.score_estimate) { - this.score_estimate.handleClick( + if (this.score_estimator) { + this.score_estimator.handleClick( pt.i, pt.j, ev.ctrlKey || ev.metaKey || ev.altKey || ev.shiftKey, + press_duration_ms, ); } this.emit("update"); @@ -380,9 +387,9 @@ export class GobanCanvas extends GobanCore implements GobanCanvasInterface { try { const pos = getRelativeEventPosition(ev); const pt = this.xy2ij(pos.x, pos.y); - if (GobanCore.hooks.addCoordinatesToChatInput) { - GobanCore.hooks.addCoordinatesToChatInput( - this.engine.prettyCoords(pt.i, pt.j), + if (callbacks.addCoordinatesToChatInput) { + callbacks.addCoordinatesToChatInput( + this.engine.prettyCoordinates(pt.i, pt.j), ); } } catch (e) { @@ -393,6 +400,10 @@ export class GobanCanvas extends GobanCore implements GobanCanvasInterface { if (this.mode === "analyze" && this.analyze_tool === "draw") { /* might want to interpret this as a start/stop of a line segment */ + } else if (this.mode === "analyze" && this.analyze_tool === "score") { + // nothing to do here + } else if (this.mode === "analyze" && this.analyze_tool === "removal") { + this.onAnalysisToggleStoneRemoval(ev); } else { const pos = getRelativeEventPosition(ev); const pt = this.xy2ij(pos.x, pos.y); @@ -405,7 +416,7 @@ export class GobanCanvas extends GobanCore implements GobanCanvasInterface { } } - this.onTap(ev, double_clicked, right_click); + this.onTap(ev, double_clicked, right_click, press_duration_ms); this.onMouseOut(ev); } } catch (e) { @@ -414,6 +425,7 @@ export class GobanCanvas extends GobanCore implements GobanCanvasInterface { }; const pointerDown = (ev: MouseEvent | TouchEvent): void => { + pointer_down_timestamp = performance.now(); try { dragging = true; if (this.mode === "analyze" && this.analyze_tool === "draw") { @@ -434,6 +446,10 @@ export class GobanCanvas extends GobanCore implements GobanCanvasInterface { } this.onLabelingStart(ev); + } else if (this.mode === "analyze" && this.analyze_tool === "score") { + this.onAnalysisScoringStart(ev); + } else if (this.mode === "analyze" && this.analyze_tool === "removal") { + // nothing to do here, we act on pointerUp } } catch (e) { console.error(e); @@ -449,6 +465,10 @@ export class GobanCanvas extends GobanCore implements GobanCanvasInterface { this.onPenMove(ev); } else if (dragging && this.mode === "analyze" && this.analyze_tool === "label") { this.onLabelingMove(ev); + } else if (dragging && this.mode === "analyze" && this.analyze_tool === "score") { + this.onAnalysisScoringMove(ev); + } else if (dragging && this.mode === "analyze" && this.analyze_tool === "removal") { + // nothing for moving } else { this.onMouseMove(ev); } @@ -728,7 +748,12 @@ export class GobanCanvas extends GobanCore implements GobanCanvasInterface { this.pen_ctx.stroke(); } } - private onTap(event: MouseEvent | TouchEvent, double_tap: boolean, right_click: boolean): void { + private onTap( + event: MouseEvent | TouchEvent, + double_tap: boolean, + right_click: boolean, + press_duration_ms: number, + ): void { if ( !( this.stone_placement_enabled && @@ -808,7 +833,7 @@ export class GobanCanvas extends GobanCore implements GobanCanvasInterface { } const sent = this.sendMove({ game_id: this.game_id, - move: GoMath.encodeMove(x, y), + move: encodeMove(x, y), }); if (sent) { this.playMovementSound(); @@ -831,22 +856,20 @@ export class GobanCanvas extends GobanCore implements GobanCanvasInterface { if ( this.engine.phase === "stone removal" && - this.engine.isActivePlayer(this.player_id) + this.engine.isActivePlayer(this.player_id) && + this.engine.cur_move === this.engine.last_official_move ) { - let removed: 0 | 1; - let group: Group; - if (event.shiftKey) { - removed = !this.engine.removal[y][x] ? 1 : 0; - group = [{ x, y }]; - } else { - [[removed, group]] = this.engine.toggleMetaGroupRemoval(x, y); - } + const { removed, group } = this.engine.toggleSingleGroupRemoval( + x, + y, + event.shiftKey || press_duration_ms > 500, + ); if (group.length) { this.socket.send("game/removed_stones/set", { game_id: this.game_id, - removed: !!removed, - stones: GoMath.encodeMoves(group), + removed: removed, + stones: encodeMoves(group), }); } } else if (this.mode === "puzzle") { @@ -1474,42 +1497,41 @@ export class GobanCanvas extends GobanCore implements GobanCanvasInterface { /* Colored stones */ - if (this.colored_circles) { - if (this.colored_circles[j][i]) { - const circle = this.colored_circles[j][i]; - const color = circle.color; + const circle = this.colored_circles?.[j][i]; + if (circle) { + const color = circle.color; - ctx.save(); - ctx.globalAlpha = 1.0; - const radius = Math.floor(this.square_size * 0.5) - 0.5; - let lineWidth = radius * (circle.border_width || 0.1); + ctx.save(); + ctx.globalAlpha = 1.0; + const radius = Math.floor(this.square_size * 0.5) - 0.5; + let lineWidth = radius * (circle.border_width || 0.1); - if (lineWidth < 0.3) { - lineWidth = 0; - } - ctx.fillStyle = color; - ctx.strokeStyle = circle.border_color || "#000000"; - if (lineWidth > 0) { - ctx.lineWidth = lineWidth; - } - ctx.beginPath(); - ctx.arc( - cx, - cy, - Math.max(0.1, radius - lineWidth / 2), - 0.001, - 2 * Math.PI, - false, - ); /* 0.001 to workaround fucked up chrome bug */ - if (lineWidth > 0) { - ctx.stroke(); - } - ctx.fill(); - ctx.restore(); + if (lineWidth < 0.3) { + lineWidth = 0; + } + ctx.fillStyle = color; + ctx.strokeStyle = circle.border_color || "#000000"; + if (lineWidth > 0) { + ctx.lineWidth = lineWidth; } + ctx.beginPath(); + ctx.arc( + cx, + cy, + Math.max(0.1, radius - lineWidth / 2), + 0.001, + 2 * Math.PI, + false, + ); /* 0.001 to workaround fucked up chrome bug */ + if (lineWidth > 0) { + ctx.stroke(); + } + ctx.fill(); + ctx.restore(); } /* Draw stones & hovers */ + let draw_red_x = false; { if ( stone_color /* if there is really a stone here */ || @@ -1527,9 +1549,9 @@ export class GobanCanvas extends GobanCore implements GobanCanvasInterface { (this.getPuzzlePlacementSetting && this.getPuzzlePlacementSetting().mode === "play"))) || (this.scoring_mode && - this.score_estimate && - this.score_estimate.board[j][i] && - this.score_estimate.removal[j][i]) || + this.score_estimator && + this.score_estimator.board[j][i] && + this.score_estimator.removal[j][i]) || (this.engine && this.engine.phase === "stone removal" && this.engine.board[j][i] && @@ -1538,20 +1560,21 @@ export class GobanCanvas extends GobanCore implements GobanCanvasInterface { pos.white ) { //let color = stone_color ? stone_color : (this.move_selected ? this.engine.otherPlayer() : this.engine.player); - let transparent = false; + let translucent = false; let stoneAlphaValue = 0.6; let color; if ( this.scoring_mode && - this.score_estimate && - this.score_estimate.board[j][i] && - this.score_estimate.removal[j][i] + this.score_estimator && + this.score_estimator.board[j][i] && + this.score_estimator.removal[j][i] ) { - color = this.score_estimate.board[j][i]; - transparent = true; + color = this.score_estimator.board[j][i]; + translucent = true; } else if ( this.engine && - (this.engine.phase === "stone removal" || + ((this.engine.phase === "stone removal" && + this.engine.last_official_move === this.engine.cur_move) || (this.engine.phase === "finished" && this.mode !== "analyze")) && this.engine.board && this.engine.removal && @@ -1559,7 +1582,7 @@ export class GobanCanvas extends GobanCore implements GobanCanvasInterface { this.engine.removal[j][i] ) { color = this.engine.board[j][i]; - transparent = true; + translucent = true; } else if (stone_color) { color = stone_color; } else if ( @@ -1593,12 +1616,16 @@ export class GobanCanvas extends GobanCore implements GobanCanvasInterface { } } else if (pos.black || pos.white) { color = pos.black ? 1 : 2; - transparent = true; + translucent = true; stoneAlphaValue = this.variation_stone_opacity; } else { color = this.engine.player; } + if (pos.stone_removed) { + translucent = true; + } + if (!(this.autoplaying_puzzle_move && !stone_color)) { text_color = color === 1 ? this.theme_black_text_color : this.theme_white_text_color; @@ -1628,7 +1655,7 @@ export class GobanCanvas extends GobanCore implements GobanCanvasInterface { ctx.save(); let shadow_ctx: CanvasRenderingContext2D | null | undefined = this.shadow_ctx; - if (!stone_color || transparent) { + if (!stone_color || translucent) { ctx.globalAlpha = stoneAlphaValue; shadow_ctx = null; } @@ -1669,14 +1696,8 @@ export class GobanCanvas extends GobanCore implements GobanCanvasInterface { ctx.restore(); } - if ( - pos.blue_move && - this.colored_circles && - this.colored_circles[j] && - this.colored_circles[j][i] - ) { - const circle = this.colored_circles[j][i]; - + const circle = this.colored_circles?.[j]?.[i]; + if (pos.blue_move && circle) { ctx.save(); ctx.globalAlpha = 1.0; const radius = Math.floor(this.square_size * 0.5) - 0.5; @@ -1703,37 +1724,91 @@ export class GobanCanvas extends GobanCore implements GobanCanvasInterface { } ctx.restore(); } + + /* Red X if the stone is marked for removal */ + if ( + (this.engine && + this.engine.phase === "stone removal" && + this.engine.last_official_move === this.engine.cur_move && + this.engine.board[j][i] && + this.engine.removal[j][i]) || + (this.scoring_mode && + this.score_estimator && + this.score_estimator.board[j][i] && + this.score_estimator.removal[j][i]) || + pos.stone_removed + ) { + draw_red_x = true; + } } } + if ( + draw_red_x || + (this.mode === "analyze" && + this.analyze_tool === "removal" && + this.last_hover_square && + this.last_hover_square.x === i && + this.last_hover_square.y === j) || + (this.engine.phase === "stone removal" && + this.engine.isActivePlayer(this.player_id) && + this.engine.cur_move === this.engine.last_official_move && + this.last_hover_square && + this.last_hover_square.x === i && + this.last_hover_square.y === j) + ) { + const opacity = this.engine.board[j][i] ? 1.0 : 0.2; + ctx.lineCap = "square"; + ctx.save(); + ctx.beginPath(); + ctx.lineWidth = this.square_size * 0.125; + ctx.globalAlpha = opacity; + const r = Math.max(1, this.metrics.mid * 0.65); + ctx.moveTo(cx - r, cy - r); + ctx.lineTo(cx + r, cy + r); + ctx.moveTo(cx + r, cy - r); + ctx.lineTo(cx - r, cy + r); + ctx.strokeStyle = "#ff0000"; + ctx.stroke(); + ctx.restore(); + draw_last_move = false; + } /* Draw Scores */ { if ( - (pos.score && (this.engine.phase !== "finished" || this.mode === "play")) || + (pos.score && + (this.engine.phase !== "finished" || + this.mode === "play" || + this.mode === "analyze")) || (this.scoring_mode && - this.score_estimate && - (this.score_estimate.territory[j][i] || - (this.score_estimate.removal[j][i] && - this.score_estimate.board[j][i] === 0))) || + this.score_estimator && + (this.score_estimator.territory[j][i] || + (this.score_estimator.removal[j][i] && + this.score_estimator.board[j][i] === 0))) || ((this.engine.phase === "stone removal" || (this.engine.phase === "finished" && this.mode === "play")) && this.engine.board[j][i] === 0 && - this.engine.removal[j][i]) + (this.engine.removal[j][i] || pos.needs_sealing)) || + (this.mode === "analyze" && + this.analyze_tool === "score" && + this.last_hover_square && + this.last_hover_square.x === i && + this.last_hover_square.y === j) ) { ctx.beginPath(); let color = pos.score; if ( this.scoring_mode && - this.score_estimate && - (this.score_estimate.territory[j][i] || - (this.score_estimate.removal[j][i] && - this.score_estimate.board[j][i] === 0)) + this.score_estimator && + (this.score_estimator.territory[j][i] || + (this.score_estimator.removal[j][i] && + this.score_estimator.board[j][i] === 0)) ) { - color = this.score_estimate.territory[j][i] === 1 ? "black" : "white"; + color = this.score_estimator.territory[j][i] === 1 ? "black" : "white"; if ( - this.score_estimate.board[j][i] === 0 && - this.score_estimate.removal[j][i] + this.score_estimator.board[j][i] === 0 && + this.score_estimator.removal[j][i] ) { color = "dame"; } @@ -1748,6 +1823,20 @@ export class GobanCanvas extends GobanCore implements GobanCanvasInterface { color = "dame"; } + if (pos.needs_sealing) { + color = "seal"; + } + + if ( + this.mode === "analyze" && + this.analyze_tool === "score" && + this.last_hover_square && + this.last_hover_square.x === i && + this.last_hover_square.y === j + ) { + color = this.analyze_subtool; + } + if (color === "white") { ctx.fillStyle = this.theme_black_text_color; ctx.strokeStyle = "#777777"; @@ -1757,6 +1846,13 @@ export class GobanCanvas extends GobanCore implements GobanCanvasInterface { } else if (color === "dame") { ctx.fillStyle = "#ff0000"; ctx.strokeStyle = "#365FE6"; + } else if (color === "seal") { + ctx.fillStyle = "#ff0000"; + ctx.strokeStyle = "#E079CE"; + } + if (color?.[0] === "#") { + ctx.fillStyle = color; + ctx.strokeStyle = color_blend("#888888", color); } ctx.lineWidth = Math.ceil(this.square_size * 0.065) - 0.5; @@ -2102,7 +2198,7 @@ export class GobanCanvas extends GobanCore implements GobanCanvasInterface { /* Score Estimation */ if ( - (this.scoring_mode === true && this.score_estimate) || + (this.scoring_mode === true && this.score_estimator) || (this.scoring_mode === "stalling-scoring-mode" && this.stalling_score_estimate && this.mode !== "analyze") @@ -2110,7 +2206,7 @@ export class GobanCanvas extends GobanCore implements GobanCanvasInterface { const se = this.scoring_mode === "stalling-scoring-mode" ? this.stalling_score_estimate - : this.score_estimate; + : this.score_estimator; const est = se!.ownership[j][i]; ctx.beginPath(); @@ -2162,11 +2258,9 @@ export class GobanCanvas extends GobanCore implements GobanCanvasInterface { } /* Colored stones */ - if (this.colored_circles) { - if (this.colored_circles[j][i]) { - const circle = this.colored_circles[j][i]; - ret += "circle " + circle.color; - } + const circle = this.colored_circles?.[j][i]; + if (circle) { + ret += "circle " + circle.color; } /* Figure out marks for this spot */ @@ -2201,6 +2295,7 @@ export class GobanCanvas extends GobanCore implements GobanCanvasInterface { } /* Draw stones & hovers */ + let draw_red_x = false; { if ( stone_color /* if there is really a stone here */ || @@ -2218,9 +2313,9 @@ export class GobanCanvas extends GobanCore implements GobanCanvasInterface { (this.getPuzzlePlacementSetting && this.getPuzzlePlacementSetting().mode !== "play"))) || (this.scoring_mode && - this.score_estimate && - this.score_estimate.board[j][i] && - this.score_estimate.removal[j][i]) || + this.score_estimator && + this.score_estimator.board[j][i] && + this.score_estimator.removal[j][i]) || (this.engine && this.engine.phase === "stone removal" && this.engine.board[j][i] && @@ -2228,16 +2323,16 @@ export class GobanCanvas extends GobanCore implements GobanCanvasInterface { pos.black || pos.white ) { - let transparent = false; + let translucent = false; let color; if ( this.scoring_mode && - this.score_estimate && - this.score_estimate.board[j][i] && - this.score_estimate.removal[j][i] + this.score_estimator && + this.score_estimator.board[j][i] && + this.score_estimator.removal[j][i] ) { - color = this.score_estimate.board[j][i]; - transparent = true; + color = this.score_estimator.board[j][i]; + translucent = true; } else if ( this.engine && this.engine.phase === "stone removal" && @@ -2247,7 +2342,7 @@ export class GobanCanvas extends GobanCore implements GobanCanvasInterface { this.engine.removal[j][i] ) { color = this.engine.board[j][i]; - transparent = true; + translucent = true; } else if (stone_color) { color = stone_color; } else if ( @@ -2264,11 +2359,16 @@ export class GobanCanvas extends GobanCore implements GobanCanvasInterface { } } else if (pos.black || pos.white) { color = pos.black ? 1 : 2; - transparent = true; + translucent = true; } else { color = this.engine.player; } + //if (this.mode === "analyze" && pos.stone_removed) { + if (pos.stone_removed) { + translucent = true; + } + if (color === 1) { ret += this.theme_black.getStoneHash(i, j, this.theme_black_stones, this); } @@ -2276,10 +2376,43 @@ export class GobanCanvas extends GobanCore implements GobanCanvasInterface { ret += this.theme_white.getStoneHash(i, j, this.theme_white_stones, this); } - ret += (transparent ? "T" : "") + color + ","; + if ( + (this.engine && + this.engine.phase === "stone removal" && + this.engine.last_official_move === this.engine.cur_move && + this.engine.board[j][i] && + this.engine.removal[j][i]) || + (this.scoring_mode && + this.score_estimator && + this.score_estimator.board[j][i] && + this.score_estimator.removal[j][i]) || + //(this.mode === "analyze" && pos.stone_removed) + pos.stone_removed + ) { + draw_red_x = true; + } + + ret += (translucent ? "T" : "") + color + ","; } } + if ( + draw_red_x || + (this.mode === "analyze" && + this.analyze_tool === "removal" && + this.last_hover_square && + this.last_hover_square.x === i && + this.last_hover_square.y === j) || + (this.engine.phase === "stone removal" && + this.engine.isActivePlayer(this.player_id) && + this.engine.cur_move === this.engine.last_official_move && + this.last_hover_square && + this.last_hover_square.x === i && + this.last_hover_square.y === j) + ) { + ret += "redX"; + } + /* Draw square highlights if any */ { if (pos.hint || (this.highlight_movetree_moves && movetree_contains_this_square)) { @@ -2313,7 +2446,7 @@ export class GobanCanvas extends GobanCore implements GobanCanvasInterface { transparent = false; } - if (this.scoring_mode && this.score_estimate && this.score_estimate.removal[j][i]) { + if (this.scoring_mode && this.score_estimator && this.score_estimator.removal[j][i]) { draw_x = true; transparent = false; } @@ -2329,29 +2462,37 @@ export class GobanCanvas extends GobanCore implements GobanCanvasInterface { /* Draw Scores */ { if ( - (pos.score && (this.engine.phase !== "finished" || this.mode === "play")) || + (pos.score && + (this.engine.phase !== "finished" || + this.mode === "play" || + this.mode === "analyze")) || (this.scoring_mode && - this.score_estimate && - (this.score_estimate.territory[j][i] || - (this.score_estimate.removal[j][i] && - this.score_estimate.board[j][i] === 0))) || + this.score_estimator && + (this.score_estimator.territory[j][i] || + (this.score_estimator.removal[j][i] && + this.score_estimator.board[j][i] === 0))) || ((this.engine.phase === "stone removal" || (this.engine.phase === "finished" && this.mode === "play")) && this.engine.board[j][i] === 0 && - this.engine.removal[j][i]) + (this.engine.removal[j][i] || pos.needs_sealing)) || + (this.mode === "analyze" && + this.analyze_tool === "score" && + this.last_hover_square && + this.last_hover_square.x === i && + this.last_hover_square.y === j) ) { let color = pos.score; if ( this.scoring_mode && - this.score_estimate && - (this.score_estimate.territory[j][i] || - (this.score_estimate.removal[j][i] && - this.score_estimate.board[j][i] === 0)) + this.score_estimator && + (this.score_estimator.territory[j][i] || + (this.score_estimator.removal[j][i] && + this.score_estimator.board[j][i] === 0)) ) { - color = this.score_estimate.territory[j][i] === 1 ? "black" : "white"; + color = this.score_estimator.territory[j][i] === 1 ? "black" : "white"; if ( - this.score_estimate.board[j][i] === 0 && - this.score_estimate.removal[j][i] + this.score_estimator.board[j][i] === 0 && + this.score_estimator.removal[j][i] ) { color = "dame"; } @@ -2366,12 +2507,26 @@ export class GobanCanvas extends GobanCore implements GobanCanvasInterface { color = "dame"; } + if (pos.needs_sealing) { + color = "seal"; + } + + if ( + this.mode === "analyze" && + this.analyze_tool === "score" && + this.last_hover_square && + this.last_hover_square.x === i && + this.last_hover_square.y === j + ) { + color = this.analyze_subtool; + } + if ( this.scoring_mode && - this.score_estimate && - this.score_estimate.territory[j][i] + this.score_estimator && + this.score_estimator.territory[j][i] ) { - color = this.score_estimate.territory[j][i] === 1 ? "black" : "white"; + color = this.score_estimator.territory[j][i] === 1 ? "black" : "white"; } ret += "score " + color + ","; } @@ -2480,7 +2635,7 @@ export class GobanCanvas extends GobanCore implements GobanCanvasInterface { /* Score Estimation */ if ( - (this.scoring_mode === true && this.score_estimate) || + (this.scoring_mode === true && this.score_estimator) || (this.scoring_mode === "stalling-scoring-mode" && this.stalling_score_estimate && this.mode !== "analyze") @@ -2488,7 +2643,7 @@ export class GobanCanvas extends GobanCore implements GobanCanvasInterface { const se = this.scoring_mode === "stalling-scoring-mode" ? this.stalling_score_estimate - : this.score_estimate; + : this.score_estimator; const est = se!.ownership[j][i]; ret += est.toFixed(5) + ","; @@ -2560,7 +2715,7 @@ export class GobanCanvas extends GobanCore implements GobanCanvasInterface { throw new Error(`Failed to obtain drawing context for board`); } - this.setThemes(this.getSelectedThemes(), true); + this.setTheme(this.getSelectedThemes(), true); } catch (e) { setTimeout(() => { throw e; @@ -2608,7 +2763,7 @@ export class GobanCanvas extends GobanCore implements GobanCanvasInterface { this.square_size + this.square_size / 2; const y = j * this.square_size + this.square_size / 2; - place(GoMath.pretty_coor_num2ch(c), x, y); + place(encodePrettyXCoordinate(c), x, y); } break; case "1-1": @@ -2732,7 +2887,7 @@ export class GobanCanvas extends GobanCore implements GobanCanvasInterface { this.__draw_state.length !== this.height || this.__draw_state[0].length !== this.width ) { - this.__draw_state = GoMath.makeStringMatrix(this.width, this.height); + this.__draw_state = makeMatrix(this.width, this.height, ""); } /* Set font for text overlay */ @@ -2818,15 +2973,15 @@ export class GobanCanvas extends GobanCore implements GobanCanvasInterface { this.emit("clear-message"); } - protected setThemes(themes: GobanSelectedThemes, dont_redraw: boolean): void { + protected setTheme(themes: GobanSelectedThemes, dont_redraw: boolean): void { if (this.no_display) { return; } this.themes = themes; - const BoardTheme = GoThemes["board"]?.[themes.board] || GoThemes["board"]["Plain"]; - const WhiteTheme = GoThemes["white"]?.[themes.white] || GoThemes["white"]["Plain"]; - const BlackTheme = GoThemes["black"]?.[themes.black] || GoThemes["black"]["Plain"]; + const BoardTheme = THEMES["board"]?.[themes.board] || THEMES["board"]["Plain"]; + const WhiteTheme = THEMES["white"]?.[themes.white] || THEMES["white"]["Plain"]; + const BlackTheme = THEMES["black"]?.[themes.black] || THEMES["black"]["Plain"]; this.theme_board = new BoardTheme(); this.theme_white = new WhiteTheme(this.theme_board); this.theme_black = new BlackTheme(this.theme_board); @@ -2992,7 +3147,7 @@ export class GobanCanvas extends GobanCore implements GobanCanvasInterface { } private onLabelingStart(ev: MouseEvent | TouchEvent) { const pos = getRelativeEventPosition(ev); - this.last_label_position = this.xy2ij(pos.x, pos.y); + this.last_label_position = this.xy2ij(pos.x, pos.y, false); { const x = this.last_label_position.i; @@ -3048,8 +3203,8 @@ export class GobanCanvas extends GobanCore implements GobanCanvasInterface { protected watchSelectedThemes(cb: (themes: GobanSelectedThemes) => void): { remove: () => any; } { - if (GobanCore.hooks.watchSelectedThemes) { - return GobanCore.hooks.watchSelectedThemes(cb); + if (callbacks.watchSelectedThemes) { + return callbacks.watchSelectedThemes(cb); } return { remove: () => {} }; } @@ -3343,11 +3498,7 @@ export class GobanCanvas extends GobanCore implements GobanCanvasInterface { const text_color = color === 1 ? this.theme_black_text_color : this.theme_white_text_color; let label = ""; - switch ( - GobanCore.hooks.getMoveTreeNumbering - ? GobanCore.hooks.getMoveTreeNumbering() - : "move-number" - ) { + switch (callbacks.getMoveTreeNumbering ? callbacks.getMoveTreeNumbering() : "move-number") { case "move-coordinates": label = node.pretty_coordinates; break; diff --git a/src/Goban/Goban.ts b/src/Goban/Goban.ts new file mode 100644 index 00000000..8953866a --- /dev/null +++ b/src/Goban/Goban.ts @@ -0,0 +1,371 @@ +/* + * Copyright (C) Online-Go.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { MARK_TYPES } from "./InteractiveBase"; +import { OGSConnectivity } from "./OGSConnectivity"; +import { GobanConfig } from "../GobanBase"; +import { callbacks } from "./callbacks"; +import { makeMatrix, StoneStringBuilder } from "engine"; +import { getRelativeEventPosition } from "./canvas_utils"; +import { THEMES, THEMES_SORTED } from "./themes"; + +export const GOBAN_FONT = "Verdana,Arial,sans-serif"; +export interface GobanSelectedThemes { + board: string; + white: string; + black: string; +} +export type LabelPosition = + | "all" + | "none" + | "top-left" + | "top-right" + | "bottom-right" + | "bottom-left"; + +export interface GobanMetrics { + width: number; + height: number; + mid: number; + offset: number; +} + +/** + * Goban serves as a base class for our renderers as well as a namespace for various + * classes, types, and enums. + * + * You can't create an instance of a Goban directly, you have to create an instance of + * one of the renderers, such as GobanSVG. + */ +export abstract class Goban extends OGSConnectivity { + static THEMES = THEMES; + static THEMES_SORTED = THEMES_SORTED; + + protected abstract setTheme(themes: GobanSelectedThemes, dont_redraw: boolean): void; + + protected parent!: HTMLElement; + private analysis_scoring_color?: "black" | "white" | string; + private analysis_scoring_last_position: { i: number; j: number } = { i: NaN, j: NaN }; + + constructor(config: GobanConfig, preloaded_data?: GobanConfig) { + super(config, preloaded_data); + + if (config.display_width && this.original_square_size === "auto") { + const suppress_redraw = true; + this.setSquareSizeBasedOnDisplayWidth(config.display_width, suppress_redraw); + } + + this.on("load", (_config) => { + if (this.display_width && this.original_square_size === "auto") { + const suppress_redraw = true; + this.setSquareSizeBasedOnDisplayWidth(this.display_width, suppress_redraw); + } + }); + } + public override destroy(): void { + super.destroy(); + } + + protected getSelectedThemes(): GobanSelectedThemes { + if (callbacks.getSelectedThemes) { + return callbacks.getSelectedThemes(); + } + //return {white:'Plain', black:'Plain', board:'Plain'}; + //return {white:'Plain', black:'Plain', board:'Kaya'}; + return { white: "Shell", black: "Slate", board: "Kaya" }; + } + + protected putOrClearLabel(x: number, y: number, mode?: "put" | "clear"): boolean { + let ret = false; + if (mode == null || typeof mode === "undefined") { + if (this.analyze_subtool === "letters" || this.analyze_subtool === "numbers") { + this.label_mark = this.label_character; + ret = this.toggleMark(x, y, this.label_character, true); + if (ret === true) { + this.incrementLabelCharacter(); + } else { + this.setLabelCharacterFromMarks(); + } + } else { + this.label_mark = this.analyze_subtool; + ret = this.toggleMark(x, y, this.analyze_subtool); + } + } else { + if (mode === "put") { + ret = this.toggleMark(x, y, this.label_mark, this.label_mark.length <= 3, true); + } else { + const marks = this.getMarks(x, y); + + for (let i = 0; i < MARK_TYPES.length; ++i) { + delete marks[MARK_TYPES[i]]; + } + this.drawSquare(x, y); + } + } + + this.syncReviewMove(); + return ret; + } + + protected getAnalysisScoreColorAtLocation( + x: number, + y: number, + ): "black" | "white" | string | undefined { + return this.getMarks(x, y).score; + } + protected putAnalysisScoreColorAtLocation( + x: number, + y: number, + color?: "black" | "white" | string, + sync_review_move: boolean = true, + ): void { + const marks = this.getMarks(x, y); + marks.score = color; + this.drawSquare(x, y); + if (sync_review_move) { + this.syncReviewMove(); + } + } + protected putAnalysisRemovalAtLocation(x: number, y: number, removal?: boolean): void { + const marks = this.getMarks(x, y); + marks.remove = removal; + marks.stone_removed = removal; + this.drawSquare(x, y); + this.syncReviewMove(); + } + + /** Marks scores on the board when in analysis mode. Note: this will not + * clear existing scores, this is intentional as I think it's the expected + * behavior of reviewers */ + public markAnalysisScores() { + if (this.mode !== "analyze") { + console.error("markAnalysisScores called when not in analyze mode"); + return; + } + + /* Clear any previous auto-markings */ + if (this.marked_analysis_score) { + for (let x = 0; x < this.width; ++x) { + for (let y = 0; y < this.height; ++y) { + if (this.marked_analysis_score[y][x]) { + this.putAnalysisScoreColorAtLocation(x, y, undefined, false); + } + } + } + } + + this.marked_analysis_score = makeMatrix(this.width, this.height, false); + + const board_state = this.engine.cloneBoardState(); + + for (let x = 0; x < this.width; ++x) { + for (let y = 0; y < this.height; ++y) { + board_state.removal[y][x] ||= !!this.getMarks(x, y).stone_removed; + } + } + + const territory_scoring = + this.engine.rules === "japanese" || this.engine.rules === "korean"; + const scores = board_state.computeScoringLocations(!territory_scoring); + for (const color of ["black", "white"] as ("black" | "white")[]) { + for (const loc of scores[color].locations) { + this.putAnalysisScoreColorAtLocation(loc.x, loc.y, color, false); + this.marked_analysis_score[loc.y][loc.x] = true; + } + } + this.syncReviewMove(); + } + + public setSquareSizeBasedOnDisplayWidth(display_width: number, suppress_redraw = false): void { + let n_squares = Math.max( + this.bounded_width + +this.draw_left_labels + +this.draw_right_labels, + this.bounded_height + +this.draw_bottom_labels + +this.draw_top_labels, + ); + this.display_width = display_width; + + if (isNaN(this.display_width)) { + console.error("Invalid display width. (NaN)"); + this.display_width = 320; + } + + if (isNaN(n_squares)) { + console.error("Invalid n_squares: ", n_squares); + console.error("bounded_width: ", this.bounded_width); + console.error("this.draw_left_labels: ", this.draw_left_labels); + console.error("this.draw_right_labels: ", this.draw_right_labels); + console.error("bounded_height: ", this.bounded_height); + console.error("this.draw_top_labels: ", this.draw_top_labels); + console.error("this.draw_bottom_labels: ", this.draw_bottom_labels); + n_squares = 19; + } + + this.setSquareSize(Math.floor(this.display_width / n_squares), suppress_redraw); + } + + public setLabelPosition(label_position: LabelPosition) { + this.draw_top_labels = label_position === "all" || label_position.indexOf("top") >= 0; + this.draw_left_labels = label_position === "all" || label_position.indexOf("left") >= 0; + this.draw_right_labels = label_position === "all" || label_position.indexOf("right") >= 0; + this.draw_bottom_labels = label_position === "all" || label_position.indexOf("bottom") >= 0; + this.setSquareSizeBasedOnDisplayWidth(Number(this.display_width)); + this.redraw(true); + } + + protected onAnalysisToggleStoneRemoval(ev: MouseEvent | TouchEvent) { + const pos = getRelativeEventPosition(ev, this.parent); + this.analysis_removal_last_position = this.xy2ij(pos.x, pos.y, false); + const { i, j } = this.analysis_removal_last_position; + const x = i; + const y = j; + + if (!(x >= 0 && x < this.width && y >= 0 && y < this.height)) { + return; + } + + const existing_removal_state = this.getMarks(x, y).stone_removed; + + if (existing_removal_state) { + this.analysis_removal_state = undefined; + } else { + this.analysis_removal_state = true; + } + + const all_strings = new StoneStringBuilder(this.engine); + const stone_string = all_strings.getGroup(x, y); + + stone_string.map((loc) => { + this.putAnalysisRemovalAtLocation(loc.x, loc.y, this.analysis_removal_state); + }); + + // If we have any scores on the board, we assume we are interested in those + // and we recompute scores, updating + const have_any_scores = this.marked_analysis_score?.some((row) => row.includes(true)); + + if (have_any_scores) { + this.markAnalysisScores(); + } + } + + /** Clears any analysis scores on the board */ + public clearAnalysisScores() { + delete this.marked_analysis_score; + if (this.mode !== "analyze") { + console.error("clearAnalysisScores called when not in analyze mode"); + return; + } + for (let x = 0; x < this.width; ++x) { + for (let y = 0; y < this.height; ++y) { + this.putAnalysisScoreColorAtLocation(x, y, undefined, false); + } + } + this.syncReviewMove(); + } + + public setSquareSize(new_ss: number, suppress_redraw = false): void { + const redraw = this.square_size !== new_ss && !suppress_redraw; + this.square_size = Math.max(new_ss, 1); + if (redraw) { + this.redraw(true); + } + } + public computeMetrics(): GobanMetrics { + if (!this.square_size || this.square_size <= 0) { + this.square_size = 12; + } + + const ret = { + width: + this.square_size * + (this.bounded_width + +this.draw_left_labels + +this.draw_right_labels), + height: + this.square_size * + (this.bounded_height + +this.draw_top_labels + +this.draw_bottom_labels), + mid: this.square_size / 2, + offset: 0, + }; + + if (this.square_size % 2 === 0) { + ret.mid -= 0.5; + ret.offset = 0.5; + } + + return ret; + } + + protected onAnalysisScoringStart(ev: MouseEvent | TouchEvent) { + const pos = getRelativeEventPosition(ev, this.parent); + this.analysis_scoring_last_position = this.xy2ij(pos.x, pos.y, false); + + { + const x = this.analysis_scoring_last_position.i; + const y = this.analysis_scoring_last_position.j; + if (!(x >= 0 && x < this.width && y >= 0 && y < this.height)) { + return; + } + } + + const existing_color = this.getAnalysisScoreColorAtLocation( + this.analysis_scoring_last_position.i, + this.analysis_scoring_last_position.j, + ); + + if (existing_color === this.analyze_subtool) { + this.analysis_scoring_color = undefined; + } else { + this.analysis_scoring_color = this.analyze_subtool; + } + + this.putAnalysisScoreColorAtLocation( + this.analysis_scoring_last_position.i, + this.analysis_scoring_last_position.j, + this.analysis_scoring_color, + ); + + /* clear hover */ + if (this.__last_pt.valid) { + const last_hover = this.last_hover_square; + delete this.last_hover_square; + if (last_hover) { + this.drawSquare(last_hover.x, last_hover.y); + } + } + this.__last_pt = this.xy2ij(-1, -1); + this.drawSquare( + this.analysis_scoring_last_position.i, + this.analysis_scoring_last_position.j, + ); + } + protected onAnalysisScoringMove(ev: MouseEvent | TouchEvent) { + const pos = getRelativeEventPosition(ev, this.parent); + const cur = this.xy2ij(pos.x, pos.y); + + { + const x = cur.i; + const y = cur.j; + if (!(x >= 0 && x < this.width && y >= 0 && y < this.height)) { + return; + } + } + + if ( + cur.i !== this.analysis_scoring_last_position.i || + cur.j !== this.analysis_scoring_last_position.j + ) { + this.analysis_scoring_last_position = cur; + this.putAnalysisScoreColorAtLocation(cur.i, cur.j, this.analysis_scoring_color); + } + } +} diff --git a/src/Goban/InteractiveBase.ts b/src/Goban/InteractiveBase.ts new file mode 100644 index 00000000..66a55093 --- /dev/null +++ b/src/Goban/InteractiveBase.ts @@ -0,0 +1,1804 @@ +/* + * Copyright (C) Online-Go.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + GobanEngine, + GobanEnginePhase, + GobanEngineRules, + ReviewMessage, + PlayerColor, + PuzzlePlacementSetting, + Score, + ConditionalMoveTree, + GobanMoveError, +} from "engine"; +import { NumberMatrix, encodeMove, makeMatrix, makeEmptyMatrix } from "engine/util"; +import { MoveTree, MarkInterface } from "engine/MoveTree"; +import { ScoreEstimator } from "engine/ScoreEstimator"; +import { computeAverageMoveTime, niceInterval, matricesAreEqual } from "engine/util"; +import { _ } from "engine/translate"; +import { + JGOFIntersection, + JGOFPlayerClock, + JGOFTimeControlSystem, + JGOFNumericPlayerColor, +} from "engine/formats/JGOF"; +import { AdHocClock, AdHocPauseControl } from "engine/formats/AdHocFormat"; +import { ServerToClient, StallingScoreEstimate } from "engine/protocol"; +import { callbacks } from "./callbacks"; +import { + GobanBase, + AnalysisTool, + AnalysisSubTool, + GobanModes, + GobanChatLog, + GobanBounds, + GobanConfig, + JGOFClockWithTransmitting, +} from "../GobanBase"; + +declare let swal: any; + +export const SCORE_ESTIMATION_TRIALS = 1000; +export const SCORE_ESTIMATION_TOLERANCE = 0.3; +export const MARK_TYPES: Array = [ + "letter", + "circle", + "square", + "triangle", + "sub_triangle", + "cross", + "black", + "white", + "score", + "stone_removed", +]; + +export interface ColoredCircle { + move: JGOFIntersection; + color: string; + border_width?: number; + border_color?: string; +} + +export interface AudioClockEvent { + /** Number of seconds left in the current period */ + countdown_seconds: number; + + /** Full player clock information */ + clock: JGOFPlayerClock; + + /** The player (id) whose turn it is */ + player_id: string; + + /** The player whose turn it is */ + color: PlayerColor; + + /** Time control system being used by the clock */ + time_control_system: JGOFTimeControlSystem; + + /** True if we are in overtime. This is only ever set for systems that have + * a concept of overtime. + */ + in_overtime: boolean; +} + +export interface MoveCommand { + //game_id?: number | string; + game_id: number; + move: string; + blur?: number; + clock?: JGOFPlayerClock; +} + +export interface StateUpdateEvents { + mode: (d: GobanModes) => void; + title: (d: string) => void; + phase: (d: GobanEnginePhase) => void; + cur_move: (d: MoveTree) => void; + cur_review_move: (d: MoveTree | undefined) => void; + last_official_move: (d: MoveTree) => void; + submit_move: (d: (() => void) | undefined) => void; + analyze_tool: (d: AnalysisTool) => void; + analyze_subtool: (d: AnalysisSubTool) => void; + score_estimate: (d: ScoreEstimator | null) => void; + strict_seki_mode: (d: boolean) => void; + rules: (d: GobanEngineRules) => void; + winner: (d: number | undefined) => void; + undo_requested: (d: number | undefined) => void; // move number of the last undo request + undo_canceled: () => void; + paused: (d: boolean) => void; + outcome: (d: string) => void; + review_owner_id: (d: number | undefined) => void; + review_controller_id: (d: number | undefined) => void; + stalling_score_estimate: ServerToClient["game/:id/stalling_score_estimate"]; +} + +/** + * This class serves as a functionality layer encapsulating core interactions + * we do with a Goban, we have it as a separate base class simply to help with + * code organization and to keep our Goban class size down. + */ +export abstract class GobanInteractive extends GobanBase { + public abstract sendTimedOut(): void; + public abstract sent_timed_out_message: boolean; /// Expected to be true if sendTimedOut has been called + protected abstract sendMove(mv: MoveCommand, cb?: () => void): boolean; + + public conditional_starting_color: "black" | "white" | "invalid" = "invalid"; + public conditional_tree: ConditionalMoveTree = new ConditionalMoveTree(null); + public double_click_submit: boolean; + public variation_stone_opacity: number; + public draw_bottom_labels: boolean; + public draw_left_labels: boolean; + public draw_right_labels: boolean; + public draw_top_labels: boolean; + public visual_undo_request_indicator: boolean; + public height: number; + public last_clock?: AdHocClock; + public last_emitted_clock?: JGOFClockWithTransmitting; + public clock_should_be_paused_for_move_submission: boolean = false; + public previous_mode: string; + public one_click_submit: boolean; + public pen_marks: Array; + public readonly game_id: number; + public readonly review_id: number; + public showing_scores: boolean = false; + public stalling_score_estimate?: StallingScoreEstimate; + public width: number; + + public pause_control?: AdHocPauseControl; + public paused_since?: number; + public chat_log: GobanChatLog = []; + + protected last_paused_state: boolean | null = null; + protected last_paused_by_player_state: boolean | null = null; + protected analysis_removal_state?: boolean; + protected analysis_removal_last_position: { i: number; j: number } = { i: NaN, j: NaN }; + protected marked_analysis_score?: boolean[][]; + + /* Properties that emit change events */ + private _mode: GobanModes = "play"; + public get mode(): GobanModes { + return this._mode; + } + public set mode(mode: GobanModes) { + if (this._mode === mode) { + return; + } + this._mode = mode; + this.emit("mode", this.mode); + } + + private _title: string = "play"; + public get title(): string { + return this._title; + } + public set title(title: string) { + if (this._title === title) { + return; + } + this._title = title; + this.emit("title", this.title); + } + + private _submit_move?: () => void; + public get submit_move(): (() => void) | undefined { + return this._submit_move; + } + public set submit_move(submit_move: (() => void) | undefined) { + if (this._submit_move === submit_move) { + return; + } + this._submit_move = submit_move; + this.emit("submit_move", this.submit_move); + } + + private _analyze_tool: AnalysisTool = "stone"; + public get analyze_tool(): AnalysisTool { + return this._analyze_tool; + } + public set analyze_tool(analyze_tool: AnalysisTool) { + if (this._analyze_tool === analyze_tool) { + return; + } + this._analyze_tool = analyze_tool; + this.emit("analyze_tool", this.analyze_tool); + } + + private _analyze_subtool: AnalysisSubTool = "alternate"; + public get analyze_subtool(): AnalysisSubTool { + return this._analyze_subtool; + } + public set analyze_subtool(analyze_subtool: AnalysisSubTool) { + if (this._analyze_subtool === analyze_subtool) { + return; + } + this._analyze_subtool = analyze_subtool; + this.emit("analyze_subtool", this.analyze_subtool); + } + + private _score_estimator: ScoreEstimator | null = null; + public get score_estimator(): ScoreEstimator | null { + return this._score_estimator; + } + public set score_estimator(score_estimate: ScoreEstimator | null) { + if (this._score_estimator === score_estimate) { + return; + } + this._score_estimator = score_estimate; + this.emit("score_estimate", this.score_estimator); + this._score_estimator?.when_ready + .then(() => { + this.emit("score_estimate", this.score_estimator); + }) + .catch(() => { + return; + }); + } + + private _review_owner_id?: number; + public get review_owner_id(): number | undefined { + return this._review_owner_id; + } + public set review_owner_id(review_owner_id: number | undefined) { + if (this._review_owner_id === review_owner_id) { + return; + } + this._review_owner_id = review_owner_id; + this.emit("review_owner_id", this.review_owner_id); + } + + private _review_controller_id?: number; + public get review_controller_id(): number | undefined { + return this._review_controller_id; + } + public set review_controller_id(review_controller_id: number | undefined) { + if (this._review_controller_id === review_controller_id) { + return; + } + this._review_controller_id = review_controller_id; + this.emit("review_controller_id", this.review_controller_id); + } + + protected __board_redraw_pen_layer_timer: any = null; + protected __clock_timer?: ReturnType; + protected __draw_state: string[][]; + protected __last_pt: { i: number; j: number; valid: boolean } = { i: -1, j: -1, valid: false }; + protected __update_move_tree: any = null; /* timer */ + protected analysis_move_counter: number; + protected stone_removal_auto_scoring_done?: boolean = false; + protected bounded_height: number; + protected bounded_width: number; + protected bounds: GobanBounds; + protected conditional_path: string = ""; + public config: GobanConfig; + protected current_cmove?: ConditionalMoveTree; + protected currently_my_cmove: boolean = false; + protected dirty_redraw: any = null; // timer + protected disconnectedFromGame: boolean = true; + protected display_width?: number; + protected done_loading_review: boolean = false; + protected dont_draw_last_move: boolean; + protected last_move_radius: number; + protected circle_radius: number; + protected edit_color?: "black" | "white"; + protected errorHandler: (e: Error) => void; + protected heatmap?: NumberMatrix; + protected colored_circles?: Array>; + protected game_type: string; + protected getPuzzlePlacementSetting?: () => PuzzlePlacementSetting; + protected highlight_movetree_moves: boolean; + protected interactive: boolean; + protected isInPushedAnalysis: () => boolean; + protected leavePushedAnalysis: () => void; + protected isPlayerController: () => boolean; + protected isPlayerOwner: () => boolean; + protected label_character: string; + protected label_mark: string = "[UNSET]"; + protected last_hover_square?: JGOFIntersection; + protected last_move?: MoveTree; + protected last_phase?: GobanEnginePhase; + protected last_review_message: ReviewMessage; + protected last_sound_played_for_a_stone_placement?: string; + protected last_stone_sound: number; + protected move_selected?: JGOFIntersection; + protected no_display: boolean; + protected onError?: (error: Error) => void; + protected on_game_screen: boolean; + protected original_square_size: number | ((goban: GobanBase) => number) | "auto"; + protected player_id: number; + protected puzzle_autoplace_delay: number; + protected restrict_moves_to_movetree: boolean; + protected review_had_gamedata: boolean; + protected scoring_mode: boolean | "stalling-scoring-mode"; + protected shift_key_is_down: boolean; + protected show_move_numbers: boolean; + protected show_variation_move_numbers: boolean; + protected square_size: number = 10; + protected stone_placement_enabled: boolean; + protected sendLatencyTimer?: ReturnType; + + protected abstract setTitle(title: string): void; + protected abstract enableDrawing(): void; + protected abstract disableDrawing(): void; + + protected preloaded_data?: GobanConfig; + + constructor(config: GobanConfig, preloaded_data?: GobanConfig) { + super(); + + this.preloaded_data = preloaded_data; + + this.on("clock", (clock) => { + if (clock) { + this.last_emitted_clock = clock; + } + }); + + /* Apply defaults */ + const C: any = {}; + const default_config = this.defaultConfig(); + for (const k in default_config) { + C[k] = (default_config as any)[k]; + } + for (const k in config) { + C[k] = (config as any)[k]; + } + config = C; + + /* Apply config */ + //window['active_gobans'][this.goban_id] = this; + this.on_game_screen = this.getLocation().indexOf("/game/") >= 0; + this.no_display = false; + + this.width = config.width || 19; + this.height = config.height || 19; + this.bounds = config.bounds || { + top: 0, + left: 0, + bottom: this.height - 1, + right: this.width - 1, + }; + this.bounded_width = this.bounds ? this.bounds.right - this.bounds.left + 1 : this.width; + this.bounded_height = this.bounds ? this.bounds.bottom - this.bounds.top + 1 : this.height; + //this.black_name = config["black_name"]; + //this.white_name = config["white_name"]; + //this.move_number = config["move_number"]; + //this.setGameClock(null); + this.last_stone_sound = -1; + this.scoring_mode = false; + + this.game_type = config.game_type || ""; + this.one_click_submit = "one_click_submit" in config ? !!config.one_click_submit : false; + this.double_click_submit = + "double_click_submit" in config ? !!config.double_click_submit : true; + this.variation_stone_opacity = + typeof config.variation_stone_opacity !== "undefined" + ? config.variation_stone_opacity + : 0.6; + this.visual_undo_request_indicator = + "visual_undo_request_indicator" in config + ? !!config.visual_undo_request_indicator + : false; + this.original_square_size = config.square_size || "auto"; + //this.square_size = config["square_size"] || "auto"; + this.interactive = !!config.interactive; + this.pen_marks = []; + + this.config = repair_config(config); + this.__draw_state = makeMatrix(this.width, this.height, ""); + this.game_id = + (typeof config.game_id === "string" ? parseInt(config.game_id) : config.game_id) || 0; + this.player_id = config.player_id || 0; + this.review_id = config.review_id || 0; + this.last_review_message = {}; + this.review_had_gamedata = false; + this.puzzle_autoplace_delay = config.puzzle_autoplace_delay || 300; + this.isPlayerOwner = config.isPlayerOwner || (() => false); /* for reviews */ + this.isPlayerController = config.isPlayerController || (() => false); /* for reviews */ + this.isInPushedAnalysis = config.isInPushedAnalysis + ? config.isInPushedAnalysis + : () => false; + this.leavePushedAnalysis = config.leavePushedAnalysis + ? config.leavePushedAnalysis + : () => { + return; + }; + //this.onPendingResignation = config.onPendingResignation; + //this.onPendingResignationCleared = config.onPendingResignationCleared; + if ("onError" in config) { + this.onError = config.onError; + } + this.dont_draw_last_move = !!config.dont_draw_last_move; + this.last_move_radius = config.last_move_radius || 0.25; + this.circle_radius = config.circle_radius || 0.25; + this.getPuzzlePlacementSetting = config.getPuzzlePlacementSetting; + this.mode = config.mode || "play"; + this.previous_mode = this.mode; + this.label_character = "A"; + //this.edit_color = null; + this.stone_placement_enabled = false; + this.highlight_movetree_moves = false; + this.restrict_moves_to_movetree = false; + this.analysis_move_counter = 0; + //this.wait_for_game_to_start = config.wait_for_game_to_start; + this.errorHandler = (e) => { + if (e instanceof GobanMoveError) { + if (e.message_id === "stone_already_placed_here") { + return; + } + } + /* + if (e.message === _("A stone has already been placed here") || e.message === "A stone has already been placed here") { + return; + } + */ + if (e instanceof GobanMoveError && e.message_id === "illegal_self_capture") { + this.showMessage("self_capture_not_allowed", { error: e }, 5000); + return; + } else { + this.showMessage("error", { error: e }, 5000); + } + if (this.onError) { + this.onError(e); + } + }; + + this.draw_top_labels = "draw_top_labels" in config ? !!config.draw_top_labels : true; + this.draw_left_labels = "draw_left_labels" in config ? !!config.draw_left_labels : true; + this.draw_right_labels = "draw_right_labels" in config ? !!config.draw_right_labels : true; + this.draw_bottom_labels = + "draw_bottom_labels" in config ? !!config.draw_bottom_labels : true; + this.show_move_numbers = this.getShowMoveNumbers(); + this.show_variation_move_numbers = this.getShowVariationMoveNumbers(); + + if (this.bounds.left > 0) { + this.draw_left_labels = false; + } + if (this.bounds.top > 0) { + this.draw_top_labels = false; + } + if (this.bounds.right < this.width - 1) { + this.draw_right_labels = false; + } + if (this.bounds.bottom < this.height - 1) { + this.draw_bottom_labels = false; + } + + if (typeof config.square_size === "function") { + this.square_size = config.square_size(this) as number; + if (isNaN(this.square_size)) { + console.error("Invalid square size set: (NaN)"); + this.square_size = 12; + } + } else if (typeof config.square_size === "number") { + this.square_size = config.square_size; + } + /* + if (config.display_width && this.original_square_size === "auto") { + this.setSquareSizeBasedOnDisplayWidth(config.display_width, true) / suppress_redraw / true); + } + */ + + this.__update_move_tree = null; + this.shift_key_is_down = false; + } + + /** Goban calls some abstract methods as part of the construction + * process. Because our subclasses might (and do) need to do some of their + * own config before these are called, we set this function to be called + * by our subclass after it's done it's own internal config stuff. + */ + protected post_config_constructor(): GobanEngine { + let ret: GobanEngine; + + delete this.current_cmove; /* set in setConditionalTree */ + this.currently_my_cmove = false; + this.setConditionalTree(undefined); + + delete this.last_hover_square; + this.__last_pt = this.xy2ij(-1, -1); + + if (this.preloaded_data) { + ret = this.load(this.preloaded_data); + } else { + ret = this.load(this.config); + } + + return ret; + } + + protected getCoordinateDisplaySystem(): "A1" | "1-1" { + if (callbacks.getCoordinateDisplaySystem) { + return callbacks.getCoordinateDisplaySystem(); + } + return "A1"; + } + protected getShowMoveNumbers(): boolean { + if (callbacks.getShowMoveNumbers) { + return callbacks.getShowMoveNumbers(); + } + return false; + } + protected getShowVariationMoveNumbers(): boolean { + if (callbacks.getShowVariationMoveNumbers) { + return callbacks.getShowVariationMoveNumbers(); + } + return false; + } + public static getMoveTreeNumbering(): string { + if (callbacks.getMoveTreeNumbering) { + return callbacks.getMoveTreeNumbering(); + } + return "move-number"; + } + public static getCDNReleaseBase(): string { + if (callbacks.getCDNReleaseBase) { + return callbacks.getCDNReleaseBase(); + } + return ""; + } + public static getSoundEnabled(): boolean { + if (callbacks.getSoundEnabled) { + return callbacks.getSoundEnabled(); + } + return true; + } + public static getSoundVolume(): number { + if (callbacks.getSoundVolume) { + return callbacks.getSoundVolume(); + } + return 0.5; + } + protected defaultConfig(): any { + if (callbacks.defaultConfig) { + return callbacks.defaultConfig(); + } + return {}; + } + public isAnalysisDisabled(perGameSettingAppliesToNonPlayers: boolean = false): boolean { + if (callbacks.isAnalysisDisabled) { + return callbacks.isAnalysisDisabled(this, perGameSettingAppliesToNonPlayers); + } + return false; + } + + protected getLocation(): string { + if (callbacks.getLocation) { + return callbacks.getLocation(); + } + return window.location.pathname; + } + public override destroy(): void { + super.destroy(); + + delete (this as any).isPlayerController; + delete (this as any).isPlayerOwner; + delete (this as any).isInPushedAnalysis; + delete (this as any).leavePushedAnalysis; + delete (this as any).onError; + delete (this as any).onScoreEstimationUpdated; + delete (this as any).getPuzzlePlacementSetting; + } + protected scheduleRedrawPenLayer(): void { + if (!this.__board_redraw_pen_layer_timer) { + this.__board_redraw_pen_layer_timer = setTimeout(() => { + if (this.engine.cur_move.pen_marks.length) { + this.drawPenMarks(this.engine.cur_move.pen_marks); + } else if (this.pen_marks.length) { + this.clearAnalysisDrawing(); + } + this.__board_redraw_pen_layer_timer = null; + }, 100); + } + } + + protected getWidthForSquareSize(square_size: number): number { + return ( + (this.bounded_width + +this.draw_left_labels + +this.draw_right_labels) * square_size + ); + } + protected xy2ij( + x: number, + y: number, + anti_slip: boolean = true, + ): { i: number; j: number; valid: boolean } { + if (x > 0 && y > 0) { + if (this.bounds.left > 0) { + x += this.bounds.left * this.square_size; + } else { + x -= +this.draw_left_labels * this.square_size; + } + + if (this.bounds.top > 0) { + y += this.bounds.top * this.square_size; + } else { + y -= +this.draw_top_labels * this.square_size; + } + } + + const ii = x / this.square_size; + const jj = y / this.square_size; + let i = Math.floor(ii); + let j = Math.floor(jj); + const border_distance = Math.min(ii - i, jj - j, 1 - (ii - i), 1 - (jj - j)); + if (border_distance < 0.1 && anti_slip) { + // have a "dead zone" in between squares to avoid misclicks + i = -1; + j = -1; + } + return { i: i, j: j, valid: i >= 0 && j >= 0 && i < this.width && j < this.height }; + } + public setAnalyzeTool(tool: AnalysisTool, subtool: AnalysisSubTool | undefined | null) { + this.analyze_tool = tool; + this.analyze_subtool = subtool ?? "alternate"; + + if (tool === "stone" && subtool === "black") { + this.edit_color = "black"; + } else if (tool === "stone" && subtool === "white") { + this.edit_color = "white"; + } else { + delete this.edit_color; + } + + this.setLabelCharacterFromMarks(this.analyze_subtool as "letters" | "numbers"); + + if (tool === "draw") { + this.enablePen(); + } + } + + protected setSubmit(fn?: () => void): void { + this.submit_move = fn; + this.emit("submit_move", fn); + } + + public markDirty(): void { + if (!this.dirty_redraw) { + this.dirty_redraw = setTimeout(() => { + this.dirty_redraw = null; + this.redraw(); + }, 1); + } + } + + public set(x: number, y: number, player: JGOFNumericPlayerColor): void { + this.markDirty(); + } + + protected updateMoveTree(): void { + this.move_tree_redraw(); + } + protected updateOrRedrawMoveTree(): void { + if (this.engine.move_tree_layout_dirty) { + this.move_tree_redraw(); + } else { + this.updateMoveTree(); + } + } + + public setBounds(bounds: GobanBounds): void { + this.bounds = bounds || { top: 0, left: 0, bottom: this.height - 1, right: this.width - 1 }; + + if (this.bounds) { + this.bounded_width = this.bounds.right - this.bounds.left + 1; + this.bounded_height = this.bounds.bottom - this.bounds.top + 1; + } else { + this.bounded_width = this.width; + this.bounded_height = this.height; + } + + this.draw_left_labels = !!this.config.draw_left_labels; + this.draw_right_labels = !!this.config.draw_right_labels; + this.draw_top_labels = !!this.config.draw_top_labels; + this.draw_bottom_labels = !!this.config.draw_bottom_labels; + + if (this.bounds.left > 0) { + this.draw_left_labels = false; + } + if (this.bounds.top > 0) { + this.draw_top_labels = false; + } + if (this.bounds.right < this.width - 1) { + this.draw_right_labels = false; + } + if (this.bounds.bottom < this.height - 1) { + this.draw_bottom_labels = false; + } + } + + public load(config: GobanConfig): GobanEngine { + config = repair_config(config); + for (const k in config) { + (this.config as any)[k] = (config as any)[k]; + } + this.clearMessage(); + + const new_width = config.width || 19; + const new_height = config.height || 19; + // this signalizes that we can keep the old engine + // we progressively && more and more conditions + let keep_old_engine = new_width === this.width && new_height === this.height; + this.width = new_width; + this.height = new_height; + + delete this.move_selected; + + this.bounds = config.bounds || { + top: 0, + left: 0, + bottom: this.height - 1, + right: this.width - 1, + }; + if (this.bounds) { + this.bounded_width = this.bounds.right - this.bounds.left + 1; + this.bounded_height = this.bounds.bottom - this.bounds.top + 1; + } else { + this.bounded_width = this.width; + this.bounded_height = this.height; + } + + if (config.display_width !== undefined) { + this.display_width = config.display_width; + } + /* + if (this.display_width && this.original_square_size === "auto") { + const suppress_redraw = true; + this.setSquareSizeBasedOnDisplayWidth(this.display_width, suppress_redraw); + } + */ + + if ( + !this.__draw_state || + this.__draw_state.length !== this.height || + this.__draw_state[0].length !== this.width + ) { + this.__draw_state = makeMatrix(this.width, this.height, ""); + } + + this.chat_log = []; + const main_log: GobanChatLog = (config.chat_log || []).map((x) => { + x.channel = "main"; + return x; + }); + const spectator_log: GobanChatLog = (config.spectator_log || []).map((x) => { + x.channel = "spectator"; + return x; + }); + const malkovich_log: GobanChatLog = (config.malkovich_log || []).map((x) => { + x.channel = "malkovich"; + return x; + }); + this.chat_log = this.chat_log.concat(main_log, spectator_log, malkovich_log); + this.chat_log.sort((a, b) => a.date - b.date); + + for (const line of this.chat_log) { + this.emit("chat", line); + } + + // set up player_pool so we can find player details by id later + if (!config.player_pool) { + config.player_pool = {}; + } + + if (config.players) { + config.player_pool[config.players.black.id] = config.players.black; + config.player_pool[config.players.white.id] = config.players.white; + } + + if (config.rengo_teams) { + for (const player of config.rengo_teams.black.concat(config.rengo_teams.white)) { + config.player_pool[player.id] = player; + } + } + + /* This must be done last as it will invoke the appropriate .set actions to set the board in it's correct state */ + const old_engine = this.engine; + + // we need to have an engine to be able to keep it + keep_old_engine = keep_old_engine && old_engine !== null && old_engine !== undefined; + // we only keep the old engine in analyze mode & finished state + // JM: this keep_old_engine functionality is being added to fix resetting analyze state on network + // reconnect + keep_old_engine = + keep_old_engine && this.mode === "analyze" && old_engine.phase === "finished"; + + // NOTE: the construction needs to be side-effect free, because we might not use the new state + // so we create the engine twice (in case where keep_old_engine = false) + // here, it is created without the callback to `this` so that it cannot mess things up + const new_engine = new GobanEngine(config); + + /* + if (old_engine) { + console.log("old size", old_engine.move_tree.size()); + console.log("new size", new_engine.move_tree.size()); + console.log( + "old contains new", + old_engine.move_tree.containsOtherTreeAsSubset(new_engine.move_tree), + ); + console.log( + "new contains old", + new_engine.move_tree.containsOtherTreeAsSubset(old_engine.move_tree), + ); + } + */ + + // more sanity checks + keep_old_engine = keep_old_engine && old_engine.phase === new_engine.phase; + // just to be on the safe side, + // we only keep the old engine, if replacing it with new would not bring no new moves + // (meaning: old has at least all the moves of new one, possibly more == such as the analysis) + keep_old_engine = + keep_old_engine && old_engine.move_tree.containsOtherTreeAsSubset(new_engine.move_tree); + + if (!keep_old_engine) { + // we create the engine anew, this time with the callback argument, + // in case the constructor some side effects on `this` + // (JM: which it currently does) + this.engine = new GobanEngine(config, this); + this.emit("engine.updated", this.engine); + this.engine.parentEventEmitter = this; + } + + this.paused_since = config.paused_since; + this.pause_control = config.pause_control; + + /* + if (this.move_number) { + this.move_number.text(this.engine.getMoveNumber()); + } + */ + + if (this.config.marks && this.engine) { + this.setMarks(this.config.marks); + } + this.setConditionalTree(); + + if (this.getPuzzlePlacementSetting) { + if ( + this.engine.puzzle_player_move_mode === "fixed" && + this.getPuzzlePlacementSetting().mode === "play" + ) { + this.highlight_movetree_moves = true; + this.restrict_moves_to_movetree = true; + } + if ( + this.getPuzzlePlacementSetting && + this.getPuzzlePlacementSetting().mode !== "play" + ) { + this.highlight_movetree_moves = true; + } + } + + if (!(old_engine && matricesAreEqual(old_engine.board, this.engine.board))) { + this.redraw(true); + } + + this.updatePlayerToMoveTitle(); + if (this.mode === "play") { + if (this.engine.playerToMove() === this.player_id) { + this.enableStonePlacement(); + } else { + this.disableStonePlacement(); + } + } else { + if (this.stone_placement_enabled) { + this.disableStonePlacement(); + this.enableStonePlacement(); + } + } + if (!keep_old_engine) { + this.setLastOfficialMove(); + } + + this.emit("update"); + this.emit("load", config); + + return this.engine; + } + public setForRemoval( + x: number, + y: number, + removed: boolean, + emit_stone_removal_updated: boolean = true, + ) { + if (removed) { + this.getMarks(x, y).stone_removed = true; + this.getMarks(x, y).remove = true; + } else { + this.getMarks(x, y).stone_removed = false; + this.getMarks(x, y).remove = false; + } + this.drawSquare(x, y); + this.emit("set-for-removal", { x, y, removed }); + if (emit_stone_removal_updated) { + this.emit("stone-removal.updated"); + } + } + public showScores(score: Score, only_show_territory: boolean = false): void { + this.hideScores(); + this.showing_scores = true; + + for (let i = 0; i < 2; ++i) { + const color: "black" | "white" = i ? "black" : "white"; + const moves = this.engine.decodeMoves(score[color].scoring_positions); + for (let j = 0; j < moves.length; ++j) { + const mv = moves[j]; + if (only_show_territory && this.engine.board[mv.y][mv.x] > 0) { + continue; + } + if (mv.y < 0 || mv.x < 0) { + console.error("Negative scoring position: ", mv); + console.error( + "Scoring positions [" + color + "]: ", + score[color].scoring_positions, + ); + } else { + this.getMarks(mv.x, mv.y).score = color; + this.drawSquare(mv.x, mv.y); + } + } + } + } + public hideScores(): void { + this.showing_scores = false; + for (let j = 0; j < this.height; ++j) { + for (let i = 0; i < this.width; ++i) { + if (this.getMarks(i, j).score) { + delete this.getMarks(i, j).score; + //this.getMarks(i, j).score = false; + this.drawSquare(i, j); + } + } + } + } + public showStallingScoreEstimate(sse: StallingScoreEstimate): void { + this.hideScores(); + this.showing_scores = true; + this.scoring_mode = "stalling-scoring-mode"; + this.stalling_score_estimate = sse; + this.redraw(); + } + + public updatePlayerToMoveTitle(): void { + switch (this.engine.phase) { + case "play": + if ( + this.player_id && + this.player_id === this.engine.playerToMove() && + this.engine.cur_move.id === this.engine.last_official_move.id + ) { + if ( + this.engine.cur_move.passed() && + this.engine.handicapMovesLeft() <= 0 && + this.engine.cur_move.parent + ) { + this.setTitle(_("Your move - opponent passed")); + if (this.last_move && this.last_move.x >= 0) { + this.drawSquare(this.last_move.x, this.last_move.y); + } + } else { + this.setTitle(_("Your move")); + } + if ( + this.engine.cur_move.id === this.engine.last_official_move.id && + this.mode === "play" + ) { + this.emit("state_text", { title: _("Your move") }); + } + } else { + const color = this.engine.playerColor(this.engine.playerToMove()); + + let title; + if (color === "black") { + title = _("Black to move"); + } else { + title = _("White to move"); + } + this.setTitle(title); + if ( + this.engine.cur_move.id === this.engine.last_official_move.id && + this.mode === "play" + ) { + this.emit("state_text", { title: title, show_moves_made_count: true }); + } + } + break; + + case "stone removal": + this.setTitle(_("Stone Removal")); + this.emit("state_text", { title: _("Stone Removal Phase") }); + break; + + case "finished": + this.setTitle(_("Game Finished")); + this.emit("state_text", { title: _("Game Finished") }); + break; + + default: + this.setTitle(this.engine.phase); + break; + } + } + public disableStonePlacement(): void { + this.stone_placement_enabled = false; + if (this.__last_pt && this.__last_pt.valid) { + this.drawSquare(this.__last_pt.i, this.__last_pt.j); + } + } + public enableStonePlacement(): void { + if (this.stone_placement_enabled) { + this.disableStonePlacement(); + } + + this.stone_placement_enabled = true; + if (this.__last_pt && this.__last_pt.valid) { + this.drawSquare(this.__last_pt.i, this.__last_pt.j); + } + } + public showFirst(dont_update_display?: boolean): void { + this.engine.jumpTo(this.engine.move_tree); + if (!dont_update_display) { + this.updateTitleAndStonePlacement(); + this.emit("update"); + } + } + public showPrevious(dont_update_display?: boolean): void { + if (this.mode === "conditional") { + if (this.conditional_path.length >= 2) { + const prev_path = this.conditional_path.substr(0, this.conditional_path.length - 2); + this.jumpToLastOfficialMove(); + this.followConditionalPath(prev_path); + } + } else { + if (this.move_selected) { + this.jumpToLastOfficialMove(); + return; + } + + this.engine.showPrevious(); + } + + if (!dont_update_display) { + this.updateTitleAndStonePlacement(); + this.emit("update"); + } + } + public showNext(dont_update_display?: boolean): void { + if (this.mode === "conditional") { + if (this.current_cmove) { + if (this.currently_my_cmove) { + if (this.current_cmove.move !== null) { + this.followConditionalPath(this.current_cmove.move); + } + } else { + for (const ch in this.current_cmove.children) { + this.followConditionalPath(ch); + break; + } + } + } + } else { + if (this.move_selected) { + return; + } + this.engine.showNext(); + } + + if (!dont_update_display) { + this.updateTitleAndStonePlacement(); + this.emit("update"); + } + } + public prevSibling(): void { + const sibling = this.engine.cur_move.prevSibling(); + if (sibling) { + this.engine.jumpTo(sibling); + this.emit("update"); + } + } + public nextSibling(): void { + const sibling = this.engine.cur_move.nextSibling(); + if (sibling) { + this.engine.jumpTo(sibling); + this.emit("update"); + } + } + + public jumpToLastOfficialMove(): void { + delete this.move_selected; + this.engine.jumpToLastOfficialMove(); + this.updateTitleAndStonePlacement(); + + this.conditional_path = ""; + this.currently_my_cmove = false; + if (this.mode === "conditional") { + this.current_cmove = this.conditional_tree; + } + + this.emit("update"); + } + protected setLastOfficialMove(): void { + this.engine.setLastOfficialMove(); + this.updateTitleAndStonePlacement(); + } + protected isLastOfficialMove(): boolean { + return this.engine.isLastOfficialMove(); + } + + public updateTitleAndStonePlacement(): void { + this.updatePlayerToMoveTitle(); + + if (this.engine.phase === "stone removal" || this.scoring_mode) { + this.enableStonePlacement(); + } else if (this.engine.phase === "play") { + switch (this.mode) { + case "play": + if ( + this.isLastOfficialMove() && + this.engine.playerToMove() === this.player_id + ) { + this.enableStonePlacement(); + } else { + this.disableStonePlacement(); + } + break; + + case "analyze": + case "conditional": + case "puzzle": + this.disableStonePlacement(); + this.enableStonePlacement(); + break; + } + } else if (this.engine.phase === "finished") { + this.disableStonePlacement(); + if (this.mode === "analyze") { + this.enableStonePlacement(); + } + } + } + + public setConditionalTree(conditional_tree?: ConditionalMoveTree): void { + if (typeof conditional_tree === "undefined") { + this.conditional_tree = new ConditionalMoveTree(null); + } else { + this.conditional_tree = conditional_tree; + } + this.current_cmove = this.conditional_tree; + + this.emit("conditional-moves.updated"); + this.emit("update"); + } + public followConditionalPath(move_path: string) { + const moves = this.engine.decodeMoves(move_path); + for (let i = 0; i < moves.length; ++i) { + this.engine.place(moves[i].x, moves[i].y); + this.followConditionalSegment(moves[i].x, moves[i].y); + } + this.emit("conditional-moves.updated"); + } + protected followConditionalSegment(x: number, y: number): void { + const mv = encodeMove(x, y); + this.conditional_path += mv; + + if (!this.current_cmove) { + throw new Error(`followConditionalSegment called when current_cmove was not set`); + } + + if (this.currently_my_cmove) { + if (mv !== this.current_cmove.move) { + this.current_cmove.children = {}; + } + this.current_cmove.move = mv; + } else { + let cmove = null; + if (mv in this.current_cmove.children) { + cmove = this.current_cmove.children[mv]; + } else { + cmove = new ConditionalMoveTree(null, this.current_cmove); + this.current_cmove.children[mv] = cmove; + } + this.current_cmove = cmove; + } + + this.currently_my_cmove = !this.currently_my_cmove; + this.emit("conditional-moves.updated"); + } + private deleteConditionalSegment(x: number, y: number) { + this.conditional_path += encodeMove(x, y); + + if (!this.current_cmove) { + throw new Error(`deleteConditionalSegment called when current_cmove was not set`); + } + + if (this.currently_my_cmove) { + this.current_cmove.children = {}; + this.current_cmove.move = null; + const cur = this.current_cmove; + const parent = cur.parent; + this.current_cmove = parent; + if (parent) { + for (const mv in parent.children) { + if (parent.children[mv] === cur) { + delete parent.children[mv]; + } + } + } + } else { + console.error( + "deleteConditionalSegment called on other player's move, which doesn't make sense", + ); + return; + /* + -- actually this code may work below, we just don't have a ui to drive it for testing so we throw an error + + let cmove = null; + if (mv in this.current_cmove.children) { + delete this.current_cmove.children[mv]; + } + */ + } + + this.currently_my_cmove = !this.currently_my_cmove; + this.emit("conditional-moves.updated"); + } + public deleteConditionalPath(move_path: string): void { + const moves = this.engine.decodeMoves(move_path); + if (moves.length) { + for (let i = 0; i < moves.length - 1; ++i) { + if (i !== moves.length - 2) { + this.engine.place(moves[i].x, moves[i].y); + } + this.followConditionalSegment(moves[i].x, moves[i].y); + } + this.deleteConditionalSegment(moves[moves.length - 1].x, moves[moves.length - 1].y); + this.conditional_path = this.conditional_path.substr( + 0, + this.conditional_path.length - 4, + ); + } + this.emit("conditional-moves.updated"); + } + public getCurrentConditionalPath(): string { + return this.conditional_path; + } + + public setToPreviousMode(dont_jump_to_official_move?: boolean): boolean { + return this.setMode(this.previous_mode as GobanModes, dont_jump_to_official_move); + } + public setModeDeferred(mode: GobanModes): void { + setTimeout(() => { + this.setMode(mode); + }, 1); + } + public setMode(mode: GobanModes, dont_jump_to_official_move?: boolean): boolean { + if ( + mode === "conditional" && + this.player_id === this.engine.playerToMove() && + this.mode !== "score estimation" + ) { + /* this shouldn't ever get called, but incase we screw up.. */ + try { + swal.fire("Can't enter conditional move planning when it's your turn"); + } catch (e) { + console.error(e); + } + return false; + } + + this.setSubmit(); + + if ( + ["play", "analyze", "conditional", "edit", "score estimation", "puzzle"].indexOf( + mode, + ) === -1 + ) { + try { + swal.fire("Invalid mode for Goban: " + mode); + } catch (e) { + console.error(e); + } + return false; + } + + if ( + this.engine.config.disable_analysis && + this.engine.phase !== "finished" && + (mode === "analyze" || mode === "conditional") + ) { + try { + swal.fire("Unable to enter " + mode + " mode"); + } catch (e) { + console.error(e); + } + return false; + } + + if (mode === "conditional") { + this.conditional_starting_color = this.engine.playerColor(); + } + + let redraw = true; + + this.previous_mode = this.mode; + this.mode = mode; + if (!dont_jump_to_official_move) { + this.jumpToLastOfficialMove(); + } + + if (this.mode !== "analyze" || this.analyze_tool !== "draw") { + this.disablePen(); + } else { + this.enablePen(); + } + + if (mode === "play" && this.engine.phase !== "finished") { + this.engine.cur_move.clearMarks(); + redraw = true; + } + + if (redraw) { + this.clearAnalysisDrawing(); + this.redraw(); + } + this.updateTitleAndStonePlacement(); + + return true; + } + public setEditColor(color: "black" | "white"): void { + this.edit_color = color; + this.updateTitleAndStonePlacement(); + } + protected playMovementSound(): void { + if ( + this.last_sound_played_for_a_stone_placement === + this.engine.cur_move.x + "," + this.engine.cur_move.y + ) { + return; + } + this.last_sound_played_for_a_stone_placement = + this.engine.cur_move.x + "," + this.engine.cur_move.y; + + let idx; + do { + idx = Math.round(Math.random() * 10000) % 5; /* 5 === number of stone sounds */ + } while (idx === this.last_stone_sound); + this.last_stone_sound = idx; + + if (this.last_sound_played_for_a_stone_placement === "-1,-1") { + this.emit("audio-pass"); + } else { + this.emit("audio-stone", { + x: this.engine.cur_move.x, + y: this.engine.cur_move.y, + width: this.engine.width, + height: this.engine.height, + color: this.engine.colorNotToMove(), + }); + } + } + /** This is a callback that gets called by GobanEngine.getState to save and + * board state as it pushes and pops state. Our renderers can override this + * to save state they need. */ + /* + public getState(): any { + const ret = null; + return ret; + } + */ + + public setMarks(marks: { [mark: string]: string }, dont_draw?: boolean): void { + for (const key in marks) { + const locations = this.engine.decodeMoves(marks[key]); + for (let i = 0; i < locations.length; ++i) { + const pt = locations[i]; + this.setMark(pt.x, pt.y, key, dont_draw); + } + } + } + public setHeatmap(heatmap?: NumberMatrix, dont_draw?: boolean) { + this.heatmap = heatmap; + if (!dont_draw) { + this.redraw(true); + } + } + public setColoredCircles(circles?: Array, dont_draw?: boolean): void { + if (!circles || circles.length === 0) { + delete this.colored_circles; + return; + } + + this.colored_circles = makeEmptyMatrix(this.width, this.height); + for (const circle of circles) { + const mv = circle.move; + this.colored_circles[mv.y][mv.x] = circle; + } + if (!dont_draw) { + this.redraw(true); + } + } + + public setColoredMarks(colored_marks: { + [key: string]: { move: string; color: string }; + }): void { + for (const key in colored_marks) { + const locations = this.engine.decodeMoves(colored_marks[key].move); + for (let i = 0; i < locations.length; ++i) { + const pt = locations[i]; + this.setMarkColor(pt.x, pt.y, colored_marks[key].color); + this.setMark(pt.x, pt.y, key, false); + } + } + } + + protected setMarkColor(x: number, y: number, color: string) { + this.engine.cur_move.getMarks(x, y).color = color; + } + + protected setLetterMark(x: number, y: number, mark: string, drawSquare?: boolean): void { + this.engine.cur_move.getMarks(x, y).letter = mark; + if (drawSquare) { + this.drawSquare(x, y); + } + } + public setSubscriptMark(x: number, y: number, mark: string, drawSquare: boolean = true): void { + this.engine.cur_move.getMarks(x, y).subscript = mark; + if (drawSquare) { + this.drawSquare(x, y); + } + } + public setCustomMark(x: number, y: number, mark: string, drawSquare?: boolean): void { + this.engine.cur_move.getMarks(x, y)[mark] = true; + if (drawSquare) { + this.drawSquare(x, y); + } + } + public deleteCustomMark(x: number, y: number, mark: string, drawSquare?: boolean): void { + delete this.engine.cur_move.getMarks(x, y)[mark]; + if (drawSquare) { + this.drawSquare(x, y); + } + } + + public editPlaceByPrettyCoordinates( + coordinates: string, + color: JGOFNumericPlayerColor, + isTrunkMove?: boolean, + ): void { + for (const mv of this.engine.decodeMoves(coordinates)) { + this.engine.editPlace(mv.x, mv.y, color, isTrunkMove); + } + } + public placeByPrettyCoordinates(coordinates: string): void { + for (const mv of this.engine.decodeMoves(coordinates)) { + const removed_stones: Array = []; + const removed_count = this.engine.place( + mv.x, + mv.y, + undefined, + undefined, + undefined, + undefined, + undefined, + removed_stones, + ); + + if (removed_count > 0) { + this.emit("audio-capture-stones", { + count: removed_count, + already_captured: 0, + }); + this.debouncedEmitCapturedStones(removed_stones); + } + } + } + public setMarkByPrettyCoordinates( + coordinates: string, + mark: number | string, + dont_draw?: boolean, + ): void { + for (const mv of this.engine.decodeMoves(coordinates)) { + this.setMark(mv.x, mv.y, mark, dont_draw); + } + } + public setMark(x: number, y: number, mark: number | string, dont_draw?: boolean): void { + try { + if (x >= 0 && y >= 0) { + if (typeof mark === "number") { + mark = "" + mark; + } + + if (mark.startsWith("score-")) { + const color = mark.split("-")[1]; + this.getMarks(x, y).score = color; + if (!dont_draw) { + this.drawSquare(x, y); + } + } else if (mark.length <= 3 || parseFloat(mark)) { + this.setLetterMark(x, y, mark, !dont_draw); + } else { + this.setCustomMark(x, y, mark, !dont_draw); + } + } + } catch (e) { + console.error(e.stack); + } + } + protected setTransientMark( + x: number, + y: number, + mark: number | string, + dont_draw?: boolean, + ): void { + try { + if (x >= 0 && y >= 0) { + if (typeof mark === "number") { + mark = "" + mark; + } + + if (mark.length <= 3) { + this.engine.cur_move.getMarks(x, y).transient_letter = mark; + } else { + this.engine.cur_move.getMarks(x, y)["transient_" + mark] = true; + } + + if (!dont_draw) { + this.drawSquare(x, y); + } + } + } catch (e) { + console.error(e.stack); + } + } + public getMarks(x: number, y: number): MarkInterface { + if (this.engine && this.engine.cur_move) { + return this.engine.cur_move.getMarks(x, y); + } + return {}; + } + protected toggleMark( + x: number, + y: number, + mark: number | string, + force_label?: boolean, + force_put?: boolean, + ): boolean { + let ret = true; + if (typeof mark === "number") { + mark = "" + mark; + } + const marks = this.getMarks(x, y); + + const clearMarks = () => { + for (let i = 0; i < MARK_TYPES.length; ++i) { + delete marks[MARK_TYPES[i]]; + } + }; + + if (force_label || /^[a-zA-Z0-9]{1,2}$/.test(mark)) { + if (!force_put && "letter" in marks) { + clearMarks(); + ret = false; + } else { + clearMarks(); + marks.letter = mark; + } + } else { + if (!force_put && mark in marks) { + clearMarks(); + ret = false; + } else { + clearMarks(); + this.getMarks(x, y)[mark] = true; + } + } + this.drawSquare(x, y); + return ret; + } + protected incrementLabelCharacter(): void { + const seq1 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; + if (parseInt(this.label_character)) { + this.label_character = "" + (parseInt(this.label_character) + 1); + } else if (seq1.indexOf(this.label_character) !== -1) { + this.label_character = seq1[(seq1.indexOf(this.label_character) + 1) % seq1.length]; + } + } + protected setLabelCharacterFromMarks(set_override?: "numbers" | "letters"): void { + if (set_override === "letters" || /^[a-zA-Z]$/.test(this.label_character)) { + const seq1 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; + let idx = -1; + + for (let y = 0; y < this.height; ++y) { + for (let x = 0; x < this.width; ++x) { + const ch = this.getMarks(x, y).letter; + if (ch) { + idx = Math.max(idx, seq1.indexOf(ch)); + } + } + } + + this.label_character = seq1[idx + (1 % seq1.length)]; + } + if (set_override === "numbers" || /^[0-9]+$/.test(this.label_character)) { + let val = 0; + + for (let y = 0; y < this.height; ++y) { + for (let x = 0; x < this.width; ++x) { + const mark_as_number: number = parseInt(this.getMarks(x, y).letter || ""); + if (mark_as_number) { + val = Math.max(val, mark_as_number); + } + } + } + + this.label_character = "" + (val + 1); + } + } + public setLabelCharacter(ch: string): void { + this.label_character = ch; + if (this.last_hover_square) { + this.drawSquare(this.last_hover_square.x, this.last_hover_square.y); + } + } + public clearMark(x: number, y: number, mark: string | number): void { + try { + if (typeof mark === "number") { + mark = "" + mark; + } + + if (/^[a-zA-Z0-9]{1,2}$/.test(mark)) { + this.getMarks(x, y).letter = ""; + } else { + this.getMarks(x, y)[mark] = false; + } + this.drawSquare(x, y); + } catch (e) { + console.error(e); + } + } + protected clearTransientMark(x: number, y: number, mark: string | number): void { + try { + if (typeof mark === "number") { + mark = "" + mark; + } + + if (/^[a-zA-Z0-9]{1,2}$/.test(mark)) { + this.getMarks(x, y).transient_letter = ""; + } else { + this.getMarks(x, y)["transient_" + mark] = false; + } + this.drawSquare(x, y); + } catch (e) { + console.error(e); + } + } + public updateScoreEstimation(): void { + if (this.score_estimator) { + const est = this.score_estimator.estimated_hard_score - this.engine.komi; + if (callbacks.updateScoreEstimation) { + callbacks.updateScoreEstimation(est > 0 ? "black" : "white", Math.abs(est)); + } + if (this.config.onScoreEstimationUpdated) { + this.config.onScoreEstimationUpdated(est > 0 ? "black" : "white", Math.abs(est)); + } + this.emit("score_estimate", this.score_estimator); + } + } + + public isCurrentUserAPlayer(): boolean { + return this.player_id in this.engine.player_pool; + } + + public setScoringMode(tf: boolean, prefer_remote: boolean = false): MoveTree { + this.scoring_mode = tf; + const ret = this.engine.cur_move; + + if (this.scoring_mode) { + this.showMessage("processing", undefined, -1); + this.setMode("score estimation", true); + this.clearMessage(); + const should_autoscore = false; + this.score_estimator = this.engine.estimateScore( + SCORE_ESTIMATION_TRIALS, + SCORE_ESTIMATION_TOLERANCE, + prefer_remote, + should_autoscore, + ); + this.enableStonePlacement(); + this.redraw(true); + this.emit("update"); + } else { + if (this.previous_mode === "analyze" || this.previous_mode === "conditional") { + this.setToPreviousMode(true); + } else { + this.setMode("play"); + } + this.redraw(true); + } + + return ret; + } + + private last_emitted_captured_stones: Array = []; + + /* Emits the captured-stones event, only if didn't just emitted it with + * the same removed_stones. That situation happens when the client signals + * the removal, and then we get a second followup confirmation from the + * server, we need both sources of the event for when the user has two + * clients pointed at the same game, but we don't want to emit the event + * twice on the device that submitted the move in the first place. */ + public debouncedEmitCapturedStones(removed_stones: Array): void { + if (removed_stones.length > 0) { + const captured_stones = removed_stones + .map((o) => ({ x: o.x, y: o.y })) + .sort((a, b) => { + if (a.x < b.x) { + return -1; + } else if (a.x > b.x) { + return 1; + } else if (a.y < b.y) { + return -1; + } else if (a.y > b.y) { + return 1; + } else { + return 0; + } + }); + + let different = captured_stones.length !== this.last_emitted_captured_stones.length; + if (!different) { + for (let i = 0; i < captured_stones.length; ++i) { + if ( + captured_stones[i].x !== this.last_emitted_captured_stones[i].x || + captured_stones[i].y !== this.last_emitted_captured_stones[i].y + ) { + different = true; + break; + } + } + } + + if (different) { + this.last_emitted_captured_stones = removed_stones; + this.emit("captured-stones", { removed_stones }); + } + } + } +} + +function repair_config(config: GobanConfig): GobanConfig { + if (config.time_control) { + if (!config.time_control.system && (config.time_control as any).time_control) { + (config.time_control as any).system = (config.time_control as any).time_control; + console.log( + "Repairing goban config: time_control.time_control -> time_control.system = ", + (config.time_control as any).system, + ); + } + if (!config.time_control.speed) { + const tpm = computeAverageMoveTime(config.time_control, config.width, config.height); + (config.time_control as any).speed = + tpm === 0 || tpm > 3600 ? "correspondence" : tpm < 10 ? "blitz" : "live"; + console.log( + "Repairing goban config: time_control.speed = ", + (config.time_control as any).speed, + ); + } + } + + return config; +} diff --git a/src/Goban/OGSConnectivity.ts b/src/Goban/OGSConnectivity.ts new file mode 100644 index 00000000..c647e7ee --- /dev/null +++ b/src/Goban/OGSConnectivity.ts @@ -0,0 +1,2056 @@ +/* + * Copyright (C) Online-Go.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { AudioClockEvent, GobanInteractive, MARK_TYPES, MoveCommand } from "./InteractiveBase"; +import { GobanConfig, JGOFClockWithTransmitting } from "../GobanBase"; +import { callbacks } from "./callbacks"; +import { _, interpolate } from "engine/translate"; +import { focus_tracker } from "./focus_tracker"; +import { + AdHocClock, + AdHocPauseControl, + AdHocPlayerClock, + AUTOSCORE_TOLERANCE, + AUTOSCORE_TRIALS, + ConditionalMoveResponse, + deepEqual, + deepClone, + encodeMove, + GobanSocket, + GobanSocketEvents, + ConditionalMoveTree, + GobanEngine, + init_wasm_ownership_estimator, + JGOFIntersection, + JGOFPauseState, + JGOFPlayerClock, + JGOFPlayerSummary, + JGOFTimeControl, + MarkInterface, + niceInterval, + ReviewMessage, + ScoreEstimator, + JGOFMove, + makeMatrix, +} from "engine"; +import { + //ServerToClient, + GameChatMessage, + //GameChatLine, + //StallingScoreEstimate, +} from "engine/protocol"; + +declare let swal: any; + +interface JGOFPlayerClockWithTimedOut extends JGOFPlayerClock { + timed_out: boolean; +} +/** + * This class serves as a functionality layer encapsulating the logic connection + * that manages connections to the online-go.com servers. + * + * We have it as a separate base class simply to help with code organization + * and to keep our Goban class size down. + */ +export abstract class OGSConnectivity extends GobanInteractive { + public sent_timed_out_message: boolean = false; + protected socket!: GobanSocket; + protected socket_event_bindings: Array<[keyof GobanSocketEvents, () => void]> = []; + protected connectToReviewSent?: boolean; + + constructor(config: GobanConfig, preloaded_data?: GobanConfig) { + super(config, preloaded_data); + this.setGameClock(null); + + this.on("load", (config) => { + if ( + this.engine.phase === "stone removal" && + !("auto_scoring_done" in this) && + !("auto_scoring_done" in (this as any).engine) + ) { + this.performStoneRemovalAutoScoring(); + } + }); + } + + protected override post_config_constructor(): GobanEngine { + const ret = super.post_config_constructor(); + + if ("server_socket" in this.config && this.config["server_socket"]) { + if (!this.preloaded_data) { + this.showMessage("loading", undefined, -1); + } + this.connect(this.config["server_socket"]); + } + + return ret; + } + + public override destroy(): void { + super.destroy(); + if (this.socket) { + this.disconnect(); + } + if (this.sendLatencyTimer) { + clearInterval(this.sendLatencyTimer); + delete this.sendLatencyTimer; + } + + /* Clear various timeouts that may be running */ + this.clock_should_be_paused_for_move_submission = false; + this.setGameClock(null); + } + + protected _socket_on(event: KeyT, cb: any) { + this.socket.on(event, cb); + this.socket_event_bindings.push([event, cb]); + } + + protected getClockDrift(): number { + if (callbacks.getClockDrift) { + return callbacks.getClockDrift(); + } + console.warn("getClockDrift not provided for Goban instance"); + return 0; + } + protected getNetworkLatency(): number { + if (callbacks.getNetworkLatency) { + return callbacks.getNetworkLatency(); + } + console.warn("getNetworkLatency not provided for Goban instance"); + return 0; + } + + protected connect(server_socket: GobanSocket): void { + const socket = (this.socket = server_socket); + + this.disconnectedFromGame = false; + //this.on_disconnects = []; + + const send_connect_message = () => { + if (this.disconnectedFromGame) { + return; + } + + if (this.review_id) { + this.connectToReviewSent = true; + this.done_loading_review = false; + this.setTitle(_("Review")); + if (!this.disconnectedFromGame) { + socket.send("review/connect", { + review_id: this.review_id, + }); + } + this.emit("chat-reset"); + } else if (this.game_id) { + if (!this.disconnectedFromGame) { + socket.send("game/connect", { + game_id: this.game_id, + chat: !!this.config.connect_to_chat, + }); + } + } + + if (!this.sendLatencyTimer) { + const sendLatency = () => { + if (!this.interactive) { + return; + } + if (!this.isCurrentUserAPlayer()) { + return; + } + if (!callbacks.getNetworkLatency) { + return; + } + const latency = callbacks.getNetworkLatency(); + if (!latency) { + return; + } + + if (!this.game_id || this.game_id <= 0) { + return; + } + + this.socket.send("game/latency", { + game_id: this.game_id, + latency: this.getNetworkLatency(), + }); + }; + this.sendLatencyTimer = niceInterval(sendLatency, 5000); + sendLatency(); + } + }; + + if (socket.connected) { + send_connect_message(); + } + + this._socket_on("connect", send_connect_message); + this._socket_on("disconnect", (): void => { + if (this.disconnectedFromGame) { + return; + } + }); + + let reconnect = false; + + this._socket_on("connect", () => { + if (this.disconnectedFromGame) { + return; + } + if (reconnect) { + this.emit("audio-reconnected"); + } + reconnect = true; + }); + this._socket_on("disconnect", (): void => { + if (this.disconnectedFromGame) { + return; + } + this.emit("audio-disconnected"); + }); + + let prefix = null; + + if (this.game_id) { + prefix = "game/" + this.game_id + "/"; + } + if (this.review_id) { + prefix = "review/" + this.review_id + "/"; + } + + this._socket_on((prefix + "error") as keyof GobanSocketEvents, (msg: any): void => { + if (this.disconnectedFromGame) { + return; + } + this.emit("error", msg); + let duration = 500; + + if (msg === "This is a protected game" || msg === "This is a protected review") { + duration = -1; + } + + this.showMessage("error", { error: { message: _(msg) } }, duration); + console.error("ERROR: ", msg); + }); + + /*****************/ + /*** Game mode ***/ + /*****************/ + if (this.game_id) { + this._socket_on( + (prefix + "gamedata") as keyof GobanSocketEvents, + (obj: GobanConfig): void => { + if (this.disconnectedFromGame) { + return; + } + + this.clearMessage(); + //this.onClearChatLogs(); + + this.emit("chat-reset"); + focus_tracker.reset(); + + if ( + this.last_phase && + this.last_phase !== "finished" && + obj.phase === "finished" + ) { + const winner = obj.winner; + let winner_color: "black" | "white" | undefined; + if (typeof winner === "number") { + winner_color = winner === obj.black_player_id ? "black" : "white"; + } else if (winner === "black" || winner === "white") { + winner_color = winner; + } + + if (winner_color) { + this.emit("audio-game-ended", winner_color); + } + } + if (obj.phase) { + this.last_phase = obj.phase; + } else { + console.warn(`Game gamedata missing phase`); + } + this.load(obj); + this.emit("gamedata", obj); + }, + ); + this._socket_on( + (prefix + "chat") as keyof GobanSocketEvents, + (obj: GameChatMessage): void => { + if (this.disconnectedFromGame) { + return; + } + obj.line.channel = obj.channel; + this.chat_log.push(obj.line); + this.emit("chat", obj.line); + }, + ); + this._socket_on((prefix + "reset-chats") as keyof GobanSocketEvents, (): void => { + if (this.disconnectedFromGame) { + return; + } + this.emit("chat-reset"); + }); + this._socket_on( + (prefix + "chat/remove") as keyof GobanSocketEvents, + (obj: any): void => { + if (this.disconnectedFromGame) { + return; + } + this.emit("chat-remove", obj); + }, + ); + this._socket_on((prefix + "message") as keyof GobanSocketEvents, (msg: any): void => { + if (this.disconnectedFromGame) { + return; + } + this.showMessage("server_message", { message: msg }); + }); + delete this.last_phase; + + this._socket_on((prefix + "latency") as keyof GobanSocketEvents, (obj: any): void => { + if (this.disconnectedFromGame) { + return; + } + + if (this.engine) { + if (!this.engine.latencies) { + this.engine.latencies = {}; + } + this.engine.latencies[obj.player_id] = obj.latency; + } + }); + this._socket_on((prefix + "clock") as keyof GobanSocketEvents, (obj: any): void => { + if (this.disconnectedFromGame) { + return; + } + + this.clock_should_be_paused_for_move_submission = false; + this.setGameClock(obj); + + this.updateTitleAndStonePlacement(); + this.emit("update"); + }); + this._socket_on( + (prefix + "phase") as keyof GobanSocketEvents, + (new_phase: any): void => { + if (this.disconnectedFromGame) { + return; + } + + this.setMode("play"); + if (new_phase !== "finished") { + this.engine.clearRemoved(); + } + + if (this.engine.phase !== new_phase) { + if (new_phase === "stone removal") { + this.emit("audio-enter-stone-removal"); + } + if (new_phase === "play" && this.engine.phase === "stone removal") { + this.emit("audio-resume-game-from-stone-removal"); + } + } + + this.engine.phase = new_phase; + + if (this.engine.phase === "stone removal") { + this.performStoneRemovalAutoScoring(); + } else { + delete this.stone_removal_auto_scoring_done; + } + + this.updateTitleAndStonePlacement(); + this.emit("update"); + }, + ); + this._socket_on( + (prefix + "undo_requested") as keyof GobanSocketEvents, + (move_number: string): void => { + if (this.disconnectedFromGame) { + return; + } + + this.engine.undo_requested = parseInt(move_number); + this.emit("update"); + this.emit("audio-undo-requested"); + if (this.visual_undo_request_indicator) { + this.redraw(true); // need to update the mark on the last move + } + }, + ); + this._socket_on((prefix + "undo_canceled") as keyof GobanSocketEvents, (): void => { + if (this.disconnectedFromGame) { + return; + } + + this.engine.undo_requested = undefined; // can't call delete here because this is a getter/setter + this.emit("update"); + this.emit("undo_canceled"); + if (this.visual_undo_request_indicator) { + this.redraw(true); + } + }); + this._socket_on((prefix + "undo_accepted") as keyof GobanSocketEvents, (): void => { + if (this.disconnectedFromGame) { + return; + } + + if (!this.engine.undo_requested) { + console.warn("Undo accepted, but no undo requested, we might be out of sync"); + try { + swal.fire( + "Game synchronization error related to undo, please reload your game page", + ); + } catch (e) { + console.error(e); + } + return; + } + + this.engine.undo_requested = undefined; + + this.setMode("play"); + this.engine.showPrevious(); + this.engine.setLastOfficialMove(); + + this.setConditionalTree(); + + this.engine.undo_requested = undefined; + this.updateTitleAndStonePlacement(); + this.emit("update"); + this.emit("audio-undo-granted"); + }); + this._socket_on((prefix + "move") as keyof GobanSocketEvents, (move_obj: any): void => { + try { + if (this.disconnectedFromGame) { + return; + } + focus_tracker.reset(); + + if (move_obj.game_id !== this.game_id) { + console.error( + "Invalid move for this game received [" + this.game_id + "]", + move_obj, + ); + return; + } + const move = move_obj.move; + + if (this.isInPushedAnalysis()) { + this.leavePushedAnalysis(); + } + + /* clear any undo state that may be hanging around */ + this.engine.undo_requested = undefined; + + const mv = this.engine.decodeMoves(move); + + if (mv.length > 1) { + console.warn( + "More than one move provided in encoded move in a `move` event. That's odd.", + ); + } + + const the_move = mv[0]; + + if (this.mode === "conditional" || this.mode === "play") { + this.setMode("play"); + } + + let jump_to_move = null; + if ( + this.engine.cur_move.id !== this.engine.last_official_move.id && + ((this.engine.cur_move.parent == null && + this.engine.cur_move.trunk_next != null) || + this.engine.cur_move.parent?.id !== this.engine.last_official_move.id) + ) { + jump_to_move = this.engine.cur_move; + } + this.engine.jumpToLastOfficialMove(); + + if (this.engine.playerToMove() !== this.player_id) { + const t = this.conditional_tree.getChild( + encodeMove(the_move.x, the_move.y), + ); + t.move = null; + this.setConditionalTree(t); + } + + if (this.engine.getMoveNumber() !== move_obj.move_number - 1) { + this.showMessage("synchronization_error"); + setTimeout(() => { + window.location.href = window.location.href; + }, 2500); + console.error( + "Synchronization error, we thought move should be " + + this.engine.getMoveNumber() + + " server thought it should be " + + (move_obj.move_number - 1), + ); + + return; + } + + const score_before_move = + this.engine.computeScore(true)[this.engine.colorToMove()].prisoners; + + let removed_count = 0; + const removed_stones: Array = []; + if (the_move.edited) { + this.engine.editPlace(the_move.x, the_move.y, the_move.color || 0); + } else { + removed_count = this.engine.place( + the_move.x, + the_move.y, + false, + false, + false, + true, + true, + removed_stones, + ); + } + + if (the_move.player_update && this.engine.player_pool) { + //console.log("`move` got player update:", the_move.player_update); + this.engine.cur_move.player_update = the_move.player_update; + this.engine.updatePlayers(the_move.player_update); + } + + if (the_move.played_by) { + this.engine.cur_move.played_by = the_move.played_by; + } + + this.setLastOfficialMove(); + delete this.move_selected; + + if (jump_to_move) { + this.engine.jumpTo(jump_to_move); + } + + this.emit("update"); + this.playMovementSound(); + if (removed_count) { + console.log("audio-capture-stones", { + count: removed_count, + already_captured: score_before_move, + }); + this.emit("audio-capture-stones", { + count: removed_count, + already_captured: score_before_move, + }); + this.debouncedEmitCapturedStones(removed_stones); + } + + this.emit("move-made"); + + /* + if (this.move_number) { + this.move_number.text(this.engine.getMoveNumber()); + } + */ + } catch (e) { + console.error(e); + } + }); + + this._socket_on( + (prefix + "player_update") as keyof GobanSocketEvents, + (player_update: JGOFPlayerSummary): void => { + try { + let jump_to_move = null; + if ( + this.engine.cur_move.id !== this.engine.last_official_move.id && + ((this.engine.cur_move.parent == null && + this.engine.cur_move.trunk_next != null) || + this.engine.cur_move.parent?.id !== + this.engine.last_official_move.id) + ) { + jump_to_move = this.engine.cur_move; + } + this.engine.jumpToLastOfficialMove(); + + this.engine.cur_move.player_update = player_update; + this.engine.updatePlayers(player_update); + + if (this.mode === "conditional" || this.mode === "play") { + this.setMode("play"); + } else { + console.warn("unexpected player_update received!"); + } + + if (jump_to_move) { + this.engine.jumpTo(jump_to_move); + } + } catch (e) { + console.error(e); + } + this.emit("player-update", player_update); + }, + ); + + this._socket_on( + (prefix + "conditional_moves") as keyof GobanSocketEvents, + (cmoves: { + player_id: number; + move_number: number; + moves: ConditionalMoveResponse | null; + }): void => { + if (this.disconnectedFromGame) { + return; + } + + if (cmoves.moves == null) { + this.setConditionalTree(); + } else { + this.setConditionalTree(ConditionalMoveTree.decode(cmoves.moves)); + } + }, + ); + this._socket_on( + (prefix + "removed_stones") as keyof GobanSocketEvents, + (cfg: any): void => { + if (this.disconnectedFromGame) { + return; + } + + if ("strict_seki_mode" in cfg) { + this.engine.strict_seki_mode = cfg.strict_seki_mode; + } else { + const removed = cfg.removed; + const stones = cfg.stones; + let moves: JGOFMove[]; + if (!stones) { + moves = []; + } else { + moves = this.engine.decodeMoves(stones); + } + + for (let i = 0; i < moves.length; ++i) { + this.engine.setRemoved(moves[i].x, moves[i].y, removed, false); + } + this.emit("stone-removal.updated"); + } + this.updateTitleAndStonePlacement(); + this.emit("update"); + }, + ); + this._socket_on( + (prefix + "removed_stones_accepted") as keyof GobanSocketEvents, + (cfg: any): void => { + if (this.disconnectedFromGame) { + return; + } + + const player_id = cfg.player_id; + const stones = cfg.stones; + + if (player_id === 0) { + this.engine.players["white"].accepted_stones = stones; + this.engine.players["black"].accepted_stones = stones; + } else { + const color = this.engine.playerColor(player_id); + if (color === "invalid") { + console.error( + `Invalid player_id ${player_id} in removed_stones_accepted`, + { + cfg, + player_id: this.player_id, + players: this.engine.players, + }, + ); + throw new Error( + `Invalid player_id ${player_id} in removed_stones_accepted`, + ); + } else { + this.engine.players[color].accepted_stones = stones; + this.engine.players[color].accepted_strict_seki_mode = + "strict_seki_mode" in cfg ? cfg.strict_seki_mode : false; + } + } + this.updateTitleAndStonePlacement(); + this.emit("stone-removal.accepted"); + this.emit("update"); + }, + ); + + const auto_resign_state: { [id: number]: boolean } = {}; + + this._socket_on((prefix + "auto_resign") as keyof GobanSocketEvents, (obj: any) => { + this.emit("auto-resign", { + game_id: obj.game_id, + player_id: obj.player_id, + expiration: obj.expiration, + }); + auto_resign_state[obj.player_id] = true; + this.emit("audio-other-player-disconnected", { + player_id: obj.player_id, + }); + }); + this._socket_on( + (prefix + "clear_auto_resign") as keyof GobanSocketEvents, + (obj: any) => { + this.emit("clear-auto-resign", { + game_id: obj.game_id, + player_id: obj.player_id, + }); + if (auto_resign_state[obj.player_id]) { + this.emit("audio-other-player-reconnected", { + player_id: obj.player_id, + }); + delete auto_resign_state[obj.player_id]; + } + }, + ); + this._socket_on( + (prefix + "stalling_score_estimate") as keyof GobanSocketEvents, + (obj: any): void => { + if (this.disconnectedFromGame) { + return; + } + console.log("Score estimate received: ", obj); + //obj.line.channel = obj.channel; + //this.chat_log.push(obj.line); + this.engine.stalling_score_estimate = obj; + this.engine.config.stalling_score_estimate = obj; + this.emit("stalling_score_estimate", obj); + }, + ); + } + + /*******************/ + /*** Review mode ***/ + /*******************/ + let bulk_processing = false; + const process_r = (obj: ReviewMessage) => { + if (this.disconnectedFromGame) { + return; + } + + if (obj.chat) { + obj.chat.channel = "discussion"; + if (!obj.chat.chat_id) { + obj.chat.chat_id = obj.chat.player_id + "." + obj.chat.date; + } + this.chat_log.push(obj.chat as any); + this.emit("chat", obj.chat); + } + + if (obj["remove-chat"]) { + this.emit("chat-remove", { chat_ids: [obj["remove-chat"]] }); + } + + if (obj.gamedata) { + if (obj.gamedata.phase === "stone removal") { + obj.gamedata.phase = "finished"; + } + + this.load(obj.gamedata); + this.review_had_gamedata = true; + } + + if (obj.player_update && this.engine.player_pool) { + console.log("process_r got player update:", obj.player_update); + this.engine.updatePlayers(obj.player_update); + } + + if (obj.owner) { + this.review_owner_id = typeof obj.owner === "object" ? obj.owner.id : obj.owner; + } + if (obj.controller) { + this.review_controller_id = + typeof obj.controller === "object" ? obj.controller.id : obj.controller; + } + + if ( + !this.isPlayerController() || + !this.done_loading_review || + "om" in obj /* official moves are always alone in these object broadcasts */ || + "undo" in obj /* official moves are always alone in these object broadcasts */ + ) { + const cur_move = this.engine.cur_move; + const follow = + this.engine.cur_review_move == null || + this.engine.cur_review_move.id === cur_move.id; + let do_redraw = false; + if ("f" in obj && typeof obj.m === "string") { + /* specifying node */ + const t = this.done_loading_review; + this.done_loading_review = + false; /* this prevents drawing from being drawn when we do a follow path. */ + this.engine.followPath(obj.f || 0, obj.m); + this.drawSquare(this.engine.cur_move.x, this.engine.cur_move.y); + this.done_loading_review = t; + this.engine.setAsCurrentReviewMove(); + this.scheduleRedrawPenLayer(); + } + + if ("om" in obj) { + /* Official move [comes from live review of game] */ + const t = this.engine.cur_review_move || this.engine.cur_move; + const mv = this.engine.decodeMoves([obj.om] as any)[0]; + const follow_om = t.id === this.engine.last_official_move.id; + this.engine.jumpToLastOfficialMove(); + this.engine.place(mv.x, mv.y, false, false, true, true, true); + this.engine.setLastOfficialMove(); + if ( + (t.x !== mv.x || + t.y !== mv.y) /* case when a branch has been promoted to trunk */ && + !follow_om + ) { + /* case when they were on a last official move, auto-follow to next */ + this.engine.jumpTo(t); + } + this.engine.setAsCurrentReviewMove(); + if (this.done_loading_review) { + this.move_tree_redraw(); + } + } + + if ("undo" in obj) { + /* Official undo move [comes from live review of game] */ + const t = this.engine.cur_review_move; + const cur_move_undone = + this.engine.cur_review_move?.id === this.engine.last_official_move.id; + this.engine.jumpToLastOfficialMove(); + this.engine.showPrevious(); + this.engine.setLastOfficialMove(); + if (!cur_move_undone) { + if (t) { + this.engine.jumpTo(t); + } else { + console.warn( + `No valid move to jump back to in review game relay of undo`, + ); + } + } + this.engine.setAsCurrentReviewMove(); + if (this.done_loading_review) { + this.move_tree_redraw(); + } + } + + if (this.engine.cur_review_move) { + if (typeof obj["t"] === "string") { + /* set text */ + this.engine.cur_review_move.text = obj["t"]; + } + if ("t+" in obj) { + /* append to text */ + this.engine.cur_review_move.text += obj["t+"]; + } + if (typeof obj.k !== "undefined") { + /* set marks */ + const t = this.engine.cur_move; + this.engine.cur_review_move.clearMarks(); + this.engine.cur_move = this.engine.cur_review_move; + this.setMarks(obj["k"], this.engine.cur_move.id !== t.id); + this.engine.cur_move = t; + if (this.engine.cur_move.id === t.id) { + this.redraw(); + } + } + if ("clearpen" in obj) { + this.engine.cur_review_move.pen_marks = []; + this.scheduleRedrawPenLayer(); + do_redraw = false; + } + if ("delete" in obj) { + const t = this.engine.cur_review_move.parent; + this.engine.cur_review_move.remove(); + this.engine.jumpTo(t); + this.engine.setAsCurrentReviewMove(); + this.scheduleRedrawPenLayer(); + if (this.done_loading_review) { + this.move_tree_redraw(); + } + } + if (typeof obj.pen !== "undefined") { + /* start pen */ + this.engine.cur_review_move.pen_marks.push({ + color: obj["pen"], + points: [], + }); + } + if (typeof obj.pp !== "undefined") { + /* update pen marks */ + try { + const pts = + this.engine.cur_review_move.pen_marks[ + this.engine.cur_review_move.pen_marks.length - 1 + ].points; + this.engine.cur_review_move.pen_marks[ + this.engine.cur_review_move.pen_marks.length - 1 + ].points = pts.concat(obj["pp"]); + this.scheduleRedrawPenLayer(); + do_redraw = false; + } catch (e) { + console.error(e); + } + } + } + + if (this.done_loading_review) { + if (!follow) { + this.engine.jumpTo(cur_move); + this.move_tree_redraw(); + } else { + if (do_redraw) { + this.redraw(true); + } + if (!this.__update_move_tree) { + this.__update_move_tree = setTimeout(() => { + this.__update_move_tree = null; + this.updateOrRedrawMoveTree(); + this.emit("update"); + }, 100); + } + } + } + } + + if ("controller" in obj) { + if (!("owner" in obj)) { + /* only false at index 0 of the replay log */ + if (this.isPlayerController()) { + this.emit("review.sync-to-current-move"); + } + this.updateTitleAndStonePlacement(); + const line = { + system: true, + chat_id: uuid(), + body: interpolate(_("Control passed to %s"), [ + typeof obj.controller === "number" + ? `%%%PLAYER-${obj.controller}%%%` + : obj.controller?.username || "[missing controller name]", + ]), + channel: "system", + }; + //this.chat_log.push(line); + this.emit("chat", line); + this.emit("update"); + } + } + if (!bulk_processing) { + this.emit("review.updated"); + } + }; + + if (this.review_id) { + this._socket_on( + `review/${this.review_id}/full_state`, + (entries: Array) => { + try { + if (!entries || entries.length === 0) { + console.error("Blank full state received, ignoring"); + return; + } + if (this.disconnectedFromGame) { + return; + } + + this.disableDrawing(); + /* TODO: Clear our state here better */ + + this.emit("review.load-start"); + bulk_processing = true; + for (let i = 0; i < entries.length; ++i) { + process_r(entries[i]); + } + bulk_processing = false; + + this.enableDrawing(); + /* + if (this.isPlayerController()) { + this.done_loading_review = true; + this.drawPenMarks(this.engine.cur_move.pen_marks); + this.redraw(true); + return; + } + */ + + this.done_loading_review = true; + this.drawPenMarks(this.engine.cur_move.pen_marks); + this.emit("review.load-end"); + this.emit("review.updated"); + this.move_tree_redraw(); + this.redraw(true); + } catch (e) { + console.error(e); + } + }, + ); + this._socket_on(`review/${this.review_id}/r`, process_r); + } + + return; + } + + protected disconnect(): void { + this.emit("destroy"); + if (!this.disconnectedFromGame) { + this.disconnectedFromGame = true; + if (this.socket && this.socket.connected) { + if (this.review_id) { + this.socket.send("review/disconnect", { review_id: this.review_id }); + } + if (this.game_id) { + this.socket.send("game/disconnect", { game_id: this.game_id }); + } + } + } + for (const pair of this.socket_event_bindings) { + this.socket.off(pair[0], pair[1]); + } + this.socket_event_bindings = []; + } + + public sendChat(msg_body: string, type: string) { + if (typeof msg_body === "string" && msg_body.length === 0) { + return; + } + + const msg: any = { + body: msg_body, + }; + + if (this.game_id) { + msg["type"] = type; + msg["game_id"] = this.game_id; + msg["move_number"] = this.engine.getCurrentMoveNumber(); + this.socket.send("game/chat", msg); + } else { + const diff = this.engine.getMoveDiff(); + msg["review_id"] = this.review_id; + msg["from"] = diff.from; + msg["moves"] = diff.moves; + this.socket.send("review/chat", msg); + } + } + + /** + * When we think our clock has runout, send a message to the server + * letting it know. Otherwise we have to wait for the server grace + * period to expire for it to time us out. + */ + public sendTimedOut(): void { + if (!this.sent_timed_out_message) { + if (this.engine?.phase === "play") { + console.log("Sending timed out"); + + this.sent_timed_out_message = true; + this.socket.send("game/timed_out", { + game_id: this.game_id, + }); + } + } + } + public syncReviewMove(msg_override?: ReviewMessage, node_text?: string): void { + if ( + this.review_id && + (this.isPlayerController() || + (this.isPlayerOwner() && msg_override && msg_override.controller)) && + this.done_loading_review + ) { + if (this.isInPushedAnalysis()) { + return; + } + + const diff = this.engine.getMoveDiff(); + this.engine.setAsCurrentReviewMove(); + + let msg: ReviewMessage; + + if (!msg_override) { + const marks: { [mark: string]: string } = {}; + for (let y = 0; y < this.height; ++y) { + for (let x = 0; x < this.width; ++x) { + const pos = this.getMarks(x, y); + for (let i = 0; i < MARK_TYPES.length; ++i) { + if (MARK_TYPES[i] in pos && pos[MARK_TYPES[i]]) { + const mark_key: keyof MarkInterface = + MARK_TYPES[i] === "letter" + ? pos.letter || "[ERR]" + : MARK_TYPES[i] === "score" + ? `score-${pos.score}` + : MARK_TYPES[i]; + if (!(mark_key in marks)) { + marks[mark_key] = ""; + } + marks[mark_key] += encodeMove(x, y); + } + } + } + } + + if (!node_text && node_text !== "") { + node_text = this.engine.cur_move.text || ""; + } + + msg = { + f: diff.from, + t: node_text, + m: diff.moves, + k: marks, + }; + const tmp = deepClone(msg); + + if (this.last_review_message.f === msg.f && this.last_review_message.m === msg.m) { + delete msg["f"]; + delete msg["m"]; + + const txt_idx = node_text.indexOf(this.engine.cur_move.text || ""); + if (txt_idx === 0) { + delete msg["t"]; + if (node_text !== this.engine.cur_move.text) { + msg["t+"] = node_text.substr(this.engine.cur_move.text.length); + } + } + + if (deepEqual(marks, this.last_review_message.k)) { + delete msg["k"]; + } + } else { + this.scheduleRedrawPenLayer(); + } + this.engine.cur_move.text = node_text; + this.last_review_message = tmp; + + if (Object.keys(msg).length === 0) { + return; + } + } else { + msg = msg_override; + if (msg.clearpen) { + this.engine.cur_move.pen_marks = []; + } + } + + msg.review_id = this.review_id; + + this.socket.send("review/append", msg); + } + } + + protected sendMove(mv: MoveCommand, cb?: () => void): boolean { + if (!mv.blur) { + mv.blur = focus_tracker.getMaxBlurDurationSinceLastReset(); + focus_tracker.reset(); + } + this.setConditionalTree(); + + // Add `.clock` to the move sent to the server + try { + if (this.player_id) { + if (this.__clock_timer) { + clearTimeout(this.__clock_timer); + delete this.__clock_timer; + this.clock_should_be_paused_for_move_submission = true; + } + + const original_clock = this.last_clock; + if (!original_clock) { + throw new Error(`No last_clock when calling sendMove()`); + } + let color: "black" | "white"; + + if (this.player_id === original_clock.black_player_id) { + color = "black"; + } else if (this.player_id === original_clock.white_player_id) { + color = "white"; + } else { + throw new Error(`Player id ${this.player_id} not found in clock`); + } + + if (color) { + const clock_drift = callbacks?.getClockDrift ? callbacks?.getClockDrift() : 0; + + const current_server_time = Date.now() - clock_drift; + + const pause_control = this.pause_control; + + const paused = pause_control + ? isPaused(AdHocPauseControl2JGOFPauseState(pause_control)) + : false; + + const elapsed: number = original_clock.start_mode + ? 0 + : paused && original_clock.paused_since + ? Math.max(original_clock.paused_since, original_clock.last_move) - + original_clock.last_move + : current_server_time - original_clock.last_move; + + const clock = this.computeNewPlayerClock( + original_clock[`${color}_time`] as any, + true, + elapsed, + this.config.time_control as any, + ); + + if (clock.timed_out) { + this.sendTimedOut(); + return false; + } + + mv.clock = clock; + } else { + throw new Error(`No color for player_id ${this.player_id}`); + } + } + } catch (e) { + console.error(e); + } + + // Send the move. If we aren't getting a response, show a message + // indicating such and try reloading after a few more seconds. + let reload_timeout: ReturnType; + const timeout = setTimeout(() => { + this.showMessage("error_submitting_move", undefined, -1); + + reload_timeout = setTimeout(() => { + window.location.reload(); + }, 5000); + }, 5000); + this.emit("submitting-move", true); + this.socket.send("game/move", mv, () => { + if (reload_timeout) { + clearTimeout(reload_timeout); + } + clearTimeout(timeout); + this.clearMessage(); + this.emit("submitting-move", false); + if (cb) { + cb(); + } + }); + + return true; + } + public giveReviewControl(player_id: number): void { + this.syncReviewMove({ controller: player_id }); + } + + public saveConditionalMoves(): void { + this.socket.send("game/conditional_moves/set", { + move_number: this.engine.getCurrentMoveNumber(), + game_id: this.game_id, + conditional_moves: this.conditional_tree.encode(), + }); + this.emit("conditional-moves.updated"); + } + + public resign(): void { + this.socket.send("game/resign", { + game_id: this.game_id, + }); + } + protected sendPendingResignation(): void { + this.socket.send("game/delayed_resign", { + game_id: this.game_id, + }); + } + protected clearPendingResignation(): void { + this.socket.send("game/clear_delayed_resign", { + game_id: this.game_id, + }); + } + public cancelGame(): void { + this.socket.send("game/cancel", { + game_id: this.game_id, + }); + } + protected annul(): void { + this.socket.send("game/annul", { + game_id: this.game_id, + }); + } + public pass(): void { + if (this.mode === "conditional") { + this.followConditionalSegment(-1, -1); + } + + this.engine.place(-1, -1); + if (this.mode === "play") { + this.sendMove({ + game_id: this.game_id, + move: encodeMove(-1, -1), + }); + } else { + this.syncReviewMove(); + this.move_tree_redraw(); + } + } + public requestUndo(): void { + this.socket.send("game/undo/request", { + game_id: this.game_id, + move_number: this.engine.getCurrentMoveNumber(), + }); + } + public acceptUndo(): void { + this.socket.send("game/undo/accept", { + game_id: this.game_id, + move_number: this.engine.getCurrentMoveNumber(), + }); + } + public cancelUndo(): void { + this.socket.send("game/undo/cancel", { + game_id: this.game_id, + move_number: this.engine.getCurrentMoveNumber(), + }); + } + public pauseGame(): void { + this.socket.send("game/pause", { + game_id: this.game_id, + }); + } + public resumeGame(): void { + this.socket.send("game/resume", { + game_id: this.game_id, + }); + } + + public deleteBranch(): void { + if (!this.engine.cur_move.trunk) { + if (this.isPlayerController()) { + this.syncReviewMove({ delete: 1 }); + } + this.engine.deleteCurMove(); + this.emit("update"); + this.move_tree_redraw(); + } + } + + /** This is a callback that gets called by GobanEngine.setState to load + * previously saved board state. */ + //public setState(state: any): void { + public setState(): void { + if ((this.game_type === "review" || this.game_type === "demo") && this.engine) { + this.drawPenMarks(this.engine.cur_move.pen_marks); + if (this.isPlayerController() && this.connectToReviewSent) { + this.syncReviewMove(); + } + } + + this.setLabelCharacterFromMarks(); + this.markDirty(); + } + + public sendPreventStalling(winner: "black" | "white"): void { + this.socket.send("game/prevent_stalling", { + game_id: this.game_id, + winner, + }); + } + public sendPreventEscaping(winner: "black" | "white", annul: boolean): void { + this.socket.send("game/prevent_escaping", { + game_id: this.game_id, + winner, + annul, + }); + } + + public performStoneRemovalAutoScoring(): void { + try { + if ( + !(window as any)["user"] || + !this.on_game_screen || + !this.engine || + (((window as any)["user"].id as number) !== this.engine.players.black.id && + ((window as any)["user"].id as number) !== this.engine.players.white.id) + ) { + return; + } + } catch (e) { + console.error(e.stack); + return; + } + + this.stone_removal_auto_scoring_done = true; + + this.showMessage("processing", undefined, -1); + const do_score_estimation = () => { + const se = new ScoreEstimator( + this.engine, + this, + AUTOSCORE_TRIALS, + AUTOSCORE_TOLERANCE, + true /* prefer remote */, + true /* autoscore */, + /* Don't use existing stone removal markings for auto scoring */ + makeMatrix(this.width, this.height, false), + ); + + se.when_ready + .then(() => { + const current_removed = this.engine.getStoneRemovalString(); + const new_removed = se.getProbablyDead(); + + this.engine.clearRemoved(); + const moves = this.engine.decodeMoves(new_removed); + for (let i = 0; i < moves.length; ++i) { + this.engine.setRemoved(moves[i].x, moves[i].y, true, false); + } + + this.emit("stone-removal.updated"); + + this.engine.needs_sealing = se.autoscored_needs_sealing; + this.emit("stone-removal.needs-sealing", se.autoscored_needs_sealing); + + this.updateTitleAndStonePlacement(); + this.emit("update"); + + this.socket.send("game/removed_stones/set", { + game_id: this.game_id, + removed: false, + needs_sealing: se.autoscored_needs_sealing, + stones: current_removed, + }); + this.socket.send("game/removed_stones/set", { + game_id: this.game_id, + removed: true, + needs_sealing: se.autoscored_needs_sealing, + stones: new_removed, + }); + + this.clearMessage(); + }) + .catch((err) => { + console.error(`Auto-scoring error: `, err); + this.clearMessage(); + this.showMessage( + "error", + { + error: { + message: "Auto-scoring failed, please manually score the game", + }, + }, + 3000, + ); + }); + }; + + setTimeout(() => { + init_wasm_ownership_estimator() + .then(do_score_estimation) + .catch((err) => console.error(err)); + }, 10); + } + + public acceptRemovedStones(): void { + const stones = this.engine.getStoneRemovalString(); + this.engine.players[ + this.engine.playerColor(this.config.player_id) as "black" | "white" + ].accepted_stones = stones; + this.socket.send("game/removed_stones/accept", { + game_id: this.game_id, + stones: stones, + strict_seki_mode: this.engine.strict_seki_mode, + }); + } + public rejectRemovedStones(): void { + delete this.engine.players[ + this.engine.playerColor(this.config.player_id) as "black" | "white" + ].accepted_stones; + this.socket.send("game/removed_stones/reject", { + game_id: this.game_id, + }); + } + + /* Computes the relative latency between the target player and the current viewer. + * For example, if player P has a latency of 500ms and we have a latency of 200ms, + * the relative latency will be 300ms. This is used to artificially delay the clock + * countdown for that player to minimize the amount of apparent time jumping that can + * happen as clocks are synchronized */ + public getPlayerRelativeLatency(player_id: number): number { + if (player_id === this.player_id) { + return 0; + } + + // If the other latency is not available for whatever reason, use our own latency as a better-than-0 guess */ + const other_latency = this.engine?.latencies?.[player_id] || this.getNetworkLatency(); + + return other_latency - this.getNetworkLatency(); + } + public getLastReviewMessage(): ReviewMessage { + return this.last_review_message; + } + public setLastReviewMessage(m: ReviewMessage): void { + this.last_review_message = m; + } + + public setGameClock(original_clock: AdHocClock | null): void { + if (this.__clock_timer) { + clearTimeout(this.__clock_timer); + delete this.__clock_timer; + } + + if (!original_clock) { + this.emit("clock", null); + return; + } + + if (!this.config.time_control || !this.config.time_control.system) { + this.emit("clock", null); + return; + } + const time_control: JGOFTimeControl = this.config.time_control; + + this.last_clock = original_clock; + + let current_server_time = 0; + function update_current_server_time() { + if (callbacks.getClockDrift) { + const server_time_offset = callbacks.getClockDrift(); + current_server_time = Date.now() - server_time_offset; + } + } + update_current_server_time(); + + const clock: JGOFClockWithTransmitting = { + current_player: + original_clock.current_player === original_clock.black_player_id + ? "black" + : "white", + current_player_id: original_clock.current_player.toString(), + time_of_last_move: original_clock.last_move, + paused_since: original_clock.paused_since, + black_clock: { main_time: 0 }, + white_clock: { main_time: 0 }, + black_move_transmitting: 0, + white_move_transmitting: 0, + }; + + if (original_clock.pause) { + if (original_clock.pause.paused) { + this.paused_since = original_clock.pause.paused_since; + this.pause_control = original_clock.pause.pause_control; + + /* correct for when we used to store paused_since in terms of seconds instead of ms */ + if (this.paused_since < 2000000000) { + this.paused_since *= 1000; + } + + clock.paused_since = original_clock.pause.paused_since; + clock.pause_state = AdHocPauseControl2JGOFPauseState( + original_clock.pause.pause_control, + ); + } else { + delete this.paused_since; + delete this.pause_control; + } + } + + if (original_clock.start_mode) { + clock.start_mode = true; + } + + const last_audio_event: { [player_id: string]: AudioClockEvent } = { + black: { + countdown_seconds: 0, + clock: { main_time: 0 }, + player_id: "", + color: "black", + time_control_system: "none", + in_overtime: false, + }, + white: { + countdown_seconds: 0, + clock: { main_time: 0 }, + player_id: "", + color: "white", + time_control_system: "none", + in_overtime: false, + }, + }; + + const do_update = () => { + if (!time_control || !time_control.system) { + return; + } + + update_current_server_time(); + + const next_update_time = 100; + + if (clock.start_mode) { + clock.start_time_left = original_clock.expiration - current_server_time; + } + + if (this.paused_since) { + clock.paused_since = this.paused_since; + if (!this.pause_control) { + throw new Error(`Invalid pause_control state when performing clock do_update`); + } + clock.pause_state = AdHocPauseControl2JGOFPauseState(this.pause_control); + if (clock.pause_state.stone_removal) { + clock.stone_removal_time_left = original_clock.expiration - current_server_time; + } + } + + if (!clock.pause_state || Object.keys(clock.pause_state).length === 0) { + delete clock.paused_since; + delete clock.pause_state; + } + + if (this.last_paused_state === null) { + this.last_paused_state = !!clock.pause_state; + } else { + const cur_paused = !!clock.pause_state; + if (cur_paused !== this.last_paused_state) { + this.last_paused_state = cur_paused; + if (cur_paused) { + this.emit("audio-game-paused"); + } else { + this.emit("audio-game-resumed"); + } + } + } + + if (this.last_paused_by_player_state === null) { + this.last_paused_by_player_state = !!this.pause_control?.paused; + } else { + const cur_paused = !!this.pause_control?.paused; + if (cur_paused !== this.last_paused_by_player_state) { + this.last_paused_by_player_state = cur_paused; + if (cur_paused) { + this.emit("paused", cur_paused); + } else { + this.emit("paused", cur_paused); + } + } + } + + const elapsed: number = clock.paused_since + ? Math.max(clock.paused_since, original_clock.last_move) - original_clock.last_move + : current_server_time - original_clock.last_move; + + const black_relative_latency = this.getPlayerRelativeLatency( + original_clock.black_player_id, + ); + const white_relative_latency = this.getPlayerRelativeLatency( + original_clock.white_player_id, + ); + + const black_elapsed = Math.max(0, elapsed - Math.abs(black_relative_latency)); + const white_elapsed = Math.max(0, elapsed - Math.abs(white_relative_latency)); + + clock.black_clock = this.computeNewPlayerClock( + original_clock.black_time as AdHocPlayerClock, + clock.current_player === "black" && !clock.start_mode, + black_elapsed, + time_control, + ); + + clock.white_clock = this.computeNewPlayerClock( + original_clock.white_time as AdHocPlayerClock, + clock.current_player === "white" && !clock.start_mode, + white_elapsed, + time_control, + ); + + const wall_clock_elapsed = current_server_time - original_clock.last_move; + clock.black_move_transmitting = + clock.current_player === "black" + ? Math.max(0, black_relative_latency - wall_clock_elapsed) + : 0; + clock.white_move_transmitting = + clock.current_player === "white" + ? Math.max(0, white_relative_latency - wall_clock_elapsed) + : 0; + + if (!this.sent_timed_out_message && !this.clock_should_be_paused_for_move_submission) { + if ( + clock.current_player === "white" && + this.player_id === this.engine.config.white_player_id + ) { + if ((clock.white_clock as JGOFPlayerClockWithTimedOut).timed_out) { + this.sendTimedOut(); + } + } + if ( + clock.current_player === "black" && + this.player_id === this.engine.config.black_player_id + ) { + if ((clock.black_clock as JGOFPlayerClockWithTimedOut).timed_out) { + this.sendTimedOut(); + } + } + } + + if (this.clock_should_be_paused_for_move_submission && this.last_emitted_clock) { + this.emit("clock", this.last_emitted_clock); + } else { + this.emit("clock", clock); + } + + // check if we need to update our audio + if ( + (this.mode === "play" || + this.mode === "analyze" || + this.mode === "conditional" || + this.mode === "score estimation") && + this.engine.phase === "play" + ) { + // Move's and clock events are separate, so this just checks to make sure that when we + // update, we are updating when the engine and clock agree on whose turn it is. + const current_color = + this.engine.last_official_move.stoneColor === "black" ? "white" : "black"; + const current_player = this.engine.players[current_color].id.toString(); + + if (current_color === clock.current_player) { + const player_clock: JGOFPlayerClock = + clock.current_player === "black" ? clock.black_clock : clock.white_clock; + const audio_clock: AudioClockEvent = { + countdown_seconds: 0, + clock: player_clock, + player_id: current_player, + color: current_color, + time_control_system: time_control.system, + in_overtime: false, + }; + + switch (time_control.system) { + case "simple": + if (audio_clock.countdown_seconds === time_control.per_move) { + // When byo-yomi resets, we don't want to play the sound for the + // top of the second mark because it's going to get clipped short + // very soon as time passes and we're going to start playing the + // next second sound. + audio_clock.countdown_seconds = -1; + } else { + audio_clock.countdown_seconds = Math.ceil( + player_clock.main_time / 1000, + ); + } + break; + + case "absolute": + case "fischer": + audio_clock.countdown_seconds = Math.ceil( + player_clock.main_time / 1000, + ); + break; + + case "byoyomi": + if (player_clock.main_time > 0) { + audio_clock.countdown_seconds = Math.ceil( + player_clock.main_time / 1000, + ); + } else { + audio_clock.in_overtime = true; + audio_clock.countdown_seconds = Math.ceil( + (player_clock.period_time_left || 0) / 1000, + ); + if ((player_clock.periods_left || 0) <= 0) { + audio_clock.countdown_seconds = -1; + } + + /* + if ( + audio_clock.countdown_seconds === time_control.period_time && + audio_clock.in_overtime == last_audio_event[clock.current_player].in_overtime + ) { + // When byo-yomi resets, we don't want to play the sound for the + // top of the second mark because it's going to get clipped short + // very soon as time passes and we're going to start playing the + // next second sound. + audio_clock.countdown_seconds = -1; + } + */ + } + break; + + case "canadian": + if (player_clock.main_time > 0) { + audio_clock.countdown_seconds = Math.ceil( + player_clock.main_time / 1000, + ); + } else { + audio_clock.in_overtime = true; + audio_clock.countdown_seconds = Math.ceil( + (player_clock.block_time_left || 0) / 1000, + ); + + if (audio_clock.countdown_seconds === time_control.period_time) { + // When we start a new period, we don't want to play the sound for the + // top of the second mark because it's going to get clipped short + // very soon as time passes and we're going to start playing the + // next second sound. + audio_clock.countdown_seconds = -1; + } + } + break; + + case "none": + break; + + default: + throw new Error( + `Unsupported time control system: ${(time_control as any).system}`, + ); + } + + const cur = audio_clock; + const last = last_audio_event[clock.current_player]; + if ( + cur.countdown_seconds !== last.countdown_seconds || + cur.player_id !== last.player_id || + cur.in_overtime !== last.in_overtime + ) { + last_audio_event[clock.current_player] = audio_clock; + if (audio_clock.countdown_seconds > 0) { + this.emit("audio-clock", audio_clock); + } + } + } else { + // Engine and clock code didn't agree on whose turn it was, don't emit audio-clock event yet + } + } + + if (this.engine.phase !== "finished") { + this.__clock_timer = setTimeout(do_update, next_update_time); + } + }; + + do_update(); + } + + protected computeNewPlayerClock( + original_player_clock: Readonly, + is_current_player: boolean, + time_elapsed: number, + time_control: Readonly, + ): JGOFPlayerClockWithTimedOut { + const ret: JGOFPlayerClockWithTimedOut = { + main_time: 0, + timed_out: false, + }; + + const original_clock = this.last_clock; + if (!original_clock) { + throw new Error(`No last_clock when computing new player clock`); + } + + const tcs: string = "" + time_control.system; + switch (time_control.system) { + case "simple": + ret.main_time = is_current_player + ? Math.max(0, time_control.per_move - time_elapsed / 1000) * 1000 + : time_control.per_move * 1000; + if (ret.main_time <= 0) { + ret.timed_out = true; + } + break; + + case "none": + ret.main_time = 0; + break; + + case "absolute": + /* + ret.main_time = is_current_player + ? Math.max( + 0, + original_clock_expiration + raw_clock_pause_offset - current_server_time, + ) + : Math.max(0, original_player_clock.thinking_time * 1000); + */ + ret.main_time = is_current_player + ? Math.max(0, original_player_clock.thinking_time * 1000 - time_elapsed) + : original_player_clock.thinking_time * 1000; + if (ret.main_time <= 0) { + ret.timed_out = true; + } + break; + + case "fischer": + ret.main_time = is_current_player + ? Math.max(0, original_player_clock.thinking_time * 1000 - time_elapsed) + : original_player_clock.thinking_time * 1000; + if (ret.main_time <= 0) { + ret.timed_out = true; + } + break; + + case "byoyomi": + if (is_current_player) { + let overtime_usage = 0; + if (original_player_clock.thinking_time > 0) { + ret.main_time = original_player_clock.thinking_time * 1000 - time_elapsed; + if (ret.main_time <= 0) { + overtime_usage = -ret.main_time; + ret.main_time = 0; + } + } else { + ret.main_time = 0; + overtime_usage = time_elapsed; + } + ret.periods_left = original_player_clock.periods || 0; + ret.period_time_left = time_control.period_time * 1000; + if (overtime_usage > 0) { + const periods_used = Math.floor( + overtime_usage / (time_control.period_time * 1000), + ); + ret.periods_left -= periods_used; + ret.period_time_left = + time_control.period_time * 1000 - + (overtime_usage - periods_used * time_control.period_time * 1000); + + if (ret.periods_left < 0) { + ret.periods_left = 0; + } + + if (ret.period_time_left < 0) { + ret.period_time_left = 0; + } + } + } else { + ret.main_time = original_player_clock.thinking_time * 1000; + ret.periods_left = original_player_clock.periods; + ret.period_time_left = time_control.period_time * 1000; + } + + if (ret.main_time <= 0 && (ret.periods_left || 0) === 0) { + ret.timed_out = true; + } + break; + + case "canadian": + if (is_current_player) { + let overtime_usage = 0; + if (original_player_clock.thinking_time > 0) { + ret.main_time = original_player_clock.thinking_time * 1000 - time_elapsed; + if (ret.main_time <= 0) { + overtime_usage = -ret.main_time; + ret.main_time = 0; + } + } else { + ret.main_time = 0; + overtime_usage = time_elapsed; + } + ret.moves_left = original_player_clock.moves_left; + ret.block_time_left = (original_player_clock.block_time || 0) * 1000; + + if (overtime_usage > 0) { + ret.block_time_left -= overtime_usage; + + if (ret.block_time_left < 0) { + ret.block_time_left = 0; + } + } + } else { + ret.main_time = original_player_clock.thinking_time * 1000; + ret.moves_left = original_player_clock.moves_left; + ret.block_time_left = (original_player_clock.block_time || 0) * 1000; + } + + if (ret.main_time <= 0 && ret.block_time_left <= 0) { + ret.timed_out = true; + } + break; + + default: + throw new Error(`Unsupported time control system: ${tcs}`); + } + + return ret; + } + + /* DEPRECATED - this method should no longer be used and will likely be + * removed in the future, all Japanese games will start using strict seki + * scoring in the near future */ + public setStrictSekiMode(tf: boolean): void { + if (this.engine.phase !== "stone removal") { + throw "Not in stone removal phase"; + } + if (this.engine.strict_seki_mode === tf) { + return; + } + this.engine.strict_seki_mode = tf; + + this.socket.send("game/removed_stones/set", { + game_id: this.game_id, + stones: "", + removed: false, + strict_seki_mode: tf, + }); + } +} + +function uuid(): string { + // cspell: words yxxx + return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => { + const r = (Math.random() * 16) | 0; + const v = c === "x" ? r : (r & 0x3) | 0x8; + return v.toString(16); + }); +} + +function isPaused(pause_state: JGOFPauseState): boolean { + for (const _key in pause_state) { + return true; + } + return false; +} +function AdHocPauseControl2JGOFPauseState(pause_control: AdHocPauseControl): JGOFPauseState { + const ret: JGOFPauseState = {}; + + for (const k in pause_control) { + const matches = k.match(/vacation-([0-9]+)/); + if (matches) { + const player_id = matches[1]; + if (!ret.vacation) { + ret.vacation = {}; + } + ret.vacation[player_id] = true; + } else { + switch (k) { + case "stone-removal": + ret.stone_removal = true; + break; + + case "weekend": + ret.weekend = true; + break; + + case "server": + case "system": + ret.server = true; + break; + + case "paused": + ret.player = { + player_id: pause_control.paused?.pausing_player_id.toString() || "0", + pauses_left: pause_control.paused?.pauses_left || 0, + }; + break; + + case "moderator_paused": + ret.moderator = pause_control.moderator_paused?.moderator_id.toString() || "0"; + break; + + default: + throw new Error(`Unhandled pause control key: ${k}`); + } + } + } + + return ret; +} diff --git a/src/Goban/README.md b/src/Goban/README.md new file mode 100644 index 00000000..e142df4f --- /dev/null +++ b/src/Goban/README.md @@ -0,0 +1,51 @@ + + +This directory contains primarily front end `Goban` functionality. + +The main class here is the `Goban` class, however because there is a lot of +code and functionality that get's bundled up into a `Goban`, we've broken up +that functionality across several files which implement different units of +functionality and we use class inheritance to stack them up to something +usable. + + + + +```mermaid +--- +title: Goban functionality layers +--- +classDiagram + SVGRenderer --|> Goban : Rendering implementation + CanvasRenderer --|> Goban: Rendering implementation + Goban --|> OGSConnectivity : extends + OGSConnectivity --|> InteractiveBase: extends + InteractiveBase --|> GobanBase: extends + + + + class SVGRenderer { + Final rendering functionality + } + class CanvasRenderer { + Final rendering functionality + } + + class Goban { + Full functionality exposed + Common DOM manipulation functionality for our renderers + } + + class OGSConnectivity { + Encapsulates socket connection logic + } + + class InteractiveBase { + General purpose interactive functionality + No DOM expectations at this layer + } + + class GobanBase { + Very abstract base that the Engine can use to interact with the Goban + } +``` diff --git a/src/GobanSVG.ts b/src/Goban/SVGRenderer.ts similarity index 89% rename from src/GobanSVG.ts rename to src/Goban/SVGRenderer.ts index 632f06d1..def47cb8 100644 --- a/src/GobanSVG.ts +++ b/src/Goban/SVGRenderer.ts @@ -14,23 +14,29 @@ * limitations under the License. */ -import { JGOF, JGOFIntersection, JGOFNumericPlayerColor } from "./JGOF"; +import { JGOF, JGOFIntersection, JGOFNumericPlayerColor } from "engine/formats/JGOF"; -import { AdHocFormat } from "./AdHocFormat"; +import { AdHocFormat } from "engine/formats/AdHocFormat"; //import { GobanCore, GobanSelectedThemes, GobanMetrics, GOBAN_FONT } from "./GobanCore"; -import { GobanCore, GobanConfig, GobanSelectedThemes, GobanMetrics } from "./GobanCore"; -import { GoEngine } from "./GoEngine"; -import * as GoMath from "./GoMath"; -import { Group } from "./GoStoneGroup"; -import { MoveTree } from "./MoveTree"; -import { GoTheme } from "./GoTheme"; -import { GoThemes } from "./GoThemes"; -import { MoveTreePenMarks } from "./MoveTree"; +import { GobanConfig } from "../GobanBase"; +import { GobanEngine } from "engine"; +import { MoveTree } from "engine/MoveTree"; +import { GobanTheme, THEMES } from "./themes"; +import { MoveTreePenMarks } from "engine/MoveTree"; import { getRelativeEventPosition } from "./canvas_utils"; -import { getRandomInt } from "./GoUtil"; -import { _ } from "./translate"; -import { formatMessage, MessageID } from "./messages"; +import { _ } from "engine/translate"; +import { formatMessage, MessageID } from "engine/messages"; +import { + color_blend, + encodeMove, + encodeMoves, + encodePrettyXCoordinate, + getRandomInt, + makeMatrix, +} from "engine/util"; +import { callbacks } from "./callbacks"; +import { Goban, GobanMetrics, GobanSelectedThemes } from "./Goban"; //import { GobanCanvasConfig, GobanCanvasInterface } from "./GobanCanvas"; @@ -44,14 +50,14 @@ const __theme_cache: { declare let ResizeObserver: any; -export interface GobanSVGConfig extends GobanConfig { +export interface SVGRendererGobanConfig extends GobanConfig { board_div?: HTMLElement; title_div?: HTMLElement; move_tree_container?: HTMLElement; last_move_opacity?: number; } -interface ViewPortInterface { +interface MoveTreeViewPortInterface { offset_x: number; offset_y: number; minx: number; @@ -64,7 +70,7 @@ const HOT_PINK = "#ff69b4"; //interface GobanCanvasInterface { interface GobanSVGInterface { - engine: GoEngine; + engine: GobanEngine; move_tree_container?: HTMLElement; clearAnalysisDrawing(): void; @@ -90,9 +96,8 @@ interface GobanSVGInterface { destroy(): void; } -export class GobanSVG extends GobanCore implements GobanSVGInterface { - public engine: GoEngine; - private parent: HTMLElement; +export class SVGRenderer extends Goban implements GobanSVGInterface { + public engine: GobanEngine; //private board_div: HTMLElement; private svg: SVGElement; private svg_defs: SVGDefsElement; @@ -136,25 +141,24 @@ export class GobanSVG extends GobanCore implements GobanSVGInterface { black: "Plain", white: "Plain", }; - private theme_black!: GoTheme; + private theme_black!: GobanTheme; private theme_black_stones: Array = []; private theme_black_text_color: string = HOT_PINK; private theme_blank_text_color: string = HOT_PINK; - private theme_board!: GoTheme; + private theme_board!: GobanTheme; private theme_faded_line_color: string = HOT_PINK; private theme_faded_star_color: string = HOT_PINK; //private theme_faded_text_color:string; private theme_line_color: string = ""; private theme_star_color: string = ""; private theme_stone_radius: number = 10; - private theme_white!: GoTheme; + private theme_white!: GobanTheme; private theme_white_stones: Array = []; private theme_white_text_color: string = HOT_PINK; - constructor(config: GobanSVGConfig, preloaded_data?: AdHocFormat | JGOF) { + constructor(config: SVGRendererGobanConfig, preloaded_data?: AdHocFormat | JGOF) { /* TODO: Need to reconcile the clock fields before we can get rid of this `any` cast */ super(config, preloaded_data as any); - console.info("GobanSVG created"); if (config.board_div) { this.parent = config["board_div"]; @@ -201,7 +205,7 @@ export class GobanSVG extends GobanCore implements GobanSVGInterface { // this.theme_board // this.theme_white // this.theme_black - this.setThemes(this.getSelectedThemes(), true); + this.setTheme(this.getSelectedThemes(), true); let first_pass = true; const watcher = this.watchSelectedThemes((themes: GobanSelectedThemes) => { if ( @@ -214,7 +218,7 @@ export class GobanSVG extends GobanCore implements GobanSVGInterface { delete __theme_cache.black?.["Custom"]; delete __theme_cache.white?.["Custom"]; delete __theme_cache.board?.["Custom"]; - this.setThemes(themes, first_pass ? true : false); + this.setTheme(themes, first_pass ? true : false); first_pass = false; }); this.on("destroy", () => watcher.remove()); @@ -236,7 +240,7 @@ export class GobanSVG extends GobanCore implements GobanSVGInterface { this.detachPenLayer(); } - public destroy(): void { + public override destroy(): void { super.destroy(); this.clearMessage(); @@ -300,8 +304,10 @@ export class GobanSVG extends GobanCore implements GobanSVGInterface { let dragging = false; let last_click_square = this.xy2ij(0, 0); + let pointer_down_timestamp = 0; const pointerUp = (ev: MouseEvent | TouchEvent, double_clicked: boolean): void => { + const press_duration_ms = performance.now() - pointer_down_timestamp; try { if (!dragging) { return; @@ -320,11 +326,12 @@ export class GobanSVG extends GobanCore implements GobanSVGInterface { const pos = getRelativeEventPosition(ev, this.parent); const pt = this.xy2ij(pos.x, pos.y); if (pt.i >= 0 && pt.i < this.width && pt.j >= 0 && pt.j < this.height) { - if (this.score_estimate) { - this.score_estimate.handleClick( + if (this.score_estimator) { + this.score_estimator.handleClick( pt.i, pt.j, ev.ctrlKey || ev.metaKey || ev.altKey || ev.shiftKey, + press_duration_ms, ); } this.emit("update"); @@ -336,9 +343,9 @@ export class GobanSVG extends GobanCore implements GobanSVGInterface { try { const pos = getRelativeEventPosition(ev, this.parent); const pt = this.xy2ij(pos.x, pos.y); - if (GobanCore.hooks.addCoordinatesToChatInput) { - GobanCore.hooks.addCoordinatesToChatInput( - this.engine.prettyCoords(pt.i, pt.j), + if (callbacks.addCoordinatesToChatInput) { + callbacks.addCoordinatesToChatInput( + this.engine.prettyCoordinates(pt.i, pt.j), ); } } catch (e) { @@ -349,6 +356,10 @@ export class GobanSVG extends GobanCore implements GobanSVGInterface { if (this.mode === "analyze" && this.analyze_tool === "draw") { /* might want to interpret this as a start/stop of a line segment */ + } else if (this.mode === "analyze" && this.analyze_tool === "score") { + // nothing to do here + } else if (this.mode === "analyze" && this.analyze_tool === "removal") { + this.onAnalysisToggleStoneRemoval(ev); } else { const pos = getRelativeEventPosition(ev, this.parent); const pt = this.xy2ij(pos.x, pos.y); @@ -361,7 +372,7 @@ export class GobanSVG extends GobanCore implements GobanSVGInterface { } } - this.onTap(ev, double_clicked, right_click); + this.onTap(ev, double_clicked, right_click, press_duration_ms); this.onMouseOut(ev); } } catch (e) { @@ -370,6 +381,7 @@ export class GobanSVG extends GobanCore implements GobanSVGInterface { }; const pointerDown = (ev: MouseEvent | TouchEvent): void => { + pointer_down_timestamp = performance.now(); try { dragging = true; if (this.mode === "analyze" && this.analyze_tool === "draw") { @@ -390,6 +402,10 @@ export class GobanSVG extends GobanCore implements GobanSVGInterface { } this.onLabelingStart(ev); + } else if (this.mode === "analyze" && this.analyze_tool === "score") { + this.onAnalysisScoringStart(ev); + } else if (this.mode === "analyze" && this.analyze_tool === "removal") { + // nothing to do here, we act on pointerUp } } catch (e) { console.error(e); @@ -405,6 +421,10 @@ export class GobanSVG extends GobanCore implements GobanSVGInterface { this.onPenMove(ev); } else if (dragging && this.mode === "analyze" && this.analyze_tool === "label") { this.onLabelingMove(ev); + } else if (dragging && this.mode === "analyze" && this.analyze_tool === "score") { + this.onAnalysisScoringMove(ev); + } else if (dragging && this.mode === "analyze" && this.analyze_tool === "removal") { + // nothing for moving } else { this.onMouseMove(ev); } @@ -700,7 +720,12 @@ export class GobanSVG extends GobanCore implements GobanSVGInterface { this.pen_layer?.appendChild(path_element); } } - private onTap(event: MouseEvent | TouchEvent, double_tap: boolean, right_click: boolean): void { + private onTap( + event: MouseEvent | TouchEvent, + double_tap: boolean, + right_click: boolean, + press_duration_ms: number, + ): void { if ( !( this.stone_placement_enabled && @@ -780,7 +805,7 @@ export class GobanSVG extends GobanCore implements GobanSVGInterface { } const sent = this.sendMove({ game_id: this.game_id, - move: GoMath.encodeMove(x, y), + move: encodeMove(x, y), }); if (sent) { this.playMovementSound(); @@ -803,22 +828,20 @@ export class GobanSVG extends GobanCore implements GobanSVGInterface { if ( this.engine.phase === "stone removal" && - this.engine.isActivePlayer(this.player_id) + this.engine.isActivePlayer(this.player_id) && + this.engine.cur_move === this.engine.last_official_move ) { - let removed: 0 | 1; - let group: Group; - if (event.shiftKey) { - removed = !this.engine.removal[y][x] ? 1 : 0; - group = [{ x, y }]; - } else { - [[removed, group]] = this.engine.toggleMetaGroupRemoval(x, y); - } + const { removed, group } = this.engine.toggleSingleGroupRemoval( + x, + y, + event.shiftKey || press_duration_ms > 500, + ); if (group.length) { this.socket.send("game/removed_stones/set", { game_id: this.game_id, - removed: !!removed, - stones: GoMath.encodeMoves(group), + removed: removed, + stones: encodeMoves(group), }); } } else if (this.mode === "puzzle") { @@ -1403,34 +1426,33 @@ export class GobanSVG extends GobanCore implements GobanSVGInterface { } /* Colored stones */ - if (this.colored_circles) { - if (this.colored_circles[j][i]) { - const circle = this.colored_circles[j][i]; - const radius = Math.floor(this.square_size * 0.5) - 0.5; - let lineWidth = radius * (circle.border_width || 0.1); - if (lineWidth < 0.3) { - lineWidth = 0; - } - - const circ = document.createElementNS("http://www.w3.org/2000/svg", "circle"); - circ.setAttribute("class", "colored-circle"); - circ.setAttribute("fill", circle.color); - if (circle.border_color) { - circ.setAttribute("stroke", circle.border_color); - } - if (lineWidth > 0) { - circ.setAttribute("stroke-width", lineWidth.toFixed(1)); - } else { - circ.setAttribute("stroke-width", "1px"); - } - circ.setAttribute("cx", cx.toString()); - circ.setAttribute("cy", cy.toString()); - circ.setAttribute("r", Math.max(0.1, radius - lineWidth / 2).toString()); - cell.appendChild(circ); + const circle = this.colored_circles?.[j][i]; + if (circle) { + const radius = Math.floor(this.square_size * 0.5) - 0.5; + let lineWidth = radius * (circle.border_width || 0.1); + if (lineWidth < 0.3) { + lineWidth = 0; + } + + const circ = document.createElementNS("http://www.w3.org/2000/svg", "circle"); + circ.setAttribute("class", "colored-circle"); + circ.setAttribute("fill", circle.color); + if (circle.border_color) { + circ.setAttribute("stroke", circle.border_color); + } + if (lineWidth > 0) { + circ.setAttribute("stroke-width", lineWidth.toFixed(1)); + } else { + circ.setAttribute("stroke-width", "1px"); } + circ.setAttribute("cx", cx.toString()); + circ.setAttribute("cy", cy.toString()); + circ.setAttribute("r", Math.max(0.1, radius - lineWidth / 2).toString()); + cell.appendChild(circ); } /* Draw stones & hovers */ + let draw_red_x = false; { if ( stone_color /* if there is really a stone here */ || @@ -1448,9 +1470,9 @@ export class GobanSVG extends GobanCore implements GobanSVGInterface { (this.getPuzzlePlacementSetting && this.getPuzzlePlacementSetting().mode === "play"))) || (this.scoring_mode && - this.score_estimate && - this.score_estimate.board[j][i] && - this.score_estimate.removal[j][i]) || + this.score_estimator && + this.score_estimator.board[j][i] && + this.score_estimator.removal[j][i]) || (this.engine && this.engine.phase === "stone removal" && this.engine.board[j][i] && @@ -1459,20 +1481,21 @@ export class GobanSVG extends GobanCore implements GobanSVGInterface { pos.white ) { //let color = stone_color ? stone_color : (this.move_selected ? this.engine.otherPlayer() : this.engine.player); - let transparent = false; + let translucent = false; let stoneAlphaValue = 0.6; let color; if ( this.scoring_mode && - this.score_estimate && - this.score_estimate.board[j][i] && - this.score_estimate.removal[j][i] + this.score_estimator && + this.score_estimator.board[j][i] && + this.score_estimator.removal[j][i] ) { - color = this.score_estimate.board[j][i]; - transparent = true; + color = this.score_estimator.board[j][i]; + translucent = true; } else if ( this.engine && - (this.engine.phase === "stone removal" || + ((this.engine.phase === "stone removal" && + this.engine.last_official_move === this.engine.cur_move) || (this.engine.phase === "finished" && this.mode !== "analyze")) && this.engine.board && this.engine.removal && @@ -1480,7 +1503,7 @@ export class GobanSVG extends GobanCore implements GobanSVGInterface { this.engine.removal[j][i] ) { color = this.engine.board[j][i]; - transparent = true; + translucent = true; } else if (stone_color) { color = stone_color; } else if ( @@ -1514,12 +1537,16 @@ export class GobanSVG extends GobanCore implements GobanSVGInterface { } } else if (pos.black || pos.white) { color = pos.black ? 1 : 2; - transparent = true; + translucent = true; stoneAlphaValue = this.variation_stone_opacity; } else { color = this.engine.player; } + if (pos.stone_removed) { + translucent = true; + } + if (!(this.autoplaying_puzzle_move && !stone_color)) { text_color = color === 1 ? this.theme_black_text_color : this.theme_white_text_color; @@ -1547,7 +1574,7 @@ export class GobanSVG extends GobanCore implements GobanSVGInterface { return; } - const stone_transparent = transparent || !stone_color; + const stone_transparent = translucent || !stone_color; if (color === 1) { const stone = this.theme_black.getStone( @@ -1590,13 +1617,9 @@ export class GobanSVG extends GobanCore implements GobanSVGInterface { } } - if ( - pos.blue_move && - this.colored_circles && - this.colored_circles[j] && - this.colored_circles[j][i] - ) { - const circle = this.colored_circles[j][i]; + /** Draw the circle around the blue move */ + const circle = this.colored_circles?.[j][i]; + if (pos.blue_move && circle) { const radius = Math.floor(this.square_size * 0.5) - 0.5; let lineWidth = radius * (circle.border_width || 0.1); if (lineWidth < 0.3) { @@ -1613,40 +1636,102 @@ export class GobanSVG extends GobanCore implements GobanSVGInterface { } else { circ.setAttribute("stroke-width", "1px"); } + circ.setAttribute("fill", "none"); circ.setAttribute("cx", cx.toString()); circ.setAttribute("cy", cy.toString()); circ.setAttribute("r", Math.max(0.1, radius - lineWidth / 2).toString()); cell.appendChild(circ); } + + /* Red X if the stone is marked for removal */ + if ( + (this.engine && + this.engine.phase === "stone removal" && + this.engine.last_official_move === this.engine.cur_move && + this.engine.board[j][i] && + this.engine.removal[j][i]) || + (this.scoring_mode && + this.score_estimator && + this.score_estimator.board[j][i] && + this.score_estimator.removal[j][i]) || + //(this.mode === "analyze" && pos.stone_removed) + pos.stone_removed + ) { + draw_red_x = true; + } } } + if ( + draw_red_x || + (this.mode === "analyze" && + this.analyze_tool === "removal" && + this.last_hover_square && + this.last_hover_square.x === i && + this.last_hover_square.y === j) || + (this.engine.phase === "stone removal" && + this.engine.isActivePlayer(this.player_id) && + this.engine.cur_move === this.engine.last_official_move && + this.last_hover_square && + this.last_hover_square.x === i && + this.last_hover_square.y === j) + ) { + const r = Math.max(1, this.metrics.mid * 0.75); + const cross = document.createElementNS("http://www.w3.org/2000/svg", "path"); + cross.setAttribute("class", "removal-cross"); + cross.setAttribute("stroke", "#ff0000"); + cross.setAttribute("stroke-width", `${this.square_size * 0.125}px`); + cross.setAttribute("fill", "none"); + cross.setAttribute( + "d", + ` + M ${cx - r} ${cy - r} + L ${cx + r} ${cy + r} + M ${cx + r} ${cy - r} + L ${cx - r} ${cy + r} + `, + ); + const opacity = this.engine.board[j][i] ? 1.0 : 0.2; + cross.setAttribute("stroke-opacity", opacity?.toString()); + + cell.appendChild(cross); + } + /* Draw Scores */ { if ( - (pos.score && (this.engine.phase !== "finished" || this.mode === "play")) || + (pos.score && + (this.engine.phase !== "finished" || + this.mode === "play" || + this.mode === "analyze")) || (this.scoring_mode && - this.score_estimate && - (this.score_estimate.territory[j][i] || - (this.score_estimate.removal[j][i] && - this.score_estimate.board[j][i] === 0))) || + this.score_estimator && + (this.score_estimator.territory[j][i] || + (this.score_estimator.removal[j][i] && + this.score_estimator.board[j][i] === 0))) || ((this.engine.phase === "stone removal" || (this.engine.phase === "finished" && this.mode === "play")) && this.engine.board[j][i] === 0 && - this.engine.removal[j][i]) + (this.engine.removal[j][i] || pos.needs_sealing)) || + (this.mode === "analyze" && + this.analyze_tool === "score" && + this.last_hover_square && + this.last_hover_square.x === i && + this.last_hover_square.y === j) ) { let color = pos.score; + if ( this.scoring_mode && - this.score_estimate && - (this.score_estimate.territory[j][i] || - (this.score_estimate.removal[j][i] && - this.score_estimate.board[j][i] === 0)) + this.score_estimator && + (this.score_estimator.territory[j][i] || + (this.score_estimator.removal[j][i] && + this.score_estimator.board[j][i] === 0)) ) { - color = this.score_estimate.territory[j][i] === 1 ? "black" : "white"; + color = this.score_estimator.territory[j][i] === 1 ? "black" : "white"; if ( - this.score_estimate.board[j][i] === 0 && - this.score_estimate.removal[j][i] + this.score_estimator.board[j][i] === 0 && + this.score_estimator.removal[j][i] ) { color = "dame"; } @@ -1661,6 +1746,20 @@ export class GobanSVG extends GobanCore implements GobanSVGInterface { color = "dame"; } + if (pos.needs_sealing) { + color = "seal"; + } + + if ( + this.mode === "analyze" && + this.analyze_tool === "score" && + this.last_hover_square && + this.last_hover_square.x === i && + this.last_hover_square.y === j + ) { + color = this.analyze_subtool; + } + const r = this.square_size * 0.15; const rect = document.createElementNS("http://www.w3.org/2000/svg", "rect"); rect.setAttribute("x", (cx - r).toFixed(1)); @@ -1679,6 +1778,15 @@ export class GobanSVG extends GobanCore implements GobanSVGInterface { rect.setAttribute("fill-opacity", "0.2"); rect.setAttribute("stroke", "#365FE6"); } + if (color === "seal") { + rect.setAttribute("fill-opacity", "0.8"); + rect.setAttribute("fill", "#ff4444"); + rect.setAttribute("stroke", "#E079CE"); + } + if (color?.[0] === "#") { + rect.setAttribute("fill", color); + rect.setAttribute("stroke", color_blend("#888888", color)); + } rect.setAttribute( "stroke-width", (Math.ceil(this.square_size * 0.065) - 0.5).toFixed(1), @@ -2031,7 +2139,7 @@ export class GobanSVG extends GobanCore implements GobanSVGInterface { /* Score Estimation */ if ( - (this.scoring_mode === true && this.score_estimate) || + (this.scoring_mode === true && this.score_estimator) || (this.scoring_mode === "stalling-scoring-mode" && this.stalling_score_estimate && this.mode !== "analyze") @@ -2039,7 +2147,7 @@ export class GobanSVG extends GobanCore implements GobanSVGInterface { const se = this.scoring_mode === "stalling-scoring-mode" ? this.stalling_score_estimate - : this.score_estimate; + : this.score_estimator; const est = se!.ownership[j][i]; const color = est < 0 ? "white" : "black"; const color_num = color === "black" ? 1 : 2; @@ -2096,11 +2204,9 @@ export class GobanSVG extends GobanCore implements GobanSVGInterface { } /* Colored stones */ - if (this.colored_circles) { - if (this.colored_circles[j][i]) { - const circle = this.colored_circles[j][i]; - ret += "circle " + circle.color; - } + const circle = this.colored_circles?.[j][i]; + if (circle) { + ret += "circle " + circle.color; } /* Figure out marks for this spot */ @@ -2135,6 +2241,7 @@ export class GobanSVG extends GobanCore implements GobanSVGInterface { } /* Draw stones & hovers */ + let draw_red_x = false; { if ( stone_color /* if there is really a stone here */ || @@ -2152,9 +2259,9 @@ export class GobanSVG extends GobanCore implements GobanSVGInterface { (this.getPuzzlePlacementSetting && this.getPuzzlePlacementSetting().mode !== "play"))) || (this.scoring_mode && - this.score_estimate && - this.score_estimate.board[j][i] && - this.score_estimate.removal[j][i]) || + this.score_estimator && + this.score_estimator.board[j][i] && + this.score_estimator.removal[j][i]) || (this.engine && this.engine.phase === "stone removal" && this.engine.board[j][i] && @@ -2162,16 +2269,16 @@ export class GobanSVG extends GobanCore implements GobanSVGInterface { pos.black || pos.white ) { - let transparent = false; + let translucent = false; let color; if ( this.scoring_mode && - this.score_estimate && - this.score_estimate.board[j][i] && - this.score_estimate.removal[j][i] + this.score_estimator && + this.score_estimator.board[j][i] && + this.score_estimator.removal[j][i] ) { - color = this.score_estimate.board[j][i]; - transparent = true; + color = this.score_estimator.board[j][i]; + translucent = true; } else if ( this.engine && this.engine.phase === "stone removal" && @@ -2181,7 +2288,7 @@ export class GobanSVG extends GobanCore implements GobanSVGInterface { this.engine.removal[j][i] ) { color = this.engine.board[j][i]; - transparent = true; + translucent = true; } else if (stone_color) { color = stone_color; } else if ( @@ -2198,11 +2305,16 @@ export class GobanSVG extends GobanCore implements GobanSVGInterface { } } else if (pos.black || pos.white) { color = pos.black ? 1 : 2; - transparent = true; + translucent = true; } else { color = this.engine.player; } + //if (this.mode === "analyze" && pos.stone_removed) { + if (pos.stone_removed) { + translucent = true; + } + if (color === 1) { ret += this.theme_black.getStoneHash(i, j, this.theme_black_stones, this); } @@ -2210,10 +2322,52 @@ export class GobanSVG extends GobanCore implements GobanSVGInterface { ret += this.theme_white.getStoneHash(i, j, this.theme_white_stones, this); } - ret += (transparent ? "T" : "") + color + ","; + if ( + pos.blue_move && + this.colored_circles && + this.colored_circles[j] && + this.colored_circles[j][i] + ) { + ret += "blue"; + } + + if ( + (this.engine && + this.engine.phase === "stone removal" && + this.engine.last_official_move === this.engine.cur_move && + this.engine.board[j][i] && + this.engine.removal[j][i]) || + (this.scoring_mode && + this.score_estimator && + this.score_estimator.board[j][i] && + this.score_estimator.removal[j][i]) || + //(this.mode === "analyze" && pos.stone_removed) + pos.stone_removed + ) { + draw_red_x = true; + } + + ret += (translucent ? "T" : "") + color + ","; } } + if ( + draw_red_x || + (this.mode === "analyze" && + this.analyze_tool === "removal" && + this.last_hover_square && + this.last_hover_square.x === i && + this.last_hover_square.y === j) || + (this.engine.phase === "stone removal" && + this.engine.isActivePlayer(this.player_id) && + this.engine.cur_move === this.engine.last_official_move && + this.last_hover_square && + this.last_hover_square.x === i && + this.last_hover_square.y === j) + ) { + ret += "redX"; + } + /* Draw square highlights if any */ { if (pos.hint || (this.highlight_movetree_moves && movetree_contains_this_square)) { @@ -2247,7 +2401,7 @@ export class GobanSVG extends GobanCore implements GobanSVGInterface { transparent = false; } - if (this.scoring_mode && this.score_estimate && this.score_estimate.removal[j][i]) { + if (this.scoring_mode && this.score_estimator && this.score_estimator.removal[j][i]) { draw_x = true; transparent = false; } @@ -2263,29 +2417,37 @@ export class GobanSVG extends GobanCore implements GobanSVGInterface { /* Draw Scores */ { if ( - (pos.score && (this.engine.phase !== "finished" || this.mode === "play")) || + (pos.score && + (this.engine.phase !== "finished" || + this.mode === "play" || + this.mode === "analyze")) || (this.scoring_mode && - this.score_estimate && - (this.score_estimate.territory[j][i] || - (this.score_estimate.removal[j][i] && - this.score_estimate.board[j][i] === 0))) || + this.score_estimator && + (this.score_estimator.territory[j][i] || + (this.score_estimator.removal[j][i] && + this.score_estimator.board[j][i] === 0))) || ((this.engine.phase === "stone removal" || (this.engine.phase === "finished" && this.mode === "play")) && this.engine.board[j][i] === 0 && - this.engine.removal[j][i]) + (this.engine.removal[j][i] || pos.needs_sealing)) || + (this.mode === "analyze" && + this.analyze_tool === "score" && + this.last_hover_square && + this.last_hover_square.x === i && + this.last_hover_square.y === j) ) { let color = pos.score; if ( this.scoring_mode && - this.score_estimate && - (this.score_estimate.territory[j][i] || - (this.score_estimate.removal[j][i] && - this.score_estimate.board[j][i] === 0)) + this.score_estimator && + (this.score_estimator.territory[j][i] || + (this.score_estimator.removal[j][i] && + this.score_estimator.board[j][i] === 0)) ) { - color = this.score_estimate.territory[j][i] === 1 ? "black" : "white"; + color = this.score_estimator.territory[j][i] === 1 ? "black" : "white"; if ( - this.score_estimate.board[j][i] === 0 && - this.score_estimate.removal[j][i] + this.score_estimator.board[j][i] === 0 && + this.score_estimator.removal[j][i] ) { color = "dame"; } @@ -2300,12 +2462,25 @@ export class GobanSVG extends GobanCore implements GobanSVGInterface { color = "dame"; } + if (pos.needs_sealing) { + color = "seal"; + } + + if ( + this.mode === "analyze" && + this.analyze_tool === "score" && + this.last_hover_square && + this.last_hover_square.x === i && + this.last_hover_square.y === j + ) { + color = this.analyze_subtool; + } if ( this.scoring_mode && - this.score_estimate && - this.score_estimate.territory[j][i] + this.score_estimator && + this.score_estimator.territory[j][i] ) { - color = this.score_estimate.territory[j][i] === 1 ? "black" : "white"; + color = this.score_estimator.territory[j][i] === 1 ? "black" : "white"; } ret += "score " + color + ","; } @@ -2414,7 +2589,7 @@ export class GobanSVG extends GobanCore implements GobanSVGInterface { /* Score Estimation */ if ( - (this.scoring_mode === true && this.score_estimate) || + (this.scoring_mode === true && this.score_estimator) || (this.scoring_mode === "stalling-scoring-mode" && this.stalling_score_estimate && this.mode !== "analyze") @@ -2422,7 +2597,7 @@ export class GobanSVG extends GobanCore implements GobanSVGInterface { const se = this.scoring_mode === "stalling-scoring-mode" ? this.stalling_score_estimate - : this.score_estimate; + : this.score_estimator; const est = se!.ownership[j][i]; ret += est.toFixed(5) + ","; @@ -2626,7 +2801,7 @@ export class GobanSVG extends GobanCore implements GobanSVGInterface { this.square_size + this.square_size / 2; const y = j * this.square_size + this.square_size / 2; - place(GoMath.pretty_coor_num2ch(c), x, y); + place(encodePrettyXCoordinate(c), x, y); } break; case "1-1": @@ -2759,7 +2934,7 @@ export class GobanSVG extends GobanCore implements GobanSVGInterface { this.__set_board_width = metrics.width; this.__set_board_height = metrics.height; - this.setThemes(this.getSelectedThemes(), true); + this.setTheme(this.getSelectedThemes(), true); } catch (e) { setTimeout(() => { throw e; @@ -2807,7 +2982,7 @@ export class GobanSVG extends GobanCore implements GobanSVGInterface { this.__draw_state.length !== this.height || this.__draw_state[0].length !== this.width ) { - this.__draw_state = GoMath.makeStringMatrix(this.width, this.height); + this.__draw_state = makeMatrix(this.width, this.height, ""); } for (let j = this.bounds.top; j <= this.bounds.bottom; ++j) { @@ -2893,16 +3068,16 @@ export class GobanSVG extends GobanCore implements GobanSVGInterface { this.emit("clear-message"); } - protected setThemes(themes: GobanSelectedThemes, dont_redraw: boolean): void { + protected setTheme(themes: GobanSelectedThemes, dont_redraw: boolean): void { if (this.no_display) { console.log("No display"); return; } this.themes = themes; - const BoardTheme = GoThemes["board"]?.[themes.board] || GoThemes["board"]["Plain"]; - const WhiteTheme = GoThemes["white"]?.[themes.white] || GoThemes["white"]["Plain"]; - const BlackTheme = GoThemes["black"]?.[themes.black] || GoThemes["black"]["Plain"]; + const BoardTheme = THEMES["board"]?.[themes.board] || THEMES["board"]["Plain"]; + const WhiteTheme = THEMES["white"]?.[themes.white] || THEMES["white"]["Plain"]; + const BlackTheme = THEMES["black"]?.[themes.black] || THEMES["black"]["Plain"]; this.theme_board = new BoardTheme(); this.theme_white = new WhiteTheme(this.theme_board); this.theme_black = new BlackTheme(this.theme_board); @@ -2988,7 +3163,7 @@ export class GobanSVG extends GobanCore implements GobanSVGInterface { } private onLabelingStart(ev: MouseEvent | TouchEvent) { const pos = getRelativeEventPosition(ev, this.parent); - this.last_label_position = this.xy2ij(pos.x, pos.y); + this.last_label_position = this.xy2ij(pos.x, pos.y, false); { const x = this.last_label_position.i; @@ -3034,7 +3209,6 @@ export class GobanSVG extends GobanCore implements GobanSVGInterface { this.setLabelCharacterFromMarks(); } } - protected setTitle(title: string): void { this.title = title; if (this.title_div) { @@ -3044,8 +3218,8 @@ export class GobanSVG extends GobanCore implements GobanSVGInterface { protected watchSelectedThemes(cb: (themes: GobanSelectedThemes) => void): { remove: () => any; } { - if (GobanCore.hooks.watchSelectedThemes) { - return GobanCore.hooks.watchSelectedThemes(cb); + if (callbacks.watchSelectedThemes) { + return callbacks.watchSelectedThemes(cb); } return { remove: () => {} }; } @@ -3262,7 +3436,7 @@ export class GobanSVG extends GobanCore implements GobanSVGInterface { svg: SVGElement, node: MoveTree, active_path_number: number, - viewport: ViewPortInterface, + viewport: MoveTreeViewPortInterface, ): void { const stone_idx = node.move_number * 31; const cx = node.layout_cx - viewport.offset_x; @@ -3302,11 +3476,7 @@ export class GobanSVG extends GobanCore implements GobanSVGInterface { const text_color = color === 1 ? this.theme_black_text_color : this.theme_white_text_color; let label = ""; - switch ( - GobanCore.hooks.getMoveTreeNumbering - ? GobanCore.hooks.getMoveTreeNumbering() - : "move-number" - ) { + switch (callbacks.getMoveTreeNumbering ? callbacks.getMoveTreeNumbering() : "move-number") { case "move-coordinates": label = node.pretty_coordinates; break; @@ -3369,7 +3539,7 @@ export class GobanSVG extends GobanCore implements GobanSVGInterface { svg: SVGElement, node: MoveTree, active_path_number: number, - viewport: ViewPortInterface, + viewport: MoveTreeViewPortInterface, ): void { if (node.trunk_next) { this.move_tree_drawRecursive(svg, node.trunk_next, active_path_number, viewport); @@ -3392,7 +3562,7 @@ export class GobanSVG extends GobanCore implements GobanSVGInterface { svg: SVGElement, node: MoveTree, color: string, - viewport: ViewPortInterface, + viewport: MoveTreeViewPortInterface, ): void { const rect = document.createElementNS("http://www.w3.org/2000/svg", "rect"); const sx = @@ -3407,7 +3577,7 @@ export class GobanSVG extends GobanCore implements GobanSVGInterface { svg.appendChild(rect); } - move_tree_drawPath(svg: SVGElement, node: MoveTree, viewport: ViewPortInterface): void { + move_tree_drawPath(svg: SVGElement, node: MoveTree, viewport: MoveTreeViewPortInterface): void { if (node.parent) { if (node.parent.layout_cx < viewport.minx && node.layout_cx < viewport.minx) { return; @@ -3444,7 +3614,7 @@ export class GobanSVG extends GobanCore implements GobanSVGInterface { svg: SVGElement, from_node: MoveTree, to_node: MoveTree, - viewport: ViewPortInterface, + viewport: MoveTreeViewPortInterface, ): void { let A: MoveTree = from_node; let B: MoveTree = to_node; @@ -3498,7 +3668,7 @@ export class GobanSVG extends GobanCore implements GobanSVGInterface { move_tree_recursiveDrawPath( svg: SVGElement, node: MoveTree, - viewport: ViewPortInterface, + viewport: MoveTreeViewPortInterface, ): void { if (node.trunk_next) { this.move_tree_recursiveDrawPath(svg, node.trunk_next, viewport); diff --git a/src/TestGoban.ts b/src/Goban/TestGoban.ts similarity index 61% rename from src/TestGoban.ts rename to src/Goban/TestGoban.ts index 556b2528..d8621acb 100644 --- a/src/TestGoban.ts +++ b/src/Goban/TestGoban.ts @@ -15,27 +15,22 @@ * limitations under the License. */ -// This is a minimal implementation of GobanCore. Currently it is just enough -// to build (in other words, silence the abstract method errors). In the future -// I was thinking we'd add: -// - [ARRANGE] easy to read board state input. For instance, maybe it can be -// initialized with a GnuGo-style ASCII string instead of an Array of 1s 2s -// and 0s. -// - [ASSERT] public state tracking: `is_pen_enabled`, `current_message`, -// `current_title` etc. A way for testers to peer into the internals +import { GobanConfig } from "../GobanBase"; +import { GobanEngine } from "engine/GobanEngine"; +import { MessageID } from "engine/messages"; +import { MoveTreePenMarks } from "engine/MoveTree"; +import { Goban, GobanSelectedThemes } from "./Goban"; -import { GobanConfig, GobanCore, GobanSelectedThemes } from "./GobanCore"; -import { GoEngine } from "./GoEngine"; -import { MessageID } from "./messages"; -import { MoveTreePenMarks } from "./MoveTree"; - -export class TestGoban extends GobanCore { - public engine: GoEngine; +/** + * This is a minimal implementation of Goban, primarily used for unit tests. + */ +export class TestGoban extends Goban { + public engine: GobanEngine; constructor(config: GobanConfig) { super(config); - this.engine = new GoEngine(config); + this.engine = new GobanEngine(config); } public enablePen(): void {} @@ -48,7 +43,7 @@ export class TestGoban extends GobanCore { timeout?: number | undefined, ): void {} public clearMessage(): void {} - protected setThemes(themes: GobanSelectedThemes, dont_redraw: boolean): void {} + protected setTheme(themes: GobanSelectedThemes, dont_redraw: boolean): void {} public drawSquare(i: number, j: number): void {} public redraw(force_clear?: boolean | undefined): void {} public move_tree_redraw(no_warp?: boolean | undefined): void {} diff --git a/src/Goban/callbacks.ts b/src/Goban/callbacks.ts new file mode 100644 index 00000000..d4cf714a --- /dev/null +++ b/src/Goban/callbacks.ts @@ -0,0 +1,84 @@ +/* + * Copyright (C) Online-Go.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { GobanBase } from "../GobanBase"; +import { GobanSelectedThemes } from "./Goban"; + +export interface GobanCallbacks { + defaultConfig?: () => any; + getCoordinateDisplaySystem?: () => "A1" | "1-1"; + isAnalysisDisabled?: (goban: GobanBase, perGameSettingAppliesToNonPlayers: boolean) => boolean; + + getClockDrift?: () => number; + getNetworkLatency?: () => number; + getLocation?: () => string; + getShowMoveNumbers?: () => boolean; + getShowVariationMoveNumbers?: () => boolean; + getMoveTreeNumbering?: () => "move-coordinates" | "none" | "move-number"; + getCDNReleaseBase?: () => string; + getSoundEnabled?: () => boolean; + getSoundVolume?: () => number; + + watchSelectedThemes?: (cb: (themes: GobanSelectedThemes) => void) => { remove: () => any }; + getSelectedThemes?: () => GobanSelectedThemes; + + customBlackStoneColor?: () => string; + customBlackTextColor?: () => string; + customWhiteStoneColor?: () => string; + customWhiteTextColor?: () => string; + customBoardColor?: () => string; + customBoardLineColor?: () => string; + customBoardUrl?: () => string; + customBlackStoneUrl?: () => string; + customWhiteStoneUrl?: () => string; + + canvasAllocationErrorHandler?: ( + note: string | null, + error: Error, + extra: { + total_allocations_made: number; + total_pixels_allocated: number; + width?: number | string; + height?: number | string; + }, + ) => void; + + addCoordinatesToChatInput?: (coordinates: string) => void; + updateScoreEstimation?: ( + est_winning_color: "black" | "white", + number_of_points: number, + ) => void; + + toast?: (message_id: string, duration: number) => void; +} + +export const callbacks: GobanCallbacks = { + getClockDrift: () => 0, +}; + +/** + * Set's callback functions to be called in various situations. You can set any + * or all of the callbacks, only the provided callbacks will be updated. + */ +export function setGobanCallbacks(newCallbacks: GobanCallbacks): void { + for (const key in newCallbacks) { + if (newCallbacks[key as keyof GobanCallbacks] !== undefined) { + callbacks[key as keyof GobanCallbacks] = newCallbacks[ + key as keyof GobanCallbacks + ] as any; + } + } +} diff --git a/src/canvas_utils.ts b/src/Goban/canvas_utils.ts similarity index 97% rename from src/canvas_utils.ts rename to src/Goban/canvas_utils.ts index bf8bf912..0eb95985 100644 --- a/src/canvas_utils.ts +++ b/src/Goban/canvas_utils.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { GobanCore } from "./GobanCore"; +import { callbacks } from "./callbacks"; let __deviceCanvasScalingRatio = 0; let canvases_allocated = 0; @@ -95,8 +95,8 @@ export function validateCanvas( } if (err) { - if (GobanCore.hooks.canvasAllocationErrorHandler) { - GobanCore.hooks.canvasAllocationErrorHandler(err_string, err, { + if (callbacks.canvasAllocationErrorHandler) { + callbacks.canvasAllocationErrorHandler(err_string, err, { total_pixels_allocated, total_allocations_made: canvases_allocated, width, diff --git a/src/Goban/focus_tracker.ts b/src/Goban/focus_tracker.ts new file mode 100644 index 00000000..8d12df56 --- /dev/null +++ b/src/Goban/focus_tracker.ts @@ -0,0 +1,71 @@ +/* + * Copyright (C) Online-Go.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * This provides window focus tracking functionality to aid + * in anti-cheat measures. + */ + +class FocusTracker { + hasFocus: boolean = true; + lastFocus: number = Date.now(); + outOfFocusDurations: Array = []; + + constructor() { + try { + window.addEventListener("blur", this.onBlur); + window.addEventListener("focus", this.onFocus); + } catch (e) { + console.error(e); + } + } + + reset(): void { + this.lastFocus = Date.now(); + this.outOfFocusDurations = []; + } + + getMaxBlurDurationSinceLastReset(): number { + if (!this.hasFocus) { + this.outOfFocusDurations.push(Date.now() - this.lastFocus); + } + + if (this.outOfFocusDurations.length === 0) { + return 0; + } + + const ret = Math.max.apply(Math.max, this.outOfFocusDurations); + + if (!this.hasFocus) { + this.outOfFocusDurations.pop(); + } + + return ret; + } + + onFocus = () => { + this.hasFocus = true; + this.outOfFocusDurations.push(Date.now() - this.lastFocus); + this.lastFocus = Date.now(); + }; + + onBlur = () => { + this.hasFocus = false; + this.lastFocus = Date.now(); + }; +} + +export const focus_tracker = new FocusTracker(); diff --git a/src/GoTheme.ts b/src/Goban/themes/GobanTheme.ts similarity index 93% rename from src/GoTheme.ts rename to src/Goban/themes/GobanTheme.ts index 689fecd4..f129d244 100644 --- a/src/GoTheme.ts +++ b/src/Goban/themes/GobanTheme.ts @@ -14,15 +14,15 @@ * limitations under the License. */ -import { GobanCore } from "./GobanCore"; +import { GobanBase } from "../../GobanBase"; -export interface GoThemeBackgroundCSS { +export interface GobanThemeBackgroundCSS { "background-color"?: string; "background-image"?: string; "background-size"?: string; } -export interface GoThemeBackgroundReactStyles { +export interface GobanThemeBackgroundReactStyles { backgroundColor?: string; backgroundImage?: string; backgroundSize?: string; @@ -54,12 +54,12 @@ export interface SVGStoneParameters { url?: string; } -export class GoTheme { +export class GobanTheme { public name: string; public styles: { [style_name: string]: string } = {}; - protected parent?: GoTheme; // An optional parent theme + protected parent?: GobanTheme; // An optional parent theme - constructor(parent?: GoTheme) { + constructor(parent?: GobanTheme) { this.name = `[ERROR theme missing name]`; this.parent = parent; } @@ -210,15 +210,16 @@ export class GoTheme { return invisible_circle_to_cast_shadow; } - public placeWhiteStoneSVG( + private placeStoneSVG( cell: SVGGraphicsElement, shadow_cell: SVGGraphicsElement | undefined, stone: string, cx: number, cy: number, radius: number, + shadow_circle_color: string, ): [SVGElement, SVGElement | undefined] { - const shadow = this.placeStoneShadowSVG(shadow_cell, cx, cy, radius, "#eeeeee"); + const shadow = this.placeStoneShadowSVG(shadow_cell, cx, cy, radius, shadow_circle_color); const ref = document.createElementNS("http://www.w3.org/2000/svg", "use"); ref.setAttribute("href", `#${stone}`); @@ -229,7 +230,7 @@ export class GoTheme { return [ref, shadow]; } - public placeBlackStoneSVG( + public placeWhiteStoneSVG( cell: SVGGraphicsElement, shadow_cell: SVGGraphicsElement | undefined, stone: string, @@ -237,21 +238,24 @@ export class GoTheme { cy: number, radius: number, ): [SVGElement, SVGElement | undefined] { - const shadow = this.placeStoneShadowSVG(shadow_cell, cx, cy, radius, "#222222"); - - const ref = document.createElementNS("http://www.w3.org/2000/svg", "use"); - ref.setAttribute("href", `#${stone}`); - ref.setAttribute("x", `${cx - radius}`); - ref.setAttribute("y", `${cy - radius}`); - cell.appendChild(ref); + return this.placeStoneSVG(cell, shadow_cell, stone, cx, cy, radius, "#eeeeee"); + } - return [ref, shadow]; + public placeBlackStoneSVG( + cell: SVGGraphicsElement, + shadow_cell: SVGGraphicsElement | undefined, + stone: string, + cx: number, + cy: number, + radius: number, + ): [SVGElement, SVGElement | undefined] { + return this.placeStoneSVG(cell, shadow_cell, stone, cx, cy, radius, "#222222"); } /* Resolve which stone graphic we should use. By default we just pick a * random one, if there are multiple images, otherwise whatever was * returned by the pre-render method */ - public getStone(x: number, y: number, stones: any, _goban: GobanCore): any { + public getStone(x: number, y: number, stones: any, _goban: GobanBase): any { const ret = Array.isArray(stones) ? stones[((x + 1) * 53 * ((y + 1) * 97)) % stones.length] : stones; @@ -267,7 +271,7 @@ export class GoTheme { /* Resolve which stone graphic we should use. By default we just pick a * random one, if there are multiple images, otherwise whatever was * returned by the pre-render method */ - public getStoneHash(x: number, y: number, stones: any, _goban: GobanCore): string { + public getStoneHash(x: number, y: number, stones: any, _goban: GobanBase): string { if (Array.isArray(stones)) { return "" + (((x + 1) * 53 * ((y + 1) * 97)) % stones.length); } @@ -301,7 +305,7 @@ export class GoTheme { } /* Returns a set of CSS styles that should be applied to the background layer (ie the board) */ - public getBackgroundCSS(): GoThemeBackgroundCSS { + public getBackgroundCSS(): GobanThemeBackgroundCSS { return { "background-color": "#DCB35C", "background-image": "", @@ -309,9 +313,9 @@ export class GoTheme { } /* Returns a set of CSS styles (for react) that should be applied to the background layer (ie the board) */ - public getReactStyles(): GoThemeBackgroundReactStyles { - const ret: GoThemeBackgroundReactStyles = {}; - const css: GoThemeBackgroundCSS = this.getBackgroundCSS(); + public getReactStyles(): GobanThemeBackgroundReactStyles { + const ret: GobanThemeBackgroundReactStyles = {}; + const css: GobanThemeBackgroundCSS = this.getBackgroundCSS(); ret.backgroundColor = css["background-color"]; ret.backgroundImage = css["background-image"]; diff --git a/src/themes/board_plain.ts b/src/Goban/themes/board_plain.ts similarity index 51% rename from src/themes/board_plain.ts rename to src/Goban/themes/board_plain.ts index 5d5a0629..942f4e8e 100644 --- a/src/themes/board_plain.ts +++ b/src/Goban/themes/board_plain.ts @@ -14,10 +14,10 @@ * limitations under the License. */ -import { GoTheme, GoThemeBackgroundCSS } from "../GoTheme"; -import { GoThemesInterface } from "../GoThemes"; -import { GobanCore } from "../GobanCore"; -import { _ } from "../translate"; +import { GobanTheme, GobanThemeBackgroundCSS } from "./GobanTheme"; +import { ThemesInterface } from "./"; +import { callbacks } from "../callbacks"; +import { _ } from "engine/translate"; // Converts a six-digit hex string to rgba() notation function hexToRgba(raw: string, alpha: number = 1): string { @@ -31,250 +31,238 @@ function hexToRgba(raw: string, alpha: number = 1): string { return `rgba(${r}, ${g}, ${b}, ${alpha})`; } -export default function (GoThemes: GoThemesInterface) { - class Plain extends GoTheme { - sort(): number { +export default function (THEMES: ThemesInterface) { + class Plain extends GobanTheme { + override sort(): number { return 1; } - get theme_name(): string { + override get theme_name(): string { return "Plain"; } - getBackgroundCSS(): GoThemeBackgroundCSS { + override getBackgroundCSS(): GobanThemeBackgroundCSS { return { "background-color": "#DCB35C", "background-image": "", }; } - getLineColor(): string { + override getLineColor(): string { return "#000000"; } - getFadedLineColor(): string { + override getFadedLineColor(): string { return hexToRgba("#000000", 0.5); } - getStarColor(): string { + override getStarColor(): string { return "#000000"; } - getFadedStarColor(): string { + override getFadedStarColor(): string { return hexToRgba("#000000", 0.5); } - getBlankTextColor(): string { + override getBlankTextColor(): string { return "#000000"; } - getLabelTextColor(): string { + override getLabelTextColor(): string { return hexToRgba("#000000", 0.75); } } _("Plain"); // ensure translation exists - GoThemes["board"]["Plain"] = Plain; + THEMES["board"]["Plain"] = Plain; - class Custom extends GoTheme { - sort(): number { + class Custom extends GobanTheme { + override sort(): number { return 200; //last, because this is the "customisable" one } - get theme_name(): string { + override get theme_name(): string { return "Custom"; } - getBackgroundCSS(): GoThemeBackgroundCSS { + override getBackgroundCSS(): GobanThemeBackgroundCSS { return { - "background-color": GobanCore.hooks.customBoardColor - ? GobanCore.hooks.customBoardColor() + "background-color": callbacks.customBoardColor + ? callbacks.customBoardColor() : "#DCB35C", "background-image": - GobanCore.hooks.customBoardUrl && GobanCore.hooks.customBoardUrl() !== "" - ? "url('" + GobanCore.hooks.customBoardUrl() + "')" + callbacks.customBoardUrl && callbacks.customBoardUrl() !== "" + ? "url('" + callbacks.customBoardUrl() + "')" : "", "background-size": "cover", }; } - getLineColor(): string { - return GobanCore.hooks.customBoardLineColor - ? GobanCore.hooks.customBoardLineColor() - : "#000000"; + override getLineColor(): string { + return callbacks.customBoardLineColor ? callbacks.customBoardLineColor() : "#000000"; } - getFadedLineColor(): string { + override getFadedLineColor(): string { return hexToRgba( - GobanCore.hooks.customBoardLineColor - ? GobanCore.hooks.customBoardLineColor() - : "#000000", + callbacks.customBoardLineColor ? callbacks.customBoardLineColor() : "#000000", 0.5, ); } - getStarColor(): string { - return GobanCore.hooks.customBoardLineColor - ? GobanCore.hooks.customBoardLineColor() - : "#000000"; + override getStarColor(): string { + return callbacks.customBoardLineColor ? callbacks.customBoardLineColor() : "#000000"; } - getFadedStarColor(): string { + override getFadedStarColor(): string { return hexToRgba( - GobanCore.hooks.customBoardLineColor - ? GobanCore.hooks.customBoardLineColor() - : "#000000", + callbacks.customBoardLineColor ? callbacks.customBoardLineColor() : "#000000", 0.5, ); } - getBlankTextColor(): string { - return GobanCore.hooks.customBoardLineColor - ? GobanCore.hooks.customBoardLineColor() - : "#000000"; + override getBlankTextColor(): string { + return callbacks.customBoardLineColor ? callbacks.customBoardLineColor() : "#000000"; } - getLabelTextColor(): string { + override getLabelTextColor(): string { return hexToRgba( - GobanCore.hooks.customBoardLineColor - ? GobanCore.hooks.customBoardLineColor() - : "#000000", + callbacks.customBoardLineColor ? callbacks.customBoardLineColor() : "#000000", 0.75, ); } } _("Custom"); // ensure translation exists - GoThemes["board"]["Custom"] = Custom; + THEMES["board"]["Custom"] = Custom; - class Night extends GoTheme { - sort(): number { + class Night extends GobanTheme { + override sort(): number { return 100; } - get theme_name(): string { + override get theme_name(): string { return "Night Play"; } - getBackgroundCSS(): GoThemeBackgroundCSS { + override getBackgroundCSS(): GobanThemeBackgroundCSS { return { "background-color": "#444444", "background-image": "", }; } - getLineColor(): string { + override getLineColor(): string { return "#555555"; } - getFadedLineColor(): string { + override getFadedLineColor(): string { return "#333333"; } - getStarColor(): string { + override getStarColor(): string { return "#555555"; } - getFadedStarColor(): string { + override getFadedStarColor(): string { return "#333333"; } - getBlankTextColor(): string { + override getBlankTextColor(): string { return "#ffffff"; } - getLabelTextColor(): string { + override getLabelTextColor(): string { return "#555555"; } } _("Night Play"); // ensure translation exists - GoThemes["board"]["Night Play"] = Night; + THEMES["board"]["Night Play"] = Night; - class HNG extends GoTheme { + class HNG extends GobanTheme { static C = "#00193E"; static C2 = "#004C75"; - sort(): number { + override sort(): number { return 105; } - get theme_name(): string { + override get theme_name(): string { return "HNG"; } - getBackgroundCSS(): GoThemeBackgroundCSS { + override getBackgroundCSS(): GobanThemeBackgroundCSS { return { "background-color": "#00e7fc", "background-image": "", }; } - getLineColor(): string { + override getLineColor(): string { return HNG.C; } - getFadedLineColor(): string { + override getFadedLineColor(): string { return "#00AFBF"; } - getStarColor(): string { + override getStarColor(): string { return HNG.C; } - getFadedStarColor(): string { + override getFadedStarColor(): string { return "#00AFBF"; } - getBlankTextColor(): string { + override getBlankTextColor(): string { return "#000000"; } - getLabelTextColor(): string { + override getLabelTextColor(): string { return HNG.C2; } } _("HNG"); // ensure translation exists - GoThemes["board"]["HNG"] = HNG; + THEMES["board"]["HNG"] = HNG; - class HNGNight extends GoTheme { + class HNGNight extends GobanTheme { static C = "#007591"; - sort(): number { + override sort(): number { return 105; } - get theme_name(): string { + override get theme_name(): string { return "HNG Night"; } - getBackgroundCSS(): GoThemeBackgroundCSS { + override getBackgroundCSS(): GobanThemeBackgroundCSS { return { "background-color": "#090C1F", "background-image": "", }; } - getLineColor(): string { + override getLineColor(): string { return HNGNight.C; } - getFadedLineColor(): string { + override getFadedLineColor(): string { return "#4481B5"; } - getStarColor(): string { + override getStarColor(): string { return HNGNight.C; } - getFadedStarColor(): string { + override getFadedStarColor(): string { return "#4481B5"; } - getBlankTextColor(): string { + override getBlankTextColor(): string { return "#ffffff"; } - getLabelTextColor(): string { + override getLabelTextColor(): string { return "#4481B5"; } } _("HNG Night"); // ensure translation exists - GoThemes["board"]["HNG Night"] = HNGNight; + THEMES["board"]["HNG Night"] = HNGNight; - class Book extends GoTheme { - sort(): number { + class Book extends GobanTheme { + override sort(): number { return 110; } - get theme_name(): string { + override get theme_name(): string { return "Book"; } - getBackgroundCSS(): GoThemeBackgroundCSS { + override getBackgroundCSS(): GobanThemeBackgroundCSS { return { "background-color": "#ffffff", "background-image": "", }; } - getLineColor(): string { + override getLineColor(): string { return "#555555"; } - getFadedLineColor(): string { + override getFadedLineColor(): string { return "#999999"; } - getStarColor(): string { + override getStarColor(): string { return "#555555"; } - getFadedStarColor(): string { + override getFadedStarColor(): string { return "#999999"; } - getBlankTextColor(): string { + override getBlankTextColor(): string { return "#000000"; } - getLabelTextColor(): string { + override getLabelTextColor(): string { return "#555555"; } } _("Book"); // ensure translation exists - GoThemes["board"]["Book"] = Book; + THEMES["board"]["Book"] = Book; } diff --git a/src/themes/board_woods.ts b/src/Goban/themes/board_woods.ts similarity index 54% rename from src/themes/board_woods.ts rename to src/Goban/themes/board_woods.ts index bcaeddbe..a8d4426d 100644 --- a/src/themes/board_woods.ts +++ b/src/Goban/themes/board_woods.ts @@ -14,269 +14,269 @@ * limitations under the License. */ -import { GoTheme, GoThemeBackgroundCSS } from "../GoTheme"; -import { GoThemesInterface } from "../GoThemes"; -import { _ } from "../translate"; -import { GobanCore } from "../GobanCore"; +import { GobanTheme, GobanThemeBackgroundCSS } from "./GobanTheme"; +import { ThemesInterface } from "./"; +import { _ } from "engine/translate"; +import { callbacks } from "../callbacks"; function getCDNReleaseBase() { - if (GobanCore.hooks.getCDNReleaseBase) { - return GobanCore.hooks.getCDNReleaseBase(); + if (callbacks.getCDNReleaseBase) { + return callbacks.getCDNReleaseBase(); } return ""; } -export default function (GoThemes: GoThemesInterface) { - class Kaya extends GoTheme { - sort(): number { +export default function (THEMES: ThemesInterface) { + class Kaya extends GobanTheme { + override sort(): number { return 10; } - get theme_name(): string { + override get theme_name(): string { return "Kaya"; } - getBackgroundCSS(): GoThemeBackgroundCSS { + override getBackgroundCSS(): GobanThemeBackgroundCSS { return { "background-color": "#DCB35C", "background-image": "url('" + getCDNReleaseBase() + "/img/kaya.jpg')", }; } - getLineColor(): string { + override getLineColor(): string { return "#000000"; } - getFadedLineColor(): string { + override getFadedLineColor(): string { return "#888888"; } - getStarColor(): string { + override getStarColor(): string { return "#000000"; } - getFadedStarColor(): string { + override getFadedStarColor(): string { return "#888888"; } - getBlankTextColor(): string { + override getBlankTextColor(): string { return "#000000"; } - getLabelTextColor(): string { + override getLabelTextColor(): string { return "#444444"; } } _("Kaya"); // ensure translation - GoThemes["board"]["Kaya"] = Kaya; + THEMES["board"]["Kaya"] = Kaya; - class RedOak extends GoTheme { - sort(): number { + class RedOak extends GobanTheme { + override sort(): number { return 20; } - get theme_name(): string { + override get theme_name(): string { return "Red Oak"; } - getBackgroundCSS(): GoThemeBackgroundCSS { + override getBackgroundCSS(): GobanThemeBackgroundCSS { return { "background-color": "#DCB35C", "background-image": "url('" + getCDNReleaseBase() + "/img/oak.jpg')", }; } - getLineColor(): string { + override getLineColor(): string { return "#000000"; } - getFadedLineColor(): string { + override getFadedLineColor(): string { return "#888888"; } - getStarColor(): string { + override getStarColor(): string { return "#000000"; } - getFadedStarColor(): string { + override getFadedStarColor(): string { return "#888888"; } - getBlankTextColor(): string { + override getBlankTextColor(): string { return "#000000"; } - getLabelTextColor(): string { + override getLabelTextColor(): string { return "#000000"; } } _("Red Oak"); // ensure translation - GoThemes["board"]["Red Oak"] = RedOak; + THEMES["board"]["Red Oak"] = RedOak; - class Persimmon extends GoTheme { - sort(): number { + class Persimmon extends GobanTheme { + override sort(): number { return 30; } - get theme_name(): string { + override get theme_name(): string { return "Persimmon"; } - getBackgroundCSS(): GoThemeBackgroundCSS { + override getBackgroundCSS(): GobanThemeBackgroundCSS { return { "background-color": "#DCB35C", "background-image": "url('" + getCDNReleaseBase() + "/img/persimmon.jpg')", }; } - getLineColor(): string { + override getLineColor(): string { return "#000000"; } - getFadedLineColor(): string { + override getFadedLineColor(): string { return "#888888"; } - getStarColor(): string { + override getStarColor(): string { return "#000000"; } - getFadedStarColor(): string { + override getFadedStarColor(): string { return "#888888"; } - getBlankTextColor(): string { + override getBlankTextColor(): string { return "#000000"; } - getLabelTextColor(): string { + override getLabelTextColor(): string { return "#000000"; } } _("Persimmon"); // ensure translation - GoThemes["board"]["Persimmon"] = Persimmon; + THEMES["board"]["Persimmon"] = Persimmon; - class BlackWalnut extends GoTheme { - sort(): number { + class BlackWalnut extends GobanTheme { + override sort(): number { return 40; } - get theme_name(): string { + override get theme_name(): string { return "Black Walnut"; } - getBackgroundCSS(): GoThemeBackgroundCSS { + override getBackgroundCSS(): GobanThemeBackgroundCSS { return { "background-color": "#DCB35C", "background-image": "url('" + getCDNReleaseBase() + "/img/black_walnut.jpg')", }; } - getLineColor(): string { + override getLineColor(): string { return "#000000"; } - getFadedLineColor(): string { + override getFadedLineColor(): string { return "#4A2F24"; } - getStarColor(): string { + override getStarColor(): string { return "#000000"; } - getFadedStarColor(): string { + override getFadedStarColor(): string { return "#4A2F24"; } - getBlankTextColor(): string { + override getBlankTextColor(): string { return "#000000"; } - getLabelTextColor(): string { + override getLabelTextColor(): string { return "#000000"; } } _("Black Walnut"); // ensure translation - GoThemes["board"]["Black Walnut"] = BlackWalnut; + THEMES["board"]["Black Walnut"] = BlackWalnut; - class Granite extends GoTheme { - sort(): number { + class Granite extends GobanTheme { + override sort(): number { return 40; } - get theme_name(): string { + override get theme_name(): string { return "Granite"; } - getBackgroundCSS(): GoThemeBackgroundCSS { + override getBackgroundCSS(): GobanThemeBackgroundCSS { return { "background-color": "#DCB35C", "background-image": "url('" + getCDNReleaseBase() + "/img/granite.jpg')", }; } - getLineColor(): string { + override getLineColor(): string { return "#cccccc"; } - getFadedLineColor(): string { + override getFadedLineColor(): string { return "#888888"; } - getStarColor(): string { + override getStarColor(): string { return "#cccccc"; } - getFadedStarColor(): string { + override getFadedStarColor(): string { return "#888888"; } - getBlankTextColor(): string { + override getBlankTextColor(): string { return "#ffffff"; } - getLabelTextColor(): string { + override getLabelTextColor(): string { return "#cccccc"; } } _("Granite"); // ensure translation - GoThemes["board"]["Granite"] = Granite; + THEMES["board"]["Granite"] = Granite; - class Anime extends GoTheme { - sort(): number { + class Anime extends GobanTheme { + override sort(): number { return 10; } - get theme_name(): string { + override get theme_name(): string { return "Anime"; } - getBackgroundCSS(): GoThemeBackgroundCSS { + override getBackgroundCSS(): GobanThemeBackgroundCSS { return { "background-color": "#DCB35C", "background-image": "url('" + getCDNReleaseBase() + "/img/anime_board.svg')", "background-size": "cover", }; } - getLineColor(): string { + override getLineColor(): string { return "#000000"; } - getFadedLineColor(): string { + override getFadedLineColor(): string { return "#888888"; } - getStarColor(): string { + override getStarColor(): string { return "#000000"; } - getFadedStarColor(): string { + override getFadedStarColor(): string { return "#888888"; } - getBlankTextColor(): string { + override getBlankTextColor(): string { return "#000000"; } - getLabelTextColor(): string { + override getLabelTextColor(): string { return "#444444"; } } _("Anime"); // ensure translation - GoThemes["board"]["Anime"] = Anime; + THEMES["board"]["Anime"] = Anime; - class BrightKaya extends GoTheme { - sort(): number { + class BrightKaya extends GobanTheme { + override sort(): number { return 15; } - get theme_name(): string { + override get theme_name(): string { return "Bright Kaya"; } - getBackgroundCSS(): GoThemeBackgroundCSS { + override getBackgroundCSS(): GobanThemeBackgroundCSS { return { "background-color": "#DBB25B", "background-image": "url('" + getCDNReleaseBase() + "/img/kaya.jpg')", }; } - getLineColor(): string { + override getLineColor(): string { return "#FFFFFF"; } - getFadedLineColor(): string { + override getFadedLineColor(): string { return "#FFFFFF"; } - getStarColor(): string { + override getStarColor(): string { return "#FFFFFF"; } - getFadedStarColor(): string { + override getFadedStarColor(): string { return "#999999"; } - getBlankTextColor(): string { + override getBlankTextColor(): string { return "#FFFFFF"; } - getLabelTextColor(): string { + override getLabelTextColor(): string { return "#FFFFFF"; } } _("Bright Kaya"); // ensure translation - GoThemes["board"]["Bright Kaya"] = BrightKaya; + THEMES["board"]["Bright Kaya"] = BrightKaya; } diff --git a/src/themes/image_stones.ts b/src/Goban/themes/image_stones.ts similarity index 78% rename from src/themes/image_stones.ts rename to src/Goban/themes/image_stones.ts index 37215d3e..b6c1d980 100644 --- a/src/themes/image_stones.ts +++ b/src/Goban/themes/image_stones.ts @@ -14,20 +14,20 @@ * limitations under the License. */ -import { GobanCore } from "../GobanCore"; -import { GoTheme } from "../GoTheme"; -import { GoThemesInterface } from "../GoThemes"; -import { _ } from "../translate"; +import { GobanTheme } from "./GobanTheme"; +import { ThemesInterface } from "./"; +import { _ } from "engine/translate"; import { deviceCanvasScalingRatio, allocateCanvasOrError } from "../canvas_utils"; import { renderShadow } from "./rendered_stones"; import { renderPlainStone } from "./plain_stones"; +import { callbacks } from "../callbacks"; -const anime_black_imagedata = makeSvgImageData(require("../../assets/img/anime_black.svg")); -const anime_white_imagedata = makeSvgImageData(require("../../assets/img/anime_white.svg")); +const anime_black_imagedata = makeSvgImageData(require("../../../assets/img/anime_black.svg")); +const anime_white_imagedata = makeSvgImageData(require("../../../assets/img/anime_white.svg")); function getCDNReleaseBase() { - if (GobanCore.hooks.getCDNReleaseBase) { - return GobanCore.hooks.getCDNReleaseBase(); + if (callbacks.getCDNReleaseBase) { + return callbacks.getCDNReleaseBase(); } return ""; } @@ -152,7 +152,7 @@ function stoneCastsShadow(radius: number): boolean { return radius >= 10; } -export default function (GoThemes: GoThemesInterface) { +export default function (THEMES: ThemesInterface) { /* Firefox doesn't support drawing inlined SVGs into canvases. One can * attach them to the dom just fine, but not draw them into a canvas for * whatever reason. So, for firefox we have to load the exact same SVG off @@ -167,11 +167,11 @@ export default function (GoThemes: GoThemesInterface) { // ignore } - class Common extends GoTheme { - stoneCastsShadow(radius: number): boolean { + class Common extends GobanTheme { + override stoneCastsShadow(radius: number): boolean { return stoneCastsShadow(radius * deviceCanvasScalingRatio()); } - placeBlackStone( + override placeBlackStone( ctx: CanvasRenderingContext2D, shadow_ctx: CanvasRenderingContext2D | null, stone: StoneType, @@ -181,7 +181,7 @@ export default function (GoThemes: GoThemesInterface) { ): void { placeRenderedImageStone(ctx, shadow_ctx, stone, cx, cy, radius); } - placeWhiteStone( + override placeWhiteStone( ctx: CanvasRenderingContext2D, shadow_ctx: CanvasRenderingContext2D | null, stone: StoneType, @@ -194,14 +194,14 @@ export default function (GoThemes: GoThemesInterface) { } class Anime extends Common { - sort() { + override sort() { return 30; } - get theme_name(): string { + override get theme_name(): string { return "Anime"; } - preRenderBlack( + override preRenderBlack( radius: number, _seed: number, deferredRenderCallback: () => void, @@ -214,7 +214,7 @@ export default function (GoThemes: GoThemesInterface) { //return preRenderImageStone(radius, anime_black_imagedata); } - preRenderWhite( + override preRenderWhite( radius: number, _seed: number, deferredRenderCallback: () => void, @@ -227,15 +227,15 @@ export default function (GoThemes: GoThemesInterface) { //return preRenderImageStone(radius, anime_white_imagedata); } - getBlackTextColor(_color: string): string { + override getBlackTextColor(_color: string): string { return "#ffffff"; } - getWhiteTextColor(_color: string): string { + override getWhiteTextColor(_color: string): string { return "#000000"; } - public placeStoneShadowSVG( + public override placeStoneShadowSVG( shadow_cell: SVGGraphicsElement | undefined, cx: number, cy: number, @@ -261,7 +261,7 @@ export default function (GoThemes: GoThemesInterface) { return shadow; } - public preRenderBlackSVG( + public override preRenderBlackSVG( defs: SVGDefsElement, radius: number, _seed: number, @@ -280,7 +280,7 @@ export default function (GoThemes: GoThemesInterface) { return [`anime-black-${radius}`]; } - public preRenderWhiteSVG( + public override preRenderWhiteSVG( defs: SVGDefsElement, radius: number, _seed: number, @@ -300,19 +300,19 @@ export default function (GoThemes: GoThemesInterface) { } } - GoThemes["black"]["Anime"] = Anime; - GoThemes["white"]["Anime"] = Anime; + THEMES["black"]["Anime"] = Anime; + THEMES["white"]["Anime"] = Anime; class Custom extends Common { - sort() { + override sort() { return 200; // last - in the "url customizable" slot. } - get theme_name(): string { + override get theme_name(): string { return "Custom"; } - placeBlackStone( + override placeBlackStone( ctx: CanvasRenderingContext2D, shadow_ctx: CanvasRenderingContext2D | null, stone: StoneType, @@ -320,10 +320,7 @@ export default function (GoThemes: GoThemesInterface) { cy: number, radius: number, ): void { - if ( - GobanCore.hooks.customBlackStoneUrl && - GobanCore.hooks.customBlackStoneUrl() !== "" - ) { + if (callbacks.customBlackStoneUrl && callbacks.customBlackStoneUrl() !== "") { placeRenderedImageStone(ctx, shadow_ctx, stone, cx, cy, radius); } else { renderPlainStone( @@ -337,39 +334,32 @@ export default function (GoThemes: GoThemesInterface) { } } - preRenderBlack( + override preRenderBlack( radius: number, _seed: number, deferredRenderCallback: () => void, ): StoneTypeArray | boolean { - if ( - !GobanCore.hooks.customBlackStoneUrl || - GobanCore.hooks.customBlackStoneUrl() === "" - ) { + if (!callbacks.customBlackStoneUrl || callbacks.customBlackStoneUrl() === "") { return true; } return preRenderImageStone( radius, - GobanCore.hooks.customBlackStoneUrl ? GobanCore.hooks.customBlackStoneUrl() : "", + callbacks.customBlackStoneUrl ? callbacks.customBlackStoneUrl() : "", deferredRenderCallback, false /* show_shadow */, ); //return preRenderImageStone(radius, anime_black_imagedata); } - public getBlackStoneColor(): string { - return GobanCore.hooks.customBlackStoneColor - ? GobanCore.hooks.customBlackStoneColor() - : "#000000"; + public override getBlackStoneColor(): string { + return callbacks.customBlackStoneColor ? callbacks.customBlackStoneColor() : "#000000"; } - public getBlackTextColor(): string { - return GobanCore.hooks.customBlackTextColor - ? GobanCore.hooks.customBlackTextColor() - : "#FFFFFF"; + public override getBlackTextColor(): string { + return callbacks.customBlackTextColor ? callbacks.customBlackTextColor() : "#FFFFFF"; } - placeWhiteStone( + override placeWhiteStone( ctx: CanvasRenderingContext2D, shadow_ctx: CanvasRenderingContext2D | null, stone: StoneType, @@ -377,10 +367,7 @@ export default function (GoThemes: GoThemesInterface) { cy: number, radius: number, ): void { - if ( - GobanCore.hooks.customWhiteStoneUrl && - GobanCore.hooks.customWhiteStoneUrl() !== "" - ) { + if (callbacks.customWhiteStoneUrl && callbacks.customWhiteStoneUrl() !== "") { placeRenderedImageStone(ctx, shadow_ctx, stone, cx, cy, radius); } else { renderPlainStone( @@ -394,48 +381,38 @@ export default function (GoThemes: GoThemesInterface) { } } - preRenderWhite( + override preRenderWhite( radius: number, _seed: number, deferredRenderCallback: () => void, ): StoneTypeArray | boolean { - if ( - !GobanCore.hooks.customWhiteStoneUrl || - GobanCore.hooks.customWhiteStoneUrl() === "" - ) { + if (!callbacks.customWhiteStoneUrl || callbacks.customWhiteStoneUrl() === "") { return true; } return preRenderImageStone( radius, - GobanCore.hooks.customWhiteStoneUrl ? GobanCore.hooks.customWhiteStoneUrl() : "", + callbacks.customWhiteStoneUrl ? callbacks.customWhiteStoneUrl() : "", deferredRenderCallback, false /* show_shadow */, ); //return preRenderImageStone(radius, anime_white_imagedata); } - public getWhiteStoneColor(): string { - return GobanCore.hooks.customWhiteStoneColor - ? GobanCore.hooks.customWhiteStoneColor() - : "#FFFFFF"; + public override getWhiteStoneColor(): string { + return callbacks.customWhiteStoneColor ? callbacks.customWhiteStoneColor() : "#FFFFFF"; } - public getWhiteTextColor(): string { - return GobanCore.hooks.customWhiteTextColor - ? GobanCore.hooks.customWhiteTextColor() - : "#000000"; + public override getWhiteTextColor(): string { + return callbacks.customWhiteTextColor ? callbacks.customWhiteTextColor() : "#000000"; } - public preRenderBlackSVG( + public override preRenderBlackSVG( defs: SVGDefsElement, radius: number, _seed: number, deferredRenderCallback: () => void, ): string[] { - if ( - !GobanCore.hooks.customBlackStoneUrl || - GobanCore.hooks.customBlackStoneUrl() === "" - ) { + if (!callbacks.customBlackStoneUrl || callbacks.customBlackStoneUrl() === "") { return super.preRenderBlackSVG(defs, radius, _seed, deferredRenderCallback); } @@ -443,7 +420,7 @@ export default function (GoThemes: GoThemesInterface) { this.renderSVG( { id: `custom-black-${radius}`, - url: GobanCore.hooks.customBlackStoneUrl(), + url: callbacks.customBlackStoneUrl(), }, radius, ), @@ -452,16 +429,13 @@ export default function (GoThemes: GoThemesInterface) { return [`custom-black-${radius}`]; } - public preRenderWhiteSVG( + public override preRenderWhiteSVG( defs: SVGDefsElement, radius: number, _seed: number, deferredRenderCallback: () => void, ): string[] { - if ( - !GobanCore.hooks.customWhiteStoneUrl || - GobanCore.hooks.customWhiteStoneUrl() === "" - ) { + if (!callbacks.customWhiteStoneUrl || callbacks.customWhiteStoneUrl() === "") { return super.preRenderWhiteSVG(defs, radius, _seed, deferredRenderCallback); } @@ -469,7 +443,7 @@ export default function (GoThemes: GoThemesInterface) { this.renderSVG( { id: `custom-white-${radius}`, - url: GobanCore.hooks.customWhiteStoneUrl(), + url: callbacks.customWhiteStoneUrl(), }, radius, ), @@ -479,6 +453,6 @@ export default function (GoThemes: GoThemesInterface) { } } - GoThemes["black"]["Custom"] = Custom; - GoThemes["white"]["Custom"] = Custom; + THEMES["black"]["Custom"] = Custom; + THEMES["white"]["Custom"] = Custom; } diff --git a/src/Goban/themes/index.ts b/src/Goban/themes/index.ts new file mode 100644 index 00000000..93c80787 --- /dev/null +++ b/src/Goban/themes/index.ts @@ -0,0 +1,67 @@ +/* + * Copyright (C) Online-Go.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { GobanTheme } from "./GobanTheme"; + +import { GobanTheme } from "./GobanTheme"; + +export interface ThemesInterface { + white: { [name: string]: typeof GobanTheme }; + black: { [name: string]: typeof GobanTheme }; + board: { [name: string]: typeof GobanTheme }; + + // Exists so we can do for (const theme of THEMES) { ...THEMES[theme]... } + [key: string]: { [name: string]: typeof GobanTheme }; +} + +export const THEMES: ThemesInterface = { + white: {}, + black: {}, + board: {}, +}; +export const THEMES_SORTED: { + white: GobanTheme[]; + black: GobanTheme[]; + board: GobanTheme[]; + + // Exists so we can do for (const theme of THEMES_SORTED) { ...THEMES_SORTED[theme]... } + [key: string]: GobanTheme[]; +} = { white: [], black: [], board: [] }; + +import init_board_plain from "./board_plain"; +import init_board_woods from "./board_woods"; +import init_plain_stones from "./plain_stones"; +import init_rendered from "./rendered_stones"; +import init_image_stones from "./image_stones"; + +init_board_plain(THEMES); +init_board_woods(THEMES); +init_plain_stones(THEMES); +init_rendered(THEMES); +init_image_stones(THEMES); + +function theme_sort(a: GobanTheme, b: GobanTheme) { + return a.sort() - b.sort(); +} + +for (const k in THEMES) { + THEMES_SORTED[k as keyof ThemesInterface] = Object.keys(THEMES[k as keyof ThemesInterface]).map( + (n) => { + return new THEMES[k as keyof ThemesInterface][n](); + }, + ); + THEMES_SORTED[k as keyof ThemesInterface].sort(theme_sort); +} diff --git a/src/themes/plain_stones.ts b/src/Goban/themes/plain_stones.ts similarity index 85% rename from src/themes/plain_stones.ts rename to src/Goban/themes/plain_stones.ts index 6ccf4fee..ec276050 100644 --- a/src/themes/plain_stones.ts +++ b/src/Goban/themes/plain_stones.ts @@ -14,9 +14,9 @@ * limitations under the License. */ -import { GoTheme } from "../GoTheme"; -import { GoThemesInterface } from "../GoThemes"; -import { _ } from "../translate"; +import { GobanTheme } from "./GobanTheme"; +import { ThemesInterface } from "./"; +import { _ } from "engine/translate"; export function renderPlainStone( ctx: CanvasRenderingContext2D, @@ -50,23 +50,23 @@ export function renderPlainStone( ctx.fill(); } -export default function (GoThemes: GoThemesInterface) { - class Stone extends GoTheme { - sort(): number { +export default function (THEMES: ThemesInterface) { + class Stone extends GobanTheme { + override sort(): number { return 1; } } class Plain extends Stone { - get theme_name(): string { + override get theme_name(): string { return "Plain"; } - preRenderBlack(radius: number, seed: number): boolean { + override preRenderBlack(radius: number, seed: number): boolean { return true; } - placeBlackStone( + override placeBlackStone( ctx: CanvasRenderingContext2D, shadow_ctx: CanvasRenderingContext2D, stone: any, @@ -84,19 +84,19 @@ export default function (GoThemes: GoThemesInterface) { ); } - public getBlackStoneColor(): string { + public override getBlackStoneColor(): string { return "#000000"; } - public getBlackTextColor(): string { + public override getBlackTextColor(): string { return "#FFFFFF"; } - preRenderWhite(radius: number, seed: number): any { + override preRenderWhite(radius: number, seed: number): any { return true; } - placeWhiteStone( + override placeWhiteStone( ctx: CanvasRenderingContext2D, shadow_ctx: CanvasRenderingContext2D, stone: any, @@ -114,15 +114,15 @@ export default function (GoThemes: GoThemesInterface) { ); } - public getWhiteStoneColor(): string { + public override getWhiteStoneColor(): string { return "#FFFFFF"; } - public getWhiteTextColor(): string { + public override getWhiteTextColor(): string { return "#000000"; } - public preRenderBlackSVG( + public override preRenderBlackSVG( defs: SVGDefsElement, radius: number, _seed: number, @@ -167,7 +167,7 @@ export default function (GoThemes: GoThemesInterface) { return ret; } - public preRenderWhiteSVG( + public override preRenderWhiteSVG( defs: SVGDefsElement, radius: number, _seed: number, @@ -207,6 +207,6 @@ export default function (GoThemes: GoThemesInterface) { } } - GoThemes["black"]["Plain"] = Plain; - GoThemes["white"]["Plain"] = Plain; + THEMES["black"]["Plain"] = Plain; + THEMES["white"]["Plain"] = Plain; } diff --git a/src/themes/rendered_stones.ts b/src/Goban/themes/rendered_stones.ts similarity index 93% rename from src/themes/rendered_stones.ts rename to src/Goban/themes/rendered_stones.ts index 43cf3ed8..a9d4291a 100644 --- a/src/themes/rendered_stones.ts +++ b/src/Goban/themes/rendered_stones.ts @@ -14,9 +14,9 @@ * limitations under the License. */ -import { GoTheme } from "../GoTheme"; -import { GoThemesInterface } from "../GoThemes"; -import { _ } from "../translate"; +import { GobanTheme } from "./GobanTheme"; +import { ThemesInterface } from "./"; +import { _ } from "engine/translate"; import { deviceCanvasScalingRatio, allocateCanvasOrError } from "../canvas_utils"; type StoneType = { stone: HTMLCanvasElement; shadow: HTMLCanvasElement }; @@ -477,12 +477,12 @@ function stoneCastsShadow(radius: number): boolean { return radius >= 10; } -export default function (GoThemes: GoThemesInterface) { - class Common extends GoTheme { - stoneCastsShadow(radius: number): boolean { +export default function (THEMES: ThemesInterface) { + class Common extends GobanTheme { + override stoneCastsShadow(radius: number): boolean { return stoneCastsShadow(radius * deviceCanvasScalingRatio()); } - placeBlackStone( + override placeBlackStone( ctx: CanvasRenderingContext2D, shadow_ctx: CanvasRenderingContext2D | null, stone: StoneType, @@ -492,7 +492,7 @@ export default function (GoThemes: GoThemesInterface) { ): void { placeRenderedStone(ctx, shadow_ctx, stone, cx, cy, radius); } - placeWhiteStone( + override placeWhiteStone( ctx: CanvasRenderingContext2D, shadow_ctx: CanvasRenderingContext2D | null, stone: StoneType, @@ -506,14 +506,14 @@ export default function (GoThemes: GoThemesInterface) { /* Slate & Shell { */ class Slate extends Common { - sort() { + override sort() { return 30; } - get theme_name(): string { + override get theme_name(): string { return "Slate"; } - preRenderBlack(radius: number, seed: number): StoneTypeArray { + override preRenderBlack(radius: number, seed: number): StoneTypeArray { return preRenderStone(radius, seed, { base_color: "rgba(30,30,35,1.0)", light: normalized([-4, -4, 5]), @@ -523,10 +523,10 @@ export default function (GoThemes: GoThemesInterface) { specular_light_distance: 8, }); } - getBlackTextColor(color: string): string { + override getBlackTextColor(color: string): string { return "#ffffff"; } - public preRenderBlackSVG( + public override preRenderBlackSVG( defs: SVGDefsElement, radius: number, seed: number, @@ -562,17 +562,17 @@ export default function (GoThemes: GoThemesInterface) { } _("Slate"); // ensure translation - GoThemes["black"]["Slate"] = Slate; + THEMES["black"]["Slate"] = Slate; class Shell extends Common { - sort() { + override sort() { return 30; } - get theme_name(): string { + override get theme_name(): string { return "Shell"; } - preRenderWhite(radius: number, seed: number): StoneTypeArray { + override preRenderWhite(radius: number, seed: number): StoneTypeArray { let ret: StoneTypeArray = []; for (let i = 0; i < 10; ++i) { ret = ret.concat( @@ -590,7 +590,7 @@ export default function (GoThemes: GoThemesInterface) { return ret; } - public preRenderWhiteSVG( + public override preRenderWhiteSVG( defs: SVGDefsElement, radius: number, seed: number, @@ -707,24 +707,24 @@ export default function (GoThemes: GoThemesInterface) { return ret; } - getWhiteTextColor(color: string): string { + override getWhiteTextColor(color: string): string { return "#000000"; } } _("Shell"); // ensure translation - GoThemes["white"]["Shell"] = Shell; + THEMES["white"]["Shell"] = Shell; /* Glass { */ class Glass extends Common { - sort() { + override sort() { return 20; } - get theme_name(): string { + override get theme_name(): string { return "Glass"; } - preRenderBlack(radius: number, seed: number): StoneTypeArray { + override preRenderBlack(radius: number, seed: number): StoneTypeArray { return preRenderStone(radius, seed, { base_color: "rgba(15,15,20,1.0)", light: normalized([-4, -4, 2]), @@ -734,11 +734,11 @@ export default function (GoThemes: GoThemesInterface) { specular_light_distance: 10, }); } - getBlackTextColor(color: string): string { + override getBlackTextColor(color: string): string { return "#ffffff"; } - preRenderWhite(radius: number, seed: number): StoneTypeArray { + override preRenderWhite(radius: number, seed: number): StoneTypeArray { return preRenderStone(radius, (seed *= 13), { base_color: "rgba(207,205,206,1.0)", light: normalized([-4, -4, 2]), @@ -749,11 +749,11 @@ export default function (GoThemes: GoThemesInterface) { }); } - getWhiteTextColor(color: string): string { + override getWhiteTextColor(color: string): string { return "#000000"; } - public preRenderBlackSVG( + public override preRenderBlackSVG( defs: SVGDefsElement, radius: number, _seed: number, @@ -789,7 +789,7 @@ export default function (GoThemes: GoThemesInterface) { return [key]; } - public preRenderWhiteSVG( + public override preRenderWhiteSVG( defs: SVGDefsElement, radius: number, _seed: number, @@ -830,20 +830,20 @@ export default function (GoThemes: GoThemesInterface) { } _("Glass"); // ensure translation - GoThemes["black"]["Glass"] = Glass; - GoThemes["white"]["Glass"] = Glass; + THEMES["black"]["Glass"] = Glass; + THEMES["white"]["Glass"] = Glass; /* Worn Glass { */ class WornGlass extends Common { - sort() { + override sort() { return 21; } - get theme_name(): string { + override get theme_name(): string { return "Worn Glass"; } - preRenderBlack(radius: number, seed: number): StoneTypeArray { + override preRenderBlack(radius: number, seed: number): StoneTypeArray { return preRenderStone(radius, seed, { base_color: "rgba(15,15,20,1.0)", light: normalized([-4, -4, 2]), @@ -853,11 +853,11 @@ export default function (GoThemes: GoThemesInterface) { specular_light_distance: 10, }); } - getBlackTextColor(color: string): string { + override getBlackTextColor(color: string): string { return "#ffffff"; } - preRenderWhite(radius: number, seed: number): StoneTypeArray { + override preRenderWhite(radius: number, seed: number): StoneTypeArray { return preRenderStone(radius, (seed *= 13), { base_color: "rgba(189,189,194,1.0)", light: normalized([-4, -4, 2]), @@ -868,11 +868,11 @@ export default function (GoThemes: GoThemesInterface) { }); } - getWhiteTextColor(color: string): string { + override getWhiteTextColor(color: string): string { return "#000000"; } - public preRenderBlackSVG( + public override preRenderBlackSVG( defs: SVGDefsElement, radius: number, _seed: number, @@ -908,7 +908,7 @@ export default function (GoThemes: GoThemesInterface) { return [key]; } - public preRenderWhiteSVG( + public override preRenderWhiteSVG( defs: SVGDefsElement, radius: number, _seed: number, @@ -949,19 +949,19 @@ export default function (GoThemes: GoThemesInterface) { } _("Worn Glass"); // ensure translation - GoThemes["black"]["Worn Glass"] = WornGlass; - GoThemes["white"]["Worn Glass"] = WornGlass; + THEMES["black"]["Worn Glass"] = WornGlass; + THEMES["white"]["Worn Glass"] = WornGlass; /* Night { */ class Night extends Common { - sort() { + override sort() { return 100; } - get theme_name(): string { + override get theme_name(): string { return "Night"; } - preRenderBlack(radius: number, seed: number): StoneTypeArray { + override preRenderBlack(radius: number, seed: number): StoneTypeArray { return preRenderStone(radius, seed, { base_color: "rgba(15,15,20,1.0)", light: normalized([-4, -4, 2]), @@ -971,11 +971,11 @@ export default function (GoThemes: GoThemesInterface) { specular_light_distance: 10, }); } - getBlackTextColor(color: string): string { + override getBlackTextColor(color: string): string { return "#888888"; } - preRenderWhite(radius: number, seed: number): StoneTypeArray { + override preRenderWhite(radius: number, seed: number): StoneTypeArray { return preRenderStone(radius, (seed *= 13), { base_color: "rgba(100,100,100,1.0)", light: normalized([-4, -4, 2]), @@ -986,10 +986,10 @@ export default function (GoThemes: GoThemesInterface) { }); } - getWhiteTextColor(color: string): string { + override getWhiteTextColor(color: string): string { return "#000000"; } - public preRenderBlackSVG( + public override preRenderBlackSVG( defs: SVGDefsElement, radius: number, _seed: number, @@ -1025,7 +1025,7 @@ export default function (GoThemes: GoThemesInterface) { return [key]; } - public preRenderWhiteSVG( + public override preRenderWhiteSVG( defs: SVGDefsElement, radius: number, _seed: number, @@ -1068,6 +1068,6 @@ export default function (GoThemes: GoThemesInterface) { } _("Night"); // ensure translation - GoThemes["black"]["Night"] = Night; - GoThemes["white"]["Night"] = Night; + THEMES["black"]["Night"] = Night; + THEMES["white"]["Night"] = Night; } diff --git a/src/GobanBase.ts b/src/GobanBase.ts new file mode 100644 index 00000000..404dafee --- /dev/null +++ b/src/GobanBase.ts @@ -0,0 +1,366 @@ +/* + * Copyright (C) Online-Go.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + GobanEngine, + GobanEngineConfig, + GobanEnginePhase, + GobanEngineRules, + PlayerColor, + PuzzleConfig, + PuzzlePlacementSetting, +} from "engine"; +import { MoveTree, MoveTreePenMarks } from "engine/MoveTree"; +import { ScoreEstimator } from "engine/ScoreEstimator"; +import { setGobanTranslations } from "engine/translate"; +import { + JGOFClock, + JGOFIntersection, + JGOFTimeControl, + JGOFPlayerClock, + JGOFTimeControlSystem, + JGOFPlayerSummary, + JGOFSealingIntersection, + JGOFNumericPlayerColor, + JGOFMove, +} from "engine/formats/JGOF"; +import { AdHocPackedMove, AdHocPauseControl } from "engine/formats/AdHocFormat"; +import { MessageID } from "engine/messages"; +import type { GobanSocket } from "engine/GobanSocket"; +import type { ServerToClient, GameChatLine } from "engine/protocol"; +import { EventEmitter } from "eventemitter3"; +import { setGobanCallbacks } from "./Goban/callbacks"; + +let last_goban_id = 0; + +export type GobanModes = "play" | "puzzle" | "score estimation" | "analyze" | "conditional"; + +export type AnalysisTool = "stone" | "draw" | "label" | "score" | "removal"; +export type AnalysisSubTool = + | "black" + | "white" + | "alternate" + | "letters" + | "numbers" + | string /* label character(s) */; + +export interface GobanBounds { + top: number; + left: number; + right: number; + bottom: number; +} + +export type GobanChatLog = Array; + +export interface GobanConfig extends GobanEngineConfig, PuzzleConfig { + display_width?: number; + + interactive?: boolean; + mode?: GobanModes; + square_size?: number | ((goban: GobanBase) => number) | "auto"; + + getPuzzlePlacementSetting?: () => PuzzlePlacementSetting; + + chat_log?: GobanChatLog; + spectator_log?: GobanChatLog; + malkovich_log?: GobanChatLog; + + // pause control + pause_control?: AdHocPauseControl; + paused_since?: number; + + // settings + draw_top_labels?: boolean; + draw_left_labels?: boolean; + draw_bottom_labels?: boolean; + draw_right_labels?: boolean; + bounds?: GobanBounds; + dont_draw_last_move?: boolean; + dont_show_messages?: boolean; + last_move_radius?: number; + circle_radius?: number; + one_click_submit?: boolean; + double_click_submit?: boolean; + variation_stone_opacity?: number; + visual_undo_request_indicator?: boolean; + + // + auth?: string; + time_control?: JGOFTimeControl; + marks?: { [mark: string]: string }; + + // + isPlayerOwner?: () => boolean; + isPlayerController?: () => boolean; + isInPushedAnalysis?: () => boolean; + leavePushedAnalysis?: () => void; + onError?: (err: Error) => void; + onScoreEstimationUpdated?: (winning_color: "black" | "white", points: number) => void; + + // + game_type?: "temporary"; + + // puzzle stuff + /* + puzzle_autoplace_delay?: number; + puzzle_opponent_move_mode?: PuzzleOpponentMoveMode; + puzzle_player_move_mode?: PuzzlePlayerMoveMode; + puzzle_rank = puzzle && puzzle.puzzle_rank ? puzzle.puzzle_rank : 0; + puzzle_collection = (puzzle && puzzle.collection ? puzzle.collection.id : 0); + puzzle_type = (puzzle && puzzle.type ? puzzle.type : ""); + */ + + // deprecated + username?: string; + server_socket?: GobanSocket; + connect_to_chat?: number | boolean; +} + +export interface AudioClockEvent { + /** Number of seconds left in the current period */ + countdown_seconds: number; + + /** Full player clock information */ + clock: JGOFPlayerClock; + + /** The player (id) whose turn it is */ + player_id: string; + + /** The player whose turn it is */ + color: PlayerColor; + + /** Time control system being used by the clock */ + time_control_system: JGOFTimeControlSystem; + + /** True if we are in overtime. This is only ever set for systems that have + * a concept of overtime. + */ + in_overtime: boolean; +} + +export interface JGOFClockWithTransmitting extends JGOFClock { + black_move_transmitting: number; // estimated ms left for transmission, or 0 if complete + white_move_transmitting: number; // estimated ms left for transmission, or 0 if complete +} + +export interface StateUpdateEvents { + mode: (d: GobanModes) => void; + title: (d: string) => void; + phase: (d: GobanEnginePhase) => void; + cur_move: (d: MoveTree) => void; + cur_review_move: (d: MoveTree | undefined) => void; + last_official_move: (d: MoveTree) => void; + submit_move: (d: (() => void) | undefined) => void; + analyze_tool: (d: AnalysisTool) => void; + analyze_subtool: (d: AnalysisSubTool) => void; + score_estimate: (d: ScoreEstimator | null) => void; + strict_seki_mode: (d: boolean) => void; + rules: (d: GobanEngineRules) => void; + winner: (d: number | undefined) => void; + undo_requested: (d: number | undefined) => void; // move number of the last undo request + undo_canceled: () => void; + paused: (d: boolean) => void; + outcome: (d: string) => void; + review_owner_id: (d: number | undefined) => void; + review_controller_id: (d: number | undefined) => void; + stalling_score_estimate: ServerToClient["game/:id/stalling_score_estimate"]; +} + +export interface GobanEvents extends StateUpdateEvents { + "destroy": () => void; + "update": () => void; + "chat-reset": () => void; + "error": (d: any) => void; + "gamedata": (d: any) => void; + "chat": (d: any) => void; + "engine.updated": (engine: GobanEngine) => void; + "load": (config: GobanConfig) => void; + "show-message": (message: { + formatted: string; + message_id: string; + parameters?: { [key: string]: any }; + }) => void; + "clear-message": () => void; + "submitting-move": (tf: boolean) => void; + "chat-remove": (ids: { chat_ids: Array }) => void; + "move-made": () => void; + "player-update": (player: JGOFPlayerSummary) => void; + "played-by-click": (player: { player_id: number; x: number; y: number }) => void; + "review.sync-to-current-move": () => void; + "review.updated": () => void; + "review.load-start": () => void; + "review.load-end": () => void; + "puzzle-wrong-answer": () => void; + "puzzle-correct-answer": () => void; + "state_text": (state: { title: string; show_moves_made_count?: boolean }) => void; + "advance-to-next-board": () => void; + "auto-resign": (obj: { game_id: number; player_id: number; expiration: number }) => void; + "clear-auto-resign": (obj: { game_id: number; player_id: number }) => void; + "set-for-removal": { x: number; y: number; removed: boolean }; + "captured-stones": (obj: { removed_stones: Array }) => void; + "stone-removal.accepted": () => void; + "stone-removal.updated": () => void; + "stone-removal.needs-sealing": (positions: undefined | JGOFSealingIntersection[]) => void; + "conditional-moves.updated": () => void; + "puzzle-place": (obj: { + x: number; + y: number; + width: number; + height: number; + color: "black" | "white"; + }) => void; + "clock": (clock: JGOFClockWithTransmitting | null) => void; + "audio-game-started": (obj: { + /** Player to move */ + player_id: number; + }) => void; + "audio-game-ended": (winner: "black" | "white" | "tie") => void; + "audio-pass": () => void; + "audio-stone": (obj: { + x: number; + y: number; + width: number; + height: number; + color: "black" | "white"; + }) => void; + "audio-other-player-disconnected": (obj: { player_id: number }) => void; + "audio-other-player-reconnected": (obj: { player_id: number }) => void; + "audio-clock": (event: AudioClockEvent) => void; + "audio-disconnected": () => void; // your connection has been lost to the server + "audio-reconnected": () => void; // your connection has been reestablished + "audio-capture-stones": (obj: { + count: number /* number of stones we just captured */; + already_captured: number /* number of stones that have already been captured by this player */; + }) => void; + "audio-game-paused": () => void; + "audio-game-resumed": () => void; + "audio-enter-stone-removal": () => void; + "audio-resume-game-from-stone-removal": () => void; + "audio-undo-requested": () => void; + "audio-undo-granted": () => void; +} + +/** + * Goban serves as a base class for our renderers as well as a namespace for various + * classes, types, and enums. + * + * You can't create an instance of a Goban directly, you have to create an instance of + * one of the renderers, such as GobanSVG. + */ + +export abstract class GobanBase extends EventEmitter { + /* Static functions */ + static setTranslations = setGobanTranslations; + static setCallbacks = setGobanCallbacks; + + /** Base fields **/ + public readonly goban_id = ++last_goban_id; + + private _destroyed = false; + public get destroyed(): boolean { + return this._destroyed; + } + + /* The rest of these fields are for subclasses of Goban, namely used by the renderers */ + public abstract engine: GobanEngine; + + public abstract enablePen(): void; + public abstract disablePen(): void; + public abstract clearAnalysisDrawing(): void; + public abstract drawPenMarks(pen_marks: MoveTreePenMarks): void; + public abstract showMessage( + msg_id: MessageID, + parameters?: { [key: string]: any }, + timeout?: number, + ): void; + public abstract clearMessage(): void; + public abstract drawSquare(i: number, j: number): void; + public abstract redraw(force_clear?: boolean): void; + public abstract move_tree_redraw(no_warp?: boolean): void; + /* Because this is used on the server side too, we can't have the HTMLElement + * type here. */ + public abstract setMoveTreeContainer(container: any /* HTMLElement */): void; + + /** Called by engine when a location has been set to a color. */ + public abstract set(x: number, y: number, player: JGOFNumericPlayerColor): void; + /** Called when a location is marked or unmarked for removal */ + public abstract setForRemoval( + x: number, + y: number, + removed: boolean, + emit_stone_removal_updated: boolean, + ): void; + /** Called when Engine.setState loads a previously saved board state. */ + public abstract setState(): void; + + public abstract updateScoreEstimation(): void; + + constructor() { + super(); + } + + public destroy() { + this.emit("destroy"); + this._destroyed = true; + this.engine.removeAllListeners(); + this.removeAllListeners(); + } + + /** + * Decodes any of the various ways we express moves that we've accumulated over the years into + * a unified `JGOFMove[]`. + */ + public decodeMoves( + move_obj: + | string + | AdHocPackedMove + | AdHocPackedMove[] + | JGOFMove + | JGOFMove[] + | [object] + | undefined, + ): JGOFMove[] { + return this.engine.decodeMoves(move_obj); + } + + /* Encodes a move list like `[{x: 0, y: 0}, {x:1, y:2}]` into our move string + * format `"aabc"` */ + public encodeMoves(lst: JGOFMove[]): string { + return this.engine.encodeMoves(lst); + } + + /* Encodes a single move `{x:1, y:2}` into our move string + * format `"bc"` */ + public encodeMove(lst: JGOFMove): string { + return this.engine.encodeMove(lst); + } + + /** Encodes an x,y pair or a move object like {x: 0, y: 0} into a move string like "A1" */ + public prettyCoordinates(x: JGOFMove): string; + public prettyCoordinates(x: number, y: number): string; + public prettyCoordinates(x: number | JGOFMove, y?: number): string { + return this.engine.prettyCoordinates(x as any, y as any); + } + + /** + * Decodes a move string like `"A11"` into a move object like `{x: 0, y: 10}`. Also + * handles the special cases like `".."` and "pass" which map to `{x: -1, y: -1}`. + */ + public decodePrettyCoordinates(coordinates: string): JGOFMove { + return this.engine.decodePrettyCoordinates(coordinates); + } +} diff --git a/src/GobanCore.ts b/src/GobanCore.ts deleted file mode 100644 index 435666ef..00000000 --- a/src/GobanCore.ts +++ /dev/null @@ -1,4144 +0,0 @@ -/* - * Copyright (C) Online-Go.com - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { - AUTOSCORE_TRIALS, - AUTOSCORE_TOLERANCE, - GoEngine, - GoEngineConfig, - GoEnginePhase, - GoEngineRules, - ReviewMessage, - PlayerColor, - PuzzleConfig, - PuzzlePlacementSetting, - Score, -} from "./GoEngine"; -import { GobanMoveError } from "./GobanError"; -import { Move, NumberMatrix, Intersection, encodeMove } from "./GoMath"; -import * as GoMath from "./GoMath"; -import { GoConditionalMove, ConditionalMoveResponse } from "./GoConditionalMove"; -import { MoveTree, MarkInterface, MoveTreePenMarks } from "./MoveTree"; -import { init_score_estimator, ScoreEstimator } from "./ScoreEstimator"; -import { deepEqual, dup, computeAverageMoveTime, niceInterval } from "./GoUtil"; -import { _, interpolate } from "./translate"; -import { - JGOFClock, - JGOFIntersection, - JGOFTimeControl, - JGOFPlayerClock, - JGOFTimeControlSystem, - JGOFNumericPlayerColor, - JGOFPauseState, - JGOFPlayerSummary, -} from "./JGOF"; -import { AdHocClock, AdHocPlayerClock, AdHocPauseControl } from "./AdHocFormat"; -import { MessageID } from "./messages"; -import { GobanSocket, GobanSocketEvents } from "./GobanSocket"; -import { ServerToClient, GameChatMessage, GameChatLine, StallingScoreEstimate } from "./protocol"; -import { EventEmitter } from "eventemitter3"; - -declare let swal: any; - -export const GOBAN_FONT = "Verdana,Arial,sans-serif"; - -declare const CLIENT: boolean; -export const SCORE_ESTIMATION_TRIALS = 1000; -export const SCORE_ESTIMATION_TOLERANCE = 0.3; -export const MARK_TYPES: Array = [ - "letter", - "circle", - "square", - "triangle", - "sub_triangle", - "cross", - "black", - "white", -]; -export type LabelPosition = - | "all" - | "none" - | "top-left" - | "top-right" - | "bottom-right" - | "bottom-left"; - -let last_goban_id = 0; - -interface JGOFPlayerClockWithTimedOut extends JGOFPlayerClock { - timed_out: boolean; -} - -export type GobanModes = "play" | "puzzle" | "score estimation" | "analyze" | "conditional"; - -export type AnalysisTool = "stone" | "draw" | "label"; -export type AnalysisSubTool = - | "black" - | "white" - | "alternate" - | "letters" - | "numbers" - | string /* label character(s) */; - -export interface ColoredCircle { - move: JGOFIntersection; - color: string; - border_width?: number; - border_color?: string; -} - -export interface GobanSelectedThemes { - board: string; - white: string; - black: string; -} - -export interface GobanBounds { - top: number; - left: number; - right: number; - bottom: number; -} - -export type GobanChatLog = Array; - -export interface GobanConfig extends GoEngineConfig, PuzzleConfig { - display_width?: number; - - interactive?: boolean; - mode?: GobanModes; - square_size?: number | ((goban: GobanCore) => number) | "auto"; - - getPuzzlePlacementSetting?: () => PuzzlePlacementSetting; - - chat_log?: GobanChatLog; - spectator_log?: GobanChatLog; - malkovich_log?: GobanChatLog; - - // pause control - pause_control?: AdHocPauseControl; - paused_since?: number; - - // settings - draw_top_labels?: boolean; - draw_left_labels?: boolean; - draw_bottom_labels?: boolean; - draw_right_labels?: boolean; - bounds?: GobanBounds; - dont_draw_last_move?: boolean; - dont_show_messages?: boolean; - last_move_radius?: number; - circle_radius?: number; - one_click_submit?: boolean; - double_click_submit?: boolean; - variation_stone_opacity?: number; - visual_undo_request_indicator?: boolean; - - // - auth?: string; - time_control?: JGOFTimeControl; - marks?: { [mark: string]: string }; - - // - isPlayerOwner?: () => boolean; - isPlayerController?: () => boolean; - isInPushedAnalysis?: () => boolean; - leavePushedAnalysis?: () => void; - onError?: (err: Error) => void; - onScoreEstimationUpdated?: (winning_color: "black" | "white", points: number) => void; - - // - game_type?: "temporary"; - - // puzzle stuff - /* - puzzle_autoplace_delay?: number; - puzzle_opponent_move_mode?: PuzzleOpponentMoveMode; - puzzle_player_move_mode?: PuzzlePlayerMoveMode; - puzzle_rank = puzzle && puzzle.puzzle_rank ? puzzle.puzzle_rank : 0; - puzzle_collection = (puzzle && puzzle.collection ? puzzle.collection.id : 0); - puzzle_type = (puzzle && puzzle.type ? puzzle.type : ""); - */ - - // deprecated - username?: string; - server_socket?: GobanSocket; - connect_to_chat?: number | boolean; -} - -export interface AudioClockEvent { - /** Number of seconds left in the current period */ - countdown_seconds: number; - - /** Full player clock information */ - clock: JGOFPlayerClock; - - /** The player (id) whose turn it is */ - player_id: string; - - /** The player whose turn it is */ - color: PlayerColor; - - /** Time control system being used by the clock */ - time_control_system: JGOFTimeControlSystem; - - /** True if we are in overtime. This is only ever set for systems that have - * a concept of overtime. - */ - in_overtime: boolean; -} - -interface MoveCommand { - //game_id?: number | string; - game_id: number; - move: string; - blur?: number; - clock?: JGOFPlayerClock; -} - -export interface JGOFClockWithTransmitting extends JGOFClock { - black_move_transmitting: number; // estimated ms left for transmission, or 0 if complete - white_move_transmitting: number; // estimated ms left for transmission, or 0 if complete -} - -export interface StateUpdateEvents { - mode: (d: GobanModes) => void; - title: (d: string) => void; - phase: (d: GoEnginePhase) => void; - cur_move: (d: MoveTree) => void; - cur_review_move: (d: MoveTree | undefined) => void; - last_official_move: (d: MoveTree) => void; - submit_move: (d: (() => void) | undefined) => void; - analyze_tool: (d: AnalysisTool) => void; - analyze_subtool: (d: AnalysisSubTool) => void; - score_estimate: (d: ScoreEstimator | null) => void; - strict_seki_mode: (d: boolean) => void; - rules: (d: GoEngineRules) => void; - winner: (d: number | undefined) => void; - undo_requested: (d: number | undefined) => void; // move number of the last undo request - undo_canceled: () => void; - paused: (d: boolean) => void; - outcome: (d: string) => void; - review_owner_id: (d: number | undefined) => void; - review_controller_id: (d: number | undefined) => void; - stalling_score_estimate: ServerToClient["game/:id/stalling_score_estimate"]; -} - -export interface Events extends StateUpdateEvents { - "destroy": () => void; - "update": () => void; - "chat-reset": () => void; - "error": (d: any) => void; - "gamedata": (d: any) => void; - "chat": (d: any) => void; - "engine.updated": (engine: GoEngine) => void; - "load": (config: GobanConfig) => void; - "show-message": (message: { - formatted: string; - message_id: string; - parameters?: { [key: string]: any }; - }) => void; - "clear-message": () => void; - "submitting-move": (tf: boolean) => void; - "chat-remove": (ids: { chat_ids: Array }) => void; - "move-made": () => void; - "player-update": (player: JGOFPlayerSummary) => void; - "played-by-click": (player: { player_id: number; x: number; y: number }) => void; - "review.sync-to-current-move": () => void; - "review.updated": () => void; - "review.load-start": () => void; - "review.load-end": () => void; - "puzzle-wrong-answer": () => void; - "puzzle-correct-answer": () => void; - "state_text": (state: { title: string; show_moves_made_count?: boolean }) => void; - "advance-to-next-board": () => void; - "auto-resign": (obj: { game_id: number; player_id: number; expiration: number }) => void; - "clear-auto-resign": (obj: { game_id: number; player_id: number }) => void; - "set-for-removal": { x: number; y: number; removed: boolean }; - "captured-stones": (obj: { removed_stones: Array }) => void; - "stone-removal.accepted": () => void; - "stone-removal.updated": () => void; - "conditional-moves.updated": () => void; - "puzzle-place": (obj: { - x: number; - y: number; - width: number; - height: number; - color: "black" | "white"; - }) => void; - "clock": (clock: JGOFClockWithTransmitting | null) => void; - "audio-game-started": (obj: { - /** Player to move */ - player_id: number; - }) => void; - "audio-game-ended": (winner: "black" | "white" | "tie") => void; - "audio-pass": () => void; - "audio-stone": (obj: { - x: number; - y: number; - width: number; - height: number; - color: "black" | "white"; - }) => void; - "audio-other-player-disconnected": (obj: { player_id: number }) => void; - "audio-other-player-reconnected": (obj: { player_id: number }) => void; - "audio-clock": (event: AudioClockEvent) => void; - "audio-disconnected": () => void; // your connection has been lost to the server - "audio-reconnected": () => void; // your connection has been reestablished - "audio-capture-stones": (obj: { - count: number /* number of stones we just captured */; - already_captured: number /* number of stones that have already been captured by this player */; - }) => void; - "audio-game-paused": () => void; - "audio-game-resumed": () => void; - "audio-enter-stone-removal": () => void; - "audio-resume-game-from-stone-removal": () => void; - "audio-undo-requested": () => void; - "audio-undo-granted": () => void; -} - -export interface GobanHooks { - defaultConfig?: () => any; - getCoordinateDisplaySystem?: () => "A1" | "1-1"; - isAnalysisDisabled?: (goban: GobanCore, perGameSettingAppliesToNonPlayers: boolean) => boolean; - - getClockDrift?: () => number; - getNetworkLatency?: () => number; - getLocation?: () => string; - getShowMoveNumbers?: () => boolean; - getShowVariationMoveNumbers?: () => boolean; - getMoveTreeNumbering?: () => "move-coordinates" | "none" | "move-number"; - getCDNReleaseBase?: () => string; - getSoundEnabled?: () => boolean; - getSoundVolume?: () => number; - - watchSelectedThemes?: (cb: (themes: GobanSelectedThemes) => void) => { remove: () => any }; - getSelectedThemes?: () => GobanSelectedThemes; - - customBlackStoneColor?: () => string; - customBlackTextColor?: () => string; - customWhiteStoneColor?: () => string; - customWhiteTextColor?: () => string; - customBoardColor?: () => string; - customBoardLineColor?: () => string; - customBoardUrl?: () => string; - customBlackStoneUrl?: () => string; - customWhiteStoneUrl?: () => string; - - canvasAllocationErrorHandler?: ( - note: string | null, - error: Error, - extra: { - total_allocations_made: number; - total_pixels_allocated: number; - width?: number | string; - height?: number | string; - }, - ) => void; - - addCoordinatesToChatInput?: (coordinates: string) => void; - updateScoreEstimation?: ( - est_winning_color: "black" | "white", - number_of_points: number, - ) => void; -} - -export interface GobanMetrics { - width: number; - height: number; - mid: number; - offset: number; -} - -export abstract class GobanCore extends EventEmitter { - public conditional_starting_color: "black" | "white" | "invalid" = "invalid"; - public conditional_tree: GoConditionalMove = new GoConditionalMove(null); - public double_click_submit: boolean; - public variation_stone_opacity: number; - public draw_bottom_labels: boolean; - public draw_left_labels: boolean; - public draw_right_labels: boolean; - public draw_top_labels: boolean; - public visual_undo_request_indicator: boolean; - public abstract engine: GoEngine; - public height: number; - public last_clock?: AdHocClock; - public last_emitted_clock?: JGOFClockWithTransmitting; - public clock_should_be_paused_for_move_submission: boolean = false; - public previous_mode: string; - public one_click_submit: boolean; - public pen_marks: Array; - public readonly game_id: number; - public readonly review_id: number; - public showing_scores: boolean = false; - public stalling_score_estimate?: StallingScoreEstimate; - public width: number; - - public pause_control?: AdHocPauseControl; - public paused_since?: number; - public sent_timed_out_message: boolean = false; - public chat_log: GobanChatLog = []; - - private last_paused_state: boolean | null = null; - private last_paused_by_player_state: boolean | null = null; - - /* Properties that emit change events */ - private _mode: GobanModes = "play"; - public get mode(): GobanModes { - return this._mode; - } - public set mode(mode: GobanModes) { - if (this._mode === mode) { - return; - } - this._mode = mode; - this.emit("mode", this.mode); - } - - private _title: string = "play"; - public get title(): string { - return this._title; - } - public set title(title: string) { - if (this._title === title) { - return; - } - this._title = title; - this.emit("title", this.title); - } - - private _submit_move?: () => void; - public get submit_move(): (() => void) | undefined { - return this._submit_move; - } - public set submit_move(submit_move: (() => void) | undefined) { - if (this._submit_move === submit_move) { - return; - } - this._submit_move = submit_move; - this.emit("submit_move", this.submit_move); - } - - private _analyze_tool: AnalysisTool = "stone"; - public get analyze_tool(): AnalysisTool { - return this._analyze_tool; - } - public set analyze_tool(analyze_tool: AnalysisTool) { - if (this._analyze_tool === analyze_tool) { - return; - } - this._analyze_tool = analyze_tool; - this.emit("analyze_tool", this.analyze_tool); - } - - private _analyze_subtool: AnalysisSubTool = "alternate"; - public get analyze_subtool(): AnalysisSubTool { - return this._analyze_subtool; - } - public set analyze_subtool(analyze_subtool: AnalysisSubTool) { - if (this._analyze_subtool === analyze_subtool) { - return; - } - this._analyze_subtool = analyze_subtool; - this.emit("analyze_subtool", this.analyze_subtool); - } - - private _score_estimate: ScoreEstimator | null = null; - public get score_estimate(): ScoreEstimator | null { - return this._score_estimate; - } - public set score_estimate(score_estimate: ScoreEstimator | null) { - if (this._score_estimate === score_estimate) { - return; - } - this._score_estimate = score_estimate; - this.emit("score_estimate", this.score_estimate); - this._score_estimate?.when_ready - .then(() => { - this.emit("score_estimate", this.score_estimate); - }) - .catch(() => { - return; - }); - } - - private _review_owner_id?: number; - public get review_owner_id(): number | undefined { - return this._review_owner_id; - } - public set review_owner_id(review_owner_id: number | undefined) { - if (this._review_owner_id === review_owner_id) { - return; - } - this._review_owner_id = review_owner_id; - this.emit("review_owner_id", this.review_owner_id); - } - - private _review_controller_id?: number; - public get review_controller_id(): number | undefined { - return this._review_controller_id; - } - public set review_controller_id(review_controller_id: number | undefined) { - if (this._review_controller_id === review_controller_id) { - return; - } - this._review_controller_id = review_controller_id; - this.emit("review_controller_id", this.review_controller_id); - } - - protected __board_redraw_pen_layer_timer: any = null; - protected __clock_timer?: ReturnType; - protected __draw_state: Array>; - protected __last_pt: { i: number; j: number; valid: boolean } = { i: -1, j: -1, valid: false }; - protected __update_move_tree: any = null; /* timer */ - protected analysis_move_counter: number; - protected auto_scoring_done?: boolean = false; - protected bounded_height: number; - protected bounded_width: number; - protected bounds: GobanBounds; - protected conditional_path: string = ""; - public config: GobanConfig; - protected current_cmove?: GoConditionalMove; - protected currently_my_cmove: boolean = false; - protected destroyed: boolean; - protected dirty_redraw: any = null; // timer - protected disconnectedFromGame: boolean = true; - protected display_width?: number; - protected done_loading_review: boolean = false; - protected dont_draw_last_move: boolean; - protected last_move_radius: number; - protected circle_radius: number; - protected edit_color?: "black" | "white"; - protected errorHandler: (e: Error) => void; - protected heatmap?: NumberMatrix; - protected colored_circles?: Array>; - protected game_type: string; - protected getPuzzlePlacementSetting?: () => PuzzlePlacementSetting; - protected goban_id: number; - protected highlight_movetree_moves: boolean; - protected interactive: boolean; - protected isInPushedAnalysis: () => boolean; - protected leavePushedAnalysis: () => void; - protected isPlayerController: () => boolean; - protected isPlayerOwner: () => boolean; - protected label_character: string; - protected label_mark: string = "[UNSET]"; - protected last_hover_square?: Intersection; - protected last_move?: MoveTree; - protected last_phase?: GoEnginePhase; - protected last_review_message: ReviewMessage; - protected last_sound_played_for_a_stone_placement?: string; - protected last_stone_sound: number; - protected move_selected?: Intersection; - protected no_display: boolean; - protected onError?: (error: Error) => void; - protected on_game_screen: boolean; - protected original_square_size: number | ((goban: GobanCore) => number) | "auto"; - protected player_id: number; - protected puzzle_autoplace_delay: number; - protected restrict_moves_to_movetree: boolean; - protected review_had_gamedata: boolean; - protected scoring_mode: boolean | "stalling-scoring-mode"; - protected shift_key_is_down: boolean; - protected show_move_numbers: boolean; - protected show_variation_move_numbers: boolean; - protected square_size: number = 10; - protected stone_placement_enabled: boolean; - protected sendLatencyTimer?: ReturnType; - - protected socket!: GobanSocket; - protected socket_event_bindings: Array<[keyof GobanSocketEvents, () => void]> = []; - protected connectToReviewSent?: boolean; - - /** GobanCore calls some abstract methods as part of the construction - * process. Because our subclasses might (and do) need to do some of their - * own config before these are called, we set this function to be called - * by our subclass after it's done it's own internal config stuff. - */ - protected post_config_constructor: () => GoEngine; - - public abstract enablePen(): void; - public abstract disablePen(): void; - public abstract clearAnalysisDrawing(): void; - public abstract drawPenMarks(pen_marks: MoveTreePenMarks): void; - public abstract showMessage( - msg_id: MessageID, - parameters?: { [key: string]: any }, - timeout?: number, - ): void; - public abstract clearMessage(): void; - protected abstract setThemes(themes: GobanSelectedThemes, dont_redraw: boolean): void; - public abstract drawSquare(i: number, j: number): void; - public abstract redraw(force_clear?: boolean): void; - public abstract move_tree_redraw(no_warp?: boolean): void; - /* Because this is used on the server side too, we can't have the HTMLElement - * type here. */ - public abstract setMoveTreeContainer(container: any /* HTMLElement */): void; - protected abstract setTitle(title: string): void; - protected abstract enableDrawing(): void; - protected abstract disableDrawing(): void; - - public static hooks: GobanHooks = { - getClockDrift: () => 0, - }; - - constructor(config: GobanConfig, preloaded_data?: GobanConfig) { - super(); - - this.on("clock", (clock) => { - if (clock) { - this.last_emitted_clock = clock; - } - }); - - this.goban_id = ++last_goban_id; - - /* Apply defaults */ - const C: any = {}; - const default_config = this.defaultConfig(); - for (const k in default_config) { - C[k] = (default_config as any)[k]; - } - for (const k in config) { - C[k] = (config as any)[k]; - } - config = C; - - /* Apply config */ - //window['active_gobans'][this.goban_id] = this; - this.destroyed = false; - this.on_game_screen = this.getLocation().indexOf("/game/") >= 0; - this.no_display = false; - - this.width = config.width || 19; - this.height = config.height || 19; - this.bounds = config.bounds || { - top: 0, - left: 0, - bottom: this.height - 1, - right: this.width - 1, - }; - this.bounded_width = this.bounds ? this.bounds.right - this.bounds.left + 1 : this.width; - this.bounded_height = this.bounds ? this.bounds.bottom - this.bounds.top + 1 : this.height; - //this.black_name = config["black_name"]; - //this.white_name = config["white_name"]; - //this.move_number = config["move_number"]; - this.setGameClock(null); - this.last_stone_sound = -1; - this.scoring_mode = false; - - this.game_type = config.game_type || ""; - this.one_click_submit = "one_click_submit" in config ? !!config.one_click_submit : false; - this.double_click_submit = - "double_click_submit" in config ? !!config.double_click_submit : true; - this.variation_stone_opacity = - typeof config.variation_stone_opacity !== "undefined" - ? config.variation_stone_opacity - : 0.6; - this.visual_undo_request_indicator = - "visual_undo_request_indicator" in config - ? !!config.visual_undo_request_indicator - : false; - this.original_square_size = config.square_size || "auto"; - //this.square_size = config["square_size"] || "auto"; - this.interactive = !!config.interactive; - this.pen_marks = []; - - this.config = repair_config(config); - this.__draw_state = GoMath.makeStringMatrix(this.width, this.height); - this.game_id = - (typeof config.game_id === "string" ? parseInt(config.game_id) : config.game_id) || 0; - this.player_id = config.player_id || 0; - this.review_id = config.review_id || 0; - this.last_review_message = {}; - this.review_had_gamedata = false; - this.puzzle_autoplace_delay = config.puzzle_autoplace_delay || 300; - this.isPlayerOwner = config.isPlayerOwner || (() => false); /* for reviews */ - this.isPlayerController = config.isPlayerController || (() => false); /* for reviews */ - this.isInPushedAnalysis = config.isInPushedAnalysis - ? config.isInPushedAnalysis - : () => false; - this.leavePushedAnalysis = config.leavePushedAnalysis - ? config.leavePushedAnalysis - : () => { - return; - }; - //this.onPendingResignation = config.onPendingResignation; - //this.onPendingResignationCleared = config.onPendingResignationCleared; - if ("onError" in config) { - this.onError = config.onError; - } - this.dont_draw_last_move = !!config.dont_draw_last_move; - this.last_move_radius = config.last_move_radius || 0.25; - this.circle_radius = config.circle_radius || 0.25; - this.getPuzzlePlacementSetting = config.getPuzzlePlacementSetting; - this.mode = config.mode || "play"; - this.previous_mode = this.mode; - this.label_character = "A"; - //this.edit_color = null; - this.stone_placement_enabled = false; - this.highlight_movetree_moves = false; - this.restrict_moves_to_movetree = false; - this.analysis_move_counter = 0; - //this.wait_for_game_to_start = config.wait_for_game_to_start; - this.errorHandler = (e) => { - if (e instanceof GobanMoveError) { - if (e.message_id === "stone_already_placed_here") { - return; - } - } - /* - if (e.message === _("A stone has already been placed here") || e.message === "A stone has already been placed here") { - return; - } - */ - if (e instanceof GobanMoveError && e.message_id === "move_is_suicidal") { - this.showMessage("self_capture_not_allowed", { error: e }, 5000); - return; - } else { - this.showMessage("error", { error: e }, 5000); - } - if (this.onError) { - this.onError(e); - } - }; - - this.draw_top_labels = "draw_top_labels" in config ? !!config.draw_top_labels : true; - this.draw_left_labels = "draw_left_labels" in config ? !!config.draw_left_labels : true; - this.draw_right_labels = "draw_right_labels" in config ? !!config.draw_right_labels : true; - this.draw_bottom_labels = - "draw_bottom_labels" in config ? !!config.draw_bottom_labels : true; - this.show_move_numbers = this.getShowMoveNumbers(); - this.show_variation_move_numbers = this.getShowVariationMoveNumbers(); - - if (this.bounds.left > 0) { - this.draw_left_labels = false; - } - if (this.bounds.top > 0) { - this.draw_top_labels = false; - } - if (this.bounds.right < this.width - 1) { - this.draw_right_labels = false; - } - if (this.bounds.bottom < this.height - 1) { - this.draw_bottom_labels = false; - } - - if (typeof config.square_size === "function") { - this.square_size = config.square_size(this) as number; - if (isNaN(this.square_size)) { - console.error("Invalid square size set: (NaN)"); - this.square_size = 12; - } - } else if (typeof config.square_size === "number") { - this.square_size = config.square_size; - } - if (config.display_width && this.original_square_size === "auto") { - this.setSquareSizeBasedOnDisplayWidth(config.display_width, /* suppress_redraw */ true); - } - - this.__update_move_tree = null; - this.shift_key_is_down = false; - - this.post_config_constructor = (): GoEngine => { - let ret: GoEngine; - - delete this.current_cmove; /* set in setConditionalTree */ - this.currently_my_cmove = false; - this.setConditionalTree(undefined); - - delete this.last_hover_square; - this.__last_pt = this.xy2ij(-1, -1); - - if (preloaded_data) { - ret = this.load(preloaded_data); - } else { - ret = this.load(config); - } - if ("server_socket" in config && config["server_socket"]) { - if (!preloaded_data) { - this.showMessage("loading", undefined, -1); - } - this.connect(config["server_socket"]); - } - - return ret; - }; - } - - protected _socket_on(event: KeyT, cb: any) { - this.socket.on(event, cb); - this.socket_event_bindings.push([event, cb]); - } - - public static setHooks(hooks: GobanHooks): void { - for (const name in hooks) { - (GobanCore.hooks as any)[name] = (hooks as any)[name]; - } - } - - protected getClockDrift(): number { - if (GobanCore.hooks.getClockDrift) { - return GobanCore.hooks.getClockDrift(); - } - console.warn("getClockDrift not provided for Goban instance"); - return 0; - } - protected getNetworkLatency(): number { - if (GobanCore.hooks.getNetworkLatency) { - return GobanCore.hooks.getNetworkLatency(); - } - console.warn("getNetworkLatency not provided for Goban instance"); - return 0; - } - protected getCoordinateDisplaySystem(): "A1" | "1-1" { - if (GobanCore.hooks.getCoordinateDisplaySystem) { - return GobanCore.hooks.getCoordinateDisplaySystem(); - } - return "A1"; - } - protected getShowMoveNumbers(): boolean { - if (GobanCore.hooks.getShowMoveNumbers) { - return GobanCore.hooks.getShowMoveNumbers(); - } - return false; - } - protected getShowVariationMoveNumbers(): boolean { - if (GobanCore.hooks.getShowVariationMoveNumbers) { - return GobanCore.hooks.getShowVariationMoveNumbers(); - } - return false; - } - public static getMoveTreeNumbering(): string { - if (GobanCore.hooks.getMoveTreeNumbering) { - return GobanCore.hooks.getMoveTreeNumbering(); - } - return "move-number"; - } - public static getCDNReleaseBase(): string { - if (GobanCore.hooks.getCDNReleaseBase) { - return GobanCore.hooks.getCDNReleaseBase(); - } - return ""; - } - public static getSoundEnabled(): boolean { - if (GobanCore.hooks.getSoundEnabled) { - return GobanCore.hooks.getSoundEnabled(); - } - return true; - } - public static getSoundVolume(): number { - if (GobanCore.hooks.getSoundVolume) { - return GobanCore.hooks.getSoundVolume(); - } - return 0.5; - } - protected defaultConfig(): any { - if (GobanCore.hooks.defaultConfig) { - return GobanCore.hooks.defaultConfig(); - } - return {}; - } - public isAnalysisDisabled(perGameSettingAppliesToNonPlayers: boolean = false): boolean { - if (GobanCore.hooks.isAnalysisDisabled) { - return GobanCore.hooks.isAnalysisDisabled(this, perGameSettingAppliesToNonPlayers); - } - return false; - } - - protected getLocation(): string { - if (GobanCore.hooks.getLocation) { - return GobanCore.hooks.getLocation(); - } - return window.location.pathname; - } - protected getSelectedThemes(): GobanSelectedThemes { - if (GobanCore.hooks.getSelectedThemes) { - return GobanCore.hooks.getSelectedThemes(); - } - //return {white:'Plain', black:'Plain', board:'Plain'}; - //return {white:'Plain', black:'Plain', board:'Kaya'}; - return { white: "Shell", black: "Slate", board: "Kaya" }; - } - protected connect(server_socket: GobanSocket): void { - const socket = (this.socket = server_socket); - - this.disconnectedFromGame = false; - //this.on_disconnects = []; - - const send_connect_message = () => { - if (this.disconnectedFromGame) { - return; - } - - if (this.review_id) { - this.connectToReviewSent = true; - this.done_loading_review = false; - this.setTitle(_("Review")); - if (!this.disconnectedFromGame) { - socket.send("review/connect", { - review_id: this.review_id, - }); - } - this.emit("chat-reset"); - } else if (this.game_id) { - if (!this.disconnectedFromGame) { - socket.send("game/connect", { - game_id: this.game_id, - chat: !!this.config.connect_to_chat, - }); - } - } - - if (!this.sendLatencyTimer) { - const sendLatency = () => { - if (!this.interactive) { - return; - } - if (!this.isCurrentUserAPlayer()) { - return; - } - if (!GobanCore.hooks.getNetworkLatency) { - return; - } - const latency = GobanCore.hooks.getNetworkLatency(); - if (!latency) { - return; - } - - if (!this.game_id || this.game_id <= 0) { - return; - } - - this.socket.send("game/latency", { - game_id: this.game_id, - latency: this.getNetworkLatency(), - }); - }; - this.sendLatencyTimer = niceInterval(sendLatency, 5000); - sendLatency(); - } - }; - - if (socket.connected) { - send_connect_message(); - } - - this._socket_on("connect", send_connect_message); - this._socket_on("disconnect", (): void => { - if (this.disconnectedFromGame) { - return; - } - }); - - let reconnect = false; - - this._socket_on("connect", () => { - if (this.disconnectedFromGame) { - return; - } - if (reconnect) { - this.emit("audio-reconnected"); - } - reconnect = true; - }); - this._socket_on("disconnect", (): void => { - if (this.disconnectedFromGame) { - return; - } - this.emit("audio-disconnected"); - }); - - let prefix = null; - - if (this.game_id) { - prefix = "game/" + this.game_id + "/"; - } - if (this.review_id) { - prefix = "review/" + this.review_id + "/"; - } - - this._socket_on((prefix + "error") as keyof GobanSocketEvents, (msg: any): void => { - if (this.disconnectedFromGame) { - return; - } - this.emit("error", msg); - let duration = 500; - - if (msg === "This is a protected game" || msg === "This is a protected review") { - duration = -1; - } - - this.showMessage("error", { error: { message: _(msg) } }, duration); - console.error("ERROR: ", msg); - }); - - /*****************/ - /*** Game mode ***/ - /*****************/ - if (this.game_id) { - this._socket_on( - (prefix + "gamedata") as keyof GobanSocketEvents, - (obj: GobanConfig): void => { - if (this.disconnectedFromGame) { - return; - } - - this.clearMessage(); - //this.onClearChatLogs(); - - this.emit("chat-reset"); - focus_tracker.reset(); - - if ( - this.last_phase && - this.last_phase !== "finished" && - obj.phase === "finished" - ) { - const winner = obj.winner; - let winner_color: "black" | "white" | undefined; - if (typeof winner === "number") { - winner_color = winner === obj.black_player_id ? "black" : "white"; - } else if (winner === "black" || winner === "white") { - winner_color = winner; - } - - if (winner_color) { - this.emit("audio-game-ended", winner_color); - } - } - if (obj.phase) { - this.last_phase = obj.phase; - } else { - console.warn(`Game gamedata missing phase`); - } - this.load(obj); - this.emit("gamedata", obj); - }, - ); - this._socket_on( - (prefix + "chat") as keyof GobanSocketEvents, - (obj: GameChatMessage): void => { - if (this.disconnectedFromGame) { - return; - } - obj.line.channel = obj.channel; - this.chat_log.push(obj.line); - this.emit("chat", obj.line); - }, - ); - this._socket_on((prefix + "reset-chats") as keyof GobanSocketEvents, (): void => { - if (this.disconnectedFromGame) { - return; - } - this.emit("chat-reset"); - }); - this._socket_on( - (prefix + "chat/remove") as keyof GobanSocketEvents, - (obj: any): void => { - if (this.disconnectedFromGame) { - return; - } - this.emit("chat-remove", obj); - }, - ); - this._socket_on((prefix + "message") as keyof GobanSocketEvents, (msg: any): void => { - if (this.disconnectedFromGame) { - return; - } - this.showMessage("server_message", { message: msg }); - }); - delete this.last_phase; - - this._socket_on((prefix + "latency") as keyof GobanSocketEvents, (obj: any): void => { - if (this.disconnectedFromGame) { - return; - } - - if (this.engine) { - if (!this.engine.latencies) { - this.engine.latencies = {}; - } - this.engine.latencies[obj.player_id] = obj.latency; - } - }); - this._socket_on((prefix + "clock") as keyof GobanSocketEvents, (obj: any): void => { - if (this.disconnectedFromGame) { - return; - } - - this.clock_should_be_paused_for_move_submission = false; - this.setGameClock(obj); - - this.updateTitleAndStonePlacement(); - this.emit("update"); - }); - this._socket_on( - (prefix + "phase") as keyof GobanSocketEvents, - (new_phase: any): void => { - if (this.disconnectedFromGame) { - return; - } - - this.setMode("play"); - if (new_phase !== "finished") { - this.engine.clearRemoved(); - } - - if (this.engine.phase !== new_phase) { - if (new_phase === "stone removal") { - this.emit("audio-enter-stone-removal"); - } - if (new_phase === "play" && this.engine.phase === "stone removal") { - this.emit("audio-resume-game-from-stone-removal"); - } - } - - this.engine.phase = new_phase; - - if (this.engine.phase === "stone removal") { - this.autoScore(); - } else { - delete this.auto_scoring_done; - } - - this.updateTitleAndStonePlacement(); - this.emit("update"); - }, - ); - this._socket_on( - (prefix + "undo_requested") as keyof GobanSocketEvents, - (move_number: string): void => { - if (this.disconnectedFromGame) { - return; - } - - this.engine.undo_requested = parseInt(move_number); - this.emit("update"); - this.emit("audio-undo-requested"); - if (this.visual_undo_request_indicator) { - this.redraw(true); // need to update the mark on the last move - } - }, - ); - this._socket_on((prefix + "undo_canceled") as keyof GobanSocketEvents, (): void => { - if (this.disconnectedFromGame) { - return; - } - - this.engine.undo_requested = undefined; // can't call delete here because this is a getter/setter - this.emit("update"); - this.emit("undo_canceled"); - if (this.visual_undo_request_indicator) { - this.redraw(true); - } - }); - this._socket_on((prefix + "undo_accepted") as keyof GobanSocketEvents, (): void => { - if (this.disconnectedFromGame) { - return; - } - - if (!this.engine.undo_requested) { - console.warn("Undo accepted, but no undo requested, we might be out of sync"); - try { - swal.fire( - "Game synchronization error related to undo, please reload your game page", - ); - } catch (e) { - console.error(e); - } - return; - } - - this.engine.undo_requested = undefined; - - this.setMode("play"); - this.engine.showPrevious(); - this.engine.setLastOfficialMove(); - - this.setConditionalTree(); - - this.engine.undo_requested = undefined; - this.updateTitleAndStonePlacement(); - this.emit("update"); - this.emit("audio-undo-granted"); - }); - this._socket_on((prefix + "move") as keyof GobanSocketEvents, (move_obj: any): void => { - try { - if (this.disconnectedFromGame) { - return; - } - focus_tracker.reset(); - - if (move_obj.game_id !== this.game_id) { - console.error( - "Invalid move for this game received [" + this.game_id + "]", - move_obj, - ); - return; - } - const move = move_obj.move; - - if (this.isInPushedAnalysis()) { - this.leavePushedAnalysis(); - } - - /* clear any undo state that may be hanging around */ - this.engine.undo_requested = undefined; - - const mv = this.engine.decodeMoves(move); - - if (mv.length > 1) { - console.warn( - "More than one move provided in encoded move in a `move` event. That's odd.", - ); - } - - const the_move = mv[0]; - - if (this.mode === "conditional" || this.mode === "play") { - this.setMode("play"); - } - - let jump_to_move = null; - if ( - this.engine.cur_move.id !== this.engine.last_official_move.id && - ((this.engine.cur_move.parent == null && - this.engine.cur_move.trunk_next != null) || - this.engine.cur_move.parent?.id !== this.engine.last_official_move.id) - ) { - jump_to_move = this.engine.cur_move; - } - this.engine.jumpToLastOfficialMove(); - - if (this.engine.playerToMove() !== this.player_id) { - const t = this.conditional_tree.getChild( - GoMath.encodeMove(the_move.x, the_move.y), - ); - t.move = null; - this.setConditionalTree(t); - } - - if (this.engine.getMoveNumber() !== move_obj.move_number - 1) { - this.showMessage("synchronization_error"); - setTimeout(() => { - window.location.href = window.location.href; - }, 2500); - console.error( - "Synchronization error, we thought move should be " + - this.engine.getMoveNumber() + - " server thought it should be " + - (move_obj.move_number - 1), - ); - - return; - } - - const score_before_move = - this.engine.computeScore(true)[this.engine.colorToMove()].prisoners; - - let removed_count = 0; - const removed_stones: Array = []; - if (the_move.edited) { - this.engine.editPlace(the_move.x, the_move.y, the_move.color || 0); - } else { - removed_count = this.engine.place( - the_move.x, - the_move.y, - false, - false, - false, - true, - true, - removed_stones, - ); - } - - if (the_move.player_update && this.engine.player_pool) { - //console.log("`move` got player update:", the_move.player_update); - this.engine.cur_move.player_update = the_move.player_update; - this.engine.updatePlayers(the_move.player_update); - } - - if (the_move.played_by) { - this.engine.cur_move.played_by = the_move.played_by; - } - - this.setLastOfficialMove(); - delete this.move_selected; - - if (jump_to_move) { - this.engine.jumpTo(jump_to_move); - } - - this.emit("update"); - this.playMovementSound(); - if (removed_count) { - console.log("audio-capture-stones", { - count: removed_count, - already_captured: score_before_move, - }); - this.emit("audio-capture-stones", { - count: removed_count, - already_captured: score_before_move, - }); - this.debouncedEmitCapturedStones(removed_stones); - } - - this.emit("move-made"); - - /* - if (this.move_number) { - this.move_number.text(this.engine.getMoveNumber()); - } - */ - } catch (e) { - console.error(e); - } - }); - - this._socket_on( - (prefix + "player_update") as keyof GobanSocketEvents, - (player_update: JGOFPlayerSummary): void => { - try { - let jump_to_move = null; - if ( - this.engine.cur_move.id !== this.engine.last_official_move.id && - ((this.engine.cur_move.parent == null && - this.engine.cur_move.trunk_next != null) || - this.engine.cur_move.parent?.id !== - this.engine.last_official_move.id) - ) { - jump_to_move = this.engine.cur_move; - } - this.engine.jumpToLastOfficialMove(); - - this.engine.cur_move.player_update = player_update; - this.engine.updatePlayers(player_update); - - if (this.mode === "conditional" || this.mode === "play") { - this.setMode("play"); - } else { - console.warn("unexpected player_update received!"); - } - - if (jump_to_move) { - this.engine.jumpTo(jump_to_move); - } - } catch (e) { - console.error(e); - } - this.emit("player-update", player_update); - }, - ); - - this._socket_on( - (prefix + "conditional_moves") as keyof GobanSocketEvents, - (cmoves: { - player_id: number; - move_number: number; - moves: ConditionalMoveResponse | null; - }): void => { - if (this.disconnectedFromGame) { - return; - } - - if (cmoves.moves == null) { - this.setConditionalTree(); - } else { - this.setConditionalTree(GoConditionalMove.decode(cmoves.moves)); - } - }, - ); - this._socket_on( - (prefix + "removed_stones") as keyof GobanSocketEvents, - (cfg: any): void => { - if (this.disconnectedFromGame) { - return; - } - - if ("strict_seki_mode" in cfg) { - this.engine.strict_seki_mode = cfg.strict_seki_mode; - } else { - const removed = cfg.removed; - const stones = cfg.stones; - let moves: Array; - if (!stones) { - moves = []; - } else { - moves = this.engine.decodeMoves(stones); - } - - for (let i = 0; i < moves.length; ++i) { - this.engine.setRemoved(moves[i].x, moves[i].y, removed, false); - } - this.emit("stone-removal.updated"); - } - this.updateTitleAndStonePlacement(); - this.emit("update"); - }, - ); - this._socket_on( - (prefix + "removed_stones_accepted") as keyof GobanSocketEvents, - (cfg: any): void => { - if (this.disconnectedFromGame) { - return; - } - - const player_id = cfg.player_id; - const stones = cfg.stones; - - if (player_id === 0) { - this.engine.players["white"].accepted_stones = stones; - this.engine.players["black"].accepted_stones = stones; - } else { - const color = this.engine.playerColor(player_id); - if (color === "invalid") { - console.error( - `Invalid player_id ${player_id} in removed_stones_accepted`, - { - cfg, - player_id: this.player_id, - players: this.engine.players, - }, - ); - throw new Error( - `Invalid player_id ${player_id} in removed_stones_accepted`, - ); - } else { - this.engine.players[color].accepted_stones = stones; - this.engine.players[color].accepted_strict_seki_mode = - "strict_seki_mode" in cfg ? cfg.strict_seki_mode : false; - } - } - this.updateTitleAndStonePlacement(); - this.emit("stone-removal.accepted"); - this.emit("update"); - }, - ); - - const auto_resign_state: { [id: number]: boolean } = {}; - - this._socket_on((prefix + "auto_resign") as keyof GobanSocketEvents, (obj: any) => { - this.emit("auto-resign", { - game_id: obj.game_id, - player_id: obj.player_id, - expiration: obj.expiration, - }); - auto_resign_state[obj.player_id] = true; - this.emit("audio-other-player-disconnected", { - player_id: obj.player_id, - }); - }); - this._socket_on( - (prefix + "clear_auto_resign") as keyof GobanSocketEvents, - (obj: any) => { - this.emit("clear-auto-resign", { - game_id: obj.game_id, - player_id: obj.player_id, - }); - if (auto_resign_state[obj.player_id]) { - this.emit("audio-other-player-reconnected", { - player_id: obj.player_id, - }); - delete auto_resign_state[obj.player_id]; - } - }, - ); - this._socket_on( - (prefix + "stalling_score_estimate") as keyof GobanSocketEvents, - (obj: any): void => { - if (this.disconnectedFromGame) { - return; - } - console.log("Score estimate received: ", obj); - //obj.line.channel = obj.channel; - //this.chat_log.push(obj.line); - this.engine.stalling_score_estimate = obj; - this.engine.config.stalling_score_estimate = obj; - this.emit("stalling_score_estimate", obj); - }, - ); - } - - /*******************/ - /*** Review mode ***/ - /*******************/ - let bulk_processing = false; - const process_r = (obj: ReviewMessage) => { - if (this.disconnectedFromGame) { - return; - } - - if (obj.chat) { - obj.chat.channel = "discussion"; - if (!obj.chat.chat_id) { - obj.chat.chat_id = obj.chat.player_id + "." + obj.chat.date; - } - this.chat_log.push(obj.chat as any); - this.emit("chat", obj.chat); - } - - if (obj["remove-chat"]) { - this.emit("chat-remove", { chat_ids: [obj["remove-chat"]] }); - } - - if (obj.gamedata) { - if (obj.gamedata.phase === "stone removal") { - obj.gamedata.phase = "finished"; - } - - this.load(obj.gamedata); - this.review_had_gamedata = true; - } - - if (obj.player_update && this.engine.player_pool) { - console.log("process_r got player update:", obj.player_update); - this.engine.updatePlayers(obj.player_update); - } - - if (obj.owner) { - this.review_owner_id = typeof obj.owner === "object" ? obj.owner.id : obj.owner; - } - if (obj.controller) { - this.review_controller_id = - typeof obj.controller === "object" ? obj.controller.id : obj.controller; - } - - if ( - !this.isPlayerController() || - !this.done_loading_review || - "om" in obj /* official moves are always alone in these object broadcasts */ || - "undo" in obj /* official moves are always alone in these object broadcasts */ - ) { - const cur_move = this.engine.cur_move; - const follow = - this.engine.cur_review_move == null || - this.engine.cur_review_move.id === cur_move.id; - let do_redraw = false; - if ("f" in obj && typeof obj.m === "string") { - /* specifying node */ - const t = this.done_loading_review; - this.done_loading_review = - false; /* this prevents drawing from being drawn when we do a follow path. */ - this.engine.followPath(obj.f || 0, obj.m); - this.drawSquare(this.engine.cur_move.x, this.engine.cur_move.y); - this.done_loading_review = t; - this.engine.setAsCurrentReviewMove(); - this.scheduleRedrawPenLayer(); - } - - if ("om" in obj) { - /* Official move [comes from live review of game] */ - const t = this.engine.cur_review_move || this.engine.cur_move; - const mv = this.engine.decodeMoves([obj.om] as any)[0]; - const follow_om = t.id === this.engine.last_official_move.id; - this.engine.jumpToLastOfficialMove(); - this.engine.place(mv.x, mv.y, false, false, true, true, true); - this.engine.setLastOfficialMove(); - if ( - (t.x !== mv.x || - t.y !== mv.y) /* case when a branch has been promoted to trunk */ && - !follow_om - ) { - /* case when they were on a last official move, auto-follow to next */ - this.engine.jumpTo(t); - } - this.engine.setAsCurrentReviewMove(); - if (this.done_loading_review) { - this.move_tree_redraw(); - } - } - - if ("undo" in obj) { - /* Official undo move [comes from live review of game] */ - const t = this.engine.cur_review_move; - const cur_move_undone = - this.engine.cur_review_move?.id === this.engine.last_official_move.id; - this.engine.jumpToLastOfficialMove(); - this.engine.showPrevious(); - this.engine.setLastOfficialMove(); - if (!cur_move_undone) { - if (t) { - this.engine.jumpTo(t); - } else { - console.warn( - `No valid move to jump back to in review game relay of undo`, - ); - } - } - this.engine.setAsCurrentReviewMove(); - if (this.done_loading_review) { - this.move_tree_redraw(); - } - } - - if (this.engine.cur_review_move) { - if (typeof obj["t"] === "string") { - /* set text */ - this.engine.cur_review_move.text = obj["t"]; - } - if ("t+" in obj) { - /* append to text */ - this.engine.cur_review_move.text += obj["t+"]; - } - if (typeof obj.k !== "undefined") { - /* set marks */ - const t = this.engine.cur_move; - this.engine.cur_review_move.clearMarks(); - this.engine.cur_move = this.engine.cur_review_move; - this.setMarks(obj["k"], this.engine.cur_move.id !== t.id); - this.engine.cur_move = t; - } - if ("clearpen" in obj) { - this.engine.cur_review_move.pen_marks = []; - this.scheduleRedrawPenLayer(); - do_redraw = false; - } - if ("delete" in obj) { - const t = this.engine.cur_review_move.parent; - this.engine.cur_review_move.remove(); - this.engine.jumpTo(t); - this.engine.setAsCurrentReviewMove(); - this.scheduleRedrawPenLayer(); - if (this.done_loading_review) { - this.move_tree_redraw(); - } - } - if (typeof obj.pen !== "undefined") { - /* start pen */ - this.engine.cur_review_move.pen_marks.push({ - color: obj["pen"], - points: [], - }); - } - if (typeof obj.pp !== "undefined") { - /* update pen marks */ - try { - const pts = - this.engine.cur_review_move.pen_marks[ - this.engine.cur_review_move.pen_marks.length - 1 - ].points; - this.engine.cur_review_move.pen_marks[ - this.engine.cur_review_move.pen_marks.length - 1 - ].points = pts.concat(obj["pp"]); - this.scheduleRedrawPenLayer(); - do_redraw = false; - } catch (e) { - console.error(e); - } - } - } - - if (this.done_loading_review) { - if (!follow) { - this.engine.jumpTo(cur_move); - this.move_tree_redraw(); - } else { - if (do_redraw) { - this.redraw(true); - } - if (!this.__update_move_tree) { - this.__update_move_tree = setTimeout(() => { - this.__update_move_tree = null; - this.updateOrRedrawMoveTree(); - this.emit("update"); - }, 100); - } - } - } - } - - if ("controller" in obj) { - if (!("owner" in obj)) { - /* only false at index 0 of the replay log */ - if (this.isPlayerController()) { - this.emit("review.sync-to-current-move"); - } - this.updateTitleAndStonePlacement(); - const line = { - system: true, - chat_id: uuid(), - body: interpolate(_("Control passed to %s"), [ - typeof obj.controller === "number" - ? `%%%PLAYER-${obj.controller}%%%` - : obj.controller?.username || "[missing controller name]", - ]), - channel: "system", - }; - //this.chat_log.push(line); - this.emit("chat", line); - this.emit("update"); - } - } - if (!bulk_processing) { - this.emit("review.updated"); - } - }; - - if (this.review_id) { - this._socket_on( - `review/${this.review_id}/full_state`, - (entries: Array) => { - try { - if (!entries || entries.length === 0) { - console.error("Blank full state received, ignoring"); - return; - } - if (this.disconnectedFromGame) { - return; - } - - this.disableDrawing(); - /* TODO: Clear our state here better */ - - this.emit("review.load-start"); - bulk_processing = true; - for (let i = 0; i < entries.length; ++i) { - process_r(entries[i]); - } - bulk_processing = false; - - this.enableDrawing(); - /* - if (this.isPlayerController()) { - this.done_loading_review = true; - this.drawPenMarks(this.engine.cur_move.pen_marks); - this.redraw(true); - return; - } - */ - - this.done_loading_review = true; - this.drawPenMarks(this.engine.cur_move.pen_marks); - this.emit("review.load-end"); - this.emit("review.updated"); - this.move_tree_redraw(); - this.redraw(true); - } catch (e) { - console.error(e); - } - }, - ); - this._socket_on(`review/${this.review_id}/r`, process_r); - } - - return; - } - public destroy(): void { - this.emit("destroy"); - //delete window['active_gobans'][this.goban_id]; - this.destroyed = true; - if (this.socket) { - this.disconnect(); - } - if (this.sendLatencyTimer) { - clearInterval(this.sendLatencyTimer); - delete this.sendLatencyTimer; - } - - /* Clear various timeouts that may be running */ - this.clock_should_be_paused_for_move_submission = false; - this.setGameClock(null); - - delete (this as any).isPlayerController; - delete (this as any).isPlayerOwner; - delete (this as any).isInPushedAnalysis; - delete (this as any).leavePushedAnalysis; - delete (this as any).onError; - delete (this as any).onScoreEstimationUpdated; - delete (this as any).getPuzzlePlacementSetting; - - this.engine.removeAllListeners(); - this.removeAllListeners(); - } - protected disconnect(): void { - this.emit("destroy"); - if (!this.disconnectedFromGame) { - this.disconnectedFromGame = true; - if (this.socket && this.socket.connected) { - if (this.review_id) { - this.socket.send("review/disconnect", { review_id: this.review_id }); - } - if (this.game_id) { - this.socket.send("game/disconnect", { game_id: this.game_id }); - } - } - } - for (const pair of this.socket_event_bindings) { - this.socket.off(pair[0], pair[1]); - } - this.socket_event_bindings = []; - } - protected scheduleRedrawPenLayer(): void { - if (!this.__board_redraw_pen_layer_timer) { - this.__board_redraw_pen_layer_timer = setTimeout(() => { - if (this.engine.cur_move.pen_marks.length) { - this.drawPenMarks(this.engine.cur_move.pen_marks); - } else if (this.pen_marks.length) { - this.clearAnalysisDrawing(); - } - this.__board_redraw_pen_layer_timer = null; - }, 100); - } - } - - public sendChat(msg_body: string, type: string) { - if (typeof msg_body === "string" && msg_body.length === 0) { - return; - } - - const msg: any = { - body: msg_body, - }; - - if (this.game_id) { - msg["type"] = type; - msg["game_id"] = this.game_id; - msg["move_number"] = this.engine.getCurrentMoveNumber(); - this.socket.send("game/chat", msg); - } else { - const diff = this.engine.getMoveDiff(); - msg["review_id"] = this.review_id; - msg["from"] = diff.from; - msg["moves"] = diff.moves; - this.socket.send("review/chat", msg); - } - } - - protected getWidthForSquareSize(square_size: number): number { - return ( - (this.bounded_width + +this.draw_left_labels + +this.draw_right_labels) * square_size - ); - } - protected xy2ij(x: number, y: number): { i: number; j: number; valid: boolean } { - if (x > 0 && y > 0) { - if (this.bounds.left > 0) { - x += this.bounds.left * this.square_size; - } else { - x -= +this.draw_left_labels * this.square_size; - } - - if (this.bounds.top > 0) { - y += this.bounds.top * this.square_size; - } else { - y -= +this.draw_top_labels * this.square_size; - } - } - - const ii = x / this.square_size; - const jj = y / this.square_size; - let i = Math.floor(ii); - let j = Math.floor(jj); - const border_distance = Math.min(ii - i, jj - j, 1 - (ii - i), 1 - (jj - j)); - if (border_distance < 0.1) { - // have a "dead zone" in between squares to avoid misclicks - i = -1; - j = -1; - } - return { i: i, j: j, valid: i >= 0 && j >= 0 && i < this.width && j < this.height }; - } - public setAnalyzeTool(tool: AnalysisTool, subtool: AnalysisSubTool | undefined | null) { - this.analyze_tool = tool; - this.analyze_subtool = subtool ?? "alternate"; - - if (tool === "stone" && subtool === "black") { - this.edit_color = "black"; - } else if (tool === "stone" && subtool === "white") { - this.edit_color = "white"; - } else { - delete this.edit_color; - } - - this.setLabelCharacterFromMarks(this.analyze_subtool as "letters" | "numbers"); - - if (tool === "draw") { - this.enablePen(); - } - } - - protected putOrClearLabel(x: number, y: number, mode?: "put" | "clear"): boolean { - let ret = false; - if (mode == null || typeof mode === "undefined") { - if (this.analyze_subtool === "letters" || this.analyze_subtool === "numbers") { - this.label_mark = this.label_character; - ret = this.toggleMark(x, y, this.label_character, true); - if (ret === true) { - this.incrementLabelCharacter(); - } else { - this.setLabelCharacterFromMarks(); - } - } else { - this.label_mark = this.analyze_subtool; - ret = this.toggleMark(x, y, this.analyze_subtool); - } - } else { - if (mode === "put") { - ret = this.toggleMark(x, y, this.label_mark, this.label_mark.length <= 3, true); - } else { - const marks = this.getMarks(x, y); - - for (let i = 0; i < MARK_TYPES.length; ++i) { - delete marks[MARK_TYPES[i]]; - } - this.drawSquare(x, y); - } - } - - this.syncReviewMove(); - return ret; - } - public setSquareSize(new_ss: number, suppress_redraw = false): void { - const redraw = this.square_size !== new_ss && !suppress_redraw; - this.square_size = Math.max(new_ss, 1); - if (redraw) { - this.redraw(true); - } - } - public setSquareSizeBasedOnDisplayWidth(display_width: number, suppress_redraw = false): void { - let n_squares = Math.max( - this.bounded_width + +this.draw_left_labels + +this.draw_right_labels, - this.bounded_height + +this.draw_bottom_labels + +this.draw_top_labels, - ); - this.display_width = display_width; - - if (isNaN(this.display_width)) { - console.error("Invalid display width. (NaN)"); - this.display_width = 320; - } - - if (isNaN(n_squares)) { - console.error("Invalid n_squares: ", n_squares); - console.error("bounded_width: ", this.bounded_width); - console.error("this.draw_left_labels: ", this.draw_left_labels); - console.error("this.draw_right_labels: ", this.draw_right_labels); - console.error("bounded_height: ", this.bounded_height); - console.error("this.draw_top_labels: ", this.draw_top_labels); - console.error("this.draw_bottom_labels: ", this.draw_bottom_labels); - n_squares = 19; - } - - this.setSquareSize(Math.floor(this.display_width / n_squares), suppress_redraw); - } - - public setCoordinates(label_position: LabelPosition) { - this.draw_top_labels = label_position === "all" || label_position.indexOf("top") >= 0; - this.draw_left_labels = label_position === "all" || label_position.indexOf("left") >= 0; - this.draw_right_labels = label_position === "all" || label_position.indexOf("right") >= 0; - this.draw_bottom_labels = label_position === "all" || label_position.indexOf("bottom") >= 0; - this.setSquareSizeBasedOnDisplayWidth(Number(this.display_width)); - this.redraw(true); - } - - public setStrictSekiMode(tf: boolean): void { - if (this.engine.phase !== "stone removal") { - throw "Not in stone removal phase"; - } - if (this.engine.strict_seki_mode === tf) { - return; - } - this.engine.strict_seki_mode = tf; - - this.socket.send("game/removed_stones/set", { - game_id: this.game_id, - stones: "", - removed: false, - strict_seki_mode: tf, - }); - } - public computeMetrics(): GobanMetrics { - if (!this.square_size || this.square_size <= 0) { - this.square_size = 12; - } - - const ret = { - width: - this.square_size * - (this.bounded_width + +this.draw_left_labels + +this.draw_right_labels), - height: - this.square_size * - (this.bounded_height + +this.draw_top_labels + +this.draw_bottom_labels), - mid: this.square_size / 2, - offset: 0, - }; - - if (this.square_size % 2 === 0) { - ret.mid -= 0.5; - ret.offset = 0.5; - } - - return ret; - } - protected setSubmit(fn?: () => void): void { - this.submit_move = fn; - this.emit("submit_move", fn); - } - - public markDirty(): void { - if (!this.dirty_redraw) { - this.dirty_redraw = setTimeout(() => { - this.dirty_redraw = null; - this.redraw(); - }, 1); - } - } - - protected updateMoveTree(): void { - this.move_tree_redraw(); - } - protected updateOrRedrawMoveTree(): void { - if (this.engine.move_tree_layout_dirty) { - this.move_tree_redraw(); - } else { - this.updateMoveTree(); - } - } - - public setBounds(bounds: GobanBounds): void { - this.bounds = bounds || { top: 0, left: 0, bottom: this.height - 1, right: this.width - 1 }; - - if (this.bounds) { - this.bounded_width = this.bounds.right - this.bounds.left + 1; - this.bounded_height = this.bounds.bottom - this.bounds.top + 1; - } else { - this.bounded_width = this.width; - this.bounded_height = this.height; - } - - this.draw_left_labels = !!this.config.draw_left_labels; - this.draw_right_labels = !!this.config.draw_right_labels; - this.draw_top_labels = !!this.config.draw_top_labels; - this.draw_bottom_labels = !!this.config.draw_bottom_labels; - - if (this.bounds.left > 0) { - this.draw_left_labels = false; - } - if (this.bounds.top > 0) { - this.draw_top_labels = false; - } - if (this.bounds.right < this.width - 1) { - this.draw_right_labels = false; - } - if (this.bounds.bottom < this.height - 1) { - this.draw_bottom_labels = false; - } - } - - public load(config: GobanConfig): GoEngine { - config = repair_config(config); - for (const k in config) { - (this.config as any)[k] = (config as any)[k]; - } - this.clearMessage(); - - const new_width = config.width || 19; - const new_height = config.height || 19; - // this signalizes that we can keep the old engine - // we progressively && more and more conditions - let keep_old_engine = new_width === this.width && new_height === this.height; - this.width = new_width; - this.height = new_height; - - delete this.move_selected; - - this.bounds = config.bounds || { - top: 0, - left: 0, - bottom: this.height - 1, - right: this.width - 1, - }; - if (this.bounds) { - this.bounded_width = this.bounds.right - this.bounds.left + 1; - this.bounded_height = this.bounds.bottom - this.bounds.top + 1; - } else { - this.bounded_width = this.width; - this.bounded_height = this.height; - } - - if (config.display_width !== undefined) { - this.display_width = config.display_width; - } - if (this.display_width && this.original_square_size === "auto") { - this.setSquareSizeBasedOnDisplayWidth(this.display_width, /* suppress_redraw */ true); - } - - if ( - !this.__draw_state || - this.__draw_state.length !== this.height || - this.__draw_state[0].length !== this.width - ) { - this.__draw_state = GoMath.makeStringMatrix(this.width, this.height); - } - - this.chat_log = []; - const main_log: GobanChatLog = (config.chat_log || []).map((x) => { - x.channel = "main"; - return x; - }); - const spectator_log: GobanChatLog = (config.spectator_log || []).map((x) => { - x.channel = "spectator"; - return x; - }); - const malkovich_log: GobanChatLog = (config.malkovich_log || []).map((x) => { - x.channel = "malkovich"; - return x; - }); - this.chat_log = this.chat_log.concat(main_log, spectator_log, malkovich_log); - this.chat_log.sort((a, b) => a.date - b.date); - - for (const line of this.chat_log) { - this.emit("chat", line); - } - - // set up player_pool so we can find player details by id later - if (!config.player_pool) { - config.player_pool = {}; - } - - if (config.players) { - config.player_pool[config.players.black.id] = config.players.black; - config.player_pool[config.players.white.id] = config.players.white; - } - - if (config.rengo_teams) { - for (const player of config.rengo_teams.black.concat(config.rengo_teams.white)) { - config.player_pool[player.id] = player; - } - } - - /* This must be done last as it will invoke the appropriate .set actions to set the board in it's correct state */ - const old_engine = this.engine; - - // we need to have an engine to be able to keep it - keep_old_engine = keep_old_engine && old_engine !== null && old_engine !== undefined; - // we only keep the old engine in analyze mode & finished state - // JM: this keep_old_engine functionality is being added to fix resetting analyze state on network - // reconnect - keep_old_engine = - keep_old_engine && this.mode === "analyze" && old_engine.phase === "finished"; - - // NOTE: the construction needs to be side-effect free, because we might not use the new state - // so we create the engine twice (in case where keep_old_engine = false) - // here, it is created without the callback to `this` so that it cannot mess things up - const new_engine = new GoEngine(config); - - /* - if (old_engine) { - console.log("old size", old_engine.move_tree.size()); - console.log("new size", new_engine.move_tree.size()); - console.log( - "old contains new", - old_engine.move_tree.containsOtherTreeAsSubset(new_engine.move_tree), - ); - console.log( - "new contains old", - new_engine.move_tree.containsOtherTreeAsSubset(old_engine.move_tree), - ); - } - */ - - // more sanity checks - keep_old_engine = keep_old_engine && old_engine.phase === new_engine.phase; - // just to be on the safe side, - // we only keep the old engine, if replacing it with new would not bring no new moves - // (meaning: old has at least all the moves of new one, possibly more == such as the analysis) - keep_old_engine = - keep_old_engine && old_engine.move_tree.containsOtherTreeAsSubset(new_engine.move_tree); - - if (!keep_old_engine) { - // we create the engine anew, this time with the callback argument, - // in case the constructor some side effects on `this` - // (JM: which it currently does) - this.engine = new GoEngine(config, this); - this.emit("engine.updated", this.engine); - this.engine.parentEventEmitter = this; - this.engine.getState_callback = () => { - return this.getState(); - }; - this.engine.setState_callback = (state) => { - return this.setState(state); - }; - } - - this.paused_since = config.paused_since; - this.pause_control = config.pause_control; - - /* - if (this.move_number) { - this.move_number.text(this.engine.getMoveNumber()); - } - */ - - if (this.config.marks && this.engine) { - this.setMarks(this.config.marks); - } - this.setConditionalTree(); - - if (this.getPuzzlePlacementSetting) { - if ( - this.engine.puzzle_player_move_mode === "fixed" && - this.getPuzzlePlacementSetting().mode === "play" - ) { - this.highlight_movetree_moves = true; - this.restrict_moves_to_movetree = true; - } - if ( - this.getPuzzlePlacementSetting && - this.getPuzzlePlacementSetting().mode !== "play" - ) { - this.highlight_movetree_moves = true; - } - } - - if ( - !(old_engine && old_engine.boardMatricesAreTheSame(old_engine.board, this.engine.board)) - ) { - this.redraw(true); - } - - this.updatePlayerToMoveTitle(); - if (this.mode === "play") { - if (this.engine.playerToMove() === this.player_id) { - this.enableStonePlacement(); - } else { - this.disableStonePlacement(); - } - } else { - if (this.stone_placement_enabled) { - this.disableStonePlacement(); - this.enableStonePlacement(); - } - } - if (!keep_old_engine) { - this.setLastOfficialMove(); - } - this.emit("update"); - - if ( - this.engine.phase === "stone removal" && - !("auto_scoring_done" in this) && - !("auto_scoring_done" in (this as any).engine) - ) { - (this as any).autoScore(); - } - - this.emit("load", config); - return this.engine; - } - public set(x: number, y: number, player: JGOFNumericPlayerColor): void { - this.markDirty(); - } - public setForRemoval( - x: number, - y: number, - removed: number, - emit_stone_removal_updated: boolean = true, - ) { - if (removed) { - this.getMarks(x, y).stone_removed = true; - this.getMarks(x, y).remove = true; - } else { - this.getMarks(x, y).stone_removed = false; - this.getMarks(x, y).remove = false; - } - this.drawSquare(x, y); - this.emit("set-for-removal", { x, y, removed: !!removed }); - if (emit_stone_removal_updated) { - this.emit("stone-removal.updated"); - } - } - public showScores(score: Score, only_show_territory: boolean = false): void { - this.hideScores(); - this.showing_scores = true; - - for (let i = 0; i < 2; ++i) { - const color: "black" | "white" = i ? "black" : "white"; - const moves = this.engine.decodeMoves(score[color].scoring_positions); - for (let j = 0; j < moves.length; ++j) { - const mv = moves[j]; - if (only_show_territory && this.engine.board[mv.y][mv.x] > 0) { - continue; - } - if (mv.y < 0 || mv.x < 0) { - console.error("Negative scoring position: ", mv); - console.error( - "Scoring positions [" + color + "]: ", - score[color].scoring_positions, - ); - } else { - this.getMarks(mv.x, mv.y).score = color; - this.drawSquare(mv.x, mv.y); - } - } - } - } - public hideScores(): void { - this.showing_scores = false; - for (let j = 0; j < this.height; ++j) { - for (let i = 0; i < this.width; ++i) { - if (this.getMarks(i, j).score) { - this.getMarks(i, j).score = false; - this.drawSquare(i, j); - } - } - } - } - public showStallingScoreEstimate(sse: StallingScoreEstimate): void { - this.hideScores(); - this.showing_scores = true; - this.scoring_mode = "stalling-scoring-mode"; - this.stalling_score_estimate = sse; - this.redraw(); - } - - public updatePlayerToMoveTitle(): void { - switch (this.engine.phase) { - case "play": - if ( - this.player_id && - this.player_id === this.engine.playerToMove() && - this.engine.cur_move.id === this.engine.last_official_move.id - ) { - if ( - this.engine.cur_move.passed() && - this.engine.handicapMovesLeft() <= 0 && - this.engine.cur_move.parent - ) { - this.setTitle(_("Your move - opponent passed")); - if (this.last_move && this.last_move.x >= 0) { - this.drawSquare(this.last_move.x, this.last_move.y); - } - } else { - this.setTitle(_("Your move")); - } - if ( - this.engine.cur_move.id === this.engine.last_official_move.id && - this.mode === "play" - ) { - this.emit("state_text", { title: _("Your move") }); - } - } else { - const color = this.engine.playerColor(this.engine.playerToMove()); - - let title; - if (color === "black") { - title = _("Black to move"); - } else { - title = _("White to move"); - } - this.setTitle(title); - if ( - this.engine.cur_move.id === this.engine.last_official_move.id && - this.mode === "play" - ) { - this.emit("state_text", { title: title, show_moves_made_count: true }); - } - } - break; - - case "stone removal": - this.setTitle(_("Stone Removal")); - this.emit("state_text", { title: _("Stone Removal Phase") }); - break; - - case "finished": - this.setTitle(_("Game Finished")); - this.emit("state_text", { title: _("Game Finished") }); - break; - - default: - this.setTitle(this.engine.phase); - break; - } - } - public disableStonePlacement(): void { - this.stone_placement_enabled = false; - if (this.__last_pt && this.__last_pt.valid) { - this.drawSquare(this.__last_pt.i, this.__last_pt.j); - } - } - public enableStonePlacement(): void { - if (this.stone_placement_enabled) { - this.disableStonePlacement(); - } - - this.stone_placement_enabled = true; - if (this.__last_pt && this.__last_pt.valid) { - this.drawSquare(this.__last_pt.i, this.__last_pt.j); - } - } - public showFirst(dont_update_display?: boolean): void { - this.engine.jumpTo(this.engine.move_tree); - if (!dont_update_display) { - this.updateTitleAndStonePlacement(); - this.emit("update"); - } - } - public showPrevious(dont_update_display?: boolean): void { - if (this.mode === "conditional") { - if (this.conditional_path.length >= 2) { - const prev_path = this.conditional_path.substr(0, this.conditional_path.length - 2); - this.jumpToLastOfficialMove(); - this.followConditionalPath(prev_path); - } - } else { - if (this.move_selected) { - this.jumpToLastOfficialMove(); - return; - } - - this.engine.showPrevious(); - } - - if (!dont_update_display) { - this.updateTitleAndStonePlacement(); - this.emit("update"); - } - } - public showNext(dont_update_display?: boolean): void { - if (this.mode === "conditional") { - if (this.current_cmove) { - if (this.currently_my_cmove) { - if (this.current_cmove.move !== null) { - this.followConditionalPath(this.current_cmove.move); - } - } else { - for (const ch in this.current_cmove.children) { - this.followConditionalPath(ch); - break; - } - } - } - } else { - if (this.move_selected) { - return; - } - this.engine.showNext(); - } - - if (!dont_update_display) { - this.updateTitleAndStonePlacement(); - this.emit("update"); - } - } - public prevSibling(): void { - const sibling = this.engine.cur_move.prevSibling(); - if (sibling) { - this.engine.jumpTo(sibling); - this.emit("update"); - } - } - public nextSibling(): void { - const sibling = this.engine.cur_move.nextSibling(); - if (sibling) { - this.engine.jumpTo(sibling); - this.emit("update"); - } - } - public deleteBranch(): void { - if (!this.engine.cur_move.trunk) { - if (this.isPlayerController()) { - this.syncReviewMove({ delete: 1 }); - } - this.engine.deleteCurMove(); - this.emit("update"); - this.move_tree_redraw(); - } - } - - public jumpToLastOfficialMove(): void { - delete this.move_selected; - this.engine.jumpToLastOfficialMove(); - this.updateTitleAndStonePlacement(); - - this.conditional_path = ""; - this.currently_my_cmove = false; - if (this.mode === "conditional") { - this.current_cmove = this.conditional_tree; - } - - this.emit("update"); - } - protected setLastOfficialMove(): void { - this.engine.setLastOfficialMove(); - this.updateTitleAndStonePlacement(); - } - protected isLastOfficialMove(): boolean { - return this.engine.isLastOfficialMove(); - } - - public updateTitleAndStonePlacement(): void { - this.updatePlayerToMoveTitle(); - - if (this.engine.phase === "stone removal" || this.scoring_mode) { - this.enableStonePlacement(); - } else if (this.engine.phase === "play") { - switch (this.mode) { - case "play": - if ( - this.isLastOfficialMove() && - this.engine.playerToMove() === this.player_id - ) { - this.enableStonePlacement(); - } else { - this.disableStonePlacement(); - } - break; - - case "analyze": - case "conditional": - case "puzzle": - this.disableStonePlacement(); - this.enableStonePlacement(); - break; - } - } else if (this.engine.phase === "finished") { - this.disableStonePlacement(); - if (this.mode === "analyze") { - this.enableStonePlacement(); - } - } - } - - public setConditionalTree(conditional_tree?: GoConditionalMove): void { - if (typeof conditional_tree === "undefined") { - this.conditional_tree = new GoConditionalMove(null); - } else { - this.conditional_tree = conditional_tree; - } - this.current_cmove = this.conditional_tree; - - this.emit("conditional-moves.updated"); - this.emit("update"); - } - public followConditionalPath(move_path: string) { - const moves = this.engine.decodeMoves(move_path); - for (let i = 0; i < moves.length; ++i) { - this.engine.place(moves[i].x, moves[i].y); - this.followConditionalSegment(moves[i].x, moves[i].y); - } - this.emit("conditional-moves.updated"); - } - protected followConditionalSegment(x: number, y: number): void { - const mv = encodeMove(x, y); - this.conditional_path += mv; - - if (!this.current_cmove) { - throw new Error(`followConditionalSegment called when current_cmove was not set`); - } - - if (this.currently_my_cmove) { - if (mv !== this.current_cmove.move) { - this.current_cmove.children = {}; - } - this.current_cmove.move = mv; - } else { - let cmove = null; - if (mv in this.current_cmove.children) { - cmove = this.current_cmove.children[mv]; - } else { - cmove = new GoConditionalMove(null, this.current_cmove); - this.current_cmove.children[mv] = cmove; - } - this.current_cmove = cmove; - } - - this.currently_my_cmove = !this.currently_my_cmove; - this.emit("conditional-moves.updated"); - } - private deleteConditionalSegment(x: number, y: number) { - this.conditional_path += encodeMove(x, y); - - if (!this.current_cmove) { - throw new Error(`deleteConditionalSegment called when current_cmove was not set`); - } - - if (this.currently_my_cmove) { - this.current_cmove.children = {}; - this.current_cmove.move = null; - const cur = this.current_cmove; - const parent = cur.parent; - this.current_cmove = parent; - if (parent) { - for (const mv in parent.children) { - if (parent.children[mv] === cur) { - delete parent.children[mv]; - } - } - } - } else { - console.error( - "deleteConditionalSegment called on other player's move, which doesn't make sense", - ); - return; - /* - -- actually this code may work below, we just don't have a ui to drive it for testing so we throw an error - - let cmove = null; - if (mv in this.current_cmove.children) { - delete this.current_cmove.children[mv]; - } - */ - } - - this.currently_my_cmove = !this.currently_my_cmove; - this.emit("conditional-moves.updated"); - } - public deleteConditionalPath(move_path: string): void { - const moves = this.engine.decodeMoves(move_path); - if (moves.length) { - for (let i = 0; i < moves.length - 1; ++i) { - if (i !== moves.length - 2) { - this.engine.place(moves[i].x, moves[i].y); - } - this.followConditionalSegment(moves[i].x, moves[i].y); - } - this.deleteConditionalSegment(moves[moves.length - 1].x, moves[moves.length - 1].y); - this.conditional_path = this.conditional_path.substr( - 0, - this.conditional_path.length - 4, - ); - } - this.emit("conditional-moves.updated"); - } - public getCurrentConditionalPath(): string { - return this.conditional_path; - } - public saveConditionalMoves(): void { - this.socket.send("game/conditional_moves/set", { - move_number: this.engine.getCurrentMoveNumber(), - game_id: this.game_id, - conditional_moves: this.conditional_tree.encode(), - }); - this.emit("conditional-moves.updated"); - } - - public setToPreviousMode(dont_jump_to_official_move?: boolean): boolean { - return this.setMode(this.previous_mode as GobanModes, dont_jump_to_official_move); - } - public setModeDeferred(mode: GobanModes): void { - setTimeout(() => { - this.setMode(mode); - }, 1); - } - public setMode(mode: GobanModes, dont_jump_to_official_move?: boolean): boolean { - if ( - mode === "conditional" && - this.player_id === this.engine.playerToMove() && - this.mode !== "score estimation" - ) { - /* this shouldn't ever get called, but incase we screw up.. */ - try { - swal.fire("Can't enter conditional move planning when it's your turn"); - } catch (e) { - console.error(e); - } - return false; - } - - this.setSubmit(); - - if ( - ["play", "analyze", "conditional", "edit", "score estimation", "puzzle"].indexOf( - mode, - ) === -1 - ) { - try { - swal.fire("Invalid mode for Goban: " + mode); - } catch (e) { - console.error(e); - } - return false; - } - - if ( - this.engine.config.disable_analysis && - this.engine.phase !== "finished" && - (mode === "analyze" || mode === "conditional") - ) { - try { - swal.fire("Unable to enter " + mode + " mode"); - } catch (e) { - console.error(e); - } - return false; - } - - if (mode === "conditional") { - this.conditional_starting_color = this.engine.playerColor(); - } - - let redraw = true; - - this.previous_mode = this.mode; - this.mode = mode; - if (!dont_jump_to_official_move) { - this.jumpToLastOfficialMove(); - } - - if (this.mode !== "analyze" || this.analyze_tool !== "draw") { - this.disablePen(); - } else { - this.enablePen(); - } - - if (mode === "play" && this.engine.phase !== "finished") { - this.engine.cur_move.clearMarks(); - redraw = true; - } - - if (redraw) { - this.clearAnalysisDrawing(); - this.redraw(); - } - this.updateTitleAndStonePlacement(); - - return true; - } - public resign(): void { - this.socket.send("game/resign", { - game_id: this.game_id, - }); - } - protected sendPendingResignation(): void { - this.socket.send("game/delayed_resign", { - game_id: this.game_id, - }); - } - protected clearPendingResignation(): void { - this.socket.send("game/clear_delayed_resign", { - game_id: this.game_id, - }); - } - public cancelGame(): void { - this.socket.send("game/cancel", { - game_id: this.game_id, - }); - } - protected annul(): void { - this.socket.send("game/annul", { - game_id: this.game_id, - }); - } - public pass(): void { - if (this.mode === "conditional") { - this.followConditionalSegment(-1, -1); - } - - this.engine.place(-1, -1); - if (this.mode === "play") { - this.sendMove({ - game_id: this.game_id, - move: encodeMove(-1, -1), - }); - } else { - this.syncReviewMove(); - this.move_tree_redraw(); - } - } - public requestUndo(): void { - this.socket.send("game/undo/request", { - game_id: this.game_id, - move_number: this.engine.getCurrentMoveNumber(), - }); - } - public acceptUndo(): void { - this.socket.send("game/undo/accept", { - game_id: this.game_id, - move_number: this.engine.getCurrentMoveNumber(), - }); - } - public cancelUndo(): void { - this.socket.send("game/undo/cancel", { - game_id: this.game_id, - move_number: this.engine.getCurrentMoveNumber(), - }); - } - public pauseGame(): void { - this.socket.send("game/pause", { - game_id: this.game_id, - }); - } - public resumeGame(): void { - this.socket.send("game/resume", { - game_id: this.game_id, - }); - } - - public sendPreventStalling(winner: "black" | "white"): void { - this.socket.send("game/prevent_stalling", { - game_id: this.game_id, - winner, - }); - } - public sendPreventEscaping(winner: "black" | "white", annul: boolean): void { - this.socket.send("game/prevent_escaping", { - game_id: this.game_id, - winner, - annul, - }); - } - - public acceptRemovedStones(): void { - const stones = this.engine.getStoneRemovalString(); - this.engine.players[ - this.engine.playerColor(this.config.player_id) as "black" | "white" - ].accepted_stones = stones; - this.socket.send("game/removed_stones/accept", { - game_id: this.game_id, - stones: stones, - strict_seki_mode: this.engine.strict_seki_mode, - }); - } - public rejectRemovedStones(): void { - delete this.engine.players[ - this.engine.playerColor(this.config.player_id) as "black" | "white" - ].accepted_stones; - this.socket.send("game/removed_stones/reject", { - game_id: this.game_id, - }); - } - public setEditColor(color: "black" | "white"): void { - this.edit_color = color; - this.updateTitleAndStonePlacement(); - } - protected playMovementSound(): void { - if ( - this.last_sound_played_for_a_stone_placement === - this.engine.cur_move.x + "," + this.engine.cur_move.y - ) { - return; - } - this.last_sound_played_for_a_stone_placement = - this.engine.cur_move.x + "," + this.engine.cur_move.y; - - let idx; - do { - idx = Math.round(Math.random() * 10000) % 5; /* 5 === number of stone sounds */ - } while (idx === this.last_stone_sound); - this.last_stone_sound = idx; - - if (this.last_sound_played_for_a_stone_placement === "-1,-1") { - this.emit("audio-pass"); - } else { - this.emit("audio-stone", { - x: this.engine.cur_move.x, - y: this.engine.cur_move.y, - width: this.engine.width, - height: this.engine.height, - color: this.engine.colorNotToMove(), - }); - } - } - protected setState(state: any): void { - if ((this.game_type === "review" || this.game_type === "demo") && this.engine) { - this.drawPenMarks(this.engine.cur_move.pen_marks); - if (this.isPlayerController() && this.connectToReviewSent) { - this.syncReviewMove(); - } - } - - this.setLabelCharacterFromMarks(); - this.markDirty(); - } - protected getState(): {} { - /* This is a callback that gets called by GoEngine.getState to store board state in its state stack */ - const ret = {}; - return ret; - } - public giveReviewControl(player_id: number): void { - this.syncReviewMove({ controller: player_id }); - } - - public setMarks(marks: { [mark: string]: string }, dont_draw?: boolean): void { - for (const key in marks) { - const locations = this.engine.decodeMoves(marks[key]); - for (let i = 0; i < locations.length; ++i) { - const pt = locations[i]; - this.setMark(pt.x, pt.y, key, dont_draw); - } - } - } - public setHeatmap(heatmap?: NumberMatrix, dont_draw?: boolean) { - this.heatmap = heatmap; - if (!dont_draw) { - this.redraw(true); - } - } - public setColoredCircles(circles?: Array, dont_draw?: boolean): void { - if (!circles || circles.length === 0) { - delete this.colored_circles; - return; - } - - this.colored_circles = GoMath.makeEmptyObjectMatrix(this.width, this.height); - for (const circle of circles) { - const mv = circle.move; - this.colored_circles[mv.y][mv.x] = circle; - } - if (!dont_draw) { - this.redraw(true); - } - } - - public setColoredMarks(colored_marks: { - [key: string]: { move: string; color: string }; - }): void { - for (const key in colored_marks) { - const locations = this.engine.decodeMoves(colored_marks[key].move); - for (let i = 0; i < locations.length; ++i) { - const pt = locations[i]; - this.setMarkColor(pt.x, pt.y, colored_marks[key].color); - this.setMark(pt.x, pt.y, key, false); - } - } - } - - protected setMarkColor(x: number, y: number, color: string) { - this.engine.cur_move.getMarks(x, y).color = color; - } - - protected setLetterMark(x: number, y: number, mark: string, drawSquare?: boolean): void { - this.engine.cur_move.getMarks(x, y).letter = mark; - if (drawSquare) { - this.drawSquare(x, y); - } - } - public setSubscriptMark(x: number, y: number, mark: string, drawSquare: boolean = true): void { - this.engine.cur_move.getMarks(x, y).subscript = mark; - if (drawSquare) { - this.drawSquare(x, y); - } - } - public setCustomMark(x: number, y: number, mark: string, drawSquare?: boolean): void { - this.engine.cur_move.getMarks(x, y)[mark] = true; - if (drawSquare) { - this.drawSquare(x, y); - } - } - public deleteCustomMark(x: number, y: number, mark: string, drawSquare?: boolean): void { - delete this.engine.cur_move.getMarks(x, y)[mark]; - if (drawSquare) { - this.drawSquare(x, y); - } - } - - public editPlaceByPrettyCoord( - coord: string, - color: JGOFNumericPlayerColor, - isTrunkMove?: boolean, - ): void { - for (const mv of this.engine.decodeMoves(coord)) { - this.engine.editPlace(mv.x, mv.y, color, isTrunkMove); - } - } - public placeByPrettyCoord(coord: string): void { - for (const mv of this.engine.decodeMoves(coord)) { - const removed_stones: Array = []; - const removed_count = this.engine.place( - mv.x, - mv.y, - undefined, - undefined, - undefined, - undefined, - undefined, - removed_stones, - ); - - if (removed_count > 0) { - this.emit("audio-capture-stones", { - count: removed_count, - already_captured: 0, - }); - this.debouncedEmitCapturedStones(removed_stones); - } - } - } - public setMarkByPrettyCoord(coord: string, mark: number | string, dont_draw?: boolean): void { - for (const mv of this.engine.decodeMoves(coord)) { - this.setMark(mv.x, mv.y, mark, dont_draw); - } - } - public setMark(x: number, y: number, mark: number | string, dont_draw?: boolean): void { - try { - if (x >= 0 && y >= 0) { - if (typeof mark === "number") { - mark = "" + mark; - } - - if (mark.length <= 3 || parseFloat(mark)) { - this.setLetterMark(x, y, mark, !dont_draw); - } else { - this.setCustomMark(x, y, mark, !dont_draw); - } - } - } catch (e) { - console.error(e.stack); - } - } - protected setTransientMark( - x: number, - y: number, - mark: number | string, - dont_draw?: boolean, - ): void { - try { - if (x >= 0 && y >= 0) { - if (typeof mark === "number") { - mark = "" + mark; - } - - if (mark.length <= 3) { - this.engine.cur_move.getMarks(x, y).transient_letter = mark; - } else { - this.engine.cur_move.getMarks(x, y)["transient_" + mark] = true; - } - - if (!dont_draw) { - this.drawSquare(x, y); - } - } - } catch (e) { - console.error(e.stack); - } - } - public getMarks(x: number, y: number): MarkInterface { - if (this.engine && this.engine.cur_move) { - return this.engine.cur_move.getMarks(x, y); - } - return {}; - } - protected toggleMark( - x: number, - y: number, - mark: number | string, - force_label?: boolean, - force_put?: boolean, - ): boolean { - let ret = true; - if (typeof mark === "number") { - mark = "" + mark; - } - const marks = this.getMarks(x, y); - - const clearMarks = () => { - for (let i = 0; i < MARK_TYPES.length; ++i) { - delete marks[MARK_TYPES[i]]; - } - }; - - if (force_label || /^[a-zA-Z0-9]{1,2}$/.test(mark)) { - if (!force_put && "letter" in marks) { - clearMarks(); - ret = false; - } else { - clearMarks(); - marks.letter = mark; - } - } else { - if (!force_put && mark in marks) { - clearMarks(); - ret = false; - } else { - clearMarks(); - this.getMarks(x, y)[mark] = true; - } - } - this.drawSquare(x, y); - return ret; - } - protected incrementLabelCharacter(): void { - const seq1 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; - if (parseInt(this.label_character)) { - this.label_character = "" + (parseInt(this.label_character) + 1); - } else if (seq1.indexOf(this.label_character) !== -1) { - this.label_character = seq1[(seq1.indexOf(this.label_character) + 1) % seq1.length]; - } - } - protected setLabelCharacterFromMarks(set_override?: "numbers" | "letters"): void { - if (set_override === "letters" || /^[a-zA-Z]$/.test(this.label_character)) { - const seq1 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; - let idx = -1; - - for (let y = 0; y < this.height; ++y) { - for (let x = 0; x < this.width; ++x) { - const ch = this.getMarks(x, y).letter; - if (ch) { - idx = Math.max(idx, seq1.indexOf(ch)); - } - } - } - - this.label_character = seq1[idx + (1 % seq1.length)]; - } - if (set_override === "numbers" || /^[0-9]+$/.test(this.label_character)) { - let val = 0; - - for (let y = 0; y < this.height; ++y) { - for (let x = 0; x < this.width; ++x) { - const mark_as_number: number = parseInt(this.getMarks(x, y).letter || ""); - if (mark_as_number) { - val = Math.max(val, mark_as_number); - } - } - } - - this.label_character = "" + (val + 1); - } - } - public setLabelCharacter(ch: string): void { - this.label_character = ch; - if (this.last_hover_square) { - this.drawSquare(this.last_hover_square.x, this.last_hover_square.y); - } - } - public clearMark(x: number, y: number, mark: string | number): void { - try { - if (typeof mark === "number") { - mark = "" + mark; - } - - if (/^[a-zA-Z0-9]{1,2}$/.test(mark)) { - this.getMarks(x, y).letter = ""; - } else { - this.getMarks(x, y)[mark] = false; - } - this.drawSquare(x, y); - } catch (e) { - console.error(e); - } - } - protected clearTransientMark(x: number, y: number, mark: string | number): void { - try { - if (typeof mark === "number") { - mark = "" + mark; - } - - if (/^[a-zA-Z0-9]{1,2}$/.test(mark)) { - this.getMarks(x, y).transient_letter = ""; - } else { - this.getMarks(x, y)["transient_" + mark] = false; - } - this.drawSquare(x, y); - } catch (e) { - console.error(e); - } - } - public updateScoreEstimation(): void { - if (this.score_estimate) { - const est = this.score_estimate.estimated_hard_score - this.engine.komi; - if (GobanCore.hooks.updateScoreEstimation) { - GobanCore.hooks.updateScoreEstimation(est > 0 ? "black" : "white", Math.abs(est)); - } - if (this.config.onScoreEstimationUpdated) { - this.config.onScoreEstimationUpdated(est > 0 ? "black" : "white", Math.abs(est)); - } - this.emit("score_estimate", this.score_estimate); - } - } - public autoScore(): void { - try { - if ( - !(window as any)["user"] || - !this.on_game_screen || - !this.engine || - (((window as any)["user"].id as number) !== this.engine.players.black.id && - ((window as any)["user"].id as number) !== this.engine.players.white.id) - ) { - return; - } - } catch (e) { - console.error(e.stack); - return; - } - - this.auto_scoring_done = true; - - this.showMessage("processing", undefined, -1); - const do_score_estimation = () => { - const se = new ScoreEstimator( - this, - this.engine, - AUTOSCORE_TRIALS, - AUTOSCORE_TOLERANCE, - true /* prefer remote */, - true /* autoscore */, - ); - - se.when_ready - .then(() => { - const current_removed = this.engine.getStoneRemovalString(); - const new_removed = se.getProbablyDead(); - - this.engine.clearRemoved(); - const moves = this.engine.decodeMoves(new_removed); - for (let i = 0; i < moves.length; ++i) { - this.engine.setRemoved(moves[i].x, moves[i].y, true, false); - } - this.emit("stone-removal.updated"); - - this.updateTitleAndStonePlacement(); - this.emit("update"); - - this.socket.send("game/removed_stones/set", { - game_id: this.game_id, - removed: false, - stones: current_removed, - }); - this.socket.send("game/removed_stones/set", { - game_id: this.game_id, - removed: true, - stones: new_removed, - }); - - this.clearMessage(); - }) - .catch((err) => { - console.error(`Auto-scoring error: `, err); - this.clearMessage(); - this.showMessage( - "error", - { - error: { - message: "Auto-scoring failed, please manually score the game", - }, - }, - 3000, - ); - }); - }; - - setTimeout(() => { - init_score_estimator() - .then(do_score_estimation) - .catch((err) => console.error(err)); - }, 10); - } - - protected sendMove(mv: MoveCommand, cb?: () => void): boolean { - if (!mv.blur) { - mv.blur = focus_tracker.getMaxBlurDurationSinceLastReset(); - focus_tracker.reset(); - } - this.setConditionalTree(); - - // Add `.clock` to the move sent to the server - try { - if (this.player_id) { - if (this.__clock_timer) { - clearTimeout(this.__clock_timer); - delete this.__clock_timer; - this.clock_should_be_paused_for_move_submission = true; - } - - const original_clock = this.last_clock; - if (!original_clock) { - throw new Error(`No last_clock when calling sendMove()`); - } - let color: "black" | "white"; - - if (this.player_id === original_clock.black_player_id) { - color = "black"; - } else if (this.player_id === original_clock.white_player_id) { - color = "white"; - } else { - throw new Error(`Player id ${this.player_id} not found in clock`); - } - - if (color) { - const clock_drift = GobanCore.hooks?.getClockDrift - ? GobanCore.hooks?.getClockDrift() - : 0; - - const current_server_time = Date.now() - clock_drift; - - const pause_control = this.pause_control; - - const paused = pause_control - ? isPaused(AdHocPauseControl2JGOFPauseState(pause_control)) - : false; - - const elapsed: number = original_clock.start_mode - ? 0 - : paused && original_clock.paused_since - ? Math.max(original_clock.paused_since, original_clock.last_move) - - original_clock.last_move - : current_server_time - original_clock.last_move; - - const clock = this.computeNewPlayerClock( - original_clock[`${color}_time`] as any, - true, - elapsed, - this.config.time_control as any, - ); - - if (clock.timed_out) { - this.sendTimedOut(); - return false; - } - - mv.clock = clock; - } else { - throw new Error(`No color for player_id ${this.player_id}`); - } - } - } catch (e) { - console.error(e); - } - - // Send the move. If we aren't getting a response, show a message - // indicating such and try reloading after a few more seconds. - let reload_timeout: ReturnType; - const timeout = setTimeout(() => { - this.showMessage("error_submitting_move", undefined, -1); - - reload_timeout = setTimeout(() => { - window.location.reload(); - }, 5000); - }, 5000); - this.emit("submitting-move", true); - this.socket.send("game/move", mv, () => { - if (reload_timeout) { - clearTimeout(reload_timeout); - } - clearTimeout(timeout); - this.clearMessage(); - this.emit("submitting-move", false); - if (cb) { - cb(); - } - }); - - return true; - } - - public sendTimedOut(): void { - // When we think our clock has runout, send a message to the server - // letting it know. Otherwise we have to wait for the server grace - // period to expire for it to time us out. - if (!this.sent_timed_out_message) { - if (this.engine?.phase === "play") { - console.log("Sending timed out"); - - this.sent_timed_out_message = true; - this.socket.send("game/timed_out", { - game_id: this.game_id, - }); - } - } - } - public isCurrentUserAPlayer(): boolean { - return this.player_id in this.engine.player_pool; - } - - public setGameClock(original_clock: AdHocClock | null): void { - if (this.__clock_timer) { - clearTimeout(this.__clock_timer); - delete this.__clock_timer; - } - - if (!original_clock) { - this.emit("clock", null); - return; - } - - if (!this.config.time_control || !this.config.time_control.system) { - this.emit("clock", null); - return; - } - const time_control: JGOFTimeControl = this.config.time_control; - - this.last_clock = original_clock; - - let current_server_time = 0; - function update_current_server_time() { - if (GobanCore.hooks.getClockDrift) { - const server_time_offset = GobanCore.hooks.getClockDrift(); - current_server_time = Date.now() - server_time_offset; - } - } - update_current_server_time(); - - const clock: JGOFClockWithTransmitting = { - current_player: - original_clock.current_player === original_clock.black_player_id - ? "black" - : "white", - current_player_id: original_clock.current_player.toString(), - time_of_last_move: original_clock.last_move, - paused_since: original_clock.paused_since, - black_clock: { main_time: 0 }, - white_clock: { main_time: 0 }, - black_move_transmitting: 0, - white_move_transmitting: 0, - }; - - if (original_clock.pause) { - if (original_clock.pause.paused) { - this.paused_since = original_clock.pause.paused_since; - this.pause_control = original_clock.pause.pause_control; - - /* correct for when we used to store paused_since in terms of seconds instead of ms */ - if (this.paused_since < 2000000000) { - this.paused_since *= 1000; - } - - clock.paused_since = original_clock.pause.paused_since; - clock.pause_state = AdHocPauseControl2JGOFPauseState( - original_clock.pause.pause_control, - ); - } else { - delete this.paused_since; - delete this.pause_control; - } - } - - if (original_clock.start_mode) { - clock.start_mode = true; - } - - const last_audio_event: { [player_id: string]: AudioClockEvent } = { - black: { - countdown_seconds: 0, - clock: { main_time: 0 }, - player_id: "", - color: "black", - time_control_system: "none", - in_overtime: false, - }, - white: { - countdown_seconds: 0, - clock: { main_time: 0 }, - player_id: "", - color: "white", - time_control_system: "none", - in_overtime: false, - }, - }; - - const do_update = () => { - if (!time_control || !time_control.system) { - return; - } - - update_current_server_time(); - - const next_update_time = 100; - - if (clock.start_mode) { - clock.start_time_left = original_clock.expiration - current_server_time; - } - - if (this.paused_since) { - clock.paused_since = this.paused_since; - if (!this.pause_control) { - throw new Error(`Invalid pause_control state when performing clock do_update`); - } - clock.pause_state = AdHocPauseControl2JGOFPauseState(this.pause_control); - if (clock.pause_state.stone_removal) { - clock.stone_removal_time_left = original_clock.expiration - current_server_time; - } - } - - if (!clock.pause_state || Object.keys(clock.pause_state).length === 0) { - delete clock.paused_since; - delete clock.pause_state; - } - - if (this.last_paused_state === null) { - this.last_paused_state = !!clock.pause_state; - } else { - const cur_paused = !!clock.pause_state; - if (cur_paused !== this.last_paused_state) { - this.last_paused_state = cur_paused; - if (cur_paused) { - this.emit("audio-game-paused"); - } else { - this.emit("audio-game-resumed"); - } - } - } - - if (this.last_paused_by_player_state === null) { - this.last_paused_by_player_state = !!this.pause_control?.paused; - } else { - const cur_paused = !!this.pause_control?.paused; - if (cur_paused !== this.last_paused_by_player_state) { - this.last_paused_by_player_state = cur_paused; - if (cur_paused) { - this.emit("paused", cur_paused); - } else { - this.emit("paused", cur_paused); - } - } - } - - const elapsed: number = clock.paused_since - ? Math.max(clock.paused_since, original_clock.last_move) - original_clock.last_move - : current_server_time - original_clock.last_move; - - const black_relative_latency = this.getPlayerRelativeLatency( - original_clock.black_player_id, - ); - const white_relative_latency = this.getPlayerRelativeLatency( - original_clock.white_player_id, - ); - - const black_elapsed = Math.max(0, elapsed - Math.abs(black_relative_latency)); - const white_elapsed = Math.max(0, elapsed - Math.abs(white_relative_latency)); - - clock.black_clock = this.computeNewPlayerClock( - original_clock.black_time as AdHocPlayerClock, - clock.current_player === "black" && !clock.start_mode, - black_elapsed, - time_control, - ); - - clock.white_clock = this.computeNewPlayerClock( - original_clock.white_time as AdHocPlayerClock, - clock.current_player === "white" && !clock.start_mode, - white_elapsed, - time_control, - ); - - const wall_clock_elapsed = current_server_time - original_clock.last_move; - clock.black_move_transmitting = - clock.current_player === "black" - ? Math.max(0, black_relative_latency - wall_clock_elapsed) - : 0; - clock.white_move_transmitting = - clock.current_player === "white" - ? Math.max(0, white_relative_latency - wall_clock_elapsed) - : 0; - - if (!this.sent_timed_out_message && !this.clock_should_be_paused_for_move_submission) { - if ( - clock.current_player === "white" && - this.player_id === this.engine.config.white_player_id - ) { - if ((clock.white_clock as JGOFPlayerClockWithTimedOut).timed_out) { - this.sendTimedOut(); - } - } - if ( - clock.current_player === "black" && - this.player_id === this.engine.config.black_player_id - ) { - if ((clock.black_clock as JGOFPlayerClockWithTimedOut).timed_out) { - this.sendTimedOut(); - } - } - } - - if (this.clock_should_be_paused_for_move_submission && this.last_emitted_clock) { - this.emit("clock", this.last_emitted_clock); - } else { - this.emit("clock", clock); - } - - // check if we need to update our audio - if ( - (this.mode === "play" || - this.mode === "analyze" || - this.mode === "conditional" || - this.mode === "score estimation") && - this.engine.phase === "play" - ) { - // Move's and clock events are separate, so this just checks to make sure that when we - // update, we are updating when the engine and clock agree on whose turn it is. - const current_color = - this.engine.last_official_move.stoneColor === "black" ? "white" : "black"; - const current_player = this.engine.players[current_color].id.toString(); - - if (current_color === clock.current_player) { - const player_clock: JGOFPlayerClock = - clock.current_player === "black" ? clock.black_clock : clock.white_clock; - const audio_clock: AudioClockEvent = { - countdown_seconds: 0, - clock: player_clock, - player_id: current_player, - color: current_color, - time_control_system: time_control.system, - in_overtime: false, - }; - - switch (time_control.system) { - case "simple": - if (audio_clock.countdown_seconds === time_control.per_move) { - // When byo-yomi resets, we don't want to play the sound for the - // top of the second mark because it's going to get clipped short - // very soon as time passes and we're going to start playing the - // next second sound. - audio_clock.countdown_seconds = -1; - } else { - audio_clock.countdown_seconds = Math.ceil( - player_clock.main_time / 1000, - ); - } - break; - - case "absolute": - case "fischer": - audio_clock.countdown_seconds = Math.ceil( - player_clock.main_time / 1000, - ); - break; - - case "byoyomi": - if (player_clock.main_time > 0) { - audio_clock.countdown_seconds = Math.ceil( - player_clock.main_time / 1000, - ); - } else { - audio_clock.in_overtime = true; - audio_clock.countdown_seconds = Math.ceil( - (player_clock.period_time_left || 0) / 1000, - ); - if ((player_clock.periods_left || 0) <= 0) { - audio_clock.countdown_seconds = -1; - } - - /* - if ( - audio_clock.countdown_seconds === time_control.period_time && - audio_clock.in_overtime == last_audio_event[clock.current_player].in_overtime - ) { - // When byo-yomi resets, we don't want to play the sound for the - // top of the second mark because it's going to get clipped short - // very soon as time passes and we're going to start playing the - // next second sound. - audio_clock.countdown_seconds = -1; - } - */ - } - break; - - case "canadian": - if (player_clock.main_time > 0) { - audio_clock.countdown_seconds = Math.ceil( - player_clock.main_time / 1000, - ); - } else { - audio_clock.in_overtime = true; - audio_clock.countdown_seconds = Math.ceil( - (player_clock.block_time_left || 0) / 1000, - ); - - if (audio_clock.countdown_seconds === time_control.period_time) { - // When we start a new period, we don't want to play the sound for the - // top of the second mark because it's going to get clipped short - // very soon as time passes and we're going to start playing the - // next second sound. - audio_clock.countdown_seconds = -1; - } - } - break; - - case "none": - break; - - default: - throw new Error( - `Unsupported time control system: ${(time_control as any).system}`, - ); - } - - const cur = audio_clock; - const last = last_audio_event[clock.current_player]; - if ( - cur.countdown_seconds !== last.countdown_seconds || - cur.player_id !== last.player_id || - cur.in_overtime !== last.in_overtime - ) { - last_audio_event[clock.current_player] = audio_clock; - if (audio_clock.countdown_seconds > 0) { - this.emit("audio-clock", audio_clock); - } - } - } else { - // Engine and clock code didn't agree on whose turn it was, don't emit audio-clock event yet - } - } - - if (this.engine.phase !== "finished") { - this.__clock_timer = setTimeout(do_update, next_update_time); - } - }; - - do_update(); - } - - protected computeNewPlayerClock( - original_player_clock: Readonly, - is_current_player: boolean, - time_elapsed: number, - time_control: Readonly, - ): JGOFPlayerClockWithTimedOut { - const ret: JGOFPlayerClockWithTimedOut = { - main_time: 0, - timed_out: false, - }; - - const original_clock = this.last_clock; - if (!original_clock) { - throw new Error(`No last_clock when computing new player clock`); - } - - const tcs: string = "" + time_control.system; - switch (time_control.system) { - case "simple": - ret.main_time = is_current_player - ? Math.max(0, time_control.per_move - time_elapsed / 1000) * 1000 - : time_control.per_move * 1000; - if (ret.main_time <= 0) { - ret.timed_out = true; - } - break; - - case "none": - ret.main_time = 0; - break; - - case "absolute": - /* - ret.main_time = is_current_player - ? Math.max( - 0, - original_clock_expiration + raw_clock_pause_offset - current_server_time, - ) - : Math.max(0, original_player_clock.thinking_time * 1000); - */ - ret.main_time = is_current_player - ? Math.max(0, original_player_clock.thinking_time * 1000 - time_elapsed) - : original_player_clock.thinking_time * 1000; - if (ret.main_time <= 0) { - ret.timed_out = true; - } - break; - - case "fischer": - ret.main_time = is_current_player - ? Math.max(0, original_player_clock.thinking_time * 1000 - time_elapsed) - : original_player_clock.thinking_time * 1000; - if (ret.main_time <= 0) { - ret.timed_out = true; - } - break; - - case "byoyomi": - if (is_current_player) { - let overtime_usage = 0; - if (original_player_clock.thinking_time > 0) { - ret.main_time = original_player_clock.thinking_time * 1000 - time_elapsed; - if (ret.main_time <= 0) { - overtime_usage = -ret.main_time; - ret.main_time = 0; - } - } else { - ret.main_time = 0; - overtime_usage = time_elapsed; - } - ret.periods_left = original_player_clock.periods || 0; - ret.period_time_left = time_control.period_time * 1000; - if (overtime_usage > 0) { - const periods_used = Math.floor( - overtime_usage / (time_control.period_time * 1000), - ); - ret.periods_left -= periods_used; - ret.period_time_left = - time_control.period_time * 1000 - - (overtime_usage - periods_used * time_control.period_time * 1000); - - if (ret.periods_left < 0) { - ret.periods_left = 0; - } - - if (ret.period_time_left < 0) { - ret.period_time_left = 0; - } - } - } else { - ret.main_time = original_player_clock.thinking_time * 1000; - ret.periods_left = original_player_clock.periods; - ret.period_time_left = time_control.period_time * 1000; - } - - if (ret.main_time <= 0 && (ret.periods_left || 0) === 0) { - ret.timed_out = true; - } - break; - - case "canadian": - if (is_current_player) { - let overtime_usage = 0; - if (original_player_clock.thinking_time > 0) { - ret.main_time = original_player_clock.thinking_time * 1000 - time_elapsed; - if (ret.main_time <= 0) { - overtime_usage = -ret.main_time; - ret.main_time = 0; - } - } else { - ret.main_time = 0; - overtime_usage = time_elapsed; - } - ret.moves_left = original_player_clock.moves_left; - ret.block_time_left = (original_player_clock.block_time || 0) * 1000; - - if (overtime_usage > 0) { - ret.block_time_left -= overtime_usage; - - if (ret.block_time_left < 0) { - ret.block_time_left = 0; - } - } - } else { - ret.main_time = original_player_clock.thinking_time * 1000; - ret.moves_left = original_player_clock.moves_left; - ret.block_time_left = (original_player_clock.block_time || 0) * 1000; - } - - if (ret.main_time <= 0 && ret.block_time_left <= 0) { - ret.timed_out = true; - } - break; - - default: - throw new Error(`Unsupported time control system: ${tcs}`); - } - - return ret; - } - - public syncReviewMove(msg_override?: ReviewMessage, node_text?: string): void { - if ( - this.review_id && - (this.isPlayerController() || - (this.isPlayerOwner() && msg_override && msg_override.controller)) && - this.done_loading_review - ) { - if (this.isInPushedAnalysis()) { - return; - } - - const diff = this.engine.getMoveDiff(); - this.engine.setAsCurrentReviewMove(); - - let msg: ReviewMessage; - - if (!msg_override) { - const marks: { [mark: string]: string } = {}; - for (let y = 0; y < this.height; ++y) { - for (let x = 0; x < this.width; ++x) { - const pos = this.getMarks(x, y); - for (let i = 0; i < MARK_TYPES.length; ++i) { - if (MARK_TYPES[i] in pos && pos[MARK_TYPES[i]]) { - const mark_key: keyof MarkInterface = - MARK_TYPES[i] === "letter" - ? pos.letter || "[ERR]" - : MARK_TYPES[i]; - if (!(mark_key in marks)) { - marks[mark_key] = ""; - } - marks[mark_key] += encodeMove(x, y); - } - } - } - } - - if (!node_text && node_text !== "") { - node_text = this.engine.cur_move.text || ""; - } - - msg = { - f: diff.from, - t: node_text, - m: diff.moves, - k: marks, - }; - const tmp = dup(msg); - - if (this.last_review_message.f === msg.f && this.last_review_message.m === msg.m) { - delete msg["f"]; - delete msg["m"]; - - const txt_idx = node_text.indexOf(this.engine.cur_move.text || ""); - if (txt_idx === 0) { - delete msg["t"]; - if (node_text !== this.engine.cur_move.text) { - msg["t+"] = node_text.substr(this.engine.cur_move.text.length); - } - } - - if (deepEqual(marks, this.last_review_message.k)) { - delete msg["k"]; - } - } else { - this.scheduleRedrawPenLayer(); - } - this.engine.cur_move.text = node_text; - this.last_review_message = tmp; - - if (Object.keys(msg).length === 0) { - return; - } - } else { - msg = msg_override; - if (msg.clearpen) { - this.engine.cur_move.pen_marks = []; - } - } - - msg.review_id = this.review_id; - - this.socket.send("review/append", msg); - } - } - public setScoringMode(tf: boolean, prefer_remote: boolean = false): MoveTree { - this.scoring_mode = tf; - const ret = this.engine.cur_move; - - if (this.scoring_mode) { - this.showMessage("processing", undefined, -1); - this.setMode("score estimation", true); - this.clearMessage(); - const autoscore = false; - this.score_estimate = this.engine.estimateScore( - SCORE_ESTIMATION_TRIALS, - SCORE_ESTIMATION_TOLERANCE, - prefer_remote, - autoscore, - ); - this.enableStonePlacement(); - this.redraw(true); - this.emit("update"); - } else { - if (this.previous_mode === "analyze" || this.previous_mode === "conditional") { - this.setToPreviousMode(true); - } else { - this.setMode("play"); - } - this.redraw(true); - } - - return ret; - } - - /* Computes the relative latency between the target player and the current viewer. - * For example, if player P has a latency of 500ms and we have a latency of 200ms, - * the relative latency will be 300ms. This is used to artificially delay the clock - * countdown for that player to minimize the amount of apparent time jumping that can - * happen as clocks are synchronized */ - public getPlayerRelativeLatency(player_id: number): number { - if (player_id === this.player_id) { - return 0; - } - - // If the other latency is not available for whatever reason, use our own latency as a better-than-0 guess */ - const other_latency = this.engine?.latencies?.[player_id] || this.getNetworkLatency(); - - return other_latency - this.getNetworkLatency(); - } - public getLastReviewMessage(): ReviewMessage { - return this.last_review_message; - } - public setLastReviewMessage(m: ReviewMessage): void { - this.last_review_message = m; - } - - private last_emitted_captured_stones: Array = []; - - /* Emits the captured-stones event, only if didn't just emitted it with - * the same removed_stones. That situation happens when the client signals - * the removal, and then we get a second followup confirmation from the - * server, we need both sources of the event for when the user has two - * clients pointed at the same game, but we don't want to emit the event - * twice on the device that submitted the move in the first place. */ - public debouncedEmitCapturedStones(removed_stones: Array): void { - if (removed_stones.length > 0) { - const captured_stones = removed_stones - .map((o) => ({ x: o.x, y: o.y })) - .sort((a, b) => { - if (a.x < b.x) { - return -1; - } else if (a.x > b.x) { - return 1; - } else if (a.y < b.y) { - return -1; - } else if (a.y > b.y) { - return 1; - } else { - return 0; - } - }); - - let different = captured_stones.length !== this.last_emitted_captured_stones.length; - if (!different) { - for (let i = 0; i < captured_stones.length; ++i) { - if ( - captured_stones[i].x !== this.last_emitted_captured_stones[i].x || - captured_stones[i].y !== this.last_emitted_captured_stones[i].y - ) { - different = true; - break; - } - } - } - - if (different) { - this.last_emitted_captured_stones = removed_stones; - this.emit("captured-stones", { removed_stones }); - } - } - } -} -function uuid(): string { - // cspell: words yxxx - return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => { - const r = (Math.random() * 16) | 0; - const v = c === "x" ? r : (r & 0x3) | 0x8; - return v.toString(16); - }); -} - -function AdHocPauseControl2JGOFPauseState(pause_control: AdHocPauseControl): JGOFPauseState { - const ret: JGOFPauseState = {}; - - for (const k in pause_control) { - const matches = k.match(/vacation-([0-9]+)/); - if (matches) { - const player_id = matches[1]; - if (!ret.vacation) { - ret.vacation = {}; - } - ret.vacation[player_id] = true; - } else { - switch (k) { - case "stone-removal": - ret.stone_removal = true; - break; - - case "weekend": - ret.weekend = true; - break; - - case "server": - case "system": - ret.server = true; - break; - - case "paused": - ret.player = { - player_id: pause_control.paused?.pausing_player_id.toString() || "0", - pauses_left: pause_control.paused?.pauses_left || 0, - }; - break; - - case "moderator_paused": - ret.moderator = pause_control.moderator_paused?.moderator_id.toString() || "0"; - break; - - default: - throw new Error(`Unhandled pause control key: ${k}`); - } - } - } - - return ret; -} - -function repair_config(config: GobanConfig): GobanConfig { - if (config.time_control) { - if (!config.time_control.system && (config.time_control as any).time_control) { - (config.time_control as any).system = (config.time_control as any).time_control; - console.log( - "Repairing goban config: time_control.time_control -> time_control.system = ", - (config.time_control as any).system, - ); - } - if (!config.time_control.speed) { - const tpm = computeAverageMoveTime(config.time_control, config.width, config.height); - (config.time_control as any).speed = - tpm === 0 || tpm > 3600 ? "correspondence" : tpm < 10 ? "blitz" : "live"; - console.log( - "Repairing goban config: time_control.speed = ", - (config.time_control as any).speed, - ); - } - } - - return config; -} - -class FocusTracker { - hasFocus: boolean = true; - lastFocus: number = Date.now(); - outOfFocusDurations: Array = []; - - constructor() { - try { - if (CLIENT) { - window.addEventListener("blur", this.onBlur); - window.addEventListener("focus", this.onFocus); - } - } catch (e) { - console.error(e); - } - } - - reset(): void { - this.lastFocus = Date.now(); - this.outOfFocusDurations = []; - } - - getMaxBlurDurationSinceLastReset(): number { - if (!this.hasFocus) { - this.outOfFocusDurations.push(Date.now() - this.lastFocus); - } - - if (this.outOfFocusDurations.length === 0) { - return 0; - } - - const ret = Math.max.apply(Math.max, this.outOfFocusDurations); - - if (!this.hasFocus) { - this.outOfFocusDurations.pop(); - } - - return ret; - } - - onFocus = () => { - this.hasFocus = true; - this.outOfFocusDurations.push(Date.now() - this.lastFocus); - this.lastFocus = Date.now(); - }; - - onBlur = () => { - this.hasFocus = false; - this.lastFocus = Date.now(); - }; -} - -function isPaused(pause_state: JGOFPauseState): boolean { - for (const _key in pause_state) { - return true; - } - return false; -} - -export const focus_tracker = new FocusTracker(); diff --git a/src/__tests__/GoMath_GoStoneGroup.test.ts b/src/__tests__/GoMath_GoStoneGroup.test.ts deleted file mode 100644 index ebdda3fa..00000000 --- a/src/__tests__/GoMath_GoStoneGroup.test.ts +++ /dev/null @@ -1,88 +0,0 @@ -import * as GoMath from "../GoMath"; -import { GoStoneGroups } from "../GoStoneGroups"; - -// Here is a board displaying many of the features GoStoneGroup cares about. - -// A B C D E -// 5 . . O X . -// 4 O O O X X -// 3 X . O X . -// 2 . X O X X -// 1 X X O X . - -// A2: Eye, but not a "strong" eye -// E1, E3, E5: Strong eyes -// D1-E5 stones: Strong string -// all empty space except B3: Territory -// A5-B5, A2: Territory in seki - -const FEATURE_BOARD = [ - [0, 0, 1, 2, 0], - [1, 1, 1, 2, 2], - [2, 0, 1, 2, 0], - [0, 2, 1, 2, 2], - [2, 2, 1, 2, 0], -]; - -const REMOVAL = GoMath.makeMatrix(5, 5); - -function makeGoMathWithFeatureBoard() { - return new GoStoneGroups({ - board: FEATURE_BOARD, - removal: REMOVAL, - width: 5, - height: 5, - }); -} - -test("Group ID Map", () => { - const gm = makeGoMathWithFeatureBoard(); - - expect(gm.group_id_map).toEqual([ - [1, 1, 2, 3, 4], - [2, 2, 2, 3, 3], - [5, 6, 2, 3, 7], - [8, 9, 2, 3, 3], - [9, 9, 2, 3, 10], - ]); -}); - -test("Eyes", () => { - const gm = makeGoMathWithFeatureBoard(); - - const eyes = gm.groups.filter((g) => g.is_eye).map((g) => g.id); - - expect(eyes).toEqual([4, 7, 8, 10]); -}); - -test("Strong eyes", () => { - const gm = makeGoMathWithFeatureBoard(); - - const strong_eyes = gm.groups.filter((g) => g.is_strong_eye).map((g) => g.id); - - expect(strong_eyes).toEqual([4, 7, 10]); -}); - -test("Strong Strings", () => { - const gm = makeGoMathWithFeatureBoard(); - - const strong_strings = gm.groups.filter((g) => g.is_strong_string).map((g) => g.id); - - expect(strong_strings).toEqual([3]); -}); - -test("Territory", () => { - const gm = makeGoMathWithFeatureBoard(); - - const territory = gm.groups.filter((g) => g.is_territory).map((g) => g.id); - - expect(territory).toEqual([1, 4, 7, 8, 10]); -}); - -test("Territory in seki", () => { - const gm = makeGoMathWithFeatureBoard(); - - const territory_in_seki = gm.groups.filter((g) => g.is_territory_in_seki).map((g) => g.id); - - expect(territory_in_seki).not.toContain([1, 8]); -}); diff --git a/src/autoscore.ts b/src/autoscore.ts deleted file mode 100644 index 17e9c86f..00000000 --- a/src/autoscore.ts +++ /dev/null @@ -1,782 +0,0 @@ -/* - * Copyright (C) Online-Go.com - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/** - * The autoscore function takes an existing board state, two ownership - * matrices, and does it's best to determine which stones should be - * removed, which intersections should be considered dame, and what - * should be left alone. - */ - -import { GoStoneGroups } from "./GoStoneGroups"; -import { JGOFNumericPlayerColor } from "./JGOF"; -import { makeMatrix, num2char } from "./GoMath"; - -interface AutoscoreResults { - result: JGOFNumericPlayerColor[][]; - removed_string: string; - removed: [number, number, /* reason */ string][]; -} - -const REMOVAL_THRESHOLD = 0.7; -const WHITE_THRESHOLD = -REMOVAL_THRESHOLD; -const BLACK_THRESHOLD = REMOVAL_THRESHOLD; - -function isWhite(ownership: number): boolean { - return ownership <= WHITE_THRESHOLD; -} - -function isBlack(ownership: number): boolean { - return ownership >= BLACK_THRESHOLD; -} - -function isDameOrUnknown(ownership: number): boolean { - return ownership > WHITE_THRESHOLD && ownership < BLACK_THRESHOLD; -} - -type DebugOutput = string; - -let debug_output = ""; -function debug(...args: any[]) { - debug_output += args.join(" ") + "\n"; -} -function reset_debug_output() { - debug_output = ""; -} - -export function autoscore( - board: JGOFNumericPlayerColor[][], - black_plays_first_ownership: number[][], - white_plays_first_ownership: number[][], -): [AutoscoreResults, DebugOutput] { - const original_board = board.map((row) => row.slice()); // copy - const width = board[0].length; - const height = board.length; - const removed: [number, number, string][] = []; - const removal = makeMatrix(width, height); - const is_settled = makeMatrix(width, height); - const settled = makeMatrix(width, height); - const final_ownership = makeMatrix(board[0].length, board.length); - - const average_ownership = makeMatrix(width, height); - for (let y = 0; y < height; ++y) { - for (let x = 0; x < width; ++x) { - average_ownership[y][x] = - (black_plays_first_ownership[y][x] + white_plays_first_ownership[y][x]) / 2; - } - } - - reset_debug_output(); - debug("Initial board:"); - debug_board_output(board); - - debug("Ownership if black moves first:"); - debug_ownership_output(black_plays_first_ownership); - - debug("Ownership if white moves first:"); - debug_ownership_output(white_plays_first_ownership); - - settle_agreed_upon_territory(); - remove_obviously_dead_stones(); - mark_settled_positions(); - clear_unsettled_stones_from_territory(); - seal_territory(); - compute_final_ownership(); - final_dame_pass(); - - return [ - { - result: final_ownership, - removed_string: removed.map((pt) => `${num2char(pt[0])}${num2char(pt[1])}`).join(""), - removed, - }, - debug_output, - ]; - - /** Marks a position as being removed (either dead stone or dame) */ - function remove(x: number, y: number, reason: string) { - if (removal[y][x]) { - return; - } - - removed.push([x, y, reason]); - board[y][x] = JGOFNumericPlayerColor.EMPTY; - removal[y][x] = 1; - } - - /* - * Settle agreed-upon territory - * - * The purpose of this function is to ignore potential invasions - * by looking at the average territory ownership. If overall the - * territory is owned by one player, then we mark it as settled along - * with adjacent groups. - */ - function settle_agreed_upon_territory() { - debug("### Settling agreed upon territory"); - - const groups = new GoStoneGroups({ - width, - height, - board, - removal: makeMatrix(width, height), - }); - - debug_groups(groups); - - groups.foreachGroup((group) => { - const color = group.territory_color; - if (group.is_territory && color) { - let total_ownership = 0; - - group.foreachStone((point) => { - const x = point.x; - const y = point.y; - total_ownership += average_ownership[y][x]; - }); - - const avg = total_ownership / group.points.length; - - if ( - (color === JGOFNumericPlayerColor.BLACK && avg > BLACK_THRESHOLD) || - (color === JGOFNumericPlayerColor.WHITE && avg < WHITE_THRESHOLD) - ) { - group.foreachStone((point) => { - const x = point.x; - const y = point.y; - is_settled[y][x] = 1; - settled[y][x] = color; - }); - group.neighbors.forEach((neighbor) => { - neighbor.foreachStone((point) => { - const x = point.x; - const y = point.y; - is_settled[y][x] = 1; - settled[y][x] = color; - }); - }); - } - } - }); - } - - /* - * Remove obviously dead stones - * - * If we estimate that if either player moves first, yet a stone - * is dead, then we say the players agree - the stone is dead. This - * function detects these cases and removes the stones. - */ - function remove_obviously_dead_stones() { - debug("### Removing stones both agree on:"); - for (let y = 0; y < height; ++y) { - for (let x = 0; x < width; ++x) { - if ( - board[y][x] === JGOFNumericPlayerColor.WHITE && - isBlack(black_plays_first_ownership[y][x]) && - isBlack(white_plays_first_ownership[y][x]) - ) { - remove(x, y, "both players agree this is captured by black"); - } else if ( - board[y][x] === JGOFNumericPlayerColor.BLACK && - isWhite(black_plays_first_ownership[y][x]) && - isWhite(white_plays_first_ownership[y][x]) - ) { - remove(x, y, "both players agree this is captured by white"); - } else if ( - board[y][x] === JGOFNumericPlayerColor.EMPTY && - isDameOrUnknown(black_plays_first_ownership[y][x]) && - isDameOrUnknown(white_plays_first_ownership[y][x]) - ) { - remove(x, y, "both players agree this is dame"); - } - } - } - } - - /* - * Mark settled intersections as settled - * - * If both players agree on the ownership of an intersection, then - * mark it as settled for that player. - */ - function mark_settled_positions() { - debug("### Marking settled positions"); - for (let y = 0; y < height; ++y) { - for (let x = 0; x < width; ++x) { - if ( - isWhite(black_plays_first_ownership[y][x]) && - isWhite(white_plays_first_ownership[y][x]) - ) { - is_settled[y][x] = 1; - settled[y][x] = JGOFNumericPlayerColor.WHITE; - } - - if ( - isBlack(black_plays_first_ownership[y][x]) && - isBlack(white_plays_first_ownership[y][x]) - ) { - is_settled[y][x] = 1; - settled[y][x] = JGOFNumericPlayerColor.BLACK; - } - } - } - - debug_print_settled(is_settled); - debug_board_output(board); - } - - /* - * Consider unsettled groups (as defined by looking at connected - * intersections that are not settled, regardless of whether they have a - * stone on them or not). - * - * Pick an owner for this area based first on the average ownership - * of the area. If the average ownership exceeds our threshold, then - * we assume that the area is owned by the player. Otherwise, if the - * owner isn't clear according to our ownership estimations, then we - * go by the majority of the surrounding and contained stones - the - * one with the most stones wins. - * - * If we've determined a color for the area, then we remove any stones - * that are not of that color in the unsettled area. - * - * After this, we mark the area as settled. - */ - function clear_unsettled_stones_from_territory() { - /* - * Consider unsettled groups. Count the unsettled stones along with - * their neighboring stones - */ - const groups = new GoStoneGroups({ - width, - height, - board: is_settled, - removal: makeMatrix(width, height), - }); - - groups.foreachGroup((group) => { - // if this group is a settled group, ignore it, we don't care about those - const pt = group.points[0]; - if (is_settled[pt.y][pt.x]) { - return; - } - - // Otherwise, count - const surrounding = [ - 0, // empty - 0, // black - 0, // white - ]; - const contained = [ - 0, // empty - 0, // black - 0, // white - ]; - let total_ownership_estimate = 0; - - const already_tallied = makeMatrix(width, height); - function tally_edge(x: number, y: number) { - if (x < 0 || x >= width || y < 0 || y >= height) { - return; - } - if (already_tallied[y][x]) { - return; - } - if (is_settled[y][x]) { - surrounding[settled[y][x]]++; - already_tallied[y][x] = 1; - } - } - - group.foreachStone((point) => { - const x = point.x; - const y = point.y; - contained[board[y][x]]++; - tally_edge(x - 1, y); - tally_edge(x + 1, y); - tally_edge(x, y - 1); - tally_edge(x, y + 1); - - total_ownership_estimate += - black_plays_first_ownership[y][x] + white_plays_first_ownership[y][x]; - }); - - const average_color_estimate = total_ownership_estimate / group.points.length; - - let color_judgement: JGOFNumericPlayerColor; - - const total = [ - surrounding[0] + contained[0], - surrounding[1] + contained[1], - surrounding[2] + contained[2], - ]; - if (average_color_estimate > 0.5) { - color_judgement = JGOFNumericPlayerColor.BLACK; - } else if (total_ownership_estimate < -0.5) { - color_judgement = JGOFNumericPlayerColor.WHITE; - } else { - if (total[JGOFNumericPlayerColor.BLACK] > total[JGOFNumericPlayerColor.WHITE]) { - color_judgement = JGOFNumericPlayerColor.BLACK; - } else if ( - total[JGOFNumericPlayerColor.WHITE] > total[JGOFNumericPlayerColor.BLACK] - ) { - color_judgement = JGOFNumericPlayerColor.WHITE; - } else { - color_judgement = JGOFNumericPlayerColor.EMPTY; - } - } - - group.foreachStone((point) => { - const x = point.x; - const y = point.y; - if (board[y][x] && board[y][x] !== color_judgement) { - remove(x, y, "clearing unsettled stones within assumed territory"); - is_settled[y][x] = 1; - settled[y][x] = color_judgement; - } - }); - - debug( - "Group: ", - group.id, - "contained", - contained, - "surrounding", - surrounding, - " total ownership estimate", - total_ownership_estimate, - " average color estimate", - average_color_estimate, - " color judgement", - color_judgement === JGOFNumericPlayerColor.BLACK - ? "black" - : color_judgement === JGOFNumericPlayerColor.WHITE - ? "white" - : "empty", - ); - }); - } - - /* - * Attempt to seal territory - * - * This function attempts to seal territory that has been overlooked - * by the players. - * - * We do this by looking at unowned territory that has been settled - * by the players as either dame or owned by one player. If the - * intersection is owned by one player but immediately adjacent to - * an intersection owned by the other player, then we mark it as - * dame to (help) seal the territory. - * - * Note, this needs to be run after obviously dead stones have been - * removed. - */ - - function seal_territory() { - debug(`### Sealing territory`); - //const dame_map = makeMatrix(width, height); - { - let groups = new GoStoneGroups( - { - width, - height, - board, - removal, - }, - original_board, - ); - - debug("Initial groups:"); - debug_groups(groups); - - groups.foreachGroup((group) => { - // unowned territory - if (group.color === JGOFNumericPlayerColor.EMPTY && !group.is_territory) { - group.foreachStone((point) => { - const x = point.x; - const y = point.y; - - // If we have an intersection we believe is owned by a player, but it is also - // adjacent to another the other players stone, mark it as dame - if (is_settled[y][x] && settled[y][x] !== JGOFNumericPlayerColor.EMPTY) { - const opposing_color = - settled[y][x] === JGOFNumericPlayerColor.BLACK - ? JGOFNumericPlayerColor.WHITE - : JGOFNumericPlayerColor.BLACK; - const adjacent_to_opposing_color = - board[y + 1]?.[x] === opposing_color || - board[y - 1]?.[x] === opposing_color || - board[y][x + 1] === opposing_color || - board[y][x - 1] === opposing_color; - - if (adjacent_to_opposing_color) { - remove(x, y, "sealing territory"); - is_settled[y][x] = 1; - settled[y][x] = JGOFNumericPlayerColor.EMPTY; - } - } - }); - } - }); - - groups = new GoStoneGroups( - { - width: board[0].length, - height: board.length, - board, - removal, - }, - original_board, - ); - debug("Sealed groups:"); - debug_groups(groups); - - debug("Settle sealed groups"); - groups.foreachGroup((group) => { - if (group.is_territory || group.color !== JGOFNumericPlayerColor.EMPTY) { - group.foreachStone((point) => { - is_settled[point.y][point.x] = 1; - settled[point.y][point.x] = group.color; - }); - } - }); - } - } - - function compute_final_ownership() { - for (let y = 0; y < board.length; ++y) { - for (let x = 0; x < board[y].length; ++x) { - if (is_settled[y][x]) { - //final_ownership[y][x] = board[y][x]; - final_ownership[y][x] = settled[y][x]; - } else { - if ( - isBlack(black_plays_first_ownership[y][x]) && - isBlack(white_plays_first_ownership[y][x]) - ) { - final_ownership[y][x] = JGOFNumericPlayerColor.BLACK; - } else if ( - isWhite(black_plays_first_ownership[y][x]) && - isWhite(white_plays_first_ownership[y][x]) - ) { - final_ownership[y][x] = JGOFNumericPlayerColor.WHITE; - } else { - final_ownership[y][x] = JGOFNumericPlayerColor.EMPTY; - } - } - } - } - - // fill in territory for final ownership - { - const groups = new GoStoneGroups( - { - width, - height, - board, - removal, - }, - original_board, - ); - groups.foreachGroup((group) => { - if ( - group.color === JGOFNumericPlayerColor.EMPTY && - group.is_territory && - group.territory_color - - //&& !group.is_territory_in_seki - ) { - group.foreachStone((point) => { - if (is_settled[point.y][point.x]) { - final_ownership[point.y][point.x] = group.territory_color; - } - }); - } - }); - - debug("Final ownership:"); - debug_board_output(final_ownership); - } - } - - function final_dame_pass() { - for (let y = 0; y < final_ownership.length; ++y) { - for (let x = 0; x < final_ownership[y].length; ++x) { - if (final_ownership[y][x] === JGOFNumericPlayerColor.EMPTY) { - remove(x, y, "final dame"); - } - } - } - } -} - -function debug_ownership_output(ownership: number[][]) { - let out = "\n "; - const x_coords = "ABCDEFGHJKLMNOPQRST"; // cspell: disable-line - - for (let x = 0; x < ownership[0].length; ++x) { - out += `${x_coords[x]}`; - } - out += "\n"; - - for (let y = 0; y < ownership.length; ++y) { - out += ` ${ownership.length - y} `.substr(-3); - for (let x = 0; x < ownership[y].length; ++x) { - //out += ` ${(" " + ownership[y][x].toFixed(1)).substr(-4)} `; - out += colorizeOwnership(ownership[y][x]); - } - out += " " + ` ${ownership.length - y} `.substr(-3); - out += "\n"; - } - - out += " "; - for (let x = 0; x < ownership[0].length; ++x) { - out += `${x_coords[x]}`; - } - out += "\n"; - - out += "\n"; - - debug(out); -} - -function colorizeOwnership(ownership: number): string { - const mag = Math.round(Math.abs(ownership * 10)); - let mag_str = ""; - if (mag > 9) { - mag_str = ownership > 0 ? "B" : "W"; - } else { - mag_str = mag.toString(); - } - - if (mag < 7) { - if (ownership > 0) { - return blue(mag_str); - } else { - return cyanBright(mag_str); - } - } - if (ownership > 0) { - return blackBright(mag_str); - } else { - return whiteBright(mag_str); - } -} - -function debug_board_output(board: JGOFNumericPlayerColor[][]) { - let out = " "; - const x_coords = "ABCDEFGHJKLMNOPQRST"; // cspell: disable-line - - for (let x = 0; x < board[0].length; ++x) { - out += `${x_coords[x]}`; - } - out += "\n"; - - for (let y = 0; y < board.length; ++y) { - out += ` ${board.length - y} `.substr(-3); - for (let x = 0; x < board[y].length; ++x) { - let c = ""; - if (board[y][x] === 0) { - c = "."; - } else if (board[y][x] === 1) { - c = "B"; - } else if (board[y][x] === 2) { - c = "W"; - } else { - c = "?"; - } - out += colorizeIntersection(c); - } - - out += " " + ` ${board.length - y} `.substr(-3); - out += "\n"; - } - - out += " "; - for (let x = 0; x < board[0].length; ++x) { - out += `${x_coords[x]}`; - } - out += "\n"; - - out += "\n"; - debug(out); -} - -function colorizeIntersection(c: string): string { - if (c === "B" || c === "s") { - return black(c); - } else if (c === "W") { - return whiteBright(c); - } else if (c === "?") { - return red(c); - } else if (c === ".") { - return blue(c); - } else if (c === " " || c === "_") { - return blue("_"); - } - return yellow(c); -} - -function debug_print_settled(board: number[][]) { - let out = " "; - const x_coords = "ABCDEFGHJKLMNOPQRST"; // cspell: disable-line - - for (let x = 0; x < board[0].length; ++x) { - out += `${x_coords[x]}`; - } - out += "\n"; - - for (let y = 0; y < board.length; ++y) { - out += ` ${board.length - y} `.substr(-3); - for (let x = 0; x < board[y].length; ++x) { - out += colorizeIntersection(board[y][x] ? "s" : " "); - } - - out += " " + ` ${board.length - y} `.substr(-3); - out += "\n"; - } - - out += " "; - for (let x = 0; x < board[0].length; ++x) { - out += `${x_coords[x]}`; - } - out += "\n"; - - out += "\n"; - debug(out); -} - -function debug_groups(groups: GoStoneGroups) { - const group_map: string[][] = makeMatrix( - groups.group_id_map[0].length, - groups.group_id_map.length, - ) as any; - const symbols = "1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; - - let group_idx = 0; - - groups.foreachGroup((group) => { - let group_color = red; - - if (group.color === JGOFNumericPlayerColor.EMPTY) { - if (group.is_territory_in_seki) { - group_color = yellow; - } else if (group.is_territory) { - if (group.territory_color) { - group_color = - group.territory_color === JGOFNumericPlayerColor.BLACK ? black : white; - } else { - group_color = blue; - } - } else { - group_color = magenta; - } - } else if (group.color === JGOFNumericPlayerColor.BLACK) { - group_color = black; - } else if (group.color === JGOFNumericPlayerColor.WHITE) { - group_color = white; - } else { - group_color = red; - } - - const symbol = symbols[group_idx % symbols.length]; - - group.foreachStone((point) => { - group_map[point.y][point.x] = group_color(symbol); - }); - group_idx++; - }); - - debug("Group map:"); - debug("Legend: "); - debug(" " + black("Black") + " "); - debug(" " + white("White") + " "); - debug(" " + blue("Dame") + " "); - debug(" " + yellow("Territory in Seki") + " "); - debug(" " + magenta("Undecided territory") + " "); - debug(" " + red("Error") + " "); - - debug_group_map(group_map); -} - -function debug_group_map(board: string[][]) { - let out = " "; - const x_coords = "ABCDEFGHJKLMNOPQRST"; // cspell: disable-line - - for (let x = 0; x < board[0].length; ++x) { - out += `${x_coords[x]}`; - } - out += "\n"; - - for (let y = 0; y < board.length; ++y) { - out += ` ${board.length - y} `.substr(-3); - for (let x = 0; x < board[y].length; ++x) { - out += board[y][x]; - } - - out += " " + ` ${board.length - y} `.substr(-3); - out += "\n"; - } - - out += " "; - for (let x = 0; x < board[0].length; ++x) { - out += `${x_coords[x]}`; - } - out += "\n"; - - out += "\n"; - debug(out); -} - -function white(str: string) { - return `\x1b[37m${str}\x1b[0m`; -} -function red(str: string) { - return `\x1b[31m${str}\x1b[0m`; -} -/* -function green(str: string) { - return `\x1b[32m${str}\x1b[0m`; -} -*/ -function yellow(str: string) { - return `\x1b[33m${str}\x1b[0m`; -} -function blue(str: string) { - return `\x1b[34m${str}\x1b[0m`; -} -function magenta(str: string) { - return `\x1b[35m${str}\x1b[0m`; -} -/* -function cyan(str: string) { - return `\x1b[36m${str}\x1b[0m`; -} -*/ -function black(str: string) { - return `\x1b[30m${str}\x1b[0m`; -} -function whiteBright(str: string) { - return `\x1b[97m${str}\x1b[0m`; -} -function cyanBright(str: string) { - return `\x1b[96m${str}\x1b[0m`; -} -function blackBright(str: string) { - return `\x1b[90m${str}\x1b[0m`; -} diff --git a/src/engine/BoardState.ts b/src/engine/BoardState.ts new file mode 100644 index 00000000..0cd7d7ac --- /dev/null +++ b/src/engine/BoardState.ts @@ -0,0 +1,439 @@ +/* + * Copyright (C) Online-Go.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { GobanEvents } from "../GobanBase"; +import { EventEmitter } from "eventemitter3"; +import { JGOFIntersection, JGOFNumericPlayerColor } from "./formats/JGOF"; +import { makeMatrix } from "./util"; +import * as goscorer from "goscorer"; +import { StoneStringBuilder } from "./StoneStringBuilder"; +import type { GobanBase } from "../GobanBase"; +import { RawStoneString } from "./StoneString"; +import { cloneMatrix, matricesAreEqual } from "./util"; +import { callbacks } from "../Goban/callbacks"; + +export interface BoardConfig { + width?: number; + height?: number; + board?: JGOFNumericPlayerColor[][]; + removal?: boolean[][]; + player?: JGOFNumericPlayerColor; + board_is_repeating?: boolean; + white_prisoners?: number; + black_prisoners?: number; + isobranch_hash?: string; + //udata_state?: any; +} + +export interface ScoringLocations { + black: { + territory: number; + stones: number; + locations: JGOFIntersection[]; + }; + white: { + territory: number; + stones: number; + locations: JGOFIntersection[]; + }; +} + +/* When flood filling we use these to keep track of locations we've visited */ +let __current_flood_fill_value = 0; +const __flood_fill_scratch_pad: number[] = Array(25 * 25).fill(0); + +export class BoardState extends EventEmitter implements BoardConfig { + public readonly height: number = 19; + //public readonly rules:GobanEngineRules = 'japanese'; + public readonly width: number = 19; + public board: JGOFNumericPlayerColor[][]; + public removal: boolean[][]; + protected goban_callback?: GobanBase; + + public player: JGOFNumericPlayerColor; + public board_is_repeating: boolean; + public white_prisoners: number; + public black_prisoners: number; + + /** + * Constructs a new board with the given configuration. If height/width + * are not provided, they will be inferred from the board array, or will + * default to 19x19 if no board is provided. + * + * Any state matrices (board, removal, etc..) provided will be cloned + * and must have the same dimensionality. + */ + constructor(config: BoardConfig, goban_callback?: GobanBase) { + super(); + + this.goban_callback = goban_callback; + this.width = config.width ?? config.board?.[0]?.length ?? 19; + this.height = config.height ?? config.board?.length ?? 19; + + /* Clone our boards if they are provided, otherwise make new ones */ + this.board = config.board + ? cloneMatrix(config.board) + : makeMatrix(this.width, this.height, JGOFNumericPlayerColor.EMPTY); + this.removal = config.removal + ? cloneMatrix(config.removal) + : makeMatrix(this.width, this.height, false); + + /* Sanity check */ + if (this.height !== this.board.length || this.width !== this.board[0].length) { + throw new Error("Board size mismatch"); + } + + if (this.height !== this.removal.length || this.width !== this.removal[0].length) { + throw new Error("Removal size mismatch"); + } + + this.player = config.player ?? JGOFNumericPlayerColor.EMPTY; + this.board_is_repeating = config.board_is_repeating ?? false; + this.white_prisoners = config.white_prisoners ?? 0; + this.black_prisoners = config.black_prisoners ?? 0; + } + + /** Clone the entire BoardState */ + public cloneBoardState(): BoardState { + return new BoardState(this, this.goban_callback); + } + + /** Returns a clone of .board */ + public cloneBoard(): JGOFNumericPlayerColor[][] { + return this.board.map((row) => row.slice()); + } + + /** + * Toggles a group of stones for removal or restoration. + * + * By default, if we are marking a group for removal but the group is + * almost certainly alive (two eyes, etc), this will result in a no-op, + * unless force_removal is set to true. + */ + public toggleSingleGroupRemoval( + x: number, + y: number, + force_removal: boolean = false, + ): { + removed: boolean; + group: RawStoneString; + } { + const empty = { removed: false, group: [] }; + if (x < 0 || y < 0) { + return empty; + } + + try { + if (x >= 0 && y >= 0) { + const removing = !this.removal[y][x]; + const group_color = this.board[y][x]; + + if (group_color === JGOFNumericPlayerColor.EMPTY) { + /* Nothing to toggle. Note: we used to allow allow specific marking of + * dame by "removing" empty locations, however now we let our scoring + * engine figure dame out and if we need to communicate dame, we use + * the score drawing functionality */ + return empty; + } + + const groups = new StoneStringBuilder(this, this.board); + const selected_group = groups.getGroup(x, y); + + /* If we're clicking on a group, do a sanity check to see if we think + * there is a very good chance that the group is actually definitely alive. + * If so, refuse to remove it, unless a player has instructed us to forcefully + * remove it. */ + if (removing && !force_removal) { + const scores = goscorer.territoryScoring( + this.board, + this.removal as any, + false, + ); + let total_territory_adjacency_count = 0; + let total_territory_group_count = 0; + selected_group.foreachNeighboringEmptyString((gr) => { + let is_territory_group = false; + gr.map((pt) => { + if ( + scores[pt.y][pt.x].isTerritoryFor === this.board[y][x] && + !scores[pt.y][pt.x].isFalseEye + ) { + is_territory_group = true; + } + }); + + if (is_territory_group) { + total_territory_group_count += 1; + total_territory_adjacency_count += gr.intersections.length; + } + }); + if (total_territory_adjacency_count >= 5 || total_territory_group_count >= 2) { + console.log("This group is almost assuredly alive, refusing to remove"); + callbacks.toast?.("refusing_to_remove_group_is_alive", 4000); + return empty; + } + } + + /* Otherwise, if it might be fine to mark as dead, or we are restoring the + * stone string, or we are forcefully removing the group, do the marking. + */ + selected_group.map(({ x, y }) => this.setRemoved(x, y, removing, false)); + + this.emit("stone-removal.updated"); + return { removed: removing, group: selected_group.intersections }; + } + } catch (err) { + console.log(err.stack); + } + + return empty; + } + + /** Sets a position as being removed or not removed. If + * `emit_stone_removal_updated` is set to false, the + * "stone-removal.updated" event will not be emitted, and it is up to the + * caller to emit this event appropriately. + */ + public setRemoved( + x: number, + y: number, + removed: boolean, + emit_stone_removal_updated: boolean = true, + ): void { + if (x < 0 || y < 0) { + return; + } + if (x > this.width || y > this.height) { + return; + } + this.removal[y][x] = removed; + if (this.goban_callback) { + this.goban_callback.setForRemoval(x, y, this.removal[y][x], emit_stone_removal_updated); + } + } + + /** Clear all stone removals */ + public clearRemoved(): void { + let updated = false; + for (let y = 0; y < this.height; ++y) { + for (let x = 0; x < this.width; ++x) { + if (this.removal[y][x]) { + updated = true; + this.setRemoved(x, y, false, false); + } + } + } + if (updated) { + this.emit("stone-removal.updated"); + } + } + + /** + * Returns an array of groups connected to the given group. This is a bit + * faster than using StoneGroupBuilder because we only compute the values + * we need. + */ + public getNeighboringRawStoneStrings(raw_stone_string: RawStoneString): RawStoneString[] { + const gr = raw_stone_string; + ++__current_flood_fill_value; + this._floodFillMarkFilled(raw_stone_string); + const ret: Array = []; + this.foreachNeighbor(raw_stone_string, (x, y) => { + if (this.board[y][x]) { + ++__current_flood_fill_value; + this._floodFillMarkFilled(gr); + for (let i = 0; i < ret.length; ++i) { + this._floodFillMarkFilled(ret[i]); + } + const g = this.getRawStoneString(x, y, false); + if (g.length) { + /* can be zero if the piece has already been marked */ + ret.push(g); + } + } + }); + return ret; + } + + /** Returns an array of x/y pairs of all the same color */ + public getRawStoneString(x: number, y: number, clearMarks: boolean): RawStoneString { + const color = this.board[y][x]; + if (clearMarks) { + ++__current_flood_fill_value; + } + const toCheckX = [x]; + const toCheckY = [y]; + const ret = []; + while (toCheckX.length) { + x = toCheckX.pop() || 0; + y = toCheckY.pop() || 0; + + if (__flood_fill_scratch_pad[y * this.width + x] === __current_flood_fill_value) { + continue; + } + __flood_fill_scratch_pad[y * this.width + x] = __current_flood_fill_value; + + if (this.board[y][x] === color) { + const pt = { x: x, y: y }; + ret.push(pt); + this.foreachNeighbor(pt, addToCheck); + } + } + function addToCheck(x: number, y: number): void { + toCheckX.push(x); + toCheckY.push(y); + } + + return ret; + } + + private _floodFillMarkFilled(group: RawStoneString): void { + for (let i = 0; i < group.length; ++i) { + __flood_fill_scratch_pad[group[i].y * this.width + group[i].x] = + __current_flood_fill_value; + } + } + public countLiberties(raw_stone_string: RawStoneString): number { + let ct = 0; + const mat = makeMatrix(this.width, this.height, 0); + const counter = (x: number, y: number) => { + if (this.board[y][x] === 0 && mat[y][x] === 0) { + mat[y][x] = 1; + ct += 1; + } + }; + for (let i = 0; i < raw_stone_string.length; ++i) { + this.foreachNeighbor(raw_stone_string[i], counter); + } + return ct; + } + + public foreachNeighbor( + pt_or_raw_stone_string: JGOFIntersection | RawStoneString, + callback: (x: number, y: number) => void, + ): void { + if (pt_or_raw_stone_string instanceof Array) { + const group = pt_or_raw_stone_string; + const callback_done = new Array(this.height * this.width); + for (let i = 0; i < group.length; ++i) { + callback_done[group[i].x + group[i].y * this.width] = true; + } + + /* We only want to call the callback once per point */ + const callback_one_time = (x: number, y: number) => { + const idx = x + y * this.width; + if (callback_done[idx]) { + return; + } + callback_done[idx] = true; + callback(x, y); + }; + + for (let i = 0; i < group.length; ++i) { + const pt = group[i]; + if (pt.x - 1 >= 0) { + callback_one_time(pt.x - 1, pt.y); + } + if (pt.x + 1 !== this.width) { + callback_one_time(pt.x + 1, pt.y); + } + if (pt.y - 1 >= 0) { + callback_one_time(pt.x, pt.y - 1); + } + if (pt.y + 1 !== this.height) { + callback_one_time(pt.x, pt.y + 1); + } + } + } else { + const pt = pt_or_raw_stone_string; + if (pt.x - 1 >= 0) { + callback(pt.x - 1, pt.y); + } + if (pt.x + 1 !== this.width) { + callback(pt.x + 1, pt.y); + } + if (pt.y - 1 >= 0) { + callback(pt.x, pt.y - 1); + } + if (pt.y + 1 !== this.height) { + callback(pt.x, pt.y + 1); + } + } + } + + /** Returns true if the `.board` field from the other board is equal to this one */ + public boardEquals(other: BoardState): boolean { + return matricesAreEqual(this.board, other.board); + } + + /** + * Computes scoring locations for the board. If `area_scoring` is true, we + * will use area scoring rules, otherwise we will use territory scoring rules + * (which implies omitting territory in seki). + */ + public computeScoringLocations(area_scoring: boolean): ScoringLocations { + const ret: ScoringLocations = { + black: { + territory: 0, + stones: 0, + locations: [], + }, + white: { + territory: 0, + stones: 0, + locations: [], + }, + }; + + if (area_scoring) { + const scoring = goscorer.areaScoring(this.board, this.removal); + for (let y = 0; y < this.height; ++y) { + for (let x = 0; x < this.width; ++x) { + if (scoring[y][x] === goscorer.BLACK) { + if (this.board[y][x] === JGOFNumericPlayerColor.BLACK) { + ret.black.stones += 1; + } else { + ret.black.territory += 1; + } + ret.black.locations.push({ x, y }); + } else if (scoring[y][x] === goscorer.WHITE) { + if (this.board[y][x] === JGOFNumericPlayerColor.WHITE) { + ret.white.stones += 1; + } else { + ret.white.territory += 1; + } + ret.white.locations.push({ x, y }); + } + } + } + } else { + const scoring = goscorer.territoryScoring(this.board, this.removal); + for (let y = 0; y < this.height; ++y) { + for (let x = 0; x < this.width; ++x) { + if (scoring[y][x].isTerritoryFor === goscorer.BLACK) { + ret.black.territory += 1; + ret.black.locations.push({ x, y }); + } else if (scoring[y][x].isTerritoryFor === goscorer.WHITE) { + ret.white.territory += 1; + ret.white.locations.push({ x, y }); + } + } + } + } + + return ret; + } +} diff --git a/src/GoConditionalMove.ts b/src/engine/ConditionalMoveTree.ts similarity index 68% rename from src/GoConditionalMove.ts rename to src/engine/ConditionalMoveTree.ts index 8539715b..c471e16d 100644 --- a/src/GoConditionalMove.ts +++ b/src/engine/ConditionalMoveTree.ts @@ -21,51 +21,51 @@ export type ConditionalMoveResponse = [ string | null, /** next move tree */ - ConditionalMoveTree, + ConditionalMoveResponseTree, ]; -export interface ConditionalMoveTree { +export interface ConditionalMoveResponseTree { [move: string]: ConditionalMoveResponse; } -export class GoConditionalMove { +export class ConditionalMoveTree { children: { - [move: string]: GoConditionalMove; + [move: string]: ConditionalMoveTree; }; - parent?: GoConditionalMove; + parent?: ConditionalMoveTree; move: string | null; - constructor(move: string | null, parent?: GoConditionalMove) { + constructor(move: string | null, parent?: ConditionalMoveTree) { this.move = move; this.parent = parent; this.children = {}; } encode(): ConditionalMoveResponse { - const ret: ConditionalMoveTree = {}; + const ret: ConditionalMoveResponseTree = {}; for (const ch in this.children) { ret[ch] = this.children[ch].encode(); } return [this.move, ret]; } - static decode(data: ConditionalMoveResponse): GoConditionalMove { + static decode(data: ConditionalMoveResponse): ConditionalMoveTree { const move = data[0]; const children = data[1]; - const ret = new GoConditionalMove(move); + const ret = new ConditionalMoveTree(move); for (const ch in children) { - const child = GoConditionalMove.decode(children[ch]); + const child = ConditionalMoveTree.decode(children[ch]); child.parent = ret; ret.children[ch] = child; } return ret; } - getChild(mv: string): GoConditionalMove { + getChild(mv: string): ConditionalMoveTree { if (mv in this.children) { return this.children[mv]; } - return new GoConditionalMove(null, this); + return new ConditionalMoveTree(null, this); } - duplicate(): GoConditionalMove { - return GoConditionalMove.decode(this.encode()); + duplicate(): ConditionalMoveTree { + return ConditionalMoveTree.decode(this.encode()); } } diff --git a/src/GoEngine.ts b/src/engine/GobanEngine.ts similarity index 77% rename from src/GoEngine.ts rename to src/engine/GobanEngine.ts index ba0121f1..b07cfb5a 100644 --- a/src/GoEngine.ts +++ b/src/engine/GobanEngine.ts @@ -14,25 +14,34 @@ * limitations under the License. */ +import { BoardState, BoardConfig } from "./BoardState"; import { GobanMoveError } from "./GobanError"; import { MoveTree, MoveTreeJson } from "./MoveTree"; -import { Move, Intersection, encodeMove } from "./GoMath"; -import * as GoMath from "./GoMath"; -import { Group } from "./GoStoneGroup"; -import { GoStoneGroups } from "./GoStoneGroups"; +import { + decodeMoves, + decodePrettyCoordinates, + encodeMove, + encodeMoves, + positionId, + prettyCoordinates, + sortMoves, +} from "./util"; +import { RawStoneString } from "./StoneString"; import { ScoreEstimator } from "./ScoreEstimator"; -import { GobanCore, Events } from "./GobanCore"; +import { GobanBase, GobanEvents } from "../GobanBase"; import { JGOFTimeControl, JGOFNumericPlayerColor, JGOFMove, JGOFPlayerSummary, JGOFIntersection, -} from "./JGOF"; -import { AdHocPackedMove } from "./AdHocFormat"; + JGOFSealingIntersection, +} from "./formats/JGOF"; +import { AdHocPackedMove } from "./formats/AdHocFormat"; import { _ } from "./translate"; import { EventEmitter } from "eventemitter3"; import { GameClock, StallingScoreEstimate } from "./protocol"; +import * as goscorer from "goscorer"; declare const CLIENT: boolean; declare const SERVER: boolean; @@ -40,9 +49,9 @@ declare const SERVER: boolean; export const AUTOSCORE_TRIALS = 1000; export const AUTOSCORE_TOLERANCE = 0.1; -export type GoEnginePhase = "play" | "stone removal" | "finished"; -export type GoEngineRules = "chinese" | "aga" | "japanese" | "korean" | "ing" | "nz"; -export type GoEngineSuperKoAlgorithm = +export type GobanEnginePhase = "play" | "stone removal" | "finished"; +export type GobanEngineRules = "chinese" | "aga" | "japanese" | "korean" | "ing" | "nz"; +export type GobanEngineSuperKoAlgorithm = | "psk" | "csk" | "ssk" @@ -63,21 +72,7 @@ export interface Score { black: PlayerScore; } -export interface GoEngineState { - player: JGOFNumericPlayerColor; - board_is_repeating: boolean; - white_prisoners: number; - black_prisoners: number; - board: Array>; - isobranch_hash?: string; - - /** User data state, the Goban's usually want to store some state in here, which is - * obtained and set by calling the getState_callback - */ - udata_state: any; -} - -export interface GoEnginePlayerEntry { +export interface GobanEnginePlayerEntry { id: number; username: string; country?: string; @@ -97,7 +92,7 @@ export interface GoEnginePlayerEntry { // The word "array" is deliberately included in the type name to differentiate from a move tree. export type GobanMovesArray = Array | Array; -export interface GoEngineConfig { +export interface GobanEngineConfig extends BoardConfig { game_id?: number | string; review_id?: number; game_name?: string; @@ -112,24 +107,24 @@ export interface GoEngineConfig { handicap_rank_difference?: number; handicap?: number; komi?: number; - rules?: GoEngineRules; - phase?: GoEnginePhase; - initial_state?: GoEngineInitialState; + rules?: GobanEngineRules; + phase?: GobanEnginePhase; + initial_state?: GobanEngineInitialState; marks?: { [mark: string]: string }; latencies?: { [player_id: string]: number }; - player_pool?: { [id: number]: GoEnginePlayerEntry }; // we need this to get player details from player_id in player_update events + player_pool?: { [id: number]: GobanEnginePlayerEntry }; // we need this to get player details from player_id in player_update events players?: { - black: GoEnginePlayerEntry; - white: GoEnginePlayerEntry; + black: GobanEnginePlayerEntry; + white: GobanEnginePlayerEntry; }; rengo?: boolean; rengo_teams?: { - black: Array; - white: Array; + black: Array; + white: Array; }; rengo_casual_mode?: boolean; reviews?: { - [review_id: number]: GoEnginePlayerEntry; + [review_id: number]: GobanEnginePlayerEntry; }; time_control?: JGOFTimeControl; @@ -161,20 +156,27 @@ export interface GoEngineConfig { white_must_pass_last?: boolean; aga_handicap_scoring?: boolean; opponent_plays_first_after_resume?: boolean; - superko_algorithm?: GoEngineSuperKoAlgorithm; + superko_algorithm?: GobanEngineSuperKoAlgorithm; stalling_score_estimate?: StallingScoreEstimate; // This is used in gtp2ogs clock?: GameClock; - /** When loading initial state or moves, by default GoEngine will try and + /** When loading initial state or moves, by default GobanEngine will try and * handle bad data by just resorting to 'edit placing' moves. If this is * true, then those errors are thrown instead. */ throw_all_errors?: boolean; - /** Removed stones in stone removal phase */ - removed?: string; + /** Removed stones in stone removal phase + * Passing an array of JGOFMove objects is preferred, the string + * format exists for historical backwards compatibility. It is an + * encoded move string, e.g. "aa" for A19 + */ + removed?: string | JGOFMove[]; + + /** Intersections that need to be sealed before scoring should happen */ + needs_sealing?: JGOFSealingIntersection[]; // this is weird, we should migrate away from this ogs?: { @@ -201,7 +203,7 @@ export interface GoEngineConfig { white_player_id?: number; } -export interface GoEngineInitialState { +export interface GobanEngineInitialState { black?: string; white?: string; } @@ -255,21 +257,19 @@ export interface ReviewMessage { /** Sets the owner of the review */ "owner"?: number | { id: number; username: string }; /** Initial gamedata to review */ - "gamedata"?: GoEngineConfig; + "gamedata"?: GobanEngineConfig; /** Sets the controller of the review */ "controller"?: number | { id: number; username: string }; /** Updated information about the players, such as name etc. */ "player_update"?: JGOFPlayerSummary; } -export interface PuzzleConfig { +export interface PuzzleConfig extends BoardConfig { //mode: "puzzle"; mode?: string; name?: string; puzzle_type?: string; - width?: number; - height?: number; - initial_state?: GoEngineInitialState; + initial_state?: GobanEngineInitialState; marks?: { [mark: string]: string }; puzzle_autoplace_delay?: number; puzzle_opponent_move_mode?: PuzzleOpponentMoveMode; @@ -289,20 +289,16 @@ export type PuzzlePlacementSetting = | { mode: "setup"; color: JGOFNumericPlayerColor } | { mode: "place"; color: 0 }; -let __currentMarker = 0; - export type PlayerColor = "black" | "white"; -export class GoEngine extends EventEmitter { +export class GobanEngine extends BoardState { //public readonly players.black.id:number; //public readonly players.white.id:number; public throw_all_errors?: boolean; - public board: Array>; //public cur_review_move?: MoveTree; - public getState_callback?: () => any; public handicap_rank_difference?: number; public handicap: number = NaN; - public initial_state: GoEngineInitialState = { black: "", white: "" }; + public initial_state: GobanEngineInitialState = { black: "", white: "" }; public komi: number = NaN; public move_tree: MoveTree; public move_tree_layout_vector: Array = @@ -311,12 +307,11 @@ export class GoEngine extends EventEmitter { {}; /* For use by MoveTree layout and rendering */ public move_tree_layout_dirty: boolean = false; /* For use by MoveTree layout and rendering */ public readonly name: string = ""; - public player: JGOFNumericPlayerColor; - public player_pool: { [id: number]: GoEnginePlayerEntry }; + public player_pool: { [id: number]: GobanEnginePlayerEntry }; public latencies?: { [player_id: string]: number }; public players: { - black: GoEnginePlayerEntry; - white: GoEnginePlayerEntry; + black: GobanEnginePlayerEntry; + white: GobanEnginePlayerEntry; } = { black: { username: "black", id: NaN }, white: { username: "white", id: NaN }, @@ -327,13 +322,9 @@ export class GoEngine extends EventEmitter { public puzzle_player_move_mode: PuzzlePlayerMoveMode = "free"; public puzzle_rank: number = NaN; public puzzle_type: string = "[missing puzzle type]"; - public readonly config: GoEngineConfig; + public readonly config: GobanEngineConfig; public readonly disable_analysis: boolean = false; - public readonly height: number = 19; - //public readonly rules:GoEngineRules = 'japanese'; - public readonly width: number = 19; - public removal: Array>; - public setState_callback?: (state: any) => void; + //public readonly rules:GobanEngineRules = 'japanese'; public time_control: JGOFTimeControl = { system: "none", speed: "correspondence", @@ -341,22 +332,22 @@ export class GoEngine extends EventEmitter { }; public game_id: number = NaN; public review_id?: number; - public decoded_moves: Array = []; + public decoded_moves: Array = []; public automatic_stone_removal: boolean = false; public group_ids?: Array; public rengo?: boolean; public rengo_teams?: { - [colour: string]: Array; // TBD index this by PlayerColour + [colour: string]: Array; // TBD index this by PlayerColour }; public rengo_casual_mode: boolean; public stalling_score_estimate?: StallingScoreEstimate; /* Properties that emit change events */ - private _phase: GoEnginePhase = "play"; - public get phase(): GoEnginePhase { + private _phase: GobanEnginePhase = "play"; + public get phase(): GobanEnginePhase { return this._phase; } - public set phase(phase: GoEnginePhase) { + public set phase(phase: GobanEnginePhase) { if (this._phase === phase) { return; } @@ -412,11 +403,11 @@ export class GoEngine extends EventEmitter { this.emit("strict_seki_mode", this.strict_seki_mode); } - private _rules: GoEngineRules = "japanese"; // can't be readonly at this point since parseSGF sets it - public get rules(): GoEngineRules { + private _rules: GobanEngineRules = "japanese"; // can't be readonly at this point since parseSGF sets it + public get rules(): GobanEngineRules { return this._rules; } - public set rules(rules: GoEngineRules) { + public set rules(rules: GobanEngineRules) { if (this._rules === rules) { return; } @@ -466,16 +457,12 @@ export class GoEngine extends EventEmitter { private allow_ko: boolean = false; private allow_self_capture: boolean = false; private allow_superko: boolean = false; - private superko_algorithm: GoEngineSuperKoAlgorithm = "psk"; - private black_prisoners: number = 0; - private white_prisoners: number = 0; - private board_is_repeating: boolean; - private goban_callback?: GobanCore; + private superko_algorithm: GobanEngineSuperKoAlgorithm = "psk"; private dontStoreBoardHistory: boolean; public free_handicap_placement: boolean = false; private loading_sgf: boolean = false; - private marks: Array>; private move_before_jump?: MoveTree; + public needs_sealing?: Array; //private mv:Move; public score_prisoners: boolean = false; public score_stones: boolean = false; @@ -485,23 +472,31 @@ export class GoEngine extends EventEmitter { public territory_included_in_sgf: boolean = false; constructor( - config: GoEngineConfig, - goban_callback?: GobanCore, + config: GobanEngineConfig, + goban_callback?: GobanBase, dontStoreBoardHistory?: boolean, ) { - super(); - try { - /* We had a bug where we were filling in some initial state data incorrectly when we were dealing with - * sgfs, so this code exists for sgf 'games' < 800k in the database.. -anoek 2014-08-13 */ - if ("original_sgf" in config) { - config.initial_state = { black: "", white: "" }; - } - } catch (e) { - console.log(e); - } - - GoEngine.normalizeConfig(config); - GoEngine.fillDefaults(config); + super( + GobanEngine.fillDefaults( + GobanEngine.migrateConfig( + ((config: GobanEngineConfig): GobanEngineConfig => { + /* We had a bug where we were filling in some initial state + * data incorrectly when we were dealing with sgfs, so this + * code exists for sgf 'games' < 800k in the database.. + * -anoek 2014-08-13 */ + try { + if ("original_sgf" in config) { + config.initial_state = { black: "", white: "" }; + } + } catch (e) { + console.log(e); + } + return config; + })(config), + ), + ), + goban_callback, + ); for (const k in config) { if (k !== "move_tree") { @@ -518,9 +513,6 @@ export class GoEngine extends EventEmitter { this.goban_callback = goban_callback; this.goban_callback.engine = this; } - this.board = []; - this.removal = []; - this.marks = []; this.white_prisoners = 0; this.black_prisoners = 0; this.board_is_repeating = false; @@ -532,20 +524,6 @@ export class GoEngine extends EventEmitter { this.rengo_casual_mode = config.rengo_casual_mode || false; - for (let y = 0; y < this.height; ++y) { - const row: Array = []; - const mark_row = []; - const removal_row: Array<-1 | 0 | 1> = []; - for (let x = 0; x < this.width; ++x) { - row.push(0); - mark_row.push(0); - removal_row.push(0); - } - this.board.push(row); - this.marks.push(mark_row); - this.removal.push(removal_row); - } - try { this.config.original_disable_analysis = this.config.disable_analysis; if ( @@ -658,7 +636,7 @@ export class GoEngine extends EventEmitter { this.cur_move.player === JGOFNumericPlayerColor.BLACK ? "black" : "white" - } at ${this.prettyCoords(mv.x, mv.y)} (${mv.x}, ${mv.y})`, + } at ${this.prettyCoordinates(mv.x, mv.y)} (${mv.x}, ${mv.y})`, stack: e.stack, }); console.log(config.errors[config.errors.length - 1]); @@ -682,6 +660,16 @@ export class GoEngine extends EventEmitter { } this.emit("stone-removal.updated"); } + if (config.needs_sealing) { + this.needs_sealing = config.needs_sealing; + if (this.phase === "stone removal") { + for (const intersection of config.needs_sealing) { + this.setNeedsSealing(intersection.x, intersection.y, true); + } + } + + this.emit("stone-removal.needs-sealing", config.needs_sealing); + } function unpackMoveTree(cur: MoveTree, tree: MoveTreeJson): void { cur.loadJsonForThisNode(tree); @@ -703,40 +691,65 @@ export class GoEngine extends EventEmitter { } } + /** + * Decodes any of the various ways we express moves that we've accumulated over the years into + * a unified `JGOFMove[]`. + */ public decodeMoves( - move_obj: AdHocPackedMove | string | Array | [object] | Array, - ): Array { - return GoMath.decodeMoves(move_obj, this.width, this.height); - } - private getState(): GoEngineState { - const state: GoEngineState = { - player: this.player, - board_is_repeating: this.board_is_repeating, - white_prisoners: this.white_prisoners, - black_prisoners: this.black_prisoners, - udata_state: this.getState_callback ? this.getState_callback() : null, - board: new Array(this.height), - }; + move_obj: + | string + | AdHocPackedMove + | AdHocPackedMove[] + | JGOFMove + | JGOFMove[] + | [object] + | undefined, + ): JGOFMove[] { + return decodeMoves(move_obj, this.width, this.height); + } - for (let y = 0; y < this.height; ++y) { - const row = new Array(this.width); - for (let x = 0; x < this.width; ++x) { - row[x] = this.board[y][x]; - } - state.board[y] = row; + /* Encodes a move list like `[{x: 0, y: 0}, {x:1, y:2}]` into our move string + * format `"aabc"` */ + public encodeMoves(lst: JGOFMove[]): string { + return encodeMoves(lst); + } + + /* Encodes a single move `{x:1, y:2}` into our move string + * format `"bc"` */ + public encodeMove(lst: JGOFMove): string { + return encodeMoves([lst]); + } + + /** + * Decodes a move string like `"A11"` into a move object like `{x: 0, y: 10}`. Also + * handles the special cases like `".."` and "pass" which map to `{x: -1, y: -1}`. + */ + public decodePrettyCoordinates(coordinates: string): JGOFMove { + return decodePrettyCoordinates(coordinates, this.height); + } + + /** Encodes an x,y pair or a move object like {x: 0, y: 0} into a move string like "A1" */ + public prettyCoordinates(x: JGOFMove): string; + public prettyCoordinates(x: number, y: number): string; + public prettyCoordinates(x: number | JGOFMove, y?: number): string { + if (typeof x !== "number") { + y = x.y; + x = x.x; } + return prettyCoordinates(x, y as number, this.height); + } - return state; + private getState(): BoardState { + return this.cloneBoardState(); } - private setState(state: GoEngineState): GoEngineState { + private setState(state: BoardState): BoardState { this.player = state.player; this.white_prisoners = state.white_prisoners; this.black_prisoners = state.black_prisoners; this.board_is_repeating = state.board_is_repeating; - if (this.setState_callback) { - this.setState_callback(state.udata_state); - } + //this.goban_callback?.setState(state.udata_state); + this.goban_callback?.setState?.(); const redrawn: { [s: string]: boolean } = {}; @@ -757,37 +770,9 @@ export class GoEngine extends EventEmitter { return state; } - public boardMatricesAreTheSame( - m1: Array>, - m2: Array>, - ): boolean { - if (m1.length !== m2.length || m1[0].length !== m2[0].length) { - return false; - } - - for (let y = 0; y < m1.length; ++y) { - for (let x = 0; x < m1[0].length; ++x) { - if (m1[y][x] !== m2[y][x]) { - return false; - } - } - } - return true; - } - private boardStatesAreTheSame(state1: GoEngineState, state2: GoEngineState): boolean { - for (let y = 0; y < this.height; ++y) { - for (let x = 0; x < this.width; ++x) { - if (state1.board[y][x] !== state2.board[y][x]) { - return false; - } - } - } - - return true; - } public currentPositionId(): string { - return GoMath.positionId(this.board, this.height, this.width); + return positionId(this.board, this.height, this.width); } public followPath( @@ -967,7 +952,7 @@ export class GoEngine extends EventEmitter { public getMoveDiff(): { from: number; moves: string } { const branch_point = this.cur_move.getBranchPoint(); let cur: MoveTree | null = this.cur_move; - const moves: Array = []; + const moves: JGOFMove[] = []; while (cur && cur.id !== branch_point.id) { moves.push({ @@ -980,7 +965,7 @@ export class GoEngine extends EventEmitter { } moves.reverse(); - return { from: branch_point.getMoveIndex(), moves: GoMath.encodeMoves(moves) }; + return { from: branch_point.getMoveIndex(), moves: encodeMoves(moves) }; } public setAsCurrentReviewMove(): void { if (this.dontStoreBoardHistory) { @@ -1048,167 +1033,8 @@ export class GoEngine extends EventEmitter { private opponent(): JGOFNumericPlayerColor { return this.player === 1 ? 2 : 1; } - public prettyCoords(x: number, y: number): string { - return GoMath.prettyCoords(x, y, this.height); - } - private incrementCurrentMarker(): void { - ++__currentMarker; - } - private markGroup(group: Group): void { - for (let i = 0; i < group.length; ++i) { - this.marks[group[i].y][group[i].x] = __currentMarker; - } - } - private foreachNeighbor_checkAndDo( - x: number, - y: number, - done_array: Array, - fn_of_neighbor_pt: (x: number, y: number) => void, - ): void { - const idx = x + y * this.width; - if (done_array[idx]) { - return; - } - done_array[idx] = true; - fn_of_neighbor_pt(x, y); - } - - public foreachNeighbor( - pt_or_group: Intersection | Group, - fn_of_neighbor_pt: (x: number, y: number) => void, - ): void { - if (pt_or_group instanceof Array) { - const group = pt_or_group; - const done_array = new Array(this.height * this.width); - for (let i = 0; i < group.length; ++i) { - done_array[group[i].x + group[i].y * this.width] = true; - } - - for (let i = 0; i < group.length; ++i) { - const pt = group[i]; - if (pt.x - 1 >= 0) { - this.foreachNeighbor_checkAndDo(pt.x - 1, pt.y, done_array, fn_of_neighbor_pt); - } - if (pt.x + 1 !== this.width) { - this.foreachNeighbor_checkAndDo(pt.x + 1, pt.y, done_array, fn_of_neighbor_pt); - } - if (pt.y - 1 >= 0) { - this.foreachNeighbor_checkAndDo(pt.x, pt.y - 1, done_array, fn_of_neighbor_pt); - } - if (pt.y + 1 !== this.height) { - this.foreachNeighbor_checkAndDo(pt.x, pt.y + 1, done_array, fn_of_neighbor_pt); - } - } - } else { - const pt = pt_or_group; - if (pt.x - 1 >= 0) { - fn_of_neighbor_pt(pt.x - 1, pt.y); - } - if (pt.x + 1 !== this.width) { - fn_of_neighbor_pt(pt.x + 1, pt.y); - } - if (pt.y - 1 >= 0) { - fn_of_neighbor_pt(pt.x, pt.y - 1); - } - if (pt.y + 1 !== this.height) { - fn_of_neighbor_pt(pt.x, pt.y + 1); - } - } - } - /** Returns an array of x/y pairs of all the same color */ - private getGroup(x: number, y: number, clearMarks: boolean): Group { - const color = this.board[y][x]; - if (clearMarks) { - this.incrementCurrentMarker(); - } - const toCheckX = [x]; - const toCheckY = [y]; - const ret = []; - while (toCheckX.length) { - x = toCheckX.pop() || 0; - y = toCheckY.pop() || 0; - - if (this.marks[y][x] === __currentMarker) { - continue; - } - this.marks[y][x] = __currentMarker; - - if (this.board[y][x] === color) { - const pt = { x: x, y: y }; - ret.push(pt); - this.foreachNeighbor(pt, addToCheck); - } - } - function addToCheck(x: number, y: number): void { - toCheckX.push(x); - toCheckY.push(y); - } - - return ret; - } - /** Returns an array of groups connected to the given group */ - private getConnectedGroups(group: Group): Array { - const gr = group; - this.incrementCurrentMarker(); - this.markGroup(group); - const ret: Array = []; - this.foreachNeighbor(group, (x, y) => { - if (this.board[y][x]) { - this.incrementCurrentMarker(); - this.markGroup(gr); - for (let i = 0; i < ret.length; ++i) { - this.markGroup(ret[i]); - } - const g = this.getGroup(x, y, false); - if (g.length) { - /* can be zero if the piece has already been marked */ - ret.push(g); - } - } - }); - return ret; - } - private getConnectedOpenSpace(group: Group): Group { - const gr = group; - this.incrementCurrentMarker(); - this.markGroup(group); - const ret: Group = []; - const included: { [s: string]: boolean } = {}; - - this.foreachNeighbor(group, (x, y) => { - if (!this.board[y][x]) { - this.incrementCurrentMarker(); - this.markGroup(gr); - //for (let i = 0; i < ret.length; ++i) { - this.markGroup(ret); - //} - const g = this.getGroup(x, y, false); - for (let i = 0; i < g.length; ++i) { - if (!included[g[i].x + "," + g[i].y]) { - ret.push(g[i]); - included[g[i].x + "," + g[i].y] = true; - } - } - } - }); - return ret; - } - private countLiberties(group: Group): number { - let ct = 0; - const mat = GoMath.makeMatrix(this.width, this.height, 0); - const counter = (x: number, y: number) => { - if (this.board[y][x] === 0 && mat[y][x] === 0) { - mat[y][x] = 1; - ct += 1; - } - }; - for (let i = 0; i < group.length; ++i) { - this.foreachNeighbor(group[i], counter); - } - return ct; - } - private captureGroup(group: Group): number { + private captureGroup(group: RawStoneString): number { for (let i = 0; i < group.length; ++i) { const x = group[i].x; const y = group[i].y; @@ -1226,27 +1052,6 @@ export class GoEngine extends EventEmitter { return group.length; } - public computeLibertyMap(): Array> { - const liberties = GoMath.makeMatrix(this.width, this.height, 0); - if (!this.board) { - return liberties; - } - - for (let y = 0; y < this.height; ++y) { - for (let x = 0; x < this.width; ++x) { - if (this.board[y][x] && !liberties[y][x]) { - const group = this.getGroup(x, y, true); - const count = this.countLiberties(group); - for (const e of group) { - liberties[e.y][e.x] = count; - } - } - } - } - - return liberties; - } - public isParticipant(player_id: number): boolean { // Note: in theory we get participants from the engine each move, with the intention that we store and use here, // which would be more efficient, but needs careful consideration of timing and any other gotchas @@ -1315,7 +1120,7 @@ export class GoEngine extends EventEmitter { checkForKo?: boolean, errorOnSuperKo?: boolean, dontCheckForSuperKo?: boolean, - dontCheckForSuicide?: boolean, + dontCheckForSelfCapture?: boolean, isTrunkMove?: boolean, removed_stones?: Array, ): number { @@ -1328,7 +1133,7 @@ export class GoEngine extends EventEmitter { if (this.board[y][x] !== this.player) { console.log( "Invalid duplicate stone placement at " + - this.prettyCoords(x, y) + + this.prettyCoordinates(x, y) + " board color: " + this.board[y][x] + " placed color: " + @@ -1344,15 +1149,15 @@ export class GoEngine extends EventEmitter { throw new GobanMoveError( this.game_id || this.review_id || 0, this.cur_move?.move_number ?? -1, - this.prettyCoords(x, y), + this.prettyCoordinates(x, y), "stone_already_placed_here", ); } this.board[y][x] = this.player; - let suicide_move = false; - const player_group = this.getGroup(x, y, true); - const opponent_groups = this.getConnectedGroups(player_group); + let self_capture_move = false; + const player_group = this.getRawStoneString(x, y, true); + const opponent_groups = this.getNeighboringRawStoneStrings(player_group); for (let i = 0; i < opponent_groups.length; ++i) { if (this.countLiberties(opponent_groups[i]) === 0) { @@ -1364,31 +1169,27 @@ export class GoEngine extends EventEmitter { } if (pieces_removed === 0) { if (this.countLiberties(player_group) === 0) { - if (this.allow_self_capture || dontCheckForSuicide) { + if (this.allow_self_capture || dontCheckForSelfCapture) { pieces_removed += this.captureGroup(player_group); - suicide_move = true; + self_capture_move = true; } else { this.board[y][x] = 0; throw new GobanMoveError( this.game_id || this.review_id || 0, this.cur_move?.move_number ?? -1, - this.prettyCoords(x, y), - "move_is_suicidal", + this.prettyCoordinates(x, y), + "illegal_self_capture", ); } } } if (checkForKo && !this.allow_ko) { - const current_state = this.getState(); - if ( - !this.cur_move.edited && - this.boardStatesAreTheSame(current_state, this.cur_move.index(-1).state) - ) { + if (!this.cur_move.edited && this.boardEquals(this.cur_move.index(-1).state)) { throw new GobanMoveError( this.game_id || this.review_id || 0, this.cur_move?.move_number ?? -1, - this.prettyCoords(x, y), + this.prettyCoordinates(x, y), "illegal_ko_move", ); } @@ -1402,14 +1203,14 @@ export class GoEngine extends EventEmitter { throw new GobanMoveError( this.game_id || this.review_id || 0, this.cur_move?.move_number ?? -1, - this.prettyCoords(x, y), + this.prettyCoordinates(x, y), "illegal_board_repetition", ); } } } - if (!suicide_move) { + if (!self_capture_move) { if (this.goban_callback) { this.goban_callback.set(x, y, this.player); } @@ -1443,10 +1244,8 @@ export class GoEngine extends EventEmitter { return pieces_removed; } - public isBoardRepeating(superko_rule: GoEngineSuperKoAlgorithm): boolean { + public isBoardRepeating(superko_rule: GobanEngineSuperKoAlgorithm): boolean { const MAX_SUPERKO_SEARCH = 30; /* any more than this is probably a waste of time. This may be overkill even. */ - const current_state = this.getState(); - //var current_state = this.cur_move.state; const current_player_to_move = this.player; const check_situational = superko_rule === "ssk"; @@ -1458,7 +1257,7 @@ export class GoEngine extends EventEmitter { ) { if (t) { if (!check_situational || t.player === current_player_to_move) { - if (this.boardStatesAreTheSame(t.state, current_state)) { + if (this.boardEquals(t.state)) { return true; } } @@ -1524,7 +1323,7 @@ export class GoEngine extends EventEmitter { break; } } - this.initial_state.black = GoMath.encodeMoves(moves); + this.initial_state.black = encodeMoves(moves); moves = this.decodeMoves(this.initial_state?.white || ""); for (let i = 0; i < moves.length; ++i) { @@ -1533,7 +1332,7 @@ export class GoEngine extends EventEmitter { break; } } - this.initial_state.white = GoMath.encodeMoves(moves); + this.initial_state.white = encodeMoves(moves); /* Then add if applicable */ if (color) { @@ -1541,7 +1340,7 @@ export class GoEngine extends EventEmitter { this.initial_state[color === 1 ? "black" : "white"] || "", ); moves.push({ x: x, y: y, color: color }); - this.initial_state[color === 1 ? "black" : "white"] = GoMath.encodeMoves(moves); + this.initial_state[color === 1 ? "black" : "white"] = encodeMoves(moves); } } @@ -1585,139 +1384,10 @@ export class GoEngine extends EventEmitter { }; } - public toggleMetaGroupRemoval(x: number, y: number): Array<[0 | 1, Group]> { - try { - if (x >= 0 && y >= 0) { - const removing: 0 | 1 = !this.removal[y][x] ? 1 : 0; - const group = this.getGroup(x, y, true); - let removed_stones = this.setGroupForRemoval(x, y, removing, false)[1]; - const empty_spaces = []; - - const group_color = this.board[y][x]; - if (group_color === 0) { - /* just toggle open area */ - } else { - /* for stones though, toggle the selected stone group any any stone - * groups which are adjacent to it through open area */ - const already_done: { [str: string]: boolean } = {}; - - let space = this.getConnectedOpenSpace(group); - for (let i = 0; i < space.length; ++i) { - const pt = space[i]; - - if (already_done[pt.x + "," + pt.y]) { - continue; - } - already_done[pt.x + "," + pt.y] = true; - - if (this.board[pt.y][pt.x] === 0) { - const far_neighbors = this.getConnectedGroups([space[i]]); - for (let j = 0; j < far_neighbors.length; ++j) { - const fpt = far_neighbors[j][0]; - if (this.board[fpt.y][fpt.x] === group_color) { - const res = this.setGroupForRemoval( - fpt.x, - fpt.y, - removing, - false, - ); - removed_stones = removed_stones.concat(res[1]); - space = space.concat(this.getConnectedOpenSpace(res[1])); - } - } - empty_spaces.push(pt); - } - } - } - - this.emit("stone-removal.updated"); - - if (!removing) { - return [[removing, removed_stones]]; - } else { - return [ - [removing, removed_stones], - [!removing ? 1 : 0, empty_spaces], - ]; - } - } - } catch (err) { - console.log(err.stack); - } - - return [[0, []]]; - } - - /** Sets an entire group as being removed or not removed. If `emit_stone_removal_updated` - * is set to false, the "stone-removal.updated" event will not be emitted, and it is up - * to the caller to emit this event appropriately. - */ - private setGroupForRemoval( - x: number, - y: number, - toggle_set: -1 | 0 | 1, - emit_stone_removal_updated: boolean = true, - ): [-1 | 0 | 1, Group] { - /* - If toggle_set === -1, toggle the selection from marked / unmarked. - If toggle_set === 0, unmark the group for removal - If toggle_set === 1, mark the group for removal - - returns [removing 0/1, [group removed]]; - */ - - if (x >= 0 && y >= 0) { - const group = this.getGroup(x, y, true); - const removing = toggle_set === -1 ? (!this.removal[y][x] ? 1 : 0) : toggle_set; - - for (let i = 0; i < group.length; ++i) { - const x = group[i].x; - const y = group[i].y; - this.setRemoved(x, y, removing, emit_stone_removal_updated); - } - - return [removing, group]; - } - return [0, []]; - } - - /** Sets a position as being removed or not removed. If `emit_stone_removal_updated` is set to - * false, the "stone-removal.updated" event will not be emitted, and it is up to the caller - * to emit this event appropriately. - */ - public setRemoved( - x: number, - y: number, - removed: boolean | 0 | 1, - emit_stone_removal_updated: boolean = true, - ): void { - if (x < 0 || y < 0) { - return; - } - if (x > this.width || y > this.height) { - return; - } - this.removal[y][x] = removed ? 1 : 0; - if (this.goban_callback) { - this.goban_callback.setForRemoval(x, y, this.removal[y][x], emit_stone_removal_updated); - } + public setNeedsSealing(x: number, y: number, needs_sealing?: boolean): void { + this.cur_move.getMarks(x, y).needs_sealing = needs_sealing; } - public clearRemoved(): void { - let updated = false; - - for (let y = 0; y < this.height; ++y) { - for (let x = 0; x < this.width; ++x) { - if (this.removal[y][x]) { - updated = true; - this.setRemoved(x, y, 0, false); - } - } - } - if (updated) { - this.emit("stone-removal.updated"); - } - } public getStoneRemovalString(): string { let ret = ""; const arr = []; @@ -1732,7 +1402,7 @@ export class GoEngine extends EventEmitter { ret += arr[i]; } - return GoMath.sortMoves(ret, this.width, this.height); + return sortMoves(ret, this.width, this.height); } public getMoveNumber(): number { @@ -1742,8 +1412,12 @@ export class GoEngine extends EventEmitter { return this.last_official_move.move_number; } - /* Returns a details object containing the total score and the breakdown of the - * scoring details */ + /** + * Computes the score of the current board state. + * + * If only_prisoners is true, we return the same data structure for convenience, but only + * the prisoners will be counted, other sources of points will be zero. + */ public computeScore(only_prisoners?: boolean): Score { const ret = { white: { @@ -1766,118 +1440,67 @@ export class GoEngine extends EventEmitter { }, }; - let removed_black = 0; - let removed_white = 0; + // Tally up prisoners when appropriate + if (only_prisoners || this.score_prisoners) { + ret.white.prisoners = this.white_prisoners; + ret.black.prisoners = this.black_prisoners; - /* clear removed */ - if (!this.goban_callback || this.goban_callback.mode !== "analyze") { for (let y = 0; y < this.height; ++y) { for (let x = 0; x < this.width; ++x) { if (this.removal[y][x]) { - if (this.board[y][x] === 1) { - ++removed_black; + if (this.board[y][x] === JGOFNumericPlayerColor.BLACK) { + ret.white.prisoners += 1; } - if (this.board[y][x] === 2) { - ++removed_white; + if (this.board[y][x] === JGOFNumericPlayerColor.WHITE) { + ret.black.prisoners += 1; } - this.board[y][x] = 0; } } } } - const scored: Array> = []; - for (let y = 0; y < this.height; ++y) { - const row = []; - for (let x = 0; x < this.width; ++x) { - row.push(0); + // Tally everything else if we want that information + if (!only_prisoners) { + if (!this.score_territory) { + throw new Error("The score_territory flag should always be set to true"); } - scored.push(row); - } - - const markScored = (group: Group) => { - let ct = 0; - for (let i = 0; i < group.length; ++i) { - const x = group[i].x; - const y = group[i].y; - - const old_board = this.cur_move.state.board; - - /* XXX: TODO: When we implement stone removal and scoring stuff - * into the review mode and analysis mode, this needs to change to - appropriately consider removals */ - const in_review = false; - try { - /* - if (this.board && this.board.review_id) { - in_review = true; - } - */ - } catch (e) {} - if (!this.removal[y][x] || old_board[y][x] || in_review) { - ++ct; - scored[y][x] = 1; - } - } - return ct; - }; - //if (this.phase !== "play") { - if (!only_prisoners && this.score_territory) { - const gm = new GoStoneGroups(this, this.cur_move.state.board); - //console.log(gm); - - gm.foreachGroup((gr) => { - if (gr.is_territory) { - //console.log(gr); - if ( - !this.score_territory_in_seki && - gr.is_territory_in_seki && - this.strict_seki_mode - ) { - return; - } - if (gr.territory_color === 1) { - ret["black"].scoring_positions += GoMath.encodeMoves(gr.points); - ret["black"].territory += markScored(gr.points); - } else { - ret["white"].scoring_positions += GoMath.encodeMoves(gr.points); - ret["white"].territory += markScored(gr.points); - } - for (let i = 0; i < gr.points.length; ++i) { - const pt = gr.points[i]; - if (this.board[pt.y][pt.x] && !this.removal[pt.y][pt.x]) { - /* This can happen as people are using the edit tool to force stone position colors */ - /* This can also happen now that we are doing estimate based scoring */ - //console.log("Point "+ GoMath.prettyCoords(pt.x, pt.y, this.height) +" should be removed, but is not because of an edit"); - //throw "Fucking hell: " + pt.x + "," + pt.y; + if (this.score_stones) { + const scoring = goscorer.areaScoring(this.board, this.removal); + for (let y = 0; y < this.height; ++y) { + for (let x = 0; x < this.width; ++x) { + if (scoring[y][x] === goscorer.BLACK) { + if (this.board[y][x] === JGOFNumericPlayerColor.BLACK) { + ret.black.stones += 1; + } else { + ret.black.territory += 1; + } + ret.black.scoring_positions += encodeMove(x, y); + } else if (scoring[y][x] === goscorer.WHITE) { + if (this.board[y][x] === JGOFNumericPlayerColor.WHITE) { + ret.white.stones += 1; + } else { + ret.white.territory += 1; + } + ret.white.scoring_positions += encodeMove(x, y); } } } - }); - } - - if (!only_prisoners && this.score_stones) { - for (let y = 0; y < this.height; ++y) { - for (let x = 0; x < this.width; ++x) { - if (this.board[y][x]) { - if (this.board[y][x] === 1) { - ++ret.black.stones; + } else { + const scoring = goscorer.territoryScoring(this.board, this.removal); + for (let y = 0; y < this.height; ++y) { + for (let x = 0; x < this.width; ++x) { + if (scoring[y][x].isTerritoryFor === goscorer.BLACK) { + ret.black.territory += 1; ret.black.scoring_positions += encodeMove(x, y); - } else { - ++ret.white.stones; + } else if (scoring[y][x].isTerritoryFor === goscorer.WHITE) { + ret.white.territory += 1; ret.white.scoring_positions += encodeMove(x, y); } } } } } - //} - - if (only_prisoners || this.score_prisoners) { - ret["black"].prisoners = this.black_prisoners + removed_white; - ret["white"].prisoners = this.white_prisoners + removed_black; - } ret["black"].total = ret["black"].stones + @@ -1913,6 +1536,7 @@ export class GoEngine extends EventEmitter { return ret; } + public handicapMovesLeft(): number { if (this.free_handicap_placement) { return Math.max(0, this.handicap - this.getMoveNumber()); @@ -1920,7 +1544,11 @@ export class GoEngine extends EventEmitter { return 0; } - private static normalizeConfig(config: GoEngineConfig): void { + /** + * This function migrates old config's to whatever our current standard is + * for configs. + */ + private static migrateConfig(config: GobanEngineConfig): GobanEngineConfig { if (config.ladder !== config.ladder_id) { config.ladder_id = config.ladder; } @@ -1965,8 +1593,14 @@ export class GoEngine extends EventEmitter { ); } } + + return config; } - public static fillDefaults(game_obj: GoEngineConfig): GoEngineConfig { + /** + * This function fills in default values for any missing fields in the + * config. + */ + public static fillDefaults(game_obj: GobanEngineConfig): GobanEngineConfig { if (!("phase" in game_obj)) { game_obj.phase = "play"; } @@ -1974,7 +1608,7 @@ export class GoEngine extends EventEmitter { game_obj.rules = "japanese"; } - const defaults: GoEngineConfig = {}; + const defaults: GobanEngineConfig = {}; //defaults.history = []; defaults.game_id = 0; @@ -2268,7 +1902,7 @@ export class GoEngine extends EventEmitter { return game_obj; } - public static clearRuleSettings(game_obj: GoEngineConfig): GoEngineConfig { + public static clearRuleSettings(game_obj: GobanEngineConfig): GobanEngineConfig { delete game_obj.allow_self_capture; delete game_obj.automatic_stone_removal; delete game_obj.allow_ko; @@ -2598,7 +2232,7 @@ export class GoEngine extends EventEmitter { case "RU": { instructions.push(() => { - let rules: GoEngineRules = "japanese"; + let rules: GobanEngineRules = "japanese"; switch (val.toLowerCase()) { case "japanese": @@ -2818,15 +2452,15 @@ export class GoEngine extends EventEmitter { trials: number, tolerance: number, prefer_remote: boolean = false, - autoscore: boolean = false, + should_autoscore: boolean = false, ): ScoreEstimator { const se = new ScoreEstimator( - this.goban_callback, this, + this.goban_callback, trials, tolerance, prefer_remote, - autoscore, + should_autoscore, ); return se.score(); } @@ -2886,8 +2520,11 @@ export class GoEngine extends EventEmitter { return ret; } - public parentEventEmitter?: EventEmitter; - emit(event: K, ...args: EventEmitter.EventArgs): boolean { + public parentEventEmitter?: EventEmitter; + public override emit( + event: K, + ...args: EventEmitter.EventArgs + ): boolean { let ret: boolean = super.emit(event, ...args); if (this.parentEventEmitter) { ret = this.parentEventEmitter.emit(event, ...args) || ret; diff --git a/src/GobanError.ts b/src/engine/GobanError.ts similarity index 98% rename from src/GobanError.ts rename to src/engine/GobanError.ts index d618736e..03f63730 100644 --- a/src/GobanError.ts +++ b/src/engine/GobanError.ts @@ -21,7 +21,7 @@ export type GobanIOErrorMessageId = "failed_to_load_sgf"; export type GobanMoveErrorMessageId = | "stone_already_placed_here" - | "move_is_suicidal" + | "illegal_self_capture" | "illegal_ko_move" | "illegal_board_repetition" | "move_error"; // generic diff --git a/src/GobanSocket.ts b/src/engine/GobanSocket.ts similarity index 98% rename from src/GobanSocket.ts rename to src/engine/GobanSocket.ts index 2dd0e880..aa1585b9 100644 --- a/src/GobanSocket.ts +++ b/src/engine/GobanSocket.ts @@ -15,7 +15,7 @@ */ import { EventEmitter } from "eventemitter3"; -import { niceInterval } from "./GoUtil"; +import { niceInterval } from "./util"; import { ClientToServer, ClientToServerBase, ServerToClient } from "./protocol"; type GobanSocketClientToServerMessage = [keyof SendProtocol, any?, number?]; @@ -65,7 +65,7 @@ const RECONNECTION_INTERVALS = [ const DEFAULT_PING_INTERVAL = 10000; export type DataArgument = Entry extends (...args: infer A) => void ? A[0] : never; -export type ResponseType = Entry extends (...args: any[]) => infer R ? R : never; +export type ProtocolResponseType = Entry extends (...args: any[]) => infer R ? R : never; /** * This is a simple wrapper around the WebSocket API that provides a @@ -352,7 +352,7 @@ export class GobanSocket< public send( command: Command, data: DataArgument, - cb?: (data: ResponseType, error?: any) => void, + cb?: (data: ProtocolResponseType, error?: any) => void, ): void { const request: GobanSocketClientToServerMessage = cb ? [command, data, ++this.last_request_id] @@ -388,7 +388,7 @@ export class GobanSocket< public sendPromise( command: Command, data: DataArgument, - ): Promise> { + ): Promise> { return new Promise((resolve, reject) => { this.send(command, data, (data, error) => { if (error) { diff --git a/src/MoveTree.ts b/src/engine/MoveTree.ts similarity index 91% rename from src/MoveTree.ts rename to src/engine/MoveTree.ts index a4eaf65a..fe505036 100644 --- a/src/MoveTree.ts +++ b/src/engine/MoveTree.ts @@ -14,12 +14,18 @@ * limitations under the License. */ -import * as GoMath from "./GoMath"; -import { GoEngine, GoEngineState } from "./GoEngine"; -import { encodeMove } from "./GoMath"; -import { AdHocPackedMove } from "./AdHocFormat"; -import { JGOFNumericPlayerColor, JGOFPlayerSummary } from "./JGOF"; -import { escapeSGFText, newline2space } from "./Misc"; +import { GobanEngine } from "./GobanEngine"; +import { BoardState } from "./BoardState"; +import { + decodeMoves, + encodeCoordinate, + encodeMove, + makeObjectMatrix, + prettyCoordinates, +} from "./util"; +import { AdHocPackedMove } from "./formats/AdHocFormat"; +import { JGOFNumericPlayerColor, JGOFPlayerSummary } from "./formats/JGOF"; +import { escapeSGFText, newlines_to_spaces } from "./util"; export interface MarkInterface { triangle?: boolean; @@ -30,7 +36,8 @@ export interface MarkInterface { letter?: string; subscript?: string; transient_letter?: string; - score?: string | boolean; + //score?: string | boolean; + score?: string; chat_triangle?: boolean; sub_triangle?: boolean; remove?: boolean; @@ -40,6 +47,7 @@ export interface MarkInterface { black?: boolean; white?: boolean; color?: string; + needs_sealing?: boolean; [label: string]: string | boolean | undefined; } @@ -76,6 +84,15 @@ let __move_tree_id = 0; let __isobranches_state_hash: { [hash: string]: Array } = {}; /* used while finding isobranches */ +interface BoardStateWithIsobranchHash extends BoardState { + /** + * The isobranch hash is a hash of the board state. This field is used by + * the move tree to detect isomorphic branches. This field is populated + * when recomputeIsoBranches is called. + * */ + isobranch_hash?: string; +} + /* TODO: If we're on the server side, we shouldn't be doing anything with marks */ export class MoveTree { public static readonly stone_radius = 11; @@ -97,16 +114,16 @@ export class MoveTree { public line_color: number; public trunk: boolean; public text: string; - private readonly engine: GoEngine; + private readonly engine: GobanEngine; public x: number; public y: number; public edited: boolean; - public state: GoEngineState; + public state: BoardStateWithIsobranchHash; public pen_marks: MoveTreePenMarks = []; public player_update: JGOFPlayerSummary | undefined; public played_by: number | undefined; - /* public for use by renderer */ + /* public for use by renderers when drawing move trees */ public active_path_number: number = 0; public layout_cx: number = 0; public layout_cy: number = 0; @@ -119,12 +136,13 @@ export class MoveTree { /* These need to be protected by accessor methods now that we're not * initializing them on construction */ private chat_log?: Array; - private marks?: Array>; + private marks?: MarkInterface[][]; + private stashed_marks: MarkInterface[][][] = []; public isobranches: any; private isobranch_hash?: string; constructor( - engine: GoEngine, + engine: GobanEngine, trunk: boolean, x: number, y: number, @@ -132,12 +150,12 @@ export class MoveTree { player: JGOFNumericPlayerColor, move_number: number, parent: MoveTree | null, - state: GoEngineState, + state: BoardState, ) { this.id = ++__move_tree_id; this.x = x; this.y = y; - this.pretty_coordinates = engine.prettyCoords(x, y); + this.pretty_coordinates = engine.prettyCoordinates(x, y); //this.label; //this.label_metrics; this.layout_x = 0; @@ -157,7 +175,8 @@ export class MoveTree { this.text = ""; } - toJson(): MoveTreeJson { + /** Serializes our MoveTree into a MoveTreeJson object */ + public toJson(): MoveTreeJson { const ret: MoveTreeJson = { x: this.x, y: this.y, @@ -194,10 +213,12 @@ export class MoveTree { return ret; } - loadJsonForThisNode(json: MoveTreeJson): void { + + /** Loads the state of this MoveTree node from a MoveTreeJson object */ + public loadJsonForThisNode(json: MoveTreeJson): void { /* Unlike toJson, restoring from the json blob is a collaborative effort between - * MoveTree and the GoEngine because of all the state we capture along the way.. - * so during restoration GoEngine will form the tree, and for each node call this + * MoveTree and the GobanEngine because of all the state we capture along the way.. + * so during restoration GobanEngine will form the tree, and for each node call this * method with the json that was captured with toJson for this node */ if (json.x !== this.x || json.y !== this.y) { @@ -221,7 +242,8 @@ export class MoveTree { } } - recomputeIsobranches(): void { + /** Recomputes the isobranches for the entire tree. This needs to be called on the root node. */ + public recomputeIsobranches(): void { if (this.parent) { throw new Error("MoveTree.recomputeIsobranches needs to be called from the root node"); } @@ -568,25 +590,40 @@ export class MoveTree { this.remove(); } } - getChatLog(): Array { + getChatLog(): any[] { if (!this.chat_log) { this.chat_log = []; } return this.chat_log; } - getAllMarks(): Array> { + getAllMarks(): MarkInterface[][] { if (!this.marks) { this.marks = this.clearMarks(); } return this.marks; } - setAllMarks(marks: Array>): void { + setAllMarks(marks: MarkInterface[][]): void { this.marks = marks; } - clearMarks(): Array> { - this.marks = GoMath.makeObjectMatrix(this.engine.width, this.engine.height); + clearMarks(): MarkInterface[][] { + this.marks = makeObjectMatrix(this.engine.width, this.engine.height); return this.marks; } + + /** Saves the current marks in our stash, restore them with popMarks */ + public stashMarks(): void { + this.stashed_marks.push(this.getAllMarks()); + this.clearMarks(); + } + + /** Restores previously stashed marks */ + public popStashedMarks(): void { + if (this.stashed_marks.length > 0) { + this.marks = this.stashed_marks.pop(); + } + } + + /** Returns true if there are any marks that have been set */ hasMarks(): boolean { if (!this.marks) { return false; @@ -603,7 +640,9 @@ export class MoveTree { } return false; } - foreachMarkedPosition(fn: (i: number, j: number) => void): void { + + /** Calls a callback for each positions that has a mark on it */ + public foreachMarkedPosition(fn: (i: number, j: number) => void): void { if (!this.marks) { return; } @@ -668,8 +707,8 @@ export class MoveTree { if (this.x === -1) { ret.push(""); } else { - ret.push(GoMath.coor_num2ch(this.x)); - ret.push(GoMath.coor_num2ch(this.y)); + ret.push(encodeCoordinate(this.x)); + ret.push(encodeCoordinate(this.y)); } ret.push("]"); txt.push(this.text); @@ -696,7 +735,7 @@ export class MoveTree { for (let y = 0; y < this.marks.length; ++y) { for (let x = 0; x < this.marks[0].length; ++x) { const m = this.marks[y][x]; - const pos = GoMath.coor_num2ch(x) + GoMath.coor_num2ch(y); + const pos = encodeCoordinate(x) + encodeCoordinate(y); if (m.triangle) { ret.push("TR[" + pos + "]"); } @@ -712,7 +751,7 @@ export class MoveTree { if (m.letter) { // https://www.red-bean.com/sgf/properties.html // LB is composed type of simple text (== no newlines, escaped colon) - const body = newline2space(escapeSGFText(m.letter, true)); + const body = newlines_to_spaces(escapeSGFText(m.letter, true)); ret.push("LB[" + pos + ":" + body + "]"); } } @@ -1046,10 +1085,10 @@ export class MoveTree { try { if (typeof message === "object") { if (message.type === "analysis") { - const moves = GoMath.decodeMoves(message.moves, width, height); + const moves = decodeMoves(message.moves, width, height); let move_str = ""; for (let i = 0; i < moves.length; ++i) { - move_str += GoMath.prettyCoords(moves[i].x, moves[i].y, height) + " "; + move_str += prettyCoordinates(moves[i].x, moves[i].y, height) + " "; } return message.name + ". From move " + message.from + ": " + move_str; diff --git a/src/engine/README.md b/src/engine/README.md new file mode 100644 index 00000000..a6b715b0 --- /dev/null +++ b/src/engine/README.md @@ -0,0 +1,5 @@ +The goban engine module contains all of the logic for playing the game and +communicating with the Online-Go.com game servers. + +The code in this module **MUST** be able to be compiled and largely operate +in both browser and node environments. diff --git a/src/ScoreEstimator.ts b/src/engine/ScoreEstimator.ts similarity index 71% rename from src/ScoreEstimator.ts rename to src/engine/ScoreEstimator.ts index f15da52a..91dadb71 100644 --- a/src/ScoreEstimator.ts +++ b/src/engine/ScoreEstimator.ts @@ -14,19 +14,16 @@ * limitations under the License. */ -import { dup } from "./GoUtil"; -import { encodeMove, encodeMoves } from "./GoMath"; -import * as GoMath from "./GoMath"; -import { GoStoneGroup } from "./GoStoneGroup"; -import { GoStoneGroups } from "./GoStoneGroups"; -import { GobanCore } from "./GobanCore"; -import { GoEngine, PlayerScore, GoEngineRules } from "./GoEngine"; -import { JGOFNumericPlayerColor } from "./JGOF"; +import { encodeMove, makeMatrix, NumberMatrix } from "./util"; +import { StoneString } from "./StoneString"; +import { StoneStringBuilder } from "./StoneStringBuilder"; +import type { GobanBase } from "../GobanBase"; +import { GobanEngine, PlayerScore, GobanEngineRules } from "./GobanEngine"; +import { JGOFMove, JGOFNumericPlayerColor, JGOFSealingIntersection } from "./formats/JGOF"; import { _ } from "./translate"; -import { estimateScoreWasm } from "./local_estimators/wasm_estimator"; - -export { init_score_estimator, estimateScoreWasm } from "./local_estimators/wasm_estimator"; -export { estimateScoreVoronoi } from "./local_estimators/voronoi"; +import { wasm_estimate_ownership, remote_estimate_ownership } from "./ownership_estimators"; +import * as goscorer from "goscorer"; +import { BoardState } from "./BoardState"; /* In addition to the local estimators, we have a RemoteScoring system * which needs to be initialized by either the client or the server if we want @@ -38,7 +35,7 @@ export interface ScoreEstimateRequest { width: number; height: number; board_state: JGOFNumericPlayerColor[][]; - rules: GoEngineRules; + rules: GobanEngineRules; black_prisoners?: number; white_prisoners?: number; komi?: number; @@ -64,15 +61,10 @@ export interface ScoreEstimateResponse { autoscored_board_state?: JGOFNumericPlayerColor[][]; /** Intersections that are dead or dame. Only defined if autoscore was true in the request. */ - autoscored_removed?: string; -} + autoscored_removed?: JGOFMove[]; -let remote_scorer: ((req: ScoreEstimateRequest) => Promise) | undefined; -/* This is used on both the client and server side */ -export function set_remote_scorer( - scorer: (req: ScoreEstimateRequest) => Promise, -): void { - remote_scorer = scorer; + /** Coordinates that still need sealing */ + autoscored_needs_sealing?: JGOFSealingIntersection[]; } /** @@ -90,16 +82,13 @@ type LocalEstimator = ( color_to_move: "black" | "white", trials: number, tolerance: number, -) => GoMath.NumberMatrix; -let local_scorer = estimateScoreWasm; -export function set_local_scorer(scorer: LocalEstimator) { - local_scorer = scorer; +) => NumberMatrix; +let local_ownership_estimator = wasm_estimate_ownership; +export function set_local_ownership_estimator(estimator: LocalEstimator) { + local_ownership_estimator = estimator; } -export class ScoreEstimator { - width: number; - height: number; - board: Array>; +export class ScoreEstimator extends BoardState { white: PlayerScore = { total: 0, stones: 0, @@ -119,10 +108,8 @@ export class ScoreEstimator { komi: 0, }; - engine: GoEngine; - private groups: GoStoneGroups; - removal: Array>; - goban_callback?: GobanCore; + engine: GobanEngine; + private groups: StoneStringBuilder; tolerance: number; amount: number = NaN; ownership: Array>; @@ -134,35 +121,38 @@ export class ScoreEstimator { when_ready: Promise; prefer_remote: boolean; autoscored_state?: JGOFNumericPlayerColor[][]; - autoscored_removed?: string; + autoscored_removed?: JGOFMove[]; autoscore: boolean = false; + public autoscored_needs_sealing?: JGOFSealingIntersection[]; constructor( - goban_callback: GobanCore | undefined, - engine: GoEngine, + engine: GobanEngine, + goban_callback: GobanBase | undefined, trials: number, tolerance: number, prefer_remote: boolean = false, autoscore: boolean = false, + removal?: boolean[][], ) { - this.goban_callback = goban_callback; + super(engine, goban_callback); + + if (removal) { + this.removal = removal; + } this.engine = engine; - this.width = engine.width; - this.height = engine.height; this.color_to_move = engine.colorToMove(); - this.board = dup(engine.board); - this.removal = GoMath.makeMatrix(this.width, this.height, 0); - this.ownership = GoMath.makeMatrix(this.width, this.height, 0); - this.territory = GoMath.makeMatrix(this.width, this.height, 0); + this.board = engine.cloneBoard(); + this.ownership = makeMatrix(this.width, this.height, 0); + this.territory = makeMatrix(this.width, this.height, 0); this.estimated_hard_score = 0.0; this.trials = trials; this.tolerance = tolerance; this.prefer_remote = prefer_remote; this.autoscore = autoscore; - this.territory = GoMath.makeMatrix(this.width, this.height, 0); - this.groups = new GoStoneGroups(this); + this.territory = makeMatrix(this.width, this.height, 0); + this.groups = new StoneStringBuilder(this); this.when_ready = this.estimateScore(this.trials, this.tolerance, autoscore); } @@ -172,7 +162,7 @@ export class ScoreEstimator { return this.estimateScoreLocal(trials, tolerance); } - if (remote_scorer) { + if (remote_estimate_ownership) { return this.estimateScoreRemote(autoscore); } else { return this.estimateScoreLocal(trials, tolerance); @@ -186,7 +176,7 @@ export class ScoreEstimator { : 0; return new Promise((resolve, reject) => { - if (!remote_scorer) { + if (!remote_estimate_ownership) { throw new Error("Remote scoring not setup"); } @@ -199,13 +189,13 @@ export class ScoreEstimator { board_state.push(row); } - remote_scorer({ + remote_estimate_ownership({ player_to_move: this.engine.colorToMove(), width: this.engine.width, height: this.engine.height, rules: this.engine.rules, board_state: board_state, - autoscore, + autoscore: autoscore, jwt: "", // this gets set by the remote_scorer method }) .then((res: ScoreEstimateResponse) => { @@ -225,6 +215,7 @@ export class ScoreEstimator { res.score -= this.engine.getHandicapPointAdjustmentForWhite(); this.autoscored_removed = res.autoscored_removed; this.autoscored_state = res.autoscored_board_state; + this.autoscored_needs_sealing = res.autoscored_needs_sealing; if (this.autoscored_state) { this.updateEstimate( @@ -261,7 +252,7 @@ export class ScoreEstimator { tolerance = 0.25; } - const board = GoMath.makeMatrix(this.width, this.height); + const board = makeMatrix(this.width, this.height, 0); for (let y = 0; y < this.height; ++y) { for (let x = 0; x < this.width; ++x) { board[y][x] = this.board[y][x] === 2 ? -1 : this.board[y][x]; @@ -271,7 +262,12 @@ export class ScoreEstimator { } } - const ownership = local_scorer(board, this.engine.colorToMove(), trials, tolerance); + const ownership = local_ownership_estimator( + board, + this.engine.colorToMove(), + trials, + tolerance, + ); const estimated_score = sum_board(ownership); const adjusted = adjust_estimate(this.engine, this.board, ownership, estimated_score); @@ -302,8 +298,11 @@ export class ScoreEstimator { getProbablyDead(): string { if (this.autoscored_removed) { console.info("Returning autoscored_removed for getProbablyDead"); - return this.autoscored_removed; + return this.autoscored_removed.map(encodeMove).join(""); } else { + // This still happens with local scoring I believe, we should probably run the autoscore + // logic for local scoring and ensure the autoscore_removed field is always set, then + // remove this probably dead code all together. console.warn("Not able to use autoscored_removed for getProbablyDead"); } @@ -331,88 +330,24 @@ export class ScoreEstimator { } return ret; } - handleClick(i: number, j: number, mod_key: boolean) { - if (mod_key) { - this.setRemoved(i, j, !this.removal[j][i] ? 1 : 0); - } else { - this.toggleMetaGroupRemoval(i, j); - } + handleClick(i: number, j: number, mod_key: boolean, press_duration_ms: number): void { + this.toggleSingleGroupRemoval(i, j, mod_key || press_duration_ms > 500); this.estimateScore(this.trials, this.tolerance, this.autoscore).catch(() => { /* empty */ }); } - private removeGroup(g: GoStoneGroup, removing: boolean) { - g.foreachStone(({ x, y }) => this.setRemoved(x, y, removing ? 1 : 0)); + public override setRemoved(x: number, y: number, removed: boolean): void { + this.clearAutoScore(); + super.setRemoved(x, y, removed); } - toggleMetaGroupRemoval(x: number, y: number): void { - const already_done: { [k: string]: boolean } = {}; - const space_groups: Array = []; - let group_color: JGOFNumericPlayerColor; - - try { - if (x >= 0 && y >= 0) { - const removing = !this.removal[y][x]; - const group = this.getGroup(x, y); - this.removeGroup(group, removing); - - group_color = this.board[y][x]; - if (group_color === 0) { - /* just toggle open area */ - } else { - /* for stones though, toggle the selected stone group any any stone - * groups which are adjacent to it through open area */ - - group.foreachNeighborSpaceGroup((g) => { - if (!already_done[g.id]) { - space_groups.push(g); - already_done[g.id] = true; - } - }); - - while (space_groups.length) { - const cur_space_group = space_groups.pop(); - cur_space_group?.foreachNeighborEnemyGroup((g) => { - if (!already_done[g.id]) { - already_done[g.id] = true; - if (g.color === group_color) { - this.removeGroup(g, removing); - g.foreachNeighborSpaceGroup((g_space) => { - if (!already_done[g_space.id]) { - space_groups.push(g_space); - already_done[g_space.id] = true; - } - }); - } - } - }); - } - } - } - } catch (e) { - console.log(e.stack); - } - } - setRemoved(x: number, y: number, removed: number): void { + public override clearRemoved(): void { this.clearAutoScore(); - - this.removal[y][x] = removed; - if (this.goban_callback) { - this.goban_callback.setForRemoval(x, y, this.removal[y][x]); - } - } - clearRemoved(): void { - this.clearAutoScore(); - for (let y = 0; y < this.height; ++y) { - for (let x = 0; x < this.width; ++x) { - if (this.removal[y][x]) { - this.setRemoved(x, y, 0); - } - } - } + super.clearRemoved(); } + clearAutoScore(): void { if (this.autoscored_removed || this.autoscored_state) { this.autoscored_removed = undefined; @@ -424,7 +359,7 @@ export class ScoreEstimator { getStoneRemovalString(): string { if (this.autoscored_removed) { console.info("Returning autoscored_removed for getStoneRemovalString"); - return this.autoscored_removed; + return this.autoscored_removed.map(encodeMove).join(""); } else { console.warn("Not able to use autoscored_removed for getStoneRemovalString"); } @@ -444,14 +379,12 @@ export class ScoreEstimator { } return ret; } - getGroup(x: number, y: number): GoStoneGroup { - return this.groups.groups[this.groups.group_id_map[y][x]]; + getGroup(x: number, y: number): StoneString { + return this.groups.stone_strings[this.groups.stone_string_id_map[y][x]]; } /** - * This gets run after we've instructed the estimator how/when to fill dame, - * manually mark removed/dame, etc.. it does an official scoring from the - * remaining territory. + * Computes a rough estimation of ownership and score. */ score(): ScoreEstimator { this.white = { @@ -476,7 +409,7 @@ export class ScoreEstimator { let removed_black = 0; let removed_white = 0; - /* clear removed */ + // clear removed for (let y = 0; y < this.height; ++y) { for (let x = 0; x < this.width; ++x) { if (this.removal[y][x]) { @@ -491,32 +424,52 @@ export class ScoreEstimator { } } - if (this.engine.score_territory) { - const groups = new GoStoneGroups(this); - - groups.foreachGroup((gr) => { - if (gr.is_territory) { - if (!this.engine.score_territory_in_seki && gr.is_territory_in_seki) { - return; - } - if (gr.territory_color === 1) { - this.black.scoring_positions += encodeMoves(gr.points); - } else { - this.white.scoring_positions += encodeMoves(gr.points); + /* Note: this scoring just ensures our estimator is filled in with at least + * official territory and stones. Usually however, the estimation will already + * have all of this stuff marked, it's just to make sure we don't miss some + * obvious territory. + */ + if (this.engine.score_stones) { + const scoring = goscorer.areaScoring( + this.board, + this.removal.map((row) => row.map((x) => !!x)), + ); + for (let y = 0; y < this.height; ++y) { + for (let x = 0; x < this.width; ++x) { + if (scoring[y][x] === goscorer.BLACK) { + if (this.board[y][x] === JGOFNumericPlayerColor.BLACK) { + this.black.stones += 1; + } else { + this.black.territory += 1; + } + this.black.scoring_positions += encodeMove(x, y); + } else if (scoring[y][x] === goscorer.WHITE) { + if (this.board[y][x] === JGOFNumericPlayerColor.WHITE) { + this.white.stones += 1; + } else { + this.white.territory += 1; + } + this.white.scoring_positions += encodeMove(x, y); } } - }); - } - - if (this.engine.score_stones) { + } + } else { + const scoring = goscorer.territoryScoring( + this.board, + this.removal.map((row) => row.map((x) => !!x)), + ); for (let y = 0; y < this.height; ++y) { for (let x = 0; x < this.width; ++x) { - if (this.board[y][x]) { - if (this.board[y][x] === 1) { - ++this.black.stones; + if (scoring[y][x].isUnscorableFalseEye) { + this.board[y][x] = 0; + this.territory[y][x] = 0; + this.ownership[y][x] = 0; + } else { + if (scoring[y][x].isTerritoryFor === goscorer.BLACK) { + this.black.territory += 1; this.black.scoring_positions += encodeMove(x, y); - } else { - ++this.white.stones; + } else if (scoring[y][x].isTerritoryFor === goscorer.WHITE) { + this.white.territory += 1; this.white.scoring_positions += encodeMove(x, y); } } @@ -551,14 +504,14 @@ export class ScoreEstimator { * @param score estimated score (not accounting for captures) */ export function adjust_estimate( - engine: GoEngine, + engine: GobanEngine, board: Array>, area_map: number[][], score: number, ) { let adjusted_score = score - engine.getHandicapPointAdjustmentForWhite(); const { width, height } = get_dimensions(board); - const ownership = GoMath.makeMatrix(width, height); + const ownership = makeMatrix(width, height, 0); // For Japanese rules we use territory counting. Don't even // attempt to handle rules with score_stones and not @@ -611,7 +564,7 @@ function get_dimensions(board: Array>) { return { width: board[0].length, height: board.length }; } -function sum_board(board: GoMath.NumberMatrix) { +function sum_board(board: NumberMatrix) { const { width, height } = get_dimensions(board); let sum = 0; for (let y = 0; y < height; y++) { diff --git a/src/engine/StoneString.ts b/src/engine/StoneString.ts new file mode 100644 index 00000000..8e9aa042 --- /dev/null +++ b/src/engine/StoneString.ts @@ -0,0 +1,129 @@ +/* + * Copyright (C) Online-Go.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { JGOFNumericPlayerColor, JGOFIntersection } from "./formats/JGOF"; + +/** A raw stone string is simply an array of intersections */ +export type RawStoneString = Array; + +/** + * A StoneString instance represents a group of intersections that + * are connected to each other and are all the same color. + */ +export class StoneString { + public readonly intersections: Array; + public readonly neighbors: Array; + public readonly color: JGOFNumericPlayerColor; + public readonly id: number; + public territory_color: JGOFNumericPlayerColor = 0; + public is_territory: boolean = false; + + private __added_neighbors: { [group_id: number]: boolean }; + private neighboring_space: StoneString[]; + private neighboring_stone_strings: StoneString[]; + + constructor(id: number, color: JGOFNumericPlayerColor) { + this.intersections = []; + this.neighbors = []; + this.neighboring_space = []; + this.neighboring_stone_strings = []; + this.id = id; + this.color = color; + + this.__added_neighbors = {}; + } + public map(fn: (loc: JGOFIntersection) => void): void { + for (let i = 0; i < this.intersections.length; ++i) { + fn(this.intersections[i]); + } + } + public foreachNeighboringString(fn: (stone_string: StoneString) => void): void { + for (let i = 0; i < this.neighbors.length; ++i) { + fn(this.neighbors[i]); + } + } + public foreachNeighboringEmptyString(fn: (stone_string: StoneString) => void): void { + for (let i = 0; i < this.neighboring_space.length; ++i) { + fn(this.neighboring_space[i]); + } + } + public foreachNeighboringStoneString(fn: (stone_string: StoneString) => void): void { + for (let i = 0; i < this.neighboring_stone_strings.length; ++i) { + fn(this.neighboring_stone_strings[i]); + } + } + public size(): number { + return this.intersections.length; + } + + /** Add a stone to the group. This should probably only be called by StoneStringBuilder. */ + _addStone(x: number, y: number): void { + this.intersections.push({ x: x, y: y }); + } + + /** Adds a stone string to our neighbor list. This should probably only be called by StoneStringBuilder. */ + _addNeighborGroup(group: StoneString): void { + if (!(group.id in this.__added_neighbors)) { + this.neighbors.push(group); + this.__added_neighbors[group.id] = true; + + if (group.color !== this.color) { + if (group.color === JGOFNumericPlayerColor.EMPTY) { + this.neighboring_space.push(group); + } else { + this.neighboring_stone_strings.push(group); + } + } + } + } + + /** + * Compute if this string is considered potential territory (if all of it's + * neighbors are the same color). NOTE: This does not perform any advanced + * logic to determine seki status or anything like that, this only looks to + * see if the string contains EMPTY locations and that all of the + * surrounding neighboring are the same color. This should probably only + * be called by StoneStringBuilder. + */ + _computeIsTerritory(): void { + /* An empty group is considered territory if all of it's neighbors are + * the same color */ + this.is_territory = false; + this.territory_color = 0; + if (this.color) { + return; + } + + let color: JGOFNumericPlayerColor = 0; + for (let i = 0; i < this.neighbors.length; ++i) { + if (this.neighbors[i].color !== 0) { + color = this.neighbors[i].color; + break; + } + } + + this.foreachNeighboringString((gr) => { + if (gr.color !== 0 && color !== gr.color) { + color = 0; + } + }); + + if (color) { + this.is_territory = true; + this.territory_color = color; + } + } +} diff --git a/src/GoStoneGroups.ts b/src/engine/StoneStringBuilder.ts similarity index 57% rename from src/GoStoneGroups.ts rename to src/engine/StoneStringBuilder.ts index 483350dd..b5aa757e 100644 --- a/src/GoStoneGroups.ts +++ b/src/engine/StoneStringBuilder.ts @@ -14,22 +14,23 @@ * limitations under the License. */ -import * as GoMath from "./GoMath"; -import { GoStoneGroup, BoardState } from "./GoStoneGroup"; -import { JGOFNumericPlayerColor } from "./JGOF"; +import { StoneString } from "./StoneString"; +import { BoardState } from "./BoardState"; +import { JGOFNumericPlayerColor } from "./formats/JGOF"; +import { makeMatrix } from "./util"; -export class GoStoneGroups { +export class StoneStringBuilder { private state: BoardState; - public group_id_map: Array>; - public groups: Array; + public readonly stone_string_id_map: number[][]; + public readonly stone_strings: StoneString[]; - constructor(state: BoardState, original_board?: Array>) { - const groups: Array = Array(1); // this is indexed by group_id, so we 1 index this array so group_id >= 1 - const group_id_map: Array> = GoMath.makeMatrix(state.width, state.height); + constructor(state: BoardState, original_board?: JGOFNumericPlayerColor[][]) { + const stone_strings: StoneString[] = Array(1); // this is indexed by group_id, so we 1 index this array so group_id >= 1 + const group_id_map = makeMatrix(state.width, state.height, 0); this.state = state; - this.group_id_map = group_id_map; - this.groups = groups; + this.stone_string_id_map = group_id_map; + this.stone_strings = stone_strings; const floodFill = ( x: number, @@ -75,67 +76,45 @@ export class GoStoneGroups { ); } - if (!(group_id_map[y][x] in groups)) { - groups.push( - new GoStoneGroup(this.state, group_id_map[y][x], this.state.board[y][x]), - ); + if (!(group_id_map[y][x] in stone_strings)) { + stone_strings.push(new StoneString(group_id_map[y][x], this.state.board[y][x])); } - groups[group_id_map[y][x]].addStone(x, y); + stone_strings[group_id_map[y][x]]._addStone(x, y); } } /* Compute group neighbors */ this.foreachGroup((gr) => { - gr.foreachStone((pt) => { + gr.map((pt) => { const x = pt.x; const y = pt.y; if (x - 1 >= 0 && group_id_map[y][x - 1] !== gr.id) { - gr.addNeighborGroup(groups[group_id_map[y][x - 1]]); + gr._addNeighborGroup(stone_strings[group_id_map[y][x - 1]]); } if (x + 1 < this.state.width && group_id_map[y][x + 1] !== gr.id) { - gr.addNeighborGroup(groups[group_id_map[y][x + 1]]); + gr._addNeighborGroup(stone_strings[group_id_map[y][x + 1]]); } if (y - 1 >= 0 && group_id_map[y - 1][x] !== gr.id) { - gr.addNeighborGroup(groups[group_id_map[y - 1][x]]); + gr._addNeighborGroup(stone_strings[group_id_map[y - 1][x]]); } if (y + 1 < this.state.height && group_id_map[y + 1][x] !== gr.id) { - gr.addNeighborGroup(groups[group_id_map[y + 1][x]]); - } - for (let Y = -1; Y <= 1; ++Y) { - for (let X = -1; X <= 1; ++X) { - if ( - x + X >= 0 && - x + X < this.state.width && - y + Y >= 0 && - y + Y < this.state.height - ) { - gr.addCornerGroup(x + X, y + Y, groups[group_id_map[y + Y][x + X]]); - } - } + gr._addNeighborGroup(stone_strings[group_id_map[y + 1][x]]); } }); }); this.foreachGroup((gr) => { - gr.computeIsTerritory(); - }); - this.foreachGroup((gr) => { - gr.computeIsTerritoryInSeki(); - }); - this.foreachGroup((gr) => { - gr.computeIsEye(); - }); - this.foreachGroup((gr) => { - gr.computeIsStrongEye(); - }); - this.foreachGroup((gr) => { - gr.computeIsStrongString(); + gr._computeIsTerritory(); }); } - public foreachGroup(fn: (gr: GoStoneGroup) => void) { - for (let i = 1; i < this.groups.length; ++i) { - fn(this.groups[i]); + public foreachGroup(fn: (gr: StoneString) => void) { + for (let i = 1; i < this.stone_strings.length; ++i) { + fn(this.stone_strings[i]); } } + + public getGroup(x: number, y: number): StoneString { + return this.stone_strings[this.stone_string_id_map[y][x]]; + } } diff --git a/src/engine/autoscore.ts b/src/engine/autoscore.ts new file mode 100644 index 00000000..3657189f --- /dev/null +++ b/src/engine/autoscore.ts @@ -0,0 +1,1189 @@ +/* + * Copyright (C) Online-Go.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * The autoscore function takes an existing board state, two ownership + * matrices, and does it's best to determine which stones should be + * removed, which intersections should be considered dame, and what + * should be left alone. + */ + +import { StoneStringBuilder } from "./StoneStringBuilder"; +import { JGOFNumericPlayerColor, JGOFSealingIntersection, JGOFMove } from "./formats/JGOF"; +import { char2num, makeMatrix, num2char, encodePrettyXCoordinate } from "./util"; +import { GobanEngine, GobanEngineInitialState, GobanEngineRules } from "./GobanEngine"; +import { BoardState } from "./BoardState"; + +interface AutoscoreResults { + result: JGOFNumericPlayerColor[][]; + sealed_result: JGOFNumericPlayerColor[][]; + removed: JGOFMove[]; + needs_sealing: JGOFSealingIntersection[]; +} + +const REMOVAL_THRESHOLD = 0.7; +const SEAL_THRESHOLD = 0.3; +const WHITE_THRESHOLD = -REMOVAL_THRESHOLD; +const BLACK_THRESHOLD = REMOVAL_THRESHOLD; +const WHITE_SEAL_THRESHOLD = -SEAL_THRESHOLD; +const BLACK_SEAL_THRESHOLD = SEAL_THRESHOLD; + +function isWhite(ownership: number): boolean { + return ownership <= WHITE_THRESHOLD; +} + +function isBlack(ownership: number): boolean { + return ownership >= BLACK_THRESHOLD; +} + +export function autoscore( + board: JGOFNumericPlayerColor[][], + rules: GobanEngineRules, + black_plays_first_ownership: number[][], + white_plays_first_ownership: number[][], +): [AutoscoreResults, DebugOutput] { + const original_board = board.map((row) => row.slice()); // copy + const width = board[0].length; + const height = board.length; + const removed: JGOFMove[] = []; + const removal = makeMatrix(width, height, false); + const is_settled = makeMatrix(width, height, 0); + const settled = makeMatrix(width, height, 0); + const final_ownership = makeMatrix(board[0].length, board.length, 0); + const final_sealed_ownership = makeMatrix(board[0].length, board.length, 0); + const sealed = makeMatrix(width, height, 0); + const needs_sealing: JGOFSealingIntersection[] = []; + + const average_ownership = makeMatrix(width, height, 0); + for (let y = 0; y < height; ++y) { + for (let x = 0; x < width; ++x) { + average_ownership[y][x] = + (black_plays_first_ownership[y][x] + white_plays_first_ownership[y][x]) / 2; + } + } + + // Print out our starting state + stage("Initial state"); + debug_board_output("Board", board); + debug_ownership_output("Black plays first estimates", black_plays_first_ownership); + debug_ownership_output("White plays first estimates", white_plays_first_ownership); + debug_ownership_output("Average estimates", average_ownership); + + const groups = new StoneStringBuilder( + new BoardState({ + board, + removal: makeMatrix(width, height, false), + }), + ); + + debug_groups("Groups", groups); + + // Perform our removal logic + //normalize_ownership(); + settle_snapback_locations(); + settle_agreed_upon_stones(); + settle_agreed_upon_territory(); + remove_obviously_dead_stones(); + clear_unsettled_stones_from_territory(); + seal_territory(); + score_positions(); + + stage("Final state"); + const final_ownership_with_seals = makeMatrix(width, height, "."); + for (let y = 0; y < height; ++y) { + for (let x = 0; x < width; ++x) { + if (sealed[y][x]) { + final_ownership_with_seals[y][x] = "s"; + } else { + final_ownership_with_seals[y][x] = + final_ownership[y][x] === 1 ? "B" : final_ownership[y][x] === 2 ? "W" : "."; + } + } + } + + //debug_board_output("Final ownership", final_ownership); + debug_board_string_output("Final ownership", final_ownership_with_seals); + debug_boolean_board("Sealed", sealed, "s"); + debug_board_output("Final sealed ownership", final_sealed_ownership); + + return [ + { + result: final_ownership, + sealed_result: final_sealed_ownership, + removed, + needs_sealing, + }, + finalize_debug_output(), + ]; + + /** Marks a position as being removed (either dead stone or dame) */ + function remove(x: number, y: number, removal_reason: string) { + if (removal[y][x]) { + return; + } + + removed.push({ x, y, removal_reason }); + board[y][x] = JGOFNumericPlayerColor.EMPTY; + removal[y][x] = true; + stage_log(`Removing ${encodePrettyXCoordinate(x)}${height - y}: ${removal_reason}`); + } + + /** + * Normalizes the string ownerships, this prevents single stones out of a group being marked + * as captured when there are snapback situation still left on the board. + */ + /* + function normalize_ownership() { + stage("Ownership normalization"); + + const stone_strings = new StoneStringBuilder( + new BoardState({ + board, + removal, + }), + original_board, + ); + + stone_strings.foreachGroup((stone_string) => { + let black = 0; + let white = 0; + let avg = 0; + stone_string.intersections.forEach((point) => { + const { x, y } = point; + black += black_plays_first_ownership[y][x]; + white += white_plays_first_ownership[y][x]; + avg += average_ownership[y][x]; + }); + black /= stone_string.intersections.length; + white /= stone_string.intersections.length; + avg /= stone_string.intersections.length; + stone_string.intersections.forEach((point) => { + const { x, y } = point; + black_plays_first_ownership[y][x] = black; + white_plays_first_ownership[y][x] = white; + average_ownership[y][x] = avg; + }); + }); + + debug_board_output("Board", board); + debug_ownership_output("Black plays first estimates", black_plays_first_ownership); + debug_ownership_output("White plays first estimates", white_plays_first_ownership); + debug_ownership_output("Average estimates", average_ownership); + } + */ + + /** + * Look for groups that look like they are at risk of a snapback and + * mark them as settled to avoid trying to be too smart, let the players + * figure out what they want to with those stones, if anything. Neighboring + * strings are also marked as settled as any that aren't are likely intwined + * in the life and death and resulting status of the snapback, so again to + * avoid trying to be too smart, just trust that the players intended to + * end the game in this state and score it. + */ + function settle_snapback_locations() { + stage("Settling snapbacks"); + + const snapbacks = makeMatrix(width, height, false); + const neighbors_of_snapbacks = makeMatrix(width, height, false); + + const stone_strings = new StoneStringBuilder( + new BoardState({ + board, + removal, + }), + original_board, + ); + stone_strings.foreachGroup((stone_string) => { + if (stone_string.color === JGOFNumericPlayerColor.EMPTY) { + return; + } + + let looks_like_snapback = + stone_string.intersections.some(({ x, y }) => + isBlack(black_plays_first_ownership[y][x]), + ) && + stone_string.intersections.some(({ x, y }) => + isWhite(black_plays_first_ownership[y][x]), + ); + looks_like_snapback ||= + stone_string.intersections.some(({ x, y }) => + isBlack(white_plays_first_ownership[y][x]), + ) && + stone_string.intersections.some(({ x, y }) => + isWhite(white_plays_first_ownership[y][x]), + ); + looks_like_snapback ||= + stone_string.intersections.some(({ x, y }) => isBlack(average_ownership[y][x])) && + stone_string.intersections.some(({ x, y }) => isWhite(average_ownership[y][x])); + + if (looks_like_snapback) { + const color = stone_string.color; + stone_string.intersections.forEach(({ x, y }) => { + is_settled[y][x] = 1; + settled[y][x] = color; + snapbacks[y][x] = true; + }); + + // settle our neighbors as well as they are likely part of the snapback + stone_string.foreachNeighboringStoneString((neighbor) => { + const color = neighbor.color; + neighbor.intersections.forEach(({ x, y }) => { + is_settled[y][x] = 1; + settled[y][x] = color; + + neighbors_of_snapbacks[y][x] = true; + }); + }); + } + }); + + debug_boolean_board("Snapbacks", snapbacks, "s"); + debug_boolean_board("Neighbors of snapbacks", neighbors_of_snapbacks, "n"); + debug_boolean_board("Settled", is_settled); + } + + /* + * Settle agreed-upon territory + * + * The purpose of this function is to ignore potential invasions + * by looking at the average territory ownership. If overall the + * territory is owned by one player, then we mark it as settled along + * with adjacent groups. + */ + function settle_agreed_upon_territory() { + stage("Settling agreed upon territory"); + + const groups = new StoneStringBuilder( + new BoardState({ + board, + removal, + }), + original_board, + ); + + debug_groups("Initial", groups); + + groups.foreachGroup((group) => { + const color = group.territory_color; + if (group.is_territory && color) { + let total_ownership = 0; + + group.map((point) => { + const x = point.x; + const y = point.y; + total_ownership += average_ownership[y][x]; + }); + + const avg = total_ownership / group.intersections.length; + + if ( + (color === JGOFNumericPlayerColor.BLACK && avg > BLACK_THRESHOLD) || + (color === JGOFNumericPlayerColor.WHITE && avg < WHITE_THRESHOLD) + ) { + group.map((point) => { + const x = point.x; + const y = point.y; + is_settled[y][x] = 1; + settled[y][x] = color; + }); + group.neighbors.forEach((neighbor) => { + neighbor.map((point) => { + const x = point.x; + const y = point.y; + is_settled[y][x] = 1; + settled[y][x] = color; + }); + }); + } + } + }); + + debug_boolean_board("Settled", is_settled); + debug_board_output("Settled ownership", settled); + } + + /* + * If both players agree on the ownership of certain stones, + * mark them as settled. + */ + function settle_agreed_upon_stones() { + stage("Marking settled stones"); + for (let y = 0; y < height; ++y) { + for (let x = 0; x < width; ++x) { + if ( + board[y][x] === JGOFNumericPlayerColor.WHITE && + isWhite(black_plays_first_ownership[y][x]) && + isWhite(white_plays_first_ownership[y][x]) + ) { + is_settled[y][x] = 1; + settled[y][x] = JGOFNumericPlayerColor.WHITE; + } + + if ( + board[y][x] === JGOFNumericPlayerColor.BLACK && + isBlack(black_plays_first_ownership[y][x]) && + isBlack(white_plays_first_ownership[y][x]) + ) { + is_settled[y][x] = 1; + settled[y][x] = JGOFNumericPlayerColor.BLACK; + } + } + } + + debug_boolean_board("Settled", is_settled); + debug_board_output("Resulting board", board); + } + + /* + * Remove obviously dead stones + * + * If we estimate that if either player moves first, yet a stone + * is dead, then we say the players agree - the stone is dead. This + * function detects these cases and removes the stones. + */ + function remove_obviously_dead_stones() { + stage("Removing stones both estimates agree upon"); + for (let y = 0; y < height; ++y) { + for (let x = 0; x < width; ++x) { + if (is_settled[y][x]) { + continue; + } + if ( + board[y][x] === JGOFNumericPlayerColor.WHITE && + isBlack(black_plays_first_ownership[y][x]) && + isBlack(white_plays_first_ownership[y][x]) + ) { + remove(x, y, "both players agree this is captured by black"); + } else if ( + board[y][x] === JGOFNumericPlayerColor.BLACK && + isWhite(black_plays_first_ownership[y][x]) && + isWhite(white_plays_first_ownership[y][x]) + ) { + remove(x, y, "both players agree this is captured by white"); + } + } + } + debug_boolean_board("Removed", removal, "x"); + } + + /* + * Consider unsettled groups (as defined by looking at connected + * intersections that are not settled, regardless of whether they have a + * stone on them or not). + * + * Pick an owner for this area based first on the average ownership + * of the area. If the average ownership exceeds our threshold, then + * we assume that the area is owned by the player. Otherwise, if the + * owner isn't clear according to our ownership estimations, then we + * go by the majority of the surrounding and contained stones - the + * one with the most stones wins. + * + * If we've determined a color for the area, then we remove any stones + * that are not of that color in the unsettled area. + * + * After this, we mark the area as settled. + */ + function clear_unsettled_stones_from_territory() { + stage("Clear unsettled stones from territory"); + + const stones_removed_before = removal.map((row) => row.slice()); + + /* + * Consider unsettled groups. Count the unsettled stones along with + * their neighboring stones + */ + const groups = new StoneStringBuilder( + new BoardState({ + board: is_settled, + removal: makeMatrix(width, height, false), + }), + ); + + groups.foreachGroup((group) => { + // if this group is a settled group, ignore it, we don't care about those + const pt = group.intersections[0]; + if (is_settled[pt.y][pt.x]) { + return; + } + + // Otherwise, count + const surrounding = [ + 0, // empty + 0, // black + 0, // white + ]; + const contained = [ + 0, // empty + 0, // black + 0, // white + ]; + let total_ownership_estimate = 0; + + const already_tallied = makeMatrix(width, height, 0); + function tally_edge(x: number, y: number) { + if (x < 0 || x >= width || y < 0 || y >= height) { + return; + } + if (already_tallied[y][x]) { + return; + } + if (is_settled[y][x]) { + surrounding[settled[y][x]]++; + already_tallied[y][x] = 1; + } + } + + group.map((point) => { + const x = point.x; + const y = point.y; + contained[board[y][x]]++; + tally_edge(x - 1, y); + tally_edge(x + 1, y); + tally_edge(x, y - 1); + tally_edge(x, y + 1); + + total_ownership_estimate += + black_plays_first_ownership[y][x] + white_plays_first_ownership[y][x]; + }); + + const average_color_estimate = total_ownership_estimate / group.intersections.length; + + let color_judgement: JGOFNumericPlayerColor; + + const total = [ + surrounding[0] + contained[0], + surrounding[1] + contained[1], + surrounding[2] + contained[2], + ]; + if (average_color_estimate > 0.5) { + color_judgement = JGOFNumericPlayerColor.BLACK; + } else if (total_ownership_estimate < -0.5) { + color_judgement = JGOFNumericPlayerColor.WHITE; + } else { + if (total[JGOFNumericPlayerColor.BLACK] > total[JGOFNumericPlayerColor.WHITE]) { + color_judgement = JGOFNumericPlayerColor.BLACK; + } else if ( + total[JGOFNumericPlayerColor.WHITE] > total[JGOFNumericPlayerColor.BLACK] + ) { + color_judgement = JGOFNumericPlayerColor.WHITE; + } else { + color_judgement = JGOFNumericPlayerColor.EMPTY; + } + } + + group.map((point) => { + const x = point.x; + const y = point.y; + if (board[y][x] && board[y][x] !== color_judgement) { + const stone_color = + board[y][x] === JGOFNumericPlayerColor.BLACK ? "black" : "white"; + const judgement_color = + color_judgement === JGOFNumericPlayerColor.BLACK ? "black" : "white"; + + remove( + x, + y, + `clearing unsettled ${stone_color} stones within assumed ${judgement_color} territory `, + ); + is_settled[y][x] = 1; + settled[y][x] = color_judgement; + } + }); + }); + + const removal_diff = removal.map((row, y) => + row.map((v, x) => (v && !stones_removed_before[y][x] ? 1 : 0)), + ); + debug_boolean_board("Removed", removal_diff, "x"); + } + + /* + * Attempt to seal territory + * + * This function attempts to seal territory that has been overlooked + * by the players. + * + * We do this by looking at unowned territory that is predominantly owned + * by one of the players. Adjacent intersections to the opposing color are + * marked as points needing sealing. We mark them as dame as well to facilitate + * forced automatic scoring (e.g. bot games, moderator auto-score, correspondence + * timeouts, etc), however when both players are present it's expected that the + * interface will prohibit accepting the score until the players have resumed + * and finished the game. + * + * Note, this needs to be run after obviously dead stones have been + * removed. + */ + + function seal_territory() { + stage(`Sealing territory`); + { + let groups = new StoneStringBuilder( + new BoardState({ + board, + removal, + }), + original_board, + ); + + debug_groups("Initial groups", groups); + + groups.foreachGroup((group) => { + // Large enough unowned territory where sealing might make a difference + if ( + group.color === JGOFNumericPlayerColor.EMPTY && + !group.is_territory && + group.intersections.length > 3 + ) { + // If it looks like our group is probably mostly owned by a player, but + // there are spots that are not sealed, mark those spots as dame so our + // future scoring steps can do things like clearing out unwanted stones from + // the proposed territory, but also mark them as needing to be sealed. + // so the players have to resume to finish the game. + let total_ownership = 0; + + group.map((point) => { + const x = point.x; + const y = point.y; + total_ownership += average_ownership[y][x]; + }); + + const avg = total_ownership / group.intersections.length; + + // If we meet our sealing threshold, seal + if (avg <= WHITE_SEAL_THRESHOLD || avg >= BLACK_SEAL_THRESHOLD) { + const color = + avg <= WHITE_SEAL_THRESHOLD + ? JGOFNumericPlayerColor.WHITE + : JGOFNumericPlayerColor.BLACK; + + // For each point, if it's touching a stone of the other color, mark it + // as a seal point. + group.map((point) => { + const x = point.x; + const y = point.y; + + const opposing_color = + avg <= WHITE_SEAL_THRESHOLD + ? JGOFNumericPlayerColor.BLACK + : JGOFNumericPlayerColor.WHITE; + + const adjacent_to_opposing_color = + board[y + 1]?.[x] === opposing_color || + board[y - 1]?.[x] === opposing_color || + board[y][x + 1] === opposing_color || + board[y][x - 1] === opposing_color; + + if (adjacent_to_opposing_color) { + //remove(x, y, "sealing territory"); + is_settled[y][x] = 1; + settled[y][x] = JGOFNumericPlayerColor.EMPTY; + sealed[y][x] = 1; + needs_sealing.push({ x, y, color }); + } + }); + } + } + }); + + groups = new StoneStringBuilder( + new BoardState({ + board, + removal, + }), + original_board, + ); + + debug_boolean_board("Sealed positions", sealed, "s"); + debug_groups("After sealing", groups); + } + } + + /** Compute our final ownership and scoring positions */ + function score_positions() { + stage("Score positions"); + let black_state = ""; + let white_state = ""; + + for (let y = 0; y < original_board.length; ++y) { + for (let x = 0; x < original_board[y].length; ++x) { + const v = original_board[y][x]; + const c = num2char(x) + num2char(y); + if (v === JGOFNumericPlayerColor.BLACK) { + black_state += c; + } else if (v === 2) { + white_state += c; + } + } + } + + const sealed_black_state = + black_state + + needs_sealing + .filter((s) => s.color === JGOFNumericPlayerColor.BLACK) + .map((p) => num2char(p.x) + num2char(p.y)) + .join(""); + const sealed_white_state = + white_state + + needs_sealing + .filter((s) => s.color === JGOFNumericPlayerColor.WHITE) + .map((p) => num2char(p.x) + num2char(p.y)) + .join(""); + + const real_initial_state: GobanEngineInitialState = { + black: black_state, + white: white_state, + }; + const sealed_initial_state: GobanEngineInitialState = { + black: sealed_black_state, + white: sealed_white_state, + }; + + for (const initial_state of [sealed_initial_state, real_initial_state]) { + const cur_ownership = makeMatrix(width, height, 0); + + const engine = new GobanEngine({ + width: original_board[0].length, + height: original_board.length, + initial_state, + rules, + removed, + }); + + const board = engine.board.map((row) => row.slice()); + removed.map((pt) => (board[pt.y][pt.x] = 0)); + + const score = engine.computeScore(); + const scoring_positions = makeMatrix(width, height, 0); + + for (let i = 0; i < score.black.scoring_positions.length; i += 2) { + const x = char2num(score.black.scoring_positions[i]); + const y = char2num(score.black.scoring_positions[i + 1]); + cur_ownership[y][x] = JGOFNumericPlayerColor.BLACK; + scoring_positions[y][x] = JGOFNumericPlayerColor.BLACK; + } + for (let i = 0; i < score.white.scoring_positions.length; i += 2) { + const x = char2num(score.white.scoring_positions[i]); + const y = char2num(score.white.scoring_positions[i + 1]); + cur_ownership[y][x] = JGOFNumericPlayerColor.WHITE; + scoring_positions[y][x] = JGOFNumericPlayerColor.WHITE; + } + + for (let y = 0; y < board.length; ++y) { + for (let x = 0; x < board[y].length; ++x) { + if (board[y][x] !== JGOFNumericPlayerColor.EMPTY) { + cur_ownership[y][x] = board[y][x]; + } + } + } + + const groups = new StoneStringBuilder( + new BoardState({ + board, + removal, + }), + engine.board, + ); + + if (initial_state === real_initial_state) { + substage("Unsealed"); + for (let y = 0; y < cur_ownership.length; ++y) { + final_ownership[y] = cur_ownership[y].slice(); + } + } else { + substage("Sealed"); + for (let y = 0; y < cur_ownership.length; ++y) { + final_sealed_ownership[y] = cur_ownership[y].slice(); + } + } + + debug_groups("groups", groups); + debug_board_output(`Scoring positions (${rules})`, scoring_positions); + debug_board_output("Board", board); + debug_board_output("Ownership", cur_ownership); + } + //debug_boolean_board("Sealed", sealed, "s"); + + const print_final_ownership_string = false; + // aid while correcting and forming the test files + if (print_final_ownership_string) { + let ownership_string = '\n "correct_ownership": [\n'; + for (let y = 0; y < final_ownership.length; ++y) { + ownership_string += ' "'; + for (let x = 0; x < final_ownership[y].length; ++x) { + if (sealed[y][x]) { + ownership_string += "s"; + } else { + ownership_string += + final_ownership[y][x] === 1 + ? "B" + : final_ownership[y][x] === 2 + ? "W" + : " "; + } + } + if (y !== final_ownership.length - 1) { + ownership_string += '",\n'; + } else { + ownership_string += '"\n'; + } + } + + ownership_string += " ]\n"; + + stage_log(ownership_string); + } + } +} + +function debug_ownership_output(title: string, ownership: number[][]) { + begin_board(title); + let out = " "; + const x_coords = "ABCDEFGHJKLMNOPQRST"; // cspell: disable-line + + for (let x = 0; x < ownership[0].length; ++x) { + out += `${x_coords[x]}`; + } + out += "\n"; + + for (let y = 0; y < ownership.length; ++y) { + out += ` ${ownership.length - y} `.substr(-3); + for (let x = 0; x < ownership[y].length; ++x) { + //out += ` ${(" " + ownership[y][x].toFixed(1)).substr(-4)} `; + out += colorizeOwnership(ownership[y][x]); + } + out += " " + ` ${ownership.length - y} `.substr(-3); + out += "\n"; + } + + out += " "; + for (let x = 0; x < ownership[0].length; ++x) { + out += `${x_coords[x]}`; + } + out += "\n"; + + out += "\n"; + + board_output(out); + end_board(); +} + +function colorizeOwnership(ownership: number): string { + const mag = Math.round(Math.abs(ownership * 10)); + let mag_str = ""; + if (mag > 9) { + mag_str = ownership > 0 ? "B" : "W"; + } else { + mag_str = mag.toString(); + } + + if (mag < 7) { + if (ownership > 0) { + return blue(mag_str); + } else { + return cyanBright(mag_str); + } + } + if (ownership > 0) { + return blackBright(mag_str); + } else { + return whiteBright(mag_str); + } +} + +function debug_board_output(title: string, board: JGOFNumericPlayerColor[][]) { + begin_board(title); + let out = " "; + const x_coords = "ABCDEFGHJKLMNOPQRST"; // cspell: disable-line + + for (let x = 0; x < board[0].length; ++x) { + out += `${x_coords[x]}`; + } + out += "\n"; + + for (let y = 0; y < board.length; ++y) { + out += ` ${board.length - y} `.substr(-3); + for (let x = 0; x < board[y].length; ++x) { + let c = ""; + if (board[y][x] === 0) { + c = "."; + } else if (board[y][x] === 1) { + c = "B"; + } else if (board[y][x] === 2) { + c = "W"; + } else { + c = "?"; + } + out += colorizeIntersection(c); + } + + out += " " + ` ${board.length - y} `.substr(-3); + out += "\n"; + } + + out += " "; + for (let x = 0; x < board[0].length; ++x) { + out += `${x_coords[x]}`; + } + out += "\n"; + + out += "\n"; + board_output(out); + end_board(); +} + +function debug_board_string_output(title: string, board: string[][]) { + begin_board(title); + let out = " "; + const x_coords = "ABCDEFGHJKLMNOPQRST"; // cspell: disable-line + + for (let x = 0; x < board[0].length; ++x) { + out += `${x_coords[x]}`; + } + out += "\n"; + + for (let y = 0; y < board.length; ++y) { + out += ` ${board.length - y} `.substr(-3); + for (let x = 0; x < board[y].length; ++x) { + out += colorizeIntersection(board[y][x]); + } + + out += " " + ` ${board.length - y} `.substr(-3); + out += "\n"; + } + + out += " "; + for (let x = 0; x < board[0].length; ++x) { + out += `${x_coords[x]}`; + } + out += "\n"; + + out += "\n"; + board_output(out); + end_board(); +} + +function colorizeIntersection(c: string): string { + if (c === "B" || c === "S") { + return black(c); + } else if (c === "W") { + return whiteBright(c); + } else if (c === "?") { + return red(c); + } else if (c === "x") { + return red(c); + } else if (c === "e") { + return magenta(c); + } else if (c === "s") { + return magenta(c); + } else if (c === ".") { + return blue(c); + } else if (c === " " || c === "_") { + return blue("_"); + } + return yellow(c); +} + +function debug_boolean_board(title: string, board: (boolean | number)[][], mark = "S") { + begin_board(title); + let out = " "; + const x_coords = "ABCDEFGHJKLMNOPQRST"; // cspell: disable-line + + for (let x = 0; x < board[0].length; ++x) { + out += `${x_coords[x]}`; + } + out += "\n"; + + for (let y = 0; y < board.length; ++y) { + out += ` ${board.length - y} `.substr(-3); + for (let x = 0; x < board[y].length; ++x) { + out += colorizeIntersection(board[y][x] ? mark : " "); + } + + out += " " + ` ${board.length - y} `.substr(-3); + out += "\n"; + } + + out += " "; + for (let x = 0; x < board[0].length; ++x) { + out += `${x_coords[x]}`; + } + out += "\n"; + + out += "\n"; + board_output(out); + end_board(); +} + +function debug_groups(title: string, groups: StoneStringBuilder) { + const group_map: string[][] = makeMatrix( + groups.stone_string_id_map[0].length, + groups.stone_string_id_map.length, + "", + ); + const symbols = "1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; + + let group_idx = 0; + + groups.foreachGroup((group) => { + let group_color = red; + + if (group.color === JGOFNumericPlayerColor.EMPTY) { + //if (group.is_territory_in_seki) { + if (false) { + group_color = yellow; + } else if (group.is_territory) { + if (group.territory_color) { + group_color = + group.territory_color === JGOFNumericPlayerColor.BLACK ? black : white; + } else { + group_color = blue; + } + } else { + group_color = magenta; + } + } else if (group.color === JGOFNumericPlayerColor.BLACK) { + group_color = black; + } else if (group.color === JGOFNumericPlayerColor.WHITE) { + group_color = white; + } else { + group_color = red; + } + + const symbol = symbols[group_idx % symbols.length]; + + group.map((point) => { + group_map[point.y][point.x] = group_color(symbol); + }); + group_idx++; + }); + + debug_group_map(title, group_map); +} + +function debug_group_map(title: string, board: string[][]) { + begin_board(title); + let out = " "; + const x_coords = "ABCDEFGHJKLMNOPQRST"; // cspell: disable-line + + for (let x = 0; x < board[0].length; ++x) { + out += `${x_coords[x]}`; + } + out += "\n"; + + for (let y = 0; y < board.length; ++y) { + out += ` ${board.length - y} `.substr(-3); + for (let x = 0; x < board[y].length; ++x) { + out += board[y][x]; + } + + out += " " + ` ${board.length - y} `.substr(-3); + out += "\n"; + } + + out += " "; + for (let x = 0; x < board[0].length; ++x) { + out += `${x_coords[x]}`; + } + out += "\n"; + + out += "\n"; + board_output(out); + end_board(); +} + +function white(str: string) { + return `\x1b[37m${str}\x1b[0m`; +} +function red(str: string) { + return `\x1b[31m${str}\x1b[0m`; +} +/* +function green(str: string) { + return `\x1b[32m${str}\x1b[0m`; +} +*/ +function yellow(str: string) { + return `\x1b[33m${str}\x1b[0m`; +} +function blue(str: string) { + return `\x1b[34m${str}\x1b[0m`; +} +function magenta(str: string) { + return `\x1b[35m${str}\x1b[0m`; +} +/* +function cyan(str: string) { + return `\x1b[36m${str}\x1b[0m`; +} +*/ +function black(str: string) { + return `\x1b[30m${str}\x1b[0m`; +} +function whiteBright(str: string) { + return `\x1b[97m${str}\x1b[0m`; +} +function cyanBright(str: string) { + return `\x1b[96m${str}\x1b[0m`; +} +function blackBright(str: string) { + return `\x1b[90m${str}\x1b[0m`; +} + +function count_color_code_characters(str: string): number { + let count = 0; + for (let i = 0; i < str.length; ++i) { + if (str[i] === "\x1b") { + count++; // for x1b + while (str[i] !== "m") { + ++i; + ++count; + } + } + } + return count; +} + +/******************************/ +/*** Debug output functions ***/ +/******************************/ + +type DebugOutput = string; + +let final_output = ""; +let current_stage = ""; +let board_outputs: string[] = []; +let current_board_output = ""; +let current_stage_log = ""; + +function stage(name: string) { + end_stage(); + current_stage = name; + const title_line = `#### ${current_stage} ####`; + const pounds = "#".repeat(title_line.length); + final_output += `\n\n${pounds}\n${title_line}\n${pounds}\n\n`; +} + +function substage(name: string) { + end_stage(); + + current_stage = name; + const title_line = `==== ${current_stage} ====`; + final_output += `${title_line}\n\n`; +} + +function stage_log(str: string) { + current_stage_log += " " + str + "\n"; +} + +function end_stage() { + end_board(); + + if (!current_stage) { + return; + } + + current_stage = ""; + + const boards_per_line = 5; + + while (board_outputs.length > 0) { + const wide_lines: string[] = []; + const str_grid: string[][] = []; + const segment_length = 30; + + for (let x = 0; x < board_outputs.length && x < boards_per_line; ++x) { + const lines = board_outputs[x].split("\n"); + for (let y = 0; y < lines.length; ++y) { + if (!str_grid[y]) { + str_grid[y] = []; + } + str_grid[y][x] = lines[y]; + } + } + + board_outputs = board_outputs.slice(boards_per_line); + + for (let y = 0; y < str_grid.length; ++y) { + let line = ""; + for (let x = 0; x < str_grid[y].length; ++x) { + //const segment = str_grid[y][x] ?? ""; + /* + const segment = + ((str_grid[y][x] ?? "") + " ".repeat(segment_length)).substr(segment_length) + " "; + line += segment; + */ + const num_color_code_characters = count_color_code_characters(str_grid[y][x] ?? ""); + const length_without_color_codes = + (str_grid[y][x]?.length ?? 0) - num_color_code_characters; + if (length_without_color_codes < 0) { + throw new Error("length_without_color_codes < 0"); + } + line += + (str_grid[y][x] ?? "") + + " ".repeat(Math.max(0, segment_length - length_without_color_codes)) + + " "; + } + final_output += line + "\n"; + //wide_lines.push(line); + } + + for (let i = 0; i < wide_lines.length; ++i) { + final_output += wide_lines[i] + "\n"; + } + } + + if (current_stage_log) { + final_output += "\n\nLog:\n" + current_stage_log + "\n"; + current_stage_log = ""; + } +} + +function begin_board(name: string) { + end_board(); + current_board_output = `${name}\n`; +} + +function end_board() { + if (!current_board_output) { + return; + } + board_outputs.push(current_board_output); + current_board_output = ""; +} + +function board_output(str: string) { + current_board_output += str; +} + +function finalize_debug_output(): string { + end_stage(); + const ret = final_output; + board_outputs = []; + final_output = ""; + + let legend = ""; + legend += "Stone string coloring legend (not boolean maps):\n"; + legend += " " + black("Black") + "\n"; + legend += " " + white("White") + "\n"; + legend += " " + blue("Dame") + "\n"; + //legend += " " + yellow("Territory in Seki") + "\n"; + legend += " " + magenta("Undecided territory") + "\n"; + legend += " " + red("Error") + "\n"; + + return legend + ret; +} diff --git a/src/AdHocFormat.ts b/src/engine/formats/AdHocFormat.ts similarity index 100% rename from src/AdHocFormat.ts rename to src/engine/formats/AdHocFormat.ts diff --git a/src/JGOF.ts b/src/engine/formats/JGOF.ts similarity index 95% rename from src/JGOF.ts rename to src/engine/formats/JGOF.ts index f41885e5..ad70cd56 100644 --- a/src/JGOF.ts +++ b/src/engine/formats/JGOF.ts @@ -14,10 +14,10 @@ * limitations under the License. */ -import { GobanMoveErrorMessageId } from "./GobanError"; +import { GobanMoveErrorMessageId } from "../GobanError"; /** - * JSON Go Format + * JGOF (JSON Go Format) is an attempt at normalizing the AdHocFormat. */ export interface JGOF { @@ -50,6 +50,12 @@ export interface JGOFIntersection { y: number; } +export interface JGOFSealingIntersection extends JGOFIntersection { + /** Color the intersection is probably presumed to be by the players, but + * is in fact empty. */ + color: JGOFNumericPlayerColor; +} + export interface JGOFPlayer { /** Name or username of the player */ name: string; @@ -90,6 +96,9 @@ export interface JGOFMove extends JGOFIntersection { // while it was their turn to make a move sgf_downloaded_by?: Array; // Array of users who downloaded the // game SGF before this move was made + + /** Stone removal reasoning, primarily for debugging */ + removal_reason?: string; } /*********/ diff --git a/src/engine/formats/index.ts b/src/engine/formats/index.ts new file mode 100644 index 00000000..d81a6972 --- /dev/null +++ b/src/engine/formats/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright (C) Online-Go.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export * from "./AdHocFormat"; +export * from "./JGOF"; diff --git a/src/engine.ts b/src/engine/index.ts similarity index 66% rename from src/engine.ts rename to src/engine/index.ts index 68ebfe33..a7162b11 100644 --- a/src/engine.ts +++ b/src/engine/index.ts @@ -14,20 +14,19 @@ * limitations under the License. */ +export * from "./BoardState"; +export * from "./GobanEngine"; +export * from "./autoscore"; +export * from "../GobanBase"; export * from "./GobanError"; -export * from "./GoStoneGroup"; -export * from "./GoUtil"; -export * from "./GoEngine"; -export * from "./GoConditionalMove"; +export * from "./GobanSocket"; +export * from "./ConditionalMoveTree"; export * from "./MoveTree"; -export * from "./translate"; +export * from "./ownership_estimators"; export * from "./ScoreEstimator"; -export * from "./JGOF"; -export * from "./AdHocFormat"; -export * from "./AIReview"; -export * from "./Misc"; -export * from "./test_utils"; -export * from "./autoscore"; +export * from "./StoneString"; +export * from "./StoneStringBuilder"; +export * from "./formats"; +export * from "./util"; -export * from "./GoMath"; -export * as GoMath from "./GoMath"; +export * as translate from "./translate"; diff --git a/src/messages.ts b/src/engine/messages.ts similarity index 98% rename from src/messages.ts rename to src/engine/messages.ts index 87924f84..cdd59ef6 100644 --- a/src/messages.ts +++ b/src/engine/messages.ts @@ -38,7 +38,7 @@ export function formatMessage(message_id: MessageID, parameters?: { [key: string return _("Loading..."); case "processing": return _("Processing..."); - case "move_is_suicidal": + case "illegal_self_capture": case "self_capture_not_allowed": return _("Self-capture is not allowed"); case "server_message": diff --git a/src/local_estimators/__tests__/voronoi.test.ts b/src/engine/ownership_estimators/__tests__/voronoi_estimator.test.ts similarity index 87% rename from src/local_estimators/__tests__/voronoi.test.ts rename to src/engine/ownership_estimators/__tests__/voronoi_estimator.test.ts index 930f1457..d93dd237 100644 --- a/src/local_estimators/__tests__/voronoi.test.ts +++ b/src/engine/ownership_estimators/__tests__/voronoi_estimator.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { estimateScoreVoronoi } from "../voronoi"; +import { voronoi_estimate_ownership } from "../voronoi_estimator"; test("one color only scores board for that color", () => { const board = [ @@ -23,7 +23,7 @@ test("one color only scores board for that color", () => { [0, 0, 0], ]; - expect(estimateScoreVoronoi(board)).toEqual([ + expect(voronoi_estimate_ownership(board)).toEqual([ [1, 1, 1], [1, 1, 1], [1, 1, 1], @@ -40,7 +40,7 @@ test("border is one stone wide", () => { [0, 0, 0, 0, 0, 0], ]; - expect(estimateScoreVoronoi(board)).toEqual([ + expect(voronoi_estimate_ownership(board)).toEqual([ [1, 1, 1, 1, 1, 0], [1, 1, 1, 1, 0, -1], [1, 1, 1, 0, -1, -1], diff --git a/src/engine/ownership_estimators/index.ts b/src/engine/ownership_estimators/index.ts new file mode 100644 index 00000000..1782887d --- /dev/null +++ b/src/engine/ownership_estimators/index.ts @@ -0,0 +1,19 @@ +/* + * Copyright (C) Online-Go.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export * from "./remote_estimator"; +export * from "./voronoi_estimator"; +export * from "./wasm_estimator"; diff --git a/src/engine/ownership_estimators/remote_estimator.ts b/src/engine/ownership_estimators/remote_estimator.ts new file mode 100644 index 00000000..371d9c65 --- /dev/null +++ b/src/engine/ownership_estimators/remote_estimator.ts @@ -0,0 +1,28 @@ +/* + * Copyright (C) Online-Go.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { ScoreEstimateRequest, ScoreEstimateResponse } from "../ScoreEstimator"; + +export let remote_estimate_ownership: + | ((req: ScoreEstimateRequest) => Promise) + | undefined; + +/* Sets the callback to use to preform an ownership estimate */ +export function init_remote_ownership_estimator( + scorer: (req: ScoreEstimateRequest) => Promise, +): void { + remote_estimate_ownership = scorer; +} diff --git a/src/local_estimators/voronoi.ts b/src/engine/ownership_estimators/voronoi_estimator.ts similarity index 94% rename from src/local_estimators/voronoi.ts rename to src/engine/ownership_estimators/voronoi_estimator.ts index cd0d2b7a..48244e49 100644 --- a/src/local_estimators/voronoi.ts +++ b/src/engine/ownership_estimators/voronoi_estimator.ts @@ -14,16 +14,16 @@ * limitations under the License. */ -import { dup } from "../GoUtil"; +import { cloneMatrix } from "engine/util"; /** * This estimator simply marks territory for whichever color has a * closer stone (Manhattan distance). See discussion at * https://forums.online-go.com/t/weak-score-estimator-and-japanese-rules/41041/70 */ -export function estimateScoreVoronoi(board: number[][]) { +export function voronoi_estimate_ownership(board: number[][]) { const { width, height } = get_dims(board); - const ownership: number[][] = dup(board); + const ownership: number[][] = cloneMatrix(board); let points = getPoints(board, (pt) => pt !== 0); while (points.length) { const unvisited = points diff --git a/src/local_estimators/wasm_estimator.ts b/src/engine/ownership_estimators/wasm_estimator.ts similarity index 89% rename from src/local_estimators/wasm_estimator.ts rename to src/engine/ownership_estimators/wasm_estimator.ts index 785b71f7..e24842cc 100644 --- a/src/local_estimators/wasm_estimator.ts +++ b/src/engine/ownership_estimators/wasm_estimator.ts @@ -14,14 +14,14 @@ * limitations under the License. */ +import { makeMatrix } from "engine/util"; + /* The OGSScoreEstimator method is a wasm compiled C program that * does simple random playouts. On the client, the OGSScoreEstimator script * is loaded in an async fashion, so at some point that global variable * becomes not null and can be used. */ -import * as GoMath from "../GoMath"; - declare const CLIENT: boolean; declare let OGSScoreEstimator: any; @@ -30,7 +30,7 @@ let OGSScoreEstimatorModule: any; let init_promise: Promise; -export function init_score_estimator(): Promise { +export function init_wasm_ownership_estimator(): Promise { if (!CLIENT) { throw new Error("Only initialize WASM library on the client side"); } @@ -90,7 +90,7 @@ export function init_score_estimator(): Promise { } } -export function estimateScoreWasm( +export function wasm_estimate_ownership( board: number[][], color_to_move: "black" | "white", trials: number, @@ -98,7 +98,7 @@ export function estimateScoreWasm( ) { const width = board[0].length; const height = board.length; - const ownership = GoMath.makeMatrix(width, height, 0); + const ownership = makeMatrix(width, height, 0); if (!OGSScoreEstimator_initialized) { console.warn("Score estimator not initialized yet, uptime = " + performance.now()); @@ -116,21 +116,21 @@ export function estimateScoreWasm( ++i; } } - const _estimate = OGSScoreEstimatorModule.cwrap("estimate", "number", [ + + const estimate = OGSScoreEstimatorModule.cwrap("estimate", "number", [ "number", "number", "number", "number", "number", "number", - ]); - const estimate = _estimate as ( - w: number, - h: number, - p: number, - c: number, - tr: number, - to: number, + ]) as ( + width: number, + height: number, + ptr: number, + color_to_move: number, + trials: number, + tolerance: number, ) => number; estimate(width, height, ptr, color_to_move === "black" ? 1 : -1, trials, tolerance); diff --git a/src/protocol/AIServerToClient.ts b/src/engine/protocol/AIServerToClient.ts similarity index 100% rename from src/protocol/AIServerToClient.ts rename to src/engine/protocol/AIServerToClient.ts diff --git a/src/protocol/ClientToAIServer.ts b/src/engine/protocol/ClientToAIServer.ts similarity index 98% rename from src/protocol/ClientToAIServer.ts rename to src/engine/protocol/ClientToAIServer.ts index 4329accd..cabcce4d 100644 --- a/src/protocol/ClientToAIServer.ts +++ b/src/engine/protocol/ClientToAIServer.ts @@ -15,7 +15,7 @@ */ import { ClientToServerBase, RuleSet } from "./ClientToServer"; -import { JGOFNumericPlayerColor } from "../JGOF"; +import { JGOFNumericPlayerColor } from "../formats/JGOF"; /** This is an exhaustive list of the messages that the client can send * to the AI servers. */ diff --git a/src/protocol/ClientToServer.ts b/src/engine/protocol/ClientToServer.ts similarity index 95% rename from src/protocol/ClientToServer.ts rename to src/engine/protocol/ClientToServer.ts index 18bfe4af..4123a75e 100644 --- a/src/protocol/ClientToServer.ts +++ b/src/engine/protocol/ClientToServer.ts @@ -14,9 +14,9 @@ * limitations under the License. */ -import type { JGOFPlayerClock } from "../JGOF"; -import type { ReviewMessage } from "../GoEngine"; -import type { ConditionalMoveResponse } from "../GoConditionalMove"; +import type { JGOFMove, JGOFPlayerClock, JGOFSealingIntersection } from "../formats/JGOF"; +import type { ReviewMessage } from "../GobanEngine"; +import type { ConditionalMoveResponse } from "../ConditionalMoveTree"; /** Messages that clients send, regardless of target server */ export interface ClientToServerBase { @@ -149,8 +149,15 @@ export interface ClientToServer extends ClientToServerBase { * not removed / open area. */ removed: boolean; - /** String encoded list of intersections */ - stones: string; + /** List of intersections that are to be removed. */ + stones: JGOFMove[] | string; + + /** List of intersections that need to be sealed before the game can be + * correctly scored. Note, if this is undefined, the value will not + * be changed on the server side. To specify there are no more intersections + * that need to be cleared, set it to `[]` specifically. + */ + needs_sealing?: JGOFSealingIntersection[]; /** Japanese rules technically have some special scoring rules about * whether territory in seki should be counted or not. This is supported @@ -252,7 +259,7 @@ export interface ClientToServer extends ClientToServerBase { }) => void; /** Cancels a game. This is effectively the same as resign, except the * game will not be ranked. This is only allowed within the first few - * moves of the game. (See GoEngine.gameCanBeCancelled for cancelation ) */ + * moves of the game. (See GobanEngine.gameCanBeCancelled for cancellation ) */ "game/cancel": (data: { /** The game id */ game_id: number; @@ -670,6 +677,22 @@ export interface GameListWhere { malk_only?: boolean; } +interface GameListPlayer { + username: string; + id: number; + rank: number; + professional: boolean; + accepted: boolean; + ratings: { + version: number; + overall: { + rating: number; + deviation: number; + volatility: number; + }; + }; +} + export interface GameListEntry { id: number; group_ids?: Array; @@ -683,36 +706,8 @@ export interface GameListEntry { move_number: number; paused: boolean; private: boolean; - black: { - username: string; - id: number; - rank: number; - professional: boolean; - accepted: boolean; - ratings: { - version: number; - overall: { - rating: number; - deviation: number; - volatility: number; - }; - }; - }; - white: { - username: string; - id: number; - rank: number; - professional: boolean; - accepted: boolean; - ratings: { - version: number; - overall: { - rating: number; - deviation: number; - volatility: number; - }; - }; - }; + black: GameListPlayer; + white: GameListPlayer; rengo: boolean; rengo_teams: { diff --git a/src/protocol/ServerToClient.ts b/src/engine/protocol/ServerToClient.ts similarity index 98% rename from src/protocol/ServerToClient.ts rename to src/engine/protocol/ServerToClient.ts index 25feecc2..f3bb8cdf 100644 --- a/src/protocol/ServerToClient.ts +++ b/src/engine/protocol/ServerToClient.ts @@ -20,10 +20,10 @@ import type { AutomatchPreferences, RemoteStorageReplication, } from "./ClientToServer"; -import type { JGOFTimeControl } from "../JGOF"; -import type { ConditionalMoveResponse } from "../GoConditionalMove"; -import type { GoEngineConfig, Score, ReviewMessage } from "../GoEngine"; -import type { AdHocPackedMove } from "../AdHocFormat"; +import type { JGOFTimeControl } from "../formats/JGOF"; +import type { ConditionalMoveResponse } from "../ConditionalMoveTree"; +import type { GobanEngineConfig, Score, ReviewMessage } from "../GobanEngine"; +import type { AdHocPackedMove } from "../formats/AdHocFormat"; /* NOTE: The reason for the :id non template literal key variants of our * messages is to allow typedoc generate documentation for them, @@ -401,7 +401,7 @@ export interface ServerToClient { [k: `game/${number}/error`]: ServerToClient["game/:id/error"]; /** Update the entire game state */ - "game/:id/gamedata": (data: GoEngineConfig) => void; + "game/:id/gamedata": (data: GobanEngineConfig) => void; [k: `game/${number}/gamedata`]: ServerToClient["game/:id/gamedata"]; /** Update latency information for a player */ diff --git a/src/protocol/index.ts b/src/engine/protocol/index.ts similarity index 100% rename from src/protocol/index.ts rename to src/engine/protocol/index.ts diff --git a/src/translate.ts b/src/engine/translate.ts similarity index 100% rename from src/translate.ts rename to src/engine/translate.ts diff --git a/src/AIReview.ts b/src/engine/util/ai_review_utils.ts similarity index 98% rename from src/AIReview.ts rename to src/engine/util/ai_review_utils.ts index 32e0b70e..f16f47f3 100644 --- a/src/AIReview.ts +++ b/src/engine/util/ai_review_utils.ts @@ -14,8 +14,8 @@ * limitations under the License. */ -import { JGOFAIReview, JGOFIntersection, JGOFNumericPlayerColor } from "./JGOF"; -import { MoveTree } from "./MoveTree"; +import { JGOFAIReview, JGOFIntersection, JGOFNumericPlayerColor } from "../formats/JGOF"; +import { MoveTree } from "../MoveTree"; export interface AIReviewWorstMoveEntry { player: JGOFNumericPlayerColor; diff --git a/src/engine/util/color.ts b/src/engine/util/color.ts new file mode 100644 index 00000000..9274ef35 --- /dev/null +++ b/src/engine/util/color.ts @@ -0,0 +1,45 @@ +/* + * Copyright (C) Online-Go.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** Simple 50% blend of two colors in hex format */ +export function color_blend(c1: string, c2: string): string { + const c1_rgb = hexToRgb(c1); + const c2_rgb = hexToRgb(c2); + const blend = (a: number, b: number) => Math.round((a + b) / 2); + return rgbToHex( + blend(c1_rgb.r, c2_rgb.r), + blend(c1_rgb.g, c2_rgb.g), + blend(c1_rgb.b, c2_rgb.b), + ); +} + +/** Convert hex color to RGB */ +function hexToRgb(hex: string): { r: number; g: number; b: number } { + const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); + if (!result) { + throw new Error("invalid hex color"); + } + return { + r: parseInt(result[1], 16), + g: parseInt(result[2], 16), + b: parseInt(result[3], 16), + }; +} + +/** Convert RGB color to hex */ +function rgbToHex(r: number, g: number, b: number): string { + return "#" + [r, g, b].map((c) => c.toString(16).padStart(2, "0")).join(""); +} diff --git a/src/engine/util/computeAverageMoveTime.ts b/src/engine/util/computeAverageMoveTime.ts new file mode 100644 index 00000000..de617598 --- /dev/null +++ b/src/engine/util/computeAverageMoveTime.ts @@ -0,0 +1,74 @@ +/* + * Copyright (C) Online-Go.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { JGOFTimeControl } from "engine/formats"; + +/** + * Compute the expected average time per move for a given time control. + */ + +export function computeAverageMoveTime( + time_control: JGOFTimeControl, + w?: number, + h?: number, +): number { + if (typeof time_control !== "object" || time_control === null) { + console.error( + `computeAverageMoveTime passed ${time_control} instead of a time_control object`, + ); + return time_control; + } + const moves = w && h ? averageMovesPerGame(w, h) / 2 : 90; + + try { + let t: number; + switch (time_control.system) { + case "fischer": + t = time_control.initial_time / moves + time_control.time_increment; + break; + case "byoyomi": + t = time_control.main_time / moves + time_control.period_time; + break; + case "simple": + t = time_control.per_move; + break; + case "canadian": + t = + time_control.main_time / moves + + time_control.period_time / time_control.stones_per_period; + break; + case "absolute": + t = time_control.total_time / moves; + break; + case "none": + t = 0; + break; + } + return Math.round(t); + } catch (err) { + console.error("Error computing average move time for time control: ", time_control); + console.error(err); + return 60; + } +} +/** + * Rough estimate of the average number of moves in a game based on height on + * and width. See discussion here: + * https://forums.online-go.com/t/average-game-length-on-different-board-sizes/35042/11 + */ +function averageMovesPerGame(w: number, h: number): number { + return Math.round(0.7 * w * h); +} diff --git a/src/engine/util/coordinates.ts b/src/engine/util/coordinates.ts new file mode 100644 index 00000000..d99a7df2 --- /dev/null +++ b/src/engine/util/coordinates.ts @@ -0,0 +1,69 @@ +/* + * Copyright (C) Online-Go.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { JGOFMove } from "engine/formats"; + +/* Lower case, includes i, used for our string encoding of moves */ +const COORDINATE_SEQUENCE = "abcdefghijklmnopqrstuvwxyz"; + +/* Upper case, and doesn't have I */ +const PRETTY_COORDINATE_SEQUENCE = "ABCDEFGHJKLMNOPQRSTUVWXYZ"; + +/** Decodes a single coordinate to a number */ +export function decodeCoordinate(ch: string): number { + return COORDINATE_SEQUENCE.indexOf(ch?.toLowerCase()); +} + +/** Encodes a single coordinate to a number */ +export function encodeCoordinate(coor: number): string { + return COORDINATE_SEQUENCE[coor]; +} + +/** Decodes the pretty X coordinate to a number */ +export function decodePrettyXCoordinate(ch: string): number { + return PRETTY_COORDINATE_SEQUENCE.indexOf(ch?.toUpperCase()); +} + +/** Encodes an X coordinate to a display encoding */ +export function encodePrettyXCoordinate(coor: number): string { + return PRETTY_COORDINATE_SEQUENCE[coor]; +} + +/** Encodes an x,y pair to "pretty" coordinates, like `"A3"`, or `"K10"` */ +export function prettyCoordinates(x: number, y: number, board_height: number): string { + if (x >= 0) { + return encodePrettyXCoordinate(x) + ("" + (board_height - y)); + } + return "pass"; +} + +/** Decodes GTP coordinates to a JGOFMove */ +export function decodeGTPCoordinates(move: string, width: number, height: number): JGOFMove { + if (move === ".." || move.toLowerCase() === "pass") { + return { x: -1, y: -1 }; + } + let y = height - parseInt(move.substr(1)); + const x = decodePrettyXCoordinate(move[0]); + if (x === -1) { + y = -1; + } + return { x, y }; +} + +/** Decodes pretty coordinates to a JGOFMove, this is an alias of decodeGTPCoordinates */ +export function decodePrettyCoordinates(move: string, height: number): JGOFMove { + return decodeGTPCoordinates(move, -1, height); +} diff --git a/src/engine/util/duration_strings.ts b/src/engine/util/duration_strings.ts new file mode 100644 index 00000000..c4def44c --- /dev/null +++ b/src/engine/util/duration_strings.ts @@ -0,0 +1,37 @@ +/* + * Copyright (C) Online-Go.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { interpolate, _ } from "../translate"; + +/** Takes a number of seconds and returns a string like "1d 3h 2m 52s" */ +export function shortDurationString(seconds: number) { + const weeks = Math.floor(seconds / (86400 * 7)); + seconds -= weeks * 86400 * 7; + const days = Math.floor(seconds / 86400); + seconds -= days * 86400; + const hours = Math.floor(seconds / 3600); + seconds -= hours * 3600; + const minutes = Math.floor(seconds / 60); + seconds -= minutes * 60; + return ( + "" + + (weeks ? " " + interpolate(_("%swk"), [weeks]) : "") + + (days ? " " + interpolate(_("%sd"), [days]) : "") + + (hours ? " " + interpolate(_("%sh"), [hours]) : "") + + (minutes ? " " + interpolate(_("%sm"), [minutes]) : "") + + (seconds ? " " + interpolate(_("%ss"), [seconds]) : "") + ); +} diff --git a/src/engine/util/getRandomInt.ts b/src/engine/util/getRandomInt.ts new file mode 100644 index 00000000..6c884a73 --- /dev/null +++ b/src/engine/util/getRandomInt.ts @@ -0,0 +1,19 @@ +/* + * Copyright (C) Online-Go.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** Returns a random integer between min (inclusive) and max (exclusive) */ +export function getRandomInt(min: number, max: number) { + return Math.floor(Math.random() * (max - min)) + min; +} diff --git a/src/engine/util/index.ts b/src/engine/util/index.ts new file mode 100644 index 00000000..ca4f404c --- /dev/null +++ b/src/engine/util/index.ts @@ -0,0 +1,29 @@ +/* + * Copyright (C) Online-Go.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export * from "./matrix"; +export * from "./color"; +export * from "./coordinates"; +export * from "./move_encoding"; +export * from "./sgf_utils"; +export * from "./niceInterval"; +export * from "./computeAverageMoveTime"; +export * from "./duration_strings"; +export * from "./positionId"; +export * from "./sortMoves"; +export * from "./getRandomInt"; +export * from "./object_utils"; +export * from "./ai_review_utils"; diff --git a/src/engine/util/matrix.ts b/src/engine/util/matrix.ts new file mode 100644 index 00000000..c3094a80 --- /dev/null +++ b/src/engine/util/matrix.ts @@ -0,0 +1,81 @@ +/* + * Copyright (C) Online-Go.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export type Matrix = T[][]; +export type NumberMatrix = Matrix; +export type StringMatrix = Matrix; + +/** Returns a cloned copy of the provided matrix */ +export function cloneMatrix(matrix: T[][]): T[][] { + const ret = new Array(matrix.length); + for (let i = 0; i < matrix.length; ++i) { + ret[i] = matrix[i].slice(); + } + return ret; +} + +/** + * Returns true if the contents of the two 2d matrices are equal when the + * cells are compared with === + */ +export function matricesAreEqual(m1: T[][], m2: T[][]): boolean { + if (m1.length !== m2.length) { + return false; + } + + for (let y = 0; y < m1.length; ++y) { + if (m1[y].length !== m2[y].length) { + return false; + } + + for (let x = 0; x < m1[0].length; ++x) { + if (m1[y][x] !== m2[y][x]) { + return false; + } + } + } + return true; +} + +export function makeMatrix(width: number, height: number, initialValue: T): Matrix { + const ret: Matrix = []; + for (let y = 0; y < height; ++y) { + ret.push([]); + for (let x = 0; x < width; ++x) { + ret[y].push(initialValue); + } + } + return ret; +} +export function makeObjectMatrix(width: number, height: number): Array> { + const ret = new Array>(height); + for (let y = 0; y < height; ++y) { + const row = new Array(width); + for (let x = 0; x < width; ++x) { + row[x] = {} as T; + } + ret[y] = row; + } + return ret; +} + +export function makeEmptyMatrix(width: number, height: number): Array> { + const ret = new Array>(height); + for (let y = 0; y < height; ++y) { + ret[y] = new Array(width); + } + return ret; +} diff --git a/src/engine/util/move_encoding.ts b/src/engine/util/move_encoding.ts new file mode 100644 index 00000000..2db31642 --- /dev/null +++ b/src/engine/util/move_encoding.ts @@ -0,0 +1,259 @@ +/* + * Copyright (C) Online-Go.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { JGOFMove, JGOFNumericPlayerColor, AdHocPackedMove } from "../formats"; +import { + decodeCoordinate, + decodeGTPCoordinates, + decodePrettyXCoordinate, + encodeCoordinate, +} from "./coordinates"; + +/** + * Decodes any of the various ways we express moves that we've accumulated over the years into + * a unified `JGOFMove[]`. + */ +export function decodeMoves( + move_obj: + | string + | AdHocPackedMove + | AdHocPackedMove[] + | JGOFMove + | JGOFMove[] + | [object] + | undefined, + width: number, + height: number, +): JGOFMove[] { + const ret: Array = []; + + // undefined or empty string? return empty array. + if (!move_obj) { + return []; + } + + function decodeSingleMoveArray(arr: [number, number, number, number?, object?]): JGOFMove { + const obj: JGOFMove = { + x: arr[0], + y: arr[1], + timedelta: arr.length > 2 ? arr[2] : -1, + color: (arr.length > 3 ? arr[3] : 0) as JGOFNumericPlayerColor, + }; + const extra: any = arr.length > 4 ? arr[4] : {}; + for (const k in extra) { + (obj as any)[k] = extra[k]; + } + return obj; + } + + if (move_obj instanceof Array) { + if (move_obj.length === 0) { + return []; + } + if (typeof move_obj[0] === "number") { + ret.push(decodeSingleMoveArray(move_obj as [number, number, number, number])); + } else { + if ( + typeof move_obj[0] === "object" && + "x" in move_obj[0] && + typeof move_obj[0].x === "number" + ) { + return move_obj as Array; + } + + for (let i = 0; i < move_obj.length; ++i) { + const mv: any = move_obj[i]; + if (mv instanceof Array && typeof mv[0] === "number") { + ret.push(decodeSingleMoveArray(mv as [number, number, number, number])); + } else { + throw new Error(`Unrecognized move format: ${mv}`); + } + } + } + } else if (typeof move_obj === "string") { + if (!height || !width) { + throw new Error( + `decodeMoves requires a height and width to be set when decoding a string coordinate`, + ); + } + + if (/[a-zA-Z][0-9]/.test(move_obj)) { + /* coordinate form, used from human input. */ + const move_string = move_obj; + + const moves = move_string.split(/([a-zA-Z][0-9]+|pass|[.][.])/); + for (let i = 0; i < moves.length; ++i) { + if (i % 2) { + /* even are the 'splits', which should always be blank unless there is an error */ + let x = pretty_char2num(moves[i][0]); + let y = height - parseInt(moves[i].substring(1)); + if ((width && x >= width) || x < 0) { + x = y = -1; + } + if ((height && y >= height) || y < 0) { + x = y = -1; + } + ret.push({ x: x, y: y, edited: false, color: 0 }); + } else { + if (moves[i] !== "") { + throw "Unparsed move input: " + moves[i]; + } + } + } + } else { + /* Pure letter encoded form, used for all records */ + const move_string = move_obj; + + for (let i = 0; i < move_string.length - 1; i += 2) { + let edited = false; + let color: JGOFNumericPlayerColor = 0; + if (move_string[i + 0] === "!") { + edited = true; + if (move_string.substr(i, 10) === "!undefined") { + /* bad data */ + color = 0; + i += 10; + } else { + color = parseInt(move_string[i + 1]) as JGOFNumericPlayerColor; + i += 2; + } + } + + let x = char2num(move_string[i]); + let y = char2num(move_string[i + 1]); + if (width && x >= width) { + x = y = -1; + } + if (height && y >= height) { + x = y = -1; + } + ret.push({ x: x, y: y, edited: edited, color: color }); + } + } + } else if (typeof move_obj === "object" && "x" in move_obj && typeof move_obj.x === "number") { + return [move_obj] as Array; + } else { + throw new Error("Invalid move format: " + JSON.stringify(move_obj)); + } + + return ret; +} +export function char2num(ch: string): number { + if (ch === ".") { + return -1; + } + return decodeCoordinate(ch); +} +function pretty_char2num(ch: string): number { + if (ch === ".") { + return -1; + } + return decodePrettyXCoordinate(ch); +} +export function num2char(num: number): string { + if (num === -1) { + return "."; + } + return encodeCoordinate(num); +} + +export function encodeMove(x: number | JGOFMove, y?: number): string { + if (typeof x === "number") { + if (typeof y !== "number") { + throw new Error(`Invalid y parameter to encodeMove y = ${y}`); + } + return num2char(x) + num2char(y); + } else { + const mv: JGOFMove = x; + + if (!mv.edited) { + return num2char(mv.x) + num2char(mv.y); + } else { + return "!" + mv.color + num2char(mv.x) + num2char(mv.y); + } + } +} + +/* Encodes a move list like [{x: 0, y: 0}, {x:1, y:2}] into our move string + * format "aabc" */ +export function encodeMoves(lst: JGOFMove[]): string { + let ret = ""; + for (let i = 0; i < lst.length; ++i) { + ret += encodeMove(lst[i]); + } + return ret; +} + +export function encodeMoveToArray(mv: JGOFMove): AdHocPackedMove { + // Note: despite the name here, AdHocPackedMove became a tuple at some point! + let extra: any = {}; + if (mv.blur) { + extra.blur = mv.blur; + } + if (mv.sgf_downloaded_by) { + extra.sgf_downloaded_by = mv.sgf_downloaded_by; + } + if (mv.played_by) { + extra.played_by = mv.played_by; + } + if (mv.player_update) { + extra.player_update = mv.player_update; + } + + // don't add an extra if there is nothing extra... + if (Object.keys(extra).length === 0) { + extra = undefined; + } + + const arr: AdHocPackedMove = [mv.x, mv.y, mv.timedelta ? mv.timedelta : -1, undefined, extra]; + if (mv.edited) { + arr[3] = mv.color; + if (!extra) { + arr.pop(); + } + } else { + if (!extra) { + arr.pop(); // extra + arr.pop(); // edited + } + } + return arr; +} +export function encodeMovesToArray(moves: Array): Array { + const ret: Array = []; + for (let i = 0; i < moves.length; ++i) { + ret.push(encodeMoveToArray(moves[i])); + } + return ret; +} + +// OJE Sequence format is '.root.K10.Q1' ... +export function ojeSequenceToMoves(sequence: string): Array { + const plays = sequence.split("."); + + if (plays.shift() !== "" || plays.shift() !== "root") { + throw new Error("Sequence passed to sequenceToMoves does not start with .root"); + } + + const moves = plays.map((play) => { + if (play === "pass") { + return { x: -1, y: -1 }; + } + return decodeGTPCoordinates(play, 19, 19); + }); + + return moves; +} diff --git a/src/engine/util/niceInterval.ts b/src/engine/util/niceInterval.ts new file mode 100644 index 00000000..4dd5a828 --- /dev/null +++ b/src/engine/util/niceInterval.ts @@ -0,0 +1,35 @@ +/* + * Copyright (C) Online-Go.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Like setInterval, but debounces catchups (multiple invocation in rapid + * succession less than our desired interval) that happen in some browsers when + * tabs wake up from sleep. Cleared with the standard clearInterval. + * */ +export function niceInterval( + callback: () => void, + interval: number, +): ReturnType { + let last = performance.now(); + return setInterval(() => { + const now = performance.now(); + const diff = now - last; + if (diff >= interval * 0.9) { + last = now; + callback(); + } + }, interval); +} diff --git a/src/engine/util/object_utils.ts b/src/engine/util/object_utils.ts new file mode 100644 index 00000000..657a3c0e --- /dev/null +++ b/src/engine/util/object_utils.ts @@ -0,0 +1,77 @@ +/* + * Copyright (C) Online-Go.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** Deep clones an object */ +export function deepClone(obj: any): any { + let ret: any; + if (typeof obj === "object") { + if (Array.isArray(obj)) { + ret = []; + for (let i = 0; i < obj.length; ++i) { + ret.push(deepClone(obj[i])); + } + } else { + ret = {}; + for (const i in obj) { + ret[i] = deepClone(obj[i]); + } + } + } else { + return obj; + } + return ret; +} + +/** Deep compares two objects */ +export function deepEqual(a: any, b: any) { + if (typeof a !== typeof b) { + return false; + } + + if (typeof a === "object") { + if (Array.isArray(a)) { + if (Array.isArray(b)) { + if (a.length !== b.length) { + return false; + } + for (let i = 0; i < a.length; ++i) { + if (!deepEqual(a[i], b[i])) { + return false; + } + } + } else { + return false; + } + } else { + for (const i in a) { + if (!(i in b)) { + return false; + } + if (!deepEqual(a[i], b[i])) { + return false; + } + } + for (const i in b) { + if (!(i in a)) { + return false; + } + } + } + return true; + } else { + return a === b; + } +} diff --git a/src/engine/util/positionId.ts b/src/engine/util/positionId.ts new file mode 100644 index 00000000..6c3530e9 --- /dev/null +++ b/src/engine/util/positionId.ts @@ -0,0 +1,79 @@ +/* + * Copyright (C) Online-Go.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { JGOFNumericPlayerColor } from "engine/formats"; +import { encodeMove } from "./move_encoding"; + +/** + * This is intended to be an "easy to understand" method of generating a unique id + * for a board position. + * + * The "id" is the list of all the positions of the stones, black first then white, + * separated by a colon. + * + * There are in fact 8 possible ways to list the positions (all the rotations and + * reflections of the position). The id is the lowest (alpha-numerically) of these. + * + * Colour independence for the position is achieved by takeing the lexically lower + * of the ids of the position with black and white reversed. + * + * The "easy to understand" part is that the id can be compared visually to the + * board position + * + * The downside is that the id string can be moderately long for boards with lots of stones + */ + +export type BoardTransform = (x: number, y: number) => { x: number; y: number }; +export function positionId( + position: Array>, + height: number, + width: number, +): string { + // The basic algorithm is to list where each of the stones are, in a long string. + // We do this once for each transform, selecting the lowest (lexically) as we go. + const transforms: Array = [ + (x, y) => ({ x, y }), + (x, y) => ({ x, y: height - y - 1 }), + (x, y) => ({ x: y, y: x }), + (x, y) => ({ x: y, y: width - x - 1 }), + (x, y) => ({ x: height - y - 1, y: x }), + (x, y) => ({ x: height - y - 1, y: width - x - 1 }), + (x, y) => ({ x: width - x - 1, y }), + (x, y) => ({ x: width - x - 1, y: height - y - 1 }), + ]; + + const ids = []; + + for (const transform of transforms) { + let black_state = ""; + let white_state = ""; + for (let x = 0; x < width; x++) { + for (let y = 0; y < height; y++) { + const c = transform(x, y); + if (position[x][y] === JGOFNumericPlayerColor.BLACK) { + black_state += encodeMove(c.x, c.y); + } + if (position[x][y] === JGOFNumericPlayerColor.WHITE) { + white_state += encodeMove(c.x, c.y); + } + } + } + + ids.push(`${black_state}.${white_state}`); + } + + return ids.reduce((prev, current) => (current < prev ? current : prev)); +} diff --git a/src/Misc.ts b/src/engine/util/sgf_utils.ts similarity index 89% rename from src/Misc.ts rename to src/engine/util/sgf_utils.ts index 3773c159..2aa6abe7 100644 --- a/src/Misc.ts +++ b/src/engine/util/sgf_utils.ts @@ -1,5 +1,5 @@ /* - * Copyright (C) Online-Go.com + * Copyright (C) Online-Go.com * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -54,7 +54,10 @@ export function escapeSGFText(txt: string, escapeColon: boolean = false): string return txt; } -// in SGF simple text, we also need to get rid of the newlines -export function newline2space(txt: string): string { +/** + * SGF "simple text", eg used in the LB property, we can't have newlines. This + * strips them and replaces them with spaces. + */ +export function newlines_to_spaces(txt: string): string { return txt.replace(/[\r\n]/g, " "); } diff --git a/src/engine/util/sortMoves.ts b/src/engine/util/sortMoves.ts new file mode 100644 index 00000000..6670d1ae --- /dev/null +++ b/src/engine/util/sortMoves.ts @@ -0,0 +1,41 @@ +/* + * Copyright (C) Online-Go.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { JGOFMove } from "../formats/JGOF"; +import { decodeMoves, encodeMoves } from "./move_encoding"; + +/** Returns a sorted move string, this is used in our stone removal logic */ +export function sortMoves(moves: string, width: number, height: number): string; +export function sortMoves(moves: JGOFMove[], width: number, height: number): JGOFMove[]; +export function sortMoves( + moves: string | JGOFMove[], + width: number, + height: number, +): string | JGOFMove[] { + if (moves instanceof Array) { + return moves.sort(compare_moves); + } else { + const arr = decodeMoves(moves, width, height); + arr.sort(compare_moves); + return encodeMoves(arr); + } +} + +function compare_moves(a: JGOFMove, b: JGOFMove): number { + const av = (a.edited ? 1 : 0) * 10000 + a.x + a.y * 100; + const bv = (b.edited ? 1 : 0) * 10000 + b.x + b.y * 100; + return av - bv; +} diff --git a/src/goban.ts b/src/index.ts similarity index 52% rename from src/goban.ts rename to src/index.ts index 1a5b4365..10eba92e 100644 --- a/src/goban.ts +++ b/src/index.ts @@ -14,40 +14,26 @@ * limitations under the License. */ -export * from "./GobanCore"; -export * from "./GobanCanvas"; -export * from "./GobanSVG"; -export * from "./GoConditionalMove"; -export * from "./GoEngine"; -export * from "./GobanError"; -export * from "./GoStoneGroup"; -export * from "./GoStoneGroups"; -export * from "./GoTheme"; -export * from "./GoThemes"; -export * from "./GoUtil"; -export * from "./canvas_utils"; -export * from "./MoveTree"; -export * from "./ScoreEstimator"; -export * from "./translate"; -export * from "./JGOF"; -export * from "./AIReview"; -export * from "./AdHocFormat"; -export * from "./TestGoban"; -export * from "./test_utils"; -export * from "./GobanSocket"; -export * from "./autoscore"; - -export * as GoMath from "./GoMath"; -export * as protocol from "./protocol"; -export { placeRenderedImageStone, preRenderImageStone } from "./themes/image_stones"; +export * from "engine"; +export * from "./GobanBase"; +export * from "./Goban/callbacks"; +export * from "./Goban/canvas_utils"; +export * from "./Goban/CanvasRenderer"; +export * from "./Goban/SVGRenderer"; +export * from "./Goban/themes"; +export * from "./Goban/Goban"; +export * from "./Goban/TestGoban"; // we export this for ui tests + +export * as protocol from "engine/protocol"; +export { placeRenderedImageStone, preRenderImageStone } from "./Goban/themes/image_stones"; //export { GobanCanvas as Goban, GobanCanvasConfig as GobanConfig } from "./GobanCanvas"; //export { GobanSVG as Goban, GobanSVGConfig as GobanConfig } from "./GobanSVG"; -import { GobanCanvas, GobanCanvasConfig } from "./GobanCanvas"; -import { GobanSVG, GobanSVGConfig } from "./GobanSVG"; +import { GobanCanvas, CanvasRendererGobanConfig } from "./Goban/CanvasRenderer"; +import { SVGRenderer, SVGRendererGobanConfig } from "./Goban/SVGRenderer"; -export type GobanRenderer = GobanCanvas | GobanSVG; -export type GobanRendererConfig = GobanCanvasConfig | GobanSVGConfig; +export type GobanRenderer = GobanCanvas | SVGRenderer; +export type GobanRendererConfig = CanvasRendererGobanConfig | SVGRendererGobanConfig; (window as any)["goban"] = module.exports; @@ -57,15 +43,14 @@ export function setGobanRenderer(_renderer: "svg" | "canvas") { renderer = _renderer; } -import { AdHocFormat } from "./AdHocFormat"; -import { JGOF } from "./JGOF"; +import { AdHocFormat, JGOF } from "engine"; export function createGoban( config: GobanRendererConfig, preloaded_data?: AdHocFormat | JGOF, ): GobanRenderer { if (renderer === "svg") { - return new GobanSVG(config, preloaded_data); + return new SVGRenderer(config, preloaded_data); } else { return new GobanCanvas(config, preloaded_data); } diff --git a/src/test.tsx b/src/test.tsx deleted file mode 100644 index a8867717..00000000 --- a/src/test.tsx +++ /dev/null @@ -1,690 +0,0 @@ -/* - * Copyright (C) Online-Go.com - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import * as React from "react"; -import * as ReactDOM from "react-dom/client"; -import { GobanCore, GobanConfig, GobanHooks, ColoredCircle } from "./GobanCore"; -//import { GobanPixi } from './GobanPixi'; -import { GobanCanvas, GobanCanvasConfig } from "./GobanCanvas"; -import { GobanSVG, GobanSVGConfig } from "./GobanSVG"; -import { EventEmitter } from "eventemitter3"; -import { MoveTreePenMarks } from "./MoveTree"; -import { GoThemes } from "./GoThemes"; - -let stored_config: GobanConfig = {}; -try { - stored_config = JSON.parse(localStorage.getItem("config") || "{}"); -} catch (e) {} - -GobanCore.hooks.getSelectedThemes = () => ({ - board: "Kaya", - //board: "Anime", - - white: "Plain", - black: "Plain", - //white: "Glass", - //black: "Glass", - //white: "Worn Glass", - //black: "Worn Glass", - //white: "Night", - //black: "Night", - //white: "Shell", - //black: "Slate", - //white: "Anime", - //black: "Anime", - //white: "Custom", - //black: "Custom", -}); - -GobanCore.hooks.customWhiteStoneUrl = () => { - return "https://cdn.online-go.com/goban/anime_white.svg"; -}; -GobanCore.hooks.customBlackStoneUrl = () => { - return "https://cdn.online-go.com/goban/anime_black.svg"; -}; - -const base_config: GobanConfig = Object.assign( - { - interactive: true, - mode: "puzzle", - //"player_id": 0, - //"server_socket": null, - square_size: 25, - original_sgf: ` - (;FF[4] - CA[UTF-8] - GM[1] - GN[ai japanese hc 9] - PC[https://online-go.com/review/290167] - PB[Black] - PW[White] - BR[3p] - WR[3p] - TM[0]OT[0 none] - RE[?] - SZ[19] - KM[6.5] - RU[Japanese] - - ;B[sh] - ;W[sk] - ;B[sn] - ;W[sp] - ) - `, - draw_top_labels: true, - draw_left_labels: true, - draw_right_labels: true, - draw_bottom_labels: true, - bounds: { - left: 0, - right: 18, - top: 0, - bottom: 18, - }, - }, - stored_config, -); - -const hooks: GobanHooks = { - //getCoordinateDisplaySystem: () => "1-1", - getCoordinateDisplaySystem: () => "A1", - getCDNReleaseBase: () => "", -}; -GobanCore.setHooks(hooks); - -function save() { - localStorage.setItem("config", JSON.stringify(base_config)); -} - -function clear() { - localStorage.remove("config"); -} -(window as any)["clear"] = clear; -/* - "getPuzzlePlacementSetting": () => { - return {"mode": "play"}; - }, - */ - -const fiddler = new EventEmitter(); - -function Main(): JSX.Element { - const [_update, _setUpdate] = React.useState(1); - const [svg_or_canvas, setSVGOrCanvas] = React.useState("svg"); - function forceUpdate() { - _setUpdate(_update + 1); - } - function redraw() { - save(); - forceUpdate(); - fiddler.emit("redraw"); - } - - return ( -
-
-
-
- {svg_or_canvas} mode:{" "} - -
- -
- Square size: - { - let ss = Math.max(1, parseInt(ev.target.value)); - //console.log(ss); - if (!ss) { - ss = 1; - } - base_config.square_size = ss; - forceUpdate(); - fiddler.emit("setSquareSize", ss); - }} - /> -
- -
- Top labels: - { - base_config.draw_top_labels = ev.target.checked; - redraw(); - }} - /> -
- -
- Left labels: - { - base_config.draw_left_labels = ev.target.checked; - redraw(); - }} - /> -
-
- Right labels: - { - base_config.draw_right_labels = ev.target.checked; - redraw(); - }} - /> -
-
- Bottom labels: - { - base_config.draw_bottom_labels = ev.target.checked; - redraw(); - }} - /> -
-
-
-
- Top bounds: - { - if (base_config.bounds) { - base_config.bounds.top = parseInt(ev.target.value); - } - redraw(); - }} - /> -
-
- Left bounds: - { - if (base_config.bounds) { - base_config.bounds.left = parseInt(ev.target.value); - } - redraw(); - }} - /> -
-
- Right bounds: - { - if (base_config.bounds) { - base_config.bounds.right = parseInt(ev.target.value); - } - redraw(); - }} - /> -
-
- Bottom bounds: - { - if (base_config.bounds) { - base_config.bounds.bottom = parseInt(ev.target.value); - } - redraw(); - }} - /> -
-
-
- - {/*false && */} - {Array.from( - Array( - // 20 - 0, - ), - ).map((_, idx) => - svg_or_canvas === "svg" ? ( - - ) : ( - - ), - )} - {true && (svg_or_canvas === "svg" ? : )} -
- ); -} - -interface ReactGobanProps {} - -function ReactGoban( - ctor: { new (x: GobanCanvasConfig | GobanSVGConfig): GobanClass }, - props: ReactGobanProps, -): JSX.Element { - const [elapsed, setElapsed] = React.useState(0); - const container = React.useRef(null); - const move_tree_container = React.useRef(null); - let goban: GobanCore; - - React.useEffect(() => { - const config: GobanCanvasConfig | GobanSVGConfig = Object.assign({}, base_config, { - board_div: container.current || undefined, - move_tree_container: move_tree_container.current || undefined, - }); - - goban = new ctor(config); - - goban.showMessage("loading", { foo: "bar" }, 1000); - - const heatmap: number[][] = []; - for (let i = 0; i < 19; i++) { - heatmap[i] = []; - for (let j = 0; j < 19; j++) { - heatmap[i][j] = 0.0; - } - } - - fiddler.on("setSquareSize", (ss) => { - const start = Date.now(); - goban.setSquareSize(ss); - const end = Date.now(); - console.log("SSS time: ", end - start); - }); - - fiddler.on("redraw", () => { - const start = Date.now(); - goban.draw_top_labels = !!base_config.draw_top_labels; - goban.draw_left_labels = !!base_config.draw_left_labels; - goban.draw_right_labels = !!base_config.draw_right_labels; - goban.draw_bottom_labels = !!base_config.draw_bottom_labels; - goban.config.draw_top_labels = !!base_config.draw_top_labels; - goban.config.draw_left_labels = !!base_config.draw_left_labels; - goban.config.draw_right_labels = !!base_config.draw_right_labels; - goban.config.draw_bottom_labels = !!base_config.draw_bottom_labels; - if (base_config.bounds) { - goban.setBounds(base_config.bounds); - } - goban.redraw(true); - const end = Date.now(); - console.log("Redraw time: ", end - start); - }); - - let i = 0; - const start = Date.now(); - //const NUM_MOVES = 300; - const NUM_MOVES = 20; - const interval = setInterval(() => { - i++; - if (i >= NUM_MOVES) { - if (i === NUM_MOVES) { - const end = Date.now(); - console.log("Done in ", end - start); - setElapsed(end - start); - - // setup iso branch - const cur = goban.engine.cur_move; - goban.engine.place(18, 16); - goban.engine.place(18, 17); - goban.engine.place(17, 16); - goban.engine.place(17, 17); - - goban.engine.place(18, 2); - goban.engine.place(18, 1); - - goban.engine.jumpTo(cur); - goban.engine.place(17, 16); - goban.engine.place(17, 17); - goban.engine.place(18, 16); - goban.engine.place(18, 17); - - goban.engine.place(18, 1); - goban.engine.place(18, 2); - - /* test stuff for various features */ - { - heatmap[18][18] = 1.0; - heatmap[18][17] = 0.5; - heatmap[18][16] = 0.1; - goban.setHeatmap(heatmap, true); - - // blue move - const circle: ColoredCircle = { - //move: branch.moves[0], - move: { x: 16, y: 17 }, - color: "rgba(0,0,0,0)", - }; - const circle2: ColoredCircle = { - //move: branch.moves[0], - move: { x: 17, y: 17 }, - color: "rgba(0,0,0,0)", - }; - - goban.setMark(16, 17, "blue_move", true); - goban.setMark(17, 17, "blue_move", true); - circle.border_width = 0.2; - circle.border_color = "rgb(0, 130, 255)"; - circle.color = "rgba(0, 130, 255, 0.7)"; - circle2.border_width = 0.2; - circle2.border_color = "rgb(0, 130, 255)"; - circle2.color = "rgba(0, 130, 255, 0.7)"; - goban.setColoredCircles([circle, circle2], false); - } - - // Shapes & labels - goban.setMark(15, 16, "triangle", true); - goban.setMark(15, 15, "square", true); - goban.setMark(15, 14, "circle", true); - goban.setMark(15, 13, "cross", true); - goban.setMark(15, 12, "top", true); - goban.setSubscriptMark(15, 12, "sub", true); - goban.setSubscriptMark(16, 12, "sub", true); - goban.setMark(15, 11, "A", true); - - // pen marks - const marks: MoveTreePenMarks = []; - - { - const points: number[] = []; - for (let i = 0; i < 50; ++i) { - points.push(4 + i / 10); - points.push(9 + Math.sin(i) * 19); - } - - marks.push({ - color: "#ff8800", - points, - }); - } - { - const points: number[] = []; - for (let i = 0; i < 50; ++i) { - points.push(9 + i / 10); - points.push(20 + Math.sin(i) * 19); - } - - marks.push({ - color: "#3388ff", - points, - }); - } - - //goban.drawPenMarks(marks); - } - clearInterval(interval); - return; - } - const x = Math.floor(i / 19); - const y = Math.floor(i % 19); - goban.engine.place(x, y); - if (i === 3) { - /* - goban.setMark(x, y, "blue_move", true); - - const circle: ColoredCircle = { - //move: branch.moves[0], - move: { x, y }, - color: "rgba(0,0,0,0)", - }; - - // blue move - goban.setMark(x, y, "blue_move", true); - circle.border_width = 0.5; - circle.border_color = "rgb(0, 130, 255)"; - circle.color = "rgba(0, 130, 255, 0.7)"; - goban.setColoredCircles([circle], false); - */ - } - //goban.redraw(true); - }, 1); - - return () => { - goban.destroy(); - }; - }, [container]); - - return ( - - - - {elapsed > 0 &&
Elapsed: {elapsed}ms
} -
-
-
-
-
-
- - ); -} - -/* -function ReactGobanPixi(props:ReactGobanProps):JSX.Element { - return ReactGoban(GobanPixi, props); -} -*/ - -function StoneSamples(): JSX.Element { - const div = React.useRef(null); - - React.useEffect(() => { - if (!div.current) { - console.log("no current"); - return; - } - - { - const white_theme = "Shell"; - const black_theme = "Slate"; - //const white_theme = "Glass"; - //const black_theme = "Glass"; - //const white_theme = "Worn Glass"; - //const black_theme = "Worn Glass"; - //const white_theme = "Night"; - //const black_theme = "Night"; - //const white_theme = "Plain"; - //const black_theme = "Plain"; - const radius = 80; - const cx = radius; - const cy = radius; - const size = radius * 2; - - const foo = document.createElement("div"); - - (div.current as any)?.appendChild(foo); - - { - const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); - svg.setAttribute("width", size.toFixed(0)); - svg.setAttribute("height", size.toFixed(0)); - const theme = new GoThemes["black"][black_theme](); - const defs = document.createElementNS("http://www.w3.org/2000/svg", "defs"); - svg.appendChild(defs); - - const black_stones = theme.preRenderBlackSVG(defs, radius, 123, () => {}); - - const g = document.createElementNS("http://www.w3.org/2000/svg", "g"); - svg.appendChild(g); - //for (let i = 0; i < black_stones.length; i++) { - for (let i = 0; i < 1; i++) { - theme.placeBlackStoneSVG( - g, - undefined, - black_stones[i], - cx + i * radius * 2, - cy, - radius, - ); - } - - foo.appendChild(svg); - } - - { - const theme = new GoThemes["white"][white_theme](); - const defs = document.createElementNS("http://www.w3.org/2000/svg", "defs"); - const white_stones = theme.preRenderWhiteSVG(defs, radius, 123, () => {}); - - const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); - svg.setAttribute("width", (white_stones.length * size).toFixed(0)); - svg.setAttribute("height", size.toFixed(0)); - svg.appendChild(defs); - - const g = document.createElementNS("http://www.w3.org/2000/svg", "g"); - svg.appendChild(g); - for (let i = 0; i < white_stones.length; i++) { - //for (let i = 0; i < 1; i++) { - theme.placeWhiteStoneSVG( - g, - undefined, - white_stones[i], - cx + i * radius * 2, - cy, - radius, - ); - } - - foo.appendChild(svg); - } - } - - { - const radius = 20; - const cx = radius; - const cy = radius; - const size = radius * 2; - - for (const black_theme in GoThemes["black"]) { - const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); - svg.setAttribute("width", size.toFixed(0)); - svg.setAttribute("height", size.toFixed(0)); - const theme = new GoThemes["black"][black_theme](); - const defs = document.createElementNS("http://www.w3.org/2000/svg", "defs"); - svg.appendChild(defs); - - const black_stones = theme.preRenderBlackSVG(defs, radius, 123, () => {}); - - const g = document.createElementNS("http://www.w3.org/2000/svg", "g"); - svg.appendChild(g); - //for (let i = 0; i < black_stones.length; i++) { - for (let i = 0; i < 1; i++) { - theme.placeBlackStoneSVG( - g, - undefined, - black_stones[i], - cx + i * radius * 2, - cy, - radius, - ); - } - - const label = document.createElement("label"); - label.textContent = black_theme; - label.setAttribute( - "style", - "display: inline-block; width: 100px; margin-right: 1rem; text-align: right;", - ); - const d = document.createElement("span"); - d.appendChild(label); - d.appendChild(svg); - - (div.current as any)?.appendChild(d); - } - - const br = document.createElement("br"); - (div.current as any)?.appendChild(br); - - for (const white_theme in GoThemes["white"]) { - const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); - svg.setAttribute("width", size.toFixed(0)); - svg.setAttribute("height", size.toFixed(0)); - const theme = new GoThemes["white"][white_theme](); - const defs = document.createElementNS("http://www.w3.org/2000/svg", "defs"); - svg.appendChild(defs); - - const white_stones = theme.preRenderWhiteSVG(defs, radius, 123, () => {}); - - const g = document.createElementNS("http://www.w3.org/2000/svg", "g"); - svg.appendChild(g); - //for (let i = 0; i < white_stones.length; i++) { - for (let i = 0; i < 1; i++) { - theme.placeWhiteStoneSVG( - g, - undefined, - white_stones[i], - cx + i * radius * 2, - cy, - radius, - ); - } - - const label = document.createElement("label"); - label.textContent = white_theme; - label.setAttribute( - "style", - "display: inline-block; width: 100px; margin-right: 1rem; text-align: right;", - ); - const d = document.createElement("span"); - d.appendChild(label); - d.appendChild(svg); - - (div.current as any)?.appendChild(d); - } - } - }, [div]); - - return
; -} - -function ReactGobanCanvas(props: ReactGobanProps): JSX.Element { - return ReactGoban(GobanCanvas, props); -} - -function ReactGobanSVG(props: ReactGobanProps): JSX.Element { - return ReactGoban(GobanSVG, props); -} - -const react_root = ReactDOM.createRoot(document.getElementById("test-content") as Element); -react_root.render(
); diff --git a/src/third_party/goscorer/LICENSE.txt b/src/third_party/goscorer/LICENSE.txt new file mode 100644 index 00000000..7c9cb72a --- /dev/null +++ b/src/third_party/goscorer/LICENSE.txt @@ -0,0 +1,16 @@ +Copyright 2024 David J Wu ("lightvector"). + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and +associated documentation files (the "Software"), to deal in the Software without restriction, +including without limitation the rights to use, copy, modify, merge, publish, distribute, +sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or +substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT +NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/src/third_party/goscorer/README.md b/src/third_party/goscorer/README.md new file mode 100644 index 00000000..05db26f9 --- /dev/null +++ b/src/third_party/goscorer/README.md @@ -0,0 +1,5 @@ +The contents of this directory are largely come from David J Wu's goscorer library found here: https://github.com/lightvector/goscorer + +goscorer.js was copied from the v1.0.0 tag, the latest release as of 2024-06-05 + +Code found within this directory are covered by David's license found in LICENSE.txt diff --git a/src/third_party/goscorer/goscorer.d.ts b/src/third_party/goscorer/goscorer.d.ts new file mode 100644 index 00000000..87f5cebd --- /dev/null +++ b/src/third_party/goscorer/goscorer.d.ts @@ -0,0 +1,89 @@ +/* + * Copyright (C) David J Wu + * + * An attempt at territory scoring in Go with seki detection. + * See https://github.com/lightvector/goscorer + * Original Author: lightvector + * Released under MIT license (https://github.com/lightvector/goscorer/blob/main/LICENSE.txt) + */ + +export const EMPTY: 0; +export const BLACK: 1; +export const WHITE: 2; + +export type color = typeof EMPTY | typeof BLACK | typeof WHITE; + +/** + * Indicates how a given location on the board should be scored for territory, along with other metadata. + * isTerritoryFor is the primary field, indicating the territory (EMPTY / BLACK / WHITE) at each location. + * See the Python version of this code for more detailed documentation on the fields of this class. + */ +export class LocScore { + isTerritoryFor: color; + belongsToSekiGroup: color; + isFalseEye: boolean; + isUnscorableFalseEye: boolean; + isDame: boolean; + eyeValue: number; +} +/** + * @param {color[][]} stones - BLACK or WHITE or EMPTY indicating the stones on the board. + * @param {bool[][]} markedDead - true if the location has a stone marked as dead, and false otherwise. + * @param {float} blackPointsFromCaptures - points to add to black's score due to captures + * @param {float} whitePointsFromCaptures - points to add to white's score due to captures + * @param {float} komi - points to add to white's score due to komi + * @param {bool} [scoreFalseEyes=false] - defaults to false, if set to true will score territory in false eyes even if + * is_unscorable_false_eye is true. + * @return { {black:finalBlackScore,white:finalWhiteScore} } + */ +export function finalTerritoryScore( + stones: color[][], + markedDead: boolean[][], + blackPointsFromCaptures: number, + whitePointsFromCaptures: number, + komi: number, + scoreFalseEyes?: boolean, +): { + black: number; + white: number; +}; +/** + * @param {color[][]} stones - BLACK or WHITE or EMPTY indicating the stones on the board. + * @param {bool[][]} markedDead - true if the location has a stone marked as dead, and false otherwise. + * @param {float} komi - points to add to white's score due to komi + * @return { {black:finalBlackScore,white:finalWhiteScore} } + */ +export function finalAreaScore( + stones: color[][], + markedDead: boolean[][], + komi: number, +): { + black: number; + white: number; +}; +/** + * @param {color[][]} stones - BLACK or WHITE or EMPTY indicating the stones on the board. + * @param {bool[][]} markedDead - true if the location has a stone marked as dead, and false otherwise. + * @param {bool} [scoreFalseEyes=false] - defaults to false, if set to true + * will score territory in false eyes even if is_unscorable_false_eye is + * true. + * @return {LocScore[][]} + */ +export function territoryScoring( + stones: color[][], + markedDead: boolean[][], + scoreFalseEyes?: boolean, +): LocScore[][]; +/** + * @param {color[][]} stones - BLACK or WHITE or EMPTY indicating the stones on the board. + * @param {bool[][]} markedDead - true if the location has a stone marked as dead, and false otherwise. + * @return {LocScore[][]} + */ +export function areaScoring(stones: color[][], markedDead: boolean[][]): color[][]; +export function getOpp(pla: any): number; +export function isOnBoard(y: any, x: any, ysize: any, xsize: any): boolean; +export function isOnBorder(y: any, x: any, ysize: any, xsize: any): boolean; +export function print2d(board: any, f: any): void; +export function string2d(board: any, f: any): string; +export function string2d2(board1: any, board2: any, f: any): string; +export function colorToStr(color: any): "." | "x" | "o"; diff --git a/src/third_party/goscorer/goscorer.js b/src/third_party/goscorer/goscorer.js new file mode 100644 index 00000000..c9c0dec3 --- /dev/null +++ b/src/third_party/goscorer/goscorer.js @@ -0,0 +1,1508 @@ +/** + * An attempt at territory scoring in Go with seki detection. + * See https://github.com/lightvector/goscorer + * Original Author: lightvector + * Released under MIT license (https://github.com/lightvector/goscorer/blob/main/LICENSE.txt) + */ + +const EMPTY = 0; +const BLACK = 1; +const WHITE = 2; + +/** + * Indicates how a given location on the board should be scored for territory, along with other metadata. + * isTerritoryFor is the primary field, indicating the territory (EMPTY / BLACK / WHITE) at each location. + * See the Python version of this code for more detailed documentation on the fields of this class. + */ +class LocScore { + constructor() { + this.isTerritoryFor = EMPTY; + this.belongsToSekiGroup = EMPTY; + this.isFalseEye = false; + this.isUnscorableFalseEye = false; + this.isDame = false; + this.eyeValue = 0; + } +} + +/** + * @param {color[][]} stones - BLACK or WHITE or EMPTY indicating the stones on the board. + * @param {bool[][]} markedDead - true if the location has a stone marked as dead, and false otherwise. + * @param {float} blackPointsFromCaptures - points to add to black's score due to captures + * @param {float} whitePointsFromCaptures - points to add to white's score due to captures + * @param {float} komi - points to add to white's score due to komi + * @param {bool} [scoreFalseEyes=false] - defaults to false, if set to true will score territory in false eyes even if + is_unscorable_false_eye is true. + * @return { {black:finalBlackScore,white:finalWhiteScore} } + */ +function finalTerritoryScore( + stones, + markedDead, + blackPointsFromCaptures, + whitePointsFromCaptures, + komi, + scoreFalseEyes = false +) { + const scoring = territoryScoring(stones,markedDead,scoreFalseEyes); + + const ysize = stones.length; + const xsize = stones[0].length; + let finalBlackScore = 0; + let finalWhiteScore = 0; + for(let y = 0; y { + if(row.length !== xsize) + throw new Error(`Not all rows in stones are the same length ${xsize}`); + row.forEach(value => { + if(value !== EMPTY && value !== BLACK && value !== WHITE) + throw new Error("Unexpected value in stones " + value); + }); + }); + + if(markedDead.length !== ysize) + throw new Error(`markedDead is not the same length as stones ${ysize}`); + + markedDead.forEach(row => { + if(row.length !== xsize) + throw new Error(`Not all rows in markedDead are the same length as stones ${xsize}`); + }); + + const connectionBlocks = makeArray(ysize, xsize, EMPTY); + markConnectionBlocks(ysize, xsize, stones, markedDead, connectionBlocks); + + // console.log("CONNECTIONBLOCKS"); + // print2d(connectionBlocks, (s) => + // ".xo"[s] + // ); + + const strictReachesBlack = makeArray(ysize, xsize, false); + const strictReachesWhite = makeArray(ysize, xsize, false); + markReachability(ysize, xsize, stones, markedDead, null, strictReachesBlack, strictReachesWhite); + + const reachesBlack = makeArray(ysize, xsize, false); + const reachesWhite = makeArray(ysize, xsize, false); + markReachability(ysize, xsize, stones, markedDead, connectionBlocks, reachesBlack, reachesWhite); + + const regionIds = makeArray(ysize, xsize, -1); + const regionInfosById = {}; + markRegions(ysize, xsize, stones, markedDead, connectionBlocks, reachesBlack, reachesWhite, regionIds, regionInfosById); + + // console.log("REGIONIDS"); + // print2d(regionIds, (s) => + // ".0123456789abcdefghijklmnopqrstuvwxyz"[s+1] + // ); + + const chainIds = makeArray(ysize, xsize, -1); + const chainInfosById = {}; + markChains(ysize, xsize, stones, markedDead, regionIds, chainIds, chainInfosById); + + const macrochainIds = makeArray(ysize, xsize, -1); + const macrochainInfosById = {}; + markMacrochains(ysize, xsize, stones, markedDead, connectionBlocks, regionIds, regionInfosById, chainIds, chainInfosById, macrochainIds, macrochainInfosById); + + // console.log("MACROCHAINS"); + // print2d(macrochainIds, (s) => + // ".0123456789abcdefghijklmnopqrstuvwxyz"[s+1] + // ); + + const eyeIds = makeArray(ysize, xsize, -1); + const eyeInfosById = {}; + markPotentialEyes(ysize, xsize, stones, markedDead, strictReachesBlack, strictReachesWhite, regionIds, regionInfosById, macrochainIds, macrochainInfosById, eyeIds, eyeInfosById); + + // console.log("EYE IDS"); + // print2d(eyeIds, (s) => + // ".0123456789abcdefghijklmnopqrstuvwxyz"[s+1] + // ); + + const isFalseEyePoint = makeArray(ysize, xsize, false); + markFalseEyePoints(ysize, xsize, regionIds, macrochainIds, macrochainInfosById, eyeInfosById, isFalseEyePoint); + + markEyeValues(ysize, xsize, stones, markedDead, regionIds, regionInfosById, chainIds, chainInfosById, isFalseEyePoint, eyeIds, eyeInfosById); + + const isUnscorableFalseEyePoint = makeArray(ysize, xsize, false); + markFalseEyePoints(ysize, xsize, regionIds, macrochainIds, macrochainInfosById, eyeInfosById, isUnscorableFalseEyePoint); + + const scoring = makeArrayFromCallable(ysize, xsize, () => new LocScore()); + markScoring( + ysize, xsize, stones, markedDead, scoreFalseEyes, + strictReachesBlack, strictReachesWhite, regionIds, regionInfosById, + chainIds, chainInfosById, isFalseEyePoint, eyeIds, eyeInfosById, + isUnscorableFalseEyePoint, scoring + ); + return scoring; +} + +/** + * @param {color[][]} stones - BLACK or WHITE or EMPTY indicating the stones on the board. + * @param {bool[][]} markedDead - true if the location has a stone marked as dead, and false otherwise. + * @return {LocScore[][]} + */ +function areaScoring( + stones, + markedDead, +) { + const ysize = stones.length; + const xsize = stones[0].length; + + stones.forEach(row => { + if(row.length !== xsize) + throw new Error(`Not all rows in stones are the same length ${xsize}`); + row.forEach(value => { + if(value !== EMPTY && value !== BLACK && value !== WHITE) + throw new Error("Unexpected value in stones " + value); + }); + }); + + if(markedDead.length !== ysize) + throw new Error(`markedDead is not the same length as stones ${ysize}`); + + markedDead.forEach(row => { + if(row.length !== xsize) + throw new Error(`Not all rows in markedDead are the same length as stones ${xsize}`); + }); + + const strictReachesBlack = makeArray(ysize, xsize, false); + const strictReachesWhite = makeArray(ysize, xsize, false); + markReachability(ysize, xsize, stones, markedDead, null, strictReachesBlack, strictReachesWhite); + + const scoring = makeArray(ysize, xsize, EMPTY); + + for(let y = 0; y < ysize; y++) { + for(let x = 0; x < xsize; x++) { + if(strictReachesWhite[y][x] && !strictReachesBlack[y][x]) + scoring[y][x] = WHITE; + if(strictReachesBlack[y][x] && !strictReachesWhite[y][x]) + scoring[y][x] = BLACK; + } + } + return scoring; +} + + +function getOpp(pla) { + return 3 - pla; +} + +function makeArray(ysize, xsize, initialValue) { + return Array.from({length: ysize}, () => + Array.from({length: xsize}, () => initialValue)); +} + +function makeArrayFromCallable(ysize, xsize, f) { + return Array.from({length: ysize}, () => + Array.from({length: xsize}, () => f())); +} + +function isOnBoard(y, x, ysize, xsize) { + return y >= 0 && x >= 0 && y < ysize && x < xsize; +} + +function isOnBorder(y, x, ysize, xsize) { + return y === 0 || x === 0 || y === ysize-1 || x === xsize-1; +} + +function isAdjacent(y1, x1, y2, x2) { + return (y1 === y2 && (x1 === x2 + 1 || x1 === x2 - 1)) + || (x1 === x2 && (y1 === y2 + 1 || y1 === y2 - 1)); +} + +function print2d(board, f) { + console.log(string2d(board, f)); +} + +function string2d(board, f) { + const ysize = board.length; + const lines = []; + + for(let y = 0; y < ysize; y++) { + const pieces = []; + for(let item of board[y]) + pieces.push(f(item)); + lines.push(pieces.join('')); + } + return lines.join('\n'); +} + +function string2d2(board1, board2, f) { + const ysize = board1.length; + const lines = []; + + for(let y = 0; y < ysize; y++) { + const pieces = []; + for(let x = 0; x < board1[y].length; x++) { + const item1 = board1[y][x]; + const item2 = board2[y][x]; + pieces.push(f(item1, item2)); + } + lines.push(pieces.join('')); + } + return lines.join('\n'); +} + +function colorToStr(color) { + if(color === EMPTY) + return '.'; + if(color === BLACK) + return 'x'; + if(color === WHITE) + return 'o'; + throw new Error("Invalid color: " + color); +} + +function markConnectionBlocks( + ysize, + xsize, + stones, + markedDead, + connectionBlocks // mutated by this function +) { + const patterns = [ + [ + "pp", + "@e", + "pe", + ], + [ + "ep?", + "e@e", + "ep?", + ], + [ + "pee", + "e@p", + "pee", + ], + [ + "?e?", + "p@p", + "xxx", + ], + [ + "pp", + "@e", + "xx", + ], + [ + "ep?", + "e@e", + "xxx", + ], + ]; + + for(const pla of [BLACK, WHITE]) { + const opp = getOpp(pla); + + for(const [pdydy, pdydx, pdxdy, pdxdx] of [ + [1,0,0,1], + [-1,0,0,1], + [1,0,0,-1], + [-1,0,0,-1], + [0,1,1,0], + [0,-1,1,0], + [0,1,-1,0], + [0,-1,-1,0], + ]) { + for(const pattern of patterns) { + let pylen = pattern.length; + const pxlen = pattern[0].length; + const isEdgePattern = pattern[pylen-1].includes('x'); + + if(isEdgePattern) + pylen--; + + let yRange = Array.from({length: ysize}, (_, i) => i); + let xRange = Array.from({length: xsize}, (_, i) => i); + + if(isEdgePattern) { + if(pdydy === -1) + yRange = [pattern.length-2]; + else if(pdydy === 1) + yRange = [ysize - (pattern.length-1)]; + else if(pdxdy === -1) + xRange = [pattern.length-2]; + else if(pdxdy === 1) + xRange = [xsize - (pattern.length-1)]; + } + + for(let y of yRange) { + for(let x of xRange) { + function getTargetYX(pdy, pdx) { + return [ + y + pdydy*pdy + pdxdy*pdx, + x + pdydx*pdy + pdxdx*pdx + ]; + } + + let [ty, tx] = getTargetYX(pylen-1, pxlen-1); + if(!isOnBoard(ty, tx, ysize, xsize)) + continue; + + let atLoc; + let mismatch = false; + for(let pdy = 0; pdy < pylen; pdy++) { + for(let pdx = 0; pdx < pxlen; pdx++) { + const c = pattern[pdy][pdx]; + if(c === "?") + continue; + + [ty, tx] = getTargetYX(pdy, pdx); + if(c === 'p') { + if(!(stones[ty][tx] === pla && !markedDead[ty][tx])) { + mismatch = true; + break; + } + } + else if(c === 'e') { + if(!( + stones[ty][tx] === EMPTY || + stones[ty][tx] === pla && !markedDead[ty][tx] || + stones[ty][tx] === opp && markedDead[ty][tx] + )) { + mismatch = true; + break; + } + } + else if(c === '@') { + if(stones[ty][tx] !== EMPTY) { + mismatch = true; + break; + } + atLoc = [ty, tx]; + } + else { + throw new Error("Invalid char: " + c); + } + } + if(mismatch) + break; + } + + if(!mismatch) { + [ty, tx] = atLoc; + connectionBlocks[ty][tx] = pla; + } + } + } + } + } + } +} + + +function markReachability( + ysize, + xsize, + stones, + markedDead, + connectionBlocks, + reachesBlack, // mutated by this function + reachesWhite // mutated by this function +) { + function fillReach(y, x, reachesPla, pla) { + if(!isOnBoard(y,x,ysize,xsize)) + return; + if(reachesPla[y][x]) + return; + if(stones[y][x] === getOpp(pla) && !markedDead[y][x]) + return; + + reachesPla[y][x] = true; + + if(connectionBlocks && connectionBlocks[y][x] === getOpp(pla)) + return; + + fillReach(y-1, x, reachesPla, pla); + fillReach(y+1, x, reachesPla, pla); + fillReach(y, x-1, reachesPla, pla); + fillReach(y, x+1, reachesPla, pla); + } + + for(let y = 0; y < ysize; y++) { + for(let x = 0; x < xsize; x++) { + if(stones[y][x] === BLACK && !markedDead[y][x]) + fillReach(y, x, reachesBlack, BLACK); + if(stones[y][x] === WHITE && !markedDead[y][x]) + fillReach(y, x, reachesWhite, WHITE); + } + } +} + +class RegionInfo { + constructor(regionId, color, regionAndDame, eyes) { + this.regionId = regionId; + this.color = color; + this.regionAndDame = regionAndDame; + this.eyes = eyes; + } +} + +function markRegions( + ysize, + xsize, + stones, + markedDead, + connectionBlocks, + reachesBlack, + reachesWhite, + regionIds, // mutated by this function + regionInfosById // mutated by this function +) { + function fillRegion(y, x, withId, opp, reachesPla, reachesOpp, visited) { + if(!isOnBoard(y,x,ysize,xsize)) + return; + if(visited[y][x]) + return; + if(regionIds[y][x] !== -1) + return; + if(stones[y][x] === opp && !markedDead[y][x]) + return; + + visited[y][x] = true; + regionInfosById[withId].regionAndDame.add([y, x]); + + if(reachesPla[y][x] && !reachesOpp[y][x]) + regionIds[y][x] = withId; + + if(connectionBlocks[y][x] === opp) + return; + + fillRegion(y-1, x, withId, opp, reachesPla, reachesOpp, visited); + fillRegion(y+1, x, withId, opp, reachesPla, reachesOpp, visited); + fillRegion(y, x-1, withId, opp, reachesPla, reachesOpp, visited); + fillRegion(y, x+1, withId, opp, reachesPla, reachesOpp, visited); + } + + let nextRegionId = 0; + + for(let y = 0; y < ysize; y++) { + for(let x = 0; x < xsize; x++) { + if(reachesBlack[y][x] && !reachesWhite[y][x] && regionIds[y][x] === -1) { + const regionId = nextRegionId++; + regionInfosById[regionId] = new RegionInfo( + regionId, BLACK, new CoordinateSet(), new Set() + ); + + const visited = makeArray(ysize, xsize, false); + fillRegion(y, x, regionId, WHITE, reachesBlack, reachesWhite, visited); + } + if(reachesWhite[y][x] && !reachesBlack[y][x] && regionIds[y][x] === -1) { + const regionId = nextRegionId++; + regionInfosById[regionId] = new RegionInfo( + regionId, WHITE, new CoordinateSet(), new Set() + ); + + const visited = makeArray(ysize, xsize, false); + fillRegion(y, x, regionId, BLACK, reachesWhite, reachesBlack, visited); + } + } + } +} + + +class ChainInfo { + constructor(chainId, regionId, color, points, neighbors, adjacents, liberties, isMarkedDead) { + this.chainId = chainId; + this.regionId = regionId; + this.color = color; + this.points = points; + this.neighbors = neighbors; + this.adjacents = adjacents; + this.liberties = liberties; + this.isMarkedDead = isMarkedDead; + } +} + +function markChains( + ysize, + xsize, + stones, + markedDead, + regionIds, + chainIds, // mutated by this function + chainInfosById // mutated by this function +) { + function fillChain(y, x, withId, color, isMarkedDead) { + + if(!isOnBoard(y,x,ysize,xsize)) + return; + if(chainIds[y][x] === withId) + return; + + if(chainIds[y][x] !== -1) { + const otherId = chainIds[y][x]; + chainInfosById[otherId].neighbors.add(withId); + chainInfosById[withId].neighbors.add(otherId); + chainInfosById[withId].adjacents.add([y, x]); + if(stones[y][x] == EMPTY) + chainInfosById[withId].liberties.add([y, x]); + return; + } + if(stones[y][x] !== color || markedDead[y][x] !== isMarkedDead) { + chainInfosById[withId].adjacents.add([y, x]); + if(stones[y][x] == EMPTY) + chainInfosById[withId].liberties.add([y, x]); + return; + } + + chainIds[y][x] = withId; + chainInfosById[withId].points.push([y, x]); + if(chainInfosById[withId].regionId !== regionIds[y][x]) + chainInfosById[withId].regionId = -1; + + assert(color === EMPTY || regionIds[y][x] === chainInfosById[withId].regionId); + + fillChain(y-1, x, withId, color, isMarkedDead); + fillChain(y+1, x, withId, color, isMarkedDead); + fillChain(y, x-1, withId, color, isMarkedDead); + fillChain(y, x+1, withId, color, isMarkedDead); + } + + let nextChainId = 0; + + for(let y = 0; y < ysize; y++) { + for(let x = 0; x < xsize; x++) { + if(chainIds[y][x] === -1) { + const chainId = nextChainId++; + const color = stones[y][x]; + const isMarkedDead = markedDead[y][x]; + + chainInfosById[chainId] = new ChainInfo( + chainId, + regionIds[y][x], + color, + [], + new Set(), + new CoordinateSet(), + new CoordinateSet(), + isMarkedDead + ); + + assert(isMarkedDead || color === EMPTY || regionIds[y][x] !== -1); + fillChain(y, x, chainId, color, isMarkedDead); + } + } + } +} + + +class MacroChainInfo { + constructor(macrochainId, regionId, color, points, chains, eyeNeighborsFrom) { + this.macrochainId = macrochainId; + this.regionId = regionId; + this.color = color; + this.points = points; + this.chains = chains; + this.eyeNeighborsFrom = eyeNeighborsFrom; + } +} + +function markMacrochains( + ysize, + xsize, + stones, + markedDead, + connectionBlocks, + regionIds, + regionInfosById, + chainIds, + chainInfosById, + macrochainIds, // mutated by this function + macrochainInfosById // mutated by this function +) { + let nextMacrochainId = 0; + + for(const pla of [BLACK, WHITE]) { + const opp = getOpp(pla); + const chainsHandled = new Set(); + const visited = makeArray(ysize, xsize, false); + + for(let chainId in chainInfosById) { + chainId = Number(chainId); + if(chainsHandled.has(chainId)) + continue; + + const chainInfo = chainInfosById[chainId]; + if(!(chainInfo.color === pla && !chainInfo.isMarkedDead)) + continue; + + const regionId = chainInfo.regionId; + assert(regionId !== -1); + + const macrochainId = nextMacrochainId++; + const points = []; + const chains = new Set(); + + function walkAndAccumulate(y, x) { + if(!isOnBoard(y,x,ysize,xsize)) + return; + if(visited[y][x]) + return; + + visited[y][x] = true; + + const chainId2 = chainIds[y][x]; + const chainInfo2 = chainInfosById[chainId2]; + + let shouldRecurse = false; + if(stones[y][x] === pla && !markedDead[y][x]) { + macrochainIds[y][x] = macrochainId; + points.push([y,x]); + if(!chains.has(chainId2)) { + chains.add(chainId2); + chainsHandled.add(chainId2); + } + shouldRecurse = true; + } + else if(regionIds[y][x] === -1 && connectionBlocks[y][x] !== opp) { + shouldRecurse = true; + } + + if(shouldRecurse) { + walkAndAccumulate(y-1, x); + walkAndAccumulate(y+1, x); + walkAndAccumulate(y, x-1); + walkAndAccumulate(y, x+1); + } + } + + const [y, x] = chainInfo.points[0]; + walkAndAccumulate(y, x); + + macrochainInfosById[macrochainId] = new MacroChainInfo( + macrochainId, + regionId, + pla, + points, + chains, + {} // filled in later + ); + + } + + } + +} + + +class EyeInfo { + constructor(pla, regionId, eyeId, potentialPoints, realPoints, macrochainNeighborsFrom, isLoose, eyeValue) { + this.pla = pla; + this.regionId = regionId; + this.eyeId = eyeId; + this.potentialPoints = potentialPoints; + this.realPoints = realPoints; + this.macrochainNeighborsFrom = macrochainNeighborsFrom; + this.isLoose = isLoose; + this.eyeValue = eyeValue; + } +} + +function markPotentialEyes( + ysize, + xsize, + stones, + markedDead, + strictReachesBlack, + strictReachesWhite, + regionIds, + regionInfosById, // mutated by this function + macrochainIds, + macrochainInfosById, // mutated by this function + eyeIds, // mutated by this function + eyeInfosById // mutated by this function +) { + let nextEyeId = 0; + const visited = makeArray(ysize, xsize, false); + for(let y = 0; y < ysize; y++) { + for(let x = 0; x < xsize; x++) { + if(visited[y][x]) + continue; + if(eyeIds[y][x] !== -1) + continue; + if(stones[y][x] !== EMPTY && !markedDead[y][x]) + continue; + + const regionId = regionIds[y][x]; + if(regionId === -1) + continue; + + const regionInfo = regionInfosById[regionId]; + const pla = regionInfo.color; + const isLoose = strictReachesWhite[y][x] && strictReachesBlack[y][x]; + const eyeId = nextEyeId++; + const potentialPoints = new CoordinateSet(); + const macrochainNeighborsFrom = {}; + + function accRegion(y, x, prevY, prevX) { + if(!isOnBoard(y,x,ysize,xsize)) + return; + if(visited[y][x]) + return; + if(regionIds[y][x] !== regionId) + return; + + if(macrochainIds[y][x] !== -1) { + const macrochainId = macrochainIds[y][x]; + + if(!macrochainNeighborsFrom[macrochainId]) + macrochainNeighborsFrom[macrochainId] = new CoordinateSet(); + macrochainNeighborsFrom[macrochainId].add([prevY, prevX]); + if(!macrochainInfosById[macrochainId].eyeNeighborsFrom[eyeId]) + macrochainInfosById[macrochainId].eyeNeighborsFrom[eyeId] = new CoordinateSet(); + + macrochainInfosById[macrochainId].eyeNeighborsFrom[eyeId].add([y, x]); + } + + if(stones[y][x] !== EMPTY && !markedDead[y][x]) + return; + + visited[y][x] = true; + eyeIds[y][x] = eyeId; + potentialPoints.add([y,x]); + + accRegion(y-1, x, y, x); + accRegion(y+1, x, y, x); + accRegion(y, x-1, y, x); + accRegion(y, x+1, y, x); + } + + assert(macrochainIds[y][x] === -1); + accRegion(y, x, 10000, 10000); + + eyeInfosById[eyeId] = new EyeInfo( + pla, + regionId, + eyeId, + potentialPoints, + new CoordinateSet(), // filled in later + macrochainNeighborsFrom, + isLoose, + 0 // filled in later + ); + + regionInfosById[regionId].eyes.add(eyeId); + + } + } + +} + + +function markFalseEyePoints( + ysize, + xsize, + regionIds, + macrochainIds, + macrochainInfosById, + eyeInfosById, + isFalseEyePoint // mutated by this function +) { + for(let origEyeId in eyeInfosById) { + origEyeId = Number(origEyeId); + const origEyeInfo = eyeInfosById[origEyeId]; + + for(let origMacrochainId in origEyeInfo.macrochainNeighborsFrom) { + origMacrochainId = Number(origMacrochainId); + const neighborsFromEyePoints = origEyeInfo.macrochainNeighborsFrom[origMacrochainId]; + + for(let [ey, ex] of neighborsFromEyePoints) { + let sameEyeAdjCount = 0; + for(let [y, x] of [[ey-1,ex], [ey+1,ex], [ey,ex-1], [ey,ex+1]]) { + if(origEyeInfo.potentialPoints.has([y, x])) + sameEyeAdjCount += 1; + } + if(sameEyeAdjCount > 1) + continue; + + const reachingSides = new CoordinateSet(); + const visitedMacro = new Set(); + const visitedOtherEyes = new Set(); + const visitedOrigEyePoints = new CoordinateSet(); + visitedOrigEyePoints.add([ey,ex]); + + let targetSideCount = 0; + for(let [y, x] of [[ey-1,ex], [ey+1,ex], [ey,ex-1], [ey, ex+1]]) { + if(isOnBoard(y, x, ysize, xsize) && regionIds[y][x] === origEyeInfo.regionId) + targetSideCount += 1; + } + // console.log("CHECKING EYE " + origEyeId + " " + [ey,ex]); + function search(macrochainId) { + if(visitedMacro.has(macrochainId)) + return false; + visitedMacro.add(macrochainId); + // console.log("Searching macrochain " + macrochainId + ""); + + const macrochainInfo = macrochainInfosById[macrochainId]; + for(let eyeId in macrochainInfo.eyeNeighborsFrom) { + eyeId = Number(eyeId); + if(visitedOtherEyes.has(eyeId)) + continue; + // console.log("Searching macrochain " + macrochainId + " iterating eyeId " + eyeId + ""); + + if(eyeId === origEyeId) { + // console.log("Orig!"); + const eyeInfo = eyeInfosById[eyeId]; + for(let [y, x] of macrochainInfo.eyeNeighborsFrom[eyeId]) { + if(isAdjacent(y, x, ey, ex)) { + reachingSides.add([y, x]); + if(reachingSides.size >= targetSideCount) + return true; + } + } + + const pointsReached = findRecursivelyAdjacentPoints( + eyeInfo.potentialPoints, + eyeInfo.macrochainNeighborsFrom[macrochainId], + visitedOrigEyePoints + ); + if(pointsReached.size === 0) + continue; + + pointsReached.forEach(item => visitedOrigEyePoints.add(item)); + + if(eyeInfo.eyeValue > 0) { + for(let point of pointsReached) { + if(eyeInfo.realPoints.has(point)) + return true; + } + } + + for(let [y, x] of pointsReached) { + if(isAdjacent(y, x, ey, ex)) { + reachingSides.add([y,x]); + if(reachingSides.size >= targetSideCount) + return true; + } + } + + for(let nextMacrochainId in eyeInfo.macrochainNeighborsFrom) { + if([...eyeInfo.macrochainNeighborsFrom[nextMacrochainId]].some(point => pointsReached.has(point))) { + if(search(Number(nextMacrochainId))) + return true; + } + } + + } + else { + visitedOtherEyes.add(eyeId); + const eyeInfo = eyeInfosById[eyeId]; + if(eyeInfo.eyeValue > 0) + return true; + + for(let nextMacrochainId of Object.keys(eyeInfo.macrochainNeighborsFrom)) { + if(search(Number(nextMacrochainId))) + return true; + } + } + } + return false; + }; + + if(search(origMacrochainId)) { + // pass + } + else { + isFalseEyePoint[ey][ex] = true; + } + } + } + } +} + + + +function findRecursivelyAdjacentPoints( + withinSet, + fromPoints, + excludingPoints +) { + const expanded = new CoordinateSet(); + fromPoints = [...fromPoints]; + + for(let i = 0; i < fromPoints.length; i++) { + const point = fromPoints[i]; + if(excludingPoints.has(point) || expanded.has(point) || !withinSet.has(point)) + continue; + expanded.add(point); + const [y, x] = point; + fromPoints.push([y-1, x]); + fromPoints.push([y+1, x]); + fromPoints.push([y, x-1]); + fromPoints.push([y, x+1]); + } + + return expanded; +} + + +function getPieces(ysize, xsize, points, pointsToDelete) { + const usedPoints = new CoordinateSet(); + function floodfill(point, piece) { + if(usedPoints.has(point) || pointsToDelete.has(point)) + return; + usedPoints.add(point); + piece.add(point); + const [y, x] = point; + const adjacents = [[y-1, x], [y+1, x], [y, x-1], [y, x+1]]; + for(let adjacent of adjacents) { + if(points.has(adjacent)) + floodfill(adjacent, piece); + } + } + + const pieces = []; + for(let point of points) { + if(!usedPoints.has(point)) { + const piece = new CoordinateSet(); + floodfill(point, piece); + if(piece.size > 0) + pieces.push(piece); + } + } + return pieces; +} + +function isPseudoLegal(ysize, xsize, stones, chainIds, chainInfosById, y, x, pla) { + if(stones[y][x] !== EMPTY) + return false; + const adjacents = [[y-1, x], [y+1, x], [y, x-1], [y, x+1]]; + const opp = getOpp(pla); + for(let [ay, ax] of adjacents) { + if(isOnBoard(ay, ax, ysize, xsize)) { + if(stones[ay][ax] !== opp) + return true; + if(chainInfosById[chainIds[ay][ax]].liberties.size <= 1) + return true; + } + } + return false; +} + +function countAdjacentsIn(y, x, points) { + let count = 0; + const adjacents = [[y-1, x], [y+1, x], [y, x-1], [y, x+1]]; + for(let a of adjacents) { + if(points.has(a)) + count += 1; + } + return count; +} + +class EyePointInfo { + constructor( + adjPoints, + adjEyePoints, + numEmptyAdjPoints=0, + numEmptyAdjFalsePoints=0, + numEmptyAdjEyePoints=0, + numOppAdjFalsePoints=0, + isFalseEyePoke=false, + numMovesToBlock=0, + numBlockablesDependingOnThisSpot=0 + ) { + this.adjPoints = adjPoints; + this.adjEyePoints = adjEyePoints; + this.numEmptyAdjPoints = numEmptyAdjPoints; + this.numEmptyAdjFalsePoints = numEmptyAdjFalsePoints; + this.numEmptyAdjEyePoints = numEmptyAdjEyePoints; + this.numOppAdjFalsePoints = numOppAdjFalsePoints; + this.isFalseEyePoke = isFalseEyePoke; + this.numMovesToBlock = numMovesToBlock; + this.numBlockablesDependingOnThisSpot = numBlockablesDependingOnThisSpot; + } +} + + +function count(points, predicate) { + let c = 0; + for(let p of points) + if(predicate(p)) { + c++; + } + return c; +} + +function markEyeValues( + ysize, + xsize, + stones, + markedDead, + regionIds, + regionInfosById, + chainIds, + chainInfosById, + isFalseEyePoint, + eyeIds, + eyeInfosById // mutated by this function +) { + for(let eyeId in eyeInfosById) { + eyeId = Number(eyeId); + + const eyeInfo = eyeInfosById[eyeId]; + const pla = eyeInfo.pla; + const opp = getOpp(pla); + + const infoByPoint = {}; + eyeInfo.realPoints = new CoordinateSet(); + for(let [y, x] of eyeInfo.potentialPoints) { + if(!isFalseEyePoint[y][x]) { + eyeInfo.realPoints.add([y, x]); + + const info = new EyePointInfo([], []); + infoByPoint[[y, x]] = info; + } + } + + for(let [y, x] of eyeInfo.realPoints) { + const info = infoByPoint[[y, x]]; + const adjacents = [[y-1, x], [y+1, x], [y, x-1], [y, x+1]]; + for(let [ay, ax] of adjacents) { + if(!isOnBoard(ay, ax, ysize, xsize)) + continue; + + info.adjPoints.push([ay, ax]); + if(eyeInfo.realPoints.has([ay, ax])) + info.adjEyePoints.push([ay, ax]); + } + } + + for(let [y, x] of eyeInfo.realPoints) { + const info = infoByPoint[[y, x]]; + for(let [ay, ax] of info.adjPoints) { + if(stones[ay][ax] === EMPTY) + info.numEmptyAdjPoints += 1; + if(stones[ay][ax] === EMPTY && eyeInfo.realPoints.has([ay, ax])) + info.numEmptyAdjEyePoints += 1; + if(stones[ay][ax] === EMPTY && isFalseEyePoint[ay][ax]) + info.numEmptyAdjFalsePoints += 1; + if(stones[ay][ax] === opp && isFalseEyePoint[ay][ax]) + info.numOppAdjFalsePoints += 1; + } + + if(info.numOppAdjFalsePoints > 0 && stones[y][x] === opp) + info.isFalseEyePoke = true; + if(info.numEmptyAdjFalsePoints >= 2 && stones[y][x] === opp) + info.isFalseEyePoke = true; + } + + for(let [y, x] of eyeInfo.realPoints) { + const info = infoByPoint[[y, x]]; + info.numMovesToBlock = 0; + info.numMovesToBlockNoOpps = 0; + + for(let [ay, ax] of info.adjPoints) { + let block = 0; + if(stones[ay][ax] === EMPTY && !eyeInfo.realPoints.has([ay, ax])) + block = 1; + if(stones[ay][ax] === EMPTY && [ay, ax] in infoByPoint && infoByPoint[[ay, ax]].numOppAdjFalsePoints >= 1) + block = 1; + if(stones[ay][ax] === opp && [ay, ax] in infoByPoint && infoByPoint[[ay, ax]].numEmptyAdjFalsePoints >= 1) + block = 1; + if(stones[ay][ax] === opp && isFalseEyePoint[ay][ax]) + block = 1000; + if(stones[ay][ax] === opp && [ay, ax] in infoByPoint && infoByPoint[[ay, ax]].isFalseEyePoke) + block = 1000; + + info.numMovesToBlock += block; + } + } + + let eyeValue = 0; + if(count(eyeInfo.realPoints, ([y, x]) => infoByPoint[[y, x]].numMovesToBlock <= 1) >= 1) + eyeValue = 1; + + for(let [dy, dx] of eyeInfo.realPoints) { + + if(!isPseudoLegal(ysize, xsize, stones, chainIds, chainInfosById, dy, dx, pla)) + continue; + + const pieces = getPieces(ysize, xsize, eyeInfo.realPoints, new CoordinateSet([[dy, dx]])); + if(pieces.length < 2) + continue; + + let shouldBonus = infoByPoint[[dy, dx]].numOppAdjFalsePoints === 1; + let numDefiniteEyePieces = 0; + for(let piece of pieces) { + let zeroMovesToBlock = false; + for(let point of piece) { + if(infoByPoint[point].numMovesToBlock <= 0) { + zeroMovesToBlock = true; + break; + } + if(shouldBonus && infoByPoint[point].numMovesToBlock <= 1) { + zeroMovesToBlock = true; + break; + } + } + + if(zeroMovesToBlock) + numDefiniteEyePieces++; + } + eyeValue = Math.max(eyeValue, numDefiniteEyePieces); + } + + let markedDeadCount = count(eyeInfo.realPoints, ([y,x]) => stones[y][x] === opp && markedDead[y][x]); + if(markedDeadCount >= 5) + eyeValue = Math.max(eyeValue, 1); + if(markedDeadCount >= 8) + eyeValue = Math.max(eyeValue, 2); + + if(eyeValue < 2 && ( + eyeInfo.realPoints.size + - count(eyeInfo.realPoints, ([y,x]) => infoByPoint[[y,x]].numMovesToBlock >= 1) + - count(eyeInfo.realPoints, ([y,x]) => infoByPoint[[y,x]].numMovesToBlock >= 2) + - count(eyeInfo.realPoints, ([y,x]) => stones[y][x] === opp && infoByPoint[[y,x]].adjEyePoints.length >= 2) + >= 6 + )) { + eyeValue = Math.max(eyeValue, 2); + } + + if(eyeValue < 2 && ( + count(eyeInfo.realPoints, ([y,x]) => stones[y][x] === EMPTY && infoByPoint[[y,x]].adjEyePoints.length >= 4) + + count(eyeInfo.realPoints, ([y,x]) => stones[y][x] === EMPTY && infoByPoint[[y,x]].adjEyePoints.length >= 3) + >= 6 + )) { + eyeValue = Math.max(eyeValue, 2); + } + + if(eyeValue < 2) { + for(let [dy, dx] of eyeInfo.realPoints) { + if(stones[dy][dx] !== EMPTY) + continue; + if(isOnBorder(dy, dx, ysize, xsize)) + continue; + if(!isPseudoLegal(ysize, xsize, stones, chainIds, chainInfosById, dy, dx, pla)) + continue; + + const info1 = infoByPoint[[dy, dx]]; + if(info1.numMovesToBlock > 1 || info1.adjEyePoints.length < 3) + continue; + + for(let adjacent of info1.adjEyePoints) { + const info2 = infoByPoint[adjacent]; + if(info2.adjEyePoints.length < 3) + continue; + if(info2.numMovesToBlock > 1) + continue; + + const [dy2, dx2] = adjacent; + if(stones[dy2][dx2] !== EMPTY && info2.numEmptyAdjEyePoints <= 1) + continue; + + const pieces = getPieces(ysize, xsize, eyeInfo.realPoints, new CoordinateSet([[dy, dx], adjacent])); + if(pieces.length < 2) + continue; + + let numDefiniteEyePieces = 0; + let numDoubleDefiniteEyePieces = 0; + + for(let piece of pieces) { + let numZeroMovesToBlock = 0; + for(let point of piece) { + if(infoByPoint[point].numMovesToBlock <= 0) { + numZeroMovesToBlock += 1; + if(numZeroMovesToBlock >= 2) + break; + } + } + if(numZeroMovesToBlock >= 1) + numDefiniteEyePieces += 1; + if(numZeroMovesToBlock >= 2) + numDoubleDefiniteEyePieces += 1; + } + + if(numDefiniteEyePieces >= 2 && + numDoubleDefiniteEyePieces >= 1 && + (stones[dy2][dx2] === EMPTY || numDoubleDefiniteEyePieces >= 2) + ) { + eyeValue = Math.max(eyeValue, 2); + break; + } + + } + + if(eyeValue >= 2) + break; + } + } + + if(eyeValue < 2) { + const deadOppsInEye = new CoordinateSet(); + const unplayableInEye = []; + for(let point of eyeInfo.realPoints) { + const [dy, dx] = point; + if(stones[dy][dx] === opp && markedDead[dy][dx]) + deadOppsInEye.add(point); + else if(!isPseudoLegal(ysize, xsize, stones, chainIds, chainInfosById, dy, dx, pla)) + unplayableInEye.push(point); + } + + if(deadOppsInEye.size > 0) { + let numThrowins = 0; + for(let [y,x] of eyeInfo.potentialPoints) { + if(stones[y][x] === opp && isFalseEyePoint[y][x]) + numThrowins += 1; + } + + const possibleOmissions = [...unplayableInEye]; + possibleOmissions.push(null); + let allGoodForDefender = true; + for(let omitted of possibleOmissions) { + const remainingShape = deadOppsInEye.copy(); + for(let point of unplayableInEye) { + if(point !== omitted) + remainingShape.add(point); + } + + const initialPieceCount = getPieces(ysize, xsize, remainingShape, new CoordinateSet()).length; + let numBottlenecks = 0; + let numNonBottlenecksHighDegree = 0; + for(let pointToDelete of remainingShape) { + const [dy,dx] = pointToDelete; + if(getPieces(ysize, xsize, remainingShape, new CoordinateSet([pointToDelete])).length > initialPieceCount) + numBottlenecks += 1; + else if(countAdjacentsIn(dy,dx,remainingShape) >= 3) + numNonBottlenecksHighDegree += 1; + } + + let bonus = 0; + if(remainingShape.size >= 7) + bonus += 1; + + if(initialPieceCount - numThrowins + Math.floor((numBottlenecks + numNonBottlenecksHighDegree + bonus) / 2) < 2) { + allGoodForDefender = false; + break; + } + } + if(allGoodForDefender) + eyeValue = 2; + } + } + + eyeValue = Math.min(eyeValue, 2); + eyeInfo.eyeValue = eyeValue; + } +} + +function markScoring( + ysize, + xsize, + stones, + markedDead, + scoreFalseEyes, + strictReachesBlack, + strictReachesWhite, + regionIds, + regionInfosById, + chainIds, + chainInfosById, + isFalseEyePoint, + eyeIds, + eyeInfosById, + isUnscorableFalseEyePoint, + scoring // mutated by this function +) { + const extraBlackUnscoreablePoints = new CoordinateSet(); + const extraWhiteUnscoreablePoints = new CoordinateSet(); + for(let y = 0; y < ysize; y++) { + for(let x = 0; x < xsize; x++) { + if(isUnscorableFalseEyePoint[y][x] && stones[y][x] != EMPTY && markedDead[y][x]) { + const adjacents = [[y-1, x], [y+1, x], [y, x-1], [y, x+1]]; + if(stones[y][x] == WHITE) { + for(const point of adjacents) + extraBlackUnscoreablePoints.add(point); + } + else { + for(const point of adjacents) + extraWhiteUnscoreablePoints.add(point); + } + } + } + } + + for(let y = 0; y < ysize; y++) { + for(let x = 0; x < xsize; x++) { + const s = scoring[y][x]; + const regionId = regionIds[y][x]; + + if(regionId === -1) { + s.isDame = true; + } + else { + const regionInfo = regionInfosById[regionId]; + const color = regionInfo.color; + const totalEyes = Array.from(regionInfo.eyes) + .reduce((acc, eyeId) => acc + eyeInfosById[eyeId].eyeValue, 0); + + if(totalEyes <= 1) + s.belongsToSekiGroup = regionInfo.color; + if(isFalseEyePoint[y][x]) + s.isFalseEye = true; + if(isUnscorableFalseEyePoint[y][x]) + s.isUnscorableFalseEye = true; + if((stones[y][x] == EMPTY || markedDead[y][x]) && ( + (color == BLACK && extraBlackUnscoreablePoints.has([y,x])) || + (color == WHITE && extraWhiteUnscoreablePoints.has([y,x])) + )) { + s.isUnscorableFalseEye = true; + } + + s.eyeValue = eyeIds[y][x] !== -1 ? eyeInfosById[eyeIds[y][x]].eyeValue : 0; + + if((stones[y][x] !== color || markedDead[y][x]) && + s.belongsToSekiGroup === EMPTY && + (scoreFalseEyes || !s.isUnscorableFalseEye) && + chainInfosById[chainIds[y][x]].regionId === regionId && + !(color === WHITE && strictReachesBlack[y][x]) && + !(color === BLACK && strictReachesWhite[y][x]) + ) { + s.isTerritoryFor = color; + } + } + } + } +} + +function assert(condition, message) { + if(!condition) { + throw new Error(message || "Assertion failed"); + } +} + +class CoordinateSet { + constructor(initialCoords = []) { + this.map = new Map(); + this.size = 0; + + if(initialCoords.length > 0) { + for(let coord of initialCoords) { + this.add(coord); + } + } + } + + add(coord) { + const [y, x] = coord; + if(!this.map.has(y)) { + this.map.set(y, new Set()); + } + if(!this.map.get(y).has(x)) { + this.map.get(y).add(x); + this.size++; + } + } + + has(coord) { + const [y, x] = coord; + return this.map.has(y) && this.map.get(y).has(x); + } + + forEach(callback) { + for(let [y, xSet] of this.map.entries()) { + for(let x of xSet) { + callback([y, x]); + } + } + } + + copy() { + const ret = new CoordinateSet(); + ret.map = new Map(this.map); + ret.size = this.size; + return ret; + } + + [Symbol.iterator]() { + const allCoords = []; + for(let [y, xSet] of this.map.entries()) { + for(let x of xSet) { + allCoords.push([y, x]); + } + } + return allCoords[Symbol.iterator](); + } +} + + +export { + EMPTY, + BLACK, + WHITE, + LocScore, + finalTerritoryScore, + finalAreaScore, + territoryScoring, + areaScoring, + + // Other utils + getOpp, + isOnBoard, + isOnBorder, + print2d, + string2d, + string2d2, + colorToStr, +}; + diff --git a/test/autoscore_test_files/game_33811578.json b/test/autoscore_test_files/game_33811578.json index ad67d8d4..c9a14000 100644 --- a/test/autoscore_test_files/game_33811578.json +++ b/test/autoscore_test_files/game_33811578.json @@ -63,25 +63,46 @@ [ 0.9, 0.9, 1.0, 1.0, 1.0, 1.0, 0.5, 0.4, -1.0, -0.9, -0.9, -1.0, -0.9, -1.0, -1.0, -0.9, -1.0, -0.9, -0.9], [ 0.9, 0.9, 0.9, 0.9, 0.9, 0.6, 0.3, 0.3, -1.0, -0.9, -0.9, -1.0, -0.9, -0.9, -0.9, -0.9, -0.9, -0.9, -1.0] ], - "correct_ownership": [ + "sealed_ownership": [ "BBBBBBBBBBBBBWWWWWW", "BBBBBBBBBBBBWWWWWWW", "BBBWBBBBBBBWWWWWWWW", - "BBBWW*B BBWWBBBWWWW", - "BBBBWWWWWBW *BBBWWW", + "BBBWW*BsBBWWBBBWWWW", + "BBBBWWWWWBWs*BBBWWW", "BBBBBBWWWBWBBBBBWWW", - "BBBB WWWWWBBWWBBWWW", + "BBBBsWWWWWBBWWBBWWW", "BBBBBWWWWWBBBWWWWWW", "BBBBBWWWWBBBBWWWWWW", "BBBBBB*W*BBBWWWBBW*", - "BBBBBBBWWB*BBW*BWWB", + "BBBBBBBWWBsBBW*BWWB", "BBBBBBBBWWWWBBBBBBB", "BBBBBBBBBBWWWWBBBBB", - "BBBBBBBBBBBBW**BBBB", + "BBBBBBBBBBBBWBBBBBB", "BBBBBBBBBBWWWWBBWWW", "BBBBBBBBBWWWWWWWWWW", "BBBBBBBWWWWWWWWWWWW", "BBBBBBBBWWWWWWWWWWW", "BBBBBBBBWWWWWWWWWWW" + ], + "correct_ownership": [ + " BBWWWWWW", + " B BBWWWWWWW", + " BWBB BBWWWWWWWW", + " BWW BsBBWWBBBWWWW", + " BBWWWWWBWs BWWW", + " BBBWWWBWBBB BWWW", + " sWWWWWBBWWBBWWW", + " BBWWWWWBBBWWWWWW", + " BWWWWB BWWWWWW", + " B W B BWWWBBW ", + " B BBWWBsBBW BWWB", + " BBBWWWWBBBBBBB", + " B BBBBWWWWBBBBB", + " B BBBBWBBBBBB", + " B BBWWWWBBWWW", + " B BBWWWWWWWWWW", + " B BBWWWWWWWWWWWW", + " B BWWWWWWWWWWW", + " BWWWWWWWWWWW" ] } diff --git a/test/autoscore_test_files/game_33896317.json b/test/autoscore_test_files/game_33896317.json index dc11d691..5c51257f 100644 --- a/test/autoscore_test_files/game_33896317.json +++ b/test/autoscore_test_files/game_33896317.json @@ -50,7 +50,7 @@ "WWWWBBBBBWWWW", "WWWWBBBBWWWWB", "WWWWWBBWWWBBB", - "WWWWBB WWBBBB", + "WWWWBB*WWBBBB", "WWWWWBBWWWBBB", "BWWWWWBWWWBBB", "BBBWWWWWWBBBB", diff --git a/test/autoscore_test_files/game_33921785.json b/test/autoscore_test_files/game_33921785.json index 7888bcea..5a5a9db7 100644 --- a/test/autoscore_test_files/game_33921785.json +++ b/test/autoscore_test_files/game_33921785.json @@ -33,14 +33,25 @@ [-1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0], [-1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0] ], - "correct_ownership": [ + "sealed_ownership": [ "WWWBBBBBB", "WWWWBBBBB", "WWWBBBBBB", "WWWBBBBBB", "WWBBBBBBB", "WWWBBBBBB", - "WWWWBWWB ", + "WWWWBWWBs", + "WWWWWWWWW", + "WWWWWWWWW" + ], + "correct_ownership": [ + "WWWBBB ", + "WWWWB ", + "WWWB ", + "WWWB B ", + "WWBB ", + "WWWBBBBB ", + "WWWWBWWBs", "WWWWWWWWW", "WWWWWWWWW" ] diff --git a/test/autoscore_test_files/game_35115094.json b/test/autoscore_test_files/game_35115094.json index 1845bd5d..a4fdf00c 100644 --- a/test/autoscore_test_files/game_35115094.json +++ b/test/autoscore_test_files/game_35115094.json @@ -63,14 +63,14 @@ [-1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, -1.0], [-1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, -1.0] ], - "correct_ownership": [ + "sealed_ownership": [ "BBBWWWWWWWWBBBBBWWW", "BBBWWWWWWWWBBBBWWWW", "BBBBBBBBBBBWBBBBWWW", "BBBBBBBBB*WWW*WWWWW", - "BBBBBBBBBWWWWWWBBWW", - "**BBBBWBBWWWBBBBBBW", - "*WWBWWWWWBBBBBBBBBB", + "ssBBBBBBBWWWWWWBBWW", + "sBBBBBWBBWWWBBBBBBW", + "WWWBWWWWWBBBBBBBBBB", "WWWBBBWWBBBBBBBBBBB", "WWWWWWBWBBBBWWBBBBB", "WWWWWWBWWBWB*WBBBBB", @@ -83,5 +83,26 @@ "WWWWWWWWBWBBBBBBBWW", "WWWWWWWBBBBBBBBBBBW", "WWWWWWWWBBBBBBBBBBW" + ], + "correct_ownership": [ + "BBBWWWWWWWWBBBBBWWW", + "BBBWWWWWWWWBBBBWWWW", + "BBBBBBBBBBBWBBBBWWW", + "BBBBBBBBB WWW WWWWW", + "ssBBBBBBBWWWWWWBBWW", + "sBBBBBWBBWWWBBBBBBW", + " WWBWWWWWBBBBBBBBBB", + " WBBBWWBBBBBBBBBBB", + " WWWBWBBBBWWBBBBB", + " WWWWBWWBWB WBBBBB", + " WWBBWWWWB WWBBBB", + " W WBBBWBWWBBBWWBBB", + " WBBBBBBWB WWWBWB", + " WWBBBBWWBWWWWWWW", + " W WWWBBBBWWWBBWWWW", + "WW WWBBWWWBBBBWWW", + "WWW WWWBWBBBBBBBWW", + "WWW WBBBBBBBBBBBW", + "WW WWBBBBBBBBBBW" ] } diff --git a/test/autoscore_test_files/game_35115743.json b/test/autoscore_test_files/game_35115743.json index 8e333b4b..94924c49 100644 --- a/test/autoscore_test_files/game_35115743.json +++ b/test/autoscore_test_files/game_35115743.json @@ -63,7 +63,7 @@ [ 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, -1.0, -0.9, -0.9, -0.9, -0.9, -0.9, -0.9, -1.0, -1.0, 1.0, 1.0, 1.0], [ 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, -1.0, -1.0, -0.9, -0.9, -0.9, -0.9, -0.9, -0.9, -0.9, -1.0, -1.0, 1.0, 1.0] ], - "correct_ownership": [ + "sealed_ownership": [ "BWWWWWWWWWWWWWWWWWW", "BWWWWWWWWWWWWWWWWWW", "BBWWWWWWWWWWWWWWWWW", @@ -76,12 +76,33 @@ "BBBBWWWWWWWWBBBBBBB", "BBB*WWWWWWWWWBBBBBB", "BBWWWWWWWWWWWBBBBBB", - "BBBWWWWWWWWWW*BBBBB", + "BBBWWWWWWWWWWsBBBBB", "BBBWWWWWWWWWWBBBBBB", "BBBBWWWWWWWWWWBBBBB", "BBBBBBWWWWWWWWWBBBB", "BBBBBWWWWWWWWWWBBBB", "BBBBBBBWWWWWWWWWBBB", "BBBBBBWWWWWWWWWWWBB" + ], + "correct_ownership": [ + "BWWWWWWWWWWWWWWWWWW", + "BWWWWWWWWWWWWWWWWWW", + "BBWWWWWWWWWWWWWWWWW", + "BBBWWWWWWBBWWWWWWWW", + "BBBBWWWWWBBBWWBBWBW", + "BBBBWWWWBBB BB BBB", + "BBWBBWBBBBB ", + "WWWBWWWWWBB ", + "W WWWWWWWWBBB B ", + "BBBBWWWWWWWWB B ", + "BBB WWWWWWWWWB ", + "BBWWWWWWWWWWWBB ", + "BBBWWWWWWWWWWs ", + "BBBWWWWWWWWWWBB ", + "BBBBWWWWWWWWWWBB ", + "BBBBBBWWWWWWWWWBB ", + "BBBBBWWWWWWWWWWBBB ", + "BBBBBBBWWWWWWWWWB ", + "BBBBBBWWWWWWWWWWWB " ] } diff --git a/test/autoscore_test_files/game_64554594.json b/test/autoscore_test_files/game_64554594.json index ed1aa5fb..86200a09 100644 --- a/test/autoscore_test_files/game_64554594.json +++ b/test/autoscore_test_files/game_64554594.json @@ -63,7 +63,7 @@ [ 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, -1.0, -1.0, -1.0, -0.9, -0.9, 0.7], [ 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, -1.0, -1.0, -0.9, -0.9, -0.8, -0.0] ], - "correct_ownership": [ + "sealed_ownership": [ "BBBBBBBBBBBBBBBBBBB", "BBBBBBBBBBBBBBBBBBB", "BBBBBBBBBBBBBBBBBBB", @@ -83,5 +83,26 @@ "BBBBBBBBBBBBBW*WWB*", "BBBBBBBBBBBBBW*****", "BBBBBBBBBBBBBW*****" + ], + "correct_ownership": [ + " ", + " ", + " B B ", + " B B B B ", + " B B ", + "BBBBBBB B ", + "BWWWWWWBBBBBB B ", + "WWWWWWWWWWWW B ", + "WWWWWWWWWWWWW B B ", + "WWWWWWWWWWWWWWB ", + "WWWWWWWWWWWWW B ", + "WWWWWWWWWWWWWWB B ", + "BBBWWWWWWWWWWWB ", + "BBBBBWWWWWWWWWWBB ", + "BBBBBBWWB WWWWWWB ", + "BBBBBBBBBBBBWWWWBB ", + "BBBBBBBBBBBBBWsWWB ", + "BBBBBBBBBBBBBWsss ", + "BBBBBBBBBBBBBWs " ] } diff --git a/test/autoscore_test_files/game_beta_17150.json b/test/autoscore_test_files/game_beta_17150.json new file mode 100644 index 00000000..0ac1d4cd --- /dev/null +++ b/test/autoscore_test_files/game_beta_17150.json @@ -0,0 +1,47 @@ +{ + "game_id": 17150, + "board": [ + " bWW WW ", + "WWbW WbWb", + "WWbWWWbb ", + "bbbWbbWb ", + " bWWb W ", + " bWWWWWW", + " bbbbbbW", + " b bWWWb", + " b bbWW " + ], + "black": [ + [-0.6, -0.7, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0], + [ 0.3, -0.7, 1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0], + [ 0.7, 0.9, 1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0], + [ 1.0, 1.0, 1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0], + [ 1.0, 1.0, 1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0], + [ 1.0, 1.0, 1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0], + [ 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, -1.0], + [ 1.0, 1.0, 1.0, 1.0, 1.0, 0.9, 0.7, -0.7, -0.7], + [ 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.7, 0.1, -0.6] + ], + "white": [ + [-0.6, -0.7, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0], + [ 0.1, -0.7, 1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0], + [ 0.6, 0.8, 1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0], + [ 1.0, 1.0, 1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0], + [ 1.0, 1.0, 1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0], + [ 1.0, 1.0, 1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0], + [ 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, -1.0], + [ 1.0, 1.0, 1.0, 1.0, 1.0, 0.9, 0.7, -0.7, -0.7], + [ 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.7, -0.1, -0.6] + ], + "correct_ownership": [ + " BWWWWWWW", + "WWBWWWWWW", + "WWBWWWWWW", + "BBBWWWWWW", + "BBBWWWWWW", + "BBBWWWWWW", + "BBBBBBBBW", + "BBBBBWWWB", + "BBBBBBWW " + ] +} diff --git a/test/autoscore_test_files/game_dev_51749995.json b/test/autoscore_test_files/game_dev_51749995.json new file mode 100644 index 00000000..07d323a3 --- /dev/null +++ b/test/autoscore_test_files/game_dev_51749995.json @@ -0,0 +1,108 @@ +{ + "game_id": 51749995, + "board": [ + " bbW ", + " b bbWW ", + " bWbb bbWWWW W ", + " bWW b bbWWbbbW ", + " bbWWWWWbW bW ", + " bbbW WbWbbb bW ", + " W W WbbWWbbWW ", + " bbW WWWbbbWWWW W", + " bWW WbW bW WW W", + " b W b bWWWbbW ", + " b bbWWb bbW bWWb", + " b bWWWWbbb bb ", + " bW bbbbWWWWb b ", + " bW WW bbbbWb bbb", + " bWW WbbWW WbbWWW", + " b WWbbWWWWWWWWbW", + " b bbWWWW W WW b", + " b bWbbWbWWbW ", + " bW W " + ], + "black": [ + [ 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0], + [ 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, -1.0, -1.0, -0.9, -0.9, -1.0, -1.0, -1.0], + [ 1.0, 1.0, 1.0, -1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0], + [ 1.0, 1.0, 1.0, -1.0, -1.0, -0.7, 1.0, -0.6, 1.0, 1.0, -1.0, -1.0, 1.0, 1.0, 1.0, -1.0, -1.0, -1.0, -1.0], + [ 1.0, 1.0, 1.0, 1.0, -1.0, -1.0, -1.0, -1.0, -1.0, 1.0, -1.0, -0.4, -0.2, 1.0, 1.0, 1.0, -1.0, -1.0, -1.0], + [ 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, -1.0, -1.0, -1.0, 1.0, -1.0, 1.0, 1.0, 1.0, 1.0, 1.0, -1.0, -1.0, -1.0], + [ 1.0, 1.0, 1.0, 0.8, -0.9, -1.0, -1.0, -1.0, -1.0, -1.0, 1.0, 1.0, -1.0, -1.0, 1.0, 1.0, -1.0, -1.0, -1.0], + [ 1.0, 1.0, 1.0, 1.0, 1.0, -1.0, -1.0, -1.0, -1.0, -1.0, 1.0, 1.0, 1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0], + [ 1.0, 1.0, 1.0, 1.0, 1.0, -1.0, -1.0, -1.0, -1.0, 1.0, 1.0, 1.0, 1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0], + [ 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, -0.4, -1.0, -0.4, 1.0, 1.0, 1.0, -1.0, -1.0, -1.0, 1.0, 1.0, -1.0, -0.9], + [ 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, -1.0, -1.0, 1.0, -0.6, 1.0, 1.0, -1.0, -0.9, 1.0, -1.0, -1.0, 1.0], + [ 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, -1.0, -1.0, -1.0, -1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.9], + [ 1.0, 1.0, 1.0, 1.0, 0.9, 1.0, 1.0, 1.0, 1.0, 1.0, -1.0, -1.0, -1.0, -1.0, 1.0, 1.0, 1.0, 1.0, 1.0], + [ 1.0, 1.0, 1.0, 1.0, 1.0, 0.9, 0.9, 1.0, 1.0, 1.0, 1.0, 1.0, -1.0, -0.4, -0.3, 1.0, 1.0, 1.0, 1.0], + [ 1.0, 1.0, 1.0, 1.0, 0.9, 0.9, 0.9, 0.9, 1.0, 1.0, -1.0, -1.0, -1.0, -1.0, 1.0, 1.0, -1.0, -1.0, -1.0], + [ 1.0, 1.0, 1.0, 1.0, 1.0, 0.9, 0.9, 1.0, 1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -0.9, -1.0], + [ 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -0.9], + [ 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.9, 0.8, -1.0, -0.9, -0.9, -1.0, -0.9, -1.0, -1.0, -0.9, -1.0, -0.9, -0.9], + [ 1.0, 1.0, 1.0, 1.0, 0.9, 0.9, 0.8, 0.8, -1.0, -0.9, -0.9, -1.0, -0.9, -0.9, -0.9, -1.0, -0.9, -0.9, -1.0] + ], + "white": [ + [ 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, -1.0, -0.9, -0.9, -0.9, -0.9, -1.0], + [ 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, -1.0, -1.0, -0.9, -0.9, -0.9, -0.9, -1.0], + [ 1.0, 1.0, 1.0, -1.0, 1.0, 1.0, 0.9, 1.0, 1.0, 1.0, 1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0], + [ 1.0, 1.0, 1.0, -1.0, -1.0, -0.7, 0.9, -0.7, 1.0, 1.0, -1.0, -1.0, 1.0, 1.0, 1.0, -1.0, -1.0, -1.0, -0.9], + [ 1.0, 1.0, 1.0, 1.0, -1.0, -1.0, -1.0, -1.0, -1.0, 1.0, -1.0, -0.2, 0.1, 1.0, 1.0, 1.0, -1.0, -1.0, -1.0], + [ 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, -1.0, -1.0, -1.0, 1.0, -1.0, 1.0, 1.0, 1.0, 1.0, 1.0, -1.0, -1.0, -1.0], + [ 1.0, 1.0, 1.0, 0.8, -0.8, -1.0, -1.0, -1.0, -1.0, -1.0, 1.0, 1.0, -1.0, -1.0, 1.0, 1.0, -1.0, -1.0, -1.0], + [ 1.0, 1.0, 1.0, 1.0, 1.0, -1.0, -1.0, -1.0, -1.0, -1.0, 1.0, 1.0, 1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0], + [ 1.0, 1.0, 1.0, 1.0, 1.0, -1.0, -1.0, -1.0, -1.0, 1.0, 1.0, 1.0, 1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0], + [ 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, -0.4, -1.0, -0.4, 1.0, 1.0, 1.0, -1.0, -1.0, -1.0, 1.0, 1.0, -1.0, -0.9], + [ 0.9, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, -1.0, -1.0, 1.0, -0.5, 1.0, 1.0, -1.0, -0.8, 1.0, -1.0, -1.0, 1.0], + [ 0.9, 1.0, 1.0, 1.0, 0.9, 1.0, 1.0, 1.0, -1.0, -1.0, -1.0, -1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.9], + [ 0.9, 1.0, 1.0, 1.0, 0.9, 0.9, 1.0, 1.0, 1.0, 1.0, -1.0, -1.0, -1.0, -1.0, 1.0, 1.0, 1.0, 1.0, 1.0], + [ 0.9, 1.0, 1.0, 0.9, 0.9, 0.9, 0.9, 0.9, 1.0, 1.0, 1.0, 1.0, -1.0, -0.5, -0.5, 0.9, 1.0, 1.0, 1.0], + [ 0.9, 1.0, 0.9, 1.0, 0.9, 0.9, 0.9, 0.9, 1.0, 1.0, -1.0, -1.0, -1.0, -1.0, 1.0, 1.0, -1.0, -1.0, -1.0], + [ 0.9, 1.0, 1.0, 1.0, 1.0, 0.9, 0.9, 1.0, 1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -0.9, -1.0], + [ 0.9, 1.0, 1.0, 1.0, 0.9, 1.0, 1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -0.9, -0.9], + [ 0.9, 0.9, 1.0, 1.0, 1.0, 1.0, 0.5, 0.3, -1.0, -0.9, -0.9, -1.0, -0.9, -1.0, -1.0, -0.9, -1.0, -0.9, -0.9], + [ 0.9, 0.9, 0.9, 0.9, 0.9, 0.6, 0.3, 0.2, -1.0, -0.9, -0.9, -1.0, -0.9, -0.9, -0.9, -1.0, -0.9, -0.9, -1.0] + ], + "sealed_ownership": [ + "BBBBBBBBBBBBBWWWWWW", + "BBBBBBBBBBBBWWWWWWW", + "BBBWBBBBBBBWWWWWWWW", + "BBBWW*BsBBWWBBBWWWW", + "BBBBWWWWWBWs*BBBWWW", + "BBBBBBWWWBWBBBBBWWW", + "BBBBsWWWWWBBWWBBWWW", + "BBBBBWWWWWBBBWWWWWW", + "BBBBBWWWWBBBBWWWWWW", + "BBBBBB*W*BBBWWWBBW*", + "BBBBBBBWWBsBBW*BWWB", + "BBBBBBBBWWWWBBBBBBB", + "BBBBBBBBBBWWWWBBBBB", + "BBBBBBBBBBBBWBBBBBB", + "BBBBBBBBBBWWWWBBWWW", + "BBBBBBBBBWWWWWWWWWW", + "BBBBBBBWWWWWWWWWWWW", + "BBBBBBBBWWWWWWWWWWW", + "BBBBBBBBWWWWWWWWWWW" + ], + "correct_ownership": [ + " BBWWWWWW", + " B BBWWWWWWW", + " BWBB BBWWWWWWWW", + " BWW BsBBWWBBBWWWW", + " BBWWWWWBWs BWWW", + " BBBWWWBWBBB BWWW", + " sWWWWWBBWWBBWWW", + " BBWWWWWBBBWWWWWW", + " BWWWWB BWWWWWW", + " B W B BWWWBBW ", + " B BBWWBsBBW BWWB", + " BBBWWWWBBBBBBB", + " B BBBBWWWWBBBBB", + " B BBBBWBBBBBB", + " B BBWWWWBBWWW", + " B BBWWWWWWWWWW", + " B BBWWWWWWWWWWWW", + " B BWWWWWWWWWWW", + " BWWWWWWWWWWW" + ] +} diff --git a/test/autoscore_test_files/game_dev_51750014.json b/test/autoscore_test_files/game_dev_51750014.json new file mode 100644 index 00000000..7733f919 --- /dev/null +++ b/test/autoscore_test_files/game_dev_51750014.json @@ -0,0 +1,63 @@ +{ + "game_id": 51750014, + "board": [ + " b WWWWWWW W", + "W W WWbWbW ", + "WWW WbbbbbWb", + "WbbWW b b ", + "b b WW ", + " bb W b bb", + " b W bW", + " b bW b bWW", + " bWW bWW", + " bbW bbW ", + " bWbbbbWWb", + " bWWWWWWWW", + " bWWW " + ], + "black": [ + [-1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0], + [-1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0, 1.0, -1.0, 1.0, -1.0, -0.1], + [-1.0, -1.0, -1.0, -1.0, -1.0, -1.0, 1.0, 1.0, 1.0, 1.0, 1.0, -1.0, 1.0], + [-1.0, 1.0, 1.0, -1.0, -1.0, -0.6, -0.1, -0.1, 0.5, 1.0, 1.0, 1.0, 1.0], + [1.0, 1.0, 1.0, -0.9, -1.0, -1.0, -0.7, -0.5, 0.0, 1.0, 1.0, 1.0, 1.0], + [1.0, 1.0, 1.0, 1.0, 1.0, -0.2, -1.0, -0.5, 1.0, 1.0, 1.0, 1.0, 1.0], + [1.0, 1.0, 1.0, 1.0, 1.0, -0.2, -1.0, 0.1, 0.5, 0.8, 1.0, 1.0, -1.0], + [1.0, 1.0, 1.0, 1.0, 0.7, 0.8, -1.0, -0.4, 1.0, 0.9, 1.0, -1.0, -1.0], + [1.0, 1.0, 1.0, 1.0, 1.0, -1.0, -1.0, -0.1, 0.2, 0.7, 1.0, -1.0, -1.0], + [1.0, 1.0, 1.0, 1.0, 1.0, -1.0, -0.4, 0.1, 0.5, 1.0, 1.0, -1.0, -1.0], + [1.0, 1.0, 1.0, 1.0, 1.0, -1.0, 1.0, 1.0, 1.0, 1.0, -1.0, -1.0, -1.0], + [1.0, 1.0, 1.0, 1.0, 1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0], + [1.0, 1.0, 1.0, 1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0] + ], + "white": [ + [-1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0], + [-1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0, 1.0, -1.0, 1.0, -1.0, -0.1], + [-1.0, -1.0, -1.0, -1.0, -1.0, -1.0, 1.0, 1.0, 1.0, 1.0, 1.0, -1.0, 1.0], + [-1.0, 1.0, 1.0, -1.0, -1.0, -0.6, -0.1, 0.1, 0.5, 1.0, 1.0, 1.0, 1.0], + [0.9, 0.9, 1.0, -0.6, -1.0, -1.0, -0.5, -0.1, 0.1, 1.0, 1.0, 1.0, 1.0], + [0.9, 0.9, 1.0, 1.0, 1.0, -0.3, -1.0, -0.4, 1.0, 0.9, 1.0, 1.0, 1.0], + [0.9, 1.0, 1.0, 1.0, 1.0, -0.3, -1.0, -0.0, 0.5, 1.0, 1.0, 1.0, -1.0], + [1.0, 1.0, 1.0, 1.0, 0.7, 0.8, -1.0, -0.3, 1.0, 0.9, 1.0, -1.0, -1.0], + [1.0, 1.0, 1.0, 1.0, 1.0, -1.0, -1.0, -0.2, -0.0, 0.6, 1.0, -1.0, -1.0], + [1.0, 1.0, 0.9, 1.0, 1.0, -1.0, -0.5, 0.0, 0.5, 1.0, 1.0, -1.0, -1.0], + [0.9, 0.9, 0.9, 0.6, 1.0, -1.0, 1.0, 1.0, 1.0, 1.0, -1.0, -1.0, -1.0], + [0.7, 0.9, 0.6, -0.9, 1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0], + [0.0, -0.6, -0.9, -0.9, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0] + ], + "correct_ownership": [ + "WWWWWWWWWWWWW", + "WWWWWWWWBWBW ", + "WWWWWWBBBBBWB", + "WBBWWs** B B ", + "BBB WWs** ", + "BBBBB WsB BB", + "BBBBB Ws* BW", + "BBBBBBWsB BWW", + "BBBBBWWs* BWW", + "BBBBBWs**BBWW", + "BBBBBWBBBBWWW", + "BBBBBWWWWWWWW", + "BBBBWWWWWWWWW" + ] +} diff --git a/test/autoscore_test_files/game_seki_64848549.json b/test/autoscore_test_files/game_seki_64848549.json new file mode 100644 index 00000000..f82c795c --- /dev/null +++ b/test/autoscore_test_files/game_seki_64848549.json @@ -0,0 +1,88 @@ +{ + "game_id": 64848549, + "rules": "japanese", + "board": [ + " b Wb bWb WWb W b ", + "bWWWb bWbbbbbbWbbb", + "b Wbb bWWWWWWWbbWW", + "WWWb bW WWW ", + "bbbb bWWWWWWW WWW", + "WWWWb b bbWbbWbbbbb", + " W Wb b b bb WWW", + " WWb b bbWWWWb ", + "WWWbWb bW bW WWW", + " WWb b bW bWbbW ", + " Wb bbWbbW Wbb", + "bWb bbbbWW WWbbbbb ", + "WbbbW bW WWW bb", + "W bWW bWWWWW W WWW", + "WWWW Wbb bbW WWWWbb", + "bbbWWb bbb bbbbWb ", + " b bWbbb bbW bWbbb", + "bWbbW bWWWWWWWbWb W", + " W bW bW bbb WbWbW " + ], + "black": [ + [ 1.0, 1.0, -0.1, -1.0, 1.0, 1.0, 1.0, 1.0, -1.0, 1.0, 1.0, 0.8, 0.9, 1.0, -0.0, -1.0, -0.0, 1.0, 1.0], + [ 1.0, -1.0, -1.0, -1.0, 1.0, 1.0, 1.0, 1.0, -1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, -1.0, 1.0, 1.0, 1.0], + [ 1.0, -0.1, -1.0, 1.0, 1.0, 1.0, 1.0, 1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0, 1.0, 1.0, -1.0, -1.0], + [-1.0, -1.0, -1.0, 1.0, 1.0, 1.0, 1.0, 1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0], + [ 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -0.4, -1.0, -1.0, -1.0], + [-1.0, -1.0, -1.0, -1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, -1.0, 1.0, 1.0, -1.0, 1.0, 1.0, 1.0, 1.0, 1.0], + [-1.0, -1.0, -1.0, -1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.4, 1.0, 1.0, 1.0, 1.0, 0.6, -1.0, -1.0, -1.0], + [-1.0, -1.0, -1.0, -1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.6, 1.0, 1.0, -1.0, -1.0, -1.0, -1.0, -0.8, -0.9], + [-1.0, -1.0, -1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, -1.0, 0.4, 1.0, -1.0, -0.2, -0.2, -1.0, -1.0, -1.0], + [-1.0, -1.0, -1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, -1.0, 0.2, 1.0, -1.0, 1.0, 1.0, -1.0, -0.1, 0.0], + [-1.0, -1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, -1.0, 1.0, 1.0, -1.0, 0.3, 0.5, -1.0, 1.0, 1.0], + [-1.0, -1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, -1.0, -1.0, -1.0, -1.0, -1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.9], + [-1.0, 1.0, 1.0, 1.0, -1.0, -0.3, 0.8, 1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -0.7, 1.0, 1.0], + [-1.0, -0.3, 0.4, 1.0, -1.0, -1.0, 0.1, 1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0], + [-1.0, -1.0, -1.0, -1.0, -1.0, -1.0, 1.0, 1.0, 0.3, 1.0, 1.0, -1.0, -0.5, -1.0, -1.0, -1.0, -1.0, 1.0, 1.0], + [ 1.0, 1.0, 1.0, -1.0, -1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, -1.0, 1.0, 1.0, 1.0], + [ 1.0, 1.0, 1.0, 1.0, -1.0, 1.0, 1.0, 1.0, 0.2, 1.0, 1.0, -1.0, -0.3, 0.3, 1.0, -1.0, 1.0, 1.0, 1.0], + [ 1.0, -1.0, 1.0, 1.0, -1.0, 0.3, 1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0, 1.0, -1.0, 1.0, -0.0, -1.0], + [-0.0, -1.0, 0.1, 1.0, -1.0, 0.2, 1.0, -1.0, 0.0, 1.0, 1.0, 1.0, 0.0, -1.0, 1.0, -1.0, 1.0, -1.0, -0.9] + ], + "white": [ + [ 1.0, 1.0, -0.1, -1.0, 1.0, 1.0, 1.0, 1.0, -1.0, 1.0, 1.0, 0.8, 0.9, 1.0, -0.1, -0.9, -0.0, 1.0, 1.0], + [ 1.0, -1.0, -1.0, -1.0, 1.0, 1.0, 1.0, 1.0, -1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, -1.0, 1.0, 1.0, 1.0], + [ 1.0, -0.1, -1.0, 1.0, 1.0, 1.0, 1.0, 1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0, 1.0, 1.0, -1.0, -1.0], + [-1.0, -1.0, -1.0, 1.0, 1.0, 1.0, 1.0, 1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0], + [ 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -0.5, -1.0, -1.0, -1.0], + [-1.0, -1.0, -1.0, -1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, -1.0, 1.0, 1.0, -1.0, 1.0, 1.0, 1.0, 1.0, 1.0], + [-1.0, -1.0, -1.0, -1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.4, 1.0, 1.0, 1.0, 1.0, 0.5, -1.0, -1.0, -1.0], + [-1.0, -1.0, -1.0, -1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.6, 1.0, 1.0, -1.0, -1.0, -1.0, -1.0, -0.8, -0.8], + [-1.0, -1.0, -1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, -1.0, 0.3, 1.0, -1.0, -0.2, -0.2, -1.0, -1.0, -1.0], + [-1.0, -1.0, -1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, -1.0, 0.1, 1.0, -1.0, 1.0, 1.0, -1.0, -0.2, -0.1], + [-1.0, -1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, -1.0, 1.0, 1.0, -1.0, 0.3, 0.5, -1.0, 1.0, 1.0], + [-1.0, -1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, -1.0, -1.0, -1.0, -1.0, -1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.9], + [-1.0, 1.0, 1.0, 1.0, -1.0, -0.3, 0.8, 1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -0.6, 1.0, 1.0], + [-1.0, -0.3, 0.4, 1.0, -1.0, -1.0, -0.0, 1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0], + [-1.0, -1.0, -1.0, -1.0, -1.0, -1.0, 1.0, 1.0, 0.2, 1.0, 1.0, -1.0, -0.5, -1.0, -1.0, -1.0, -1.0, 1.0, 1.0], + [ 1.0, 1.0, 1.0, -1.0, -1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, -1.0, 1.0, 1.0, 1.0], + [ 1.0, 1.0, 1.0, 1.0, -1.0, 1.0, 1.0, 1.0, 0.2, 1.0, 1.0, -1.0, -0.3, 0.2, 1.0, -1.0, 1.0, 1.0, 1.0], + [ 1.0, -1.0, 1.0, 1.0, -1.0, 0.3, 1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0, 1.0, -1.0, 1.0, -0.0, -1.0], + [-0.0, -1.0, 0.1, 1.0, -1.0, 0.2, 1.0, -1.0, -0.0, 1.0, 1.0, 1.0, 0.0, -1.0, 1.0, -1.0, 1.0, -1.0, -0.9] + ], + "correct_ownership": [ + " B WBBBBWB B W B ", + "BWWWBBBBWBBBBBBWBBB", + "B WBBBBBWWWWWWWBBWW", + "WWWBBBBBWWWWWWWWWWW", + "BBBBBBBBWWWWWWW WWW", + "WWWWBBBBBBWBBWBBBBB", + "WWWWBBBBBB B BB WWW", + "WWWWBBBBBB BBWWWW ", + "WWWBBBBBBBW BW WWW", + "WWWBBBBBBBW BWBBW ", + " WBBBBBBBBWBBW WBB", + " WBBBBBBWW WWBBBBB ", + "WBBBW BWWWWWWWW BB", + "W BWW BWWWWWWWWWWW", + "WWWW WBB BBW WWWWBB", + "BBBWWBBBBB BBBBWB ", + " B BWBBB BBW BWBBB", + "BWBBW BWWWWWWWBWB W", + " W BW BW BBB WBWBW " + ] +} diff --git a/test/test_autoscore.ts b/test/test_autoscore.ts index bf7bbead..64056249 100755 --- a/test/test_autoscore.ts +++ b/test/test_autoscore.ts @@ -16,7 +16,7 @@ */ /* This script is for development, debugging, and manual testing of the - * autoscore functionality found in src/autoscore.ts + * autoscore functionality * * Usage: * @@ -27,11 +27,10 @@ */ import { existsSync, readFileSync, readdirSync } from "fs"; -import { autoscore } from "../src/autoscore"; +import { autoscore } from "engine/autoscore"; import * as clc from "cli-color"; -import { GoEngine, GoEngineInitialState } from "../src/GoEngine"; -import { char2num, makeMatrix, num2char } from "../src/GoMath"; -import { JGOFNumericPlayerColor } from "../src/JGOF"; +import { GobanEngine, GobanEngineInitialState, char2num, makeMatrix, num2char } from "engine"; +import { JGOFMove, JGOFNumericPlayerColor, JGOFSealingIntersection } from "engine/formats/JGOF"; function run_autoscore_tests() { const test_file_directory = "autoscore_test_files"; @@ -109,9 +108,18 @@ function test_file(path: string, quiet: boolean): boolean { if (!data.correct_ownership) { throw new Error(`${path} correct_ownership field is invalid`); } + + const rules = data.rules ?? "chinese"; + + // validate ownership structures look ok for (const row of data.correct_ownership) { for (const cell of row) { - const is_w_or_b = cell === "W" || cell === "B" || cell === " " || cell === "*"; + const is_w_or_b = + cell === "W" || // owned by white + cell === "B" || // owned by black + cell === " " || // dame + cell === "*" || // anything + cell === "s"; // marked for needing to seal if (!is_w_or_b) { throw new Error( `${path} correct_ownership field contains "${cell}" which is invalid`, @@ -119,147 +127,220 @@ function test_file(path: string, quiet: boolean): boolean { } } } - - // run autoscore - const [res, debug_output] = autoscore(data.board, data.black, data.white); - - let match = true; - const matches: boolean[][] = []; - - for (let y = 0; y < res.result.length; ++y) { - matches[y] = []; - for (let x = 0; x < res.result[0].length; ++x) { - const v = res.result[y][x]; - const m = - data.correct_ownership[y][x] === "*" || - (v === 0 && data.correct_ownership[y][x] === " ") || - (v === 1 && data.correct_ownership[y][x] === "B") || - (v === 2 && data.correct_ownership[y][x] === "W"); - matches[y][x] = m; - match &&= m; + if (data.sealed_ownership) { + for (const row of data.sealed_ownership) { + for (const cell of row) { + const is_w_or_b = + cell === "W" || // owned by white + cell === "B" || // owned by black + cell === " " || // dame + cell === "*" || // anything + cell === "s"; // marked for needing to seal + if (!is_w_or_b) { + throw new Error( + `${path} correct_ownership field contains "${cell}" which is invalid`, + ); + } + } } } + // run autoscore + const [res, debug_output] = autoscore(data.board, rules, data.black, data.white); + if (!quiet) { - // Double check that when we run everything through our normal GoEngine.computeScore function, - // that we get the result we're expecting - if (match) { - const board = original_board.map((row: number[]) => row.slice()); - - let black_state = ""; - let white_state = ""; - - for (let y = 0; y < board.length; ++y) { - for (let x = 0; x < board[y].length; ++x) { - const v = board[y][x]; - const c = num2char(x) + num2char(y); - if (v === JGOFNumericPlayerColor.BLACK) { - black_state += c; - } else if (v === 2) { - white_state += c; - } + console.log(""); + console.log(debug_output); + console.log(""); + } + + let ok = true; + if (data.sealed_ownership) { + ok &&= test_result( + "Sealed ownership", + res.sealed_result, + res.removed, + res.needs_sealing, + true, + data.sealed_ownership, + quiet, + ); + } + ok &&= test_result( + "Correct ownership", + res.result, + res.removed, + res.needs_sealing, + false, + data.correct_ownership, + quiet, + ); + + return ok; + + function test_result( + mnemonic: string, + result: JGOFNumericPlayerColor[][], + removed: JGOFMove[], + needs_sealing: JGOFSealingIntersection[], + perform_sealing: boolean, + correct_ownership: string[], + quiet: boolean, + ) { + if (!quiet) { + console.log(""); + console.log(`=== Testing ${mnemonic} ===`); + } + + let match = true; + const matches: boolean[][] = []; + + for (let y = 0; y < result.length; ++y) { + matches[y] = []; + for (let x = 0; x < result[0].length; ++x) { + const v = result[y][x]; + let m = + correct_ownership[y][x] === "*" || + correct_ownership[y][x] === "s" || // seal + (v === 0 && correct_ownership[y][x] === " ") || + (v === 1 && correct_ownership[y][x] === "B") || + (v === 2 && correct_ownership[y][x] === "W"); + + if (correct_ownership[y][x] === "s") { + const has_needs_sealing = + needs_sealing.find((pt) => pt.x === x && pt.y === y) !== undefined; + + m &&= has_needs_sealing; } + + matches[y][x] = m; + match &&= m; } + } - const initial_state: GoEngineInitialState = { - black: black_state, - white: white_state, - }; + /* Ensure all needs_sealing are marked as such */ + for (const { x, y } of needs_sealing) { + if (correct_ownership[y][x] !== "s" && correct_ownership[y][x] !== "*") { + console.error( + `Engine thought we needed sealing at ${x},${y} but the that spot wasn't flagged as needing it in the test file`, + ); + match = false; + matches[y][x] = false; + } + } - const engine = new GoEngine({ - width: board[0].length, - height: board.length, - initial_state, - rules: "chinese", // for area scoring - removed: res.removed_string, - }); + if (!quiet) { + // Double check that when we run everything through our normal GobanEngine.computeScore function, + // that we get the result we're expecting. We exclude the japanese and korean rules here because + // our test file ownership maps always include territory and stones. + if (match && rules !== "japanese" && rules !== "korean") { + const board = original_board.map((row: number[]) => row.slice()); - const score = engine.computeScore(); + if (perform_sealing) { + for (const { x, y, color } of needs_sealing) { + board[y][x] = color; + } + } - const scored_board = makeMatrix(board[0].length, board.length, 0); + let black_state = ""; + let white_state = ""; + + for (let y = 0; y < board.length; ++y) { + for (let x = 0; x < board[y].length; ++x) { + const v = board[y][x]; + const c = num2char(x) + num2char(y); + if (v === JGOFNumericPlayerColor.BLACK) { + black_state += c; + } else if (v === 2) { + white_state += c; + } + } + } - for (let i = 0; i < score.black.scoring_positions.length; i += 2) { - const x = char2num(score.black.scoring_positions[i]); - const y = char2num(score.black.scoring_positions[i + 1]); - scored_board[y][x] = JGOFNumericPlayerColor.BLACK; - } - for (let i = 0; i < score.white.scoring_positions.length; i += 2) { - const x = char2num(score.white.scoring_positions[i]); - const y = char2num(score.white.scoring_positions[i + 1]); - scored_board[y][x] = JGOFNumericPlayerColor.WHITE; - } + const initial_state: GobanEngineInitialState = { + black: black_state, + white: white_state, + }; + + const engine = new GobanEngine({ + width: board[0].length, + height: board.length, + initial_state, + rules, + removed, + }); + + const score = engine.computeScore(); + + const scored_board = makeMatrix(board[0].length, board.length, 0); - let official_match = true; - const official_matches: boolean[][] = []; - for (let y = 0; y < scored_board.length; ++y) { - official_matches[y] = []; - for (let x = 0; x < scored_board[0].length; ++x) { - const v = scored_board[y][x]; - const m = - data.correct_ownership[y][x] === "*" || - (v === 0 && data.correct_ownership[y][x] === " ") || - (v === 1 && data.correct_ownership[y][x] === "B") || - (v === 2 && data.correct_ownership[y][x] === "W"); - official_matches[y][x] = m; - official_match &&= m; + for (let i = 0; i < score.black.scoring_positions.length; i += 2) { + const x = char2num(score.black.scoring_positions[i]); + const y = char2num(score.black.scoring_positions[i + 1]); + scored_board[y][x] = JGOFNumericPlayerColor.BLACK; + } + for (let i = 0; i < score.white.scoring_positions.length; i += 2) { + const x = char2num(score.white.scoring_positions[i]); + const y = char2num(score.white.scoring_positions[i + 1]); + scored_board[y][x] = JGOFNumericPlayerColor.WHITE; } - } - if (!quiet) { - if (official_match) { - console.log("Final autoscore matches official scoring"); - } else { - console.error("Official score did not match our expected scoring"); + let official_match = true; + const official_matches: boolean[][] = []; + for (let y = 0; y < scored_board.length; ++y) { + official_matches[y] = []; + for (let x = 0; x < scored_board[0].length; ++x) { + const v = scored_board[y][x]; + const m = + correct_ownership[y][x] === "*" || + correct_ownership[y][x] === "s" || + //(v === 0 && correct_ownership[y][x] === "s") || + (v === 0 && correct_ownership[y][x] === " ") || + (v === 1 && correct_ownership[y][x] === "B") || + (v === 2 && correct_ownership[y][x] === "W"); + official_matches[y][x] = m; + official_match &&= m; + } + } + + if (!quiet && !official_match) { + console.log(""); + console.log(""); + console.log("Final scored board"); print_expected( scored_board.map((row) => row.map((v) => (v === 1 ? "B" : v === 2 ? "W" : " ")).join(""), ), ); - print_mismatches(official_matches); + + if (official_match) { + console.log("Final autoscore matches official scoring"); + } else { + console.error("Official score did not match our expected scoring"); + print_mismatches(official_matches); + } } + + match &&= official_match; } - match &&= official_match; - } + if (!match) { + console.log("Expected ownership:"); + print_expected(correct_ownership); + console.log("Mismatches:"); + print_mismatches(matches); + console.log(""); - if (!match) { - console.log(""); - console.log(""); - console.log(`>>> ${path} failed`); - console.log(`>>> ${path} failed`); - console.log(`>>> ${path} failed`); - console.log(""); - console.log(debug_output); - console.log(""); - console.log("Expected ownership:"); - print_expected(data.correct_ownership); - console.log("Mismatches:"); - print_mismatches(matches); - console.log(""); - - console.log("Removed"); - for (const [x, y, reason] of res.removed) { - console.log( - ` ${"ABCDEFGHJKLMNOPQRSTUVWXYZ"[x]}${data.board.length - y}: ${reason}`, - ); + console.log(`${mnemonic} ${path} failed`); + } else { + console.log(`${mnemonic} ${path} passed`); } - - console.log(`<<< ${path} failed`); - console.log(`<<< ${path} failed`); - console.log(`<<< ${path} failed`); console.log(""); console.log(""); - } else { - console.log(""); - console.log(""); - console.log(debug_output); - console.log(""); - console.log(""); - console.log(`${path} passed`); } - } - return match; + return match; + } } if (require.main === module) { @@ -316,7 +397,9 @@ function print_expected(board: string[]) { } else if (c === " ") { out += clc.blue("."); } else if (c === "*") { - out += clc.yellow(" "); + out += clc.yellow("*"); + } else if (c === "s") { + out += clc.magenta("s"); } else { out += clc.red(c); } diff --git a/src/__tests__/GoConditionalMove.test.ts b/test/unit_tests/GoConditionalMove.test.ts similarity index 80% rename from src/__tests__/GoConditionalMove.test.ts rename to test/unit_tests/GoConditionalMove.test.ts index d0f2b258..713d7a7a 100644 --- a/src/__tests__/GoConditionalMove.test.ts +++ b/test/unit_tests/GoConditionalMove.test.ts @@ -1,4 +1,4 @@ -import { GoConditionalMove } from "../GoConditionalMove"; +import { ConditionalMoveTree } from "engine"; /** * ``` @@ -11,7 +11,7 @@ import { GoConditionalMove } from "../GoConditionalMove"; * ``` */ function makeLargeTree() { - return GoConditionalMove.decode([ + return ConditionalMoveTree.decode([ null, { aa: ["bb", { cc: [null, {}], dd: ["ee", { ff: ["gg", {}] }], hh: ["ii", {}] }], @@ -22,7 +22,7 @@ function makeLargeTree() { describe("constructor", () => { test("null", () => { - const m = new GoConditionalMove(null); + const m = new ConditionalMoveTree(null); expect(m.children).toEqual({}); expect(m.move).toBeNull(); @@ -30,7 +30,7 @@ describe("constructor", () => { }); test("with move string", () => { - const m = new GoConditionalMove("aa"); + const m = new ConditionalMoveTree("aa"); expect(m.children).toEqual({}); expect(m.move).toBe("aa"); @@ -38,8 +38,8 @@ describe("constructor", () => { }); test("with move string and parent", () => { - const p = new GoConditionalMove("aa"); - const m = new GoConditionalMove("bb", p); + const p = new ConditionalMoveTree("aa"); + const m = new ConditionalMoveTree("bb", p); expect(m.children).toEqual({}); expect(m.move).toBe("bb"); @@ -53,13 +53,13 @@ describe("constructor", () => { describe("encode/decode", () => { test("null", () => { - const m = new GoConditionalMove(null); + const m = new ConditionalMoveTree(null); expect(m.encode()).toEqual([null, {}]); }); test("with move string", () => { - const m = new GoConditionalMove("aa"); + const m = new ConditionalMoveTree("aa"); expect(m.encode()).toEqual(["aa", {}]); }); @@ -88,14 +88,14 @@ describe("encode/decode", () => { describe("duplicate", () => { test("null", () => { - const m = new GoConditionalMove(null); + const m = new ConditionalMoveTree(null); expect(m.duplicate()).toEqual(m); expect(m.duplicate()).not.toBe(m); }); test("with move string", () => { - const m = new GoConditionalMove("aa"); + const m = new ConditionalMoveTree("aa"); expect(m.duplicate()).toEqual(m); expect(m.duplicate()).not.toBe(m); @@ -116,6 +116,6 @@ describe("duplicate", () => { }); test("getChild returns GoConditionalMove if doesn't exist", () => { - const m = new GoConditionalMove(null); - expect(m.getChild("aa")).toEqual(new GoConditionalMove(null, m)); + const m = new ConditionalMoveTree(null); + expect(m.getChild("aa")).toEqual(new ConditionalMoveTree(null, m)); }); diff --git a/src/__tests__/GoEngine.test.ts b/test/unit_tests/GoEngine.test.ts similarity index 82% rename from src/__tests__/GoEngine.test.ts rename to test/unit_tests/GoEngine.test.ts index c0b48642..ead4325a 100644 --- a/src/__tests__/GoEngine.test.ts +++ b/test/unit_tests/GoEngine.test.ts @@ -1,13 +1,15 @@ //cspell: disable -import { GoEngine } from "../GoEngine"; -import { movesFromBoardState } from "../test_utils"; -import { GobanMoveError } from "../GobanError"; -import { JGOFIntersection } from "../JGOF"; -import { makeMatrix } from "../GoMath"; +import { + GobanEngine, + GobanMoveError, + JGOFIntersection, + makeMatrix, + matricesAreEqual, +} from "engine"; +import { movesFromBoardState } from "./test_utils"; test("boardMatricesAreTheSame", () => { - const engine = new GoEngine({}); const a = [ [1, 2], [3, 4], @@ -24,14 +26,14 @@ test("boardMatricesAreTheSame", () => { [1, 2, 5], [3, 4, 6], ]; - expect(engine.boardMatricesAreTheSame(a, b)).toBe(true); - expect(engine.boardMatricesAreTheSame(a, c)).toBe(false); - expect(engine.boardMatricesAreTheSame(a, d)).toBe(false); + expect(matricesAreEqual(a, b)).toBe(true); + expect(matricesAreEqual(a, c)).toBe(false); + expect(matricesAreEqual(a, d)).toBe(false); }); describe("computeScore", () => { - test("GoEngine defaults", () => { - const engine = new GoEngine({}); + test("GobanEngine defaults", () => { + const engine = new GobanEngine({}); expect(engine.computeScore()).toEqual({ black: { handicap: 0, @@ -54,8 +56,8 @@ describe("computeScore", () => { }); }); - test("GoEngine defaults", () => { - const engine = new GoEngine({}); + test("GobanEngine defaults", () => { + const engine = new GobanEngine({}); expect(engine.computeScore()).toEqual({ black: { handicap: 0, @@ -79,7 +81,7 @@ describe("computeScore", () => { }); test("Japanese handicap", () => { - const engine = new GoEngine({ rules: "japanese", handicap: 4 }); + const engine = new GobanEngine({ rules: "japanese", handicap: 4 }); expect(engine.computeScore()).toEqual({ black: expect.objectContaining({ handicap: 0, @@ -97,7 +99,7 @@ describe("computeScore", () => { }); test("AGA handicap - white is given compensation ", () => { - const engine = new GoEngine({ rules: "aga", handicap: 4 }); + const engine = new GobanEngine({ rules: "aga", handicap: 4 }); // From the AGA Concise rules of Go: // @@ -120,7 +122,7 @@ describe("computeScore", () => { [0, 1, 2, 0], [0, 1, 2, 0], ]; - const engine = new GoEngine({ width: 4, height: 4, moves: movesFromBoardState(board) }); + const engine = new GobanEngine({ width: 4, height: 4, moves: movesFromBoardState(board) }); expect(engine.computeScore()).toEqual({ black: expect.objectContaining({ @@ -146,7 +148,7 @@ describe("computeScore", () => { [0, 1, 2, 0], [0, 1, 2, 0], ]; - const engine = new GoEngine({ + const engine = new GobanEngine({ width: 4, height: 4, moves: movesFromBoardState(board), @@ -155,14 +157,14 @@ describe("computeScore", () => { expect(engine.computeScore()).toEqual({ black: expect.objectContaining({ - scoring_positions: "aaabacadbabbbcbd", + scoring_positions: "aabaabbbacbcadbd", stones: 4, territory: 4, total: 8, }), white: expect.objectContaining({ komi: 7.5, - scoring_positions: "dadbdcddcacbcccd", + scoring_positions: "cadacbdbccdccddd", stones: 4, territory: 4, total: 15.5, @@ -177,7 +179,7 @@ describe("computeScore", () => { [0, 1, 2, 0], [0, 1, 2, 1], ]; - const engine = new GoEngine({ + const engine = new GobanEngine({ width: 4, height: 4, moves: movesFromBoardState(board), @@ -188,7 +190,7 @@ describe("computeScore", () => { expect(engine.computeScore()).toEqual({ black: expect.objectContaining({ prisoners: 0, - scoring_positions: "aaabacadbabbbcbd", + scoring_positions: "aabaabbbacbcadbd", stones: 4, territory: 4, total: 8, @@ -196,7 +198,7 @@ describe("computeScore", () => { white: expect.objectContaining({ prisoners: 0, komi: 7.5, - scoring_positions: "dadbdcddcacbcccd", + scoring_positions: "cadacbdbccdccddd", stones: 4, territory: 4, total: 15.5, @@ -208,8 +210,8 @@ describe("computeScore", () => { describe("rules", () => { test("Korean is almost the same as Japanese", () => { // https://forums.online-go.com/t/just-a-brief-question/3564/10 - const korean_config = new GoEngine({ rules: "korean" }).config; - const japanese_config = new GoEngine({ rules: "japanese" }).config; + const korean_config = new GobanEngine({ rules: "korean" }).config; + const japanese_config = new GobanEngine({ rules: "japanese" }).config; delete korean_config.rules; delete japanese_config.rules; @@ -218,9 +220,9 @@ describe("rules", () => { }); }); -describe("GoEngine.place()", () => { +describe("GobanEngine.place()", () => { test("Basic test to make sure it's working", () => { - const engine = new GoEngine({}); + const engine = new GobanEngine({}); engine.place(16, 3); engine.place(3, 2); @@ -254,7 +256,7 @@ describe("GoEngine.place()", () => { }); test("stone on top of stone", () => { - const engine = new GoEngine({ width: 3, height: 3 }); + const engine = new GobanEngine({ width: 3, height: 3 }); engine.place(1, 1); @@ -264,7 +266,7 @@ describe("GoEngine.place()", () => { }); test("capture", () => { - const engine = new GoEngine({ width: 2, height: 2 }); + const engine = new GobanEngine({ width: 2, height: 2 }); engine.place(0, 1); engine.place(0, 0); @@ -277,7 +279,7 @@ describe("GoEngine.place()", () => { }); test("ko", () => { - const engine = new GoEngine({ + const engine = new GobanEngine({ width: 4, height: 3, initial_state: { @@ -301,7 +303,7 @@ describe("GoEngine.place()", () => { }); test("superko", () => { - const engine = new GoEngine({ + const engine = new GobanEngine({ rules: "chinese", initial_state: { black: "dabbcbdbccadbdcd", @@ -328,8 +330,8 @@ describe("GoEngine.place()", () => { ); }); - test("suicide", () => { - const engine = new GoEngine({ + test("self capture", () => { + const engine = new GobanEngine({ width: 2, height: 2, initial_state: { @@ -344,7 +346,7 @@ describe("GoEngine.place()", () => { */ expect(() => engine.place(0, 0)).toThrow( - new GobanMoveError(0, 0, "A2", "move_is_suicidal"), + new GobanMoveError(0, 0, "A2", "illegal_self_capture"), ); }); @@ -353,7 +355,7 @@ describe("GoEngine.place()", () => { set: jest.fn(), }; - const engine = new GoEngine( + const engine = new GobanEngine( { width: 2, height: 2, @@ -376,7 +378,7 @@ describe("GoEngine.place()", () => { }); test("removed_stones parameter", () => { - const engine = new GoEngine({ width: 2, height: 2 }); + const engine = new GobanEngine({ width: 2, height: 2 }); engine.place(0, 1); engine.place(0, 0); @@ -389,7 +391,7 @@ describe("GoEngine.place()", () => { describe("moves", () => { test("cur_review_move", () => { - const engine = new GoEngine({}); + const engine = new GobanEngine({}); const on_cur_review_move = jest.fn(); engine.addListener("cur_review_move", on_cur_review_move); @@ -412,7 +414,7 @@ describe("moves", () => { }); test("cur_move", () => { - const engine = new GoEngine({}); + const engine = new GobanEngine({}); const on_cur_move = jest.fn(); engine.addListener("cur_move", on_cur_move); @@ -428,7 +430,7 @@ describe("moves", () => { describe("setLastOfficialMove", () => { test("cur_move on trunk", () => { - const engine = new GoEngine({}); + const engine = new GobanEngine({}); const on_last_official_move = jest.fn(); engine.addListener("last_official_move", on_last_official_move); @@ -452,7 +454,7 @@ describe("moves", () => { }); test("cur_move not on trunk is an error", () => { - const engine = new GoEngine({}); + const engine = new GobanEngine({}); // isTrunkMove is false by default engine.place(10, 10); @@ -466,7 +468,7 @@ describe("moves", () => { { x: 0, y: 0 }, { x: 1, y: 1 }, ]; - const engine = new GoEngine({ width: 2, height: 2, moves: moves }); + const engine = new GobanEngine({ width: 2, height: 2, moves: moves }); expect(engine.board).toEqual([ [1, 0], @@ -481,7 +483,7 @@ describe("moves", () => { ]; // Placement errors are logged, not thrown const log_spy = jest.spyOn(console, "log").mockImplementation(() => {}); - const engine = new GoEngine({ width: 2, height: 2, moves: moves }); + const engine = new GobanEngine({ width: 2, height: 2, moves: moves }); expect(engine.board).toEqual([ [0, 0], @@ -504,7 +506,9 @@ describe("moves", () => { // Personally I don't think this should throw - it would be nice if we could just pass in // a move_tree, but not moves and moves could be inferred by traversing trunk. - expect(() => new GoEngine({ width: 2, height: 2, move_tree })).toThrow("Node mismatch"); + expect(() => new GobanEngine({ width: 2, height: 2, move_tree })).toThrow( + "Node mismatch", + ); }); test("move_tree with two trunk moves", () => { @@ -521,7 +525,7 @@ describe("moves", () => { }, }; - const engine = new GoEngine({ width: 2, height: 2, move_tree }); + const engine = new GobanEngine({ width: 2, height: 2, move_tree }); expect(engine.board).toEqual([ [0, 0], @@ -550,7 +554,7 @@ describe("moves", () => { }, }; - const engine = new GoEngine({ width: 2, height: 2, move_tree }); + const engine = new GobanEngine({ width: 2, height: 2, move_tree }); expect(engine.cur_move.move_number).toBe(0); expect(engine.showNext()).toBe(true); @@ -571,7 +575,7 @@ describe("moves", () => { }, }; - const engine = new GoEngine({ width: 2, height: 2, move_tree }); + const engine = new GobanEngine({ width: 2, height: 2, move_tree }); expect(engine.cur_move.move_number).toBe(0); expect(engine.showNextTrunk()).toBe(true); @@ -580,7 +584,7 @@ describe("moves", () => { }); test("followPath", () => { - const engine = new GoEngine({ width: 4, height: 2 }); + const engine = new GobanEngine({ width: 4, height: 2 }); engine.followPath(10, "aabacada"); expect(engine.board).toEqual([ [1, 2, 1, 2], @@ -590,7 +594,7 @@ describe("moves", () => { }); test("deleteCurMove", () => { - const engine = new GoEngine({ + const engine = new GobanEngine({ width: 4, height: 2, }); @@ -608,8 +612,8 @@ describe("moves", () => { }); describe("groups", () => { - test("toggleMetagroupRemoval", () => { - const engine = new GoEngine({ + test("toggleSingleGroupRemoval", () => { + const engine = new GobanEngine({ width: 4, height: 4, initial_state: { black: "aabbdd", white: "cacbcccd" }, @@ -625,24 +629,24 @@ describe("groups", () => { const on_removal_updated = jest.fn(); engine.addListener("stone-removal.updated", on_removal_updated); - engine.toggleMetaGroupRemoval(0, 0); + engine.toggleSingleGroupRemoval(0, 0); expect(on_removal_updated).toBeCalledTimes(1); expect(engine.removal).toEqual([ - [1, 0, 0, 0], - [0, 1, 0, 0], - [0, 0, 0, 0], - [0, 0, 0, 0], + [true, false, false, false], + [false, false, false, false], + [false, false, false, false], + [false, false, false, false], ]); - engine.toggleMetaGroupRemoval(0, 0); + engine.toggleSingleGroupRemoval(0, 0); - expect(engine.removal).toEqual(makeMatrix(4, 4)); + expect(engine.removal).toEqual(makeMatrix(4, 4, false)); }); - test("toggleMetagroupRemoval out-of-bounds", () => { - const engine = new GoEngine({ + test("toggleSingleGroupRemoval out-of-bounds", () => { + const engine = new GobanEngine({ width: 4, height: 4, initial_state: { black: "aabbdd", white: "cacbcccd" }, @@ -658,36 +662,31 @@ describe("groups", () => { const on_removal_updated = jest.fn(); engine.addListener("stone-removal.updated", on_removal_updated); - expect(engine.toggleMetaGroupRemoval(0, 4)).toEqual([[0, []]]); + expect(engine.toggleSingleGroupRemoval(0, 4)).toEqual({ removed: false, group: [] }); expect(on_removal_updated).toBeCalledTimes(0); }); - test("toggleMetagroupRemoval empty area", () => { - const engine = new GoEngine({ + test("toggleSingleGroupRemoval empty area doesn't do anything", () => { + const engine = new GobanEngine({ width: 4, height: 2, initial_state: { black: "aabb", white: "cacb" }, }); /* A B C D - * 4 x . o . - * 3 . x o . - * 2 . . o . - * 1 . . o x + * 2 x . o . + * 1 . x o . */ const on_removal_updated = jest.fn(); engine.addListener("stone-removal.updated", on_removal_updated); - expect(engine.toggleMetaGroupRemoval(0, 1)).toEqual([ - [1, [{ x: 0, y: 1 }]], - [0, []], - ]); - expect(on_removal_updated).toBeCalledTimes(1); + expect(engine.toggleSingleGroupRemoval(0, 1)).toEqual({ removed: false, group: [] }); + expect(on_removal_updated).toBeCalledTimes(0); }); test("clearRemoved", () => { - const engine = new GoEngine({ + const engine = new GobanEngine({ width: 4, height: 2, initial_state: { black: "aabb", white: "cacb" }, @@ -704,11 +703,11 @@ describe("groups", () => { engine.clearRemoved(); expect(on_removal_updated).toBeCalledTimes(1); - expect(engine.removal).toEqual(makeMatrix(4, 2)); + expect(engine.removal).toEqual(makeMatrix(4, 2, false)); }); test("clearRemoved", () => { - const engine = new GoEngine({ + const engine = new GobanEngine({ width: 4, height: 2, initial_state: { black: "aabb", white: "cacb" }, diff --git a/src/__tests__/GoEngine_sgf.test.ts b/test/unit_tests/GoEngine_sgf.test.ts similarity index 94% rename from src/__tests__/GoEngine_sgf.test.ts rename to test/unit_tests/GoEngine_sgf.test.ts index 90b90399..0d6f36a6 100644 --- a/src/__tests__/GoEngine_sgf.test.ts +++ b/test/unit_tests/GoEngine_sgf.test.ts @@ -1,18 +1,9 @@ -/** - * @jest-environment jsdom - */ - -// ^^ jsdom environment is because getLocation() returns window.location.pathname -// Same about CLIENT. -// -// TODO: move this into a setup-jest.ts file - // cspell: disable (global as any).CLIENT = true; -import { TestGoban } from "../TestGoban"; -import { MoveTree } from "../MoveTree"; +import { TestGoban } from "../../src/Goban/TestGoban"; +import { MoveTree } from "engine"; type SGFTestcase = { template: string; diff --git a/src/__tests__/GoMath.test.ts b/test/unit_tests/GoMath.test.ts similarity index 56% rename from src/__tests__/GoMath.test.ts rename to test/unit_tests/GoMath.test.ts index 976ec509..c8a2f4ba 100644 --- a/src/__tests__/GoMath.test.ts +++ b/test/unit_tests/GoMath.test.ts @@ -1,135 +1,147 @@ //cspell: disable -import { BoardState } from "../GoStoneGroup"; -import { GoStoneGroups } from "../GoStoneGroups"; -import { JGOFNumericPlayerColor } from "../JGOF"; -import * as GoMath from "../GoMath"; +import { + StoneStringBuilder, + JGOFNumericPlayerColor, + BoardState, + decodePrettyCoordinates, + encodeMove, + decodeGTPCoordinates, + decodeMoves, + encodeMovesToArray, + encodeMoveToArray, + makeEmptyMatrix, + makeMatrix, + makeObjectMatrix, + ojeSequenceToMoves, + prettyCoordinates, + sortMoves, +} from "engine"; describe("GoStoneGroups constructor", () => { test("basic board state", () => { - const THREExTHREE_board: Array> = [ + const THREExTHREE_board: JGOFNumericPlayerColor[][] = [ [1, 0, 2], [2, 1, 1], [2, 0, 1], ]; - const THREExTHREE_removal: Array> = [ - [0, 0, 0], - [0, 0, 0], - [0, 0, 0], + const THREExTHREE_removal: boolean[][] = [ + [false, false, false], + [false, false, false], + [false, false, false], ]; - const board_state: BoardState = { - width: 3, - height: 3, - board: THREExTHREE_board, - removal: THREExTHREE_removal, - }; - const board = new GoStoneGroups(board_state); + const stone_string_builder = new StoneStringBuilder( + new BoardState({ + board: THREExTHREE_board, + removal: THREExTHREE_removal, + }), + ); // TODO: examine usage in real code and flesh out expectations to reflect that usage - expect(board.groups.length).toBe(7); - expect(board.groups[0]).toBe(undefined); // what does this element represent? - expect(board.groups[1].points).toEqual([{ x: 0, y: 0 }]); - expect(board.groups[2].points).toEqual([{ x: 1, y: 0 }]); - expect(board.groups[3].points).toEqual([{ x: 2, y: 0 }]); - expect(board.groups[4].points).toEqual([ + expect(stone_string_builder.stone_strings.length).toBe(7); + expect(stone_string_builder.stone_strings[0]).toBe(undefined); // what does this element represent? + expect(stone_string_builder.stone_strings[1].intersections).toEqual([{ x: 0, y: 0 }]); + expect(stone_string_builder.stone_strings[2].intersections).toEqual([{ x: 1, y: 0 }]); + expect(stone_string_builder.stone_strings[3].intersections).toEqual([{ x: 2, y: 0 }]); + expect(stone_string_builder.stone_strings[4].intersections).toEqual([ { x: 0, y: 1 }, { x: 0, y: 2 }, ]); - expect(board.groups[5].points).toEqual([ + expect(stone_string_builder.stone_strings[5].intersections).toEqual([ { x: 1, y: 1 }, { x: 2, y: 1 }, { x: 2, y: 2 }, ]); - expect(board.groups[6].points).toEqual([{ x: 1, y: 2 }]); + expect(stone_string_builder.stone_strings[6].intersections).toEqual([{ x: 1, y: 2 }]); }); }); describe("matrices", () => { test("makeMatrix", () => { - expect(GoMath.makeMatrix(3, 2)).toEqual([ + expect(makeMatrix(3, 2, 0)).toEqual([ [0, 0, 0], [0, 0, 0], ]); - expect(GoMath.makeMatrix(3, 2, 1234)).toEqual([ + expect(makeMatrix(3, 2, 1234)).toEqual([ [1234, 1234, 1234], [1234, 1234, 1234], ]); - expect(GoMath.makeMatrix(0, 0)).toEqual([]); + expect(makeMatrix(0, 0, 0)).toEqual([]); }); - test("makeStringMatrix", () => { - expect(GoMath.makeStringMatrix(3, 2)).toEqual([ + test("makeMatrix", () => { + expect(makeMatrix(3, 2, "")).toEqual([ ["", "", ""], ["", "", ""], ]); - expect(GoMath.makeStringMatrix(3, 2, "asdf")).toEqual([ + expect(makeMatrix(3, 2, "asdf")).toEqual([ ["asdf", "asdf", "asdf"], ["asdf", "asdf", "asdf"], ]); - expect(GoMath.makeStringMatrix(0, 0)).toEqual([]); + expect(makeMatrix(0, 0, "")).toEqual([]); }); test("makeObjectMatrix", () => { - expect(GoMath.makeObjectMatrix(3, 2)).toEqual([ + expect(makeObjectMatrix(3, 2)).toEqual([ [{}, {}, {}], [{}, {}, {}], ]); - expect(GoMath.makeObjectMatrix(0, 0)).toEqual([]); + expect(makeObjectMatrix(0, 0)).toEqual([]); }); test("makeEmptyObjectMatrix", () => { - expect(GoMath.makeEmptyObjectMatrix(3, 2)).toEqual([ + expect(makeEmptyMatrix(3, 2)).toEqual([ [undefined, undefined, undefined], [undefined, undefined, undefined], ]); - expect(GoMath.makeEmptyObjectMatrix(0, 0)).toEqual([]); + expect(makeEmptyMatrix(0, 0)).toEqual([]); }); }); describe("prettyCoords", () => { test("pass", () => { - expect(GoMath.prettyCoords(-1, -1, 19)).toBe("pass"); + expect(prettyCoordinates(-1, -1, 19)).toBe("pass"); }); test("out of bounds", () => { // I doubt this is actually desired behavior. Feel free to remove this // test after verifying nothing depends on this behavior. - expect(GoMath.prettyCoords(25, 9, 19)).toBe("undefined10"); - expect(GoMath.prettyCoords(9, 25, 19)).toBe("K-6"); + expect(prettyCoordinates(25, 9, 19)).toBe("undefined10"); + expect(prettyCoordinates(9, 25, 19)).toBe("K-6"); }); test("regular moves", () => { - expect(GoMath.prettyCoords(0, 0, 19)).toBe("A19"); - expect(GoMath.prettyCoords(2, 15, 19)).toBe("C4"); - expect(GoMath.prettyCoords(9, 9, 19)).toBe("K10"); + expect(prettyCoordinates(0, 0, 19)).toBe("A19"); + expect(prettyCoordinates(2, 15, 19)).toBe("C4"); + expect(prettyCoordinates(9, 9, 19)).toBe("K10"); }); }); describe("decodeGTPCoordinate", () => { test("pass", () => { - expect(GoMath.decodeGTPCoordinate("pass", 19, 19)).toEqual({ x: -1, y: -1 }); - expect(GoMath.decodeGTPCoordinate("..", 19, 19)).toEqual({ x: -1, y: -1 }); + expect(decodeGTPCoordinates("pass", 19, 19)).toEqual({ x: -1, y: -1 }); + expect(decodeGTPCoordinates("..", 19, 19)).toEqual({ x: -1, y: -1 }); }); test("nonsense", () => { - expect(GoMath.decodeGTPCoordinate("&%", 19, 19)).toEqual({ x: -1, y: -1 }); + expect(decodeGTPCoordinates("&%", 19, 19)).toEqual({ x: -1, y: -1 }); }); test("regular moves (lowercase)", () => { - expect(GoMath.decodeGTPCoordinate("a1", 19, 19)).toEqual({ x: 0, y: 18 }); - expect(GoMath.decodeGTPCoordinate("c4", 19, 19)).toEqual({ x: 2, y: 15 }); - expect(GoMath.decodeGTPCoordinate("k10", 19, 19)).toEqual({ x: 9, y: 9 }); + expect(decodeGTPCoordinates("a1", 19, 19)).toEqual({ x: 0, y: 18 }); + expect(decodeGTPCoordinates("c4", 19, 19)).toEqual({ x: 2, y: 15 }); + expect(decodeGTPCoordinates("k10", 19, 19)).toEqual({ x: 9, y: 9 }); }); test("regular moves (lowercase)", () => { - expect(GoMath.decodeGTPCoordinate("A1", 19, 19)).toEqual({ x: 0, y: 18 }); - expect(GoMath.decodeGTPCoordinate("C4", 19, 19)).toEqual({ x: 2, y: 15 }); - expect(GoMath.decodeGTPCoordinate("K10", 19, 19)).toEqual({ x: 9, y: 9 }); + expect(decodeGTPCoordinates("A1", 19, 19)).toEqual({ x: 0, y: 18 }); + expect(decodeGTPCoordinates("C4", 19, 19)).toEqual({ x: 2, y: 15 }); + expect(decodeGTPCoordinates("K10", 19, 19)).toEqual({ x: 9, y: 9 }); }); }); describe("decodeMoves", () => { test("decodes string", () => { - expect(GoMath.decodeMoves("aabbcc", 19, 19)).toEqual([ + expect(decodeMoves("aabbcc", 19, 19)).toEqual([ { x: 0, y: 0, color: 0, edited: false }, { x: 1, y: 1, color: 0, edited: false }, { x: 2, y: 2, color: 0, edited: false }, @@ -137,76 +149,74 @@ describe("decodeMoves", () => { }); test("decodes string with passes", () => { - expect(GoMath.decodeMoves("aa..", 19, 19)).toEqual([ + expect(decodeMoves("aa..", 19, 19)).toEqual([ { x: 0, y: 0, color: 0, edited: false }, { x: -1, y: -1, color: 0, edited: false }, ]); }); test("converts JGOFMove to Array", () => { - expect(GoMath.decodeMoves({ x: 2, y: 2 }, 19, 19)).toEqual([{ x: 2, y: 2 }]); + expect(decodeMoves({ x: 2, y: 2 }, 19, 19)).toEqual([{ x: 2, y: 2 }]); }); test("throws on random object", () => { expect(() => { - GoMath.decodeMoves(new Object() as any, 19, 19); + decodeMoves(new Object() as any, 19, 19); }).toThrow("Invalid move format: {}"); }); test("x greater than width returns pass", () => { - expect(GoMath.decodeMoves("da", 3, 3)).toEqual([{ x: -1, y: -1, color: 0, edited: false }]); + expect(decodeMoves("da", 3, 3)).toEqual([{ x: -1, y: -1, color: 0, edited: false }]); }); test("y greater than height returns pass", () => { - expect(GoMath.decodeMoves("ad", 3, 3)).toEqual([{ x: -1, y: -1, color: 0, edited: false }]); + expect(decodeMoves("ad", 3, 3)).toEqual([{ x: -1, y: -1, color: 0, edited: false }]); }); test("bad data", () => { // not really sure when this happens, but there's code to handle it - expect(GoMath.decodeMoves("!undefined", 19, 19)).toEqual([ + expect(decodeMoves("!undefined", 19, 19)).toEqual([ { x: -1, y: -1, color: 0, edited: true }, ]); }); test("pretty coordinates", () => { - expect(GoMath.decodeMoves("K10", 19, 19)).toEqual([ - { x: 9, y: 9, color: 0, edited: false }, - ]); + expect(decodeMoves("K10", 19, 19)).toEqual([{ x: 9, y: 9, color: 0, edited: false }]); }); test("throws on unparsed input", () => { expect(() => { - GoMath.decodeMoves("K10z", 19, 19); + decodeMoves("K10z", 19, 19); }).toThrow("Unparsed move input: z"); }); test("pretty x greater than width returns pass", () => { - expect(GoMath.decodeMoves("D1", 3, 3)).toEqual([{ x: -1, y: -1, color: 0, edited: false }]); + expect(decodeMoves("D1", 3, 3)).toEqual([{ x: -1, y: -1, color: 0, edited: false }]); }); test("pretty y greater than height returns pass", () => { - expect(GoMath.decodeMoves("A4", 3, 3)).toEqual([{ x: -1, y: -1, color: 0, edited: false }]); + expect(decodeMoves("A4", 3, 3)).toEqual([{ x: -1, y: -1, color: 0, edited: false }]); }); test("throws without height and width", () => { // Actually this ts is meant to cover the undefined case.. expect(() => { - GoMath.decodeMoves("aabbcc", 0, 0); + decodeMoves("aabbcc", 0, 0); }).toThrow( "decodeMoves requires a height and width to be set when decoding a string coordinate", ); }); test("single packed move", () => { - expect(GoMath.decodeMoves([1, 2, 2048], 3, 3)).toEqual([ + expect(decodeMoves([1, 2, 2048], 3, 3)).toEqual([ { x: 1, y: 2, color: 0, timedelta: 2048 }, ]); }); test("Array", () => { expect( - GoMath.decodeMoves( + decodeMoves( [ { x: 4, y: 4, color: 1 }, { x: 3, y: 3, color: 2 }, @@ -222,7 +232,7 @@ describe("decodeMoves", () => { test("Array", () => { expect( - GoMath.decodeMoves( + decodeMoves( [ [1, 2, 2048, 2, { blur: 1234 }], [3, 4, 2048, 1], @@ -238,90 +248,88 @@ describe("decodeMoves", () => { test("throws without height and width", () => { expect(() => { - GoMath.decodeMoves(["asdf" as any, [3, 4, 2048]], 19, 19); + decodeMoves(["asdf" as any, [3, 4, 2048]], 19, 19); }).toThrow("Unrecognized move format: asdf"); }); test("empty array", () => { - expect(GoMath.decodeMoves([], 19, 19)).toEqual([]); + expect(decodeMoves([], 19, 19)).toEqual([]); }); }); describe("encodeMove", () => { test("corner", () => { - expect(GoMath.encodeMove(0, 0)).toBe("aa"); + expect(encodeMove(0, 0)).toBe("aa"); }); test("tengen", () => { - expect(GoMath.encodeMove(9, 9)).toBe("jj"); + expect(encodeMove(9, 9)).toBe("jj"); }); test("a19", () => { - expect(GoMath.encodeMove(0, 18)).toBe("as"); + expect(encodeMove(0, 18)).toBe("as"); }); test("t1", () => { - expect(GoMath.encodeMove(18, 0)).toBe("sa"); + expect(encodeMove(18, 0)).toBe("sa"); }); test("Move type", () => { - expect(GoMath.encodeMove({ x: 3, y: 3 })).toBe("dd"); + expect(encodeMove({ x: 3, y: 3 })).toBe("dd"); }); test("throws if x is a number but y is missing", () => { expect(() => { - GoMath.encodeMove(3); + encodeMove(3); }).toThrow("Invalid y parameter to encodeMove y = undefined"); }); }); -describe("encodePrettyCoord", () => { +describe("decodePrettyCoord", () => { test("tengen", () => { - expect(GoMath.encodePrettyCoord("k10", 19)).toBe("jj"); + expect(encodeMove(decodePrettyCoordinates("k10", 19))).toBe("jj"); }); test("a1", () => { - expect(GoMath.encodePrettyCoord("a1", 3)).toBe("ac"); + expect(encodeMove(decodePrettyCoordinates("a1", 3))).toBe("ac"); }); test("capital", () => { - expect(GoMath.encodePrettyCoord("A1", 3)).toBe("ac"); + expect(encodeMove(decodePrettyCoordinates("A1", 3))).toBe("ac"); }); test("far corner", () => { - expect(GoMath.encodePrettyCoord("c3", 3)).toBe("ca"); + expect(encodeMove(decodePrettyCoordinates("c3", 3))).toBe("ca"); }); test("pass", () => { // Is this really the pretty representation of pass? - expect(GoMath.encodePrettyCoord(".4", 3)).toBe(".."); + expect(encodeMove(decodePrettyCoordinates(".4", 3))).toBe(".."); }); }); describe("encodeMoveToArray", () => { test("x, y, timedelta", () => { - expect(GoMath.encodeMoveToArray({ x: 4, y: 5, timedelta: 678 })).toEqual([4, 5, 678]); + expect(encodeMoveToArray({ x: 4, y: 5, timedelta: 678 })).toEqual([4, 5, 678]); }); test("timedelta defaults to -1", () => { - expect(GoMath.encodeMoveToArray({ x: 1, y: 1 })).toEqual([1, 1, -1]); + expect(encodeMoveToArray({ x: 1, y: 1 })).toEqual([1, 1, -1]); }); test("if !edited color gets stripped", () => { - expect(GoMath.encodeMoveToArray({ x: 1, y: 1, timedelta: 1000, color: 2 })).toEqual([ - 1, 1, 1000, - ]); + expect(encodeMoveToArray({ x: 1, y: 1, timedelta: 1000, color: 2 })).toEqual([1, 1, 1000]); }); test("if edited color is the 4th element", () => { - expect( - GoMath.encodeMoveToArray({ x: 1, y: 1, timedelta: 1000, color: 2, edited: true }), - ).toEqual([1, 1, 1000, 2]); + expect(encodeMoveToArray({ x: 1, y: 1, timedelta: 1000, color: 2, edited: true })).toEqual([ + 1, 1, 1000, 2, + ]); }); test("extra fields are saved", () => { expect( - GoMath.encodeMoveToArray({ + encodeMoveToArray({ x: 1, y: 1, timedelta: 1000, @@ -353,7 +361,7 @@ describe("encodeMoveToArray", () => { test("encodeMovesToArray", () => { expect( - GoMath.encodeMovesToArray([ + encodeMovesToArray([ { x: 4, y: 4, timedelta: 2048 }, { x: 3, y: 3, timedelta: 1024 }, ]), @@ -363,38 +371,15 @@ test("encodeMovesToArray", () => { ]); }); -describe("stripModeratorOnlyExtraInformation", () => { - test("does not strip x, y, timedelta", () => { - expect(GoMath.stripModeratorOnlyExtraInformation([1, 2, 3])).toEqual([1, 2, 3]); - }); - - test("trims blur", () => { - expect(GoMath.stripModeratorOnlyExtraInformation([1, 2, 3, 1, { blur: 1 }])).toEqual([ - 1, 2, 3, 1, - ]); - }); - - test("trims sgf_downloaded_by", () => { - expect( - GoMath.stripModeratorOnlyExtraInformation([1, 2, 3, 1, { sgf_downloaded_by: 1234 }]), - ).toEqual([1, 2, 3, 1]); - }); - - test("doesn't trim non-mod info in extra", () => { - expect( - GoMath.stripModeratorOnlyExtraInformation([1, 2, 3, 1, { misc_extra: "asdf" }]), - ).toEqual([1, 2, 3, 1, { misc_extra: "asdf" }]); - }); -}); - +/* describe("trimJGOFMoves", () => { test("empty", () => { - expect(GoMath.trimJGOFMoves([])).toEqual([]); + expect(trimJGOFMoves([])).toEqual([]); }); test("does not trim edited=true, color=1 etc.", () => { expect( - GoMath.trimJGOFMoves([ + trimJGOFMoves([ { x: 1, y: 1, @@ -416,7 +401,7 @@ describe("trimJGOFMoves", () => { test("trims played_by", () => { expect( - GoMath.trimJGOFMoves([ + trimJGOFMoves([ { x: 1, y: 1, @@ -433,7 +418,7 @@ describe("trimJGOFMoves", () => { test("trims edited=false", () => { expect( - GoMath.trimJGOFMoves([ + trimJGOFMoves([ { x: 1, y: 1, @@ -450,7 +435,7 @@ describe("trimJGOFMoves", () => { test("trims color=0", () => { expect( - GoMath.trimJGOFMoves([ + trimJGOFMoves([ { x: 1, y: 1, @@ -473,7 +458,7 @@ describe("trimJGOFMoves", () => { edited: false, }, ]; - GoMath.trimJGOFMoves(arr); + trimJGOFMoves(arr); expect(arr).toEqual([ { x: 1, @@ -483,41 +468,42 @@ describe("trimJGOFMoves", () => { ]); }); }); +*/ describe("sortMoves", () => { test("sorted array", () => { - expect(GoMath.sortMoves("aabbcc", 3, 3)).toBe("aabbcc"); + expect(sortMoves("aabbcc", 3, 3)).toBe("aabbcc"); }); test("reversed array", () => { - expect(GoMath.sortMoves("ccbbaa", 3, 3)).toBe("aabbcc"); + expect(sortMoves("ccbbaa", 3, 3)).toBe("aabbcc"); }); test("y takes precedence", () => { - expect(GoMath.sortMoves("abba", 3, 3)).toBe("baab"); + expect(sortMoves("abba", 3, 3)).toBe("baab"); }); test("empty array", () => { - expect(GoMath.sortMoves("", 2, 2)).toBe(""); + expect(sortMoves("", 2, 2)).toBe(""); }); test("out of bounds", () => { - expect(GoMath.sortMoves("cc", 2, 2)).toBe(".."); + expect(sortMoves("cc", 2, 2)).toBe(".."); }); test("edited moves pushed to the end", () => { - expect(GoMath.sortMoves("!1aabb!2ccdd", 4, 4)).toBe("bbdd!1aa!2cc"); + expect(sortMoves("!1aabb!2ccdd", 4, 4)).toBe("bbdd!1aa!2cc"); }); test("repeat elements", () => { - expect(GoMath.sortMoves("aaaaaa", 2, 2)).toBe("aaaaaa"); + expect(sortMoves("aaaaaa", 2, 2)).toBe("aaaaaa"); }); }); describe("ojeSequenceToMoves", () => { test("bad sequence", () => { expect(() => { - GoMath.ojeSequenceToMoves("nonsense"); + ojeSequenceToMoves("nonsense"); }).toThrow("root"); }); @@ -533,6 +519,6 @@ describe("ojeSequenceToMoves", () => { ], ], ])("id of %s", (sequence, id) => { - expect(GoMath.ojeSequenceToMoves(sequence)).toStrictEqual(id); + expect(ojeSequenceToMoves(sequence)).toStrictEqual(id); }); }); diff --git a/src/__tests__/GoMath_positionId.test.ts b/test/unit_tests/GoMath_positionId.test.ts similarity index 95% rename from src/__tests__/GoMath_positionId.test.ts rename to test/unit_tests/GoMath_positionId.test.ts index 4c484f69..da71be0c 100644 --- a/src/__tests__/GoMath_positionId.test.ts +++ b/test/unit_tests/GoMath_positionId.test.ts @@ -1,7 +1,6 @@ // cspell: disable -import * as GoMath from "../GoMath"; -import { JGOFNumericPlayerColor } from "../JGOF"; +import { JGOFNumericPlayerColor, positionId } from "engine"; type Testcase = { height: number; @@ -145,5 +144,5 @@ const TEST_BOARDS: Array = [ ]; test.each(TEST_BOARDS)("Position IDs", ({ board, height, width, id }) => { - expect(GoMath.positionId(board, height, width)).toEqual(id); + expect(positionId(board, height, width)).toEqual(id); }); diff --git a/src/__tests__/GobanCanvas.test.ts b/test/unit_tests/GobanCanvas.test.ts similarity index 92% rename from src/__tests__/GobanCanvas.test.ts rename to test/unit_tests/GobanCanvas.test.ts index 3596c5c9..40d694cd 100644 --- a/src/__tests__/GobanCanvas.test.ts +++ b/test/unit_tests/GobanCanvas.test.ts @@ -1,15 +1,14 @@ -/** - * @jest-environment jsdom - */ - // cspell: disable (global as any).CLIENT = true; -import { GobanCanvas, GobanCanvasConfig } from "../GobanCanvas"; -import { GobanCore, SCORE_ESTIMATION_TOLERANCE, SCORE_ESTIMATION_TRIALS } from "../GobanCore"; -import { GobanSocket } from "../GobanSocket"; -import * as GoMath from "../GoMath"; +import { GobanCanvas, CanvasRendererGobanConfig } from "../../src/Goban/CanvasRenderer"; +import { + SCORE_ESTIMATION_TOLERANCE, + SCORE_ESTIMATION_TRIALS, +} from "../../src/Goban/InteractiveBase"; +import { GobanSocket, makeMatrix } from "engine"; +import { GobanBase } from "../../src/GobanBase"; import WS from "jest-websocket-mock"; let board_div: HTMLDivElement; @@ -38,11 +37,11 @@ function simulateMouseClick(canvas: HTMLCanvasElement, { x, y }: { x: number; y: canvas.dispatchEvent(new MouseEvent("click", eventInitDict)); } -function commonConfig(): GobanCanvasConfig { +function commonConfig(): CanvasRendererGobanConfig { return { square_size: 10, board_div: board_div, interactive: true, server_socket: mock_socket }; } -function basic3x3Config(additionalOptions?: GobanCanvasConfig): GobanCanvasConfig { +function basic3x3Config(additionalOptions?: CanvasRendererGobanConfig): CanvasRendererGobanConfig { return { ...commonConfig(), width: 3, @@ -51,7 +50,9 @@ function basic3x3Config(additionalOptions?: GobanCanvasConfig): GobanCanvasConfi }; } -function basicScorableBoardConfig(additionalOptions?: GobanCanvasConfig): GobanCanvasConfig { +function basicScorableBoardConfig( + additionalOptions?: CanvasRendererGobanConfig, +): CanvasRendererGobanConfig { return { ...commonConfig(), width: 4, @@ -312,20 +313,20 @@ describe("onTap", () => { [0, 1, 2, 0], ]); - simulateMouseClick(canvas, { x: 0, y: 0 }); + simulateMouseClick(canvas, { x: 1, y: 0 }); await expect(socket_server).toReceiveMessage( expect.arrayContaining([ "game/removed_stones/set", expect.objectContaining({ removed: true, - stones: "aaab", + stones: "babb", }), ]), ); }); - test("Shift-Clicking during stone removal toggles one stone", async () => { + test("Shift-Clicking during stone removal toggles the group", async () => { const goban = new GobanCanvas(basicScorableBoardConfig({ phase: "stone removal" })); const canvas = document.getElementById("board-canvas") as HTMLCanvasElement; @@ -338,7 +339,7 @@ describe("onTap", () => { canvas.dispatchEvent( new MouseEvent("click", { - clientX: 15, + clientX: 15 + TEST_SQUARE_SIZE, clientY: 15, shiftKey: true, }), @@ -349,7 +350,7 @@ describe("onTap", () => { "game/removed_stones/set", expect.objectContaining({ removed: true, - stones: "aa", + stones: "babb", }), ]), ); @@ -366,7 +367,7 @@ describe("onTap", () => { const canvas = document.getElementById("board-canvas") as HTMLCanvasElement; const addCoordinatesToChatInput = jest.fn(); - GobanCore.setHooks({ addCoordinatesToChatInput }); + GobanBase.setCallbacks({ addCoordinatesToChatInput }); canvas.dispatchEvent( new MouseEvent("click", { @@ -400,7 +401,7 @@ describe("onTap", () => { "game/removed_stones/set", expect.objectContaining({ removed: true, - stones: "babbbabbbbba", + stones: "babb", }), ]), ); @@ -416,10 +417,10 @@ describe("onTap", () => { const mock_score_estimate = { handleClick: jest.fn(), when_ready: Promise.resolve(), - board: GoMath.makeMatrix(4, 2), - removal: GoMath.makeMatrix(4, 2), - territory: GoMath.makeMatrix(4, 2), - ownership: GoMath.makeMatrix(4, 2), + board: makeMatrix(4, 2, 0), + removal: makeMatrix(4, 2, false), + territory: makeMatrix(4, 2, 0), + ownership: makeMatrix(4, 2, 0), }; goban.engine.estimateScore = jest.fn().mockReturnValue(mock_score_estimate); @@ -430,6 +431,7 @@ describe("onTap", () => { SCORE_ESTIMATION_TRIALS, SCORE_ESTIMATION_TOLERANCE, false, + false, ); (goban.engine.estimateScore as jest.Mock).mockClear(); diff --git a/src/__tests__/GobanCore_conditional_moves.test.ts b/test/unit_tests/GobanCore_conditional_moves.test.ts similarity index 90% rename from src/__tests__/GobanCore_conditional_moves.test.ts rename to test/unit_tests/GobanCore_conditional_moves.test.ts index d76a2826..e3260725 100644 --- a/src/__tests__/GobanCore_conditional_moves.test.ts +++ b/test/unit_tests/GobanCore_conditional_moves.test.ts @@ -1,17 +1,8 @@ -/** - * @jest-environment jsdom - */ - -// ^^ jsdom environment is because getLocation() returns window.location.pathname -// Same about CLIENT. -// -// TODO: move this into a setup-jest.ts file - // cspell: disable (global as any).CLIENT = true; -import { TestGoban } from "../TestGoban"; +import { TestGoban } from "../../src/Goban/TestGoban"; test("call FollowConditionalPath", () => { const goban = new TestGoban({ moves: [] }); diff --git a/src/__tests__/GobanSVG.test.ts b/test/unit_tests/GobanSVG.test.ts similarity index 85% rename from src/__tests__/GobanSVG.test.ts rename to test/unit_tests/GobanSVG.test.ts index b9e39201..74301e2c 100644 --- a/src/__tests__/GobanSVG.test.ts +++ b/test/unit_tests/GobanSVG.test.ts @@ -1,15 +1,14 @@ -/** - * @jest-environment jsdom - */ - // cspell: disable (global as any).CLIENT = true; -import { GobanSVG, GobanSVGConfig } from "../GobanSVG"; -import { GobanCore, SCORE_ESTIMATION_TOLERANCE, SCORE_ESTIMATION_TRIALS } from "../GobanCore"; -import { GobanSocket } from "../GobanSocket"; -import * as GoMath from "../GoMath"; +import { SVGRenderer, SVGRendererGobanConfig } from "../../src/Goban/SVGRenderer"; +import { + SCORE_ESTIMATION_TOLERANCE, + SCORE_ESTIMATION_TRIALS, +} from "../../src/Goban/InteractiveBase"; +import { GobanSocket, makeMatrix } from "engine"; +import { GobanBase } from "../../src/GobanBase"; import WS from "jest-websocket-mock"; let board_div: HTMLDivElement; @@ -38,11 +37,11 @@ function simulateMouseClick(div: HTMLElement, { x, y }: { x: number; y: number } div.dispatchEvent(new MouseEvent("click", eventInitDict)); } -function commonConfig(): GobanSVGConfig { +function commonConfig(): SVGRendererGobanConfig { return { square_size: 10, board_div: board_div, interactive: true, server_socket: mock_socket }; } -function basic3x3Config(additionalOptions?: GobanSVGConfig): GobanSVGConfig { +function basic3x3Config(additionalOptions?: SVGRendererGobanConfig): SVGRendererGobanConfig { return { ...commonConfig(), width: 3, @@ -51,7 +50,9 @@ function basic3x3Config(additionalOptions?: GobanSVGConfig): GobanSVGConfig { }; } -function basicScorableBoardConfig(additionalOptions?: GobanSVGConfig): GobanSVGConfig { +function basicScorableBoardConfig( + additionalOptions?: SVGRendererGobanConfig, +): SVGRendererGobanConfig { return { ...commonConfig(), width: 4, @@ -100,7 +101,7 @@ describe("onTap", () => { }); test("clicking without enabling stone placement has no effect", () => { - const goban = new GobanSVG(basic3x3Config()); + const goban = new SVGRenderer(basic3x3Config()); const event_layer = goban.event_layer; simulateMouseClick(event_layer, { x: 0, y: 0 }); @@ -113,7 +114,7 @@ describe("onTap", () => { }); test("clicking the top left intersection places a stone", () => { - const goban = new GobanSVG(basic3x3Config()); + const goban = new SVGRenderer(basic3x3Config()); const event_layer = goban.event_layer; goban.enableStonePlacement(); @@ -127,7 +128,7 @@ describe("onTap", () => { }); test("clicking the midpoint of two intersections has no effect", () => { - const goban = new GobanSVG(basic3x3Config()); + const goban = new SVGRenderer(basic3x3Config()); const event_layer = goban.event_layer; goban.enableStonePlacement(); @@ -141,7 +142,7 @@ describe("onTap", () => { }); test("shift clicking in analyze mode jumps to move", () => { - const goban = new GobanSVG( + const goban = new SVGRenderer( basic3x3Config({ moves: [ [0, 0], @@ -177,7 +178,7 @@ describe("onTap", () => { }); test("Clicking with the triangle subtool places a triangle", () => { - const goban = new GobanSVG(basic3x3Config({ mode: "analyze" })); + const goban = new SVGRenderer(basic3x3Config({ mode: "analyze" })); const event_layer = goban.event_layer; goban.enableStonePlacement(); @@ -193,7 +194,7 @@ describe("onTap", () => { }); test("Clicking submits a move in one-click-submit mode", async () => { - const goban = new GobanSVG(basic3x3Config({ one_click_submit: true })); + const goban = new SVGRenderer(basic3x3Config({ one_click_submit: true })); const event_layer = goban.event_layer; goban.enableStonePlacement(); @@ -213,7 +214,7 @@ describe("onTap", () => { test("Calling the submit_move() too quickly results in no submission", async () => { jest.useFakeTimers(); jest.setSystemTime(0); - const goban = new GobanSVG(basic3x3Config()); + const goban = new SVGRenderer(basic3x3Config()); const event_layer = goban.event_layer; const log_spy = jest.spyOn(console, "info").mockImplementation(() => {}); @@ -249,7 +250,7 @@ describe("onTap", () => { jest.useFakeTimers(); jest.setSystemTime(0); - const goban = new GobanSVG(basic3x3Config({ server_socket: mock_socket })); + const goban = new SVGRenderer(basic3x3Config({ server_socket: mock_socket })); const event_layer = goban.event_layer; await socket_server.connected; @@ -282,7 +283,7 @@ describe("onTap", () => { }, 500); test("Right clicking in play mode should have no effect.", () => { - const goban = new GobanSVG(basic3x3Config()); + const goban = new SVGRenderer(basic3x3Config()); const event_layer = goban.event_layer; goban.enableStonePlacement(); @@ -302,7 +303,7 @@ describe("onTap", () => { }); test("Clicking during stone removal sends remove stones message", async () => { - const goban = new GobanSVG(basicScorableBoardConfig({ phase: "stone removal" })); + const goban = new SVGRenderer(basicScorableBoardConfig({ phase: "stone removal" })); const event_layer = goban.event_layer; // Just some checks that our setup is correct @@ -312,21 +313,21 @@ describe("onTap", () => { [0, 1, 2, 0], ]); - simulateMouseClick(event_layer, { x: 0, y: 0 }); + simulateMouseClick(event_layer, { x: 1, y: 0 }); await expect(socket_server).toReceiveMessage( expect.arrayContaining([ "game/removed_stones/set", expect.objectContaining({ removed: true, - stones: "aaab", + stones: "babb", }), ]), ); }); - test("Shift-Clicking during stone removal toggles one stone", async () => { - const goban = new GobanSVG(basicScorableBoardConfig({ phase: "stone removal" })); + test("Shift-Clicking during stone removal toggles the group ", async () => { + const goban = new SVGRenderer(basicScorableBoardConfig({ phase: "stone removal" })); const event_layer = goban.event_layer; // Just some checks that our setup is correct @@ -338,7 +339,7 @@ describe("onTap", () => { event_layer.dispatchEvent( new MouseEvent("click", { - clientX: 15, + clientX: 15 + TEST_SQUARE_SIZE, clientY: 15, shiftKey: true, }), @@ -349,7 +350,7 @@ describe("onTap", () => { "game/removed_stones/set", expect.objectContaining({ removed: true, - stones: "aa", + stones: "babb", }), ]), ); @@ -361,11 +362,11 @@ describe("onTap", () => { test("Ctrl-Clicking during stone removal adds coordinates to chat", async () => { jest.useFakeTimers(); jest.setSystemTime(0); - const goban = new GobanSVG(basicScorableBoardConfig({ phase: "stone removal" })); + const goban = new SVGRenderer(basicScorableBoardConfig({ phase: "stone removal" })); const event_layer = goban.event_layer; const addCoordinatesToChatInput = jest.fn(); - GobanCore.setHooks({ addCoordinatesToChatInput }); + GobanBase.setCallbacks({ addCoordinatesToChatInput }); event_layer.dispatchEvent( new MouseEvent("click", { @@ -385,7 +386,7 @@ describe("onTap", () => { }); test("Clicking on stones during stone removal sends a socket message", async () => { - const goban = new GobanSVG(basicScorableBoardConfig({ phase: "stone removal" })); + const goban = new SVGRenderer(basicScorableBoardConfig({ phase: "stone removal" })); const event_layer = goban.event_layer; simulateMouseClick(event_layer, { x: 1, y: 0 }); @@ -399,14 +400,14 @@ describe("onTap", () => { "game/removed_stones/set", expect.objectContaining({ removed: true, - stones: "babbbabbbbba", + stones: "babb", }), ]), ); }); test("Clicking while in scoring mode triggers score_estimate.handleClick()", () => { - const goban = new GobanSVG(basicScorableBoardConfig()); + const goban = new SVGRenderer(basicScorableBoardConfig()); const event_layer = goban.event_layer; // The scoring API is a real pain to work with, mainly due to dependence @@ -415,10 +416,10 @@ describe("onTap", () => { const mock_score_estimate = { handleClick: jest.fn(), when_ready: Promise.resolve(), - board: GoMath.makeMatrix(4, 2), - removal: GoMath.makeMatrix(4, 2), - territory: GoMath.makeMatrix(4, 2), - ownership: GoMath.makeMatrix(4, 2), + board: makeMatrix(4, 2, 0), + removal: makeMatrix(4, 2, 0), + territory: makeMatrix(4, 2, 0), + ownership: makeMatrix(4, 2, 0), }; goban.engine.estimateScore = jest.fn().mockReturnValue(mock_score_estimate); @@ -429,6 +430,7 @@ describe("onTap", () => { SCORE_ESTIMATION_TRIALS, SCORE_ESTIMATION_TOLERANCE, false, + false, ); (goban.engine.estimateScore as jest.Mock).mockClear(); @@ -440,7 +442,7 @@ describe("onTap", () => { }); test("puzzle mode", () => { - const goban = new GobanSVG( + const goban = new SVGRenderer( basic3x3Config({ mode: "puzzle", getPuzzlePlacementSetting: () => ({ mode: "setup", color: 1 }), diff --git a/src/__tests__/GobanSocket.test.ts b/test/unit_tests/GobanSocket.test.ts similarity index 97% rename from src/__tests__/GobanSocket.test.ts rename to test/unit_tests/GobanSocket.test.ts index bca6fa24..50bb1914 100644 --- a/src/__tests__/GobanSocket.test.ts +++ b/test/unit_tests/GobanSocket.test.ts @@ -1,12 +1,8 @@ -/** - * @jest-environment jsdom - */ - (global as any).CLIENT = true; -import { GobanSocket, closeErrorCodeToString } from "../GobanSocket"; +import { GobanSocket, closeErrorCodeToString } from "engine"; import WS from "jest-websocket-mock"; -import * as protocol from "../protocol"; +import * as protocol from "engine/protocol"; let last_port = 48880; diff --git a/src/__tests__/ScoreEstimator.test.ts b/test/unit_tests/ScoreEstimator.test.ts similarity index 66% rename from src/__tests__/ScoreEstimator.test.ts rename to test/unit_tests/ScoreEstimator.test.ts index 9edccd3a..dfa8f169 100644 --- a/src/__tests__/ScoreEstimator.test.ts +++ b/test/unit_tests/ScoreEstimator.test.ts @@ -1,14 +1,12 @@ //cspell: disable -import { GoEngine } from "../GoEngine"; -import { makeMatrix } from "../GoMath"; +import { GobanEngine } from "engine"; +import { makeMatrix } from "engine"; +import { ScoreEstimator, adjust_estimate, set_local_ownership_estimator } from "engine"; import { - ScoreEstimator, - adjust_estimate, - set_local_scorer, - set_remote_scorer, -} from "../ScoreEstimator"; -import { estimateScoreVoronoi } from "../local_estimators/voronoi"; + init_remote_ownership_estimator, + voronoi_estimate_ownership, +} from "engine/ownership_estimators"; describe("adjust_estimate", () => { const BOARD = [ @@ -27,7 +25,7 @@ describe("adjust_estimate", () => { const KOMI = 0.5; test("adjust_estimate area", () => { - const engine = new GoEngine({ komi: KOMI, rules: "chinese" }); + const engine = new GobanEngine({ komi: KOMI, rules: "chinese" }); expect(adjust_estimate(engine, BOARD, OWNERSHIP, SCORE)).toEqual({ score: -0.5, ownership: OWNERSHIP, @@ -41,7 +39,7 @@ describe("adjust_estimate", () => { [1, 0, 0, -1], [1, 0, 0, -1], ]; - const engine = new GoEngine({ komi: KOMI, rules: "japanese" }); + const engine = new GobanEngine({ komi: KOMI, rules: "japanese" }); expect(adjust_estimate(engine, BOARD, OWNERSHIP, SCORE)).toEqual({ score: -0.5, ownership: ADJUSTED_OWNERSHIP, @@ -55,7 +53,7 @@ describe("ScoreEstimator", () => { [1, 1, -1, -1], ]; const KOMI = 0.5; - const engine = new GoEngine({ komi: KOMI, width: 4, height: 2 }); + const engine = new GobanEngine({ komi: KOMI, width: 4, height: 2 }); engine.place(1, 0); engine.place(2, 0); engine.place(1, 1); @@ -65,19 +63,25 @@ describe("ScoreEstimator", () => { const tolerance = 0.25; beforeEach(() => { - set_remote_scorer(async () => { - return { ownership: OWNERSHIP, score: -7.5 }; + init_remote_ownership_estimator(async () => { + return { + ownership: OWNERSHIP, + score: -7.5, + autoscored_board_state: OWNERSHIP, + autoscored_removed: [], + autoscored_needs_sealing: [], + }; }); - set_local_scorer(estimateScoreVoronoi); + set_local_ownership_estimator(voronoi_estimate_ownership); }); afterEach(() => { - set_remote_scorer(undefined as any); + init_remote_ownership_estimator(undefined as any); }); test("amount and winner", async () => { - const se = new ScoreEstimator(undefined, engine, trials, tolerance, false); + const se = new ScoreEstimator(engine, undefined, trials, tolerance, false); await se.when_ready; @@ -88,7 +92,7 @@ describe("ScoreEstimator", () => { }); test("local", async () => { - const se = new ScoreEstimator(undefined, engine, 10, 0.5, false); + const se = new ScoreEstimator(engine, undefined, 10, 0.5, false); await se.when_ready; @@ -111,11 +115,11 @@ describe("ScoreEstimator", () => { [4, 1], [3, 1], ]; - const engine = new GoEngine({ komi: KOMI, width: 9, height: 9, rules: "chinese" }); + const engine = new GobanEngine({ komi: KOMI, width: 9, height: 9, rules: "chinese" }); for (const [x, y] of moves) { engine.place(x, y); } - const se = new ScoreEstimator(undefined, engine, 10, 0.5, false); + const se = new ScoreEstimator(engine, undefined, 10, 0.5, false); expect(se.ownership).toEqual([ [1, 0, -1, -1, 1, 1, 1, 1, 1], @@ -130,17 +134,18 @@ describe("ScoreEstimator", () => { ]); }); - test("score()", async () => { - const se = new ScoreEstimator(undefined, engine, 10, 0.5, false); + test("score() territory", async () => { + const se = new ScoreEstimator(engine, undefined, 10, 0.5, false); await se.when_ready; se.score(); + // no score because all territory is in seki expect(se.white).toEqual({ handicap: 0, komi: 0.5, prisoners: 0, - scoring_positions: "dadb", + scoring_positions: "", stones: 0, territory: 0, total: 0.5, @@ -149,7 +154,7 @@ describe("ScoreEstimator", () => { handicap: 0, komi: 0, prisoners: 0, - scoring_positions: "aaab", + scoring_positions: "", stones: 0, territory: 0, total: 0, @@ -157,13 +162,13 @@ describe("ScoreEstimator", () => { }); test("score() chinese", async () => { - const engine = new GoEngine({ komi: KOMI, width: 4, height: 2, rules: "chinese" }); + const engine = new GobanEngine({ komi: KOMI, width: 4, height: 2, rules: "chinese" }); engine.place(1, 0); engine.place(2, 0); engine.place(1, 1); engine.place(2, 1); - const se = new ScoreEstimator(undefined, engine, 10, 0.5, false); + const se = new ScoreEstimator(engine, undefined, 10, 0.5, false); await se.when_ready; se.score(); @@ -172,19 +177,19 @@ describe("ScoreEstimator", () => { handicap: 0, komi: 0.5, prisoners: 0, - scoring_positions: "dadbcacb", + scoring_positions: "cadacbdb", stones: 2, - territory: 0, - total: 2.5, + territory: 2, + total: 4.5, }); expect(se.black).toEqual({ handicap: 0, komi: 0, prisoners: 0, - scoring_positions: "aaabbabb", + scoring_positions: "aabaabbb", stones: 2, - territory: 0, - total: 2, + territory: 2, + total: 4, }); }); @@ -192,14 +197,14 @@ describe("ScoreEstimator", () => { // . x o . // x x . o - const engine = new GoEngine({ komi: KOMI, width: 4, height: 2, rules: "japanese" }); + const engine = new GobanEngine({ komi: KOMI, width: 4, height: 2, rules: "japanese" }); engine.place(1, 0); engine.place(2, 0); engine.place(1, 1); engine.place(3, 1); engine.place(0, 1); - const se = new ScoreEstimator(undefined, engine, 10, 0.5, false); + const se = new ScoreEstimator(engine, undefined, 10, 0.5, false); await se.when_ready; se.score(); @@ -225,9 +230,9 @@ describe("ScoreEstimator", () => { }); test("score() with removed stones", async () => { - const se = new ScoreEstimator(undefined, engine, 10, 0.5, false); - se.toggleMetaGroupRemoval(1, 0); - se.toggleMetaGroupRemoval(2, 0); + const se = new ScoreEstimator(engine, undefined, 10, 0.5, false); + se.toggleSingleGroupRemoval(1, 0); + se.toggleSingleGroupRemoval(2, 0); await se.when_ready; se.score(); @@ -253,9 +258,9 @@ describe("ScoreEstimator", () => { }); test("getStoneRemovalString()", async () => { - const se = new ScoreEstimator(undefined, engine, 10, 0.5, false); - se.toggleMetaGroupRemoval(1, 0); - se.toggleMetaGroupRemoval(2, 0); + const se = new ScoreEstimator(engine, undefined, 10, 0.5, false); + se.toggleSingleGroupRemoval(1, 0); + se.toggleSingleGroupRemoval(2, 0); await se.when_ready; expect(se.getStoneRemovalString()).toBe("babbcacb"); @@ -271,13 +276,13 @@ describe("ScoreEstimator", () => { setForRemoval: jest.fn(), }; - const se = new ScoreEstimator(fake_goban as any, engine, 10, 0.5, false); + const se = new ScoreEstimator(engine, fake_goban as any, 10, 0.5, false); await se.when_ready; expect(fake_goban.updateScoreEstimation).toBeCalled(); - se.setRemoved(1, 0, 1); - expect(fake_goban.setForRemoval).toBeCalledWith(1, 0, 1); + se.setRemoved(1, 0, true); + expect(fake_goban.setForRemoval).toBeCalledWith(1, 0, true, true); }); test("getProbablyDead", async () => { @@ -285,9 +290,9 @@ describe("ScoreEstimator", () => { [1, 1, 1, 1], [1, 1, 1, 1], ]; - set_local_scorer(markBoardAllBlack); + set_local_ownership_estimator(markBoardAllBlack); - const se = new ScoreEstimator(undefined, engine, 10, 0.5, false); + const se = new ScoreEstimator(engine, undefined, 10, 0.5, false); await se.when_ready; // Note (bpj): I think this might be a bug @@ -297,15 +302,15 @@ describe("ScoreEstimator", () => { }); test("Falls back to local scorer if remote scorer is not set", async () => { - set_remote_scorer(undefined as any); + init_remote_ownership_estimator(undefined as any); const mock_local_scorer = jest.fn(); mock_local_scorer.mockReturnValue([ [1, 1, -1, -1], [1, 1, -1, -1], ]); - set_local_scorer(mock_local_scorer); + set_local_ownership_estimator(mock_local_scorer); - const se = new ScoreEstimator(undefined, engine, 10, 0.5, true); + const se = new ScoreEstimator(engine, undefined, 10, 0.5, true); await se.when_ready; expect(mock_local_scorer).toBeCalled(); @@ -316,17 +321,20 @@ describe("ScoreEstimator", () => { }); test("remote scorers do not need to set score", async () => { - const engine = new GoEngine({ komi: 3.5, width: 4, height: 2, rules: "chinese" }); + const engine = new GobanEngine({ komi: 3.5, width: 4, height: 2, rules: "chinese" }); engine.place(1, 0); engine.place(2, 0); engine.place(1, 1); engine.place(2, 1); - set_remote_scorer(async () => ({ + init_remote_ownership_estimator(async () => ({ ownership: OWNERSHIP, + autoscored_board_state: OWNERSHIP, + autoscored_removed: [], + autoscored_needs_sealing: [], })); - const se = new ScoreEstimator(undefined, engine, 10, 0.5, true); + const se = new ScoreEstimator(engine, undefined, 10, 0.5, true); await se.when_ready; expect(se.ownership).toEqual(OWNERSHIP); @@ -340,35 +348,47 @@ describe("ScoreEstimator", () => { }); test("local scorer with stones removed", async () => { - set_local_scorer(estimateScoreVoronoi); - const se = new ScoreEstimator(undefined, engine, 10, 0.5, false); + set_local_ownership_estimator(voronoi_estimate_ownership); + const se = new ScoreEstimator(engine, undefined, 10, 0.5, false); await se.when_ready; - se.handleClick(1, 0, false); - se.handleClick(2, 0, false); + se.handleClick(1, 0, false, 0); + se.handleClick(2, 0, false, 0); expect(se.removal).toEqual([ - [0, 1, 1, 0], - [0, 1, 1, 0], + [false, true, true, false], + [false, true, true, false], ]); - expect(se.ownership).toEqual(makeMatrix(4, 2)); + expect(se.ownership).toEqual(makeMatrix(4, 2, 0)); }); test("modkey", async () => { - set_local_scorer(estimateScoreVoronoi); - const se = new ScoreEstimator(undefined, engine, 10, 0.5, false); + set_local_ownership_estimator(voronoi_estimate_ownership); + const se = new ScoreEstimator(engine, undefined, 10, 0.5, false); + await se.when_ready; + + se.handleClick(1, 0, true, 0); + expect(se.removal).toEqual([ + [false, true, false, false], + [false, true, false, false], + ]); + }); + + test("long press", async () => { + set_local_ownership_estimator(voronoi_estimate_ownership); + const se = new ScoreEstimator(engine, undefined, 10, 0.5, false); await se.when_ready; - se.handleClick(1, 0, true); + se.handleClick(1, 0, false, 1000); expect(se.removal).toEqual([ - [0, 1, 0, 0], - [0, 0, 0, 0], + [false, true, false, false], + [false, true, false, false], ]); }); test("score() with captures", async () => { // A board that is split down the middle between black and white - const engine = new GoEngine({ + const engine = new GobanEngine({ width: 8, height: 8, initial_state: { black: "dadbdcdddedfdgdh", white: "eaebecedeeefegeh" }, @@ -391,7 +411,7 @@ describe("ScoreEstimator", () => { // 2 . . . X O . . . // 1 . . . X O . . . - const se = new ScoreEstimator(undefined, engine, 10, 0.5, false); + const se = new ScoreEstimator(engine, undefined, 10, 0.5, false); await se.when_ready; se.score(); diff --git a/test/unit_tests/StoneStringBuilder.test.ts b/test/unit_tests/StoneStringBuilder.test.ts new file mode 100644 index 00000000..a83f5f9c --- /dev/null +++ b/test/unit_tests/StoneStringBuilder.test.ts @@ -0,0 +1,55 @@ +import { StoneStringBuilder, BoardState, makeMatrix } from "engine"; + +// Here is a board displaying many of the features GoStoneGroup cares about. + +// A B C D E +// 5 . . O X . +// 4 O O O X X +// 3 X . O X . +// 2 . X O X X +// 1 X X O X . + +// A2: Eye, but not a "strong" eye +// E1, E3, E5: Strong eyes +// D1-E5 stones: Strong string +// all empty space except B3: Territory +// A5-B5, A2: Territory in seki + +const FEATURE_BOARD = [ + [0, 0, 1, 2, 0], + [1, 1, 1, 2, 2], + [2, 0, 1, 2, 0], + [0, 2, 1, 2, 2], + [2, 2, 1, 2, 0], +]; + +const REMOVAL = makeMatrix(5, 5, false); + +function makeGoMathWithFeatureBoard() { + return new StoneStringBuilder( + new BoardState({ + board: FEATURE_BOARD, + removal: REMOVAL, + }), + ); +} + +test("Group ID Map", () => { + const gm = makeGoMathWithFeatureBoard(); + + expect(gm.stone_string_id_map).toEqual([ + [1, 1, 2, 3, 4], + [2, 2, 2, 3, 3], + [5, 6, 2, 3, 7], + [8, 9, 2, 3, 3], + [9, 9, 2, 3, 10], + ]); +}); + +test("Territory", () => { + const gm = makeGoMathWithFeatureBoard(); + + const territory = gm.stone_strings.filter((g) => g.is_territory).map((g) => g.id); + + expect(territory).toEqual([1, 4, 7, 8, 10]); +}); diff --git a/src/__tests__/autoscore.test.ts b/test/unit_tests/autoscore.test.ts similarity index 63% rename from src/__tests__/autoscore.test.ts rename to test/unit_tests/autoscore.test.ts index 4f3e81f1..ab4fef90 100644 --- a/src/__tests__/autoscore.test.ts +++ b/test/unit_tests/autoscore.test.ts @@ -1,5 +1,5 @@ import { readFileSync, readdirSync } from "fs"; -import { autoscore } from "../autoscore"; +import { autoscore } from "engine"; describe("Auto-score tests ", () => { const files = readdirSync("test/autoscore_test_files"); @@ -30,13 +30,23 @@ describe("Auto-score tests ", () => { expect(data.correct_ownership).toBeDefined(); for (const row of data.correct_ownership) { for (const cell of row) { - const is_w_or_b = cell === "W" || cell === "B" || cell === " " || cell === "*"; + const is_w_or_b = + cell === "W" || + cell === "B" || + cell === " " || + cell === "*" || + cell === "s"; expect(is_w_or_b).toBe(true); } } // actual test - const [res, _debug_output] = autoscore(data.board, data.black, data.white); + const [res, _debug_output] = autoscore( + data.board, + data.rules ?? "chinese", + data.black, + data.white, + ); let match = true; for (let y = 0; y < res.result.length; ++y) { @@ -44,12 +54,24 @@ describe("Auto-score tests ", () => { const v = res.result[y][x]; match &&= data.correct_ownership[y][x] === "*" || + data.correct_ownership[y][x] === "s" || (v === 0 && data.correct_ownership[y][x] === " ") || (v === 1 && data.correct_ownership[y][x] === "B") || (v === 2 && data.correct_ownership[y][x] === "W"); } } + const needs_sealing = res.needs_sealing; + /* Ensure all needs_sealing are marked as such */ + for (const { x, y } of needs_sealing) { + if (data.correct_ownership[y][x] !== "s" && data.correct_ownership[y][x] !== "*") { + console.error( + `Engine thought we needed sealing at ${x},${y} but the that spot wasn't flagged as needing it in the test file`, + ); + match = false; + } + } + expect(match).toBe(true); }); } diff --git a/src/test_utils.ts b/test/unit_tests/test_utils.ts similarity index 84% rename from src/test_utils.ts rename to test/unit_tests/test_utils.ts index 6f0c5f4a..e58a3a62 100644 --- a/src/test_utils.ts +++ b/test/unit_tests/test_utils.ts @@ -15,8 +15,7 @@ * limitations under the License. */ -import { AdHocPackedMove } from "./AdHocFormat"; -import { JGOFNumericPlayerColor } from "./JGOF"; +import { AdHocPackedMove, JGOFNumericPlayerColor } from "engine/formats"; type Coordinate = { x: number; y: number }; @@ -69,3 +68,21 @@ export function movesFromBoardState(board: JGOFNumericPlayerColor[][]): AdHocPac return ret; } + +test("movesFromBoardState", () => { + const board = [ + [1, 2, 0, 0], + [2, 1, 0, 0], + [0, 0, 0, 0], + [0, 0, 0, 0], + ]; + + const moves = movesFromBoardState(board); + + expect(moves).toEqual([ + [1, 1], + [0, 1], + [0, 0], + [1, 0], + ]); +}); diff --git a/src/__tests__/Misc.test.ts b/test/unit_tests/util.test.ts similarity index 81% rename from src/__tests__/Misc.test.ts rename to test/unit_tests/util.test.ts index 74612681..06617bd4 100644 --- a/src/__tests__/Misc.test.ts +++ b/test/unit_tests/util.test.ts @@ -1,5 +1,5 @@ -import { escapeSGFText, newline2space } from "../Misc"; -import * as AdHoc from "../AdHocFormat"; +import { escapeSGFText, newlines_to_spaces } from "engine"; +import * as AdHoc from "engine/formats/AdHocFormat"; // String.raw`...` is the real string // (without js interpreting \, of which we have a ton) @@ -32,8 +32,8 @@ test("escapeSGFText handles colon iff need be", () => { expect(escapeSGFText("AC:AE", true)).toBe(String.raw`AC\:AE`); }); -test("newline2space replaces what it should", () => { - expect(newline2space("hello\nlucky\r\nboy")).toBe("hello lucky boy"); +test("newlines_to_spaces replaces what it should", () => { + expect(newlines_to_spaces("hello\nlucky\r\nboy")).toBe("hello lucky boy"); }); test("AdHoc is defined", () => { diff --git a/tsconfig.json b/tsconfig.json index 3d68a67d..3fdbbeb9 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -7,27 +7,35 @@ "outDir": "./lib/", "moduleResolution": "node", "removeComments": false, - "declaration": true, + "declaration": false, "emitDecoratorMetadata": true, "experimentalDecorators": true, "lib": ["es2015", "dom"], "baseUrl": ".", "paths": { - "*": ["src/*", "*"] + "*": ["src/*", "*"], + "engine": ["src/engine"], + "goscorer": ["src/third_party/goscorer/goscorer"] }, "forceConsistentCasingInFileNames": true, "noImplicitAny": true, "noImplicitReturns": true, "noImplicitThis": true, + "noImplicitOverride": true, "strictBindCallApply": true, "strictFunctionTypes": true, "strictNullChecks": true, "strictPropertyInitialization": true, "noUnusedLocals": true, + "allowJs": true, "sourceMap": true, "jsx": "react" }, - "files": ["./src/goban.ts", "./src/engine.ts", "./test/test_autoscore.ts"] + "files": ["src/index.ts", "src/engine/index.ts", "jest.config.ts"], + "include": ["test/**/*.ts", "scripts/**/*.ts"], + "ts-node": { + "require": ["tsconfig-paths/register"] + } } diff --git a/tsconfig.node.json b/tsconfig.node.json index 6335e130..c80ecef4 100644 --- a/tsconfig.node.json +++ b/tsconfig.node.json @@ -2,21 +2,21 @@ "compileOnSave": false, "buildOnSave": false, "compilerOptions": { - "target": "es2017", - "module": "commonjs", + "target": "es2022", + "module": "node16", "outDir": "./node/", - "moduleResolution": "node", + "moduleResolution": "node16", + "esModuleInterop": true, "removeComments": false, "declaration": true, "emitDecoratorMetadata": true, "experimentalDecorators": true, - "lib": ["es2017", "dom"], + "lib": ["es2023", "dom"], "baseUrl": ".", "paths": { - "*": [ - "src/*", - "*" - ] + "*": ["src/*", "*"], + "engine": ["src/engine"], + "goscorer": ["src/third_party/goscorer/goscorer"] }, "noImplicitAny": true, "noImplicitReturns": true, @@ -24,7 +24,5 @@ "sourceMap": true, "jsx": "react" }, - "files": [ - "./src/engine.ts" - ] + "files": ["./src/engine/index.ts"] } diff --git a/webpack.config.js b/webpack.config.js index 22214442..3659475f 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -1,18 +1,16 @@ -'use strict'; +"use strict"; -const path = require('path'); -const fs = require('fs'); -const webpack = require('webpack'); -const pkg = require('./package.json'); -const TerserPlugin = require('terser-webpack-plugin'); +const path = require("path"); +const fs = require("fs"); +const webpack = require("webpack"); +const pkg = require("./package.json"); +const TerserPlugin = require("terser-webpack-plugin"); let plugins = []; - - - -plugins.push(new webpack.BannerPlugin( -`Copyright (C) Online-Go.com +plugins.push( + new webpack.BannerPlugin( + `Copyright (C) Online-Go.com Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -25,24 +23,25 @@ distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. -`)); +`, + ), +); module.exports = (env, argv) => { - const production = argv.mode === 'production'; + const production = argv.mode === "production"; - plugins.push(new webpack.EnvironmentPlugin({ - NODE_ENV: production ? 'production' : 'development', - DEBUG: false - })); + plugins.push( + new webpack.EnvironmentPlugin({ + NODE_ENV: production ? "production" : "development", + DEBUG: false, + }), + ); const common = { - mode: production ? 'production' : 'development', + mode: production ? "production" : "development", resolve: { - modules: [ - 'src', - 'node_modules' - ], + modules: ["src", "node_modules", "src/third_party/goscorer"], extensions: [".webpack.js", ".web.js", ".ts", ".tsx", ".js"], }, @@ -53,31 +52,76 @@ module.exports = (env, argv) => { optimization: { minimizer: [ - new TerserPlugin({ - terserOptions: { - }, - }), + new TerserPlugin({ + terserOptions: {}, + }), ], }, - - devtool: 'source-map', + devtool: "source-map", }; - let ret = [ - /* web */ + // Engine only build for node (no renderers) + Object.assign({}, common, { + target: "node", + + entry: { + "goban-engine": "./src/engine/index.ts", + }, + + module: { + rules: [ + // All files with a '.ts' or '.tsx' extension will be handled by 'ts-loader'. + { + test: /\.tsx?$/, + loader: "ts-loader", + exclude: /node_modules/, + options: { + configFile: "tsconfig.node.json", + }, + }, + ], + }, + + output: { + path: __dirname + "/node", + filename: "[name].js", + globalObject: "this", + library: { + name: "goban", + type: "umd", + }, + }, + + plugins: plugins.concat([ + new webpack.DefinePlugin({ + CLIENT: false, + SERVER: true, + }), + ]), + + optimization: { + minimizer: [ + new TerserPlugin({ + terserOptions: { + safari10: true, + }, + }), + ], + }, + }), + + // With Goban renderers (web) Object.assign({}, common, { - 'target': 'web', + target: "web", entry: { - 'goban': './src/goban.ts', - 'engine': './src/engine.ts', - 'test': './src/test.tsx', + goban: "./src/index.ts", }, output: { - path: __dirname + '/lib', - filename: production ? '[name].min.js' : '[name].js', + path: __dirname + "/lib", + filename: production ? "[name].min.js" : "[name].js", library: { name: "goban", type: "umd", @@ -92,14 +136,14 @@ module.exports = (env, argv) => { exclude: /node_modules/, loader: "ts-loader", options: { - configFile: 'tsconfig.json', - } + configFile: "tsconfig.json", + }, }, { test: /\.svg$/, - loader: 'svg-inline-loader' - } - ] + loader: "svg-inline-loader", + }, + ], }, plugins: plugins.concat([ @@ -116,79 +160,26 @@ module.exports = (env, argv) => { devServer: { compress: true, - host: '0.0.0.0', + host: "0.0.0.0", port: 9000, - allowedHosts: ['all'], + allowedHosts: ["all"], static: [ - path.join(__dirname, 'assets'), - path.join(__dirname, 'test'), - path.join(__dirname, 'lib'), + path.join(__dirname, "assets"), + path.join(__dirname, "test"), + path.join(__dirname, "lib"), ], devMiddleware: { index: true, - mimeTypes: { phtml: 'text/html' }, + mimeTypes: { phtml: "text/html" }, serverSideRender: true, writeToDisk: true, }, hot: false, - } - }) - ]; - - if (production) { - ret.push( - /* node */ - Object.assign({}, common, { - 'target': 'node', - - entry: { - 'engine': './src/engine.ts', }, - - module: { - rules: [ - // All files with a '.ts' or '.tsx' extension will be handled by 'ts-loader'. - { - test: /\.tsx?$/, - loader: "ts-loader", - exclude: /node_modules/, - options: { - configFile: 'tsconfig.node.json', - } - } - ] - }, - - output: { - path: __dirname + '/node', - filename: '[name].js' - }, - - externals: { - "pixi.js": "PIXI", - "pixi.js-legacy": "PIXI", - }, - - plugins: plugins.concat([ - new webpack.DefinePlugin({ - CLIENT: false, - SERVER: true, - }), - ]), - - optimization: { - minimizer: [ - new TerserPlugin({ - terserOptions: { - safari10: true, - }, - }), - ], - }, - })); - } + }), + ]; return ret; -} +}; diff --git a/yarn.lock b/yarn.lock index 462747db..c388944e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10,156 +10,159 @@ "@jridgewell/gen-mapping" "^0.3.5" "@jridgewell/trace-mapping" "^0.3.24" -"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.12.13", "@babel/code-frame@^7.16.7", "@babel/code-frame@^7.23.5", "@babel/code-frame@^7.24.2": - version "7.24.2" - resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.24.2.tgz#718b4b19841809a58b29b68cde80bc5e1aa6d9ae" - integrity sha512-y5+tLQyV8pg3fsiln67BVLD1P13Eg4lh5RW9mF0zUuvLrv9uIQ4MCL+CRT+FTsBlBjcIan6PGsLcBN0m3ClUyQ== +"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.12.13", "@babel/code-frame@^7.16.7", "@babel/code-frame@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.24.7.tgz#882fd9e09e8ee324e496bd040401c6f046ef4465" + integrity sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA== dependencies: - "@babel/highlight" "^7.24.2" + "@babel/highlight" "^7.24.7" picocolors "^1.0.0" -"@babel/compat-data@^7.23.5": - version "7.24.4" - resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.24.4.tgz#6f102372e9094f25d908ca0d34fc74c74606059a" - integrity sha512-vg8Gih2MLK+kOkHJp4gBEIkyaIi00jgWot2D9QOmmfLC8jINSOzmCLta6Bvz/JSBCqnegV0L80jhxkol5GWNfQ== +"@babel/compat-data@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.24.7.tgz#d23bbea508c3883ba8251fb4164982c36ea577ed" + integrity sha512-qJzAIcv03PyaWqxRgO4mSU3lihncDT296vnyuE2O8uA4w3UHWI4S3hgeZd1L8W1Bft40w9JxJ2b412iDUFFRhw== "@babel/core@^7.11.6", "@babel/core@^7.12.3", "@babel/core@^7.23.9": - version "7.24.5" - resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.24.5.tgz#15ab5b98e101972d171aeef92ac70d8d6718f06a" - integrity sha512-tVQRucExLQ02Boi4vdPp49svNGcfL2GhdTCT9aldhXgCJVAI21EtRfBettiuLUwce/7r6bFdgs6JFkcdTiFttA== + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.24.7.tgz#b676450141e0b52a3d43bc91da86aa608f950ac4" + integrity sha512-nykK+LEK86ahTkX/3TgauT0ikKoNCfKHEaZYTUVupJdTLzGNvrblu4u6fa7DhZONAltdf8e662t/abY8idrd/g== dependencies: "@ampproject/remapping" "^2.2.0" - "@babel/code-frame" "^7.24.2" - "@babel/generator" "^7.24.5" - "@babel/helper-compilation-targets" "^7.23.6" - "@babel/helper-module-transforms" "^7.24.5" - "@babel/helpers" "^7.24.5" - "@babel/parser" "^7.24.5" - "@babel/template" "^7.24.0" - "@babel/traverse" "^7.24.5" - "@babel/types" "^7.24.5" + "@babel/code-frame" "^7.24.7" + "@babel/generator" "^7.24.7" + "@babel/helper-compilation-targets" "^7.24.7" + "@babel/helper-module-transforms" "^7.24.7" + "@babel/helpers" "^7.24.7" + "@babel/parser" "^7.24.7" + "@babel/template" "^7.24.7" + "@babel/traverse" "^7.24.7" + "@babel/types" "^7.24.7" convert-source-map "^2.0.0" debug "^4.1.0" gensync "^1.0.0-beta.2" json5 "^2.2.3" semver "^6.3.1" -"@babel/generator@^7.24.5", "@babel/generator@^7.7.2": - version "7.24.5" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.24.5.tgz#e5afc068f932f05616b66713e28d0f04e99daeb3" - integrity sha512-x32i4hEXvr+iI0NEoEfDKzlemF8AmtOP8CcrRaEcpzysWuoEb1KknpcvMsHKPONoKZiDuItklgWhB18xEhr9PA== +"@babel/generator@^7.24.7", "@babel/generator@^7.7.2": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.24.7.tgz#1654d01de20ad66b4b4d99c135471bc654c55e6d" + integrity sha512-oipXieGC3i45Y1A41t4tAqpnEZWgB/lC6Ehh6+rOviR5XWpTtMmLN+fGjz9vOiNRt0p6RtO6DtD0pdU3vpqdSA== dependencies: - "@babel/types" "^7.24.5" + "@babel/types" "^7.24.7" "@jridgewell/gen-mapping" "^0.3.5" "@jridgewell/trace-mapping" "^0.3.25" jsesc "^2.5.1" -"@babel/helper-compilation-targets@^7.23.6": - version "7.23.6" - resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.23.6.tgz#4d79069b16cbcf1461289eccfbbd81501ae39991" - integrity sha512-9JB548GZoQVmzrFgp8o7KxdgkTGm6xs9DW0o/Pim72UDjzr5ObUQ6ZzYPqA+g9OTS2bBQoctLJrky0RDCAWRgQ== +"@babel/helper-compilation-targets@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.24.7.tgz#4eb6c4a80d6ffeac25ab8cd9a21b5dfa48d503a9" + integrity sha512-ctSdRHBi20qWOfy27RUb4Fhp07KSJ3sXcuSvTrXrc4aG8NSYDo1ici3Vhg9bg69y5bj0Mr1lh0aeEgTvc12rMg== dependencies: - "@babel/compat-data" "^7.23.5" - "@babel/helper-validator-option" "^7.23.5" + "@babel/compat-data" "^7.24.7" + "@babel/helper-validator-option" "^7.24.7" browserslist "^4.22.2" lru-cache "^5.1.1" semver "^6.3.1" -"@babel/helper-environment-visitor@^7.22.20": - version "7.22.20" - resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz#96159db61d34a29dba454c959f5ae4a649ba9167" - integrity sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA== - -"@babel/helper-function-name@^7.23.0": - version "7.23.0" - resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz#1f9a3cdbd5b2698a670c30d2735f9af95ed52759" - integrity sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw== - dependencies: - "@babel/template" "^7.22.15" - "@babel/types" "^7.23.0" - -"@babel/helper-hoist-variables@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz#c01a007dac05c085914e8fb652b339db50d823bb" - integrity sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw== - dependencies: - "@babel/types" "^7.22.5" - -"@babel/helper-module-imports@^7.24.3": - version "7.24.3" - resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.24.3.tgz#6ac476e6d168c7c23ff3ba3cf4f7841d46ac8128" - integrity sha512-viKb0F9f2s0BCS22QSF308z/+1YWKV/76mwt61NBzS5izMzDPwdq1pTrzf+Li3npBWX9KdQbkeCt1jSAM7lZqg== - dependencies: - "@babel/types" "^7.24.0" - -"@babel/helper-module-transforms@^7.24.5": - version "7.24.5" - resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.24.5.tgz#ea6c5e33f7b262a0ae762fd5986355c45f54a545" - integrity sha512-9GxeY8c2d2mdQUP1Dye0ks3VDyIMS98kt/llQ2nUId8IsWqTF0l1LkSX0/uP7l7MCDrzXS009Hyhe2gzTiGW8A== - dependencies: - "@babel/helper-environment-visitor" "^7.22.20" - "@babel/helper-module-imports" "^7.24.3" - "@babel/helper-simple-access" "^7.24.5" - "@babel/helper-split-export-declaration" "^7.24.5" - "@babel/helper-validator-identifier" "^7.24.5" - -"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.14.5", "@babel/helper-plugin-utils@^7.24.0", "@babel/helper-plugin-utils@^7.8.0": - version "7.24.5" - resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.5.tgz#a924607dd254a65695e5bd209b98b902b3b2f11a" - integrity sha512-xjNLDopRzW2o6ba0gKbkZq5YWEBaK3PCyTOY1K2P/O07LGMhMqlMXPxwN4S5/RhWuCobT8z0jrlKGlYmeR1OhQ== - -"@babel/helper-simple-access@^7.24.5": - version "7.24.5" - resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.24.5.tgz#50da5b72f58c16b07fbd992810be6049478e85ba" - integrity sha512-uH3Hmf5q5n7n8mz7arjUlDOCbttY/DW4DYhE6FUsjKJ/oYC1kQQUvwEQWxRwUpX9qQKRXeqLwWxrqilMrf32sQ== - dependencies: - "@babel/types" "^7.24.5" - -"@babel/helper-split-export-declaration@^7.24.5": - version "7.24.5" - resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.5.tgz#b9a67f06a46b0b339323617c8c6213b9055a78b6" - integrity sha512-5CHncttXohrHk8GWOFCcCl4oRD9fKosWlIRgWm4ql9VYioKm52Mk2xsmoohvm7f3JoiLSM5ZgJuRaf5QZZYd3Q== - dependencies: - "@babel/types" "^7.24.5" - -"@babel/helper-string-parser@^7.24.1": - version "7.24.1" - resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.24.1.tgz#f99c36d3593db9540705d0739a1f10b5e20c696e" - integrity sha512-2ofRCjnnA9y+wk8b9IAREroeUP02KHp431N2mhKniy2yKIDKpbrHv9eXwm8cBeWQYcJmzv5qKCu65P47eCF7CQ== - -"@babel/helper-validator-identifier@^7.22.20", "@babel/helper-validator-identifier@^7.24.5": - version "7.24.5" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.5.tgz#918b1a7fa23056603506370089bd990d8720db62" - integrity sha512-3q93SSKX2TWCG30M2G2kwaKeTYgEUp5Snjuj8qm729SObL6nbtUldAi37qbxkD5gg3xnBio+f9nqpSepGZMvxA== - -"@babel/helper-validator-option@^7.23.5": - version "7.23.5" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.23.5.tgz#907a3fbd4523426285365d1206c423c4c5520307" - integrity sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw== - -"@babel/helpers@^7.24.5": - version "7.24.5" - resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.24.5.tgz#fedeb87eeafa62b621160402181ad8585a22a40a" - integrity sha512-CiQmBMMpMQHwM5m01YnrM6imUG1ebgYJ+fAIW4FZe6m4qHTPaRHti+R8cggAwkdz4oXhtO4/K9JWlh+8hIfR2Q== - dependencies: - "@babel/template" "^7.24.0" - "@babel/traverse" "^7.24.5" - "@babel/types" "^7.24.5" - -"@babel/highlight@^7.24.2": - version "7.24.5" - resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.24.5.tgz#bc0613f98e1dd0720e99b2a9ee3760194a704b6e" - integrity sha512-8lLmua6AVh/8SLJRRVD6V8p73Hir9w5mJrhE+IPpILG31KKlI9iz5zmBYKcWPS59qSfgP9RaSBQSHHE81WKuEw== - dependencies: - "@babel/helper-validator-identifier" "^7.24.5" +"@babel/helper-environment-visitor@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.24.7.tgz#4b31ba9551d1f90781ba83491dd59cf9b269f7d9" + integrity sha512-DoiN84+4Gnd0ncbBOM9AZENV4a5ZiL39HYMyZJGZ/AZEykHYdJw0wW3kdcsh9/Kn+BRXHLkkklZ51ecPKmI1CQ== + dependencies: + "@babel/types" "^7.24.7" + +"@babel/helper-function-name@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.24.7.tgz#75f1e1725742f39ac6584ee0b16d94513da38dd2" + integrity sha512-FyoJTsj/PEUWu1/TYRiXTIHc8lbw+TDYkZuoE43opPS5TrI7MyONBE1oNvfguEXAD9yhQRrVBnXdXzSLQl9XnA== + dependencies: + "@babel/template" "^7.24.7" + "@babel/types" "^7.24.7" + +"@babel/helper-hoist-variables@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.24.7.tgz#b4ede1cde2fd89436397f30dc9376ee06b0f25ee" + integrity sha512-MJJwhkoGy5c4ehfoRyrJ/owKeMl19U54h27YYftT0o2teQ3FJ3nQUf/I3LlJsX4l3qlw7WRXUmiyajvHXoTubQ== + dependencies: + "@babel/types" "^7.24.7" + +"@babel/helper-module-imports@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.24.7.tgz#f2f980392de5b84c3328fc71d38bd81bbb83042b" + integrity sha512-8AyH3C+74cgCVVXow/myrynrAGv+nTVg5vKu2nZph9x7RcRwzmh0VFallJuFTZ9mx6u4eSdXZfcOzSqTUm0HCA== + dependencies: + "@babel/traverse" "^7.24.7" + "@babel/types" "^7.24.7" + +"@babel/helper-module-transforms@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.24.7.tgz#31b6c9a2930679498db65b685b1698bfd6c7daf8" + integrity sha512-1fuJEwIrp+97rM4RWdO+qrRsZlAeL1lQJoPqtCYWv0NL115XM93hIH4CSRln2w52SqvmY5hqdtauB6QFCDiZNQ== + dependencies: + "@babel/helper-environment-visitor" "^7.24.7" + "@babel/helper-module-imports" "^7.24.7" + "@babel/helper-simple-access" "^7.24.7" + "@babel/helper-split-export-declaration" "^7.24.7" + "@babel/helper-validator-identifier" "^7.24.7" + +"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.14.5", "@babel/helper-plugin-utils@^7.24.7", "@babel/helper-plugin-utils@^7.8.0": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.7.tgz#98c84fe6fe3d0d3ae7bfc3a5e166a46844feb2a0" + integrity sha512-Rq76wjt7yz9AAc1KnlRKNAi/dMSVWgDRx43FHoJEbcYU6xOWaE2dVPwcdTukJrjxS65GITyfbvEYHvkirZ6uEg== + +"@babel/helper-simple-access@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.24.7.tgz#bcade8da3aec8ed16b9c4953b74e506b51b5edb3" + integrity sha512-zBAIvbCMh5Ts+b86r/CjU+4XGYIs+R1j951gxI3KmmxBMhCg4oQMsv6ZXQ64XOm/cvzfU1FmoCyt6+owc5QMYg== + dependencies: + "@babel/traverse" "^7.24.7" + "@babel/types" "^7.24.7" + +"@babel/helper-split-export-declaration@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.7.tgz#83949436890e07fa3d6873c61a96e3bbf692d856" + integrity sha512-oy5V7pD+UvfkEATUKvIjvIAH/xCzfsFVw7ygW2SI6NClZzquT+mwdTfgfdbUiceh6iQO0CHtCPsyze/MZ2YbAA== + dependencies: + "@babel/types" "^7.24.7" + +"@babel/helper-string-parser@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.24.7.tgz#4d2d0f14820ede3b9807ea5fc36dfc8cd7da07f2" + integrity sha512-7MbVt6xrwFQbunH2DNQsAP5sTGxfqQtErvBIvIMi6EQnbgUOuVYanvREcmFrOPhoXBrTtjhhP+lW+o5UfK+tDg== + +"@babel/helper-validator-identifier@^7.22.20", "@babel/helper-validator-identifier@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz#75b889cfaf9e35c2aaf42cf0d72c8e91719251db" + integrity sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w== + +"@babel/helper-validator-option@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.24.7.tgz#24c3bb77c7a425d1742eec8fb433b5a1b38e62f6" + integrity sha512-yy1/KvjhV/ZCL+SM7hBrvnZJ3ZuT9OuZgIJAGpPEToANvc3iM6iDvBnRjtElWibHU6n8/LPR/EjX9EtIEYO3pw== + +"@babel/helpers@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.24.7.tgz#aa2ccda29f62185acb5d42fb4a3a1b1082107416" + integrity sha512-NlmJJtvcw72yRJRcnCmGvSi+3jDEg8qFu3z0AFoymmzLx5ERVWyzd9kVXr7Th9/8yIJi2Zc6av4Tqz3wFs8QWg== + dependencies: + "@babel/template" "^7.24.7" + "@babel/types" "^7.24.7" + +"@babel/highlight@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.24.7.tgz#a05ab1df134b286558aae0ed41e6c5f731bf409d" + integrity sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw== + dependencies: + "@babel/helper-validator-identifier" "^7.24.7" chalk "^2.4.2" js-tokens "^4.0.0" picocolors "^1.0.0" -"@babel/parser@^7.1.0", "@babel/parser@^7.14.7", "@babel/parser@^7.20.7", "@babel/parser@^7.23.9", "@babel/parser@^7.24.0", "@babel/parser@^7.24.5": - version "7.24.5" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.24.5.tgz#4a4d5ab4315579e5398a82dcf636ca80c3392790" - integrity sha512-EOv5IK8arwh3LI47dz1b0tKUb/1uhHAnHJOrjgtQMIpu1uXd9mlFrJg9IUgGUgZ41Ch0K8REPTYpO7B76b4vJg== +"@babel/parser@^7.1.0", "@babel/parser@^7.14.7", "@babel/parser@^7.20.7", "@babel/parser@^7.23.9", "@babel/parser@^7.24.7", "@babel/parser@^7.6.0", "@babel/parser@^7.9.6": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.24.7.tgz#9a5226f92f0c5c8ead550b750f5608e766c8ce85" + integrity sha512-9uUYRm6OqQrCqQdG1iCBwBPZgN8ciDBro2nIOFaiRz1/BCxaI7CNvQbDHvsArAC7Tw9Hda/B3U+6ui9u4HWXPw== "@babel/plugin-syntax-async-generators@^7.8.4": version "7.8.4" @@ -197,11 +200,11 @@ "@babel/helper-plugin-utils" "^7.8.0" "@babel/plugin-syntax-jsx@^7.7.2": - version "7.24.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.24.1.tgz#3f6ca04b8c841811dbc3c5c5f837934e0d626c10" - integrity sha512-2eCtxZXf+kbkMIsXS4poTvT4Yu5rXiRa+9xGVT56raghjmBTKMpFNc9R4IDiB4emao9eO22Ox7CxuJG7BgExqA== + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.24.7.tgz#39a1fa4a7e3d3d7f34e2acc6be585b718d30e02d" + integrity sha512-6ddciUPe/mpMnOKv/U+RSd2vvVy+Yw/JfBB0ZHYjEZt9NLHmCUylNYlsbqCCS1Bffjlb0fCwC9Vqz+sBz6PsiQ== dependencies: - "@babel/helper-plugin-utils" "^7.24.0" + "@babel/helper-plugin-utils" "^7.24.7" "@babel/plugin-syntax-logical-assignment-operators@^7.8.3": version "7.10.4" @@ -253,44 +256,44 @@ "@babel/helper-plugin-utils" "^7.14.5" "@babel/plugin-syntax-typescript@^7.7.2": - version "7.24.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.24.1.tgz#b3bcc51f396d15f3591683f90239de143c076844" - integrity sha512-Yhnmvy5HZEnHUty6i++gcfH1/l68AHnItFHnaCv6hn9dNh0hQvvQJsxpi4BMBFN5DLeHBuucT/0DgzXif/OyRw== - dependencies: - "@babel/helper-plugin-utils" "^7.24.0" - -"@babel/template@^7.22.15", "@babel/template@^7.24.0", "@babel/template@^7.3.3": - version "7.24.0" - resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.24.0.tgz#c6a524aa93a4a05d66aaf31654258fae69d87d50" - integrity sha512-Bkf2q8lMB0AFpX0NFEqSbx1OkTHf0f+0j82mkw+ZpzBnkk7e9Ql0891vlfgi+kHwOk8tQjiQHpqh4LaSa0fKEA== - dependencies: - "@babel/code-frame" "^7.23.5" - "@babel/parser" "^7.24.0" - "@babel/types" "^7.24.0" - -"@babel/traverse@^7.24.5": - version "7.24.5" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.24.5.tgz#972aa0bc45f16983bf64aa1f877b2dd0eea7e6f8" - integrity sha512-7aaBLeDQ4zYcUFDUD41lJc1fG8+5IU9DaNSJAgal866FGvmD5EbWQgnEC6kO1gGLsX0esNkfnJSndbTXA3r7UA== - dependencies: - "@babel/code-frame" "^7.24.2" - "@babel/generator" "^7.24.5" - "@babel/helper-environment-visitor" "^7.22.20" - "@babel/helper-function-name" "^7.23.0" - "@babel/helper-hoist-variables" "^7.22.5" - "@babel/helper-split-export-declaration" "^7.24.5" - "@babel/parser" "^7.24.5" - "@babel/types" "^7.24.5" + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.24.7.tgz#58d458271b4d3b6bb27ee6ac9525acbb259bad1c" + integrity sha512-c/+fVeJBB0FeKsFvwytYiUD+LBvhHjGSI0g446PRGdSVGZLRNArBUno2PETbAly3tpiNAQR5XaZ+JslxkotsbA== + dependencies: + "@babel/helper-plugin-utils" "^7.24.7" + +"@babel/template@^7.24.7", "@babel/template@^7.3.3": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.24.7.tgz#02efcee317d0609d2c07117cb70ef8fb17ab7315" + integrity sha512-jYqfPrU9JTF0PmPy1tLYHW4Mp4KlgxJD9l2nP9fD6yT/ICi554DmrWBAEYpIelzjHf1msDP3PxJIRt/nFNfBig== + dependencies: + "@babel/code-frame" "^7.24.7" + "@babel/parser" "^7.24.7" + "@babel/types" "^7.24.7" + +"@babel/traverse@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.24.7.tgz#de2b900163fa741721ba382163fe46a936c40cf5" + integrity sha512-yb65Ed5S/QAcewNPh0nZczy9JdYXkkAbIsEo+P7BE7yO3txAY30Y/oPa3QkQ5It3xVG2kpKMg9MsdxZaO31uKA== + dependencies: + "@babel/code-frame" "^7.24.7" + "@babel/generator" "^7.24.7" + "@babel/helper-environment-visitor" "^7.24.7" + "@babel/helper-function-name" "^7.24.7" + "@babel/helper-hoist-variables" "^7.24.7" + "@babel/helper-split-export-declaration" "^7.24.7" + "@babel/parser" "^7.24.7" + "@babel/types" "^7.24.7" debug "^4.3.1" globals "^11.1.0" -"@babel/types@^7.0.0", "@babel/types@^7.20.7", "@babel/types@^7.22.5", "@babel/types@^7.23.0", "@babel/types@^7.24.0", "@babel/types@^7.24.5", "@babel/types@^7.3.3": - version "7.24.5" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.24.5.tgz#7661930afc638a5383eb0c4aee59b74f38db84d7" - integrity sha512-6mQNsaLeXTw0nxYUYu+NSa4Hx4BlF1x1x8/PMFbiR+GBSr+2DkECc69b8hgy2frEodNcvPffeH8YfWd3LI6jhQ== +"@babel/types@^7.0.0", "@babel/types@^7.20.7", "@babel/types@^7.24.7", "@babel/types@^7.3.3", "@babel/types@^7.6.1", "@babel/types@^7.9.6": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.24.7.tgz#6027fe12bc1aa724cd32ab113fb7f1988f1f66f2" + integrity sha512-XEFXSlxiG5td2EJRe8vOmRbaXVgfcBlszKujvVmWIK/UpywWljQCfzAv3RQCGujWQ1RD4YYWEAqDXfuJiy8f5Q== dependencies: - "@babel/helper-string-parser" "^7.24.1" - "@babel/helper-validator-identifier" "^7.24.5" + "@babel/helper-string-parser" "^7.24.7" + "@babel/helper-validator-identifier" "^7.24.7" to-fast-properties "^2.0.0" "@bcoe/v8-coverage@^0.2.3": @@ -298,116 +301,122 @@ resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== -"@cspell/cspell-bundled-dicts@8.7.0": - version "8.7.0" - resolved "https://registry.yarnpkg.com/@cspell/cspell-bundled-dicts/-/cspell-bundled-dicts-8.7.0.tgz#dd8d671fc6b6900b0eec9180e24fa4e69f038829" - integrity sha512-B5YQI7Dd9m0JHTmHgs7PiyP4BWXzl8ixpK+HGOwhxzh7GyfFt1Eo/gxMxBDX/9SaewEzeb2OjRpRKEFtEsto3A== +"@colors/colors@1.5.0": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9" + integrity sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ== + +"@cspell/cspell-bundled-dicts@8.8.4": + version "8.8.4" + resolved "https://registry.yarnpkg.com/@cspell/cspell-bundled-dicts/-/cspell-bundled-dicts-8.8.4.tgz#3ebb5041316dc7c4cfabb3823a6f69dd73ccb31b" + integrity sha512-k9ZMO2kayQFXB3B45b1xXze3MceAMNy9U+D7NTnWB1i3S0y8LhN53U9JWWgqHGPQaHaLHzizL7/w1aGHTA149Q== dependencies: "@cspell/dict-ada" "^4.0.2" - "@cspell/dict-aws" "^4.0.1" + "@cspell/dict-aws" "^4.0.2" "@cspell/dict-bash" "^4.1.3" - "@cspell/dict-companies" "^3.0.31" - "@cspell/dict-cpp" "^5.1.3" + "@cspell/dict-companies" "^3.1.2" + "@cspell/dict-cpp" "^5.1.8" "@cspell/dict-cryptocurrencies" "^5.0.0" "@cspell/dict-csharp" "^4.0.2" "@cspell/dict-css" "^4.0.12" "@cspell/dict-dart" "^2.0.3" "@cspell/dict-django" "^4.1.0" "@cspell/dict-docker" "^1.1.7" - "@cspell/dict-dotnet" "^5.0.0" + "@cspell/dict-dotnet" "^5.0.2" "@cspell/dict-elixir" "^4.0.3" - "@cspell/dict-en-common-misspellings" "^2.0.0" + "@cspell/dict-en-common-misspellings" "^2.0.1" "@cspell/dict-en-gb" "1.1.33" - "@cspell/dict-en_us" "^4.3.17" - "@cspell/dict-filetypes" "^3.0.3" + "@cspell/dict-en_us" "^4.3.21" + "@cspell/dict-filetypes" "^3.0.4" "@cspell/dict-fonts" "^4.0.0" "@cspell/dict-fsharp" "^1.0.1" - "@cspell/dict-fullstack" "^3.1.5" + "@cspell/dict-fullstack" "^3.1.8" "@cspell/dict-gaming-terms" "^1.0.5" "@cspell/dict-git" "^3.0.0" - "@cspell/dict-golang" "^6.0.5" + "@cspell/dict-golang" "^6.0.9" + "@cspell/dict-google" "^1.0.1" "@cspell/dict-haskell" "^4.0.1" "@cspell/dict-html" "^4.0.5" "@cspell/dict-html-symbol-entities" "^4.0.0" "@cspell/dict-java" "^5.0.6" "@cspell/dict-julia" "^1.0.1" - "@cspell/dict-k8s" "^1.0.2" + "@cspell/dict-k8s" "^1.0.5" "@cspell/dict-latex" "^4.0.0" "@cspell/dict-lorem-ipsum" "^4.0.0" "@cspell/dict-lua" "^4.0.3" "@cspell/dict-makefile" "^1.0.0" "@cspell/dict-monkeyc" "^1.0.6" - "@cspell/dict-node" "^4.0.3" - "@cspell/dict-npm" "^5.0.15" - "@cspell/dict-php" "^4.0.6" - "@cspell/dict-powershell" "^5.0.3" - "@cspell/dict-public-licenses" "^2.0.6" + "@cspell/dict-node" "^5.0.1" + "@cspell/dict-npm" "^5.0.16" + "@cspell/dict-php" "^4.0.7" + "@cspell/dict-powershell" "^5.0.4" + "@cspell/dict-public-licenses" "^2.0.7" "@cspell/dict-python" "^4.1.11" "@cspell/dict-r" "^2.0.1" "@cspell/dict-ruby" "^5.0.2" - "@cspell/dict-rust" "^4.0.2" - "@cspell/dict-scala" "^5.0.0" - "@cspell/dict-software-terms" "^3.3.18" + "@cspell/dict-rust" "^4.0.3" + "@cspell/dict-scala" "^5.0.2" + "@cspell/dict-software-terms" "^3.4.1" "@cspell/dict-sql" "^2.1.3" "@cspell/dict-svelte" "^1.0.2" "@cspell/dict-swift" "^2.0.1" "@cspell/dict-terraform" "^1.0.0" - "@cspell/dict-typescript" "^3.1.2" + "@cspell/dict-typescript" "^3.1.5" "@cspell/dict-vue" "^3.0.0" -"@cspell/cspell-json-reporter@8.7.0": - version "8.7.0" - resolved "https://registry.yarnpkg.com/@cspell/cspell-json-reporter/-/cspell-json-reporter-8.7.0.tgz#b51090a9e13e92605c6b47a7f53ffd7696a93741" - integrity sha512-LTQPEvXvCqnc+ok9WXpSISZyt4/nGse9fVEM430g0BpGzKpt3RMx49B8uasvvnanzCuikaW9+wFLmwgvraERhA== +"@cspell/cspell-json-reporter@8.8.4": + version "8.8.4" + resolved "https://registry.yarnpkg.com/@cspell/cspell-json-reporter/-/cspell-json-reporter-8.8.4.tgz#77dfddc021a2f3072bceb877ea1f26ae9893abc3" + integrity sha512-ITpOeNyDHD+4B9QmLJx6YYtrB1saRsrCLluZ34YaICemNLuumVRP1vSjcdoBtefvGugCOn5nPK7igw0r/vdAvA== dependencies: - "@cspell/cspell-types" "8.7.0" + "@cspell/cspell-types" "8.8.4" -"@cspell/cspell-pipe@8.7.0": - version "8.7.0" - resolved "https://registry.yarnpkg.com/@cspell/cspell-pipe/-/cspell-pipe-8.7.0.tgz#c257288880fdc2d5f1188a4c982bdce1ac46bfb0" - integrity sha512-ePqddIQ4arqPQgOkC146SkZxvZb9/jL7xIM5Igy2n3tiWTC5ijrX/mbHpPZ1VGcFck+1M0cJUuyhuJk+vMj3rg== +"@cspell/cspell-pipe@8.8.4": + version "8.8.4" + resolved "https://registry.yarnpkg.com/@cspell/cspell-pipe/-/cspell-pipe-8.8.4.tgz#ab24c55a4d8eacbb50858fa13259683814504149" + integrity sha512-Uis9iIEcv1zOogXiDVSegm9nzo5NRmsRDsW8CteLRg6PhyZ0nnCY1PZIUy3SbGF0vIcb/M+XsdLSh2wOPqTXww== -"@cspell/cspell-resolver@8.7.0": - version "8.7.0" - resolved "https://registry.yarnpkg.com/@cspell/cspell-resolver/-/cspell-resolver-8.7.0.tgz#4f067853f2a5fb65b766f9121f649a51d6b6b63e" - integrity sha512-grZwDFYqcBYQDaz4AkUtdyqc4UUH2J3/7yWVkBbYDPE+FQHa9ofFXzXxyjs56GJlPfi9ULpe5/Wz6uVLg8rQkQ== +"@cspell/cspell-resolver@8.8.4": + version "8.8.4" + resolved "https://registry.yarnpkg.com/@cspell/cspell-resolver/-/cspell-resolver-8.8.4.tgz#73aeb1a25834a4c083b04aa577646305ecf6fdd0" + integrity sha512-eZVw31nSeh6xKl7TzzkZVMTX/mgwhUw40/q1Sqo7CTPurIBg66oelEqKRclX898jzd2/qSK+ZFwBDxvV7QH38A== dependencies: global-directory "^4.0.1" -"@cspell/cspell-service-bus@8.7.0": - version "8.7.0" - resolved "https://registry.yarnpkg.com/@cspell/cspell-service-bus/-/cspell-service-bus-8.7.0.tgz#3f4f59072305b76e14da51e759ee6652eda37ed4" - integrity sha512-KW48iu0nTDzbedixc7iB7K7mlAZQ7QeMLuM/akxigOlvtOdVJrRa9Pfn44lwejts1ANb/IXil3GH8YylkVi76Q== +"@cspell/cspell-service-bus@8.8.4": + version "8.8.4" + resolved "https://registry.yarnpkg.com/@cspell/cspell-service-bus/-/cspell-service-bus-8.8.4.tgz#bb657b67b79f2676c65e5ee5ac28af149fcb462b" + integrity sha512-KtwJ38uPLrm2Q8osmMIAl2NToA/CMyZCxck4msQJnskdo30IPSdA1Rh0w6zXinmh1eVe0zNEVCeJ2+x23HqW+g== -"@cspell/cspell-types@8.7.0": - version "8.7.0" - resolved "https://registry.yarnpkg.com/@cspell/cspell-types/-/cspell-types-8.7.0.tgz#3f6a2824d7059a45361ef6797ebbdd0ddd6a069f" - integrity sha512-Rb+LCE5I9JEb/LE8nSViVSF8z1CWv/z4mPBIG37VMa7aUx2gAQa6gJekNfpY9YZiMzx4Tv3gDujN80ytks4pGA== +"@cspell/cspell-types@8.8.4": + version "8.8.4" + resolved "https://registry.yarnpkg.com/@cspell/cspell-types/-/cspell-types-8.8.4.tgz#1fb945f50b776456a437d4bf7438cfa14385d936" + integrity sha512-ya9Jl4+lghx2eUuZNY6pcbbrnResgEAomvglhdbEGqy+B5MPEqY5Jt45APEmGqHzTNks7oFFaiTIbXYJAFBR7A== "@cspell/dict-ada@^4.0.2": version "4.0.2" resolved "https://registry.yarnpkg.com/@cspell/dict-ada/-/dict-ada-4.0.2.tgz#8da2216660aeb831a0d9055399a364a01db5805a" integrity sha512-0kENOWQeHjUlfyId/aCM/mKXtkEgV0Zu2RhUXCBr4hHo9F9vph+Uu8Ww2b0i5a4ZixoIkudGA+eJvyxrG1jUpA== -"@cspell/dict-aws@^4.0.1": - version "4.0.1" - resolved "https://registry.yarnpkg.com/@cspell/dict-aws/-/dict-aws-4.0.1.tgz#a0e758531ae81792b928a3f406618296291a658a" - integrity sha512-NXO+kTPQGqaaJKa4kO92NAXoqS+i99dQzf3/L1BxxWVSBS3/k1f3uhmqIh7Crb/n22W793lOm0D9x952BFga3Q== +"@cspell/dict-aws@^4.0.2": + version "4.0.2" + resolved "https://registry.yarnpkg.com/@cspell/dict-aws/-/dict-aws-4.0.2.tgz#6498f1c983c80499054bb31b772aa9562f3aaaed" + integrity sha512-aNGHWSV7dRLTIn8WJemzLoMF62qOaiUQlgnsCwH5fRCD/00gsWCwg106pnbkmK4AyabyxzneOV4dfecDJWkSxw== "@cspell/dict-bash@^4.1.3": version "4.1.3" resolved "https://registry.yarnpkg.com/@cspell/dict-bash/-/dict-bash-4.1.3.tgz#25fba40825ac10083676ab2c777e471c3f71b36e" integrity sha512-tOdI3QVJDbQSwPjUkOiQFhYcu2eedmX/PtEpVWg0aFps/r6AyjUQINtTgpqMYnYuq8O1QUIQqnpx21aovcgZCw== -"@cspell/dict-companies@^3.0.31": - version "3.0.31" - resolved "https://registry.yarnpkg.com/@cspell/dict-companies/-/dict-companies-3.0.31.tgz#f0dacabc5308096c0f12db8a8b802ece604d6bf7" - integrity sha512-hKVpV/lcGKP4/DpEPS8P4osPvFH/YVLJaDn9cBIOH6/HSmL5LbFgJNKpMGaYRbhm2FEX56MKE3yn/MNeNYuesQ== +"@cspell/dict-companies@^3.1.2": + version "3.1.2" + resolved "https://registry.yarnpkg.com/@cspell/dict-companies/-/dict-companies-3.1.2.tgz#b335fe5b8847a23673bc4b964ca584339ca669a2" + integrity sha512-OwR5i1xbYuJX7FtHQySmTy3iJtPV1rZQ3jFCxFGwrA1xRQ4rtRcDQ+sTXBCIAoJHkXa84f9J3zsngOKmMGyS/w== -"@cspell/dict-cpp@^5.1.3": - version "5.1.3" - resolved "https://registry.yarnpkg.com/@cspell/dict-cpp/-/dict-cpp-5.1.3.tgz#c0c34ccdecc3ff954877a56dbbf07a7bf53b218e" - integrity sha512-sqnriXRAInZH9W75C+APBh6dtben9filPqVbIsiRMUXGg+s02ekz0z6LbS7kXeJ5mD2qXoMLBrv13qH2eIwutQ== +"@cspell/dict-cpp@^5.1.8": + version "5.1.9" + resolved "https://registry.yarnpkg.com/@cspell/dict-cpp/-/dict-cpp-5.1.9.tgz#24e5778a184df2a98a64a63326536ada6d6b2342" + integrity sha512-lZmPKn3qfkWQ7tr+yw6JhuhscsyRgRHEOpOd0fhtPt0N154FNsGebGGLW0SOZUuGgW7Nk3lCCwHP85GIemnlqQ== "@cspell/dict-cryptocurrencies@^5.0.0": version "5.0.0" @@ -429,10 +438,10 @@ resolved "https://registry.yarnpkg.com/@cspell/dict-dart/-/dict-dart-2.0.3.tgz#75e7ffe47d5889c2c831af35acdd92ebdbd4cf12" integrity sha512-cLkwo1KT5CJY5N5RJVHks2genFkNCl/WLfj+0fFjqNR+tk3tBI1LY7ldr9piCtSFSm4x9pO1x6IV3kRUY1lLiw== -"@cspell/dict-data-science@^1.0.11": - version "1.0.11" - resolved "https://registry.yarnpkg.com/@cspell/dict-data-science/-/dict-data-science-1.0.11.tgz#4eabba75c21d27253c1114b4fbbade0ead739ffc" - integrity sha512-TaHAZRVe0Zlcc3C23StZqqbzC0NrodRwoSAc8dis+5qLeLLnOCtagYQeROQvDlcDg3X/VVEO9Whh4W/z4PAmYQ== +"@cspell/dict-data-science@^2.0.1": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@cspell/dict-data-science/-/dict-data-science-2.0.1.tgz#ef8040821567786d76c6153ac3e4bc265ca65b59" + integrity sha512-xeutkzK0eBe+LFXOFU2kJeAYO6IuFUc1g7iRLr7HeCmlC4rsdGclwGHh61KmttL3+YHQytYStxaRBdGAXWC8Lw== "@cspell/dict-django@^4.1.0": version "4.1.0" @@ -444,35 +453,35 @@ resolved "https://registry.yarnpkg.com/@cspell/dict-docker/-/dict-docker-1.1.7.tgz#bcf933283fbdfef19c71a642e7e8c38baf9014f2" integrity sha512-XlXHAr822euV36GGsl2J1CkBIVg3fZ6879ZOg5dxTIssuhUOCiV2BuzKZmt6aIFmcdPmR14+9i9Xq+3zuxeX0A== -"@cspell/dict-dotnet@^5.0.0": - version "5.0.0" - resolved "https://registry.yarnpkg.com/@cspell/dict-dotnet/-/dict-dotnet-5.0.0.tgz#13690aafe14b240ad17a30225ac1ec29a5a6a510" - integrity sha512-EOwGd533v47aP5QYV8GlSSKkmM9Eq8P3G/eBzSpH3Nl2+IneDOYOBLEUraHuiCtnOkNsz0xtZHArYhAB2bHWAw== +"@cspell/dict-dotnet@^5.0.2": + version "5.0.2" + resolved "https://registry.yarnpkg.com/@cspell/dict-dotnet/-/dict-dotnet-5.0.2.tgz#d89ca8fa2e546b5e1b1f1288746d26bb627d9f38" + integrity sha512-UD/pO2A2zia/YZJ8Kck/F6YyDSpCMq0YvItpd4YbtDVzPREfTZ48FjZsbYi4Jhzwfvc6o8R56JusAE58P+4sNQ== "@cspell/dict-elixir@^4.0.3": version "4.0.3" resolved "https://registry.yarnpkg.com/@cspell/dict-elixir/-/dict-elixir-4.0.3.tgz#57c25843e46cf3463f97da72d9ef8e37c818296f" integrity sha512-g+uKLWvOp9IEZvrIvBPTr/oaO6619uH/wyqypqvwpmnmpjcfi8+/hqZH8YNKt15oviK8k4CkINIqNhyndG9d9Q== -"@cspell/dict-en-common-misspellings@^2.0.0": - version "2.0.0" - resolved "https://registry.yarnpkg.com/@cspell/dict-en-common-misspellings/-/dict-en-common-misspellings-2.0.0.tgz#708f424d75dc65237a6fcb8d253bc1e7ab641380" - integrity sha512-NOg8dlv37/YqLkCfBs5OXeJm/Wcfb/CzeOmOZJ2ZXRuxwsNuolb4TREUce0yAXRqMhawahY5TSDRJJBgKjBOdw== +"@cspell/dict-en-common-misspellings@^2.0.1": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@cspell/dict-en-common-misspellings/-/dict-en-common-misspellings-2.0.1.tgz#2e472f5128ec38299fc4489638aabdb0d0fb397e" + integrity sha512-uWaP8UG4uvcPyqaG0FzPKCm5kfmhsiiQ45Fs6b3/AEAqfq7Fj1JW0+S3qRt85FQA9SoU6gUJCz9wkK/Ylh7m5A== "@cspell/dict-en-gb@1.1.33": version "1.1.33" resolved "https://registry.yarnpkg.com/@cspell/dict-en-gb/-/dict-en-gb-1.1.33.tgz#7f1fd90fc364a5cb77111b5438fc9fcf9cc6da0e" integrity sha512-tKSSUf9BJEV+GJQAYGw5e+ouhEe2ZXE620S7BLKe3ZmpnjlNG9JqlnaBhkIMxKnNFkLY2BP/EARzw31AZnOv4g== -"@cspell/dict-en_us@^4.3.17": - version "4.3.19" - resolved "https://registry.yarnpkg.com/@cspell/dict-en_us/-/dict-en_us-4.3.19.tgz#ba79bed9cee82fdc9f76d03e85b8f07ea655c322" - integrity sha512-tHcXdkmm0t9LlRct1vgu3+h0KW/wlXCInkTiR4D/rl730q1zu2qVEgiy1saMiTUSNmdu7Hiy+Mhb+1braVqnZQ== +"@cspell/dict-en_us@^4.3.21": + version "4.3.21" + resolved "https://registry.yarnpkg.com/@cspell/dict-en_us/-/dict-en_us-4.3.21.tgz#a8191e3e04d7ea957cac6575c5c2cf98db8ffa8e" + integrity sha512-Bzoo2aS4Pej/MGIFlATpp0wMt9IzVHrhDjdV7FgkAIXbjrOn67ojbTxCgWs8AuCNVfK8lBYGEvs5+ElH1msF8w== -"@cspell/dict-filetypes@^3.0.3": - version "3.0.3" - resolved "https://registry.yarnpkg.com/@cspell/dict-filetypes/-/dict-filetypes-3.0.3.tgz#ab0723ca2f4d3d5674e9c9745efc9f144e49c905" - integrity sha512-J9UP+qwwBLfOQ8Qg9tAsKtSY/WWmjj21uj6zXTI9hRLD1eG1uUOLcfVovAmtmVqUWziPSKMr87F6SXI3xmJXgw== +"@cspell/dict-filetypes@^3.0.4": + version "3.0.4" + resolved "https://registry.yarnpkg.com/@cspell/dict-filetypes/-/dict-filetypes-3.0.4.tgz#aca71c7bb8c8805b54f382d98ded5ec75ebc1e36" + integrity sha512-IBi8eIVdykoGgIv5wQhOURi5lmCNJq0we6DvqKoPQJHthXbgsuO1qrHSiUVydMiQl/XvcnUWTMeAlVUlUClnVg== "@cspell/dict-fonts@^4.0.0": version "4.0.0" @@ -484,10 +493,10 @@ resolved "https://registry.yarnpkg.com/@cspell/dict-fsharp/-/dict-fsharp-1.0.1.tgz#d62c699550a39174f182f23c8c1330a795ab5f53" integrity sha512-23xyPcD+j+NnqOjRHgW3IU7Li912SX9wmeefcY0QxukbAxJ/vAN4rBpjSwwYZeQPAn3fxdfdNZs03fg+UM+4yQ== -"@cspell/dict-fullstack@^3.1.5": - version "3.1.5" - resolved "https://registry.yarnpkg.com/@cspell/dict-fullstack/-/dict-fullstack-3.1.5.tgz#35d18678161f214575cc613dd95564e05422a19c" - integrity sha512-6ppvo1dkXUZ3fbYn/wwzERxCa76RtDDl5Afzv2lijLoijGGUw5yYdLBKJnx8PJBGNLh829X352ftE7BElG4leA== +"@cspell/dict-fullstack@^3.1.8": + version "3.1.8" + resolved "https://registry.yarnpkg.com/@cspell/dict-fullstack/-/dict-fullstack-3.1.8.tgz#1bbfa0a165346f6eff9894cf965bf3ce26552797" + integrity sha512-YRlZupL7uqMCtEBK0bDP9BrcPnjDhz7m4GBqCc1EYqfXauHbLmDT8ELha7T/E7wsFKniHSjzwDZzhNXo2lusRQ== "@cspell/dict-gaming-terms@^1.0.5": version "1.0.5" @@ -499,10 +508,15 @@ resolved "https://registry.yarnpkg.com/@cspell/dict-git/-/dict-git-3.0.0.tgz#c275af86041a2b59a7facce37525e2af05653b95" integrity sha512-simGS/lIiXbEaqJu9E2VPoYW1OTC2xrwPPXNXFMa2uo/50av56qOuaxDrZ5eH1LidFXwoc8HROCHYeKoNrDLSw== -"@cspell/dict-golang@^6.0.5": - version "6.0.5" - resolved "https://registry.yarnpkg.com/@cspell/dict-golang/-/dict-golang-6.0.5.tgz#4dd2e2fda419730a21fb77ade3b90241ad4a5bcc" - integrity sha512-w4mEqGz4/wV+BBljLxduFNkMrd3rstBNDXmoX5kD4UTzIb4Sy0QybWCtg2iVT+R0KWiRRA56QKOvBsgXiddksA== +"@cspell/dict-golang@^6.0.9": + version "6.0.9" + resolved "https://registry.yarnpkg.com/@cspell/dict-golang/-/dict-golang-6.0.9.tgz#b26ee13fb34a8cd40fb22380de8a46b25739fcab" + integrity sha512-etDt2WQauyEQDA+qPS5QtkYTb2I9l5IfQftAllVoB1aOrT6bxxpHvMEpJ0Hsn/vezxrCqa/BmtUbRxllIxIuSg== + +"@cspell/dict-google@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@cspell/dict-google/-/dict-google-1.0.1.tgz#34701471a616011aeaaf480d4834436b6b6b1da5" + integrity sha512-dQr4M3n95uOhtloNSgB9tYYGXGGEGEykkFyRtfcp5pFuEecYUa0BSgtlGKx9RXVtJtKgR+yFT/a5uQSlt8WjqQ== "@cspell/dict-haskell@^4.0.1": version "4.0.1" @@ -520,19 +534,19 @@ integrity sha512-p0brEnRybzSSWi8sGbuVEf7jSTDmXPx7XhQUb5bgG6b54uj+Z0Qf0V2n8b/LWwIPJNd1GygaO9l8k3HTCy1h4w== "@cspell/dict-java@^5.0.6": - version "5.0.6" - resolved "https://registry.yarnpkg.com/@cspell/dict-java/-/dict-java-5.0.6.tgz#2462d6fc15f79ec15eb88ecf875b6ad2a7bf7a6a" - integrity sha512-kdE4AHHHrixyZ5p6zyms1SLoYpaJarPxrz8Tveo6gddszBVVwIUZ+JkQE1bWNLK740GWzIXdkznpUfw1hP9nXw== + version "5.0.7" + resolved "https://registry.yarnpkg.com/@cspell/dict-java/-/dict-java-5.0.7.tgz#c0b32d3c208b6419a5eddd010e87196976be2694" + integrity sha512-ejQ9iJXYIq7R09BScU2y5OUGrSqwcD+J5mHFOKbduuQ5s/Eh/duz45KOzykeMLI6KHPVxhBKpUPBWIsfewECpQ== "@cspell/dict-julia@^1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@cspell/dict-julia/-/dict-julia-1.0.1.tgz#900001417f1c4ea689530adfcc034c848458a0aa" integrity sha512-4JsCLCRhhLMLiaHpmR7zHFjj1qOauzDI5ZzCNQS31TUMfsOo26jAKDfo0jljFAKgw5M2fEG7sKr8IlPpQAYrmQ== -"@cspell/dict-k8s@^1.0.2": - version "1.0.2" - resolved "https://registry.yarnpkg.com/@cspell/dict-k8s/-/dict-k8s-1.0.2.tgz#b19e66f4ac8a4264c0f3981ac6e23e88a60f1c91" - integrity sha512-tLT7gZpNPnGa+IIFvK9SP1LrSpPpJ94a/DulzAPOb1Q2UBFwdpFd82UWhio0RNShduvKG/WiMZf/wGl98pn+VQ== +"@cspell/dict-k8s@^1.0.5": + version "1.0.5" + resolved "https://registry.yarnpkg.com/@cspell/dict-k8s/-/dict-k8s-1.0.5.tgz#4a4011d9f2f3ab628658573c5f16c0e6dbe30c29" + integrity sha512-Cj+/ZV4S+MKlwfocSJZqe/2UAd/sY8YtlZjbK25VN1nCnrsKrBjfkX29vclwSj1U9aJg4Z9jw/uMjoaKu9ZrpQ== "@cspell/dict-latex@^4.0.0": version "4.0.0" @@ -559,37 +573,37 @@ resolved "https://registry.yarnpkg.com/@cspell/dict-monkeyc/-/dict-monkeyc-1.0.6.tgz#042d042fc34a20194c8de032130808f44b241375" integrity sha512-oO8ZDu/FtZ55aq9Mb67HtaCnsLn59xvhO/t2mLLTHAp667hJFxpp7bCtr2zOrR1NELzFXmKln/2lw/PvxMSvrA== -"@cspell/dict-node@^4.0.3": - version "4.0.3" - resolved "https://registry.yarnpkg.com/@cspell/dict-node/-/dict-node-4.0.3.tgz#5ae0222d72871e82978049f8e11ea627ca42fca3" - integrity sha512-sFlUNI5kOogy49KtPg8SMQYirDGIAoKBO3+cDLIwD4MLdsWy1q0upc7pzGht3mrjuyMiPRUV14Bb0rkVLrxOhg== +"@cspell/dict-node@^5.0.1": + version "5.0.1" + resolved "https://registry.yarnpkg.com/@cspell/dict-node/-/dict-node-5.0.1.tgz#77e17c576a897a3391fce01c1cc5da60bb4c2268" + integrity sha512-lax/jGz9h3Dv83v8LHa5G0bf6wm8YVRMzbjJPG/9rp7cAGPtdrga+XANFq+B7bY5+jiSA3zvj10LUFCFjnnCCg== -"@cspell/dict-npm@^5.0.15": - version "5.0.15" - resolved "https://registry.yarnpkg.com/@cspell/dict-npm/-/dict-npm-5.0.15.tgz#c1d1646011fd0eb8ee119b481818a92223c459d1" - integrity sha512-sX0X5YWNW54F4baW7b5JJB6705OCBIZtUqjOghlJNORS5No7QY1IX1zc5FxNNu4gsaCZITAmfMi4ityXEsEThA== +"@cspell/dict-npm@^5.0.16": + version "5.0.16" + resolved "https://registry.yarnpkg.com/@cspell/dict-npm/-/dict-npm-5.0.16.tgz#696883918a9876ffd20d5f975bde74a03d27d80e" + integrity sha512-ZWPnLAziEcSCvV0c8k9Qj88pfMu+wZwM5Qks87ShsfBgI8uLZ9tGHravA7gmjH1Gd7Bgxy2ulvXtSqIWPh1lew== -"@cspell/dict-php@^4.0.6": - version "4.0.6" - resolved "https://registry.yarnpkg.com/@cspell/dict-php/-/dict-php-4.0.6.tgz#fcdee4d850f279b2757eb55c4f69a3a221ac1f7e" - integrity sha512-ySAXisf7twoVFZqBV2o/DKiCLIDTHNqfnj0EfH9OoOUR7HL3rb6zJkm0viLUFDO2G/8SyIi6YrN/6KX+Scjjjg== +"@cspell/dict-php@^4.0.7": + version "4.0.8" + resolved "https://registry.yarnpkg.com/@cspell/dict-php/-/dict-php-4.0.8.tgz#fedce3109dff13a0f3d8d88ba604d6edd2b9fb70" + integrity sha512-TBw3won4MCBQ2wdu7kvgOCR3dY2Tb+LJHgDUpuquy3WnzGiSDJ4AVelrZdE1xu7mjFJUr4q48aB21YT5uQqPZA== -"@cspell/dict-powershell@^5.0.3": - version "5.0.3" - resolved "https://registry.yarnpkg.com/@cspell/dict-powershell/-/dict-powershell-5.0.3.tgz#7bceb4e7db39f87479a6d2af3a033ce26796ae49" - integrity sha512-lEdzrcyau6mgzu1ie98GjOEegwVHvoaWtzQnm1ie4DyZgMr+N6D0Iyj1lzvtmt0snvsDFa5F2bsYzf3IMKcpcA== +"@cspell/dict-powershell@^5.0.4": + version "5.0.4" + resolved "https://registry.yarnpkg.com/@cspell/dict-powershell/-/dict-powershell-5.0.4.tgz#db2bc6a86700a2f829dc1b3b04f6cb3a916fd928" + integrity sha512-eosDShapDgBWN9ULF7+sRNdUtzRnUdsfEdBSchDm8FZA4HOqxUSZy3b/cX/Rdw0Fnw0AKgk0kzgXw7tS6vwJMQ== -"@cspell/dict-public-licenses@^2.0.6": - version "2.0.6" - resolved "https://registry.yarnpkg.com/@cspell/dict-public-licenses/-/dict-public-licenses-2.0.6.tgz#e6ac8e5cb3b0ef8503d67da14435ae86a875b6cc" - integrity sha512-bHqpSpJvLCUcWxj1ov/Ki8WjmESpYwRpQlqfdchekOTc93Huhvjm/RXVN1R4fVf4Hspyem1QVkCGqAmjJMj6sw== +"@cspell/dict-public-licenses@^2.0.7": + version "2.0.7" + resolved "https://registry.yarnpkg.com/@cspell/dict-public-licenses/-/dict-public-licenses-2.0.7.tgz#ccd67a91a6bd5ed4b5117c2f34e9361accebfcb7" + integrity sha512-KlBXuGcN3LE7tQi/GEqKiDewWGGuopiAD0zRK1QilOx5Co8XAvs044gk4MNIQftc8r0nHeUI+irJKLGcR36DIQ== "@cspell/dict-python@^4.1.11": - version "4.1.11" - resolved "https://registry.yarnpkg.com/@cspell/dict-python/-/dict-python-4.1.11.tgz#4e339def01bf468b32d459c46ecb6894970b7eb8" - integrity sha512-XG+v3PumfzUW38huSbfT15Vqt3ihNb462ulfXifpQllPok5OWynhszCLCRQjQReV+dgz784ST4ggRxW452/kVg== + version "4.2.1" + resolved "https://registry.yarnpkg.com/@cspell/dict-python/-/dict-python-4.2.1.tgz#ef0c4cc1b6d096e8ff65faee3fe15eaf6457a92e" + integrity sha512-9X2jRgyM0cxBoFQRo4Zc8oacyWnXi+0/bMI5FGibZNZV4y/o9UoFEr6agjU260/cXHTjIdkX233nN7eb7dtyRg== dependencies: - "@cspell/dict-data-science" "^1.0.11" + "@cspell/dict-data-science" "^2.0.1" "@cspell/dict-r@^2.0.1": version "2.0.1" @@ -601,20 +615,20 @@ resolved "https://registry.yarnpkg.com/@cspell/dict-ruby/-/dict-ruby-5.0.2.tgz#cf1a71380c633dec0857143d3270cb503b10679a" integrity sha512-cIh8KTjpldzFzKGgrqUX4bFyav5lC52hXDKo4LbRuMVncs3zg4hcSf4HtURY+f2AfEZzN6ZKzXafQpThq3dl2g== -"@cspell/dict-rust@^4.0.2": - version "4.0.2" - resolved "https://registry.yarnpkg.com/@cspell/dict-rust/-/dict-rust-4.0.2.tgz#e9111f0105ee6d836a1be8314f47347fd9f8fc3a" - integrity sha512-RhziKDrklzOntxAbY3AvNR58wnFGIo3YS8+dNeLY36GFuWOvXDHFStYw5Pod4f/VXbO/+1tXtywCC4zWfB2p1w== +"@cspell/dict-rust@^4.0.3": + version "4.0.3" + resolved "https://registry.yarnpkg.com/@cspell/dict-rust/-/dict-rust-4.0.3.tgz#ad61939f78bd63a07ae885f429eab24a74ad7f5e" + integrity sha512-8DFCzkFQ+2k3fDaezWc/D+0AyiBBiOGYfSDUfrTNU7wpvUvJ6cRcAUshMI/cn2QW/mmxTspRgVlXsE6GUMz00Q== -"@cspell/dict-scala@^5.0.0": - version "5.0.0" - resolved "https://registry.yarnpkg.com/@cspell/dict-scala/-/dict-scala-5.0.0.tgz#b64365ad559110a36d44ccd90edf7151ea648022" - integrity sha512-ph0twaRoV+ylui022clEO1dZ35QbeEQaKTaV2sPOsdwIokABPIiK09oWwGK9qg7jRGQwVaRPEq0Vp+IG1GpqSQ== +"@cspell/dict-scala@^5.0.2": + version "5.0.2" + resolved "https://registry.yarnpkg.com/@cspell/dict-scala/-/dict-scala-5.0.2.tgz#d732ab24610cc9f6916fb8148f6ef5bdd945fc47" + integrity sha512-v97ClgidZt99JUm7OjhQugDHmhx4U8fcgunHvD/BsXWjXNj4cTr0m0YjofyZoL44WpICsNuFV9F/sv9OM5HUEw== -"@cspell/dict-software-terms@^3.3.18": - version "3.3.20" - resolved "https://registry.yarnpkg.com/@cspell/dict-software-terms/-/dict-software-terms-3.3.20.tgz#ced0152f99228d697ab177b095f242ea73edfad2" - integrity sha512-KmPwCxYWEu7SGyT/0m/n6i6R4ZgxbmN3XcerzA6Z629Wm5iZTVfJaMWqDK2RKAyBawS7OMfxGz0W/wYU4FhJlA== +"@cspell/dict-software-terms@^3.4.1": + version "3.4.3" + resolved "https://registry.yarnpkg.com/@cspell/dict-software-terms/-/dict-software-terms-3.4.3.tgz#145188b9a25916250bfff5ba6e7ee2197ddc6c67" + integrity sha512-3E09j80zFbTkgDyoZc0hVhwVjWsG9iD8kqnHwO/5grsoqJMCdeeEWAL71Uf7+MgDqnKP4N2TwxSBzbTFKIufUQ== "@cspell/dict-sql@^2.1.3": version "2.1.3" @@ -636,27 +650,27 @@ resolved "https://registry.yarnpkg.com/@cspell/dict-terraform/-/dict-terraform-1.0.0.tgz#c7b073bb3a03683f64cc70ccaa55ce9742c46086" integrity sha512-Ak+vy4HP/bOgzf06BAMC30+ZvL9mzv21xLM2XtfnBLTDJGdxlk/nK0U6QT8VfFLqJ0ZZSpyOxGsUebWDCTr/zQ== -"@cspell/dict-typescript@^3.1.2": - version "3.1.4" - resolved "https://registry.yarnpkg.com/@cspell/dict-typescript/-/dict-typescript-3.1.4.tgz#65a7d4a00f17ad61300864e17ae3d2bcf2c2d57d" - integrity sha512-jUcPa0rsPca5ur1+G56DXnSc5hbbJkzvPHHvyQtkbPXBQd3CXPMNfrTVCgzex/7cY/7FONcpFCUwgwfni9Jqbw== +"@cspell/dict-typescript@^3.1.5": + version "3.1.5" + resolved "https://registry.yarnpkg.com/@cspell/dict-typescript/-/dict-typescript-3.1.5.tgz#15bd74651fb2cf0eff1150f07afee9543206bfab" + integrity sha512-EkIwwNV/xqEoBPJml2S16RXj65h1kvly8dfDLgXerrKw6puybZdvAHerAph6/uPTYdtLcsPyJYkPt5ISOJYrtw== "@cspell/dict-vue@^3.0.0": version "3.0.0" resolved "https://registry.yarnpkg.com/@cspell/dict-vue/-/dict-vue-3.0.0.tgz#68ccb432ad93fcb0fd665352d075ae9a64ea9250" integrity sha512-niiEMPWPV9IeRBRzZ0TBZmNnkK3olkOPYxC1Ny2AX4TGlYRajcW0WUtoSHmvvjZNfWLSg2L6ruiBeuPSbjnG6A== -"@cspell/dynamic-import@8.7.0": - version "8.7.0" - resolved "https://registry.yarnpkg.com/@cspell/dynamic-import/-/dynamic-import-8.7.0.tgz#5a758d63e080686459d4116a5e89c591038ad02a" - integrity sha512-xlEPdiHVDu+4xYkvwjL9MgklxOi9XB+Pr1H9s3Ww9WEq+q6BA3xOHxLIU/k8mhqFTMZGFZRCsdy/EwMu6SyRhQ== +"@cspell/dynamic-import@8.8.4": + version "8.8.4" + resolved "https://registry.yarnpkg.com/@cspell/dynamic-import/-/dynamic-import-8.8.4.tgz#895b30da156daa7dde9c153ea9ca7c707541edbf" + integrity sha512-tseSxrybznkmsmPaAB4aoHB9wr8Q2fOMIy3dm+yQv+U1xj+JHTN9OnUvy9sKiq0p3DQGWm/VylgSgsYaXrEHKQ== dependencies: - import-meta-resolve "^4.0.0" + import-meta-resolve "^4.1.0" -"@cspell/strong-weak-map@8.7.0": - version "8.7.0" - resolved "https://registry.yarnpkg.com/@cspell/strong-weak-map/-/strong-weak-map-8.7.0.tgz#f003680002c59f44aa63f223ae1056ffe5f51875" - integrity sha512-0bo0WwDr2lzGoCP7vbpWbDpPyuOrHKK+218txnUpx6Pn1EDBLfcDQsiZED5B6zlpwgbGi6y3vc0rWtJbjKvwzg== +"@cspell/strong-weak-map@8.8.4": + version "8.8.4" + resolved "https://registry.yarnpkg.com/@cspell/strong-weak-map/-/strong-weak-map-8.8.4.tgz#1040b09b5fcbd81eba0430d98580b3caf0825b2a" + integrity sha512-gticEJGR6yyGeLjf+mJ0jZotWYRLVQ+J0v1VpsR1nKnXTRJY15BWXgEA/ifbU/+clpyCek79NiCIXCvmP1WT4A== "@cspotcode/source-map-support@^0.8.0": version "0.8.1" @@ -687,9 +701,9 @@ eslint-visitor-keys "^3.3.0" "@eslint-community/regexpp@^4.5.1", "@eslint-community/regexpp@^4.6.1": - version "4.10.0" - resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.10.0.tgz#548f6de556857c8bb73bbee70c35dc82a2e74d63" - integrity sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA== + version "4.10.1" + resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.10.1.tgz#361461e5cb3845d874e61731c11cfedd664d83a0" + integrity sha512-Zm2NGpWELsQAD1xsJzGQpYfvICSsFkEpU0jxBjfdC6uNEWXcHnfs9hScFWtXVDVl+rBQJGrl4g1vcKIejpH9dA== "@eslint/eslintrc@^2.1.4": version "2.1.4" @@ -986,6 +1000,47 @@ "@jridgewell/resolve-uri" "^3.1.0" "@jridgewell/sourcemap-codec" "^1.4.14" +"@jscpd/core@4.0.1": + version "4.0.1" + resolved "https://registry.yarnpkg.com/@jscpd/core/-/core-4.0.1.tgz#fea15894749409499fa3069ebcd77306f5c3afff" + integrity sha512-6Migc68Z8p7q5xqW1wbF3SfIbYHPQoiLHPbJb1A1Z1H9DwImwopFkYflqRDpuamLd0Jfg2jx3ZBmHQt21NbD1g== + dependencies: + eventemitter3 "^5.0.1" + +"@jscpd/finder@4.0.1": + version "4.0.1" + resolved "https://registry.yarnpkg.com/@jscpd/finder/-/finder-4.0.1.tgz#a413cd57c92d4645d08b395cbbbbd4936abfd060" + integrity sha512-TcCT28686GeLl87EUmrBXYmuOFELVMDwyjKkcId+qjNS1zVWRd53Xd5xKwEDzkCEgen/vCs+lorLLToolXp5oQ== + dependencies: + "@jscpd/core" "4.0.1" + "@jscpd/tokenizer" "4.0.1" + blamer "^1.0.6" + bytes "^3.1.2" + cli-table3 "^0.6.5" + colors "^1.4.0" + fast-glob "^3.3.2" + fs-extra "^11.2.0" + markdown-table "^2.0.0" + pug "^3.0.3" + +"@jscpd/html-reporter@4.0.1": + version "4.0.1" + resolved "https://registry.yarnpkg.com/@jscpd/html-reporter/-/html-reporter-4.0.1.tgz#f1db556e4345b57a6f1d4b75bd14e36592c9629f" + integrity sha512-M9fFETNvXXuy4fWv0M2oMluxwrQUBtubxCHaWw21lb2G8A6SE19moe3dUkluZ/3V4BccywfeF9lSEUg84heLww== + dependencies: + colors "1.4.0" + fs-extra "^11.2.0" + pug "^3.0.3" + +"@jscpd/tokenizer@4.0.1": + version "4.0.1" + resolved "https://registry.yarnpkg.com/@jscpd/tokenizer/-/tokenizer-4.0.1.tgz#033bb7e7e84758c819876ee54173ee5149f4ad11" + integrity sha512-l/CPeEigadYcQUsUxf1wdCBfNjyAxYcQU04KciFNmSZAMY+ykJ8fZsiuyfjb+oOuDgsIPZZ9YvbvsCr6NBXueg== + dependencies: + "@jscpd/core" "4.0.1" + reprism "^0.0.11" + spark-md5 "^3.0.2" + "@leichtgewicht/ip-codec@^2.0.1": version "2.0.5" resolved "https://registry.yarnpkg.com/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz#4fc56c15c580b9adb7dc3c333a134e540b44bfb1" @@ -1103,9 +1158,9 @@ "@babel/types" "^7.0.0" "@types/babel__traverse@*", "@types/babel__traverse@^7.0.6": - version "7.20.5" - resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.20.5.tgz#7b7502be0aa80cc4ef22978846b983edaafcd4dd" - integrity sha512-WXCyOcRtH37HAUkpXhUduaxdm82b4GSlyTqajXviN4EfiuPgNYR109xMCKvpl6zPIpua0DGlMEDCq+g8EdoheQ== + version "7.20.6" + resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.20.6.tgz#8dc9f0ae0f202c08d8d4dab648912c8d6038e3f7" + integrity sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg== dependencies: "@babel/types" "^7.20.7" @@ -1166,9 +1221,9 @@ integrity sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw== "@types/express-serve-static-core@*", "@types/express-serve-static-core@^4.17.33": - version "4.19.0" - resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.19.0.tgz#3ae8ab3767d98d0b682cda063c3339e1e86ccfaa" - integrity sha512-bGyep3JqPCRry1wq+O5n7oiBgGWmeIJXPjXXCo8EK0u8duZGSYar7cGqd3ML2JUsLGeB7fmc06KYo9fLGWqPvQ== + version "4.19.3" + resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.19.3.tgz#e469a13e4186c9e1c0418fb17be8bc8ff1b19a7a" + integrity sha512-KOzM7MhcBFlmnlr/fzISFF5vGWVSvN6fTd4T+ExOt08bA/dA5kpSzY52nMsI1KDFmUREpJelPYyuslLRSjjgCg== dependencies: "@types/node" "*" "@types/qs" "*" @@ -1223,7 +1278,7 @@ dependencies: "@types/istanbul-lib-report" "*" -"@types/jest@^29.5.0": +"@types/jest@^29.5.12": version "29.5.12" resolved "https://registry.yarnpkg.com/@types/jest/-/jest-29.5.12.tgz#7f7dc6eb4cf246d2474ed78744b05d06ce025544" integrity sha512-eDC8bTvT/QhYdxJAulQikueigY5AsdBRH2yDKW3yveW7svY3+DzN84/2NUgkw10RTiJbWqZrTtoGVdYlvFJdLw== @@ -1258,16 +1313,16 @@ "@types/node" "*" "@types/node@*": - version "20.12.8" - resolved "https://registry.yarnpkg.com/@types/node/-/node-20.12.8.tgz#35897bf2bfe3469847ab04634636de09552e8256" - integrity sha512-NU0rJLJnshZWdE/097cdCBbyW1h4hEg0xpovcoAQYHl8dnEyp/NAOiE45pvc+Bd1Dt+2r94v2eGFpQJ4R7g+2w== + version "20.14.2" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.14.2.tgz#a5f4d2bcb4b6a87bffcaa717718c5a0f208f4a18" + integrity sha512-xyu6WAMVwv6AKFLB+e/7ySZVr/0zLCzOa7rSpq6jNwpqOrUbcACDWC+53d4n2QHOnDou0fbIsg8wZu/sxrnI4Q== dependencies: undici-types "~5.26.4" "@types/node@^18.15.5": - version "18.19.31" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.19.31.tgz#b7d4a00f7cb826b60a543cebdbda5d189aaecdcd" - integrity sha512-ArgCD39YpyyrtFKIqMDvjz79jto5fcI/SVUs2HwB+f0dAzq68yqOdyaSivLiLugSziTpNXLQrVb7RZFmdZzbhA== + version "18.19.34" + resolved "https://registry.yarnpkg.com/@types/node/-/node-18.19.34.tgz#c3fae2bbbdb94b4a52fe2d229d0dccce02ef3d27" + integrity sha512-eXF4pfBNV5DAMKGbI02NnDtWrQ40hAN558/2vvS4gMpMIxaf6JmD7YjnZbq0Q9TDSSkKBamime8ewRoomHdt4g== dependencies: undici-types "~5.26.4" @@ -1299,9 +1354,9 @@ "@types/react" "*" "@types/react@*", "@types/react@^18.0.28": - version "18.3.1" - resolved "https://registry.yarnpkg.com/@types/react/-/react-18.3.1.tgz#fed43985caa834a2084d002e4771e15dfcbdbe8e" - integrity sha512-V0kuGBX3+prX+DQ/7r2qsv1NsdfnCLnTgnRJ1pYnxykBhGMz+qj+box5lq7XsO5mtZsBqpjwwTu/7wszPfMBcw== + version "18.3.3" + resolved "https://registry.yarnpkg.com/@types/react/-/react-18.3.3.tgz#9679020895318b0915d7a3ab004d92d33375c45f" + integrity sha512-hti/R0pS0q1/xx+TsI73XIqk26eBsISZ2R0wUijXIngRK9R/e7Xw/cXVxQK7R5JjW+SV4zGcn5hXjudkN/pLIw== dependencies: "@types/prop-types" "*" csstype "^3.0.2" @@ -1311,6 +1366,11 @@ resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.12.0.tgz#2b35eccfcee7d38cd72ad99232fbd58bffb3c84d" integrity sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA== +"@types/sarif@^2.1.4": + version "2.1.7" + resolved "https://registry.yarnpkg.com/@types/sarif/-/sarif-2.1.7.tgz#dab4d16ba7568e9846c454a8764f33c5d98e5524" + integrity sha512-kRz0VEkJqWLf1LLVN4pT1cg1Z9wAuvI6L97V3m2f5B76Tg8d413ddvLBPTEHAZJlnn4XSvu0FkZtViCQGVyrXQ== + "@types/semver@^7.5.0": version "7.5.8" resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.8.tgz#8268a8c57a3e4abd25c165ecd36237db7948a55e" @@ -1661,6 +1721,11 @@ acorn-walk@^8.0.2, acorn-walk@^8.1.1: resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.3.2.tgz#7703af9415f1b6db9315d6895503862e231d34aa" integrity sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A== +acorn@^7.1.1: + version "7.4.1" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa" + integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== + acorn@^8.1.0, acorn@^8.4.1, acorn@^8.7.1, acorn@^8.8.1, acorn@^8.8.2, acorn@^8.9.0: version "8.11.3" resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.11.3.tgz#71e0b14e13a4ec160724b38fb7b0f233b1b81d7a" @@ -1703,9 +1768,9 @@ ajv@^6.12.4, ajv@^6.12.5: uri-js "^4.2.2" ajv@^8.0.0, ajv@^8.9.0: - version "8.13.0" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.13.0.tgz#a3939eaec9fb80d217ddf0c3376948c023f28c91" - integrity sha512-PRA911Blj99jR5RMeTunVbNXMF6Lp4vZXnk5GQjcnUWUTsrXtekg/pnmFFI2u/I36Y/2bITGS30GZCXei6uNkA== + version "8.16.0" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.16.0.tgz#22e2a92b94f005f7e0f9c9d39652ef0b8f6f0cb4" + integrity sha512-F0twR8U1ZU67JIEtekUcLkXkoO5mMMmgGD8sK/xUFzJ805jxHQl92hImFAqqXMyMYjSPOyUPAwHYhB72g5sTXw== dependencies: fast-deep-equal "^3.1.3" json-schema-traverse "^1.0.0" @@ -1836,6 +1901,16 @@ array-union@^2.1.0: resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== +asap@~2.0.3: + version "2.0.6" + resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46" + integrity sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA== + +assert-never@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/assert-never/-/assert-never-1.2.1.tgz#11f0e363bf146205fb08193b5c7b90f4d1cf44fe" + integrity sha512-TaTivMB6pYI1kXwrFlEhLeGfOqoDNdTxjCdwRfFFkEA30Eu+k48W34nlok2EYWJfFFzqaEmichdNM7th6M5HNw== + asynckit@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" @@ -1901,6 +1976,13 @@ babel-preset-jest@^29.6.3: babel-plugin-jest-hoist "^29.6.3" babel-preset-current-node-syntax "^1.0.0" +babel-walk@3.0.0-canary-5: + version "3.0.0-canary-5" + resolved "https://registry.yarnpkg.com/babel-walk/-/babel-walk-3.0.0-canary-5.tgz#f66ecd7298357aee44955f235a6ef54219104b11" + integrity sha512-GAwkz0AihzY5bkwIY5QDR+LvsRQgB/B+1foMPvi0FZPMl5fjD7ICiznUiBdLYMH1QYe6vqu4gWYytZOccLouFw== + dependencies: + "@babel/types" "^7.9.6" + balanced-match@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" @@ -1921,6 +2003,14 @@ binary-extensions@^2.0.0: resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.3.0.tgz#f6e14a97858d327252200242d4ccfe522c445522" integrity sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw== +blamer@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/blamer/-/blamer-1.0.6.tgz#653fd72ab396efe180bae65d24919a8eda841944" + integrity sha512-fv7QToPS87oD1m1bDDTf29zC/bVKJxj2Nqh1r/v4NhMtbnzDIbWOHBYIfxCjlmkVGu3FGOjKgdNG3SFm7TkvBQ== + dependencies: + execa "^4.0.0" + which "^2.0.2" + body-parser@1.20.2: version "1.20.2" resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.2.tgz#6feb0e21c4724d06de7ff38da36dad4f57a747fd" @@ -1962,22 +2052,22 @@ brace-expansion@^2.0.1: dependencies: balanced-match "^1.0.0" -braces@^3.0.2, braces@~3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" - integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== +braces@^3.0.3, braces@~3.0.2: + version "3.0.3" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" + integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== dependencies: - fill-range "^7.0.1" + fill-range "^7.1.1" browserslist@^4.21.10, browserslist@^4.22.2: - version "4.23.0" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.23.0.tgz#8f3acc2bbe73af7213399430890f86c63a5674ab" - integrity sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ== + version "4.23.1" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.23.1.tgz#ce4af0534b3d37db5c1a4ca98b9080f985041e96" + integrity sha512-TUfofFo/KsK/bWZ9TWQ5O26tsWW4Uhmt8IYklbnUa70udB6P2wA7w7o4PY4muaEPBQaAX+CEnmmIA41NVHtPVw== dependencies: - caniuse-lite "^1.0.30001587" - electron-to-chromium "^1.4.668" + caniuse-lite "^1.0.30001629" + electron-to-chromium "^1.4.796" node-releases "^2.0.14" - update-browserslist-db "^1.0.13" + update-browserslist-db "^1.0.16" bs-logger@0.x: version "0.2.6" @@ -2020,12 +2110,12 @@ bytes@3.0.0: resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048" integrity sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw== -bytes@3.1.2: +bytes@3.1.2, bytes@^3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== -call-bind@^1.0.7: +call-bind@^1.0.2, call-bind@^1.0.7: version "1.0.7" resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.7.tgz#06016599c40c56498c18769d2730be242b6fa3b9" integrity sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w== @@ -2051,10 +2141,10 @@ camelcase@^6.2.0: resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a" integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== -caniuse-lite@^1.0.30001587: - version "1.0.30001614" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001614.tgz#f894b4209376a0bf923d67d9c361d96b1dfebe39" - integrity sha512-jmZQ1VpmlRwHgdP1/uiKzgiAuGOfLEJsYFP4+GBou/QQ4U6IOJCB4NP1c+1p9RGLpwObcT94jA5/uO+F1vBbog== +caniuse-lite@^1.0.30001629: + version "1.0.30001629" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001629.tgz#907a36f4669031bd8a1a8dbc2fa08b29e0db297e" + integrity sha512-c3dl911slnQhmxUIT4HhYzT7wnBK/XYpGnYLOj4nJBaRiw52Ibe7YxlDaAeRECvA786zCuExhxIUJ2K7nHMrBw== canvas@^2.10.2: version "2.11.2" @@ -2072,11 +2162,6 @@ chalk-template@^1.1.0: dependencies: chalk "^5.2.0" -chalk@5.3.0, chalk@^5.2.0, chalk@^5.3.0: - version "5.3.0" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-5.3.0.tgz#67c20a7ebef70e7f3970a01f90fa210cb6860385" - integrity sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w== - chalk@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" @@ -2105,11 +2190,23 @@ chalk@^4.0.0, chalk@^4.1.0, chalk@^4.1.2: ansi-styles "^4.1.0" supports-color "^7.1.0" +chalk@^5.2.0, chalk@^5.3.0, chalk@~5.3.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-5.3.0.tgz#67c20a7ebef70e7f3970a01f90fa210cb6860385" + integrity sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w== + char-regex@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/char-regex/-/char-regex-1.0.2.tgz#d744358226217f981ed58f479b1d6bcc29545dcf" integrity sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw== +character-parser@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/character-parser/-/character-parser-2.2.0.tgz#c7ce28f36d4bcd9744e5ffc2c5fcde1c73261fc0" + integrity sha512-+UqJQjFEFaTAs3bNsF2j2kEN1baG/zghZbdqoYEDxGZtJo9LBzl1A+m0D4n3qKx8N2FNv8/Xp6yV9mQmBuptaw== + dependencies: + is-regex "^1.0.3" + chokidar@^3.5.3: version "3.6.0" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b" @@ -2131,9 +2228,9 @@ chownr@^2.0.0: integrity sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ== chrome-trace-event@^1.0.2: - version "1.0.3" - resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz#1015eced4741e15d06664a957dbbf50d041e26ac" - integrity sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg== + version "1.0.4" + resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz#05bffd7ff928465093314708c93bdfa9bd1f0f5b" + integrity sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ== ci-info@^3.2.0, ci-info@^3.8.0: version "3.9.0" @@ -2178,6 +2275,15 @@ cli-cursor@^4.0.0: dependencies: restore-cursor "^4.0.0" +cli-table3@^0.6.5: + version "0.6.5" + resolved "https://registry.yarnpkg.com/cli-table3/-/cli-table3-0.6.5.tgz#013b91351762739c16a9567c21a04632e449bf2f" + integrity sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ== + dependencies: + string-width "^4.2.0" + optionalDependencies: + "@colors/colors" "1.5.0" + cli-truncate@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/cli-truncate/-/cli-truncate-4.0.0.tgz#6cc28a2924fee9e25ce91e973db56c7066e6172a" @@ -2248,6 +2354,11 @@ colorette@^2.0.10, colorette@^2.0.14, colorette@^2.0.20: resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.20.tgz#9eb793e6833067f7235902fcd3b09917a000a95a" integrity sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w== +colors@1.4.0, colors@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/colors/-/colors-1.4.0.tgz#c50491479d4c1bdaed2c9ced32cf7c7dc2360f78" + integrity sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA== + combined-stream@^1.0.8: version "1.0.8" resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" @@ -2255,26 +2366,26 @@ combined-stream@^1.0.8: dependencies: delayed-stream "~1.0.0" -commander@11.1.0: - version "11.1.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-11.1.0.tgz#62fdce76006a68e5c1ab3314dc92e800eb83d906" - integrity sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ== - commander@^10.0.1: version "10.0.1" resolved "https://registry.yarnpkg.com/commander/-/commander-10.0.1.tgz#881ee46b4f77d1c1dccc5823433aa39b022cbe06" integrity sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug== -commander@^12.0.0: - version "12.0.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-12.0.0.tgz#b929db6df8546080adfd004ab215ed48cf6f2592" - integrity sha512-MwVNWlYjDTtOjX5PiD7o5pK0UrFU/OYgcJfjjK4RaHZETNtjJqrZa9Y9ds88+A+f+d5lv+561eZ+yCKoS3gbAA== +commander@^12.1.0, commander@~12.1.0: + version "12.1.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-12.1.0.tgz#01423b36f501259fdaac4d0e4d60c96c991585d3" + integrity sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA== commander@^2.12.1, commander@^2.20.0: version "2.20.3" resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== +commander@^5.0.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-5.1.0.tgz#46abbd1652f8e059bddaef99bbdcb2ad9cf179ae" + integrity sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg== + comment-json@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/comment-json/-/comment-json-4.2.3.tgz#50b487ebbf43abe44431f575ebda07d30d015365" @@ -2321,17 +2432,6 @@ concat-map@0.0.1: resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== -configstore@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/configstore/-/configstore-6.0.0.tgz#49eca2ebc80983f77e09394a1a56e0aca8235566" - integrity sha512-cD31W1v3GqUlQvbBCGcXmd2Nj9SvLDOP1oQ0YFuLETufzSPaKp11rYBsSOm7rCsW3OnIRAFM3OxRhceaXNYHkA== - dependencies: - dot-prop "^6.0.1" - graceful-fs "^4.2.6" - unique-string "^3.0.0" - write-file-atomic "^3.0.3" - xdg-basedir "^5.0.1" - connect-history-api-fallback@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz#647264845251a0daf25b97ce87834cace0f5f1c8" @@ -2342,6 +2442,14 @@ console-control-strings@^1.0.0, console-control-strings@^1.1.0: resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" integrity sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ== +constantinople@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/constantinople/-/constantinople-4.0.1.tgz#0def113fa0e4dc8de83331a5cf79c8b325213151" + integrity sha512-vCrqcSIq4//Gx74TXXCGnHpulY1dskqLTFGDmhrGxzeXL8lF8kvXv6mpNWlJj1uD4DW23D4ljAqbY4RRaaUZIw== + dependencies: + "@babel/parser" "^7.6.0" + "@babel/types" "^7.6.1" + content-disposition@0.5.4: version "0.5.4" resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe" @@ -2402,7 +2510,7 @@ create-require@^1.1.0: resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== -cross-spawn@^7.0.2, cross-spawn@^7.0.3: +cross-spawn@^7.0.0, cross-spawn@^7.0.2, cross-spawn@^7.0.3: version "7.0.3" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== @@ -2411,120 +2519,114 @@ cross-spawn@^7.0.2, cross-spawn@^7.0.3: shebang-command "^2.0.0" which "^2.0.1" -crypto-random-string@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-4.0.0.tgz#5a3cc53d7dd86183df5da0312816ceeeb5bb1fc2" - integrity sha512-x8dy3RnvYdlUcPOjkEHqozhiwzKNSq7GcPuXFbnyMOCHxX8V3OgIg/pYuabl2sbUPfIJaeAQB7PMOK8DFIdoRA== - dependencies: - type-fest "^1.0.1" - -cspell-config-lib@8.7.0: - version "8.7.0" - resolved "https://registry.yarnpkg.com/cspell-config-lib/-/cspell-config-lib-8.7.0.tgz#84bb9312ca923e5e3493fe49cdb4049dda01c9a4" - integrity sha512-depsd01GbLBo71/tfRrL5iECWQLS4CjCxA9C01dVkFAJqVB0s+K9KLKjTlq5aHOhcvo9Z3dHV+bGQCf5/Q7bfw== +cspell-config-lib@8.8.4: + version "8.8.4" + resolved "https://registry.yarnpkg.com/cspell-config-lib/-/cspell-config-lib-8.8.4.tgz#72cb7052e5c9afe0627860719ac86852f409c4f7" + integrity sha512-Xf+aL669Cm+MYZTZULVWRQXB7sRWx9qs0hPrgqxeaWabLUISK57/qwcI24TPVdYakUCoud9Nv+woGi5FcqV5ZQ== dependencies: - "@cspell/cspell-types" "8.7.0" + "@cspell/cspell-types" "8.8.4" comment-json "^4.2.3" - yaml "^2.4.1" + yaml "^2.4.3" -cspell-dictionary@8.7.0: - version "8.7.0" - resolved "https://registry.yarnpkg.com/cspell-dictionary/-/cspell-dictionary-8.7.0.tgz#b18a315046e0e09971ef35787c67e7e422eb2318" - integrity sha512-S6IpZSzIMxlOO/33NgCOuP0TPH2mZbw8d5CP44z5jajflloq8l74MeJLkeDzYfCRcm0Rtk0A5drBeMg+Ai34OA== +cspell-dictionary@8.8.4: + version "8.8.4" + resolved "https://registry.yarnpkg.com/cspell-dictionary/-/cspell-dictionary-8.8.4.tgz#9db953707abcccc5177073ae298141944566baf7" + integrity sha512-eDi61MDDZycS5EASz5FiYKJykLEyBT0mCvkYEUCsGVoqw8T9gWuWybwwqde3CMq9TOwns5pxGcFs2v9RYgtN5A== dependencies: - "@cspell/cspell-pipe" "8.7.0" - "@cspell/cspell-types" "8.7.0" - cspell-trie-lib "8.7.0" + "@cspell/cspell-pipe" "8.8.4" + "@cspell/cspell-types" "8.8.4" + cspell-trie-lib "8.8.4" fast-equals "^5.0.1" gensequence "^7.0.0" -cspell-gitignore@8.7.0: - version "8.7.0" - resolved "https://registry.yarnpkg.com/cspell-gitignore/-/cspell-gitignore-8.7.0.tgz#6eb3e0c5ec2c909b090e278808d5d1b1dee3b2c3" - integrity sha512-yvUZ86qyopUpDgn+YXP1qTpUe/lp65ZFvpMtw21lWHTFlg1OWKntr349EQU/5ben/K6koxk1FiElCBV7Lr4uFg== +cspell-gitignore@8.8.4: + version "8.8.4" + resolved "https://registry.yarnpkg.com/cspell-gitignore/-/cspell-gitignore-8.8.4.tgz#6762c9fb7d7cadb007659174efaeb448357cc924" + integrity sha512-rLdxpBh0kp0scwqNBZaWVnxEVmSK3UWyVSZmyEL4jmmjusHYM9IggfedOhO4EfGCIdQ32j21TevE0tTslyc4iA== dependencies: - cspell-glob "8.7.0" + cspell-glob "8.8.4" find-up-simple "^1.0.0" -cspell-glob@8.7.0: - version "8.7.0" - resolved "https://registry.yarnpkg.com/cspell-glob/-/cspell-glob-8.7.0.tgz#3face24a29634c4492cbfe4553e05571de808b82" - integrity sha512-AMdfx0gvROA/aIL8t8b5Y5NtMgscGZELFj6WhCSZiQSuWRxXUKiLGGLUFjx2y0hgXN9LUYOo6aBjvhnxI/v71g== +cspell-glob@8.8.4: + version "8.8.4" + resolved "https://registry.yarnpkg.com/cspell-glob/-/cspell-glob-8.8.4.tgz#b10af55ff306b9ad5114c8a2c54414f3f218d47a" + integrity sha512-+tRrOfTSbF/44uNl4idMZVPNfNM6WTmra4ZL44nx23iw1ikNhqZ+m0PC1oCVSlURNBEn8faFXjC/oT2BfgxoUQ== dependencies: - micromatch "^4.0.5" + micromatch "^4.0.7" -cspell-grammar@8.7.0: - version "8.7.0" - resolved "https://registry.yarnpkg.com/cspell-grammar/-/cspell-grammar-8.7.0.tgz#c634324ae19e9f17e1130cc9c364f67ac6e6b8ec" - integrity sha512-SGcXc7322wU2WNRi7vtpToWDXTqZHhxqvR+aIXHT2kkxlMSWp3Rvfpshd0ckgY54nZtgw7R/JtKND2jeACRpwQ== +cspell-grammar@8.8.4: + version "8.8.4" + resolved "https://registry.yarnpkg.com/cspell-grammar/-/cspell-grammar-8.8.4.tgz#91212b7210d9bf9c2fd21d604c589ca87a90a261" + integrity sha512-UxDO517iW6vs/8l4OhLpdMR7Bp+tkquvtld1gWz8WYQiDwORyf0v5a3nMh4ILYZGoolOSnDuI9UjWOLI6L/vvQ== dependencies: - "@cspell/cspell-pipe" "8.7.0" - "@cspell/cspell-types" "8.7.0" + "@cspell/cspell-pipe" "8.8.4" + "@cspell/cspell-types" "8.8.4" -cspell-io@8.7.0: - version "8.7.0" - resolved "https://registry.yarnpkg.com/cspell-io/-/cspell-io-8.7.0.tgz#443c85a6a3a7c51840d5265d95eface6ea18944e" - integrity sha512-o7OltyyvVkRG1gQrIqGpN5pUkHNnv6rvihb7Qu6cJ8jITinLGuWJuEQpgt0eF5yIr624jDbFwSzAxsFox8riQg== +cspell-io@8.8.4: + version "8.8.4" + resolved "https://registry.yarnpkg.com/cspell-io/-/cspell-io-8.8.4.tgz#a970ed76f06aebc9b64a1591024a4a854c7eb8c1" + integrity sha512-aqB/QMx+xns46QSyPEqi05uguCSxvqRnh2S/ZOhhjPlKma/7hK9niPRcwKwJXJEtNzdiZZkkC1uZt9aJe/7FTA== dependencies: - "@cspell/cspell-service-bus" "8.7.0" + "@cspell/cspell-service-bus" "8.8.4" -cspell-lib@8.7.0: - version "8.7.0" - resolved "https://registry.yarnpkg.com/cspell-lib/-/cspell-lib-8.7.0.tgz#7affbbebed9229a58034149d179eb255ea50233e" - integrity sha512-qDSHZGekwiDmouYRECTQokE+hgAuPqREm+Hb+G3DoIo3ZK5H47TtEUo8fNCw22XsKefcF8X28LiyoZwiYHVpSg== +cspell-lib@8.8.4: + version "8.8.4" + resolved "https://registry.yarnpkg.com/cspell-lib/-/cspell-lib-8.8.4.tgz#3af88990585a7e6a5f03bbf738b4434587e94cce" + integrity sha512-hK8gYtdQ9Lh86c8cEHITt5SaoJbfvXoY/wtpR4k393YR+eAxKziyv8ihQyFE/Z/FwuqtNvDrSntP9NLwTivd3g== dependencies: - "@cspell/cspell-bundled-dicts" "8.7.0" - "@cspell/cspell-pipe" "8.7.0" - "@cspell/cspell-resolver" "8.7.0" - "@cspell/cspell-types" "8.7.0" - "@cspell/dynamic-import" "8.7.0" - "@cspell/strong-weak-map" "8.7.0" + "@cspell/cspell-bundled-dicts" "8.8.4" + "@cspell/cspell-pipe" "8.8.4" + "@cspell/cspell-resolver" "8.8.4" + "@cspell/cspell-types" "8.8.4" + "@cspell/dynamic-import" "8.8.4" + "@cspell/strong-weak-map" "8.8.4" clear-module "^4.1.2" comment-json "^4.2.3" - configstore "^6.0.0" - cspell-config-lib "8.7.0" - cspell-dictionary "8.7.0" - cspell-glob "8.7.0" - cspell-grammar "8.7.0" - cspell-io "8.7.0" - cspell-trie-lib "8.7.0" + cspell-config-lib "8.8.4" + cspell-dictionary "8.8.4" + cspell-glob "8.8.4" + cspell-grammar "8.8.4" + cspell-io "8.8.4" + cspell-trie-lib "8.8.4" + env-paths "^3.0.0" fast-equals "^5.0.1" gensequence "^7.0.0" import-fresh "^3.3.0" resolve-from "^5.0.0" vscode-languageserver-textdocument "^1.0.11" vscode-uri "^3.0.8" + xdg-basedir "^5.1.0" -cspell-trie-lib@8.7.0: - version "8.7.0" - resolved "https://registry.yarnpkg.com/cspell-trie-lib/-/cspell-trie-lib-8.7.0.tgz#792382fae9773e260e01d2f31e9dba03af73c858" - integrity sha512-W3Nh2cO7gMV91r+hLqyTMgKlvRl4W5diKs5YiyOxjZumRkMBy42IzcNYtgIIacOxghklv96F5Bd1Vx/zY6ylGA== +cspell-trie-lib@8.8.4: + version "8.8.4" + resolved "https://registry.yarnpkg.com/cspell-trie-lib/-/cspell-trie-lib-8.8.4.tgz#99cc2a733cda3816646b2e7793bde581f9205f8b" + integrity sha512-yCld4ZL+pFa5DL+Arfvmkv3cCQUOfdRlxElOzdkRZqWyO6h/UmO8xZb21ixVYHiqhJGZmwc3BG9Xuw4go+RLig== dependencies: - "@cspell/cspell-pipe" "8.7.0" - "@cspell/cspell-types" "8.7.0" + "@cspell/cspell-pipe" "8.8.4" + "@cspell/cspell-types" "8.8.4" gensequence "^7.0.0" cspell@^8.3.2: - version "8.7.0" - resolved "https://registry.yarnpkg.com/cspell/-/cspell-8.7.0.tgz#41797a9f3ab07a83bcf463499b690ab0985eb0ff" - integrity sha512-77nRPgLl240C6FK8RKVKo34lP15Lzp/6bk+SKYJFwUKKXlcgWXDis+Lw4JolA741/JgHtuxmhW1C8P7dCKjJ3w== - dependencies: - "@cspell/cspell-json-reporter" "8.7.0" - "@cspell/cspell-pipe" "8.7.0" - "@cspell/cspell-types" "8.7.0" - "@cspell/dynamic-import" "8.7.0" + version "8.8.4" + resolved "https://registry.yarnpkg.com/cspell/-/cspell-8.8.4.tgz#7881d8e400c33a180ba01447c0413348a2e835d3" + integrity sha512-eRUHiXvh4iRapw3lqE1nGOEAyYVfa/0lgK/e34SpcM/ECm4QuvbfY7Yl0ozCbiYywecog0RVbeJJUEYJTN5/Mg== + dependencies: + "@cspell/cspell-json-reporter" "8.8.4" + "@cspell/cspell-pipe" "8.8.4" + "@cspell/cspell-types" "8.8.4" + "@cspell/dynamic-import" "8.8.4" chalk "^5.3.0" chalk-template "^1.1.0" - commander "^12.0.0" - cspell-gitignore "8.7.0" - cspell-glob "8.7.0" - cspell-io "8.7.0" - cspell-lib "8.7.0" + commander "^12.1.0" + cspell-gitignore "8.8.4" + cspell-glob "8.8.4" + cspell-io "8.8.4" + cspell-lib "8.8.4" fast-glob "^3.3.2" fast-json-stable-stringify "^2.1.0" file-entry-cache "^8.0.0" get-stdin "^9.0.0" - semver "^7.6.0" + semver "^7.6.2" strip-ansi "^7.1.0" vscode-uri "^3.0.8" @@ -2574,10 +2676,10 @@ debug@2.6.9: dependencies: ms "2.0.0" -debug@4, debug@4.3.4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4: - version "4.3.4" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" - integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== +debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4, debug@~4.3.4: + version "4.3.5" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.5.tgz#e83444eceb9fedd4a1da56d671ae2446a01a6e1e" + integrity sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg== dependencies: ms "2.1.2" @@ -2705,6 +2807,11 @@ doctrine@^3.0.0: dependencies: esutils "^2.0.2" +doctypes@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/doctypes/-/doctypes-1.1.0.tgz#ea80b106a87538774e8a3a4a5afe293de489e0a9" + integrity sha512-LLBi6pEqS6Do3EKQ3J0NqHWV5hhb78Pi8vvESYwyOy2c31ZEZVdtitdzsQsKb7878PEERhzUk0ftqGhG6Mz+pQ== + domexception@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/domexception/-/domexception-4.0.0.tgz#4ad1be56ccadc86fc76d033353999a8037d03673" @@ -2712,22 +2819,23 @@ domexception@^4.0.0: dependencies: webidl-conversions "^7.0.0" -dot-prop@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-6.0.1.tgz#fc26b3cf142b9e59b74dbd39ed66ce620c681083" - integrity sha512-tE7ztYzXHIeyvc7N+hR3oi7FIbf/NIjVP9hmAt3yMXzrQ072/fpjGLx2GxNxGxUl5V73MEqYzioOMoVhGMJ5cA== +dts-bundle-generator@^9.5.1: + version "9.5.1" + resolved "https://registry.yarnpkg.com/dts-bundle-generator/-/dts-bundle-generator-9.5.1.tgz#7eac7f47a2d5b51bdaf581843e7f969b88bfc225" + integrity sha512-DxpJOb2FNnEyOzMkG11sxO2dmxPjthoVWxfKqWYJ/bI/rT1rvTMktF5EKjAYrRZu6Z6t3NhOUZ0sZ5ZXevOfbA== dependencies: - is-obj "^2.0.0" + typescript ">=5.0.2" + yargs "^17.6.0" ee-first@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== -electron-to-chromium@^1.4.668: - version "1.4.753" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.753.tgz#1e9850081cbf732d669310ef8685bef0b5f5b8dd" - integrity sha512-Wn1XKa0Lc5kMe5UIwQc4+i5lhhBggF0l82C1bE3oOMASt4JVqdOyRIVc8mh0kiuL5CCptqwQJBmFbaPglLrN0Q== +electron-to-chromium@^1.4.796: + version "1.4.796" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.796.tgz#48dd6ff634b7f7df6313bd27aaa713f3af4a2b29" + integrity sha512-NglN/xprcM+SHD2XCli4oC6bWe6kHoytcyLKCWXmRL854F0qhPhaYgUswUsglnPxYaNQIg2uMY4BvaomIf3kLA== emittery@^0.13.1: version "0.13.1" @@ -2754,10 +2862,17 @@ encodeurl@~1.0.2: resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== +end-of-stream@^1.1.0: + version "1.4.4" + resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" + integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== + dependencies: + once "^1.4.0" + enhanced-resolve@^5.0.0, enhanced-resolve@^5.16.0: - version "5.16.0" - resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.16.0.tgz#65ec88778083056cb32487faa9aef82ed0864787" - integrity sha512-O+QWCviPNSSLAD9Ucn8Awv+poAkqn3T1XY5/N7kR7rQO9yfSGWkYZDwpJ+iKF7B8rxaQKWngSqACpgzeapSyoA== + version "5.17.0" + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.17.0.tgz#d037603789dd9555b89aaec7eb78845c49089bc5" + integrity sha512-dwDPwZL0dmye8Txp2gzFmA6sxALaSvdRDjPH0viLcKrtlOL3tw62nWWweVD1SdILDTJrbrL6tdWVN58Wo6U3eA== dependencies: graceful-fs "^4.2.4" tapable "^2.2.0" @@ -2767,6 +2882,11 @@ entities@^4.4.0: resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48" integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw== +env-paths@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-3.0.0.tgz#2f1e89c2f6dbd3408e1b1711dd82d62e317f58da" + integrity sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A== + envinfo@^7.7.3: version "7.13.0" resolved "https://registry.yarnpkg.com/envinfo/-/envinfo-7.13.0.tgz#81fbb81e5da35d74e814941aeab7c325a606fb31" @@ -2792,11 +2912,11 @@ es-errors@^1.3.0: integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== es-module-lexer@^1.2.1: - version "1.5.2" - resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.5.2.tgz#00b423304f2500ac59359cc9b6844951f372d497" - integrity sha512-l60ETUTmLqbVbVHv1J4/qj+M8nq7AwMzEcg3kmJDt9dCNrTk+yHcYFf/Kw75pMDwd9mPcIGCG5LcS20SxYRzFA== + version "1.5.3" + resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.5.3.tgz#25969419de9c0b1fbe54279789023e8a9a788412" + integrity sha512-i1gCgmR9dCl6Vil6UKPI/trA69s08g/syhiDK9TG0Nf1RJjjFI+AzoWW7sPufzkgYAn861skuCwJa0pIIHYxvg== -es5-ext@^0.10.35, es5-ext@^0.10.46, es5-ext@^0.10.62, es5-ext@^0.10.64, es5-ext@~0.10.14, es5-ext@~0.10.2, es5-ext@~0.10.46: +es5-ext@^0.10.35, es5-ext@^0.10.46, es5-ext@^0.10.62, es5-ext@^0.10.64, es5-ext@~0.10.14, es5-ext@~0.10.2: version "0.10.64" resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.64.tgz#12e4ffb48f1ba2ea777f1fcdd1918ef73ea21714" integrity sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg== @@ -3073,20 +3193,20 @@ events@^3.2.0: resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== -execa@8.0.1: - version "8.0.1" - resolved "https://registry.yarnpkg.com/execa/-/execa-8.0.1.tgz#51f6a5943b580f963c3ca9c6321796db8cc39b8c" - integrity sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg== +execa@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/execa/-/execa-4.1.0.tgz#4e5491ad1572f2f17a77d388c6c857135b22847a" + integrity sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA== dependencies: - cross-spawn "^7.0.3" - get-stream "^8.0.1" - human-signals "^5.0.0" - is-stream "^3.0.0" + cross-spawn "^7.0.0" + get-stream "^5.0.0" + human-signals "^1.1.1" + is-stream "^2.0.0" merge-stream "^2.0.0" - npm-run-path "^5.1.0" - onetime "^6.0.0" - signal-exit "^4.1.0" - strip-final-newline "^3.0.0" + npm-run-path "^4.0.0" + onetime "^5.1.0" + signal-exit "^3.0.2" + strip-final-newline "^2.0.0" execa@^5.0.0: version "5.1.1" @@ -3103,6 +3223,21 @@ execa@^5.0.0: signal-exit "^3.0.3" strip-final-newline "^2.0.0" +execa@~8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/execa/-/execa-8.0.1.tgz#51f6a5943b580f963c3ca9c6321796db8cc39b8c" + integrity sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg== + dependencies: + cross-spawn "^7.0.3" + get-stream "^8.0.1" + human-signals "^5.0.0" + is-stream "^3.0.0" + merge-stream "^2.0.0" + npm-run-path "^5.1.0" + onetime "^6.0.0" + signal-exit "^4.1.0" + strip-final-newline "^3.0.0" + exit@^0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c" @@ -3239,10 +3374,10 @@ file-entry-cache@^8.0.0: dependencies: flat-cache "^4.0.0" -fill-range@^7.0.1: - version "7.0.1" - resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" - integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== +fill-range@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292" + integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg== dependencies: to-regex-range "^5.0.1" @@ -3358,6 +3493,15 @@ fs-extra@^10.0.0: jsonfile "^6.0.1" universalify "^2.0.0" +fs-extra@^11.2.0: + version "11.2.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-11.2.0.tgz#e70e17dfad64232287d01929399e0ea7c86b0e5b" + integrity sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw== + dependencies: + graceful-fs "^4.2.0" + jsonfile "^6.0.1" + universalify "^2.0.0" + fs-minipass@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-2.1.0.tgz#7f5036fdbf12c63c169190cbe4199c852271f9fb" @@ -3441,6 +3585,13 @@ get-stdin@^9.0.0: resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-9.0.0.tgz#3983ff82e03d56f1b2ea0d3e60325f39d703a575" integrity sha512-dVKBjfWisLAicarI2Sf+JuBE/DghV4UzNAVe9yhEJuzeREd3JhOTE9cUaJTeSa77fsbQUK3pcOpJfM59+VKZaA== +get-stream@^5.0.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-5.2.0.tgz#4966a1795ee5ace65e706c4b7beb71257d6e22d3" + integrity sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA== + dependencies: + pump "^3.0.0" + get-stream@^6.0.0: version "6.0.1" resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7" @@ -3451,6 +3602,11 @@ get-stream@^8.0.1: resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-8.0.1.tgz#def9dfd71742cd7754a7761ed43749a27d02eca2" integrity sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA== +gitignore-to-glob@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/gitignore-to-glob/-/gitignore-to-glob-0.3.0.tgz#59f32ab3d9b66ce50299c3ed24cb0ef42a094ceb" + integrity sha512-mk74BdnK7lIwDHnotHddx1wsjMOFIThpLY3cPNniJ/2fA/tlLzHnFxIdR+4sLOu5KGgQJdij4kjJ2RoUNnCNMA== + glob-parent@^5.1.2, glob-parent@~5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" @@ -3574,6 +3730,13 @@ has-symbols@^1.0.3: resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== +has-tostringtag@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz#2cdc42d40bef2e5b4eeab7c01a73c54ce7ab5abc" + integrity sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw== + dependencies: + has-symbols "^1.0.3" + has-unicode@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" @@ -3686,6 +3849,11 @@ https-proxy-agent@^5.0.0, https-proxy-agent@^5.0.1: agent-base "6" debug "4" +human-signals@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-1.1.1.tgz#c5b1cd14f50aeae09ab6c59fe63ba3395fe4dfa3" + integrity sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw== + human-signals@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" @@ -3736,7 +3904,7 @@ import-local@^3.0.2: pkg-dir "^4.2.0" resolve-cwd "^3.0.0" -import-meta-resolve@^4.0.0: +import-meta-resolve@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/import-meta-resolve/-/import-meta-resolve-4.1.0.tgz#f9db8bead9fafa61adb811db77a2bf22c5399706" integrity sha512-I6fiaX09Xivtk+THaMfAwnA3MVA5Big1WHF1Dfx9hFuvNIWpXnorlkzhcQf6ehrqQiiZECRt1poOAkPmer3ruw== @@ -3820,6 +3988,14 @@ is-docker@^2.0.0, is-docker@^2.1.1: resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.2.1.tgz#33eeabe23cfe86f14bde4408a02c0cfb853acdaa" integrity sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ== +is-expression@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/is-expression/-/is-expression-4.0.0.tgz#c33155962abf21d0afd2552514d67d2ec16fd2ab" + integrity sha512-zMIXX63sxzG3XrkHkrAPvm/OVZVSCPNkwMHU8oTX7/U3AL78I0QXCEICXUM13BIa8TYGZ68PiTKfQz3yaTNr4A== + dependencies: + acorn "^7.1.1" + object-assign "^4.1.1" + is-extglob@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" @@ -3859,11 +4035,6 @@ is-number@^7.0.0: resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== -is-obj@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-2.0.0.tgz#473fb05d973705e3fd9620545018ca8e22ef4982" - integrity sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w== - is-path-inside@^3.0.3: version "3.0.3" resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283" @@ -3886,11 +4057,19 @@ is-potential-custom-element-name@^1.0.1: resolved "https://registry.yarnpkg.com/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz#171ed6f19e3ac554394edf78caa05784a45bebb5" integrity sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ== -is-promise@^2.2.2: +is-promise@^2.0.0, is-promise@^2.2.2: version "2.2.2" resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-2.2.2.tgz#39ab959ccbf9a774cf079f7b40c7a26f763135f1" integrity sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ== +is-regex@^1.0.3: + version "1.1.4" + resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.4.tgz#eef5663cd59fa4c0ae339505323df6854bb15958" + integrity sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg== + dependencies: + call-bind "^1.0.2" + has-tostringtag "^1.0.0" + is-stream@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077" @@ -3901,11 +4080,6 @@ is-stream@^3.0.0: resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-3.0.0.tgz#e6bfd7aa6bef69f4f472ce9bb681e3e57b4319ac" integrity sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA== -is-typedarray@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" - integrity sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA== - is-wsl@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-2.2.0.tgz#74a4c76e77ca9fd3f932f290c17ea326cd157271" @@ -4375,6 +4549,11 @@ jest@^29.7.0: import-local "^3.0.2" jest-cli "^29.7.0" +js-stringify@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/js-stringify/-/js-stringify-1.0.2.tgz#1736fddfd9724f28a3682adc6230ae7e4e9679db" + integrity sha512-rtS5ATOo2Q5k1G+DADISilDA6lv79zIiwFd6CcjuIxGKLFm5C+RLImRscVap9k55i+MOZwgliw+NejvkLuGD5g== + "js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" @@ -4395,6 +4574,30 @@ js-yaml@^4.1.0: dependencies: argparse "^2.0.1" +jscpd-sarif-reporter@4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/jscpd-sarif-reporter/-/jscpd-sarif-reporter-4.0.3.tgz#b6637fde6b40ac9bcd91fe67aebc0b95bbe96468" + integrity sha512-0T7KiWiDIVArvlBkvCorn2NFwQe7p7DJ37o4YFRuPLDpcr1jNHQlEfbFPw8hDdgJ4hpfby6A5YwyHqASKJ7drA== + dependencies: + colors "^1.4.0" + fs-extra "^11.2.0" + node-sarif-builder "^2.0.3" + +jscpd@^4.0.1: + version "4.0.4" + resolved "https://registry.yarnpkg.com/jscpd/-/jscpd-4.0.4.tgz#53ffcf5d77215c525953433cc13e55420ef0157e" + integrity sha512-tmcB7uQPYzdIwc03Z7ngWCD3vrJ96B88kaAh86f9dQ7dz1Cikj29t9Lu8kzFf1NIyhdm1MMP8HHIAXUx0L9EhQ== + dependencies: + "@jscpd/core" "4.0.1" + "@jscpd/finder" "4.0.1" + "@jscpd/html-reporter" "4.0.1" + "@jscpd/tokenizer" "4.0.1" + colors "^1.4.0" + commander "^5.0.0" + fs-extra "^11.2.0" + gitignore-to-glob "^0.3.0" + jscpd-sarif-reporter "4.0.3" + jsdoc-type-pratt-parser@~4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/jsdoc-type-pratt-parser/-/jsdoc-type-pratt-parser-4.0.0.tgz#136f0571a99c184d84ec84662c45c29ceff71114" @@ -4484,7 +4687,7 @@ json5@^1.0.1: dependencies: minimist "^1.2.0" -json5@^2.1.2, json5@^2.2.3: +json5@^2.1.2, json5@^2.2.2, json5@^2.2.3: version "2.2.3" resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== @@ -4503,6 +4706,14 @@ jsonfile@^6.0.1: optionalDependencies: graceful-fs "^4.1.6" +jstransformer@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/jstransformer/-/jstransformer-1.0.0.tgz#ed8bf0921e2f3f1ed4d5c1a44f68709ed24722c3" + integrity sha512-C9YK3Rf8q6VAPDCCU9fnqo3mAfOH6vUGnMcP4AQAYIEpWtfGLpwOTmZ+igtdK5y+VvI2n3CyYSzy4Qh34eq24A== + dependencies: + is-promise "^2.0.0" + promise "^7.0.1" + keyv@^4.5.3, keyv@^4.5.4: version "4.5.4" resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93" @@ -4541,10 +4752,10 @@ levn@^0.4.1: prelude-ls "^1.2.1" type-check "~0.4.0" -lilconfig@3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-3.0.0.tgz#f8067feb033b5b74dab4602a5f5029420be749bc" - integrity sha512-K2U4W2Ff5ibV7j7ydLr+zLAkIg5JJ4lPn1Ltsdt+Tz/IjQ8buJ55pZAxoP34lqIiwtF9iAvtLv3JGv7CAyAg+g== +lilconfig@~3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-3.1.1.tgz#9d8a246fa753106cfc205fd2d77042faca56e5e3" + integrity sha512-O18pf7nyvHTckunPWCV1XUNXU1piu01y2b7ATJ0ppkUkk8ocqVWBrYjJBCwHDjD/ZWcfyrA0P4gKhzWGi5EINQ== lines-and-columns@^1.1.6: version "1.2.4" @@ -4552,31 +4763,31 @@ lines-and-columns@^1.1.6: integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== lint-staged@^15.0.1: - version "15.2.2" - resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-15.2.2.tgz#ad7cbb5b3ab70e043fa05bff82a09ed286bc4c5f" - integrity sha512-TiTt93OPh1OZOsb5B7k96A/ATl2AjIZo+vnzFZ6oHK5FuTk63ByDtxGQpHm+kFETjEWqgkF95M8FRXKR/LEBcw== - dependencies: - chalk "5.3.0" - commander "11.1.0" - debug "4.3.4" - execa "8.0.1" - lilconfig "3.0.0" - listr2 "8.0.1" - micromatch "4.0.5" - pidtree "0.6.0" - string-argv "0.3.2" - yaml "2.3.4" - -listr2@8.0.1: - version "8.0.1" - resolved "https://registry.yarnpkg.com/listr2/-/listr2-8.0.1.tgz#4d3f50ae6cec3c62bdf0e94f5c2c9edebd4b9c34" - integrity sha512-ovJXBXkKGfq+CwmKTjluEqFi3p4h8xvkxGQQAQan22YCgef4KZ1mKGjzfGh6PL6AW5Csw0QiQPNuQyH+6Xk3hA== + version "15.2.5" + resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-15.2.5.tgz#8c342f211bdb34ffd3efd1311248fa6b50b43b50" + integrity sha512-j+DfX7W9YUvdzEZl3Rk47FhDF6xwDBV5wwsCPw6BwWZVPYJemusQmvb9bRsW23Sqsaa+vRloAWogbK4BUuU2zA== + dependencies: + chalk "~5.3.0" + commander "~12.1.0" + debug "~4.3.4" + execa "~8.0.1" + lilconfig "~3.1.1" + listr2 "~8.2.1" + micromatch "~4.0.7" + pidtree "~0.6.0" + string-argv "~0.3.2" + yaml "~2.4.2" + +listr2@~8.2.1: + version "8.2.1" + resolved "https://registry.yarnpkg.com/listr2/-/listr2-8.2.1.tgz#06a1a6efe85f23c5324180d7c1ddbd96b5eefd6d" + integrity sha512-irTfvpib/rNiD637xeevjO2l3Z5loZmuaRi0L0YE5LfijwVY96oyVn0DFD3o/teAok7nfobMG1THvvcHh/BP6g== dependencies: cli-truncate "^4.0.0" colorette "^2.0.20" eventemitter3 "^5.0.1" log-update "^6.0.0" - rfdc "^1.3.0" + rfdc "^1.3.1" wrap-ansi "^9.0.0" loader-runner@^4.1.0, loader-runner@^4.2.0: @@ -4669,13 +4880,6 @@ lru-cache@^5.1.1: dependencies: yallist "^3.0.2" -lru-cache@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" - integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== - dependencies: - yallist "^4.0.0" - lru-queue@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/lru-queue/-/lru-queue-0.1.0.tgz#2738bd9f0d3cf4f84490c5736c48699ac632cda3" @@ -4714,6 +4918,13 @@ makeerror@1.0.12: dependencies: tmpl "1.0.5" +markdown-table@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/markdown-table/-/markdown-table-2.0.0.tgz#194a90ced26d31fe753d8b9434430214c011865b" + integrity sha512-Ezda85ToJUBhM6WGaG6veasyym+Tbs3cMAw/ZhOPqXiYsr0jgocBV3j3nx+4lk47plLlIqjwuTm/ywVI+zjJ/A== + dependencies: + repeat-string "^1.0.0" + marked@^4.3.0: version "4.3.0" resolved "https://registry.yarnpkg.com/marked/-/marked-4.3.0.tgz#796362821b019f734054582038b116481b456cf3" @@ -4765,12 +4976,12 @@ methods@~1.1.2: resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w== -micromatch@4.0.5, micromatch@^4.0.0, micromatch@^4.0.2, micromatch@^4.0.4, micromatch@^4.0.5: - version "4.0.5" - resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6" - integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA== +micromatch@^4.0.0, micromatch@^4.0.2, micromatch@^4.0.4, micromatch@^4.0.7, micromatch@~4.0.7: + version "4.0.7" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.7.tgz#33e8190d9fe474a9895525f5618eee136d46c2e5" + integrity sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q== dependencies: - braces "^3.0.2" + braces "^3.0.3" picomatch "^2.3.1" mime-db@1.52.0, "mime-db@>= 1.43.0 < 2": @@ -4921,7 +5132,7 @@ neo-async@^2.6.2: resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== -next-tick@1, next-tick@^1.1.0: +next-tick@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.1.0.tgz#1836ee30ad56d67ef281b22bd199f709449b35eb" integrity sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ== @@ -4944,9 +5155,9 @@ node-forge@^1: integrity sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA== node-gyp-build@^4.3.0: - version "4.8.0" - resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.8.0.tgz#3fee9c1731df4581a3f9ead74664369ff00d26dd" - integrity sha512-u6fs2AEUljNho3EYTJNBfImO5QTo/J/1Etd+NVdCj7qWKUSN/bSLkZwhDv7I+w/MSC6qJ4cknepkAYykDdK8og== + version "4.8.1" + resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.8.1.tgz#976d3ad905e71b76086f4f0b0d3637fe79b6cda5" + integrity sha512-OSs33Z9yWr148JZcbZd5WiAXhh/n9z8TxQcdMhIOlpN9AhWpLfvVFO73+m77bBABQMaY9XSvIa+qk0jlI7Gcaw== node-int64@^0.4.0: version "0.4.0" @@ -4958,6 +5169,14 @@ node-releases@^2.0.14: resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.14.tgz#2ffb053bceb8b2be8495ece1ab6ce600c4461b0b" integrity sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw== +node-sarif-builder@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/node-sarif-builder/-/node-sarif-builder-2.0.3.tgz#179ae590ce020f97f9e45037dc1cde85aa4398ec" + integrity sha512-Pzr3rol8fvhG/oJjIq2NTVB0vmdNNlz22FENhhPojYRZ4/ee08CfK4YuKmuL54V9MLhI1kpzxfOJ/63LzmZzDg== + dependencies: + "@types/sarif" "^2.1.4" + fs-extra "^10.0.0" + nopt@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/nopt/-/nopt-5.0.0.tgz#530942bb58a512fccafe53fe210f13a25355dc88" @@ -4980,7 +5199,7 @@ normalize-path@^3.0.0, normalize-path@~3.0.0: resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== -npm-run-path@^4.0.1: +npm-run-path@^4.0.0, npm-run-path@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea" integrity sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw== @@ -5005,9 +5224,9 @@ npmlog@^5.0.1: set-blocking "^2.0.0" nwsapi@^2.2.2: - version "2.2.9" - resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.9.tgz#7f3303218372db2e9f27c27766bcfc59ae7e61c6" - integrity sha512-2f3F0SEEer8bBu0dsNCFF50N0cTThV1nWFYcEYFZttdW0lDAoybv9cQoK7X7/68Z89S7FoRrVjP1LPX4XRf9vg== + version "2.2.10" + resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.10.tgz#0b77a68e21a0b483db70b11fad055906e867cda8" + integrity sha512-QK0sRs7MKv0tKe1+5uZIQk/C8XGza4DAnztJG8iD+TpJIORARrCxczA738awHrZoHeTjSSoHqao2teO0dC/gFQ== object-assign@^4.0.1, object-assign@^4.1.1: version "4.1.1" @@ -5036,7 +5255,7 @@ on-headers@~1.0.2: resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.2.tgz#772b0ae6aaa525c399e489adfad90c403eb3c28f" integrity sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA== -once@^1.3.0, once@^1.3.1: +once@^1.3.0, once@^1.3.1, once@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== @@ -5190,17 +5409,17 @@ path-type@^4.0.0: resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== -picocolors@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" - integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== +picocolors@^1.0.0, picocolors@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.1.tgz#a8ad579b571952f0e5d25892de5445bcfe25aaa1" + integrity sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew== picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.3, picomatch@^2.3.1: version "2.3.1" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== -pidtree@0.6.0: +pidtree@~0.6.0: version "0.6.0" resolved "https://registry.yarnpkg.com/pidtree/-/pidtree-0.6.0.tgz#90ad7b6d42d5841e69e0a2419ef38f8883aa057c" integrity sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g== @@ -5253,9 +5472,9 @@ prettier-linter-helpers@^1.0.0: fast-diff "^1.1.2" prettier@^3.0.1, prettier@^3.1.1: - version "3.2.5" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.2.5.tgz#e52bc3090586e824964a8813b09aba6233b28368" - integrity sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A== + version "3.3.1" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.3.1.tgz#e68935518dd90bb7ec4821ba970e68f8de16e1ac" + integrity sha512-7CAwy5dRsxs8PHXT3twixW9/OEll8MLE0VRPCJyl7CkS6VHGPSlsVaWTiASPTyGyYRyApxlaWTzwUxVNrhcwDg== pretty-format@^29.0.0, pretty-format@^29.7.0: version "29.7.0" @@ -5271,6 +5490,13 @@ process-nextick-args@~2.0.0: resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== +promise@^7.0.1: + version "7.3.1" + resolved "https://registry.yarnpkg.com/promise/-/promise-7.3.1.tgz#064b72602b18f90f29192b8b1bc418ffd1ebd3bf" + integrity sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg== + dependencies: + asap "~2.0.3" + prompts@^2.0.1: version "2.4.2" resolved "https://registry.yarnpkg.com/prompts/-/prompts-2.4.2.tgz#7b57e73b3a48029ad10ebd44f74b01722a4cb069" @@ -5292,6 +5518,117 @@ psl@^1.1.33: resolved "https://registry.yarnpkg.com/psl/-/psl-1.9.0.tgz#d0df2a137f00794565fcaf3b2c00cd09f8d5a5a7" integrity sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag== +pug-attrs@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/pug-attrs/-/pug-attrs-3.0.0.tgz#b10451e0348165e31fad1cc23ebddd9dc7347c41" + integrity sha512-azINV9dUtzPMFQktvTXciNAfAuVh/L/JCl0vtPCwvOA21uZrC08K/UnmrL+SXGEVc1FwzjW62+xw5S/uaLj6cA== + dependencies: + constantinople "^4.0.1" + js-stringify "^1.0.2" + pug-runtime "^3.0.0" + +pug-code-gen@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/pug-code-gen/-/pug-code-gen-3.0.3.tgz#58133178cb423fe1716aece1c1da392a75251520" + integrity sha512-cYQg0JW0w32Ux+XTeZnBEeuWrAY7/HNE6TWnhiHGnnRYlCgyAUPoyh9KzCMa9WhcJlJ1AtQqpEYHc+vbCzA+Aw== + dependencies: + constantinople "^4.0.1" + doctypes "^1.1.0" + js-stringify "^1.0.2" + pug-attrs "^3.0.0" + pug-error "^2.1.0" + pug-runtime "^3.0.1" + void-elements "^3.1.0" + with "^7.0.0" + +pug-error@^2.0.0, pug-error@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/pug-error/-/pug-error-2.1.0.tgz#17ea37b587b6443d4b8f148374ec27b54b406e55" + integrity sha512-lv7sU9e5Jk8IeUheHata6/UThZ7RK2jnaaNztxfPYUY+VxZyk/ePVaNZ/vwmH8WqGvDz3LrNYt/+gA55NDg6Pg== + +pug-filters@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/pug-filters/-/pug-filters-4.0.0.tgz#d3e49af5ba8472e9b7a66d980e707ce9d2cc9b5e" + integrity sha512-yeNFtq5Yxmfz0f9z2rMXGw/8/4i1cCFecw/Q7+D0V2DdtII5UvqE12VaZ2AY7ri6o5RNXiweGH79OCq+2RQU4A== + dependencies: + constantinople "^4.0.1" + jstransformer "1.0.0" + pug-error "^2.0.0" + pug-walk "^2.0.0" + resolve "^1.15.1" + +pug-lexer@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/pug-lexer/-/pug-lexer-5.0.1.tgz#ae44628c5bef9b190b665683b288ca9024b8b0d5" + integrity sha512-0I6C62+keXlZPZkOJeVam9aBLVP2EnbeDw3An+k0/QlqdwH6rv8284nko14Na7c0TtqtogfWXcRoFE4O4Ff20w== + dependencies: + character-parser "^2.2.0" + is-expression "^4.0.0" + pug-error "^2.0.0" + +pug-linker@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/pug-linker/-/pug-linker-4.0.0.tgz#12cbc0594fc5a3e06b9fc59e6f93c146962a7708" + integrity sha512-gjD1yzp0yxbQqnzBAdlhbgoJL5qIFJw78juN1NpTLt/mfPJ5VgC4BvkoD3G23qKzJtIIXBbcCt6FioLSFLOHdw== + dependencies: + pug-error "^2.0.0" + pug-walk "^2.0.0" + +pug-load@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/pug-load/-/pug-load-3.0.0.tgz#9fd9cda52202b08adb11d25681fb9f34bd41b662" + integrity sha512-OCjTEnhLWZBvS4zni/WUMjH2YSUosnsmjGBB1An7CsKQarYSWQ0GCVyd4eQPMFJqZ8w9xgs01QdiZXKVjk92EQ== + dependencies: + object-assign "^4.1.1" + pug-walk "^2.0.0" + +pug-parser@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/pug-parser/-/pug-parser-6.0.0.tgz#a8fdc035863a95b2c1dc5ebf4ecf80b4e76a1260" + integrity sha512-ukiYM/9cH6Cml+AOl5kETtM9NR3WulyVP2y4HOU45DyMim1IeP/OOiyEWRr6qk5I5klpsBnbuHpwKmTx6WURnw== + dependencies: + pug-error "^2.0.0" + token-stream "1.0.0" + +pug-runtime@^3.0.0, pug-runtime@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/pug-runtime/-/pug-runtime-3.0.1.tgz#f636976204723f35a8c5f6fad6acda2a191b83d7" + integrity sha512-L50zbvrQ35TkpHwv0G6aLSuueDRwc/97XdY8kL3tOT0FmhgG7UypU3VztfV/LATAvmUfYi4wNxSajhSAeNN+Kg== + +pug-strip-comments@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/pug-strip-comments/-/pug-strip-comments-2.0.0.tgz#f94b07fd6b495523330f490a7f554b4ff876303e" + integrity sha512-zo8DsDpH7eTkPHCXFeAk1xZXJbyoTfdPlNR0bK7rpOMuhBYb0f5qUVCO1xlsitYd3w5FQTK7zpNVKb3rZoUrrQ== + dependencies: + pug-error "^2.0.0" + +pug-walk@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/pug-walk/-/pug-walk-2.0.0.tgz#417aabc29232bb4499b5b5069a2b2d2a24d5f5fe" + integrity sha512-yYELe9Q5q9IQhuvqsZNwA5hfPkMJ8u92bQLIMcsMxf/VADjNtEYptU+inlufAFYcWdHlwNfZOEnOOQrZrcyJCQ== + +pug@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/pug/-/pug-3.0.3.tgz#e18324a314cd022883b1e0372b8af3a1a99f7597" + integrity sha512-uBi6kmc9f3SZ3PXxqcHiUZLmIXgfgWooKWXcwSGwQd2Zi5Rb0bT14+8CJjJgI8AB+nndLaNgHGrcc6bPIB665g== + dependencies: + pug-code-gen "^3.0.3" + pug-filters "^4.0.0" + pug-lexer "^5.0.1" + pug-linker "^4.0.0" + pug-load "^3.0.0" + pug-parser "^6.0.0" + pug-runtime "^3.0.1" + pug-strip-comments "^2.0.0" + +pump@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64" + integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww== + dependencies: + end-of-stream "^1.1.0" + once "^1.3.1" + punycode@^2.1.0, punycode@^2.1.1: version "2.3.1" resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" @@ -5428,11 +5765,16 @@ regjsparser@^0.10.0: dependencies: jsesc "~0.5.0" -repeat-string@^1.6.1: +repeat-string@^1.0.0, repeat-string@^1.6.1: version "1.6.1" resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" integrity sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w== +reprism@^0.0.11: + version "0.0.11" + resolved "https://registry.yarnpkg.com/reprism/-/reprism-0.0.11.tgz#e760b85e0ae241722032cb8942a2bcab992a9083" + integrity sha512-VsxDR5QxZo08M/3nRypNlScw5r3rKeSOPdU/QhDmu3Ai3BJxHn/qgfXGWQp/tAxUtzwYNo9W6997JZR0tPLZsA== + require-directory@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" @@ -5475,7 +5817,7 @@ resolve.exports@^2.0.0: resolved "https://registry.yarnpkg.com/resolve.exports/-/resolve.exports-2.0.2.tgz#f8c934b8e6a13f539e38b7098e2e36134f01e800" integrity sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg== -resolve@^1.10.0, resolve@^1.20.0, resolve@^1.3.2: +resolve@^1.10.0, resolve@^1.15.1, resolve@^1.20.0, resolve@^1.3.2: version "1.22.8" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.8.tgz#b6c87a9f2aa06dfab52e3d70ac8cde321fa5a48d" integrity sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw== @@ -5502,7 +5844,7 @@ reusify@^1.0.4: resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== -rfdc@^1.3.0: +rfdc@^1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.3.1.tgz#2b6d4df52dffe8bb346992a10ea9451f24373a8f" integrity sha512-r5a3l5HzYlIC68TpmYKlxWjmOP6wiPJ1vWv2HeLhNsRZMrCkxeqxiHlQ21oXmQ4F3SiryXBHhAD7JZqvOJjFmg== @@ -5592,12 +5934,10 @@ semver@^6.0.0, semver@^6.3.0, semver@^6.3.1: resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== -semver@^7.3.4, semver@^7.3.5, semver@^7.3.6, semver@^7.5.3, semver@^7.5.4, semver@^7.6.0: - version "7.6.0" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.0.tgz#1a46a4db4bffcccd97b743b5005c8325f23d4e2d" - integrity sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg== - dependencies: - lru-cache "^6.0.0" +semver@^7.3.4, semver@^7.3.5, semver@^7.3.6, semver@^7.5.3, semver@^7.5.4, semver@^7.6.2: + version "7.6.2" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.2.tgz#1e3b34759f896e8f14d6134732ce798aeb0c6e13" + integrity sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w== send@0.18.0: version "0.18.0" @@ -5809,6 +6149,11 @@ source-map@^0.7.4: resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.4.tgz#a9bbe705c9d8846f4e08ff6765acf0f1b0898656" integrity sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA== +spark-md5@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/spark-md5/-/spark-md5-3.0.2.tgz#7952c4a30784347abcee73268e473b9c0167e3fc" + integrity sha512-wcFzz9cDfbuqe0FZzfi2or1sgyIrsDwmPwfZC4hiNidPdPINjeUwNfv5kldczoEAcjl9Y1L3SM7Uz2PUEQzxQw== + spdx-correct@^3.0.0: version "3.2.0" resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.2.0.tgz#4f5ab0668f0059e34f9c00dce331784a12de4e9c" @@ -5839,9 +6184,9 @@ spdx-expression-parse@^4.0.0: spdx-license-ids "^3.0.0" spdx-license-ids@^3.0.0: - version "3.0.17" - resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.17.tgz#887da8aa73218e51a1d917502d79863161a93f9c" - integrity sha512-sh8PWc/ftMqAAdFiBu6Fy6JUOYjqDJBJvIhpfDMyHrr0Rbp5liZqd4TjtQ/RgfLjKFZb+LMx5hpml5qOWy0qvg== + version "3.0.18" + resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.18.tgz#22aa922dcf2f2885a6494a261f2d8b75345d0326" + integrity sha512-xxRs31BqRYHwiMzudOrpSiHtZ8i/GeionCBDSilhYRj+9gIcI8wCZTlXZKu9vZIVqViP3dcp9qE5G6AlIaD+TQ== spdy-transport@^3.0.0: version "3.0.0" @@ -5888,7 +6233,7 @@ statuses@2.0.1: resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" integrity sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA== -string-argv@0.3.2: +string-argv@~0.3.2: version "0.3.2" resolved "https://registry.yarnpkg.com/string-argv/-/string-argv-0.3.2.tgz#2b6d0ef24b656274d957d54e0a4bbf6153dc02b6" integrity sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q== @@ -5954,6 +6299,11 @@ strip-ansi@^7.1.0: dependencies: ansi-regex "^6.0.1" +strip-bom@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" + integrity sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA== + strip-bom@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-4.0.0.tgz#9c3505c1db45bcedca3d9cf7a16f5c5aa3901878" @@ -6063,9 +6413,9 @@ terser-webpack-plugin@^5.3.10: terser "^5.26.0" terser@^5.26.0: - version "5.31.0" - resolved "https://registry.yarnpkg.com/terser/-/terser-5.31.0.tgz#06eef86f17007dbad4593f11a574c7f5eb02c6a1" - integrity sha512-Q1JFAoUKE5IMfI4Z/lkE/E6+SwgzO+x4tq4v1AyBLRj8VSYvRO6A/rQrPg1yud4g0En9EKI1TvFRF2tQFcoUkg== + version "5.31.1" + resolved "https://registry.yarnpkg.com/terser/-/terser-5.31.1.tgz#735de3c987dd671e95190e6b98cfe2f07f3cf0d4" + integrity sha512-37upzU1+viGvuFtBo9NPufCb9dwM0+l9hMxYyWfBA+fbwrPqNJAhbZ6W47bBFnZHKHTUBnMvi87434qq+qnxOg== dependencies: "@jridgewell/source-map" "^0.3.3" acorn "^8.8.2" @@ -6103,12 +6453,12 @@ thunky@^1.0.2: integrity sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA== timers-ext@^0.1.7: - version "0.1.7" - resolved "https://registry.yarnpkg.com/timers-ext/-/timers-ext-0.1.7.tgz#6f57ad8578e07a3fb9f91d9387d65647555e25c6" - integrity sha512-b85NUNzTSdodShTIbky6ZF02e8STtVVfD+fu4aXXShEELpozH+bCpJLYMPZbsABN2wDH7fJpqIoXxJpzbf0NqQ== + version "0.1.8" + resolved "https://registry.yarnpkg.com/timers-ext/-/timers-ext-0.1.8.tgz#b4e442f10b7624a29dd2aa42c295e257150cf16c" + integrity sha512-wFH7+SEAcKfJpfLPkrgMPvvwnEtj8W4IurvEyrKsDleXnKLCDw71w8jltvfLa8Rm4qQxxT4jmDBYbJG/z7qoww== dependencies: - es5-ext "~0.10.46" - next-tick "1" + es5-ext "^0.10.64" + next-tick "^1.1.0" tmpl@1.0.5: version "1.0.5" @@ -6132,6 +6482,11 @@ toidentifier@1.0.1: resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== +token-stream@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/token-stream/-/token-stream-1.0.0.tgz#cc200eab2613f4166d27ff9afc7ca56d49df6eb4" + integrity sha512-VSsyNPPW74RpHwR8Fc21uubwHY7wMDeJLys2IX5zJNih+OnAnaifKHo+1LHT7DAdloQ7apeaaWg8l7qnf/TnEg== + tough-cookie@^4.1.2: version "4.1.4" resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-4.1.4.tgz#945f1461b45b5a8c76821c33ea49c3ac192c1b36" @@ -6159,10 +6514,10 @@ ts-api-utils@^1.0.1: resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-1.3.0.tgz#4b490e27129f1e8e686b45cc4ab63714dc60eea1" integrity sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ== -ts-jest@^29.1.1: - version "29.1.2" - resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-29.1.2.tgz#7613d8c81c43c8cb312c6904027257e814c40e09" - integrity sha512-br6GJoH/WUX4pu7FbZXuWGKGNDuU7b8Uj77g/Sp7puZV6EXzuByl6JrECvm0MzVzSTkSHWTihsXt+5XYER5b+g== +ts-jest@^29.1.4: + version "29.1.4" + resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-29.1.4.tgz#26f8a55ce31e4d2ef7a1fd47dc7fa127e92793ef" + integrity sha512-YiHwDhSvCiItoAgsKtoLFCuakDzDsJ1DLDnSouTaTmdOcOwIkSzbLXduaQ6M5DRVhuZC/NYaaZ/mtHbWMv/S6Q== dependencies: bs-logger "0.x" fast-json-stable-stringify "2.x" @@ -6203,15 +6558,24 @@ ts-node@^10.9.1: v8-compile-cache-lib "^3.0.1" yn "3.1.1" +tsconfig-paths@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz#ef78e19039133446d244beac0fd6a1632e2d107c" + integrity sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg== + dependencies: + json5 "^2.2.2" + minimist "^1.2.6" + strip-bom "^3.0.0" + tslib@^1.13.0, tslib@^1.8.1: version "1.14.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== tslib@^2.6.2: - version "2.6.2" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae" - integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q== + version "2.6.3" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.3.tgz#0438f810ad7a9edcde7a241c3d80db693c8cbfe0" + integrity sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ== tslint@^6.1.3: version "6.1.3" @@ -6271,11 +6635,6 @@ type-fest@^0.8.1: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d" integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA== -type-fest@^1.0.1: - version "1.4.0" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-1.4.0.tgz#e9fb813fe3bf1744ec359d55d1affefa76f14be1" - integrity sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA== - type-is@~1.6.18: version "1.6.18" resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" @@ -6285,18 +6644,16 @@ type-is@~1.6.18: mime-types "~2.1.24" type@^2.7.2: - version "2.7.2" - resolved "https://registry.yarnpkg.com/type/-/type-2.7.2.tgz#2376a15a3a28b1efa0f5350dcf72d24df6ef98d0" - integrity sha512-dzlvlNlt6AXU7EBSfpAscydQ7gXB+pPGsPnfJnZpiNJBDj7IaJzQlBZYGdEi4R9HmPdBv2XmWJ6YUtoTa7lmCw== + version "2.7.3" + resolved "https://registry.yarnpkg.com/type/-/type-2.7.3.tgz#436981652129285cc3ba94f392886c2637ea0486" + integrity sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ== -typedarray-to-buffer@^3.1.5: - version "3.1.5" - resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080" - integrity sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q== - dependencies: - is-typedarray "^1.0.0" +typedoc-plugin-missing-exports@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/typedoc-plugin-missing-exports/-/typedoc-plugin-missing-exports-2.3.0.tgz#ae0858bf383a08345cc09a99d428234cf6b85ecf" + integrity sha512-iI9ITNNLlbsLCBBeYDyu0Qqp3GN/9AGyWNKg8bctRXuZEPT7G1L+0+MNWG9MsHcf/BFmNbXL0nQ8mC/tXRicog== -typedoc@^0.25.6: +typedoc@^0.25.13: version "0.25.13" resolved "https://registry.yarnpkg.com/typedoc/-/typedoc-0.25.13.tgz#9a98819e3b2d155a6d78589b46fa4c03768f0922" integrity sha512-pQqiwiJ+Z4pigfOnnysObszLiU3mVLWAExSPf+Mu06G/qsc3wzbuM56SZQvONhHLncLUhYzOVkjFFpFfL5AzhQ== @@ -6306,12 +6663,7 @@ typedoc@^0.25.6: minimatch "^9.0.3" shiki "^0.14.7" -typescript@=5.3.3: - version "5.3.3" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.3.3.tgz#b3ce6ba258e72e6305ba66f5c9b452aaee3ffe37" - integrity sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw== - -typescript@^5.2.2: +typescript@=5.4.5, typescript@>=5.0.2, typescript@^5.2.2: version "5.4.5" resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.4.5.tgz#42ccef2c571fdbd0f6718b1d1f5e6e5ef006f611" integrity sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ== @@ -6321,13 +6673,6 @@ undici-types@~5.26.4: resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== -unique-string@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/unique-string/-/unique-string-3.0.0.tgz#84a1c377aff5fd7a8bc6b55d8244b2bd90d75b9a" - integrity sha512-VGXBUVwxKMBUznyffQweQABPRRW1vHZAbadFZud4pLFAqRGvv/96vafgjWFqzourzr8YonlQiPgH0YCJfawoGQ== - dependencies: - crypto-random-string "^4.0.0" - universalify@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.2.0.tgz#6451760566fa857534745ab1dde952d1b1761be0" @@ -6343,13 +6688,13 @@ unpipe@1.0.0, unpipe@~1.0.0: resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== -update-browserslist-db@^1.0.13: - version "1.0.14" - resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.14.tgz#46a9367c323f8ade9a9dddb7f3ae7814b3a0b31c" - integrity sha512-JixKH8GR2pWYshIPUg/NujK3JO7JiqEEUiNArE86NQyrgUuZeTlZQN3xuS/yiV5Kb48ev9K6RqNkaJjXsdg7Jw== +update-browserslist-db@^1.0.16: + version "1.0.16" + resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.16.tgz#f6d489ed90fb2f07d67784eb3f53d7891f736356" + integrity sha512-KVbTxlBYlckhF5wgfyZXTWnMn7MMZjMu9XG8bPlliUOP9ThaF4QnhP8qrjrH7DRzHfSk0oQv1wToW+iA5GajEQ== dependencies: escalade "^3.1.2" - picocolors "^1.0.0" + picocolors "^1.0.1" uri-js@^4.2.2, uri-js@^4.4.1: version "4.4.1" @@ -6367,9 +6712,9 @@ url-parse@^1.5.3: requires-port "^1.0.0" utf-8-validate@^6.0.3: - version "6.0.3" - resolved "https://registry.yarnpkg.com/utf-8-validate/-/utf-8-validate-6.0.3.tgz#7d8c936d854e86b24d1d655f138ee27d2636d777" - integrity sha512-uIuGf9TWQ/y+0Lp+KGZCMuJWc3N9BHA+l/UmHd/oUHwJJDeysyTRxNQVkbzsIWfGFbRe3OcgML/i0mvVRPOyDA== + version "6.0.4" + resolved "https://registry.yarnpkg.com/utf-8-validate/-/utf-8-validate-6.0.4.tgz#1305a1bfd94cecb5a866e6fc74fd07f3ed7292e5" + integrity sha512-xu9GQDeFp+eZ6LnCywXN/zBancWvOpUMzgjLPSjy4BRHSmTelvn2E0DG0o1sTiw5hkCKBHo8rwSKncfRfv2EEQ== dependencies: node-gyp-build "^4.3.0" @@ -6415,6 +6760,11 @@ vary@~1.1.2: resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== +void-elements@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-3.1.0.tgz#614f7fbf8d801f0bb5f0661f5b2f5785750e4f09" + integrity sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w== + vscode-languageserver-textdocument@^1.0.11: version "1.0.11" resolved "https://registry.yarnpkg.com/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.11.tgz#0822a000e7d4dc083312580d7575fe9e3ba2e2bf" @@ -6436,9 +6786,9 @@ vscode-uri@^3.0.8: integrity sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw== vue-eslint-parser@^9.1.0: - version "9.4.2" - resolved "https://registry.yarnpkg.com/vue-eslint-parser/-/vue-eslint-parser-9.4.2.tgz#02ffcce82042b082292f2d1672514615f0d95b6d" - integrity sha512-Ry9oiGmCAK91HrKMtCrKFWmSFWvYkpGglCeFAIqDdr9zdXmMMpJOmUJS7WWsW7fX81h6mwHmUZCQQ1E0PkSwYQ== + version "9.4.3" + resolved "https://registry.yarnpkg.com/vue-eslint-parser/-/vue-eslint-parser-9.4.3.tgz#9b04b22c71401f1e8bca9be7c3e3416a4bde76a8" + integrity sha512-2rYRLWlIpaiN8xbPiDyXZXRgLGOtWxERV7ND5fFAv5qo1D2N9Fu9MNajBNc6o13lZ+24DAWCkQCvj4klgmcITg== dependencies: debug "^4.3.4" eslint-scope "^7.1.1" @@ -6639,7 +6989,7 @@ whatwg-url@^5.0.0: tr46 "~0.0.3" webidl-conversions "^3.0.0" -which@^2.0.1: +which@^2.0.1, which@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== @@ -6658,6 +7008,16 @@ wildcard@^2.0.0: resolved "https://registry.yarnpkg.com/wildcard/-/wildcard-2.0.1.tgz#5ab10d02487198954836b6349f74fff961e10f67" integrity sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ== +with@^7.0.0: + version "7.0.2" + resolved "https://registry.yarnpkg.com/with/-/with-7.0.2.tgz#ccee3ad542d25538a7a7a80aad212b9828495bac" + integrity sha512-RNGKj82nUPg3g5ygxkQl0R937xLyho1J24ItRCBTr/m1YnZkzJy1hUiHUJrc/VlsDQzsCnInEGSg3bci0Lmd4w== + dependencies: + "@babel/parser" "^7.9.6" + "@babel/types" "^7.9.6" + assert-never "^1.2.1" + babel-walk "3.0.0-canary-5" + word-wrap@^1.2.5: version "1.2.5" resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34" @@ -6686,16 +7046,6 @@ wrappy@1: resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== -write-file-atomic@^3.0.3: - version "3.0.3" - resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-3.0.3.tgz#56bd5c5a5c70481cd19c571bd39ab965a5de56e8" - integrity sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q== - dependencies: - imurmurhash "^0.1.4" - is-typedarray "^1.0.0" - signal-exit "^3.0.2" - typedarray-to-buffer "^3.1.5" - write-file-atomic@^4.0.2: version "4.0.2" resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-4.0.2.tgz#a9df01ae5b77858a027fd2e80768ee433555fcfd" @@ -6709,7 +7059,7 @@ ws@^8.11.0, ws@^8.13.0: resolved "https://registry.yarnpkg.com/ws/-/ws-8.17.0.tgz#d145d18eca2ed25aaf791a183903f7be5e295fea" integrity sha512-uJq6108EgZMAl20KagGkzCKfMEjxmKvZHG7Tlq0Z6nOky7YF7aq4mOx6xK8TJ/i1LeK4Qus7INktacctDgY8Ow== -xdg-basedir@^5.0.1: +xdg-basedir@^5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-5.1.0.tgz#1efba19425e73be1bc6f2a6ceb52a3d2c884c0c9" integrity sha512-GCPAHLvrIH13+c0SuacwvRYj2SxJXQ4kaVTT5xgL3kPrz56XxkF21IGhjSE1+W0aw7gpBWRGXLCPnPby6lSpmQ== @@ -6739,22 +7089,17 @@ yallist@^4.0.0: resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== -yaml@2.3.4: - version "2.3.4" - resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.3.4.tgz#53fc1d514be80aabf386dc6001eb29bf3b7523b2" - integrity sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA== - -yaml@^2.4.1: - version "2.4.2" - resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.4.2.tgz#7a2b30f2243a5fc299e1f14ca58d475ed4bc5362" - integrity sha512-B3VqDZ+JAg1nZpaEmWtTXUlBneoGx6CPM9b0TENK6aoSu5t73dItudwdgmi6tHlIZZId4dZ9skcAQ2UbcyAeVA== +yaml@^2.4.3, yaml@~2.4.2: + version "2.4.4" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.4.4.tgz#e463681ec48fe9567f1ce35cf1e3a25e14b7b7e7" + integrity sha512-wle6DEiBMLgJAdEPZ+E8BPFauoWbwPujfuGJJFErxYiU4txXItppe8YqeFPAaWnW5CxduQ995X6b5e1NqrmxtA== yargs-parser@^21.0.1, yargs-parser@^21.1.1: version "21.1.1" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35" integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw== -yargs@^17.3.1: +yargs@^17.3.1, yargs@^17.6.0: version "17.7.2" resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.2.tgz#991df39aca675a192b816e1e0363f9d75d2aa269" integrity sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==