From 4c0351a57b346490668b6d2de19426ec7ab76dfb Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Mon, 3 Jun 2024 08:10:17 -0600 Subject: [PATCH 01/68] WIP: Add support for needs sealing markings --- scripts/fetch_game_for_autoscore_testing.ts | 11 +- src/GobanCanvas.ts | 10 + src/GobanCore.ts | 3 + src/MoveTree.ts | 1 + src/ScoreEstimator.ts | 5 + src/autoscore.ts | 257 +++++++++++++++--- test/autoscore_test_files/game_33811578.json | 9 +- test/autoscore_test_files/game_33896317.json | 2 +- .../game_dev_51749995.json | 87 ++++++ test/test_autoscore.ts | 45 ++- 10 files changed, 380 insertions(+), 50 deletions(-) create mode 100644 test/autoscore_test_files/game_dev_51749995.json diff --git a/scripts/fetch_game_for_autoscore_testing.ts b/scripts/fetch_game_for_autoscore_testing.ts index 3280179b..ec2ccc33 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. @@ -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/GobanCanvas.ts b/src/GobanCanvas.ts index 5f938f01..4ed4bcb3 100644 --- a/src/GobanCanvas.ts +++ b/src/GobanCanvas.ts @@ -1746,6 +1746,9 @@ export class GobanCanvas extends GobanCore implements GobanCanvasInterface { this.engine.removal[j][i] ) { color = "dame"; + if (pos.needs_sealing) { + color = "seal"; + } } if (color === "white") { @@ -1757,6 +1760,9 @@ 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"; } ctx.lineWidth = Math.ceil(this.square_size * 0.065) - 0.5; @@ -2364,6 +2370,10 @@ export class GobanCanvas extends GobanCore implements GobanCanvasInterface { this.engine.removal[j][i] ) { color = "dame"; + + if (pos.needs_sealing) { + color = "seal"; + } } if ( diff --git a/src/GobanCore.ts b/src/GobanCore.ts index 435666ef..404c6421 100644 --- a/src/GobanCore.ts +++ b/src/GobanCore.ts @@ -271,6 +271,7 @@ export interface Events extends StateUpdateEvents { "captured-stones": (obj: { removed_stones: Array }) => void; "stone-removal.accepted": () => void; "stone-removal.updated": () => void; + "needs-sealing": (positions: undefined | [number, number][]) => void; "conditional-moves.updated": () => void; "puzzle-place": (obj: { x: number; @@ -3189,7 +3190,9 @@ export abstract class GobanCore extends EventEmitter { 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.emit("needs-sealing", se.autoscored_needs_sealing); this.updateTitleAndStonePlacement(); this.emit("update"); diff --git a/src/MoveTree.ts b/src/MoveTree.ts index a4eaf65a..65dcb99e 100644 --- a/src/MoveTree.ts +++ b/src/MoveTree.ts @@ -40,6 +40,7 @@ export interface MarkInterface { black?: boolean; white?: boolean; color?: string; + needs_sealing?: boolean; [label: string]: string | boolean | undefined; } diff --git a/src/ScoreEstimator.ts b/src/ScoreEstimator.ts index f15da52a..bada213d 100644 --- a/src/ScoreEstimator.ts +++ b/src/ScoreEstimator.ts @@ -65,6 +65,9 @@ export interface ScoreEstimateResponse { /** Intersections that are dead or dame. Only defined if autoscore was true in the request. */ autoscored_removed?: string; + + /** Coordinates that still need sealing */ + autoscored_needs_sealing?: [number, number][]; } let remote_scorer: ((req: ScoreEstimateRequest) => Promise) | undefined; @@ -136,6 +139,7 @@ export class ScoreEstimator { autoscored_state?: JGOFNumericPlayerColor[][]; autoscored_removed?: string; autoscore: boolean = false; + public autoscored_needs_sealing?: [number, number][]; constructor( goban_callback: GobanCore | undefined, @@ -225,6 +229,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( diff --git a/src/autoscore.ts b/src/autoscore.ts index 17e9c86f..ff67a0fb 100644 --- a/src/autoscore.ts +++ b/src/autoscore.ts @@ -29,6 +29,7 @@ interface AutoscoreResults { result: JGOFNumericPlayerColor[][]; removed_string: string; removed: [number, number, /* reason */ string][]; + needs_sealing: [number, number][]; } const REMOVAL_THRESHOLD = 0.7; @@ -70,6 +71,7 @@ export function autoscore( const is_settled = makeMatrix(width, height); const settled = makeMatrix(width, height); const final_ownership = makeMatrix(board[0].length, board.length); + const still_needs_sealing = makeMatrix(width, height); const average_ownership = makeMatrix(width, height); for (let y = 0; y < height; ++y) { @@ -90,18 +92,34 @@ export function autoscore( debug_ownership_output(white_plays_first_ownership); settle_agreed_upon_territory(); - remove_obviously_dead_stones(); + remove_obviously_dead_stones(false); + mark_settled_positions(); clear_unsettled_stones_from_territory(); - seal_territory(); + mark_territory_that_still_needs_sealing(); + + remove_obviously_dead_stones(true); + compute_final_ownership(); final_dame_pass(); + debug("Locations marked for sealing"); + debug_print_settled(still_needs_sealing); + + const needs_sealing: [number, number][] = []; + for (let y = 0; y < still_needs_sealing.length; ++y) { + for (let x = 0; x < still_needs_sealing[y].length; ++x) { + if (still_needs_sealing[y][x]) { + needs_sealing.push([x, y]); + } + } + } return [ { result: final_ownership, removed_string: removed.map((pt) => `${num2char(pt[0])}${num2char(pt[1])}`).join(""), removed, + needs_sealing, }, debug_output, ]; @@ -115,6 +133,9 @@ export function autoscore( removed.push([x, y, reason]); board[y][x] = JGOFNumericPlayerColor.EMPTY; removal[y][x] = 1; + + // clear still needs sealing if something else comes along and removes it too + //still_needs_sealing[y][x] = 0; } /* @@ -180,28 +201,35 @@ export function autoscore( * 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() { + function remove_obviously_dead_stones(remove_dame: boolean) { 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"); + if (remove_dame) { + 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"); + } + } else { + // !remove_dame + 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"); + } + + 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"); + } } } } @@ -344,7 +372,11 @@ export function autoscore( 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"); + remove( + x, + y, + `clearing unsettled stones within assumed territory (color judgement: ${color_judgement})`, + ); is_settled[y][x] = 1; settled[y][x] = color_judgement; } @@ -372,23 +404,17 @@ export function autoscore( } /* - * Attempt to seal territory + * Attempt to detect territory that still needs sealing * - * 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. + * This function attempts to find intersections that still need to be + * sealed. * * Note, this needs to be run after obviously dead stones have been * removed. */ - function seal_territory() { - debug(`### Sealing territory`); + function mark_territory_that_still_needs_sealing() { + debug(`### Looking for territory that still needs sealing`); //const dame_map = makeMatrix(width, height); { let groups = new GoStoneGroups( @@ -406,6 +432,47 @@ export function autoscore( groups.foreachGroup((group) => { // unowned territory + if ( + group.color === JGOFNumericPlayerColor.EMPTY && + !group.is_territory && + group.points.length >= 3 + ) { + let total_ownership = 0; + for (const point of group.points) { + const x = point.x; + const y = point.y; + total_ownership += average_ownership[y][x]; + } + + const avg_ownership = total_ownership / group.points.length; + + if (isBlack(avg_ownership) || isWhite(avg_ownership)) { + const opposing_color = isBlack(avg_ownership) + ? JGOFNumericPlayerColor.WHITE + : JGOFNumericPlayerColor.BLACK; + + group.foreachStone((point) => { + const x = point.x; + const y = point.y; + + 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; + still_needs_sealing[y][x] = 1; + } + }); + } + } + + // unowned territory + /* if (group.color === JGOFNumericPlayerColor.EMPTY && !group.is_territory) { group.foreachStone((point) => { const x = point.x; @@ -418,16 +485,136 @@ export function autoscore( settled[y][x] === JGOFNumericPlayerColor.BLACK ? JGOFNumericPlayerColor.WHITE : JGOFNumericPlayerColor.BLACK; + const isOpposingTerritory = (x: number, y: number) => { + if (original_board[y]?.[x] !== JGOFNumericPlayerColor.EMPTY) { + return false; + } + + if (opposing_color === JGOFNumericPlayerColor.BLACK) { + return isBlack(average_ownership[y]?.[x]); + } else { + return isWhite(average_ownership[y]?.[x]); + } + }; 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; + const adjacent_to_opposing_territory = + isOpposingTerritory(x, y + 1) || + isOpposingTerritory(x, y - 1) || + isOpposingTerritory(x + 1, y) || + isOpposingTerritory(x - 1, y); + + const is_already_removed = removal[y][x]; + + if ( + adjacent_to_opposing_color && + adjacent_to_opposing_territory && + !is_already_removed + ) { + remove(x, y, "sealing territory"); + is_settled[y][x] = 1; + settled[y][x] = JGOFNumericPlayerColor.EMPTY; + still_needs_sealing[y][x] = 1; + } + } + }); + } + */ + }); - if (adjacent_to_opposing_color) { + 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 mark_territory_that_still_needs_sealing() { + debug(`### Looking for territory that still needs sealing`); + //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 isOpposingTerritory = (x: number, y: number) => { + if (original_board[y]?.[x] !== JGOFNumericPlayerColor.EMPTY) { + return false; + } + + if (opposing_color === JGOFNumericPlayerColor.BLACK) { + return isBlack(average_ownership[y]?.[x]); + } else { + return isWhite(average_ownership[y]?.[x]); + } + }; + 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; + const adjacent_to_opposing_territory = + isOpposingTerritory(x, y + 1) || + isOpposingTerritory(x, y - 1) || + isOpposingTerritory(x + 1, y) || + isOpposingTerritory(x - 1, y); + + const is_already_removed = removal[y][x]; + + if ( + adjacent_to_opposing_color && + adjacent_to_opposing_territory && + !is_already_removed + ) { remove(x, y, "sealing territory"); is_settled[y][x] = 1; settled[y][x] = JGOFNumericPlayerColor.EMPTY; + still_needs_sealing[y][x] = 1; } } }); @@ -443,6 +630,7 @@ export function autoscore( }, original_board, ); + debug("Sealed groups:"); debug_groups(groups); @@ -457,6 +645,7 @@ export function autoscore( }); } } + */ function compute_final_ownership() { for (let y = 0; y < board.length; ++y) { diff --git a/test/autoscore_test_files/game_33811578.json b/test/autoscore_test_files/game_33811578.json index ad67d8d4..44ee88ec 100644 --- a/test/autoscore_test_files/game_33811578.json +++ b/test/autoscore_test_files/game_33811578.json @@ -67,21 +67,22 @@ "BBBBBBBBBBBBBWWWWWW", "BBBBBBBBBBBBWWWWWWW", "BBBWBBBBBBBWWWWWWWW", - "BBBWW*B BBWWBBBWWWW", + "BBBWW*BsBBWWBBBWWWW", "BBBBWWWWWBW *BBBWWW", "BBBBBBWWWBWBBBBBWWW", - "BBBB WWWWWBBWWBBWWW", + "BBBBsWWWWWBBWWBBWWW", "BBBBBWWWWWBBBWWWWWW", "BBBBBWWWWBBBBWWWWWW", "BBBBBB*W*BBBWWWBBW*", - "BBBBBBBWWB*BBW*BWWB", + "BBBBBBBWWBsBBW*BWWB", "BBBBBBBBWWWWBBBBBBB", "BBBBBBBBBBWWWWBBBBB", - "BBBBBBBBBBBBW**BBBB", + "BBBBBBBBBBBBW*sBBBB", "BBBBBBBBBBWWWWBBWWW", "BBBBBBBBBWWWWWWWWWW", "BBBBBBBWWWWWWWWWWWW", "BBBBBBBBWWWWWWWWWWW", "BBBBBBBBWWWWWWWWWWW" ] + } 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_dev_51749995.json b/test/autoscore_test_files/game_dev_51749995.json new file mode 100644 index 00000000..47f4f821 --- /dev/null +++ b/test/autoscore_test_files/game_dev_51749995.json @@ -0,0 +1,87 @@ +{ + "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, -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, -0.9, -0.9, -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, 1.0, -1.0, -1.0, -0.8, 1.0, -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, -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.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, -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.4, 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.9, -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.9, -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.8, 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.3, -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.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, 1.0, 1.0, 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, 1.0, 1.0, 1.0, 1.0, 1.0, -1.0, -0.6, -0.5, 0.9, 1.0, 1.0, 1.0], + [ 0.9, 0.9, 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, 1.0, 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.7, 0.6, -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.7, 0.5, 0.5, -1.0, -0.9, -0.9, -1.0, -0.9, -0.9, -0.9, -0.9, -0.9, -0.9, -1.0] + ], + "correct_ownership": [ + "BBBBBBBBBBBBBWWWWWW", + "BBBBBBBBBBBBWWWWWWW", + "BBBWBBBBBBBWWWWWWWW", + "BBBWW*BsBBWWBBBWWWW", + "BBBBWWWWWBW *BBBWWW", + "BBBBBBWWWBWBBBBBWWW", + "BBBBsWWWWWBBWWBBWWW", + "BBBBBWWWWWBBBWWWWWW", + "BBBBBWWWWBBBBWWWWWW", + "BBBBBB*W*BBBWWWBBW*", + "BBBBBBBWWBsBBW*BWWB", + "BBBBBBBBWWWWBBBBBBB", + "BBBBBBBBBBWWWWBBBBB", + "BBBBBBBBBBBBW*sBBBB", + "BBBBBBBBBBWWWWBBWWW", + "BBBBBBBBBWWWWWWWWWW", + "BBBBBBBWWWWWWWWWWWW", + "BBBBBBBBWWWWWWWWWWW", + "BBBBBBBBWWWWWWWWWWW" + ] +} diff --git a/test/test_autoscore.ts b/test/test_autoscore.ts index bf7bbead..84bc280c 100755 --- a/test/test_autoscore.ts +++ b/test/test_autoscore.ts @@ -111,7 +111,12 @@ function test_file(path: string, quiet: boolean): boolean { } 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`, @@ -130,16 +135,36 @@ function test_file(path: string, quiet: boolean): boolean { matches[y] = []; for (let x = 0; x < res.result[0].length; ++x) { const v = res.result[y][x]; - const m = + let m = data.correct_ownership[y][x] === "*" || + data.correct_ownership[y][x] === "s" || // seal (v === 0 && data.correct_ownership[y][x] === " ") || (v === 1 && data.correct_ownership[y][x] === "B") || (v === 2 && data.correct_ownership[y][x] === "W"); + + if (data.correct_ownership[y][x] === "s") { + const has_needs_sealing = + res.needs_sealing.find(([x2, y2]) => x2 === x && y2 === y) !== undefined; + + m &&= has_needs_sealing; + } + matches[y][x] = m; match &&= m; } } + /* Ensure all needs_sealing are marked as such */ + for (const [x, y] of res.needs_sealing) { + if (data.correct_ownership[y][x] !== "s") { + 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; + } + } + if (!quiet) { // Double check that when we run everything through our normal GoEngine.computeScore function, // that we get the result we're expecting @@ -206,15 +231,19 @@ function test_file(path: string, quiet: boolean): boolean { } if (!quiet) { + 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(""), + ), + ); + if (official_match) { console.log("Final autoscore matches official scoring"); } else { console.error("Official score did not match our expected scoring"); - print_expected( - scored_board.map((row) => - row.map((v) => (v === 1 ? "B" : v === 2 ? "W" : " ")).join(""), - ), - ); print_mismatches(official_matches); } } @@ -317,6 +346,8 @@ function print_expected(board: string[]) { out += clc.blue("."); } else if (c === "*") { out += clc.yellow(" "); + } else if (c === "s") { + out += clc.magenta("s"); } else { out += clc.red(c); } From a3bf2e316ee9caa2b7363b41ea8d41f213901dd2 Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Wed, 5 Jun 2024 06:09:28 -0600 Subject: [PATCH 02/68] Sealing logic working with our test cases --- src/GobanSVG.ts | 13 + src/autoscore.ts | 710 ++++++++---------- test/autoscore_test_files/game_33811578.json | 4 +- test/autoscore_test_files/game_33921785.json | 2 +- test/autoscore_test_files/game_35115094.json | 6 +- test/autoscore_test_files/game_35115743.json | 2 +- .../game_dev_51749995.json | 42 +- test/test_autoscore.ts | 7 +- 8 files changed, 374 insertions(+), 412 deletions(-) diff --git a/src/GobanSVG.ts b/src/GobanSVG.ts index 632f06d1..3bc52902 100644 --- a/src/GobanSVG.ts +++ b/src/GobanSVG.ts @@ -1659,6 +1659,10 @@ export class GobanSVG extends GobanCore implements GobanSVGInterface { this.engine.removal[j][i] ) { color = "dame"; + + if (pos.needs_sealing) { + color = "seal"; + } } const r = this.square_size * 0.15; @@ -1679,6 +1683,11 @@ 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"); + } rect.setAttribute( "stroke-width", (Math.ceil(this.square_size * 0.065) - 0.5).toFixed(1), @@ -2298,6 +2307,10 @@ export class GobanSVG extends GobanCore implements GobanSVGInterface { this.engine.removal[j][i] ) { color = "dame"; + + if (pos.needs_sealing) { + color = "seal"; + } } if ( diff --git a/src/autoscore.ts b/src/autoscore.ts index ff67a0fb..f74967cc 100644 --- a/src/autoscore.ts +++ b/src/autoscore.ts @@ -23,7 +23,8 @@ import { GoStoneGroups } from "./GoStoneGroups"; import { JGOFNumericPlayerColor } from "./JGOF"; -import { makeMatrix, num2char } from "./GoMath"; +import { char2num, makeMatrix, num2char, pretty_coor_num2ch } from "./GoMath"; +import { GoEngine, GoEngineInitialState } from "./GoEngine"; interface AutoscoreResults { result: JGOFNumericPlayerColor[][]; @@ -33,8 +34,11 @@ interface AutoscoreResults { } 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; @@ -44,20 +48,6 @@ 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[][], @@ -71,7 +61,8 @@ export function autoscore( const is_settled = makeMatrix(width, height); const settled = makeMatrix(width, height); const final_ownership = makeMatrix(board[0].length, board.length); - const still_needs_sealing = makeMatrix(width, height); + const sealed = makeMatrix(width, height); + const needs_sealing: [number, number][] = []; const average_ownership = makeMatrix(width, height); for (let y = 0; y < height; ++y) { @@ -81,39 +72,30 @@ export function autoscore( } } - reset_debug_output(); - debug("Initial board:"); - debug_board_output(board); - - debug("Ownership if black moves first:"); - debug_ownership_output(black_plays_first_ownership); + // 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 GoStoneGroups({ + width, + height, + board, + removal: makeMatrix(width, height), + }); - debug("Ownership if white moves first:"); - debug_ownership_output(white_plays_first_ownership); + debug_groups("Groups", groups); + // Perform our removal logic + settle_agreed_upon_territory(); + remove_obviously_dead_stones(); settle_agreed_upon_territory(); - remove_obviously_dead_stones(false); - - mark_settled_positions(); clear_unsettled_stones_from_territory(); - mark_territory_that_still_needs_sealing(); - - remove_obviously_dead_stones(true); - - compute_final_ownership(); - final_dame_pass(); + seal_territory(); + score_positions(); - debug("Locations marked for sealing"); - debug_print_settled(still_needs_sealing); - - const needs_sealing: [number, number][] = []; - for (let y = 0; y < still_needs_sealing.length; ++y) { - for (let x = 0; x < still_needs_sealing[y].length; ++x) { - if (still_needs_sealing[y][x]) { - needs_sealing.push([x, y]); - } - } - } return [ { result: final_ownership, @@ -121,7 +103,7 @@ export function autoscore( removed, needs_sealing, }, - debug_output, + finalize_debug_output(), ]; /** Marks a position as being removed (either dead stone or dame) */ @@ -133,9 +115,7 @@ export function autoscore( removed.push([x, y, reason]); board[y][x] = JGOFNumericPlayerColor.EMPTY; removal[y][x] = 1; - - // clear still needs sealing if something else comes along and removes it too - //still_needs_sealing[y][x] = 0; + stage_log(`Removing ${pretty_coor_num2ch(x)}${height - y}: ${reason}`); } /* @@ -147,16 +127,19 @@ export function autoscore( * with adjacent groups. */ function settle_agreed_upon_territory() { - debug("### Settling agreed upon territory"); + stage("Settling agreed upon territory"); - const groups = new GoStoneGroups({ - width, - height, - board, - removal: makeMatrix(width, height), - }); + const groups = new GoStoneGroups( + { + width, + height, + board, + removal, + }, + original_board, + ); - debug_groups(groups); + debug_groups("Initial", groups); groups.foreachGroup((group) => { const color = group.territory_color; @@ -192,6 +175,9 @@ export function autoscore( } } }); + + debug_boolean_board("Settled", is_settled); + debug_board_output("Settled ownership", settled); } /* @@ -201,70 +187,35 @@ export function autoscore( * 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(remove_dame: boolean) { - debug("### Removing stones both agree on:"); - for (let y = 0; y < height; ++y) { - for (let x = 0; x < width; ++x) { - if (remove_dame) { - 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"); - } - } else { - // !remove_dame - 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"); - } - - 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"); - } - } - } - } - } - - /* - * 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"); + function remove_obviously_dead_stones() { + stage("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]) ) { - is_settled[y][x] = 1; - settled[y][x] = JGOFNumericPlayerColor.WHITE; + remove(x, y, "both players agree this is captured by white"); } - - if ( - isBlack(black_plays_first_ownership[y][x]) && - isBlack(white_plays_first_ownership[y][x]) + /* + else if ( + board[y][x] === JGOFNumericPlayerColor.EMPTY && + isDameOrUnknown(black_plays_first_ownership[y][x]) && + isDameOrUnknown(white_plays_first_ownership[y][x]) ) { - is_settled[y][x] = 1; - settled[y][x] = JGOFNumericPlayerColor.BLACK; + remove(x, y, "both players agree this is dame"); } + */ } } - - debug_print_settled(is_settled); - debug_board_output(board); + debug_boolean_board("Removed", removal, "x"); } /* @@ -285,6 +236,10 @@ export function autoscore( * 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 @@ -372,49 +327,48 @@ export function autoscore( 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 stones within assumed territory (color judgement: ${color_judgement})`, + `clearing unsettled ${stone_color} stones within assumed ${judgement_color} 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", - ); }); + + 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 detect territory that still needs sealing + * Attempt to seal territory + * + * This function attempts to seal territory that has been overlooked + * by the players. * - * This function attempts to find intersections that still need to be - * sealed. + * 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 mark_territory_that_still_needs_sealing() { - debug(`### Looking for territory that still needs sealing`); + function seal_territory() { + stage(`Sealing territory`); //const dame_map = makeMatrix(width, height); { let groups = new GoStoneGroups( @@ -427,34 +381,40 @@ export function autoscore( original_board, ); - debug("Initial groups:"); - debug_groups(groups); + debug_groups("Initial groups", groups); groups.foreachGroup((group) => { - // unowned territory + // Large enough unowned territory where sealing might make a difference if ( group.color === JGOFNumericPlayerColor.EMPTY && !group.is_territory && - group.points.length >= 3 + group.points.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; - for (const point of group.points) { + + group.foreachStone((point) => { const x = point.x; const y = point.y; total_ownership += average_ownership[y][x]; - } - - const avg_ownership = total_ownership / group.points.length; + }); - if (isBlack(avg_ownership) || isWhite(avg_ownership)) { - const opposing_color = isBlack(avg_ownership) - ? JGOFNumericPlayerColor.WHITE - : JGOFNumericPlayerColor.BLACK; + const avg = total_ownership / group.points.length; + if (avg <= WHITE_SEAL_THRESHOLD || avg >= BLACK_SEAL_THRESHOLD) { group.foreachStone((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 || @@ -465,64 +425,12 @@ export function autoscore( remove(x, y, "sealing territory"); is_settled[y][x] = 1; settled[y][x] = JGOFNumericPlayerColor.EMPTY; - still_needs_sealing[y][x] = 1; + sealed[y][x] = 1; + needs_sealing.push([x, y]); } }); } } - - // 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 isOpposingTerritory = (x: number, y: number) => { - if (original_board[y]?.[x] !== JGOFNumericPlayerColor.EMPTY) { - return false; - } - - if (opposing_color === JGOFNumericPlayerColor.BLACK) { - return isBlack(average_ownership[y]?.[x]); - } else { - return isWhite(average_ownership[y]?.[x]); - } - }; - 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; - const adjacent_to_opposing_territory = - isOpposingTerritory(x, y + 1) || - isOpposingTerritory(x, y - 1) || - isOpposingTerritory(x + 1, y) || - isOpposingTerritory(x - 1, y); - - const is_already_removed = removal[y][x]; - - if ( - adjacent_to_opposing_color && - adjacent_to_opposing_territory && - !is_already_removed - ) { - remove(x, y, "sealing territory"); - is_settled[y][x] = 1; - settled[y][x] = JGOFNumericPlayerColor.EMPTY; - still_needs_sealing[y][x] = 1; - } - } - }); - } - */ }); groups = new GoStoneGroups( @@ -535,187 +443,89 @@ export function autoscore( 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; - }); - } - }); + debug_boolean_board("Sealed positions", sealed, "s"); + debug_groups("After sealing", groups); } } - /* - function mark_territory_that_still_needs_sealing() { - debug(`### Looking for territory that still needs sealing`); - //const dame_map = makeMatrix(width, height); - { - let groups = new GoStoneGroups( - { - width, - height, - board, - removal, - }, - original_board, - ); - - debug("Initial groups:"); - debug_groups(groups); + 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; + } + } + } - groups.foreachGroup((group) => { - // unowned territory - if (group.color === JGOFNumericPlayerColor.EMPTY && !group.is_territory) { - group.foreachStone((point) => { - const x = point.x; - const y = point.y; + const initial_state: GoEngineInitialState = { + black: black_state, + white: white_state, + }; - // 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 isOpposingTerritory = (x: number, y: number) => { - if (original_board[y]?.[x] !== JGOFNumericPlayerColor.EMPTY) { - return false; - } - - if (opposing_color === JGOFNumericPlayerColor.BLACK) { - return isBlack(average_ownership[y]?.[x]); - } else { - return isWhite(average_ownership[y]?.[x]); - } - }; - 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; - const adjacent_to_opposing_territory = - isOpposingTerritory(x, y + 1) || - isOpposingTerritory(x, y - 1) || - isOpposingTerritory(x + 1, y) || - isOpposingTerritory(x - 1, y); - - const is_already_removed = removal[y][x]; - - if ( - adjacent_to_opposing_color && - adjacent_to_opposing_territory && - !is_already_removed - ) { - remove(x, y, "sealing territory"); - is_settled[y][x] = 1; - settled[y][x] = JGOFNumericPlayerColor.EMPTY; - still_needs_sealing[y][x] = 1; - } - } - }); - } - }); + const removed_string = removed.map((pt) => `${num2char(pt[0])}${num2char(pt[1])}`).join(""); - groups = new GoStoneGroups( - { - width: board[0].length, - height: board.length, - board, - removal, - }, - original_board, - ); + const engine = new GoEngine({ + width: original_board[0].length, + height: original_board.length, + initial_state, + rules: "japanese", // so we can test seki code + removed: removed_string, + }); - debug("Sealed groups:"); - debug_groups(groups); + const score = engine.computeScore(); + const scoring_positions = makeMatrix(width, height); - 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; - }); - } - }); + 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]); + final_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]); + final_ownership[y][x] = JGOFNumericPlayerColor.WHITE; + scoring_positions[y][x] = JGOFNumericPlayerColor.WHITE; } - } - */ - 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; - } + if (board[y][x] !== JGOFNumericPlayerColor.EMPTY) { + final_ownership[y][x] = board[y][x]; } } } - // 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"); - } - } - } + const groups = new GoStoneGroups( + { + width, + height, + board, + removal, + }, + original_board, + ); + + debug_groups("groups", groups); + + debug_board_output("Scoring positions (JP)", scoring_positions); + debug_board_output("Board", board); + debug_board_output("Final ownership", final_ownership); + debug_boolean_board("Sealed", sealed, "s"); } } -function debug_ownership_output(ownership: number[][]) { - let out = "\n "; +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) { @@ -741,7 +551,8 @@ function debug_ownership_output(ownership: number[][]) { out += "\n"; - debug(out); + board_output(out); + end_board(); } function colorizeOwnership(ownership: number): string { @@ -767,7 +578,8 @@ function colorizeOwnership(ownership: number): string { } } -function debug_board_output(board: JGOFNumericPlayerColor[][]) { +function debug_board_output(title: string, board: JGOFNumericPlayerColor[][]) { + begin_board(title); let out = " "; const x_coords = "ABCDEFGHJKLMNOPQRST"; // cspell: disable-line @@ -803,16 +615,23 @@ function debug_board_output(board: JGOFNumericPlayerColor[][]) { out += "\n"; out += "\n"; - debug(out); + board_output(out); + end_board(); } function colorizeIntersection(c: string): string { - if (c === "B" || c === "s") { + 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 === "_") { @@ -821,7 +640,8 @@ function colorizeIntersection(c: string): string { return yellow(c); } -function debug_print_settled(board: number[][]) { +function debug_boolean_board(title: string, board: number[][], mark = "S") { + begin_board(title); let out = " "; const x_coords = "ABCDEFGHJKLMNOPQRST"; // cspell: disable-line @@ -833,7 +653,7 @@ function debug_print_settled(board: number[][]) { 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 += colorizeIntersection(board[y][x] ? mark : " "); } out += " " + ` ${board.length - y} `.substr(-3); @@ -847,10 +667,11 @@ function debug_print_settled(board: number[][]) { out += "\n"; out += "\n"; - debug(out); + board_output(out); + end_board(); } -function debug_groups(groups: GoStoneGroups) { +function debug_groups(title: string, groups: GoStoneGroups) { const group_map: string[][] = makeMatrix( groups.group_id_map[0].length, groups.group_id_map.length, @@ -891,19 +712,11 @@ function debug_groups(groups: GoStoneGroups) { 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); + debug_group_map(title, group_map); } -function debug_group_map(board: string[][]) { +function debug_group_map(title: string, board: string[][]) { + begin_board(title); let out = " "; const x_coords = "ABCDEFGHJKLMNOPQRST"; // cspell: disable-line @@ -929,7 +742,8 @@ function debug_group_map(board: string[][]) { out += "\n"; out += "\n"; - debug(out); + board_output(out); + end_board(); } function white(str: string) { @@ -969,3 +783,135 @@ function cyanBright(str: string) { 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; +} + +function stage_log(str: string) { + current_stage_log += " " + str + "\n"; +} + +function end_stage() { + end_board(); + + if (!current_stage) { + return; + } + + const title_line = `#### ${current_stage} ####`; + const pounds = "#".repeat(title_line.length); + final_output += `\n\n${pounds}\n${title_line}\n${pounds}\n\n`; + current_stage = ""; + + const wide_lines: string[] = []; + const str_grid: string[][] = []; + const segment_length = 30; + + for (let x = 0; x < board_outputs.length; ++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]; + } + } + + 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"; + } + + board_outputs = []; + + 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 += "Legend:\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/test/autoscore_test_files/game_33811578.json b/test/autoscore_test_files/game_33811578.json index 44ee88ec..30e3018a 100644 --- a/test/autoscore_test_files/game_33811578.json +++ b/test/autoscore_test_files/game_33811578.json @@ -68,7 +68,7 @@ "BBBBBBBBBBBBWWWWWWW", "BBBWBBBBBBBWWWWWWWW", "BBBWW*BsBBWWBBBWWWW", - "BBBBWWWWWBW *BBBWWW", + "BBBBWWWWWBWs*BBBWWW", "BBBBBBWWWBWBBBBBWWW", "BBBBsWWWWWBBWWBBWWW", "BBBBBWWWWWBBBWWWWWW", @@ -77,7 +77,7 @@ "BBBBBBBWWBsBBW*BWWB", "BBBBBBBBWWWWBBBBBBB", "BBBBBBBBBBWWWWBBBBB", - "BBBBBBBBBBBBW*sBBBB", + "BBBBBBBBBBBBWBBBBBB", "BBBBBBBBBBWWWWBBWWW", "BBBBBBBBBWWWWWWWWWW", "BBBBBBBWWWWWWWWWWWW", diff --git a/test/autoscore_test_files/game_33921785.json b/test/autoscore_test_files/game_33921785.json index 7888bcea..3a22af6a 100644 --- a/test/autoscore_test_files/game_33921785.json +++ b/test/autoscore_test_files/game_33921785.json @@ -40,7 +40,7 @@ "WWWBBBBBB", "WWBBBBBBB", "WWWBBBBBB", - "WWWWBWWB ", + "WWWWBWWBs", "WWWWWWWWW", "WWWWWWWWW" ] diff --git a/test/autoscore_test_files/game_35115094.json b/test/autoscore_test_files/game_35115094.json index 1845bd5d..a184f805 100644 --- a/test/autoscore_test_files/game_35115094.json +++ b/test/autoscore_test_files/game_35115094.json @@ -68,9 +68,9 @@ "BBBWWWWWWWWBBBBWWWW", "BBBBBBBBBBBWBBBBWWW", "BBBBBBBBB*WWW*WWWWW", - "BBBBBBBBBWWWWWWBBWW", - "**BBBBWBBWWWBBBBBBW", - "*WWBWWWWWBBBBBBBBBB", + "ssBBBBBBBWWWWWWBBWW", + "sBBBBBWBBWWWBBBBBBW", + "WWWBWWWWWBBBBBBBBBB", "WWWBBBWWBBBBBBBBBBB", "WWWWWWBWBBBBWWBBBBB", "WWWWWWBWWBWB*WBBBBB", diff --git a/test/autoscore_test_files/game_35115743.json b/test/autoscore_test_files/game_35115743.json index 8e333b4b..d585028e 100644 --- a/test/autoscore_test_files/game_35115743.json +++ b/test/autoscore_test_files/game_35115743.json @@ -76,7 +76,7 @@ "BBBBWWWWWWWWBBBBBBB", "BBB*WWWWWWWWWBBBBBB", "BBWWWWWWWWWWWBBBBBB", - "BBBWWWWWWWWWW*BBBBB", + "BBBWWWWWWWWWWsBBBBB", "BBBWWWWWWWWWWBBBBBB", "BBBBWWWWWWWWWWBBBBB", "BBBBBBWWWWWWWWWBBBB", diff --git a/test/autoscore_test_files/game_dev_51749995.json b/test/autoscore_test_files/game_dev_51749995.json index 47f4f821..9d956b58 100644 --- a/test/autoscore_test_files/game_dev_51749995.json +++ b/test/autoscore_test_files/game_dev_51749995.json @@ -22,62 +22,62 @@ " 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, -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, -0.9, -0.9, -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, 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.8, 1.0, -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, -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.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, 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.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, -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.9, -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.9, -1.0, -0.9, -0.9, -1.0, -0.9, -0.9, -0.9, -1.0, -0.9, -0.9, -1.0] + [ 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.8, 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.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.3, -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.5, 1.0, 1.0, -1.0, -0.8, 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, 1.0, 1.0, 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, 1.0, 1.0, 1.0, 1.0, 1.0, -1.0, -0.6, -0.5, 0.9, 1.0, 1.0, 1.0], - [ 0.9, 0.9, 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, 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, 1.0, 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.7, 0.6, -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.7, 0.5, 0.5, -1.0, -0.9, -0.9, -1.0, -0.9, -0.9, -0.9, -0.9, -0.9, -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] ], "correct_ownership": [ "BBBBBBBBBBBBBWWWWWW", "BBBBBBBBBBBBWWWWWWW", "BBBWBBBBBBBWWWWWWWW", - "BBBWW*BsBBWWBBBWWWW", - "BBBBWWWWWBW *BBBWWW", + "BBBWW BsBBWWBBBWWWW", + "BBBBWWWWWBWsBBBBWWW", "BBBBBBWWWBWBBBBBWWW", "BBBBsWWWWWBBWWBBWWW", "BBBBBWWWWWBBBWWWWWW", "BBBBBWWWWBBBBWWWWWW", - "BBBBBB*W*BBBWWWBBW*", - "BBBBBBBWWBsBBW*BWWB", + "BBBBBB W BBBWWWBBW ", + "BBBBBBBWWBsBBW BWWB", "BBBBBBBBWWWWBBBBBBB", "BBBBBBBBBBWWWWBBBBB", - "BBBBBBBBBBBBW*sBBBB", + "BBBBBBBBBBBBWBBBBBB", "BBBBBBBBBBWWWWBBWWW", "BBBBBBBBBWWWWWWWWWW", "BBBBBBBWWWWWWWWWWWW", diff --git a/test/test_autoscore.ts b/test/test_autoscore.ts index 84bc280c..90047fd4 100755 --- a/test/test_autoscore.ts +++ b/test/test_autoscore.ts @@ -156,7 +156,7 @@ function test_file(path: string, quiet: boolean): boolean { /* Ensure all needs_sealing are marked as such */ for (const [x, y] of res.needs_sealing) { - if (data.correct_ownership[y][x] !== "s") { + 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`, ); @@ -222,6 +222,7 @@ function test_file(path: string, quiet: boolean): boolean { const v = scored_board[y][x]; const m = data.correct_ownership[y][x] === "*" || + (v === 0 && 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"); @@ -266,12 +267,14 @@ function test_file(path: string, quiet: boolean): boolean { 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(`<<< ${path} failed`); console.log(`<<< ${path} failed`); @@ -345,7 +348,7 @@ 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 { From ca2b00c2c8100291bc4fe88631955936be22ea4b Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Sat, 8 Jun 2024 11:17:10 -0600 Subject: [PATCH 03/68] Integrate goscorer and update the stone removal phase code --- .eslintrc.js | 2 +- .vscode/cspell.json | 7 +- Makefile | 15 +- src/GoEngine.ts | 290 +++- src/GoMath.ts | 30 +- src/GoStoneGroup.ts | 3 +- src/GoStoneGroups.ts | 4 + src/GobanCanvas.ts | 66 +- src/GobanCore.ts | 14 +- src/GobanSVG.ts | 70 +- src/JGOF.ts | 9 + src/ScoreEstimator.ts | 14 +- src/__tests__/autoscore.test.ts | 7 +- src/autoscore.ts | 311 ++-- src/goscorer/LICENSE.txt | 16 + src/goscorer/README.md | 5 + src/goscorer/goscorer.d.ts | 89 + src/goscorer/goscorer.js | 1508 +++++++++++++++++ src/protocol/ClientToServer.ts | 13 +- test/autoscore_test_files/game_33811578.json | 24 +- test/autoscore_test_files/game_33921785.json | 13 +- test/autoscore_test_files/game_35115094.json | 23 +- test/autoscore_test_files/game_35115743.json | 23 +- test/autoscore_test_files/game_64554594.json | 23 +- .../game_dev_51749995.json | 31 +- .../game_seki_64848549.json | 88 + test/test_autoscore.ts | 334 ++-- tsconfig.json | 1 + webpack.config.js | 5 - 29 files changed, 2684 insertions(+), 354 deletions(-) create mode 100644 src/goscorer/LICENSE.txt create mode 100644 src/goscorer/README.md create mode 100644 src/goscorer/goscorer.d.ts create mode 100644 src/goscorer/goscorer.js create mode 100644 test/autoscore_test_files/game_seki_64848549.json 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..9893224c 100644 --- a/.vscode/cspell.json +++ b/.vscode/cspell.json @@ -61,6 +61,7 @@ "goban", "Gobans", "goquest", + "goscorer", "groupify", "Gulpfile", "hane", @@ -89,6 +90,7 @@ "kyus", "Leela", "lerp", + "lightvector", "linebreak", "localstorage", "malk", @@ -170,6 +172,7 @@ "unhighlight", "unitify", "Unranked", + "unscorable", "unstarted", "uservoice", "usgc", @@ -187,7 +190,5 @@ "zoomable" ], "language": "en,en-GB", - "ignorePaths": [ - "test/autoscore_test_files" - ] + "ignorePaths": ["test/autoscore_test_files", "src/goscorer"] } diff --git a/Makefile b/Makefile index 62f5f0c4..cac3c7d2 100644 --- a/Makefile +++ b/Makefile @@ -3,6 +3,11 @@ SLACK_WEBHOOK=$(shell cat ../ogs/.slack-webhook) all dev: yarn run dev + +build lib types: + yarn run build-debug + yarn run build-production + cp -Rp lib/src/* lib/ lint: yarn run lint @@ -20,9 +25,7 @@ publish push: publish_npm 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 +33,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: @@ -44,4 +45,4 @@ upload_to_cdn: 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/src/GoEngine.ts b/src/GoEngine.ts index 22bc9d26..32b394ba 100644 --- a/src/GoEngine.ts +++ b/src/GoEngine.ts @@ -28,11 +28,13 @@ import { JGOFMove, JGOFPlayerSummary, JGOFIntersection, + JGOFSealingIntersection, } from "./JGOF"; import { AdHocPackedMove } from "./AdHocFormat"; import { _ } from "./translate"; import { EventEmitter } from "eventemitter3"; import { GameClock, StallingScoreEstimate } from "./protocol"; +import * as goscorer from "./goscorer/goscorer"; declare const CLIENT: boolean; declare const SERVER: boolean; @@ -173,8 +175,15 @@ export interface GoEngineConfig { */ 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?: { @@ -476,6 +485,7 @@ export class GoEngine extends EventEmitter { 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; @@ -682,6 +692,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); @@ -1585,50 +1605,118 @@ export class GoEngine extends EventEmitter { }; } + public toggleSingleGroupRemoval( + x: number, + y: number, + force_removal: boolean = false, + ): Array<[0 | 1, Group]> { + try { + if (x >= 0 && y >= 0) { + const removing: 0 | 1 = !this.removal[y][x] ? 1 : 0; + + /* 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, + ); + const groups = new GoStoneGroups(this, this.board); + const selected_group = groups.getGroup(x, y); + let total_territory_adjacency_count = 0; + let total_territory_group_count = 0; + selected_group.foreachNeighborSpaceGroup((gr) => { + let is_territory_group = false; + gr.foreachStone((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.points.length; + } + }); + if (total_territory_adjacency_count >= 5 || total_territory_group_count >= 2) { + console.log("This group is almost assuredly alive, refusing to remove"); + GobanCore.hooks.toast?.("refusing_to_remove_group_is_alive", 4000); + return [[0, []]]; + } + } + + /* Otherwise, toggle the group */ + const group_color = this.board[y][x]; + + if (group_color === JGOFNumericPlayerColor.EMPTY) { + /* Disallow toggling of open area (old dame marking method that we no longer desire) */ + return [[0, []]]; + } + + const removed_stones = this.setGroupForRemoval(x, y, removing, false)[1]; + + this.emit("stone-removal.updated"); + return [[removing, removed_stones]]; + } + } catch (err) { + console.log(err.stack); + } + + return [[0, []]]; + } + 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_color = this.board[y][x]; + + if (group_color === JGOFNumericPlayerColor.EMPTY) { + /* Disallow toggling of open area (old dame marking method that we no longer desire) */ + return [[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 } = {}; + //if (group_color === 0) { + /* just toggle open area */ + // This condition is historical and can be removed if we find we really don't need it - anoek 2024-06-08 + //} 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]; + 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])); - } + 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); } + empty_spaces.push(pt); } } + //} this.emit("stone-removal.updated"); @@ -1718,6 +1806,11 @@ export class GoEngine extends EventEmitter { this.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 getStoneRemovalString(): string { let ret = ""; const arr = []; @@ -1742,9 +1835,140 @@ export class GoEngine extends EventEmitter { return this.last_official_move.move_number; } + /** + * 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: { + total: 0, + stones: 0, + territory: 0, + prisoners: 0, + scoring_positions: "", + handicap: this.getHandicapPointAdjustmentForWhite(), + komi: this.komi, + }, + black: { + total: 0, + stones: 0, + territory: 0, + prisoners: 0, + scoring_positions: "", + handicap: 0, + komi: 0, + }, + }; + + // Tally up prisoners when appropriate + if (only_prisoners || this.score_prisoners) { + ret.white.prisoners = this.white_prisoners; + ret.black.prisoners = this.black_prisoners; + + 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] === JGOFNumericPlayerColor.BLACK) { + ret.white.prisoners += 1; + } + if (this.board[y][x] === JGOFNumericPlayerColor.WHITE) { + ret.black.prisoners += 1; + } + } + } + } + } + + // 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"); + } + + if (this.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) { + ret.black.stones += 1; + } else { + ret.black.territory += 1; + } + ret.black.scoring_positions += GoMath.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 += GoMath.encodeMove(x, y); + } + } + } + } 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 (scoring[y][x].isTerritoryFor === goscorer.BLACK) { + ret.black.territory += 1; + ret.black.scoring_positions += GoMath.encodeMove(x, y); + } else if (scoring[y][x].isTerritoryFor === goscorer.WHITE) { + ret.white.territory += 1; + ret.white.scoring_positions += GoMath.encodeMove(x, y); + } + } + } + } + } + + ret["black"].total = + ret["black"].stones + + ret["black"].territory + + ret["black"].prisoners + + ret["black"].handicap + + ret["black"].komi; + ret["white"].total = + ret["white"].stones + + ret["white"].territory + + ret["white"].prisoners + + ret["white"].handicap + + ret["white"].komi; + + try { + if (this.outcome && this.aga_handicap_scoring) { + /* We used to have an AGA scoring bug where we'd give one point per + * handicap stone instead of per handicap stone - 1, so this check + * is for those games that we incorrectly scored so that our little + * drop down box tallies up to be "correct" for those old games + * - anoek 2015-02-01 + */ + const f = parseFloat(this.outcome); + if (f - 1 === Math.abs(ret.white.total - ret.black.total)) { + ret.white.handicap += 1; + } + } + } catch (e) { + console.log(e); + } + + this.jumpTo(this.cur_move); + + return ret; + } + /* Returns a details object containing the total score and the breakdown of the * scoring details */ - public computeScore(only_prisoners?: boolean): Score { + public computeScoreOld(only_prisoners?: boolean): Score { const ret = { white: { total: 0, diff --git a/src/GoMath.ts b/src/GoMath.ts index 88097fb2..01b3d375 100644 --- a/src/GoMath.ts +++ b/src/GoMath.ts @@ -370,14 +370,28 @@ export function trimJGOFMoves(arr: Array): Array { } /** 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); +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((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; + }); + } else { + const arr = decodeMoves(moves, width, height); + arr.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(arr); + } } // OJE Sequence format is '.root.K10.Q1' ... diff --git a/src/GoStoneGroup.ts b/src/GoStoneGroup.ts index daad7f42..18460de4 100644 --- a/src/GoStoneGroup.ts +++ b/src/GoStoneGroup.ts @@ -154,13 +154,14 @@ export class GoStoneGroup { } } } - computeIsStrongString(): void { + computeIsStrongString(): boolean { /* 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; + return this.is_strong_string; } computeIsTerritory(): void { /* An empty group is considered territory if all of it's neighbors are of diff --git a/src/GoStoneGroups.ts b/src/GoStoneGroups.ts index 483350dd..3092f245 100644 --- a/src/GoStoneGroups.ts +++ b/src/GoStoneGroups.ts @@ -138,4 +138,8 @@ export class GoStoneGroups { fn(this.groups[i]); } } + + public getGroup(x: number, y: number): GoStoneGroup { + return this.groups[this.group_id_map[y][x]]; + } } diff --git a/src/GobanCanvas.ts b/src/GobanCanvas.ts index 4ed4bcb3..72812ac4 100644 --- a/src/GobanCanvas.ts +++ b/src/GobanCanvas.ts @@ -343,8 +343,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 */ @@ -405,7 +407,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 +416,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") { @@ -728,7 +731,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 && @@ -835,11 +843,11 @@ export class GobanCanvas extends GobanCore implements GobanCanvasInterface { ) { let removed: 0 | 1; let group: Group; - if (event.shiftKey) { - removed = !this.engine.removal[y][x] ? 1 : 0; - group = [{ x, y }]; + if (event.shiftKey || press_duration_ms > 500) { + //[[removed, group]] = this.engine.toggleMetaGroupRemoval(x, y); + [[removed, group]] = this.engine.toggleSingleGroupRemoval(x, y, true); } else { - [[removed, group]] = this.engine.toggleMetaGroupRemoval(x, y); + [[removed, group]] = this.engine.toggleSingleGroupRemoval(x, y); } if (group.length) { @@ -1551,7 +1559,8 @@ export class GobanCanvas extends GobanCore implements GobanCanvasInterface { transparent = 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 && @@ -1703,6 +1712,32 @@ 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] + ) { + ctx.lineCap = "square"; + ctx.save(); + ctx.beginPath(); + ctx.lineWidth = this.square_size * 0.125; + if (transparent) { + ctx.globalAlpha = 0.6; + } + 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; + } } } @@ -1718,7 +1753,7 @@ export class GobanCanvas extends GobanCore implements GobanCanvasInterface { ((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)) ) { ctx.beginPath(); @@ -1746,9 +1781,10 @@ export class GobanCanvas extends GobanCore implements GobanCanvasInterface { this.engine.removal[j][i] ) { color = "dame"; - if (pos.needs_sealing) { - color = "seal"; - } + } + + if (pos.needs_sealing) { + color = "seal"; } if (color === "white") { @@ -2344,7 +2380,7 @@ export class GobanCanvas extends GobanCore implements GobanCanvasInterface { ((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)) ) { let color = pos.score; if ( @@ -2370,10 +2406,10 @@ export class GobanCanvas extends GobanCore implements GobanCanvasInterface { this.engine.removal[j][i] ) { color = "dame"; + } - if (pos.needs_sealing) { - color = "seal"; - } + if (pos.needs_sealing) { + color = "seal"; } if ( diff --git a/src/GobanCore.ts b/src/GobanCore.ts index 404c6421..5130e59b 100644 --- a/src/GobanCore.ts +++ b/src/GobanCore.ts @@ -44,6 +44,7 @@ import { JGOFNumericPlayerColor, JGOFPauseState, JGOFPlayerSummary, + JGOFSealingIntersection, } from "./JGOF"; import { AdHocClock, AdHocPlayerClock, AdHocPauseControl } from "./AdHocFormat"; import { MessageID } from "./messages"; @@ -271,7 +272,7 @@ export interface Events extends StateUpdateEvents { "captured-stones": (obj: { removed_stones: Array }) => void; "stone-removal.accepted": () => void; "stone-removal.updated": () => void; - "needs-sealing": (positions: undefined | [number, number][]) => void; + "stone-removal.needs-sealing": (positions: undefined | JGOFSealingIntersection[]) => void; "conditional-moves.updated": () => void; "puzzle-place": (obj: { x: number; @@ -355,6 +356,8 @@ export interface GobanHooks { est_winning_color: "black" | "white", number_of_points: number, ) => void; + + toast?: (message_id: string, duration?: number) => void; } export interface GobanMetrics { @@ -1944,6 +1947,9 @@ export abstract class GobanCore extends EventEmitter { this.redraw(true); } + /* 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"; @@ -3192,7 +3198,9 @@ export abstract class GobanCore extends EventEmitter { } this.emit("stone-removal.updated"); - this.emit("needs-sealing", se.autoscored_needs_sealing); + + this.engine.needs_sealing = se.autoscored_needs_sealing; + this.emit("stone-removal.needs-sealing", se.autoscored_needs_sealing); this.updateTitleAndStonePlacement(); this.emit("update"); @@ -3200,11 +3208,13 @@ export abstract class GobanCore extends EventEmitter { 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, }); diff --git a/src/GobanSVG.ts b/src/GobanSVG.ts index 3bc52902..d711afd1 100644 --- a/src/GobanSVG.ts +++ b/src/GobanSVG.ts @@ -300,8 +300,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; @@ -361,7 +363,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 +372,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") { @@ -700,7 +703,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 && @@ -807,11 +815,11 @@ export class GobanSVG extends GobanCore implements GobanSVGInterface { ) { let removed: 0 | 1; let group: Group; - if (event.shiftKey) { - removed = !this.engine.removal[y][x] ? 1 : 0; - group = [{ x, y }]; + if (event.shiftKey || press_duration_ms > 500) { + //[[removed, group]] = this.engine.toggleMetaGroupRemoval(x, y); + [[removed, group]] = this.engine.toggleSingleGroupRemoval(x, y, true); } else { - [[removed, group]] = this.engine.toggleMetaGroupRemoval(x, y); + [[removed, group]] = this.engine.toggleSingleGroupRemoval(x, y); } if (group.length) { @@ -1472,7 +1480,8 @@ export class GobanSVG extends GobanCore implements GobanSVGInterface { transparent = 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 && @@ -1618,6 +1627,37 @@ export class GobanSVG extends GobanCore implements GobanSVGInterface { 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] + ) { + 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} + `, + ); + if (transparent) { + cross.setAttribute("stroke-opacity", "0.6"); + } + console.log("Drawing removal cross"); + + cell.appendChild(cross); + } } } @@ -1633,7 +1673,7 @@ export class GobanSVG extends GobanCore implements GobanSVGInterface { ((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)) ) { let color = pos.score; if ( @@ -1659,10 +1699,10 @@ export class GobanSVG extends GobanCore implements GobanSVGInterface { this.engine.removal[j][i] ) { color = "dame"; + } - if (pos.needs_sealing) { - color = "seal"; - } + if (pos.needs_sealing) { + color = "seal"; } const r = this.square_size * 0.15; @@ -2281,7 +2321,7 @@ export class GobanSVG extends GobanCore implements GobanSVGInterface { ((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)) ) { let color = pos.score; if ( @@ -2307,10 +2347,10 @@ export class GobanSVG extends GobanCore implements GobanSVGInterface { this.engine.removal[j][i] ) { color = "dame"; + } - if (pos.needs_sealing) { - color = "seal"; - } + if (pos.needs_sealing) { + color = "seal"; } if ( diff --git a/src/JGOF.ts b/src/JGOF.ts index f41885e5..58d721a3 100644 --- a/src/JGOF.ts +++ b/src/JGOF.ts @@ -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/ScoreEstimator.ts b/src/ScoreEstimator.ts index bada213d..280ca6a9 100644 --- a/src/ScoreEstimator.ts +++ b/src/ScoreEstimator.ts @@ -21,7 +21,7 @@ import { GoStoneGroup } from "./GoStoneGroup"; import { GoStoneGroups } from "./GoStoneGroups"; import { GobanCore } from "./GobanCore"; import { GoEngine, PlayerScore, GoEngineRules } from "./GoEngine"; -import { JGOFNumericPlayerColor } from "./JGOF"; +import { JGOFMove, JGOFNumericPlayerColor, JGOFSealingIntersection } from "./JGOF"; import { _ } from "./translate"; import { estimateScoreWasm } from "./local_estimators/wasm_estimator"; @@ -64,10 +64,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[]; /** Coordinates that still need sealing */ - autoscored_needs_sealing?: [number, number][]; + autoscored_needs_sealing?: JGOFSealingIntersection[]; } let remote_scorer: ((req: ScoreEstimateRequest) => Promise) | undefined; @@ -137,9 +137,9 @@ 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?: [number, number][]; + public autoscored_needs_sealing?: JGOFSealingIntersection[]; constructor( goban_callback: GobanCore | undefined, @@ -307,7 +307,7 @@ 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 { console.warn("Not able to use autoscored_removed for getProbablyDead"); } @@ -429,7 +429,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"); } diff --git a/src/__tests__/autoscore.test.ts b/src/__tests__/autoscore.test.ts index 4f3e81f1..db233b2e 100644 --- a/src/__tests__/autoscore.test.ts +++ b/src/__tests__/autoscore.test.ts @@ -36,7 +36,12 @@ describe("Auto-score tests ", () => { } // 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) { diff --git a/src/autoscore.ts b/src/autoscore.ts index f74967cc..d22e9cb2 100644 --- a/src/autoscore.ts +++ b/src/autoscore.ts @@ -22,15 +22,15 @@ */ import { GoStoneGroups } from "./GoStoneGroups"; -import { JGOFNumericPlayerColor } from "./JGOF"; +import { JGOFNumericPlayerColor, JGOFSealingIntersection, JGOFMove } from "./JGOF"; import { char2num, makeMatrix, num2char, pretty_coor_num2ch } from "./GoMath"; -import { GoEngine, GoEngineInitialState } from "./GoEngine"; +import { GoEngine, GoEngineInitialState, GoEngineRules } from "./GoEngine"; interface AutoscoreResults { result: JGOFNumericPlayerColor[][]; - removed_string: string; - removed: [number, number, /* reason */ string][]; - needs_sealing: [number, number][]; + sealed_result: JGOFNumericPlayerColor[][]; + removed: JGOFMove[]; + needs_sealing: JGOFSealingIntersection[]; } const REMOVAL_THRESHOLD = 0.7; @@ -50,19 +50,21 @@ function isBlack(ownership: number): boolean { export function autoscore( board: JGOFNumericPlayerColor[][], + rules: GoEngineRules, 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 removed: JGOFMove[] = []; 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 final_sealed_ownership = makeMatrix(board[0].length, board.length); const sealed = makeMatrix(width, height); - const needs_sealing: [number, number][] = []; + const needs_sealing: JGOFSealingIntersection[] = []; const average_ownership = makeMatrix(width, height); for (let y = 0; y < height; ++y) { @@ -89,17 +91,22 @@ export function autoscore( debug_groups("Groups", groups); // Perform our removal logic + settle_agreed_upon_stones(); settle_agreed_upon_territory(); remove_obviously_dead_stones(); - settle_agreed_upon_territory(); clear_unsettled_stones_from_territory(); seal_territory(); score_positions(); + stage("Final state"); + debug_board_output("Final ownership", final_ownership); + debug_boolean_board("Sealed", sealed, "s"); + debug_board_output("Final sealed ownership", final_sealed_ownership); + return [ { result: final_ownership, - removed_string: removed.map((pt) => `${num2char(pt[0])}${num2char(pt[1])}`).join(""), + sealed_result: final_sealed_ownership, removed, needs_sealing, }, @@ -107,15 +114,15 @@ export function autoscore( ]; /** Marks a position as being removed (either dead stone or dame) */ - function remove(x: number, y: number, reason: string) { + function remove(x: number, y: number, removal_reason: string) { if (removal[y][x]) { return; } - removed.push([x, y, reason]); + removed.push({ x, y, removal_reason }); board[y][x] = JGOFNumericPlayerColor.EMPTY; removal[y][x] = 1; - stage_log(`Removing ${pretty_coor_num2ch(x)}${height - y}: ${reason}`); + stage_log(`Removing ${pretty_coor_num2ch(x)}${height - y}: ${removal_reason}`); } /* @@ -180,6 +187,38 @@ export function autoscore( 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 * @@ -188,7 +227,7 @@ export function autoscore( * function detects these cases and removes the stones. */ function remove_obviously_dead_stones() { - stage("Removing stones both agree on:"); + stage("Removing stones both estimates agree upon"); for (let y = 0; y < height; ++y) { for (let x = 0; x < width; ++x) { if ( @@ -204,15 +243,6 @@ export function autoscore( ) { 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"); - } - */ } } debug_boolean_board("Removed", removal, "x"); @@ -406,6 +436,11 @@ export function autoscore( const avg = total_ownership / group.points.length; if (avg <= WHITE_SEAL_THRESHOLD || avg >= BLACK_SEAL_THRESHOLD) { + const color = + avg <= WHITE_SEAL_THRESHOLD + ? JGOFNumericPlayerColor.WHITE + : JGOFNumericPlayerColor.BLACK; + group.foreachStone((point) => { const x = point.x; const y = point.y; @@ -422,11 +457,11 @@ export function autoscore( board[y][x - 1] === opposing_color; if (adjacent_to_opposing_color) { - remove(x, y, "sealing territory"); + //remove(x, y, "sealing territory"); is_settled[y][x] = 1; settled[y][x] = JGOFNumericPlayerColor.EMPTY; sealed[y][x] = 1; - needs_sealing.push([x, y]); + needs_sealing.push({ x, y, color }); } }); } @@ -448,6 +483,7 @@ export function autoscore( } } + /** Compute our final ownership and scoring positions */ function score_positions() { stage("Score positions"); let black_state = ""; @@ -465,61 +501,124 @@ export function autoscore( } } - const initial_state: GoEngineInitialState = { + 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: GoEngineInitialState = { black: black_state, white: white_state, }; + const sealed_initial_state: GoEngineInitialState = { + black: sealed_black_state, + white: sealed_white_state, + }; - const removed_string = removed.map((pt) => `${num2char(pt[0])}${num2char(pt[1])}`).join(""); + for (const initial_state of [sealed_initial_state, real_initial_state]) { + const cur_ownership = makeMatrix(width, height); - const engine = new GoEngine({ - width: original_board[0].length, - height: original_board.length, - initial_state, - rules: "japanese", // so we can test seki code - removed: removed_string, - }); + const engine = new GoEngine({ + width: original_board[0].length, + height: original_board.length, + initial_state, + rules, + removed, + }); - const score = engine.computeScore(); - const scoring_positions = makeMatrix(width, height); + const board = engine.board.map((row) => row.slice()); + removed.map((pt) => (board[pt.y][pt.x] = 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]); - final_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]); - final_ownership[y][x] = JGOFNumericPlayerColor.WHITE; - scoring_positions[y][x] = JGOFNumericPlayerColor.WHITE; - } + const score = engine.computeScore(); + const scoring_positions = makeMatrix(width, height); + + 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) { - final_ownership[y][x] = board[y][x]; + 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 GoStoneGroups( - { - width, - height, - board, - removal, - }, - original_board, - ); + const groups = new GoStoneGroups( + { + width, + height, + 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'; + } + } - debug_groups("groups", groups); + ownership_string += " ]\n"; - debug_board_output("Scoring positions (JP)", scoring_positions); - debug_board_output("Board", board); - debug_board_output("Final ownership", final_ownership); - debug_boolean_board("Sealed", sealed, "s"); + stage_log(ownership_string); + } } } @@ -813,6 +912,17 @@ 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) { @@ -826,55 +936,56 @@ function end_stage() { return; } - const title_line = `#### ${current_stage} ####`; - const pounds = "#".repeat(title_line.length); - final_output += `\n\n${pounds}\n${title_line}\n${pounds}\n\n`; current_stage = ""; - const wide_lines: string[] = []; - const str_grid: string[][] = []; - const segment_length = 30; + 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) { - const lines = board_outputs[x].split("\n"); - for (let y = 0; y < lines.length; ++y) { - if (!str_grid[y]) { - str_grid[y] = []; + 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]; } - str_grid[y][x] = lines[y]; } - } - 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] ?? ""; - /* + 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"); + 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)) + + " "; } - line += - (str_grid[y][x] ?? "") + - " ".repeat(Math.max(0, segment_length - length_without_color_codes)) + - " "; + final_output += line + "\n"; + //wide_lines.push(line); } - final_output += line + "\n"; - //wide_lines.push(line); - } - for (let i = 0; i < wide_lines.length; ++i) { - final_output += wide_lines[i] + "\n"; + for (let i = 0; i < wide_lines.length; ++i) { + final_output += wide_lines[i] + "\n"; + } } - board_outputs = []; - if (current_stage_log) { final_output += "\n\nLog:\n" + current_stage_log + "\n"; current_stage_log = ""; diff --git a/src/goscorer/LICENSE.txt b/src/goscorer/LICENSE.txt new file mode 100644 index 00000000..7c9cb72a --- /dev/null +++ b/src/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/goscorer/README.md b/src/goscorer/README.md new file mode 100644 index 00000000..05db26f9 --- /dev/null +++ b/src/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/goscorer/goscorer.d.ts b/src/goscorer/goscorer.d.ts new file mode 100644 index 00000000..87f5cebd --- /dev/null +++ b/src/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/goscorer/goscorer.js b/src/goscorer/goscorer.js new file mode 100644 index 00000000..c9c0dec3 --- /dev/null +++ b/src/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/src/protocol/ClientToServer.ts b/src/protocol/ClientToServer.ts index 18bfe4af..cfe91b2b 100644 --- a/src/protocol/ClientToServer.ts +++ b/src/protocol/ClientToServer.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import type { JGOFPlayerClock } from "../JGOF"; +import type { JGOFMove, JGOFPlayerClock, JGOFSealingIntersection } from "../JGOF"; import type { ReviewMessage } from "../GoEngine"; import type { ConditionalMoveResponse } from "../GoConditionalMove"; @@ -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 diff --git a/test/autoscore_test_files/game_33811578.json b/test/autoscore_test_files/game_33811578.json index 30e3018a..c9a14000 100644 --- a/test/autoscore_test_files/game_33811578.json +++ b/test/autoscore_test_files/game_33811578.json @@ -63,7 +63,7 @@ [ 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", @@ -83,6 +83,26 @@ "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_33921785.json b/test/autoscore_test_files/game_33921785.json index 3a22af6a..5a5a9db7 100644 --- a/test/autoscore_test_files/game_33921785.json +++ b/test/autoscore_test_files/game_33921785.json @@ -33,7 +33,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, -1.0, -1.0] ], - "correct_ownership": [ + "sealed_ownership": [ "WWWBBBBBB", "WWWWBBBBB", "WWWBBBBBB", @@ -43,5 +43,16 @@ "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 a184f805..a4fdf00c 100644 --- a/test/autoscore_test_files/game_35115094.json +++ b/test/autoscore_test_files/game_35115094.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, 1.0, 1.0, -1.0], [-1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -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", @@ -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 d585028e..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", @@ -83,5 +83,26 @@ "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_dev_51749995.json b/test/autoscore_test_files/game_dev_51749995.json index 9d956b58..07d323a3 100644 --- a/test/autoscore_test_files/game_dev_51749995.json +++ b/test/autoscore_test_files/game_dev_51749995.json @@ -63,18 +63,18 @@ [ 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] ], - "correct_ownership": [ + "sealed_ownership": [ "BBBBBBBBBBBBBWWWWWW", "BBBBBBBBBBBBWWWWWWW", "BBBWBBBBBBBWWWWWWWW", - "BBBWW BsBBWWBBBWWWW", - "BBBBWWWWWBWsBBBBWWW", + "BBBWW*BsBBWWBBBWWWW", + "BBBBWWWWWBWs*BBBWWW", "BBBBBBWWWBWBBBBBWWW", "BBBBsWWWWWBBWWBBWWW", "BBBBBWWWWWBBBWWWWWW", "BBBBBWWWWBBBBWWWWWW", - "BBBBBB W BBBWWWBBW ", - "BBBBBBBWWBsBBW BWWB", + "BBBBBB*W*BBBWWWBBW*", + "BBBBBBBWWBsBBW*BWWB", "BBBBBBBBWWWWBBBBBBB", "BBBBBBBBBBWWWWBBBBB", "BBBBBBBBBBBBWBBBBBB", @@ -83,5 +83,26 @@ "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_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 90047fd4..1b14a124 100755 --- a/test/test_autoscore.ts +++ b/test/test_autoscore.ts @@ -31,7 +31,7 @@ import { autoscore } from "../src/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 { JGOFMove, JGOFNumericPlayerColor, JGOFSealingIntersection } from "../src/JGOF"; function run_autoscore_tests() { const test_file_directory = "autoscore_test_files"; @@ -109,6 +109,10 @@ 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 = @@ -124,174 +128,220 @@ function test_file(path: string, quiet: boolean): boolean { } } } + 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, 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]; - let m = - data.correct_ownership[y][x] === "*" || - data.correct_ownership[y][x] === "s" || // seal - (v === 0 && data.correct_ownership[y][x] === " ") || - (v === 1 && data.correct_ownership[y][x] === "B") || - (v === 2 && data.correct_ownership[y][x] === "W"); - - if (data.correct_ownership[y][x] === "s") { - const has_needs_sealing = - res.needs_sealing.find(([x2, y2]) => x2 === x && y2 === y) !== undefined; - - m &&= has_needs_sealing; - } + const [res, debug_output] = autoscore(data.board, rules, data.black, data.white); - matches[y][x] = m; - match &&= m; - } + if (!quiet) { + console.log(""); + console.log(debug_output); + console.log(""); } - /* Ensure all needs_sealing are marked as such */ - for (const [x, y] of res.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; - matches[y][x] = false; - } + 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, + ); - 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; - } + 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 GoEngine.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: GoEngineInitialState = { + black: black_state, + white: white_state, + }; - 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] === "s") || - (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; - } - } + const engine = new GoEngine({ + width: board[0].length, + height: board.length, + initial_state, + rules, + removed, + }); - if (!quiet) { - 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(""), - ), - ); + const score = engine.computeScore(); + + const scored_board = makeMatrix(board[0].length, board.length, 0); - 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); + 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; } - } - match &&= official_match; - } + 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 (!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(""); + 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(""), + ), + ); + + 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); + } + } - /* - console.log("Removed"); - for (const [x, y, reason] of res.removed) { - console.log( - ` ${"ABCDEFGHJKLMNOPQRSTUVWXYZ"[x]}${data.board.length - y}: ${reason}`, - ); + match &&= official_match; } - */ - 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); + if (!match) { + console.log("Expected ownership:"); + print_expected(correct_ownership); + console.log("Mismatches:"); + print_mismatches(matches); + console.log(""); + + console.log(`${mnemonic} ${path} failed`); + } else { + console.log(`${mnemonic} ${path} passed`); + } console.log(""); console.log(""); - console.log(`${path} passed`); } - } - return match; + return match; + } } if (require.main === module) { diff --git a/tsconfig.json b/tsconfig.json index 3d68a67d..4085b2d7 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -25,6 +25,7 @@ "strictNullChecks": true, "strictPropertyInitialization": true, "noUnusedLocals": true, + "allowJs": true, "sourceMap": true, "jsx": "react" diff --git a/webpack.config.js b/webpack.config.js index 22214442..a6aa5815 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -166,11 +166,6 @@ module.exports = (env, argv) => { filename: '[name].js' }, - externals: { - "pixi.js": "PIXI", - "pixi.js-legacy": "PIXI", - }, - plugins: plugins.concat([ new webpack.DefinePlugin({ CLIENT: false, From fa31fa59a181eea1cfd5265711c70b500382f7b3 Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Sat, 8 Jun 2024 15:38:49 -0600 Subject: [PATCH 04/68] Fixed up tests --- jest.config.ts | 303 +++++++++++++-------------- src/GobanCanvas.ts | 1 - src/GobanCore.ts | 10 +- src/GobanSVG.ts | 2 - src/ScoreEstimator.ts | 3 + src/__tests__/GoEngine.test.ts | 23 +- src/__tests__/GobanCanvas.test.ts | 13 +- src/__tests__/GobanSVG.test.ts | 13 +- src/__tests__/ScoreEstimator.test.ts | 11 +- src/__tests__/autoscore.test.ts | 19 +- 10 files changed, 211 insertions(+), 187 deletions(-) diff --git a/jest.config.ts b/jest.config.ts index d68c4e62..5e88614e 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -4,206 +4,203 @@ */ 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, + // 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/**"], + // An array of glob patterns indicating a set of files for which coverage information should be collected + collectCoverageFrom: ["/src/**"], - // The directory where Jest should output its coverage files - coverageDirectory: "coverage", + // The directory where Jest should output its coverage files + coverageDirectory: "coverage", - // An array of regexp pattern strings used to skip coverage collection - // coveragePathIgnorePatterns: [ - // "/node_modules/" - // ], - coveragePathIgnorePatterns: [ - "/src/test.tsx", - "/src/goban.ts", - "/src/engine.ts", - ], + // An array of regexp pattern strings used to skip coverage collection + // coveragePathIgnorePatterns: [ + // "/node_modules/" + // ], + coveragePathIgnorePatterns: [ + "/src/test.tsx", + "/src/goban.ts", + "/src/engine.ts", + ".d.ts", + "wasm_estimator.ts", + ], - // Indicates which provider should be used to instrument code for coverage - coverageProvider: "v8", + // Indicates which provider should be used to instrument code for coverage + coverageProvider: "v8", - // A list of reporter names that Jest uses when writing coverage reports - // coverageReporters: [ - // "json", - // "text", - // "lcov", - // "clover" - // ], + // A list of reporter names that Jest uses when writing coverage reports + // coverageReporters: [ + // "json", + // "text", + // "lcov", + // "clover" + // ], - // An object that configures minimum threshold enforcement for coverage results - coverageThreshold: { - "global": { - "lines": 60 - } - }, + // An object that configures minimum threshold enforcement for coverage results + coverageThreshold: { + global: { + lines: 60, + }, + }, - // A path to a custom dependency extractor - // dependencyExtractor: undefined, + // A path to a custom dependency extractor + // dependencyExtractor: undefined, - // Make calling deprecated APIs throw helpful error messages - // errorOnDeprecated: false, + // Make calling deprecated APIs throw helpful error messages + // errorOnDeprecated: false, - // The default configuration for fake timers - // fakeTimers: { - // "enableGlobally": false - // }, + // The default configuration for fake timers + // fakeTimers: { + // "enableGlobally": false + // }, - // Force coverage collection from ignored files using an array of glob patterns - // forceCoverageMatch: [], + // 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 before all test suites - // globalSetup: undefined, + // A path to a module which exports an async function that is triggered once before all test suites + // globalSetup: undefined, - // 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 after all test suites + // globalTeardown: undefined, - // A set of global variables that need to be available in all test environments - // globals: {}, + // A set of global variables that need to be available in all test environments + // globals: {}, - // 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%", + // 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 directory names to be searched recursively up from the requiring module's location - // moduleDirectories: [ - // "node_modules" - // ], + // An array of directory names to be searched recursively up from the requiring module's location + // moduleDirectories: [ + // "node_modules" + // ], - // An array of file extensions your modules use - // moduleFileExtensions: [ - // "js", - // "mjs", - // "cjs", - // "jsx", - // "ts", - // "tsx", - // "json", - // "node" - // ], + // An array of file extensions your modules use + // moduleFileExtensions: [ + // "js", + // "mjs", + // "cjs", + // "jsx", + // "ts", + // "tsx", + // "json", + // "node" + // ], - // 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: {}, + // 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 regexp pattern strings, matched against all module paths before considered 'visible' to the module loader - // modulePathIgnorePatterns: [], + // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader + // modulePathIgnorePatterns: [], - // Activates notifications for test results - // notify: false, + // Activates notifications for test results + // notify: false, - // An enum that specifies notification mode. Requires { notify: true } - // notifyMode: "failure-change", + // An enum that specifies notification mode. Requires { notify: true } + // notifyMode: "failure-change", - // A preset that is used as a base for Jest's configuration - preset: 'ts-jest', + // A preset that is used as a base for Jest's configuration + preset: "ts-jest", - // Run tests from one or more projects - // projects: undefined, + // Run tests from one or more projects + // projects: undefined, - // Use this configuration option to add custom reporters to Jest - // reporters: undefined, + // Use this configuration option to add custom reporters to Jest + // reporters: undefined, - // Automatically reset mock state before every test - // resetMocks: false, + // Automatically reset mock state before every test + // resetMocks: false, - // Reset the module registry before running each individual test - // resetModules: false, + // Reset the module registry before running each individual test + // resetModules: false, - // A path to a custom resolver - // resolver: undefined, + // A path to a custom resolver + // resolver: undefined, - // Automatically restore mock state and implementation before every test - // restoreMocks: false, + // Automatically restore mock state and implementation before every test + // restoreMocks: false, - // The root directory that Jest should scan for tests and modules within - // rootDir: undefined, + // The root directory that Jest should scan for tests and modules within + // rootDir: undefined, - // A list of paths to directories that Jest should use to search for files in - // roots: [ - // "" - // ], + // A list of paths to directories that Jest should use to search for files in + // roots: [ + // "" + // ], - // Allows you to use a custom runner instead of Jest's default test runner - // runner: "jest-runner", + // Allows you to use a custom runner instead of Jest's default test runner + // runner: "jest-runner", - // The paths to modules that run some code to configure or set up the testing environment before each test - // setupFiles: [], + // The paths to modules that run some code to configure or set up the testing environment before each test + // setupFiles: [], - // 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 modules that run some code to configure or set up the testing framework before each test + // setupFilesAfterEnv: [], - // The number of seconds after which a test is considered as slow and reported as such in the results. - // slowTestThreshold: 5, + // 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 snapshot serializer modules Jest should use for snapshot testing - // snapshotSerializers: [], + // A list of paths to snapshot serializer modules Jest should use for snapshot testing + // snapshotSerializers: [], - // The test environment that will be used for testing - // testEnvironment: "jest-environment-node", + // The test environment that will be used for testing + // testEnvironment: "jest-environment-node", - // Options that will be passed to the testEnvironment - // testEnvironmentOptions: {}, + // Options that will be passed to the testEnvironment + // testEnvironmentOptions: {}, - // Adds a location field to test results - // testLocationInResults: false, + // Adds a location field to test results + // testLocationInResults: false, - // The glob patterns Jest uses to detect test files - testMatch: [ - "/src/**/__tests__/**/*.[jt]s?(x)", - "/src/**/?(*.)+(spec|test).ts" - ], + // The glob patterns Jest uses to detect test files + testMatch: ["/src/**/__tests__/**/*.[jt]s?(x)", "/src/**/?(*.)+(spec|test).ts"], - // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped - // testPathIgnorePatterns: [ - // "/node_modules/" - // ], + // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped + // testPathIgnorePatterns: [ + // "/node_modules/" + // ], - // The regexp pattern or array of patterns that Jest uses to detect test files - // testRegex: [], + // The regexp pattern or array of patterns that Jest uses to detect test files + // testRegex: [], - // This option allows the use of a custom results processor - // testResultsProcessor: undefined, + // This option allows the use of a custom results processor + // testResultsProcessor: undefined, - // This option allows use of a custom test runner - // testRunner: "jest-circus/runner", + // This option allows use of a custom test runner + // testRunner: "jest-circus/runner", - // A map from regular expressions to paths to transformers - transform: { - '^.+\\.ts?$': 'ts-jest', - "^.+\\.svg$": "jest-transform-stub", - }, + // 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 source file paths, matched files will skip transformation - transformIgnorePatterns: [ - "/node_modules/", - ], + // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation + transformIgnorePatterns: ["/node_modules/"], - // 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 modules before the module loader will automatically return a mock for them + // unmockedModulePathPatterns: undefined, - // Indicates whether each individual test should be reported during the run - // verbose: undefined, + // Indicates whether each individual test should be reported during the run + // verbose: undefined, - // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode - // watchPathIgnorePatterns: [], + // 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, + // Whether to use watchman for file crawling + // watchman: true, - - "testTimeout": 200 + testTimeout: 200, }; diff --git a/src/GobanCanvas.ts b/src/GobanCanvas.ts index 72812ac4..338be1cd 100644 --- a/src/GobanCanvas.ts +++ b/src/GobanCanvas.ts @@ -149,7 +149,6 @@ export class GobanCanvas extends GobanCore implements GobanCanvasInterface { constructor(config: GobanCanvasConfig, 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) { diff --git a/src/GobanCore.ts b/src/GobanCore.ts index 5130e59b..c6e85919 100644 --- a/src/GobanCore.ts +++ b/src/GobanCore.ts @@ -4104,11 +4104,15 @@ class FocusTracker { constructor() { try { if (CLIENT) { - window.addEventListener("blur", this.onBlur); - window.addEventListener("focus", this.onFocus); + try { + window.addEventListener("blur", this.onBlur); + window.addEventListener("focus", this.onFocus); + } catch (e) { + console.error(e); + } } } catch (e) { - console.error(e); + // no CLIENT defined, no problem } } diff --git a/src/GobanSVG.ts b/src/GobanSVG.ts index d711afd1..a485bde7 100644 --- a/src/GobanSVG.ts +++ b/src/GobanSVG.ts @@ -154,7 +154,6 @@ export class GobanSVG extends GobanCore implements GobanSVGInterface { constructor(config: GobanSVGConfig, 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"]; @@ -1654,7 +1653,6 @@ export class GobanSVG extends GobanCore implements GobanSVGInterface { if (transparent) { cross.setAttribute("stroke-opacity", "0.6"); } - console.log("Drawing removal cross"); cell.appendChild(cross); } diff --git a/src/ScoreEstimator.ts b/src/ScoreEstimator.ts index 280ca6a9..9538cc32 100644 --- a/src/ScoreEstimator.ts +++ b/src/ScoreEstimator.ts @@ -309,6 +309,9 @@ export class ScoreEstimator { console.info("Returning autoscored_removed for getProbablyDead"); 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"); } diff --git a/src/__tests__/GoEngine.test.ts b/src/__tests__/GoEngine.test.ts index c0b48642..1ab224b8 100644 --- a/src/__tests__/GoEngine.test.ts +++ b/src/__tests__/GoEngine.test.ts @@ -155,14 +155,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, @@ -188,7 +188,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 +196,7 @@ describe("computeScore", () => { white: expect.objectContaining({ prisoners: 0, komi: 7.5, - scoring_positions: "dadbdcddcacbcccd", + scoring_positions: "cadacbdbccdccddd", stones: 4, territory: 4, total: 15.5, @@ -662,7 +662,7 @@ describe("groups", () => { expect(on_removal_updated).toBeCalledTimes(0); }); - test("toggleMetagroupRemoval empty area", () => { + test("toggleMetagroupRemoval empty area doesn't do anything", () => { const engine = new GoEngine({ width: 4, height: 2, @@ -670,20 +670,15 @@ describe("groups", () => { }); /* 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.toggleMetaGroupRemoval(0, 1)).toEqual([[0, []]]); + expect(on_removal_updated).toBeCalledTimes(0); }); test("clearRemoved", () => { diff --git a/src/__tests__/GobanCanvas.test.ts b/src/__tests__/GobanCanvas.test.ts index 3596c5c9..fa67d9d6 100644 --- a/src/__tests__/GobanCanvas.test.ts +++ b/src/__tests__/GobanCanvas.test.ts @@ -312,20 +312,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 +338,7 @@ describe("onTap", () => { canvas.dispatchEvent( new MouseEvent("click", { - clientX: 15, + clientX: 15 + TEST_SQUARE_SIZE, clientY: 15, shiftKey: true, }), @@ -349,7 +349,7 @@ describe("onTap", () => { "game/removed_stones/set", expect.objectContaining({ removed: true, - stones: "aa", + stones: "babb", }), ]), ); @@ -400,7 +400,7 @@ describe("onTap", () => { "game/removed_stones/set", expect.objectContaining({ removed: true, - stones: "babbbabbbbba", + stones: "babb", }), ]), ); @@ -430,6 +430,7 @@ describe("onTap", () => { SCORE_ESTIMATION_TRIALS, SCORE_ESTIMATION_TOLERANCE, false, + false, ); (goban.engine.estimateScore as jest.Mock).mockClear(); diff --git a/src/__tests__/GobanSVG.test.ts b/src/__tests__/GobanSVG.test.ts index b9e39201..df2dfef4 100644 --- a/src/__tests__/GobanSVG.test.ts +++ b/src/__tests__/GobanSVG.test.ts @@ -312,20 +312,20 @@ 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 () => { + test("Shift-Clicking during stone removal toggles the group ", async () => { const goban = new GobanSVG(basicScorableBoardConfig({ phase: "stone removal" })); const event_layer = goban.event_layer; @@ -338,7 +338,7 @@ describe("onTap", () => { event_layer.dispatchEvent( new MouseEvent("click", { - clientX: 15, + clientX: 15 + TEST_SQUARE_SIZE, clientY: 15, shiftKey: true, }), @@ -349,7 +349,7 @@ describe("onTap", () => { "game/removed_stones/set", expect.objectContaining({ removed: true, - stones: "aa", + stones: "babb", }), ]), ); @@ -399,7 +399,7 @@ describe("onTap", () => { "game/removed_stones/set", expect.objectContaining({ removed: true, - stones: "babbbabbbbba", + stones: "babb", }), ]), ); @@ -429,6 +429,7 @@ describe("onTap", () => { SCORE_ESTIMATION_TRIALS, SCORE_ESTIMATION_TOLERANCE, false, + false, ); (goban.engine.estimateScore as jest.Mock).mockClear(); diff --git a/src/__tests__/ScoreEstimator.test.ts b/src/__tests__/ScoreEstimator.test.ts index 9edccd3a..d5a4b11f 100644 --- a/src/__tests__/ScoreEstimator.test.ts +++ b/src/__tests__/ScoreEstimator.test.ts @@ -66,7 +66,13 @@ describe("ScoreEstimator", () => { beforeEach(() => { set_remote_scorer(async () => { - return { ownership: OWNERSHIP, score: -7.5 }; + return { + ownership: OWNERSHIP, + score: -7.5, + autoscored_board_state: OWNERSHIP, + autoscored_removed: [], + autoscored_needs_sealing: [], + }; }); set_local_scorer(estimateScoreVoronoi); @@ -324,6 +330,9 @@ describe("ScoreEstimator", () => { set_remote_scorer(async () => ({ ownership: OWNERSHIP, + autoscored_board_state: OWNERSHIP, + autoscored_removed: [], + autoscored_needs_sealing: [], })); const se = new ScoreEstimator(undefined, engine, 10, 0.5, true); diff --git a/src/__tests__/autoscore.test.ts b/src/__tests__/autoscore.test.ts index db233b2e..4ba64d7b 100644 --- a/src/__tests__/autoscore.test.ts +++ b/src/__tests__/autoscore.test.ts @@ -30,7 +30,12 @@ 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); } } @@ -49,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); }); } From 114d42da44fc7398802accc421b6eeb4e231e260 Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Sat, 8 Jun 2024 15:42:47 -0600 Subject: [PATCH 05/68] Bump various npm deps Fixes some errors with node 22 --- package.json | 4 +- yarn.lock | 1184 ++++++++++++++++++++++++-------------------------- 2 files changed, 560 insertions(+), 628 deletions(-) diff --git a/package.json b/package.json index 28312cbc..01429dc6 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,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", @@ -76,7 +76,7 @@ "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", "tslint": "^6.1.3", diff --git a/yarn.lock b/yarn.lock index 462747db..fac1d991 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": + 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": + 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,117 @@ 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== +"@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 +433,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 +448,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 +488,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 +503,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 +529,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 +568,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 +610,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 +645,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 +696,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" @@ -1103,9 +1112,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 +1175,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 +1232,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 +1267,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 +1308,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" @@ -1703,9 +1712,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" @@ -1962,22 +1971,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" @@ -2051,10 +2060,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 +2081,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,6 +2109,11 @@ 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" @@ -2131,9 +2140,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" @@ -2255,20 +2264,15 @@ 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" @@ -2321,17 +2325,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" @@ -2411,120 +2404,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 +2561,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" @@ -2712,22 +2699,15 @@ 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== - dependencies: - is-obj "^2.0.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" @@ -2755,9 +2735,9 @@ encodeurl@~1.0.2: integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== 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 +2747,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 +2777,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,21 +3058,6 @@ 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== - 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" - execa@^5.0.0: version "5.1.1" resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd" @@ -3103,6 +3073,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 +3224,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" @@ -3736,7 +3721,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== @@ -3859,11 +3844,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" @@ -3901,11 +3881,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" @@ -4541,10 +4516,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 +4527,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 +4644,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" @@ -4765,12 +4733,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 +4889,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 +4912,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" @@ -5005,9 +4973,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" @@ -5190,17 +5158,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 +5221,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" @@ -5502,7 +5470,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 +5560,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" @@ -5839,9 +5805,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 +5854,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== @@ -6063,9 +6029,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 +6069,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" @@ -6159,10 +6125,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" @@ -6209,9 +6175,9 @@ tslib@^1.13.0, tslib@^1.8.1: 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 +6237,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,16 +6246,9 @@ 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== - -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" + 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== typedoc@^0.25.6: version "0.25.13" @@ -6321,13 +6275,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 +6290,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 +6314,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" @@ -6436,9 +6383,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" @@ -6686,16 +6633,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 +6646,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,15 +6676,10 @@ 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" From 26d91da4010d9740592db73286a549eda310bdaf Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Sat, 8 Jun 2024 15:44:32 -0600 Subject: [PATCH 06/68] Bump typescript --- package.json | 2 +- yarn.lock | 7 +------ 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index 01429dc6..2cacaf34 100644 --- a/package.json +++ b/package.json @@ -81,7 +81,7 @@ "ts-node": "^10.9.1", "tslint": "^6.1.3", "typedoc": "^0.25.6", - "typescript": "=5.3.3", + "typescript": "=5.4.5", "utf-8-validate": "^6.0.3", "webpack": "^5.89.0", "webpack-cli": "^5.1.4", diff --git a/yarn.lock b/yarn.lock index fac1d991..02be5d78 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6260,12 +6260,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.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== From 5d58d4db9bb8f220b909b342e91f02a6d2d80cd2 Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Sat, 8 Jun 2024 16:42:05 -0600 Subject: [PATCH 07/68] Update score estimator to use goscorer --- src/ScoreEstimator.ts | 67 ++++++++++++++++++++++++++----------------- 1 file changed, 40 insertions(+), 27 deletions(-) diff --git a/src/ScoreEstimator.ts b/src/ScoreEstimator.ts index 9538cc32..71300147 100644 --- a/src/ScoreEstimator.ts +++ b/src/ScoreEstimator.ts @@ -24,6 +24,7 @@ import { GoEngine, PlayerScore, GoEngineRules } from "./GoEngine"; import { JGOFMove, JGOFNumericPlayerColor, JGOFSealingIntersection } from "./JGOF"; import { _ } from "./translate"; import { estimateScoreWasm } from "./local_estimators/wasm_estimator"; +import * as goscorer from "./goscorer/goscorer"; export { init_score_estimator, estimateScoreWasm } from "./local_estimators/wasm_estimator"; export { estimateScoreVoronoi } from "./local_estimators/voronoi"; @@ -457,9 +458,7 @@ export class ScoreEstimator { } /** - * 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 = { @@ -484,7 +483,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]) { @@ -499,34 +498,48 @@ 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 (this.board[y][x]) { - if (this.board[y][x] === 1) { - ++this.black.stones; - this.black.scoring_positions += encodeMove(x, y); + if (scoring[y][x] === goscorer.BLACK) { + if (this.board[y][x] === JGOFNumericPlayerColor.BLACK) { + this.black.stones += 1; } else { - ++this.white.stones; - this.white.scoring_positions += encodeMove(x, y); + this.black.territory += 1; } + this.black.scoring_positions += GoMath.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 += GoMath.encodeMove(x, y); + } + } + } + } 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 (scoring[y][x].isTerritoryFor === goscorer.BLACK) { + this.black.territory += 1; + this.black.scoring_positions += GoMath.encodeMove(x, y); + } else if (scoring[y][x].isTerritoryFor === goscorer.WHITE) { + this.white.territory += 1; + this.white.scoring_positions += GoMath.encodeMove(x, y); } } } From 17d3287056ee601bdc91f449705d70c5ed11457a Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Sat, 8 Jun 2024 16:51:39 -0600 Subject: [PATCH 08/68] Remove GoStoneGroup unused code --- src/GoEngine.ts | 171 ---------------------- src/GoStoneGroup.ts | 107 +------------- src/GoStoneGroups.ts | 12 -- src/GobanCore.ts | 2 +- src/ScoreEstimator.ts | 2 +- src/__tests__/GoMath_GoStoneGroup.test.ts | 32 ---- src/autoscore.ts | 3 +- 7 files changed, 11 insertions(+), 318 deletions(-) diff --git a/src/GoEngine.ts b/src/GoEngine.ts index 32b394ba..0217bae1 100644 --- a/src/GoEngine.ts +++ b/src/GoEngine.ts @@ -1966,177 +1966,6 @@ export class GoEngine extends EventEmitter { return ret; } - /* Returns a details object containing the total score and the breakdown of the - * scoring details */ - public computeScoreOld(only_prisoners?: boolean): Score { - const ret = { - white: { - total: 0, - stones: 0, - territory: 0, - prisoners: 0, - scoring_positions: "", - handicap: this.getHandicapPointAdjustmentForWhite(), - komi: this.komi, - }, - black: { - total: 0, - stones: 0, - territory: 0, - prisoners: 0, - scoring_positions: "", - handicap: 0, - komi: 0, - }, - }; - - let removed_black = 0; - let removed_white = 0; - - /* 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] === 2) { - ++removed_white; - } - 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); - } - 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 (!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; - ret.black.scoring_positions += encodeMove(x, y); - } else { - ++ret.white.stones; - 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 + - ret["black"].territory + - ret["black"].prisoners + - ret["black"].handicap + - ret["black"].komi; - ret["white"].total = - ret["white"].stones + - ret["white"].territory + - ret["white"].prisoners + - ret["white"].handicap + - ret["white"].komi; - - try { - if (this.outcome && this.aga_handicap_scoring) { - /* We used to have an AGA scoring bug where we'd give one point per - * handicap stone instead of per handicap stone - 1, so this check - * is for those games that we incorrectly scored so that our little - * drop down box tallies up to be "correct" for those old games - * - anoek 2015-02-01 - */ - const f = parseFloat(this.outcome); - if (f - 1 === Math.abs(ret.white.total - ret.black.total)) { - ret.white.handicap += 1; - } - } - } catch (e) { - console.log(e); - } - - this.jumpTo(this.cur_move); - - return ret; - } public handicapMovesLeft(): number { if (this.free_handicap_placement) { return Math.max(0, this.handicap - this.getMoveNumber()); diff --git a/src/GoStoneGroup.ts b/src/GoStoneGroup.ts index 18460de4..d52fc0a5 100644 --- a/src/GoStoneGroup.ts +++ b/src/GoStoneGroup.ts @@ -27,32 +27,25 @@ export interface BoardState { } 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; + public readonly points: 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 corner_groups: { [y: string]: { [x: string]: GoStoneGroup } }; 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 = {}; @@ -100,69 +93,9 @@ export class GoStoneGroup { 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(): boolean { - /* 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; - return this.is_strong_string; - } computeIsTerritory(): void { /* An empty group is considered territory if all of it's neighbors are of * the same color */ @@ -191,30 +124,4 @@ export class GoStoneGroup { 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/GoStoneGroups.ts b/src/GoStoneGroups.ts index 3092f245..c5cadb00 100644 --- a/src/GoStoneGroups.ts +++ b/src/GoStoneGroups.ts @@ -119,18 +119,6 @@ export class GoStoneGroups { 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(); - }); } public foreachGroup(fn: (gr: GoStoneGroup) => void) { diff --git a/src/GobanCore.ts b/src/GobanCore.ts index c6e85919..b5f3214a 100644 --- a/src/GobanCore.ts +++ b/src/GobanCore.ts @@ -357,7 +357,7 @@ export interface GobanHooks { number_of_points: number, ) => void; - toast?: (message_id: string, duration?: number) => void; + toast?: (message_id: string, duration: number) => void; } export interface GobanMetrics { diff --git a/src/ScoreEstimator.ts b/src/ScoreEstimator.ts index 71300147..45122862 100644 --- a/src/ScoreEstimator.ts +++ b/src/ScoreEstimator.ts @@ -15,7 +15,7 @@ */ import { dup } from "./GoUtil"; -import { encodeMove, encodeMoves } from "./GoMath"; +import { encodeMove } from "./GoMath"; import * as GoMath from "./GoMath"; import { GoStoneGroup } from "./GoStoneGroup"; import { GoStoneGroups } from "./GoStoneGroups"; diff --git a/src/__tests__/GoMath_GoStoneGroup.test.ts b/src/__tests__/GoMath_GoStoneGroup.test.ts index ebdda3fa..4983ca22 100644 --- a/src/__tests__/GoMath_GoStoneGroup.test.ts +++ b/src/__tests__/GoMath_GoStoneGroup.test.ts @@ -47,30 +47,6 @@ test("Group ID Map", () => { ]); }); -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(); @@ -78,11 +54,3 @@ test("Territory", () => { 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 index d22e9cb2..d1e5ebf1 100644 --- a/src/autoscore.ts +++ b/src/autoscore.ts @@ -783,7 +783,8 @@ function debug_groups(title: string, groups: GoStoneGroups) { let group_color = red; if (group.color === JGOFNumericPlayerColor.EMPTY) { - if (group.is_territory_in_seki) { + //if (group.is_territory_in_seki) { + if (false) { group_color = yellow; } else if (group.is_territory) { if (group.territory_color) { From 3c2503d538066f4a1819e4496420c93434fdb09e Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Sat, 8 Jun 2024 16:58:17 -0600 Subject: [PATCH 09/68] Fixed up tests for score estimator using goscorer --- src/__tests__/ScoreEstimator.test.ts | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/__tests__/ScoreEstimator.test.ts b/src/__tests__/ScoreEstimator.test.ts index d5a4b11f..55474db7 100644 --- a/src/__tests__/ScoreEstimator.test.ts +++ b/src/__tests__/ScoreEstimator.test.ts @@ -136,17 +136,18 @@ describe("ScoreEstimator", () => { ]); }); - test("score()", async () => { + test("score() territory", async () => { const se = new ScoreEstimator(undefined, engine, 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, @@ -155,7 +156,7 @@ describe("ScoreEstimator", () => { handicap: 0, komi: 0, prisoners: 0, - scoring_positions: "aaab", + scoring_positions: "", stones: 0, territory: 0, total: 0, @@ -178,19 +179,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, }); }); From 0500e6e6970c6765e5bb24ead21d7cf983156ea7 Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Sat, 8 Jun 2024 17:55:57 -0600 Subject: [PATCH 10/68] Make stone removal in score estimation behave like in stone removal phase Also draw the red x's over the stones like the stone removal phase --- src/GobanCanvas.ts | 15 ++++--- src/GobanSVG.ts | 15 ++++--- src/ScoreEstimator.ts | 95 ++++++++++++++++++++++++------------------- 3 files changed, 73 insertions(+), 52 deletions(-) diff --git a/src/GobanCanvas.ts b/src/GobanCanvas.ts index 338be1cd..24dd47c2 100644 --- a/src/GobanCanvas.ts +++ b/src/GobanCanvas.ts @@ -370,6 +370,7 @@ export class GobanCanvas extends GobanCore implements GobanCanvasInterface { pt.i, pt.j, ev.ctrlKey || ev.metaKey || ev.altKey || ev.shiftKey, + press_duration_ms, ); } this.emit("update"); @@ -1714,11 +1715,15 @@ export class GobanCanvas extends GobanCore implements GobanCanvasInterface { /* 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.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_estimate && + this.score_estimate.board[j][i] && + this.score_estimate.removal[j][i]) ) { ctx.lineCap = "square"; ctx.save(); diff --git a/src/GobanSVG.ts b/src/GobanSVG.ts index a485bde7..99ad2c93 100644 --- a/src/GobanSVG.ts +++ b/src/GobanSVG.ts @@ -326,6 +326,7 @@ export class GobanSVG extends GobanCore implements GobanSVGInterface { pt.i, pt.j, ev.ctrlKey || ev.metaKey || ev.altKey || ev.shiftKey, + press_duration_ms, ); } this.emit("update"); @@ -1629,11 +1630,15 @@ export class GobanSVG extends GobanCore implements GobanSVGInterface { /* 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.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_estimate && + this.score_estimate.board[j][i] && + this.score_estimate.removal[j][i]) ) { const r = Math.max(1, this.metrics.mid * 0.75); const cross = document.createElementNS("http://www.w3.org/2000/svg", "path"); diff --git a/src/ScoreEstimator.ts b/src/ScoreEstimator.ts index 45122862..f5e4397b 100644 --- a/src/ScoreEstimator.ts +++ b/src/ScoreEstimator.ts @@ -340,11 +340,12 @@ 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); + handleClick(i: number, j: number, mod_key: boolean, press_duration_ms: number): void { + console.log(i, j, mod_key, press_duration_ms); + if (mod_key || press_duration_ms > 500) { + this.toggleSingleGroupRemoval(i, j, true); } else { - this.toggleMetaGroupRemoval(i, j); + this.toggleSingleGroupRemoval(i, j); } this.estimateScore(this.trials, this.tolerance, this.autoscore).catch(() => { @@ -356,54 +357,64 @@ export class ScoreEstimator { g.foreachStone(({ x, y }) => this.setRemoved(x, y, removing ? 1 : 0)); } - toggleMetaGroupRemoval(x: number, y: number): void { - const already_done: { [k: string]: boolean } = {}; - const space_groups: Array = []; - let group_color: JGOFNumericPlayerColor; - + public toggleSingleGroupRemoval(x: number, y: number, force_removal: boolean = false): void { 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 */ + const removing: 0 | 1 = !this.removal[y][x] ? 1 : 0; + + const groups = new GoStoneGroups(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.foreachNeighborSpaceGroup((gr) => { + let is_territory_group = false; + gr.foreachStone((pt) => { + if ( + scores[pt.y][pt.x].isTerritoryFor === this.board[y][x] && + !scores[pt.y][pt.x].isFalseEye + ) { + is_territory_group = true; + } + }); - group.foreachNeighborSpaceGroup((g) => { - if (!already_done[g.id]) { - space_groups.push(g); - already_done[g.id] = true; + if (is_territory_group) { + total_territory_group_count += 1; + total_territory_adjacency_count += gr.points.length; } }); - - 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; - } - }); - } - } - }); + if (total_territory_adjacency_count >= 5 || total_territory_group_count >= 2) { + console.log("This group is almost assuredly alive, refusing to remove"); + GobanCore.hooks.toast?.("refusing_to_remove_group_is_alive", 4000); + return; } } + + /* Otherwise, toggle the group */ + const group_color = this.board[y][x]; + + if (group_color === JGOFNumericPlayerColor.EMPTY) { + /* Disallow toggling of open area (old dame marking method that we no longer desire) */ + return; + } + + this.removeGroup(selected_group, !!removing); + return; } - } catch (e) { - console.log(e.stack); + } catch (err) { + console.log(err.stack); } } + setRemoved(x: number, y: number, removed: number): void { this.clearAutoScore(); From 6b4162ab88bc61e73290ed7f05cb6bd7613f4e53 Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Sat, 8 Jun 2024 17:59:35 -0600 Subject: [PATCH 11/68] Fixed tests --- src/ScoreEstimator.ts | 1 - src/__tests__/ScoreEstimator.test.ts | 28 ++++++++++++++++++++-------- 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/src/ScoreEstimator.ts b/src/ScoreEstimator.ts index f5e4397b..2175c6a3 100644 --- a/src/ScoreEstimator.ts +++ b/src/ScoreEstimator.ts @@ -341,7 +341,6 @@ export class ScoreEstimator { return ret; } handleClick(i: number, j: number, mod_key: boolean, press_duration_ms: number): void { - console.log(i, j, mod_key, press_duration_ms); if (mod_key || press_duration_ms > 500) { this.toggleSingleGroupRemoval(i, j, true); } else { diff --git a/src/__tests__/ScoreEstimator.test.ts b/src/__tests__/ScoreEstimator.test.ts index 55474db7..2da7f4f2 100644 --- a/src/__tests__/ScoreEstimator.test.ts +++ b/src/__tests__/ScoreEstimator.test.ts @@ -233,8 +233,8 @@ 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); + se.toggleSingleGroupRemoval(1, 0); + se.toggleSingleGroupRemoval(2, 0); await se.when_ready; se.score(); @@ -261,8 +261,8 @@ describe("ScoreEstimator", () => { test("getStoneRemovalString()", async () => { const se = new ScoreEstimator(undefined, engine, 10, 0.5, false); - se.toggleMetaGroupRemoval(1, 0); - se.toggleMetaGroupRemoval(2, 0); + se.toggleSingleGroupRemoval(1, 0); + se.toggleSingleGroupRemoval(2, 0); await se.when_ready; expect(se.getStoneRemovalString()).toBe("babbcacb"); @@ -354,8 +354,8 @@ describe("ScoreEstimator", () => { const se = new ScoreEstimator(undefined, engine, 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], @@ -369,10 +369,22 @@ describe("ScoreEstimator", () => { const se = new ScoreEstimator(undefined, engine, 10, 0.5, false); await se.when_ready; - se.handleClick(1, 0, true); + se.handleClick(1, 0, true, 0); expect(se.removal).toEqual([ [0, 1, 0, 0], - [0, 0, 0, 0], + [0, 1, 0, 0], + ]); + }); + + test("long press", async () => { + set_local_scorer(estimateScoreVoronoi); + const se = new ScoreEstimator(undefined, engine, 10, 0.5, false); + await se.when_ready; + + se.handleClick(1, 0, false, 1000); + expect(se.removal).toEqual([ + [0, 1, 0, 0], + [0, 1, 0, 0], ]); }); From 768297c254c258ed57a423571d4f829747ece2e9 Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Sun, 9 Jun 2024 06:05:16 -0600 Subject: [PATCH 12/68] Remove false eyes from score estimates when using territory scoring --- src/ScoreEstimator.ts | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/ScoreEstimator.ts b/src/ScoreEstimator.ts index 2175c6a3..f28310fd 100644 --- a/src/ScoreEstimator.ts +++ b/src/ScoreEstimator.ts @@ -544,12 +544,18 @@ export class ScoreEstimator { ); for (let y = 0; y < this.height; ++y) { for (let x = 0; x < this.width; ++x) { - if (scoring[y][x].isTerritoryFor === goscorer.BLACK) { - this.black.territory += 1; - this.black.scoring_positions += GoMath.encodeMove(x, y); - } else if (scoring[y][x].isTerritoryFor === goscorer.WHITE) { - this.white.territory += 1; - this.white.scoring_positions += GoMath.encodeMove(x, y); + 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 += GoMath.encodeMove(x, y); + } else if (scoring[y][x].isTerritoryFor === goscorer.WHITE) { + this.white.territory += 1; + this.white.scoring_positions += GoMath.encodeMove(x, y); + } } } } From efbd5d60830f6f7800e8462884564a12d7ac7bdd Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Sun, 9 Jun 2024 06:06:28 -0600 Subject: [PATCH 13/68] Remove old now unused toggleMetaGroupRemoval from GoEngine --- src/GoEngine.ts | 90 ------------------------------------------------- 1 file changed, 90 deletions(-) diff --git a/src/GoEngine.ts b/src/GoEngine.ts index 0217bae1..166c7819 100644 --- a/src/GoEngine.ts +++ b/src/GoEngine.ts @@ -1189,31 +1189,6 @@ export class GoEngine extends EventEmitter { }); 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); @@ -1671,71 +1646,6 @@ export class GoEngine extends EventEmitter { return [[0, []]]; } - 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_color = this.board[y][x]; - - if (group_color === JGOFNumericPlayerColor.EMPTY) { - /* Disallow toggling of open area (old dame marking method that we no longer desire) */ - return [[0, []]]; - } - - const group = this.getGroup(x, y, true); - let removed_stones = this.setGroupForRemoval(x, y, removing, false)[1]; - const empty_spaces = []; - - //if (group_color === 0) { - /* just toggle open area */ - // This condition is historical and can be removed if we find we really don't need it - anoek 2024-06-08 - //} 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. From 41987b95f409c666669f432cec307f54e54fd4b8 Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Sun, 9 Jun 2024 06:08:23 -0600 Subject: [PATCH 14/68] Fix tests --- src/__tests__/GoEngine.test.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/__tests__/GoEngine.test.ts b/src/__tests__/GoEngine.test.ts index 1ab224b8..052b6a6c 100644 --- a/src/__tests__/GoEngine.test.ts +++ b/src/__tests__/GoEngine.test.ts @@ -608,7 +608,7 @@ describe("moves", () => { }); describe("groups", () => { - test("toggleMetagroupRemoval", () => { + test("toggleSingleGroupRemoval", () => { const engine = new GoEngine({ width: 4, height: 4, @@ -625,23 +625,23 @@ 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], [0, 0, 0, 0], ]); - engine.toggleMetaGroupRemoval(0, 0); + engine.toggleSingleGroupRemoval(0, 0); expect(engine.removal).toEqual(makeMatrix(4, 4)); }); - test("toggleMetagroupRemoval out-of-bounds", () => { + test("toggleSingleGroupRemoval out-of-bounds", () => { const engine = new GoEngine({ width: 4, height: 4, @@ -658,11 +658,11 @@ 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([[0, []]]); expect(on_removal_updated).toBeCalledTimes(0); }); - test("toggleMetagroupRemoval empty area doesn't do anything", () => { + test("toggleSingleGroupRemoval empty area doesn't do anything", () => { const engine = new GoEngine({ width: 4, height: 2, @@ -677,7 +677,7 @@ describe("groups", () => { const on_removal_updated = jest.fn(); engine.addListener("stone-removal.updated", on_removal_updated); - expect(engine.toggleMetaGroupRemoval(0, 1)).toEqual([[0, []]]); + expect(engine.toggleSingleGroupRemoval(0, 1)).toEqual([[0, []]]); expect(on_removal_updated).toBeCalledTimes(0); }); From d51bebceb640a4a6150e6dde3563ac53d5cb1b64 Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Mon, 10 Jun 2024 06:56:23 -0600 Subject: [PATCH 15/68] Add support for score marks in analysis and review mode --- src/GobanCanvas.ts | 114 +++++++++++++++++++++++++++++++++++++++++++-- src/GobanCore.ts | 45 +++++++++++++++--- src/GobanSVG.ts | 113 ++++++++++++++++++++++++++++++++++++++++++-- src/Misc.ts | 28 +++++++++++ src/MoveTree.ts | 3 +- src/goban.ts | 1 + 6 files changed, 291 insertions(+), 13 deletions(-) diff --git a/src/GobanCanvas.ts b/src/GobanCanvas.ts index 24dd47c2..c9422fa8 100644 --- a/src/GobanCanvas.ts +++ b/src/GobanCanvas.ts @@ -35,6 +35,7 @@ import { import { getRandomInt } from "./GoUtil"; import { _ } from "./translate"; import { formatMessage, MessageID } from "./messages"; +import { color_blend } from "Misc"; const __theme_cache: { [bw: string]: { [name: string]: { [size: string]: any } }; @@ -121,6 +122,9 @@ export class GobanCanvas extends GobanCore implements GobanCanvasInterface { private last_pen_position?: [number, number]; protected metrics: GobanMetrics = { width: NaN, height: NaN, mid: NaN, offset: NaN }; + private analysis_scoring_color?: "black" | "white" | string; + private analysis_scoring_last_position: { i: number; j: number } = { i: NaN, j: NaN }; + private drawing_enabled: boolean = true; private pen_ctx?: CanvasRenderingContext2D; private pen_layer?: HTMLCanvasElement; @@ -395,6 +399,8 @@ 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 { const pos = getRelativeEventPosition(ev); const pt = this.xy2ij(pos.x, pos.y); @@ -437,6 +443,8 @@ export class GobanCanvas extends GobanCore implements GobanCanvasInterface { } this.onLabelingStart(ev); + } else if (this.mode === "analyze" && this.analyze_tool === "score") { + this.onAnalysisScoringStart(ev); } } catch (e) { console.error(e); @@ -452,6 +460,8 @@ 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 { this.onMouseMove(ev); } @@ -1757,7 +1767,12 @@ export class GobanCanvas extends GobanCore implements GobanCanvasInterface { ((this.engine.phase === "stone removal" || (this.engine.phase === "finished" && this.mode === "play")) && this.engine.board[j][i] === 0 && - (this.engine.removal[j][i] || pos.needs_sealing)) + (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(); @@ -1791,6 +1806,16 @@ export class GobanCanvas extends GobanCore implements GobanCanvasInterface { 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"; @@ -1804,6 +1829,10 @@ export class GobanCanvas extends GobanCore implements GobanCanvasInterface { 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; const r = this.square_size * 0.15; @@ -2384,7 +2413,12 @@ export class GobanCanvas extends GobanCore implements GobanCanvasInterface { ((this.engine.phase === "stone removal" || (this.engine.phase === "finished" && this.mode === "play")) && this.engine.board[j][i] === 0 && - (this.engine.removal[j][i] || pos.needs_sealing)) + (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 ( @@ -2416,6 +2450,16 @@ export class GobanCanvas extends GobanCore implements GobanCanvasInterface { 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 && @@ -3042,7 +3086,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; @@ -3089,6 +3133,70 @@ export class GobanCanvas extends GobanCore implements GobanCanvasInterface { } } + private 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.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, + ); + } + private 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); + } + } + protected setTitle(title: string): void { this.title = title; if (this.title_div) { diff --git a/src/GobanCore.ts b/src/GobanCore.ts index b5f3214a..8e99af92 100644 --- a/src/GobanCore.ts +++ b/src/GobanCore.ts @@ -68,6 +68,7 @@ export const MARK_TYPES: Array = [ "cross", "black", "white", + "score", ]; export type LabelPosition = | "all" @@ -85,7 +86,7 @@ interface JGOFPlayerClockWithTimedOut extends JGOFPlayerClock { export type GobanModes = "play" | "puzzle" | "score estimation" | "analyze" | "conditional"; -export type AnalysisTool = "stone" | "draw" | "label"; +export type AnalysisTool = "stone" | "draw" | "label" | "score"; export type AnalysisSubTool = | "black" | "white" @@ -1606,6 +1607,9 @@ export abstract class GobanCore extends EventEmitter { 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 = []; @@ -1828,7 +1832,11 @@ export abstract class GobanCore extends EventEmitter { (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 } { + 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; @@ -1848,7 +1856,7 @@ export abstract class GobanCore extends EventEmitter { 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) { + if (border_distance < 0.1 && anti_slip) { // have a "dead zone" in between squares to avoid misclicks i = -1; j = -1; @@ -1905,6 +1913,22 @@ export abstract class GobanCore extends EventEmitter { 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, + ): void { + const marks = this.getMarks(x, y); + marks.score = color; + this.drawSquare(x, y); + 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); @@ -2297,7 +2321,8 @@ export abstract class GobanCore extends EventEmitter { 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; + delete this.getMarks(i, j).score; + //this.getMarks(i, j).score = false; this.drawSquare(i, j); } } @@ -2987,7 +3012,13 @@ export abstract class GobanCore extends EventEmitter { mark = "" + mark; } - if (mark.length <= 3 || parseFloat(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); @@ -3862,7 +3893,9 @@ export abstract class GobanCore extends EventEmitter { const mark_key: keyof MarkInterface = MARK_TYPES[i] === "letter" ? pos.letter || "[ERR]" - : MARK_TYPES[i]; + : MARK_TYPES[i] === "score" + ? `score-${pos.score}` + : MARK_TYPES[i]; if (!(mark_key in marks)) { marks[mark_key] = ""; } diff --git a/src/GobanSVG.ts b/src/GobanSVG.ts index 99ad2c93..844994fc 100644 --- a/src/GobanSVG.ts +++ b/src/GobanSVG.ts @@ -31,6 +31,7 @@ import { getRelativeEventPosition } from "./canvas_utils"; import { getRandomInt } from "./GoUtil"; import { _ } from "./translate"; import { formatMessage, MessageID } from "./messages"; +import { color_blend } from "Misc"; //import { GobanCanvasConfig, GobanCanvasInterface } from "./GobanCanvas"; @@ -128,6 +129,9 @@ export class GobanSVG extends GobanCore implements GobanSVGInterface { private last_pen_position?: [number, number]; protected metrics: GobanMetrics = { width: NaN, height: NaN, mid: NaN, offset: NaN }; + private analysis_scoring_color?: "black" | "white" | string; + private analysis_scoring_last_position: { i: number; j: number } = { i: NaN, j: NaN }; + private drawing_enabled: boolean = true; protected title_div?: HTMLElement; @@ -351,6 +355,8 @@ 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 { const pos = getRelativeEventPosition(ev, this.parent); const pt = this.xy2ij(pos.x, pos.y); @@ -393,6 +399,8 @@ export class GobanSVG extends GobanCore implements GobanSVGInterface { } this.onLabelingStart(ev); + } else if (this.mode === "analyze" && this.analyze_tool === "score") { + this.onAnalysisScoringStart(ev); } } catch (e) { console.error(e); @@ -408,6 +416,8 @@ 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 { this.onMouseMove(ev); } @@ -1676,9 +1686,15 @@ export class GobanSVG extends GobanCore implements GobanSVGInterface { ((this.engine.phase === "stone removal" || (this.engine.phase === "finished" && this.mode === "play")) && this.engine.board[j][i] === 0 && - (this.engine.removal[j][i] || pos.needs_sealing)) + (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 && @@ -1708,6 +1724,16 @@ export class GobanSVG extends GobanCore implements GobanSVGInterface { 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)); @@ -1731,6 +1757,10 @@ export class GobanSVG extends GobanCore implements GobanSVGInterface { 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), @@ -2324,7 +2354,12 @@ export class GobanSVG extends GobanCore implements GobanSVGInterface { ((this.engine.phase === "stone removal" || (this.engine.phase === "finished" && this.mode === "play")) && this.engine.board[j][i] === 0 && - (this.engine.removal[j][i] || pos.needs_sealing)) + (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 ( @@ -2356,6 +2391,15 @@ export class GobanSVG extends GobanCore implements GobanSVGInterface { 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 && @@ -3044,7 +3088,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; @@ -3090,6 +3134,69 @@ export class GobanSVG extends GobanCore implements GobanSVGInterface { this.setLabelCharacterFromMarks(); } } + private 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.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, + ); + } + private 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); + } + } protected setTitle(title: string): void { this.title = title; diff --git a/src/Misc.ts b/src/Misc.ts index 3773c159..778da21d 100644 --- a/src/Misc.ts +++ b/src/Misc.ts @@ -58,3 +58,31 @@ export function escapeSGFText(txt: string, escapeColon: boolean = false): string export function newline2space(txt: string): string { return txt.replace(/[\r\n]/g, " "); } + +/** 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), + ); +} + +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), + }; +} + +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/MoveTree.ts b/src/MoveTree.ts index 65dcb99e..0f50efa5 100644 --- a/src/MoveTree.ts +++ b/src/MoveTree.ts @@ -30,7 +30,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; diff --git a/src/goban.ts b/src/goban.ts index 1a5b4365..8d007cff 100644 --- a/src/goban.ts +++ b/src/goban.ts @@ -35,6 +35,7 @@ export * from "./AdHocFormat"; export * from "./TestGoban"; export * from "./test_utils"; export * from "./GobanSocket"; +export * from "./Misc"; export * from "./autoscore"; export * as GoMath from "./GoMath"; From 0b0a369fe91ad0d2cf5d9b94321948ca634ba4c5 Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Mon, 10 Jun 2024 10:03:17 -0600 Subject: [PATCH 16/68] Support marking dead stones in analysis and reviews --- src/GobanCanvas.ts | 151 ++++++++++++++++++++++++++++++------- src/GobanCore.ts | 11 ++- src/GobanSVG.ts | 184 ++++++++++++++++++++++++++++++++++++++------- 3 files changed, 289 insertions(+), 57 deletions(-) diff --git a/src/GobanCanvas.ts b/src/GobanCanvas.ts index c9422fa8..e6f932c6 100644 --- a/src/GobanCanvas.ts +++ b/src/GobanCanvas.ts @@ -36,6 +36,7 @@ import { getRandomInt } from "./GoUtil"; import { _ } from "./translate"; import { formatMessage, MessageID } from "./messages"; import { color_blend } from "Misc"; +import { GoStoneGroups } from "GoStoneGroups"; const __theme_cache: { [bw: string]: { [name: string]: { [size: string]: any } }; @@ -124,6 +125,8 @@ export class GobanCanvas extends GobanCore implements GobanCanvasInterface { private analysis_scoring_color?: "black" | "white" | string; private analysis_scoring_last_position: { i: number; j: number } = { i: NaN, j: NaN }; + private analysis_removal_state?: boolean; + private analysis_removal_last_position: { i: number; j: number } = { i: NaN, j: NaN }; private drawing_enabled: boolean = true; private pen_ctx?: CanvasRenderingContext2D; @@ -401,6 +404,8 @@ export class GobanCanvas extends GobanCore implements GobanCanvasInterface { /* 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); @@ -445,6 +450,8 @@ 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); @@ -462,6 +469,8 @@ export class GobanCanvas extends GobanCore implements GobanCanvasInterface { 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); } @@ -849,7 +858,8 @@ 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; @@ -1528,6 +1538,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 */ || @@ -1556,7 +1567,7 @@ 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 ( @@ -1566,7 +1577,7 @@ export class GobanCanvas extends GobanCore implements GobanCanvasInterface { this.score_estimate.removal[j][i] ) { color = this.score_estimate.board[j][i]; - transparent = true; + translucent = true; } else if ( this.engine && ((this.engine.phase === "stone removal" && @@ -1578,7 +1589,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 ( @@ -1612,12 +1623,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; @@ -1647,7 +1662,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; } @@ -1733,27 +1748,44 @@ export class GobanCanvas extends GobanCore implements GobanCanvasInterface { (this.scoring_mode && this.score_estimate && this.score_estimate.board[j][i] && - this.score_estimate.removal[j][i]) + this.score_estimate.removal[j][i]) || + pos.stone_removed ) { - ctx.lineCap = "square"; - ctx.save(); - ctx.beginPath(); - ctx.lineWidth = this.square_size * 0.125; - if (transparent) { - ctx.globalAlpha = 0.6; - } - 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_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) + ) { + console.log("Should be drawing red x"); + 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 */ { @@ -2276,6 +2308,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 */ || @@ -2303,7 +2336,7 @@ export class GobanCanvas extends GobanCore implements GobanCanvasInterface { pos.black || pos.white ) { - let transparent = false; + let translucent = false; let color; if ( this.scoring_mode && @@ -2312,7 +2345,7 @@ export class GobanCanvas extends GobanCore implements GobanCanvasInterface { this.score_estimate.removal[j][i] ) { color = this.score_estimate.board[j][i]; - transparent = true; + translucent = true; } else if ( this.engine && this.engine.phase === "stone removal" && @@ -2322,7 +2355,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 ( @@ -2339,11 +2372,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); } @@ -2351,10 +2389,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_estimate && + this.score_estimate.board[j][i] && + this.score_estimate.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)) { @@ -3196,7 +3267,31 @@ export class GobanCanvas extends GobanCore implements GobanCanvasInterface { this.putAnalysisScoreColorAtLocation(cur.i, cur.j, this.analysis_scoring_color); } } + private 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 stone_group = new GoStoneGroups(this.engine).getGroup(x, y); + + stone_group.foreachStone((loc) => { + this.putAnalysisRemovalAtLocation(loc.x, loc.y, this.analysis_removal_state); + }); + } protected setTitle(title: string): void { this.title = title; if (this.title_div) { diff --git a/src/GobanCore.ts b/src/GobanCore.ts index 8e99af92..f3aaf20c 100644 --- a/src/GobanCore.ts +++ b/src/GobanCore.ts @@ -69,6 +69,7 @@ export const MARK_TYPES: Array = [ "black", "white", "score", + "stone_removed", ]; export type LabelPosition = | "all" @@ -86,7 +87,7 @@ interface JGOFPlayerClockWithTimedOut extends JGOFPlayerClock { export type GobanModes = "play" | "puzzle" | "score estimation" | "analyze" | "conditional"; -export type AnalysisTool = "stone" | "draw" | "label" | "score"; +export type AnalysisTool = "stone" | "draw" | "label" | "score" | "removal"; export type AnalysisSubTool = | "black" | "white" @@ -1929,6 +1930,13 @@ export abstract class GobanCore extends EventEmitter { this.drawSquare(x, y); 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(); + } 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); @@ -3915,6 +3923,7 @@ export abstract class GobanCore extends EventEmitter { m: diff.moves, k: marks, }; + console.log("mARKS:", marks); const tmp = dup(msg); if (this.last_review_message.f === msg.f && this.last_review_message.m === msg.m) { diff --git a/src/GobanSVG.ts b/src/GobanSVG.ts index 844994fc..48e49cdc 100644 --- a/src/GobanSVG.ts +++ b/src/GobanSVG.ts @@ -32,6 +32,7 @@ import { getRandomInt } from "./GoUtil"; import { _ } from "./translate"; import { formatMessage, MessageID } from "./messages"; import { color_blend } from "Misc"; +import { GoStoneGroups } from "GoStoneGroups"; //import { GobanCanvasConfig, GobanCanvasInterface } from "./GobanCanvas"; @@ -131,6 +132,8 @@ export class GobanSVG extends GobanCore implements GobanSVGInterface { private analysis_scoring_color?: "black" | "white" | string; private analysis_scoring_last_position: { i: number; j: number } = { i: NaN, j: NaN }; + private analysis_removal_state?: boolean; + private analysis_removal_last_position: { i: number; j: number } = { i: NaN, j: NaN }; private drawing_enabled: boolean = true; protected title_div?: HTMLElement; @@ -357,6 +360,8 @@ export class GobanSVG extends GobanCore implements GobanSVGInterface { /* 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); @@ -401,6 +406,8 @@ 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); @@ -418,6 +425,8 @@ export class GobanSVG extends GobanCore implements GobanSVGInterface { 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); } @@ -821,7 +830,8 @@ 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; @@ -1188,6 +1198,7 @@ export class GobanSVG extends GobanCore implements GobanSVGInterface { if (i < 0 || j < 0 || !this.drawing_enabled || this.no_display) { return; } + if (this.__draw_state[j][i] !== this.drawingHash(i, j)) { this.__drawSquare(i, j); } @@ -1449,6 +1460,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 */ || @@ -1477,7 +1489,7 @@ 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 ( @@ -1487,7 +1499,7 @@ export class GobanSVG extends GobanCore implements GobanSVGInterface { this.score_estimate.removal[j][i] ) { color = this.score_estimate.board[j][i]; - transparent = true; + translucent = true; } else if ( this.engine && ((this.engine.phase === "stone removal" && @@ -1499,7 +1511,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 ( @@ -1533,12 +1545,17 @@ 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 (this.mode === "analyze" && pos.stone_removed) { + 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; @@ -1566,7 +1583,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( @@ -1648,30 +1665,48 @@ export class GobanSVG extends GobanCore implements GobanSVGInterface { (this.scoring_mode && this.score_estimate && this.score_estimate.board[j][i] && - this.score_estimate.removal[j][i]) + this.score_estimate.removal[j][i]) || + //(this.mode === "analyze" && pos.stone_removed) + pos.stone_removed ) { - 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", - ` + 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} `, - ); - if (transparent) { - cross.setAttribute("stroke-opacity", "0.6"); - } + ); + const opacity = this.engine.board[j][i] ? 1.0 : 0.2; + cross.setAttribute("stroke-opacity", opacity?.toString()); - cell.appendChild(cross); - } - } + cell.appendChild(cross); } /* Draw Scores */ @@ -2217,6 +2252,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 */ || @@ -2233,6 +2269,11 @@ export class GobanSVG extends GobanCore implements GobanSVGInterface { movetree_contains_this_square || (this.getPuzzlePlacementSetting && this.getPuzzlePlacementSetting().mode !== "play"))) || + (this.mode === "analyze" && + this.analyze_tool === "removal" && + this.last_hover_square && + this.last_hover_square.x === i && + this.last_hover_square.y === j) || (this.scoring_mode && this.score_estimate && this.score_estimate.board[j][i] && @@ -2244,7 +2285,7 @@ export class GobanSVG extends GobanCore implements GobanSVGInterface { pos.black || pos.white ) { - let transparent = false; + let translucent = false; let color; if ( this.scoring_mode && @@ -2253,7 +2294,7 @@ export class GobanSVG extends GobanCore implements GobanSVGInterface { this.score_estimate.removal[j][i] ) { color = this.score_estimate.board[j][i]; - transparent = true; + translucent = true; } else if ( this.engine && this.engine.phase === "stone removal" && @@ -2263,7 +2304,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 ( @@ -2280,11 +2321,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); } @@ -2292,10 +2338,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_estimate && + this.score_estimate.board[j][i] && + this.score_estimate.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)) { @@ -3197,7 +3285,47 @@ export class GobanSVG extends GobanCore implements GobanSVGInterface { this.putAnalysisScoreColorAtLocation(cur.i, cur.j, this.analysis_scoring_color); } } + private 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 stone_group = new GoStoneGroups(this.engine).getGroup(x, y); + + stone_group.foreachStone((loc) => { + this.putAnalysisRemovalAtLocation(loc.x, loc.y, this.analysis_removal_state); + }); + + /* 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_removal_last_position.i, + this.analysis_removal_last_position.j, + ); + */ + } protected setTitle(title: string): void { this.title = title; if (this.title_div) { From e913023d080f02aa0faa444db3d32aae07ccaaf7 Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Tue, 11 Jun 2024 04:03:42 -0600 Subject: [PATCH 17/68] Refactor: merge GoUtil.ts and Misc.ts -> util.ts --- src/GobanCanvas.ts | 6 +- src/GobanCore.ts | 2 +- src/GobanSVG.ts | 28 +------ src/GobanSocket.ts | 2 +- src/Misc.ts | 88 -------------------- src/MoveTree.ts | 2 +- src/ScoreEstimator.ts | 2 +- src/__tests__/{Misc.test.ts => util.test.ts} | 2 +- src/engine.ts | 3 +- src/goban.ts | 4 +- src/local_estimators/voronoi.ts | 2 +- src/{GoUtil.ts => util.ts} | 80 +++++++++++++++++- 12 files changed, 92 insertions(+), 129 deletions(-) delete mode 100644 src/Misc.ts rename src/__tests__/{Misc.test.ts => util.test.ts} (95%) rename src/{GoUtil.ts => util.ts} (67%) diff --git a/src/GobanCanvas.ts b/src/GobanCanvas.ts index e6f932c6..e69ae7dc 100644 --- a/src/GobanCanvas.ts +++ b/src/GobanCanvas.ts @@ -32,11 +32,10 @@ import { allocateCanvasOrError, getRelativeEventPosition, } from "./canvas_utils"; -import { getRandomInt } from "./GoUtil"; import { _ } from "./translate"; import { formatMessage, MessageID } from "./messages"; -import { color_blend } from "Misc"; -import { GoStoneGroups } from "GoStoneGroups"; +import { color_blend, getRandomInt } from "./util"; +import { GoStoneGroups } from "./GoStoneGroups"; const __theme_cache: { [bw: string]: { [name: string]: { [size: string]: any } }; @@ -1769,7 +1768,6 @@ export class GobanCanvas extends GobanCore implements GobanCanvasInterface { this.last_hover_square.x === i && this.last_hover_square.y === j) ) { - console.log("Should be drawing red x"); const opacity = this.engine.board[j][i] ? 1.0 : 0.2; ctx.lineCap = "square"; ctx.save(); diff --git a/src/GobanCore.ts b/src/GobanCore.ts index f3aaf20c..8517948d 100644 --- a/src/GobanCore.ts +++ b/src/GobanCore.ts @@ -33,7 +33,7 @@ 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 { deepEqual, dup, computeAverageMoveTime, niceInterval } from "./util"; import { _, interpolate } from "./translate"; import { JGOFClock, diff --git a/src/GobanSVG.ts b/src/GobanSVG.ts index 48e49cdc..481d583d 100644 --- a/src/GobanSVG.ts +++ b/src/GobanSVG.ts @@ -28,11 +28,10 @@ import { GoTheme } from "./GoTheme"; import { GoThemes } from "./GoThemes"; import { MoveTreePenMarks } from "./MoveTree"; import { getRelativeEventPosition } from "./canvas_utils"; -import { getRandomInt } from "./GoUtil"; import { _ } from "./translate"; import { formatMessage, MessageID } from "./messages"; -import { color_blend } from "Misc"; -import { GoStoneGroups } from "GoStoneGroups"; +import { color_blend, getRandomInt } from "./util"; +import { GoStoneGroups } from "./GoStoneGroups"; //import { GobanCanvasConfig, GobanCanvasInterface } from "./GobanCanvas"; @@ -1198,7 +1197,6 @@ export class GobanSVG extends GobanCore implements GobanSVGInterface { if (i < 0 || j < 0 || !this.drawing_enabled || this.no_display) { return; } - if (this.__draw_state[j][i] !== this.drawingHash(i, j)) { this.__drawSquare(i, j); } @@ -1551,7 +1549,6 @@ export class GobanSVG extends GobanCore implements GobanSVGInterface { color = this.engine.player; } - //if (this.mode === "analyze" && pos.stone_removed) { if (pos.stone_removed) { translucent = true; } @@ -2269,11 +2266,6 @@ export class GobanSVG extends GobanCore implements GobanSVGInterface { movetree_contains_this_square || (this.getPuzzlePlacementSetting && this.getPuzzlePlacementSetting().mode !== "play"))) || - (this.mode === "analyze" && - this.analyze_tool === "removal" && - this.last_hover_square && - this.last_hover_square.x === i && - this.last_hover_square.y === j) || (this.scoring_mode && this.score_estimate && this.score_estimate.board[j][i] && @@ -3309,22 +3301,6 @@ export class GobanSVG extends GobanCore implements GobanSVGInterface { stone_group.foreachStone((loc) => { this.putAnalysisRemovalAtLocation(loc.x, loc.y, this.analysis_removal_state); }); - - /* 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_removal_last_position.i, - this.analysis_removal_last_position.j, - ); - */ } protected setTitle(title: string): void { this.title = title; diff --git a/src/GobanSocket.ts b/src/GobanSocket.ts index 2dd0e880..e556df3a 100644 --- a/src/GobanSocket.ts +++ b/src/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?]; diff --git a/src/Misc.ts b/src/Misc.ts deleted file mode 100644 index 778da21d..00000000 --- a/src/Misc.ts +++ /dev/null @@ -1,88 +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. - */ - -/* - * SPEC: https://www.red-bean.com/sgf/sgf4.html#text - * - * in sgf (as per spec): - * - slash is an escape char - * - closing bracket is a special symbol - * - whitespaces other than space & newline should be converted to space - * - in compose data type, we should also escape ':' - * (but that is only used in special SGF properties) - * - * so we gotta: - * - escape (double) all slashes in the text (so that they do not have the special meaning) - * - escape any closing brackets ] (as it closes e.g. the comment section) - * - replace whitespace - * - [opt] handle colon - */ -export function escapeSGFText(txt: string, escapeColon: boolean = false): string { - // escape slashes first - // 'blah\blah' -> 'blah\\blah' - txt = txt.replace(/\\/g, "\\\\"); - - // escape closing square bracket ] - // 'blah[9dan]' -> 'blah[9dan\]' - txt = txt.replace(/]/g, "\\]"); - - // no need to escape opening bracket, SGF grammar handles that - // 'C[[[[[[blah blah]' - // ^ after it finds the first [, it is only looking for the closing bracket - // parsing SGF properties, so the remaining [ are safely treated as text - //txt = txt.replace(/[/g, "\\["); - - // sub whitespace except newline & carriage return by space - txt = txt.replace(/[^\S\r\n]/g, " "); - - if (escapeColon) { - txt = txt.replace(/:/g, "\\:"); - } - return txt; -} - -// in SGF simple text, we also need to get rid of the newlines -export function newline2space(txt: string): string { - return txt.replace(/[\r\n]/g, " "); -} - -/** 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), - ); -} - -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), - }; -} - -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/MoveTree.ts b/src/MoveTree.ts index 0f50efa5..9288b757 100644 --- a/src/MoveTree.ts +++ b/src/MoveTree.ts @@ -19,7 +19,7 @@ import { GoEngine, GoEngineState } from "./GoEngine"; import { encodeMove } from "./GoMath"; import { AdHocPackedMove } from "./AdHocFormat"; import { JGOFNumericPlayerColor, JGOFPlayerSummary } from "./JGOF"; -import { escapeSGFText, newline2space } from "./Misc"; +import { escapeSGFText, newline2space } from "./util"; export interface MarkInterface { triangle?: boolean; diff --git a/src/ScoreEstimator.ts b/src/ScoreEstimator.ts index f28310fd..aa8a1341 100644 --- a/src/ScoreEstimator.ts +++ b/src/ScoreEstimator.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { dup } from "./GoUtil"; +import { dup } from "./util"; import { encodeMove } from "./GoMath"; import * as GoMath from "./GoMath"; import { GoStoneGroup } from "./GoStoneGroup"; diff --git a/src/__tests__/Misc.test.ts b/src/__tests__/util.test.ts similarity index 95% rename from src/__tests__/Misc.test.ts rename to src/__tests__/util.test.ts index 74612681..a6ab2ee6 100644 --- a/src/__tests__/Misc.test.ts +++ b/src/__tests__/util.test.ts @@ -1,4 +1,4 @@ -import { escapeSGFText, newline2space } from "../Misc"; +import { escapeSGFText, newline2space } from "../util"; import * as AdHoc from "../AdHocFormat"; // String.raw`...` is the real string diff --git a/src/engine.ts b/src/engine.ts index 68ebfe33..df60abdf 100644 --- a/src/engine.ts +++ b/src/engine.ts @@ -16,7 +16,6 @@ export * from "./GobanError"; export * from "./GoStoneGroup"; -export * from "./GoUtil"; export * from "./GoEngine"; export * from "./GoConditionalMove"; export * from "./MoveTree"; @@ -25,7 +24,7 @@ export * from "./ScoreEstimator"; export * from "./JGOF"; export * from "./AdHocFormat"; export * from "./AIReview"; -export * from "./Misc"; +export * from "./util"; export * from "./test_utils"; export * from "./autoscore"; diff --git a/src/goban.ts b/src/goban.ts index 8d007cff..24e0e114 100644 --- a/src/goban.ts +++ b/src/goban.ts @@ -24,7 +24,7 @@ export * from "./GoStoneGroup"; export * from "./GoStoneGroups"; export * from "./GoTheme"; export * from "./GoThemes"; -export * from "./GoUtil"; +export * from "./util"; export * from "./canvas_utils"; export * from "./MoveTree"; export * from "./ScoreEstimator"; @@ -35,7 +35,7 @@ export * from "./AdHocFormat"; export * from "./TestGoban"; export * from "./test_utils"; export * from "./GobanSocket"; -export * from "./Misc"; +export * from "./util"; export * from "./autoscore"; export * as GoMath from "./GoMath"; diff --git a/src/local_estimators/voronoi.ts b/src/local_estimators/voronoi.ts index cd0d2b7a..3ba95d22 100644 --- a/src/local_estimators/voronoi.ts +++ b/src/local_estimators/voronoi.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { dup } from "../GoUtil"; +import { dup } from "../util"; /** * This estimator simply marks territory for whichever color has a diff --git a/src/GoUtil.ts b/src/util.ts similarity index 67% rename from src/GoUtil.ts rename to src/util.ts index 956b6894..38b14312 100644 --- a/src/GoUtil.ts +++ b/src/util.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. @@ -17,9 +17,12 @@ import { _, interpolate } from "./translate"; import type { JGOFTimeControl } from "./JGOF"; +/** 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; } + export function shortDurationString(seconds: number) { const weeks = Math.floor(seconds / (86400 * 7)); seconds -= weeks * 86400 * 7; @@ -38,6 +41,7 @@ export function shortDurationString(seconds: number) { (seconds ? " " + interpolate(_("%ss"), [seconds]) : "") ); } + export function dup(obj: any): any { let ret: any; if (typeof obj === "object") { @@ -57,6 +61,7 @@ export function dup(obj: any): any { } return ret; } + export function deepEqual(a: any, b: any) { if (typeof a !== typeof b) { return false; @@ -164,3 +169,76 @@ export function niceInterval( } }, interval); } + +/* + * SPEC: https://www.red-bean.com/sgf/sgf4.html#text + * + * in sgf (as per spec): + * - slash is an escape char + * - closing bracket is a special symbol + * - whitespaces other than space & newline should be converted to space + * - in compose data type, we should also escape ':' + * (but that is only used in special SGF properties) + * + * so we gotta: + * - escape (double) all slashes in the text (so that they do not have the special meaning) + * - escape any closing brackets ] (as it closes e.g. the comment section) + * - replace whitespace + * - [opt] handle colon + */ +export function escapeSGFText(txt: string, escapeColon: boolean = false): string { + // escape slashes first + // 'blah\blah' -> 'blah\\blah' + txt = txt.replace(/\\/g, "\\\\"); + + // escape closing square bracket ] + // 'blah[9dan]' -> 'blah[9dan\]' + txt = txt.replace(/]/g, "\\]"); + + // no need to escape opening bracket, SGF grammar handles that + // 'C[[[[[[blah blah]' + // ^ after it finds the first [, it is only looking for the closing bracket + // parsing SGF properties, so the remaining [ are safely treated as text + //txt = txt.replace(/[/g, "\\["); + + // sub whitespace except newline & carriage return by space + txt = txt.replace(/[^\S\r\n]/g, " "); + + if (escapeColon) { + txt = txt.replace(/:/g, "\\:"); + } + return txt; +} + +// in SGF simple text, we also need to get rid of the newlines +export function newline2space(txt: string): string { + return txt.replace(/[\r\n]/g, " "); +} + +/** 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), + ); +} + +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), + }; +} + +function rgbToHex(r: number, g: number, b: number): string { + return "#" + [r, g, b].map((c) => c.toString(16).padStart(2, "0")).join(""); +} From d52116da75b97005d3c1ca937bc5ca4b9ade0a3a Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Tue, 11 Jun 2024 04:18:19 -0600 Subject: [PATCH 18/68] Refactor: add comments --- src/MoveTree.ts | 4 ++-- src/util.ts | 30 +++++++++++++++++++++++------- 2 files changed, 25 insertions(+), 9 deletions(-) diff --git a/src/MoveTree.ts b/src/MoveTree.ts index 9288b757..c71b612d 100644 --- a/src/MoveTree.ts +++ b/src/MoveTree.ts @@ -19,7 +19,7 @@ import { GoEngine, GoEngineState } from "./GoEngine"; import { encodeMove } from "./GoMath"; import { AdHocPackedMove } from "./AdHocFormat"; import { JGOFNumericPlayerColor, JGOFPlayerSummary } from "./JGOF"; -import { escapeSGFText, newline2space } from "./util"; +import { escapeSGFText, newlines_to_spaces } from "./util"; export interface MarkInterface { triangle?: boolean; @@ -714,7 +714,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 + "]"); } } diff --git a/src/util.ts b/src/util.ts index 38b14312..07cd8679 100644 --- a/src/util.ts +++ b/src/util.ts @@ -18,11 +18,11 @@ import { _, interpolate } from "./translate"; import type { JGOFTimeControl } from "./JGOF"; /** 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; } +/** 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; @@ -42,6 +42,7 @@ export function shortDurationString(seconds: number) { ); } +/** Deep clones an object */ export function dup(obj: any): any { let ret: any; if (typeof obj === "object") { @@ -62,6 +63,7 @@ export function dup(obj: any): any { return ret; } +/** Deep compares two objects */ export function deepEqual(a: any, b: any) { if (typeof a !== typeof b) { return false; @@ -102,11 +104,18 @@ export function deepEqual(a: any, b: any) { } } +/** + * 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 { - // 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); } +/** + * Compute the expected average time per move for a given time control. + */ export function computeAverageMoveTime( time_control: JGOFTimeControl, w?: number, @@ -152,9 +161,11 @@ export function computeAverageMoveTime( } } -/* Like setInterval, but debounces catchups that happen - * when tabs wake up on some browsers. Cleared with - * the standard clearInterval. */ +/** + * 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, @@ -210,8 +221,11 @@ 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, " "); } @@ -227,6 +241,7 @@ export function color_blend(c1: string, c2: string): string { ); } +/** 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) { @@ -239,6 +254,7 @@ function hexToRgb(hex: string): { r: number; g: number; b: number } { }; } +/** 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(""); } From 3f0c44565ec6a0be550cc52a28c60649456669b8 Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Tue, 11 Jun 2024 07:24:03 -0600 Subject: [PATCH 19/68] Refactoring common board and stone removal behavior to common base class GoEngine and ScoreEstimator had essentially the same logic. This also necessitated some refactoring of our "hook" system, now better named "callbacks" to be less confusing with react hooks. --- src/Board.ts | 204 ++++++++++++++++++++++++++++++++++ src/GoEngine.ts | 218 ++++++------------------------------- src/GoMath.ts | 9 +- src/GoStoneGroup.ts | 10 +- src/GoStoneGroups.ts | 11 +- src/GobanCanvas.ts | 31 ++---- src/GobanCore.ts | 143 ++++++++---------------- src/GobanSVG.ts | 31 ++---- src/ScoreEstimator.ts | 107 +++--------------- src/autoscore.ts | 77 ++++++------- src/callbacks.ts | 83 ++++++++++++++ src/canvas_utils.ts | 6 +- src/goban.ts | 1 + src/test.tsx | 14 +-- src/themes/board_plain.ts | 34 ++---- src/themes/board_woods.ts | 6 +- src/themes/image_stones.ts | 60 +++------- src/util.ts | 5 + 18 files changed, 499 insertions(+), 551 deletions(-) create mode 100644 src/Board.ts create mode 100644 src/callbacks.ts diff --git a/src/Board.ts b/src/Board.ts new file mode 100644 index 00000000..978671c4 --- /dev/null +++ b/src/Board.ts @@ -0,0 +1,204 @@ +/* + * 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 { Events } from "./GobanCore"; +import { EventEmitter } from "eventemitter3"; +import { JGOFNumericPlayerColor } from "./JGOF"; +import { makeMatrix } from "./GoMath"; +import * as goscorer from "./goscorer/goscorer"; +import { GoStoneGroups } from "./GoStoneGroups"; +import { GobanCore } from "./GobanCore"; +import { Group } from "./GoStoneGroup"; +import { cloneMatrix } from "./util"; +import { callbacks } from "./callbacks"; + +export interface BoardConfig { + width?: number; + height?: number; + board?: JGOFNumericPlayerColor[][]; + removal?: boolean[][]; +} + +export class Board extends EventEmitter { + public readonly height: number = 19; + //public readonly rules:GoEngineRules = 'japanese'; + public readonly width: number = 19; + public board: JGOFNumericPlayerColor[][]; + public removal: boolean[][]; + protected goban_callback?: GobanCore; + + /** + * 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?: GobanCore) { + 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"); + } + } + + /** 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: Group; + } { + 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 GoStoneGroups(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.foreachNeighborSpaceGroup((gr) => { + let is_territory_group = false; + gr.foreachStone((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.points.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.foreachStone(({ x, y }) => this.setRemoved(x, y, removing, false)); + + this.emit("stone-removal.updated"); + return { removed: removing, group: selected_group.points }; + } + } 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); + } + } + + 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"); + } + } +} diff --git a/src/GoEngine.ts b/src/GoEngine.ts index 166c7819..35b0b973 100644 --- a/src/GoEngine.ts +++ b/src/GoEngine.ts @@ -14,12 +14,12 @@ * limitations under the License. */ +import { Board, BoardConfig } from "./Board"; import { GobanMoveError } from "./GobanError"; import { MoveTree, MoveTreeJson } from "./MoveTree"; -import { Move, Intersection, encodeMove } from "./GoMath"; +import { Move, Intersection, encodeMove, makeMatrix } from "./GoMath"; import * as GoMath from "./GoMath"; import { Group } from "./GoStoneGroup"; -import { GoStoneGroups } from "./GoStoneGroups"; import { ScoreEstimator } from "./ScoreEstimator"; import { GobanCore, Events } from "./GobanCore"; import { @@ -99,7 +99,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 GoEngineConfig extends BoardConfig { game_id?: number | string; review_id?: number; game_name?: string; @@ -271,13 +271,11 @@ export interface ReviewMessage { "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; marks?: { [mark: string]: string }; puzzle_autoplace_delay?: number; @@ -302,11 +300,10 @@ let __currentMarker = 0; export type PlayerColor = "black" | "white"; -export class GoEngine extends EventEmitter { +export class GoEngine extends Board { //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; @@ -338,10 +335,7 @@ export class GoEngine extends EventEmitter { public puzzle_type: string = "[missing puzzle type]"; public readonly config: GoEngineConfig; 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 time_control: JGOFTimeControl = { system: "none", @@ -479,7 +473,6 @@ export class GoEngine extends EventEmitter { private black_prisoners: number = 0; private white_prisoners: number = 0; private board_is_repeating: boolean; - private goban_callback?: GobanCore; private dontStoreBoardHistory: boolean; public free_handicap_placement: boolean = false; private loading_sgf: boolean = false; @@ -499,19 +492,27 @@ export class GoEngine extends EventEmitter { goban_callback?: GobanCore, 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( + GoEngine.fillDefaults( + GoEngine.normalizeConfig( + ((config: GoEngineConfig): GoEngineConfig => { + /* 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") { @@ -528,8 +529,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; @@ -542,19 +541,7 @@ 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); - } + this.marks = makeMatrix(this.width, this.height, 0); try { this.config.original_disable_analysis = this.config.disable_analysis; @@ -1580,143 +1567,6 @@ export class GoEngine extends EventEmitter { }; } - public toggleSingleGroupRemoval( - x: number, - y: number, - force_removal: boolean = false, - ): Array<[0 | 1, Group]> { - try { - if (x >= 0 && y >= 0) { - const removing: 0 | 1 = !this.removal[y][x] ? 1 : 0; - - /* 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, - ); - const groups = new GoStoneGroups(this, this.board); - const selected_group = groups.getGroup(x, y); - let total_territory_adjacency_count = 0; - let total_territory_group_count = 0; - selected_group.foreachNeighborSpaceGroup((gr) => { - let is_territory_group = false; - gr.foreachStone((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.points.length; - } - }); - if (total_territory_adjacency_count >= 5 || total_territory_group_count >= 2) { - console.log("This group is almost assuredly alive, refusing to remove"); - GobanCore.hooks.toast?.("refusing_to_remove_group_is_alive", 4000); - return [[0, []]]; - } - } - - /* Otherwise, toggle the group */ - const group_color = this.board[y][x]; - - if (group_color === JGOFNumericPlayerColor.EMPTY) { - /* Disallow toggling of open area (old dame marking method that we no longer desire) */ - return [[0, []]]; - } - - const removed_stones = this.setGroupForRemoval(x, y, removing, false)[1]; - - this.emit("stone-removal.updated"); - return [[removing, removed_stones]]; - } - } 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 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 setNeedsSealing(x: number, y: number, needs_sealing?: boolean): void { this.cur_move.getMarks(x, y).needs_sealing = needs_sealing; } @@ -1799,10 +1649,7 @@ export class GoEngine extends EventEmitter { } if (this.score_stones) { - const scoring = goscorer.areaScoring( - this.board, - this.removal.map((row) => row.map((x) => !!x)), - ); + 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) { @@ -1823,10 +1670,7 @@ export class GoEngine extends EventEmitter { } } } else { - const scoring = goscorer.territoryScoring( - this.board, - this.removal.map((row) => row.map((x) => !!x)), - ); + 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) { @@ -1883,7 +1727,7 @@ export class GoEngine extends EventEmitter { return 0; } - private static normalizeConfig(config: GoEngineConfig): void { + private static normalizeConfig(config: GoEngineConfig): GoEngineConfig { if (config.ladder !== config.ladder_id) { config.ladder_id = config.ladder; } @@ -1928,6 +1772,8 @@ export class GoEngine extends EventEmitter { ); } } + + return config; } public static fillDefaults(game_obj: GoEngineConfig): GoEngineConfig { if (!("phase" in game_obj)) { diff --git a/src/GoMath.ts b/src/GoMath.ts index 01b3d375..269ba314 100644 --- a/src/GoMath.ts +++ b/src/GoMath.ts @@ -19,11 +19,12 @@ import { AdHocPackedMove } from "./AdHocFormat"; export type Move = JGOFMove; export type Intersection = JGOFIntersection; -export type NumberMatrix = Array>; -export type StringMatrix = Array>; +export type Matrix = T[][]; +export type NumberMatrix = Matrix; +export type StringMatrix = Matrix; -export function makeMatrix(width: number, height: number, initialValue: number = 0): NumberMatrix { - const ret: NumberMatrix = []; +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) { diff --git a/src/GoStoneGroup.ts b/src/GoStoneGroup.ts index d52fc0a5..50976ef3 100644 --- a/src/GoStoneGroup.ts +++ b/src/GoStoneGroup.ts @@ -15,17 +15,11 @@ */ import { Intersection } from "./GoMath"; +import { Board } from "./Board"; import { JGOFNumericPlayerColor, JGOFIntersection } from "./JGOF"; export type Group = Array; -export interface BoardState { - width: number; - height: number; - board: Array>; - removal: Array>; -} - export class GoStoneGroup { public readonly points: Array; public readonly neighbors: Array; @@ -39,7 +33,7 @@ export class GoStoneGroup { private neighboring_space: GoStoneGroup[]; private neighboring_enemy: GoStoneGroup[]; - constructor(board_state: BoardState, id: number, color: JGOFNumericPlayerColor) { + constructor(board_state: Board, id: number, color: JGOFNumericPlayerColor) { this.points = []; this.neighbors = []; this.neighboring_space = []; diff --git a/src/GoStoneGroups.ts b/src/GoStoneGroups.ts index c5cadb00..f1a9f104 100644 --- a/src/GoStoneGroups.ts +++ b/src/GoStoneGroups.ts @@ -15,17 +15,18 @@ */ import * as GoMath from "./GoMath"; -import { GoStoneGroup, BoardState } from "./GoStoneGroup"; +import { GoStoneGroup } from "./GoStoneGroup"; +import { Board } from "./Board"; import { JGOFNumericPlayerColor } from "./JGOF"; export class GoStoneGroups { - private state: BoardState; - public group_id_map: Array>; + private state: Board; + public group_id_map: number[][]; public groups: Array; - constructor(state: BoardState, original_board?: Array>) { + constructor(state: Board, 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); + const group_id_map = GoMath.makeMatrix(state.width, state.height, 0); this.state = state; this.group_id_map = group_id_map; diff --git a/src/GobanCanvas.ts b/src/GobanCanvas.ts index e69ae7dc..6b823d4b 100644 --- a/src/GobanCanvas.ts +++ b/src/GobanCanvas.ts @@ -21,7 +21,6 @@ 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"; @@ -36,6 +35,7 @@ import { _ } from "./translate"; import { formatMessage, MessageID } from "./messages"; import { color_blend, getRandomInt } from "./util"; import { GoStoneGroups } from "./GoStoneGroups"; +import { callbacks } from "./callbacks"; const __theme_cache: { [bw: string]: { [name: string]: { [size: string]: any } }; @@ -388,8 +388,8 @@ 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( + if (callbacks.addCoordinatesToChatInput) { + callbacks.addCoordinatesToChatInput( this.engine.prettyCoords(pt.i, pt.j), ); } @@ -860,19 +860,16 @@ export class GobanCanvas extends GobanCore implements GobanCanvasInterface { 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 || press_duration_ms > 500) { - //[[removed, group]] = this.engine.toggleMetaGroupRemoval(x, y); - [[removed, group]] = this.engine.toggleSingleGroupRemoval(x, y, true); - } else { - [[removed, group]] = this.engine.toggleSingleGroupRemoval(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, + removed: removed, stones: GoMath.encodeMoves(group), }); } @@ -3299,8 +3296,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: () => {} }; } @@ -3594,11 +3591,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/GobanCore.ts b/src/GobanCore.ts index 8517948d..afe2af53 100644 --- a/src/GobanCore.ts +++ b/src/GobanCore.ts @@ -51,6 +51,7 @@ import { MessageID } from "./messages"; import { GobanSocket, GobanSocketEvents } from "./GobanSocket"; import { ServerToClient, GameChatMessage, GameChatLine, StallingScoreEstimate } from "./protocol"; import { EventEmitter } from "eventemitter3"; +import { callbacks } from "./callbacks"; declare let swal: any; @@ -314,54 +315,6 @@ export interface Events extends StateUpdateEvents { "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; - - toast?: (message_id: string, duration: number) => void; -} - export interface GobanMetrics { width: number; height: number; @@ -597,10 +550,6 @@ export abstract class GobanCore extends EventEmitter { protected abstract enableDrawing(): void; protected abstract disableDrawing(): void; - public static hooks: GobanHooks = { - getClockDrift: () => 0, - }; - constructor(config: GobanConfig, preloaded_data?: GobanConfig) { super(); @@ -790,90 +739,84 @@ export abstract class GobanCore extends EventEmitter { 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(); + if (callbacks.getClockDrift) { + return callbacks.getClockDrift(); } console.warn("getClockDrift not provided for Goban instance"); return 0; } protected getNetworkLatency(): number { - if (GobanCore.hooks.getNetworkLatency) { - return GobanCore.hooks.getNetworkLatency(); + if (callbacks.getNetworkLatency) { + return callbacks.getNetworkLatency(); } console.warn("getNetworkLatency not provided for Goban instance"); return 0; } protected getCoordinateDisplaySystem(): "A1" | "1-1" { - if (GobanCore.hooks.getCoordinateDisplaySystem) { - return GobanCore.hooks.getCoordinateDisplaySystem(); + if (callbacks.getCoordinateDisplaySystem) { + return callbacks.getCoordinateDisplaySystem(); } return "A1"; } protected getShowMoveNumbers(): boolean { - if (GobanCore.hooks.getShowMoveNumbers) { - return GobanCore.hooks.getShowMoveNumbers(); + if (callbacks.getShowMoveNumbers) { + return callbacks.getShowMoveNumbers(); } return false; } protected getShowVariationMoveNumbers(): boolean { - if (GobanCore.hooks.getShowVariationMoveNumbers) { - return GobanCore.hooks.getShowVariationMoveNumbers(); + if (callbacks.getShowVariationMoveNumbers) { + return callbacks.getShowVariationMoveNumbers(); } return false; } public static getMoveTreeNumbering(): string { - if (GobanCore.hooks.getMoveTreeNumbering) { - return GobanCore.hooks.getMoveTreeNumbering(); + if (callbacks.getMoveTreeNumbering) { + return callbacks.getMoveTreeNumbering(); } return "move-number"; } public static getCDNReleaseBase(): string { - if (GobanCore.hooks.getCDNReleaseBase) { - return GobanCore.hooks.getCDNReleaseBase(); + if (callbacks.getCDNReleaseBase) { + return callbacks.getCDNReleaseBase(); } return ""; } public static getSoundEnabled(): boolean { - if (GobanCore.hooks.getSoundEnabled) { - return GobanCore.hooks.getSoundEnabled(); + if (callbacks.getSoundEnabled) { + return callbacks.getSoundEnabled(); } return true; } public static getSoundVolume(): number { - if (GobanCore.hooks.getSoundVolume) { - return GobanCore.hooks.getSoundVolume(); + if (callbacks.getSoundVolume) { + return callbacks.getSoundVolume(); } return 0.5; } protected defaultConfig(): any { - if (GobanCore.hooks.defaultConfig) { - return GobanCore.hooks.defaultConfig(); + if (callbacks.defaultConfig) { + return callbacks.defaultConfig(); } return {}; } public isAnalysisDisabled(perGameSettingAppliesToNonPlayers: boolean = false): boolean { - if (GobanCore.hooks.isAnalysisDisabled) { - return GobanCore.hooks.isAnalysisDisabled(this, perGameSettingAppliesToNonPlayers); + if (callbacks.isAnalysisDisabled) { + return callbacks.isAnalysisDisabled(this, perGameSettingAppliesToNonPlayers); } return false; } protected getLocation(): string { - if (GobanCore.hooks.getLocation) { - return GobanCore.hooks.getLocation(); + if (callbacks.getLocation) { + return callbacks.getLocation(); } return window.location.pathname; } protected getSelectedThemes(): GobanSelectedThemes { - if (GobanCore.hooks.getSelectedThemes) { - return GobanCore.hooks.getSelectedThemes(); + if (callbacks.getSelectedThemes) { + return callbacks.getSelectedThemes(); } //return {white:'Plain', black:'Plain', board:'Plain'}; //return {white:'Plain', black:'Plain', board:'Kaya'}; @@ -917,10 +860,10 @@ export abstract class GobanCore extends EventEmitter { if (!this.isCurrentUserAPlayer()) { return; } - if (!GobanCore.hooks.getNetworkLatency) { + if (!callbacks.getNetworkLatency) { return; } - const latency = GobanCore.hooks.getNetworkLatency(); + const latency = callbacks.getNetworkLatency(); if (!latency) { return; } @@ -2091,6 +2034,8 @@ export abstract class GobanCore extends EventEmitter { this.width = new_width; this.height = new_height; + console.log("New width and height", this.width, this.height); + delete this.move_selected; this.bounds = config.bounds || { @@ -2202,6 +2147,13 @@ export abstract class GobanCore extends EventEmitter { // in case the constructor some side effects on `this` // (JM: which it currently does) this.engine = new GoEngine(config, this); + console.log( + "New engine: ", + config, + this.engine.width, + this.engine.height, + this.engine.board, + ); this.emit("engine.updated", this.engine); this.engine.parentEventEmitter = this; this.engine.getState_callback = () => { @@ -2274,7 +2226,10 @@ export abstract class GobanCore extends EventEmitter { (this as any).autoScore(); } + console.log("Keeping old config?", keep_old_engine); + this.emit("load", config); + return this.engine; } public set(x: number, y: number, player: JGOFNumericPlayerColor): void { @@ -2283,7 +2238,7 @@ export abstract class GobanCore extends EventEmitter { public setForRemoval( x: number, y: number, - removed: number, + removed: boolean, emit_stone_removal_updated: boolean = true, ) { if (removed) { @@ -2294,7 +2249,7 @@ export abstract class GobanCore extends EventEmitter { this.getMarks(x, y).remove = false; } this.drawSquare(x, y); - this.emit("set-for-removal", { x, y, removed: !!removed }); + this.emit("set-for-removal", { x, y, removed }); if (emit_stone_removal_updated) { this.emit("stone-removal.updated"); } @@ -3187,8 +3142,8 @@ export abstract class GobanCore extends EventEmitter { 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 (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)); @@ -3312,9 +3267,7 @@ export abstract class GobanCore extends EventEmitter { } if (color) { - const clock_drift = GobanCore.hooks?.getClockDrift - ? GobanCore.hooks?.getClockDrift() - : 0; + const clock_drift = callbacks?.getClockDrift ? callbacks?.getClockDrift() : 0; const current_server_time = Date.now() - clock_drift; @@ -3418,8 +3371,8 @@ export abstract class GobanCore extends EventEmitter { let current_server_time = 0; function update_current_server_time() { - if (GobanCore.hooks.getClockDrift) { - const server_time_offset = GobanCore.hooks.getClockDrift(); + if (callbacks.getClockDrift) { + const server_time_offset = callbacks.getClockDrift(); current_server_time = Date.now() - server_time_offset; } } diff --git a/src/GobanSVG.ts b/src/GobanSVG.ts index 481d583d..be1fe6c5 100644 --- a/src/GobanSVG.ts +++ b/src/GobanSVG.ts @@ -22,7 +22,6 @@ import { AdHocFormat } from "./AdHocFormat"; 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"; @@ -32,6 +31,7 @@ import { _ } from "./translate"; import { formatMessage, MessageID } from "./messages"; import { color_blend, getRandomInt } from "./util"; import { GoStoneGroups } from "./GoStoneGroups"; +import { callbacks } from "./callbacks"; //import { GobanCanvasConfig, GobanCanvasInterface } from "./GobanCanvas"; @@ -344,8 +344,8 @@ 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( + if (callbacks.addCoordinatesToChatInput) { + callbacks.addCoordinatesToChatInput( this.engine.prettyCoords(pt.i, pt.j), ); } @@ -832,19 +832,16 @@ export class GobanSVG extends GobanCore implements GobanSVGInterface { 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 || press_duration_ms > 500) { - //[[removed, group]] = this.engine.toggleMetaGroupRemoval(x, y); - [[removed, group]] = this.engine.toggleSingleGroupRemoval(x, y, true); - } else { - [[removed, group]] = this.engine.toggleSingleGroupRemoval(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, + removed: removed, stones: GoMath.encodeMoves(group), }); } @@ -3311,8 +3308,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: () => {} }; } @@ -3569,11 +3566,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; diff --git a/src/ScoreEstimator.ts b/src/ScoreEstimator.ts index aa8a1341..2468aed8 100644 --- a/src/ScoreEstimator.ts +++ b/src/ScoreEstimator.ts @@ -14,7 +14,6 @@ * limitations under the License. */ -import { dup } from "./util"; import { encodeMove } from "./GoMath"; import * as GoMath from "./GoMath"; import { GoStoneGroup } from "./GoStoneGroup"; @@ -25,6 +24,7 @@ import { JGOFMove, JGOFNumericPlayerColor, JGOFSealingIntersection } from "./JGO import { _ } from "./translate"; import { estimateScoreWasm } from "./local_estimators/wasm_estimator"; import * as goscorer from "./goscorer/goscorer"; +import { Board } from "./Board"; export { init_score_estimator, estimateScoreWasm } from "./local_estimators/wasm_estimator"; export { estimateScoreVoronoi } from "./local_estimators/voronoi"; @@ -100,10 +100,7 @@ export function set_local_scorer(scorer: LocalEstimator) { local_scorer = scorer; } -export class ScoreEstimator { - width: number; - height: number; - board: Array>; +export class ScoreEstimator extends Board { white: PlayerScore = { total: 0, stones: 0, @@ -125,8 +122,6 @@ export class ScoreEstimator { engine: GoEngine; private groups: GoStoneGroups; - removal: Array>; - goban_callback?: GobanCore; tolerance: number; amount: number = NaN; ownership: Array>; @@ -143,6 +138,7 @@ export class ScoreEstimator { public autoscored_needs_sealing?: JGOFSealingIntersection[]; constructor( + /* REFACTOR TODO: Engine puts config first before callback, this should be the same */ goban_callback: GobanCore | undefined, engine: GoEngine, trials: number, @@ -150,14 +146,11 @@ export class ScoreEstimator { prefer_remote: boolean = false, autoscore: boolean = false, ) { - this.goban_callback = goban_callback; + super(engine, goban_callback); 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.board = engine.cloneBoard(); this.ownership = GoMath.makeMatrix(this.width, this.height, 0); this.territory = GoMath.makeMatrix(this.width, this.height, 0); this.estimated_hard_score = 0.0; @@ -267,7 +260,7 @@ export class ScoreEstimator { tolerance = 0.25; } - const board = GoMath.makeMatrix(this.width, this.height); + const board = GoMath.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]; @@ -341,97 +334,23 @@ export class ScoreEstimator { return ret; } handleClick(i: number, j: number, mod_key: boolean, press_duration_ms: number): void { - if (mod_key || press_duration_ms > 500) { - this.toggleSingleGroupRemoval(i, j, true); - } else { - this.toggleSingleGroupRemoval(i, j); - } + 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 toggleSingleGroupRemoval(x: number, y: number, force_removal: boolean = false): void { - try { - if (x >= 0 && y >= 0) { - const removing: 0 | 1 = !this.removal[y][x] ? 1 : 0; - - const groups = new GoStoneGroups(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.foreachNeighborSpaceGroup((gr) => { - let is_territory_group = false; - gr.foreachStone((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.points.length; - } - }); - if (total_territory_adjacency_count >= 5 || total_territory_group_count >= 2) { - console.log("This group is almost assuredly alive, refusing to remove"); - GobanCore.hooks.toast?.("refusing_to_remove_group_is_alive", 4000); - return; - } - } - - /* Otherwise, toggle the group */ - const group_color = this.board[y][x]; - - if (group_color === JGOFNumericPlayerColor.EMPTY) { - /* Disallow toggling of open area (old dame marking method that we no longer desire) */ - return; - } - - this.removeGroup(selected_group, !!removing); - return; - } - } catch (err) { - console.log(err.stack); - } - } - - setRemoved(x: number, y: number, removed: number): void { + setRemoved(x: number, y: number, removed: boolean): void { this.clearAutoScore(); - - this.removal[y][x] = removed; - if (this.goban_callback) { - this.goban_callback.setForRemoval(x, y, this.removal[y][x]); - } + super.setRemoved(x, y, removed); } + 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; @@ -595,7 +514,7 @@ export function adjust_estimate( ) { let adjusted_score = score - engine.getHandicapPointAdjustmentForWhite(); const { width, height } = get_dimensions(board); - const ownership = GoMath.makeMatrix(width, height); + const ownership = GoMath.makeMatrix(width, height, 0); // For Japanese rules we use territory counting. Don't even // attempt to handle rules with score_stones and not diff --git a/src/autoscore.ts b/src/autoscore.ts index d1e5ebf1..f6a13ce4 100644 --- a/src/autoscore.ts +++ b/src/autoscore.ts @@ -25,6 +25,7 @@ import { GoStoneGroups } from "./GoStoneGroups"; import { JGOFNumericPlayerColor, JGOFSealingIntersection, JGOFMove } from "./JGOF"; import { char2num, makeMatrix, num2char, pretty_coor_num2ch } from "./GoMath"; import { GoEngine, GoEngineInitialState, GoEngineRules } from "./GoEngine"; +import { Board } from "./Board"; interface AutoscoreResults { result: JGOFNumericPlayerColor[][]; @@ -58,15 +59,15 @@ export function autoscore( const width = board[0].length; const height = board.length; const removed: JGOFMove[] = []; - 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 final_sealed_ownership = makeMatrix(board[0].length, board.length); - const sealed = makeMatrix(width, height); + 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); + 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] = @@ -81,12 +82,12 @@ export function autoscore( debug_ownership_output("White plays first estimates", white_plays_first_ownership); debug_ownership_output("Average estimates", average_ownership); - const groups = new GoStoneGroups({ - width, - height, - board, - removal: makeMatrix(width, height), - }); + const groups = new GoStoneGroups( + new Board({ + board, + removal: makeMatrix(width, height, false), + }), + ); debug_groups("Groups", groups); @@ -121,7 +122,7 @@ export function autoscore( removed.push({ x, y, removal_reason }); board[y][x] = JGOFNumericPlayerColor.EMPTY; - removal[y][x] = 1; + removal[y][x] = true; stage_log(`Removing ${pretty_coor_num2ch(x)}${height - y}: ${removal_reason}`); } @@ -137,12 +138,10 @@ export function autoscore( stage("Settling agreed upon territory"); const groups = new GoStoneGroups( - { - width, - height, + new Board({ board, removal, - }, + }), original_board, ); @@ -274,12 +273,12 @@ export function autoscore( * 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), - }); + const groups = new GoStoneGroups( + new Board({ + 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 @@ -301,7 +300,7 @@ export function autoscore( ]; let total_ownership_estimate = 0; - const already_tallied = makeMatrix(width, height); + const already_tallied = makeMatrix(width, height, 0); function tally_edge(x: number, y: number) { if (x < 0 || x >= width || y < 0 || y >= height) { return; @@ -399,15 +398,12 @@ export function autoscore( function seal_territory() { stage(`Sealing territory`); - //const dame_map = makeMatrix(width, height); { let groups = new GoStoneGroups( - { - width, - height, + new Board({ board, removal, - }, + }), original_board, ); @@ -469,12 +465,10 @@ export function autoscore( }); groups = new GoStoneGroups( - { - width: board[0].length, - height: board.length, + new Board({ board, removal, - }, + }), original_board, ); @@ -524,7 +518,7 @@ export function autoscore( }; for (const initial_state of [sealed_initial_state, real_initial_state]) { - const cur_ownership = makeMatrix(width, height); + const cur_ownership = makeMatrix(width, height, 0); const engine = new GoEngine({ width: original_board[0].length, @@ -538,7 +532,7 @@ export function autoscore( removed.map((pt) => (board[pt.y][pt.x] = 0)); const score = engine.computeScore(); - const scoring_positions = makeMatrix(width, height); + 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]); @@ -562,12 +556,10 @@ export function autoscore( } const groups = new GoStoneGroups( - { - width, - height, + new Board({ board, removal, - }, + }), engine.board, ); @@ -739,7 +731,7 @@ function colorizeIntersection(c: string): string { return yellow(c); } -function debug_boolean_board(title: string, board: number[][], mark = "S") { +function debug_boolean_board(title: string, board: (boolean | number)[][], mark = "S") { begin_board(title); let out = " "; const x_coords = "ABCDEFGHJKLMNOPQRST"; // cspell: disable-line @@ -774,7 +766,8 @@ function debug_groups(title: string, 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; diff --git a/src/callbacks.ts b/src/callbacks.ts new file mode 100644 index 00000000..a280aa54 --- /dev/null +++ b/src/callbacks.ts @@ -0,0 +1,83 @@ +/* + * 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 { GobanCore, GobanSelectedThemes } from "./GobanCore"; + +export interface GobanCallbacks { + 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; + + 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 setCallbacks(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/canvas_utils.ts index bf8bf912..0eb95985 100644 --- a/src/canvas_utils.ts +++ b/src/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.ts b/src/goban.ts index 24e0e114..4cd3e241 100644 --- a/src/goban.ts +++ b/src/goban.ts @@ -36,6 +36,7 @@ export * from "./TestGoban"; export * from "./test_utils"; export * from "./GobanSocket"; export * from "./util"; +export * from "./callbacks"; export * from "./autoscore"; export * as GoMath from "./GoMath"; diff --git a/src/test.tsx b/src/test.tsx index a8867717..f7b51db0 100644 --- a/src/test.tsx +++ b/src/test.tsx @@ -16,20 +16,21 @@ import * as React from "react"; import * as ReactDOM from "react-dom/client"; -import { GobanCore, GobanConfig, GobanHooks, ColoredCircle } from "./GobanCore"; +import { GobanCore, GobanConfig, 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"; +import { callbacks, setCallbacks } from "./callbacks"; let stored_config: GobanConfig = {}; try { stored_config = JSON.parse(localStorage.getItem("config") || "{}"); } catch (e) {} -GobanCore.hooks.getSelectedThemes = () => ({ +callbacks.getSelectedThemes = () => ({ board: "Kaya", //board: "Anime", @@ -49,10 +50,10 @@ GobanCore.hooks.getSelectedThemes = () => ({ //black: "Custom", }); -GobanCore.hooks.customWhiteStoneUrl = () => { +callbacks.customWhiteStoneUrl = () => { return "https://cdn.online-go.com/goban/anime_white.svg"; }; -GobanCore.hooks.customBlackStoneUrl = () => { +callbacks.customBlackStoneUrl = () => { return "https://cdn.online-go.com/goban/anime_black.svg"; }; @@ -99,12 +100,11 @@ const base_config: GobanConfig = Object.assign( stored_config, ); -const hooks: GobanHooks = { +setCallbacks({ //getCoordinateDisplaySystem: () => "1-1", getCoordinateDisplaySystem: () => "A1", getCDNReleaseBase: () => "", -}; -GobanCore.setHooks(hooks); +}); function save() { localStorage.setItem("config", JSON.stringify(base_config)); diff --git a/src/themes/board_plain.ts b/src/themes/board_plain.ts index 5d5a0629..84d7565c 100644 --- a/src/themes/board_plain.ts +++ b/src/themes/board_plain.ts @@ -16,7 +16,7 @@ import { GoTheme, GoThemeBackgroundCSS } from "../GoTheme"; import { GoThemesInterface } from "../GoThemes"; -import { GobanCore } from "../GobanCore"; +import { callbacks } from "../callbacks"; import { _ } from "../translate"; // Converts a six-digit hex string to rgba() notation @@ -77,52 +77,40 @@ export default function (GoThemes: GoThemesInterface) { } getBackgroundCSS(): GoThemeBackgroundCSS { 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"; + return callbacks.customBoardLineColor ? callbacks.customBoardLineColor() : "#000000"; } 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"; + return callbacks.customBoardLineColor ? callbacks.customBoardLineColor() : "#000000"; } 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"; + return callbacks.customBoardLineColor ? callbacks.customBoardLineColor() : "#000000"; } getLabelTextColor(): string { return hexToRgba( - GobanCore.hooks.customBoardLineColor - ? GobanCore.hooks.customBoardLineColor() - : "#000000", + callbacks.customBoardLineColor ? callbacks.customBoardLineColor() : "#000000", 0.75, ); } diff --git a/src/themes/board_woods.ts b/src/themes/board_woods.ts index bcaeddbe..1fb87564 100644 --- a/src/themes/board_woods.ts +++ b/src/themes/board_woods.ts @@ -17,11 +17,11 @@ import { GoTheme, GoThemeBackgroundCSS } from "../GoTheme"; import { GoThemesInterface } from "../GoThemes"; import { _ } from "../translate"; -import { GobanCore } from "../GobanCore"; +import { callbacks } from "../callbacks"; function getCDNReleaseBase() { - if (GobanCore.hooks.getCDNReleaseBase) { - return GobanCore.hooks.getCDNReleaseBase(); + if (callbacks.getCDNReleaseBase) { + return callbacks.getCDNReleaseBase(); } return ""; } diff --git a/src/themes/image_stones.ts b/src/themes/image_stones.ts index 37215d3e..e70fed9a 100644 --- a/src/themes/image_stones.ts +++ b/src/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 { 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")); function getCDNReleaseBase() { - if (GobanCore.hooks.getCDNReleaseBase) { - return GobanCore.hooks.getCDNReleaseBase(); + if (callbacks.getCDNReleaseBase) { + return callbacks.getCDNReleaseBase(); } return ""; } @@ -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( @@ -342,15 +339,12 @@ export default function (GoThemes: GoThemesInterface) { _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 */, ); @@ -358,15 +352,11 @@ export default function (GoThemes: GoThemesInterface) { } public getBlackStoneColor(): string { - return GobanCore.hooks.customBlackStoneColor - ? GobanCore.hooks.customBlackStoneColor() - : "#000000"; + return callbacks.customBlackStoneColor ? callbacks.customBlackStoneColor() : "#000000"; } public getBlackTextColor(): string { - return GobanCore.hooks.customBlackTextColor - ? GobanCore.hooks.customBlackTextColor() - : "#FFFFFF"; + return callbacks.customBlackTextColor ? callbacks.customBlackTextColor() : "#FFFFFF"; } placeWhiteStone( @@ -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( @@ -399,15 +386,12 @@ export default function (GoThemes: GoThemesInterface) { _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 */, ); @@ -415,15 +399,11 @@ export default function (GoThemes: GoThemesInterface) { } public getWhiteStoneColor(): string { - return GobanCore.hooks.customWhiteStoneColor - ? GobanCore.hooks.customWhiteStoneColor() - : "#FFFFFF"; + return callbacks.customWhiteStoneColor ? callbacks.customWhiteStoneColor() : "#FFFFFF"; } public getWhiteTextColor(): string { - return GobanCore.hooks.customWhiteTextColor - ? GobanCore.hooks.customWhiteTextColor() - : "#000000"; + return callbacks.customWhiteTextColor ? callbacks.customWhiteTextColor() : "#000000"; } public preRenderBlackSVG( @@ -432,10 +412,7 @@ export default function (GoThemes: GoThemesInterface) { _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, ), @@ -458,10 +435,7 @@ export default function (GoThemes: GoThemesInterface) { _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, ), diff --git a/src/util.ts b/src/util.ts index 07cd8679..a740ff79 100644 --- a/src/util.ts +++ b/src/util.ts @@ -22,6 +22,11 @@ export function getRandomInt(min: number, max: number) { return Math.floor(Math.random() * (max - min)) + min; } +/** Returns a cloned copy of the provided matrix */ +export function cloneMatrix(matrix: T[][]): T[][] { + return matrix.map((row) => row.slice()); +} + /** 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)); From 5214b49f960279a8f620988a55f63c6b9bc73ffd Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Tue, 11 Jun 2024 07:43:38 -0600 Subject: [PATCH 20/68] Refactor: GoStoneGroup -> StoneString --- src/Board.ts | 8 ++--- src/GoEngine.ts | 16 +++++----- src/GobanCanvas.ts | 4 +-- src/GobanCore.ts | 2 -- src/GobanSVG.ts | 4 +-- src/ScoreEstimator.ts | 10 +++--- src/{GoStoneGroup.ts => StoneString.ts} | 31 +++++++------------ ...GoStoneGroups.ts => StoneStringBuilder.ts} | 26 +++++----------- src/autoscore.ts | 16 +++++----- src/engine.ts | 2 +- src/goban.ts | 4 +-- 11 files changed, 50 insertions(+), 73 deletions(-) rename src/{GoStoneGroup.ts => StoneString.ts} (76%) rename src/{GoStoneGroups.ts => StoneStringBuilder.ts} (80%) diff --git a/src/Board.ts b/src/Board.ts index 978671c4..0420017e 100644 --- a/src/Board.ts +++ b/src/Board.ts @@ -19,9 +19,9 @@ import { EventEmitter } from "eventemitter3"; import { JGOFNumericPlayerColor } from "./JGOF"; import { makeMatrix } from "./GoMath"; import * as goscorer from "./goscorer/goscorer"; -import { GoStoneGroups } from "./GoStoneGroups"; +import { StoneStringBuilder } from "./StoneStringBuilder"; import { GobanCore } from "./GobanCore"; -import { Group } from "./GoStoneGroup"; +import { RawStoneString } from "./StoneString"; import { cloneMatrix } from "./util"; import { callbacks } from "./callbacks"; @@ -91,7 +91,7 @@ export class Board extends EventEmitter { force_removal: boolean = false, ): { removed: boolean; - group: Group; + group: RawStoneString; } { const empty = { removed: false, group: [] }; if (x < 0 || y < 0) { @@ -111,7 +111,7 @@ export class Board extends EventEmitter { return empty; } - const groups = new GoStoneGroups(this, this.board); + 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 diff --git a/src/GoEngine.ts b/src/GoEngine.ts index 35b0b973..5a6eef1e 100644 --- a/src/GoEngine.ts +++ b/src/GoEngine.ts @@ -19,7 +19,7 @@ import { GobanMoveError } from "./GobanError"; import { MoveTree, MoveTreeJson } from "./MoveTree"; import { Move, Intersection, encodeMove, makeMatrix } from "./GoMath"; import * as GoMath from "./GoMath"; -import { Group } from "./GoStoneGroup"; +import { RawStoneString } from "./StoneString"; import { ScoreEstimator } from "./ScoreEstimator"; import { GobanCore, Events } from "./GobanCore"; import { @@ -1061,7 +1061,7 @@ export class GoEngine extends Board { private incrementCurrentMarker(): void { ++__currentMarker; } - private markGroup(group: Group): void { + private markGroup(group: RawStoneString): void { for (let i = 0; i < group.length; ++i) { this.marks[group[i].y][group[i].x] = __currentMarker; } @@ -1082,7 +1082,7 @@ export class GoEngine extends Board { } public foreachNeighbor( - pt_or_group: Intersection | Group, + pt_or_group: Intersection | RawStoneString, fn_of_neighbor_pt: (x: number, y: number) => void, ): void { if (pt_or_group instanceof Array) { @@ -1124,7 +1124,7 @@ export class GoEngine extends Board { } } /** Returns an array of x/y pairs of all the same color */ - private getGroup(x: number, y: number, clearMarks: boolean): Group { + private getGroup(x: number, y: number, clearMarks: boolean): RawStoneString { const color = this.board[y][x]; if (clearMarks) { this.incrementCurrentMarker(); @@ -1155,11 +1155,11 @@ export class GoEngine extends Board { return ret; } /** Returns an array of groups connected to the given group */ - private getConnectedGroups(group: Group): Array { + private getConnectedGroups(group: RawStoneString): Array { const gr = group; this.incrementCurrentMarker(); this.markGroup(group); - const ret: Array = []; + const ret: Array = []; this.foreachNeighbor(group, (x, y) => { if (this.board[y][x]) { this.incrementCurrentMarker(); @@ -1176,7 +1176,7 @@ export class GoEngine extends Board { }); return ret; } - private countLiberties(group: Group): number { + private countLiberties(group: RawStoneString): number { let ct = 0; const mat = GoMath.makeMatrix(this.width, this.height, 0); const counter = (x: number, y: number) => { @@ -1190,7 +1190,7 @@ export class GoEngine extends Board { } 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; diff --git a/src/GobanCanvas.ts b/src/GobanCanvas.ts index 6b823d4b..113ced0f 100644 --- a/src/GobanCanvas.ts +++ b/src/GobanCanvas.ts @@ -34,7 +34,7 @@ import { import { _ } from "./translate"; import { formatMessage, MessageID } from "./messages"; import { color_blend, getRandomInt } from "./util"; -import { GoStoneGroups } from "./GoStoneGroups"; +import { StoneStringBuilder } from "./StoneStringBuilder"; import { callbacks } from "./callbacks"; const __theme_cache: { @@ -3281,7 +3281,7 @@ export class GobanCanvas extends GobanCore implements GobanCanvasInterface { this.analysis_removal_state = true; } - const stone_group = new GoStoneGroups(this.engine).getGroup(x, y); + const stone_group = new StoneStringBuilder(this.engine).getGroup(x, y); stone_group.foreachStone((loc) => { this.putAnalysisRemovalAtLocation(loc.x, loc.y, this.analysis_removal_state); diff --git a/src/GobanCore.ts b/src/GobanCore.ts index afe2af53..0fe3f355 100644 --- a/src/GobanCore.ts +++ b/src/GobanCore.ts @@ -2226,8 +2226,6 @@ export abstract class GobanCore extends EventEmitter { (this as any).autoScore(); } - console.log("Keeping old config?", keep_old_engine); - this.emit("load", config); return this.engine; diff --git a/src/GobanSVG.ts b/src/GobanSVG.ts index be1fe6c5..d7b93307 100644 --- a/src/GobanSVG.ts +++ b/src/GobanSVG.ts @@ -30,7 +30,7 @@ import { getRelativeEventPosition } from "./canvas_utils"; import { _ } from "./translate"; import { formatMessage, MessageID } from "./messages"; import { color_blend, getRandomInt } from "./util"; -import { GoStoneGroups } from "./GoStoneGroups"; +import { StoneStringBuilder } from "./StoneStringBuilder"; import { callbacks } from "./callbacks"; //import { GobanCanvasConfig, GobanCanvasInterface } from "./GobanCanvas"; @@ -3293,7 +3293,7 @@ export class GobanSVG extends GobanCore implements GobanSVGInterface { this.analysis_removal_state = true; } - const stone_group = new GoStoneGroups(this.engine).getGroup(x, y); + const stone_group = new StoneStringBuilder(this.engine).getGroup(x, y); stone_group.foreachStone((loc) => { this.putAnalysisRemovalAtLocation(loc.x, loc.y, this.analysis_removal_state); diff --git a/src/ScoreEstimator.ts b/src/ScoreEstimator.ts index 2468aed8..38966b97 100644 --- a/src/ScoreEstimator.ts +++ b/src/ScoreEstimator.ts @@ -16,8 +16,8 @@ import { encodeMove } from "./GoMath"; import * as GoMath from "./GoMath"; -import { GoStoneGroup } from "./GoStoneGroup"; -import { GoStoneGroups } from "./GoStoneGroups"; +import { StoneString } from "./StoneString"; +import { StoneStringBuilder } from "./StoneStringBuilder"; import { GobanCore } from "./GobanCore"; import { GoEngine, PlayerScore, GoEngineRules } from "./GoEngine"; import { JGOFMove, JGOFNumericPlayerColor, JGOFSealingIntersection } from "./JGOF"; @@ -121,7 +121,7 @@ export class ScoreEstimator extends Board { }; engine: GoEngine; - private groups: GoStoneGroups; + private groups: StoneStringBuilder; tolerance: number; amount: number = NaN; ownership: Array>; @@ -160,7 +160,7 @@ export class ScoreEstimator extends Board { this.autoscore = autoscore; this.territory = GoMath.makeMatrix(this.width, this.height, 0); - this.groups = new GoStoneGroups(this); + this.groups = new StoneStringBuilder(this); this.when_ready = this.estimateScore(this.trials, this.tolerance, autoscore); } @@ -382,7 +382,7 @@ export class ScoreEstimator extends Board { } return ret; } - getGroup(x: number, y: number): GoStoneGroup { + getGroup(x: number, y: number): StoneString { return this.groups.groups[this.groups.group_id_map[y][x]]; } diff --git a/src/GoStoneGroup.ts b/src/StoneString.ts similarity index 76% rename from src/GoStoneGroup.ts rename to src/StoneString.ts index 50976ef3..1f42a2d6 100644 --- a/src/GoStoneGroup.ts +++ b/src/StoneString.ts @@ -14,24 +14,22 @@ * limitations under the License. */ -import { Intersection } from "./GoMath"; import { Board } from "./Board"; import { JGOFNumericPlayerColor, JGOFIntersection } from "./JGOF"; -export type Group = Array; +export type RawStoneString = Array; -export class GoStoneGroup { - public readonly points: Array; - public readonly neighbors: Array; +export class StoneString { + public readonly points: 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 corner_groups: { [y: string]: { [x: string]: GoStoneGroup } }; - private neighboring_space: GoStoneGroup[]; - private neighboring_enemy: GoStoneGroup[]; + private neighboring_space: StoneString[]; + private neighboring_enemy: StoneString[]; constructor(board_state: Board, id: number, color: JGOFNumericPlayerColor) { this.points = []; @@ -42,12 +40,11 @@ export class GoStoneGroup { this.color = color; this.__added_neighbors = {}; - this.corner_groups = {}; } addStone(x: number, y: number): void { this.points.push({ x: x, y: y }); } - addNeighborGroup(group: GoStoneGroup): void { + addNeighborGroup(group: StoneString): void { if (!(group.id in this.__added_neighbors)) { this.neighbors.push(group); this.__added_neighbors[group.id] = true; @@ -61,28 +58,22 @@ export class GoStoneGroup { } } } - 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 { + foreachStone(fn: (pt: JGOFIntersection) => void): void { for (let i = 0; i < this.points.length; ++i) { fn(this.points[i]); } } - foreachNeighborGroup(fn: (group: GoStoneGroup) => void): void { + foreachNeighborGroup(fn: (group: StoneString) => void): void { for (let i = 0; i < this.neighbors.length; ++i) { fn(this.neighbors[i]); } } - foreachNeighborSpaceGroup(fn: (group: GoStoneGroup) => void): void { + foreachNeighborSpaceGroup(fn: (group: StoneString) => void): void { for (let i = 0; i < this.neighboring_space.length; ++i) { fn(this.neighboring_space[i]); } } - foreachNeighborEnemyGroup(fn: (group: GoStoneGroup) => void): void { + foreachNeighborEnemyGroup(fn: (group: StoneString) => void): void { for (let i = 0; i < this.neighbors.length; ++i) { fn(this.neighboring_enemy[i]); } diff --git a/src/GoStoneGroups.ts b/src/StoneStringBuilder.ts similarity index 80% rename from src/GoStoneGroups.ts rename to src/StoneStringBuilder.ts index f1a9f104..afd8ac5c 100644 --- a/src/GoStoneGroups.ts +++ b/src/StoneStringBuilder.ts @@ -15,17 +15,17 @@ */ import * as GoMath from "./GoMath"; -import { GoStoneGroup } from "./GoStoneGroup"; +import { StoneString } from "./StoneString"; import { Board } from "./Board"; import { JGOFNumericPlayerColor } from "./JGOF"; -export class GoStoneGroups { +export class StoneStringBuilder { private state: Board; public group_id_map: number[][]; - public groups: Array; + public groups: Array; constructor(state: Board, 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 groups: Array = Array(1); // this is indexed by group_id, so we 1 index this array so group_id >= 1 const group_id_map = GoMath.makeMatrix(state.width, state.height, 0); this.state = state; @@ -78,7 +78,7 @@ 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]), + new StoneString(this.state, group_id_map[y][x], this.state.board[y][x]), ); } groups[group_id_map[y][x]].addStone(x, y); @@ -102,18 +102,6 @@ export class GoStoneGroups { 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]]); - } - } - } }); }); @@ -122,13 +110,13 @@ export class GoStoneGroups { }); } - public foreachGroup(fn: (gr: GoStoneGroup) => void) { + public foreachGroup(fn: (gr: StoneString) => void) { for (let i = 1; i < this.groups.length; ++i) { fn(this.groups[i]); } } - public getGroup(x: number, y: number): GoStoneGroup { + public getGroup(x: number, y: number): StoneString { return this.groups[this.group_id_map[y][x]]; } } diff --git a/src/autoscore.ts b/src/autoscore.ts index f6a13ce4..9f09d05e 100644 --- a/src/autoscore.ts +++ b/src/autoscore.ts @@ -21,7 +21,7 @@ * should be left alone. */ -import { GoStoneGroups } from "./GoStoneGroups"; +import { StoneStringBuilder } from "./StoneStringBuilder"; import { JGOFNumericPlayerColor, JGOFSealingIntersection, JGOFMove } from "./JGOF"; import { char2num, makeMatrix, num2char, pretty_coor_num2ch } from "./GoMath"; import { GoEngine, GoEngineInitialState, GoEngineRules } from "./GoEngine"; @@ -82,7 +82,7 @@ export function autoscore( debug_ownership_output("White plays first estimates", white_plays_first_ownership); debug_ownership_output("Average estimates", average_ownership); - const groups = new GoStoneGroups( + const groups = new StoneStringBuilder( new Board({ board, removal: makeMatrix(width, height, false), @@ -137,7 +137,7 @@ export function autoscore( function settle_agreed_upon_territory() { stage("Settling agreed upon territory"); - const groups = new GoStoneGroups( + const groups = new StoneStringBuilder( new Board({ board, removal, @@ -273,7 +273,7 @@ export function autoscore( * Consider unsettled groups. Count the unsettled stones along with * their neighboring stones */ - const groups = new GoStoneGroups( + const groups = new StoneStringBuilder( new Board({ board: is_settled, removal: makeMatrix(width, height, false), @@ -399,7 +399,7 @@ export function autoscore( function seal_territory() { stage(`Sealing territory`); { - let groups = new GoStoneGroups( + let groups = new StoneStringBuilder( new Board({ board, removal, @@ -464,7 +464,7 @@ export function autoscore( } }); - groups = new GoStoneGroups( + groups = new StoneStringBuilder( new Board({ board, removal, @@ -555,7 +555,7 @@ export function autoscore( } } - const groups = new GoStoneGroups( + const groups = new StoneStringBuilder( new Board({ board, removal, @@ -762,7 +762,7 @@ function debug_boolean_board(title: string, board: (boolean | number)[][], mark end_board(); } -function debug_groups(title: string, groups: GoStoneGroups) { +function debug_groups(title: string, groups: StoneStringBuilder) { const group_map: string[][] = makeMatrix( groups.group_id_map[0].length, groups.group_id_map.length, diff --git a/src/engine.ts b/src/engine.ts index df60abdf..6df666ee 100644 --- a/src/engine.ts +++ b/src/engine.ts @@ -15,7 +15,7 @@ */ export * from "./GobanError"; -export * from "./GoStoneGroup"; +export * from "./StoneString"; export * from "./GoEngine"; export * from "./GoConditionalMove"; export * from "./MoveTree"; diff --git a/src/goban.ts b/src/goban.ts index 4cd3e241..27a3a0b9 100644 --- a/src/goban.ts +++ b/src/goban.ts @@ -20,8 +20,8 @@ export * from "./GobanSVG"; export * from "./GoConditionalMove"; export * from "./GoEngine"; export * from "./GobanError"; -export * from "./GoStoneGroup"; -export * from "./GoStoneGroups"; +export * from "./StoneString"; +export * from "./StoneStringBuilder"; export * from "./GoTheme"; export * from "./GoThemes"; export * from "./util"; From 5137008c0b71dcc30a131cdfb6866421af72b096 Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Tue, 11 Jun 2024 07:57:48 -0600 Subject: [PATCH 21/68] Fix tests --- src/GobanCore.ts | 9 ----- src/__tests__/GoEngine.test.ts | 16 ++++---- src/__tests__/GoMath.test.ts | 47 +++++++++++------------ src/__tests__/GoMath_GoStoneGroup.test.ts | 17 ++++---- src/__tests__/GobanCanvas.test.ts | 13 ++++--- src/__tests__/GobanSVG.test.ts | 13 ++++--- src/__tests__/ScoreEstimator.test.ts | 18 ++++----- src/__tests__/util.test.ts | 6 +-- 8 files changed, 66 insertions(+), 73 deletions(-) diff --git a/src/GobanCore.ts b/src/GobanCore.ts index 0fe3f355..f2aed366 100644 --- a/src/GobanCore.ts +++ b/src/GobanCore.ts @@ -2034,8 +2034,6 @@ export abstract class GobanCore extends EventEmitter { this.width = new_width; this.height = new_height; - console.log("New width and height", this.width, this.height); - delete this.move_selected; this.bounds = config.bounds || { @@ -2147,13 +2145,6 @@ export abstract class GobanCore extends EventEmitter { // in case the constructor some side effects on `this` // (JM: which it currently does) this.engine = new GoEngine(config, this); - console.log( - "New engine: ", - config, - this.engine.width, - this.engine.height, - this.engine.board, - ); this.emit("engine.updated", this.engine); this.engine.parentEventEmitter = this; this.engine.getState_callback = () => { diff --git a/src/__tests__/GoEngine.test.ts b/src/__tests__/GoEngine.test.ts index 052b6a6c..6fc9a4e8 100644 --- a/src/__tests__/GoEngine.test.ts +++ b/src/__tests__/GoEngine.test.ts @@ -630,15 +630,15 @@ describe("groups", () => { expect(on_removal_updated).toBeCalledTimes(1); expect(engine.removal).toEqual([ - [1, 0, 0, 0], - [0, 0, 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.toggleSingleGroupRemoval(0, 0); - expect(engine.removal).toEqual(makeMatrix(4, 4)); + expect(engine.removal).toEqual(makeMatrix(4, 4, false)); }); test("toggleSingleGroupRemoval out-of-bounds", () => { @@ -658,7 +658,7 @@ describe("groups", () => { const on_removal_updated = jest.fn(); engine.addListener("stone-removal.updated", on_removal_updated); - expect(engine.toggleSingleGroupRemoval(0, 4)).toEqual([[0, []]]); + expect(engine.toggleSingleGroupRemoval(0, 4)).toEqual({ removed: false, group: [] }); expect(on_removal_updated).toBeCalledTimes(0); }); @@ -677,7 +677,7 @@ describe("groups", () => { const on_removal_updated = jest.fn(); engine.addListener("stone-removal.updated", on_removal_updated); - expect(engine.toggleSingleGroupRemoval(0, 1)).toEqual([[0, []]]); + expect(engine.toggleSingleGroupRemoval(0, 1)).toEqual({ removed: false, group: [] }); expect(on_removal_updated).toBeCalledTimes(0); }); @@ -699,7 +699,7 @@ 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", () => { diff --git a/src/__tests__/GoMath.test.ts b/src/__tests__/GoMath.test.ts index 976ec509..a8823a24 100644 --- a/src/__tests__/GoMath.test.ts +++ b/src/__tests__/GoMath.test.ts @@ -1,53 +1,52 @@ //cspell: disable -import { BoardState } from "../GoStoneGroup"; -import { GoStoneGroups } from "../GoStoneGroups"; +import { StoneStringBuilder } from "../StoneStringBuilder"; import { JGOFNumericPlayerColor } from "../JGOF"; import * as GoMath from "../GoMath"; +import { Board } from "../Board"; 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 Board({ + 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.groups.length).toBe(7); + expect(stone_string_builder.groups[0]).toBe(undefined); // what does this element represent? + expect(stone_string_builder.groups[1].points).toEqual([{ x: 0, y: 0 }]); + expect(stone_string_builder.groups[2].points).toEqual([{ x: 1, y: 0 }]); + expect(stone_string_builder.groups[3].points).toEqual([{ x: 2, y: 0 }]); + expect(stone_string_builder.groups[4].points).toEqual([ { x: 0, y: 1 }, { x: 0, y: 2 }, ]); - expect(board.groups[5].points).toEqual([ + expect(stone_string_builder.groups[5].points).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.groups[6].points).toEqual([{ x: 1, y: 2 }]); }); }); describe("matrices", () => { test("makeMatrix", () => { - expect(GoMath.makeMatrix(3, 2)).toEqual([ + expect(GoMath.makeMatrix(3, 2, 0)).toEqual([ [0, 0, 0], [0, 0, 0], ]); @@ -55,7 +54,7 @@ describe("matrices", () => { [1234, 1234, 1234], [1234, 1234, 1234], ]); - expect(GoMath.makeMatrix(0, 0)).toEqual([]); + expect(GoMath.makeMatrix(0, 0, 0)).toEqual([]); }); test("makeStringMatrix", () => { diff --git a/src/__tests__/GoMath_GoStoneGroup.test.ts b/src/__tests__/GoMath_GoStoneGroup.test.ts index 4983ca22..0a55c154 100644 --- a/src/__tests__/GoMath_GoStoneGroup.test.ts +++ b/src/__tests__/GoMath_GoStoneGroup.test.ts @@ -1,5 +1,6 @@ import * as GoMath from "../GoMath"; -import { GoStoneGroups } from "../GoStoneGroups"; +import { StoneStringBuilder } from "../StoneStringBuilder"; +import { Board } from "../Board"; // Here is a board displaying many of the features GoStoneGroup cares about. @@ -24,15 +25,15 @@ const FEATURE_BOARD = [ [2, 2, 1, 2, 0], ]; -const REMOVAL = GoMath.makeMatrix(5, 5); +const REMOVAL = GoMath.makeMatrix(5, 5, false); function makeGoMathWithFeatureBoard() { - return new GoStoneGroups({ - board: FEATURE_BOARD, - removal: REMOVAL, - width: 5, - height: 5, - }); + return new StoneStringBuilder( + new Board({ + board: FEATURE_BOARD, + removal: REMOVAL, + }), + ); } test("Group ID Map", () => { diff --git a/src/__tests__/GobanCanvas.test.ts b/src/__tests__/GobanCanvas.test.ts index fa67d9d6..a64e1331 100644 --- a/src/__tests__/GobanCanvas.test.ts +++ b/src/__tests__/GobanCanvas.test.ts @@ -7,7 +7,8 @@ (global as any).CLIENT = true; import { GobanCanvas, GobanCanvasConfig } from "../GobanCanvas"; -import { GobanCore, SCORE_ESTIMATION_TOLERANCE, SCORE_ESTIMATION_TRIALS } from "../GobanCore"; +import { SCORE_ESTIMATION_TOLERANCE, SCORE_ESTIMATION_TRIALS } from "../GobanCore"; +import { setCallbacks } from "../callbacks"; import { GobanSocket } from "../GobanSocket"; import * as GoMath from "../GoMath"; import WS from "jest-websocket-mock"; @@ -366,7 +367,7 @@ describe("onTap", () => { const canvas = document.getElementById("board-canvas") as HTMLCanvasElement; const addCoordinatesToChatInput = jest.fn(); - GobanCore.setHooks({ addCoordinatesToChatInput }); + setCallbacks({ addCoordinatesToChatInput }); canvas.dispatchEvent( new MouseEvent("click", { @@ -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: GoMath.makeMatrix(4, 2, 0), + removal: GoMath.makeMatrix(4, 2, false), + territory: GoMath.makeMatrix(4, 2, 0), + ownership: GoMath.makeMatrix(4, 2, 0), }; goban.engine.estimateScore = jest.fn().mockReturnValue(mock_score_estimate); diff --git a/src/__tests__/GobanSVG.test.ts b/src/__tests__/GobanSVG.test.ts index df2dfef4..7bc295d7 100644 --- a/src/__tests__/GobanSVG.test.ts +++ b/src/__tests__/GobanSVG.test.ts @@ -7,7 +7,8 @@ (global as any).CLIENT = true; import { GobanSVG, GobanSVGConfig } from "../GobanSVG"; -import { GobanCore, SCORE_ESTIMATION_TOLERANCE, SCORE_ESTIMATION_TRIALS } from "../GobanCore"; +import { SCORE_ESTIMATION_TOLERANCE, SCORE_ESTIMATION_TRIALS } from "../GobanCore"; +import { setCallbacks } from "../callbacks"; import { GobanSocket } from "../GobanSocket"; import * as GoMath from "../GoMath"; import WS from "jest-websocket-mock"; @@ -365,7 +366,7 @@ describe("onTap", () => { const event_layer = goban.event_layer; const addCoordinatesToChatInput = jest.fn(); - GobanCore.setHooks({ addCoordinatesToChatInput }); + setCallbacks({ addCoordinatesToChatInput }); event_layer.dispatchEvent( new MouseEvent("click", { @@ -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: GoMath.makeMatrix(4, 2, 0), + removal: GoMath.makeMatrix(4, 2, 0), + territory: GoMath.makeMatrix(4, 2, 0), + ownership: GoMath.makeMatrix(4, 2, 0), }; goban.engine.estimateScore = jest.fn().mockReturnValue(mock_score_estimate); diff --git a/src/__tests__/ScoreEstimator.test.ts b/src/__tests__/ScoreEstimator.test.ts index 2da7f4f2..a001b559 100644 --- a/src/__tests__/ScoreEstimator.test.ts +++ b/src/__tests__/ScoreEstimator.test.ts @@ -283,8 +283,8 @@ describe("ScoreEstimator", () => { 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 () => { @@ -357,11 +357,11 @@ describe("ScoreEstimator", () => { 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 () => { @@ -371,8 +371,8 @@ describe("ScoreEstimator", () => { se.handleClick(1, 0, true, 0); expect(se.removal).toEqual([ - [0, 1, 0, 0], - [0, 1, 0, 0], + [false, true, false, false], + [false, true, false, false], ]); }); @@ -383,8 +383,8 @@ describe("ScoreEstimator", () => { se.handleClick(1, 0, false, 1000); expect(se.removal).toEqual([ - [0, 1, 0, 0], - [0, 1, 0, 0], + [false, true, false, false], + [false, true, false, false], ]); }); diff --git a/src/__tests__/util.test.ts b/src/__tests__/util.test.ts index a6ab2ee6..261a4276 100644 --- a/src/__tests__/util.test.ts +++ b/src/__tests__/util.test.ts @@ -1,4 +1,4 @@ -import { escapeSGFText, newline2space } from "../util"; +import { escapeSGFText, newlines_to_spaces } from "../util"; import * as AdHoc from "../AdHocFormat"; // String.raw`...` is the real string @@ -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", () => { From 2c62b644c4b982581538ce87b0fb083117b698ed Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Tue, 11 Jun 2024 08:14:54 -0600 Subject: [PATCH 22/68] Refactor: Better stone string nomenclature --- src/Board.ts | 10 +-- src/GobanCanvas.ts | 2 +- src/GobanSVG.ts | 2 +- src/ScoreEstimator.ts | 2 +- src/StoneString.ts | 81 ++++++++++++++--------- src/StoneStringBuilder.ts | 38 +++++------ src/__tests__/GoMath.test.ts | 16 ++--- src/__tests__/GoMath_GoStoneGroup.test.ts | 4 +- src/autoscore.ts | 30 ++++----- 9 files changed, 100 insertions(+), 85 deletions(-) diff --git a/src/Board.ts b/src/Board.ts index 0420017e..523743c2 100644 --- a/src/Board.ts +++ b/src/Board.ts @@ -126,9 +126,9 @@ export class Board extends EventEmitter { ); let total_territory_adjacency_count = 0; let total_territory_group_count = 0; - selected_group.foreachNeighborSpaceGroup((gr) => { + selected_group.foreachNeighboringEmptyString((gr) => { let is_territory_group = false; - gr.foreachStone((pt) => { + gr.map((pt) => { if ( scores[pt.y][pt.x].isTerritoryFor === this.board[y][x] && !scores[pt.y][pt.x].isFalseEye @@ -139,7 +139,7 @@ export class Board extends EventEmitter { if (is_territory_group) { total_territory_group_count += 1; - total_territory_adjacency_count += gr.points.length; + total_territory_adjacency_count += gr.intersections.length; } }); if (total_territory_adjacency_count >= 5 || total_territory_group_count >= 2) { @@ -152,10 +152,10 @@ export class Board extends EventEmitter { /* 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.foreachStone(({ x, y }) => this.setRemoved(x, y, removing, false)); + selected_group.map(({ x, y }) => this.setRemoved(x, y, removing, false)); this.emit("stone-removal.updated"); - return { removed: removing, group: selected_group.points }; + return { removed: removing, group: selected_group.intersections }; } } catch (err) { console.log(err.stack); diff --git a/src/GobanCanvas.ts b/src/GobanCanvas.ts index 113ced0f..f148d276 100644 --- a/src/GobanCanvas.ts +++ b/src/GobanCanvas.ts @@ -3283,7 +3283,7 @@ export class GobanCanvas extends GobanCore implements GobanCanvasInterface { const stone_group = new StoneStringBuilder(this.engine).getGroup(x, y); - stone_group.foreachStone((loc) => { + stone_group.map((loc) => { this.putAnalysisRemovalAtLocation(loc.x, loc.y, this.analysis_removal_state); }); } diff --git a/src/GobanSVG.ts b/src/GobanSVG.ts index d7b93307..277ca757 100644 --- a/src/GobanSVG.ts +++ b/src/GobanSVG.ts @@ -3295,7 +3295,7 @@ export class GobanSVG extends GobanCore implements GobanSVGInterface { const stone_group = new StoneStringBuilder(this.engine).getGroup(x, y); - stone_group.foreachStone((loc) => { + stone_group.map((loc) => { this.putAnalysisRemovalAtLocation(loc.x, loc.y, this.analysis_removal_state); }); } diff --git a/src/ScoreEstimator.ts b/src/ScoreEstimator.ts index 38966b97..fc308cf9 100644 --- a/src/ScoreEstimator.ts +++ b/src/ScoreEstimator.ts @@ -383,7 +383,7 @@ export class ScoreEstimator extends Board { return ret; } getGroup(x: number, y: number): StoneString { - return this.groups.groups[this.groups.group_id_map[y][x]]; + return this.groups.stone_strings[this.groups.stone_string_id_map[y][x]]; } /** diff --git a/src/StoneString.ts b/src/StoneString.ts index 1f42a2d6..d9a1444f 100644 --- a/src/StoneString.ts +++ b/src/StoneString.ts @@ -14,13 +14,17 @@ * limitations under the License. */ -import { Board } from "./Board"; import { JGOFNumericPlayerColor, JGOFIntersection } from "./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 points: Array; + public readonly intersections: Array; public readonly neighbors: Array; public readonly color: JGOFNumericPlayerColor; public readonly id: number; @@ -31,8 +35,8 @@ export class StoneString { private neighboring_space: StoneString[]; private neighboring_enemy: StoneString[]; - constructor(board_state: Board, id: number, color: JGOFNumericPlayerColor) { - this.points = []; + constructor(id: number, color: JGOFNumericPlayerColor) { + this.intersections = []; this.neighbors = []; this.neighboring_space = []; this.neighboring_enemy = []; @@ -41,48 +45,61 @@ export class StoneString { this.__added_neighbors = {}; } - addStone(x: number, y: number): void { - this.points.push({ x: x, y: y }); - } - 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_enemy.push(group); - } - } - } - } - foreachStone(fn: (pt: JGOFIntersection) => void): void { - for (let i = 0; i < this.points.length; ++i) { - fn(this.points[i]); + public map(fn: (loc: JGOFIntersection) => void): void { + for (let i = 0; i < this.intersections.length; ++i) { + fn(this.intersections[i]); } } - foreachNeighborGroup(fn: (group: StoneString) => void): void { + public foreachNeighboringString(fn: (stone_string: StoneString) => void): void { for (let i = 0; i < this.neighbors.length; ++i) { fn(this.neighbors[i]); } } - foreachNeighborSpaceGroup(fn: (group: StoneString) => void): void { + public foreachNeighboringEmptyString(fn: (stone_string: StoneString) => void): void { for (let i = 0; i < this.neighboring_space.length; ++i) { fn(this.neighboring_space[i]); } } - foreachNeighborEnemyGroup(fn: (group: StoneString) => void): void { + public foreachNeighboringStoneString(fn: (stone_string: StoneString) => void): void { for (let i = 0; i < this.neighbors.length; ++i) { fn(this.neighboring_enemy[i]); } } - size(): number { - return this.points.length; + 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_enemy.push(group); + } + } + } } - computeIsTerritory(): void { - /* An empty group is considered territory if all of it's neighbors are of + + /** + * 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; @@ -98,7 +115,7 @@ export class StoneString { } } - this.foreachNeighborGroup((gr) => { + this.foreachNeighboringString((gr) => { if (gr.color !== 0 && color !== gr.color) { color = 0; } diff --git a/src/StoneStringBuilder.ts b/src/StoneStringBuilder.ts index afd8ac5c..28011568 100644 --- a/src/StoneStringBuilder.ts +++ b/src/StoneStringBuilder.ts @@ -21,16 +21,16 @@ import { JGOFNumericPlayerColor } from "./JGOF"; export class StoneStringBuilder { private state: Board; - public group_id_map: number[][]; - public groups: Array; + public readonly stone_string_id_map: number[][]; + public readonly stone_strings: StoneString[]; - constructor(state: Board, original_board?: Array>) { - const groups: Array = Array(1); // this is indexed by group_id, so we 1 index this array so group_id >= 1 + constructor(state: Board, 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 = GoMath.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, @@ -76,47 +76,45 @@ export class StoneStringBuilder { ); } - if (!(group_id_map[y][x] in groups)) { - groups.push( - new StoneString(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]]); + gr._addNeighborGroup(stone_strings[group_id_map[y + 1][x]]); } }); }); this.foreachGroup((gr) => { - gr.computeIsTerritory(); + gr._computeIsTerritory(); }); } public foreachGroup(fn: (gr: StoneString) => void) { - for (let i = 1; i < this.groups.length; ++i) { - fn(this.groups[i]); + for (let i = 1; i < this.stone_strings.length; ++i) { + fn(this.stone_strings[i]); } } public getGroup(x: number, y: number): StoneString { - return this.groups[this.group_id_map[y][x]]; + return this.stone_strings[this.stone_string_id_map[y][x]]; } } diff --git a/src/__tests__/GoMath.test.ts b/src/__tests__/GoMath.test.ts index a8823a24..5bb7edae 100644 --- a/src/__tests__/GoMath.test.ts +++ b/src/__tests__/GoMath.test.ts @@ -26,21 +26,21 @@ describe("GoStoneGroups constructor", () => { ); // TODO: examine usage in real code and flesh out expectations to reflect that usage - expect(stone_string_builder.groups.length).toBe(7); - expect(stone_string_builder.groups[0]).toBe(undefined); // what does this element represent? - expect(stone_string_builder.groups[1].points).toEqual([{ x: 0, y: 0 }]); - expect(stone_string_builder.groups[2].points).toEqual([{ x: 1, y: 0 }]); - expect(stone_string_builder.groups[3].points).toEqual([{ x: 2, y: 0 }]); - expect(stone_string_builder.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(stone_string_builder.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(stone_string_builder.groups[6].points).toEqual([{ x: 1, y: 2 }]); + expect(stone_string_builder.stone_strings[6].intersections).toEqual([{ x: 1, y: 2 }]); }); }); diff --git a/src/__tests__/GoMath_GoStoneGroup.test.ts b/src/__tests__/GoMath_GoStoneGroup.test.ts index 0a55c154..81597603 100644 --- a/src/__tests__/GoMath_GoStoneGroup.test.ts +++ b/src/__tests__/GoMath_GoStoneGroup.test.ts @@ -39,7 +39,7 @@ function makeGoMathWithFeatureBoard() { test("Group ID Map", () => { const gm = makeGoMathWithFeatureBoard(); - expect(gm.group_id_map).toEqual([ + expect(gm.stone_string_id_map).toEqual([ [1, 1, 2, 3, 4], [2, 2, 2, 3, 3], [5, 6, 2, 3, 7], @@ -51,7 +51,7 @@ test("Group ID Map", () => { test("Territory", () => { const gm = makeGoMathWithFeatureBoard(); - const territory = gm.groups.filter((g) => g.is_territory).map((g) => g.id); + 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/autoscore.ts b/src/autoscore.ts index 9f09d05e..704e0103 100644 --- a/src/autoscore.ts +++ b/src/autoscore.ts @@ -152,26 +152,26 @@ export function autoscore( if (group.is_territory && color) { let total_ownership = 0; - group.foreachStone((point) => { + group.map((point) => { const x = point.x; const y = point.y; total_ownership += average_ownership[y][x]; }); - const avg = total_ownership / group.points.length; + const avg = total_ownership / group.intersections.length; if ( (color === JGOFNumericPlayerColor.BLACK && avg > BLACK_THRESHOLD) || (color === JGOFNumericPlayerColor.WHITE && avg < WHITE_THRESHOLD) ) { - group.foreachStone((point) => { + 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.foreachStone((point) => { + neighbor.map((point) => { const x = point.x; const y = point.y; is_settled[y][x] = 1; @@ -282,7 +282,7 @@ export function autoscore( groups.foreachGroup((group) => { // if this group is a settled group, ignore it, we don't care about those - const pt = group.points[0]; + const pt = group.intersections[0]; if (is_settled[pt.y][pt.x]) { return; } @@ -314,7 +314,7 @@ export function autoscore( } } - group.foreachStone((point) => { + group.map((point) => { const x = point.x; const y = point.y; contained[board[y][x]]++; @@ -327,7 +327,7 @@ export function autoscore( black_plays_first_ownership[y][x] + white_plays_first_ownership[y][x]; }); - const average_color_estimate = total_ownership_estimate / group.points.length; + const average_color_estimate = total_ownership_estimate / group.intersections.length; let color_judgement: JGOFNumericPlayerColor; @@ -352,7 +352,7 @@ export function autoscore( } } - group.foreachStone((point) => { + group.map((point) => { const x = point.x; const y = point.y; if (board[y][x] && board[y][x] !== color_judgement) { @@ -414,7 +414,7 @@ export function autoscore( if ( group.color === JGOFNumericPlayerColor.EMPTY && !group.is_territory && - group.points.length > 3 + 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 @@ -423,13 +423,13 @@ export function autoscore( // so the players have to resume to finish the game. let total_ownership = 0; - group.foreachStone((point) => { + group.map((point) => { const x = point.x; const y = point.y; total_ownership += average_ownership[y][x]; }); - const avg = total_ownership / group.points.length; + const avg = total_ownership / group.intersections.length; if (avg <= WHITE_SEAL_THRESHOLD || avg >= BLACK_SEAL_THRESHOLD) { const color = @@ -437,7 +437,7 @@ export function autoscore( ? JGOFNumericPlayerColor.WHITE : JGOFNumericPlayerColor.BLACK; - group.foreachStone((point) => { + group.map((point) => { const x = point.x; const y = point.y; @@ -764,8 +764,8 @@ function debug_boolean_board(title: string, board: (boolean | number)[][], mark function debug_groups(title: string, groups: StoneStringBuilder) { const group_map: string[][] = makeMatrix( - groups.group_id_map[0].length, - groups.group_id_map.length, + groups.stone_string_id_map[0].length, + groups.stone_string_id_map.length, "", ); const symbols = "1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; @@ -799,7 +799,7 @@ function debug_groups(title: string, groups: StoneStringBuilder) { const symbol = symbols[group_idx % symbols.length]; - group.foreachStone((point) => { + group.map((point) => { group_map[point.y][point.x] = group_color(symbol); }); group_idx++; From 093cd76e9c633680989f18238ff71f27be55968c Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Tue, 11 Jun 2024 08:15:58 -0600 Subject: [PATCH 23/68] Rename test file --- .../{GoMath_GoStoneGroup.test.ts => StoneStringBuilder.test.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/__tests__/{GoMath_GoStoneGroup.test.ts => StoneStringBuilder.test.ts} (100%) diff --git a/src/__tests__/GoMath_GoStoneGroup.test.ts b/src/__tests__/StoneStringBuilder.test.ts similarity index 100% rename from src/__tests__/GoMath_GoStoneGroup.test.ts rename to src/__tests__/StoneStringBuilder.test.ts From 68cf82b9e5e37dca665f3ae64d6224ea98230a3b Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Tue, 11 Jun 2024 08:45:43 -0600 Subject: [PATCH 24/68] Refactor: cleanup and comment --- src/Board.ts | 3 +- src/GoEngine.ts | 241 ++++++++++++++++++++++-------------------------- 2 files changed, 112 insertions(+), 132 deletions(-) diff --git a/src/Board.ts b/src/Board.ts index 523743c2..5f96b6ca 100644 --- a/src/Board.ts +++ b/src/Board.ts @@ -187,7 +187,8 @@ export class Board extends EventEmitter { } } - clearRemoved(): void { + /** 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) { diff --git a/src/GoEngine.ts b/src/GoEngine.ts index 5a6eef1e..f9d90c12 100644 --- a/src/GoEngine.ts +++ b/src/GoEngine.ts @@ -296,7 +296,8 @@ export type PuzzlePlacementSetting = | { mode: "setup"; color: JGOFNumericPlayerColor } | { mode: "place"; color: 0 }; -let __currentMarker = 0; +/* When flood filling we use this to keep track of locations we've visited */ +let __current_flood_fill_value = 0; export type PlayerColor = "black" | "white"; @@ -476,7 +477,7 @@ export class GoEngine extends Board { private dontStoreBoardHistory: boolean; public free_handicap_placement: boolean = false; private loading_sgf: boolean = false; - private marks: Array>; + private _flood_fill_scratch_pad: number[][]; private move_before_jump?: MoveTree; public needs_sealing?: Array; //private mv:Move; @@ -529,7 +530,6 @@ export class GoEngine extends Board { this.goban_callback = goban_callback; this.goban_callback.engine = this; } - this.marks = []; this.white_prisoners = 0; this.black_prisoners = 0; this.board_is_repeating = false; @@ -541,7 +541,7 @@ export class GoEngine extends Board { this.rengo_casual_mode = config.rengo_casual_mode || false; - this.marks = makeMatrix(this.width, this.height, 0); + this._flood_fill_scratch_pad = makeMatrix(this.width, this.height, 0); try { this.config.original_disable_analysis = this.config.disable_analysis; @@ -1058,138 +1058,59 @@ export class GoEngine extends Board { public prettyCoords(x: number, y: number): string { return GoMath.prettyCoords(x, y, this.height); } - private incrementCurrentMarker(): void { - ++__currentMarker; - } - private markGroup(group: RawStoneString): 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 | RawStoneString, - fn_of_neighbor_pt: (x: number, y: number) => void, + pt_or_raw_stone_string: Intersection | RawStoneString, + callback: (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); + 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) { - done_array[group[i].x + group[i].y * this.width] = true; + 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) { - this.foreachNeighbor_checkAndDo(pt.x - 1, pt.y, done_array, fn_of_neighbor_pt); + callback_one_time(pt.x - 1, pt.y); } if (pt.x + 1 !== this.width) { - this.foreachNeighbor_checkAndDo(pt.x + 1, pt.y, done_array, fn_of_neighbor_pt); + callback_one_time(pt.x + 1, pt.y); } if (pt.y - 1 >= 0) { - this.foreachNeighbor_checkAndDo(pt.x, pt.y - 1, done_array, fn_of_neighbor_pt); + callback_one_time(pt.x, pt.y - 1); } if (pt.y + 1 !== this.height) { - this.foreachNeighbor_checkAndDo(pt.x, pt.y + 1, done_array, fn_of_neighbor_pt); + callback_one_time(pt.x, pt.y + 1); } } } else { - const pt = pt_or_group; + const pt = pt_or_raw_stone_string; if (pt.x - 1 >= 0) { - fn_of_neighbor_pt(pt.x - 1, pt.y); + callback(pt.x - 1, pt.y); } if (pt.x + 1 !== this.width) { - fn_of_neighbor_pt(pt.x + 1, pt.y); + callback(pt.x + 1, pt.y); } if (pt.y - 1 >= 0) { - fn_of_neighbor_pt(pt.x, pt.y - 1); + callback(pt.x, pt.y - 1); } if (pt.y + 1 !== this.height) { - fn_of_neighbor_pt(pt.x, pt.y + 1); + callback(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): RawStoneString { - 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: RawStoneString): 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 countLiberties(group: RawStoneString): 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: RawStoneString): number { for (let i = 0; i < group.length; ++i) { const x = group[i].x; @@ -1208,27 +1129,6 @@ export class GoEngine extends Board { 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 @@ -1333,8 +1233,8 @@ export class GoEngine extends Board { 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); + 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) { @@ -2685,4 +2585,83 @@ export class GoEngine extends Board { } return ret; } + + /** + * 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. + */ + private 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 */ + private 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 (this._flood_fill_scratch_pad[y][x] === __current_flood_fill_value) { + continue; + } + this._flood_fill_scratch_pad[y][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) { + this._flood_fill_scratch_pad[group[i].y][group[i].x] = __current_flood_fill_value; + } + } + private countLiberties(raw_stone_string: RawStoneString): 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 < raw_stone_string.length; ++i) { + this.foreachNeighbor(raw_stone_string[i], counter); + } + return ct; + } } From ffd2c6d80c57ea8be4efb360159f5dc74cad1340 Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Tue, 11 Jun 2024 08:56:11 -0600 Subject: [PATCH 25/68] Refactor: Moving generic board code from GoEngine to Board --- src/Board.ts | 139 +++++++++++++++++++++++++++++++++++++++++++++++- src/GoEngine.ts | 139 +----------------------------------------------- 2 files changed, 139 insertions(+), 139 deletions(-) diff --git a/src/Board.ts b/src/Board.ts index 5f96b6ca..f3013ef5 100644 --- a/src/Board.ts +++ b/src/Board.ts @@ -16,7 +16,7 @@ import { Events } from "./GobanCore"; import { EventEmitter } from "eventemitter3"; -import { JGOFNumericPlayerColor } from "./JGOF"; +import { JGOFIntersection, JGOFNumericPlayerColor } from "./JGOF"; import { makeMatrix } from "./GoMath"; import * as goscorer from "./goscorer/goscorer"; import { StoneStringBuilder } from "./StoneStringBuilder"; @@ -32,6 +32,10 @@ export interface BoardConfig { removal?: boolean[][]; } +/* When flood filling we use this to keep track of locations we've visited */ +let __current_flood_fill_value = 0; +const __flood_fill_scratch_pad: number[] = []; + export class Board extends EventEmitter { public readonly height: number = 19; //public readonly rules:GoEngineRules = 'japanese'; @@ -202,4 +206,137 @@ export class Board extends EventEmitter { 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); + } + } + } } diff --git a/src/GoEngine.ts b/src/GoEngine.ts index f9d90c12..161b18d3 100644 --- a/src/GoEngine.ts +++ b/src/GoEngine.ts @@ -17,7 +17,7 @@ import { Board, BoardConfig } from "./Board"; import { GobanMoveError } from "./GobanError"; import { MoveTree, MoveTreeJson } from "./MoveTree"; -import { Move, Intersection, encodeMove, makeMatrix } from "./GoMath"; +import { Move, encodeMove } from "./GoMath"; import * as GoMath from "./GoMath"; import { RawStoneString } from "./StoneString"; import { ScoreEstimator } from "./ScoreEstimator"; @@ -296,9 +296,6 @@ export type PuzzlePlacementSetting = | { mode: "setup"; color: JGOFNumericPlayerColor } | { mode: "place"; color: 0 }; -/* When flood filling we use this to keep track of locations we've visited */ -let __current_flood_fill_value = 0; - export type PlayerColor = "black" | "white"; export class GoEngine extends Board { @@ -477,7 +474,6 @@ export class GoEngine extends Board { private dontStoreBoardHistory: boolean; public free_handicap_placement: boolean = false; private loading_sgf: boolean = false; - private _flood_fill_scratch_pad: number[][]; private move_before_jump?: MoveTree; public needs_sealing?: Array; //private mv:Move; @@ -541,8 +537,6 @@ export class GoEngine extends Board { this.rengo_casual_mode = config.rengo_casual_mode || false; - this._flood_fill_scratch_pad = makeMatrix(this.width, this.height, 0); - try { this.config.original_disable_analysis = this.config.disable_analysis; if ( @@ -1059,58 +1053,6 @@ export class GoEngine extends Board { return GoMath.prettyCoords(x, y, this.height); } - public foreachNeighbor( - pt_or_raw_stone_string: Intersection | 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); - } - } - } private captureGroup(group: RawStoneString): number { for (let i = 0; i < group.length; ++i) { const x = group[i].x; @@ -2585,83 +2527,4 @@ export class GoEngine extends Board { } return ret; } - - /** - * 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. - */ - private 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 */ - private 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 (this._flood_fill_scratch_pad[y][x] === __current_flood_fill_value) { - continue; - } - this._flood_fill_scratch_pad[y][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) { - this._flood_fill_scratch_pad[group[i].y][group[i].x] = __current_flood_fill_value; - } - } - private countLiberties(raw_stone_string: RawStoneString): 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 < raw_stone_string.length; ++i) { - this.foreachNeighbor(raw_stone_string[i], counter); - } - return ct; - } } From 5a945a44f0cbe0976de3f5ce058c857ab590f898 Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Tue, 11 Jun 2024 19:16:11 -0600 Subject: [PATCH 26/68] Comments --- src/Board.ts | 4 ++-- src/GoEngine.ts | 12 ++++++++++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/Board.ts b/src/Board.ts index f3013ef5..08297140 100644 --- a/src/Board.ts +++ b/src/Board.ts @@ -32,9 +32,9 @@ export interface BoardConfig { removal?: boolean[][]; } -/* When flood filling we use this to keep track of locations we've visited */ +/* 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[] = []; +const __flood_fill_scratch_pad: number[] = Array(25 * 25).fill(0); export class Board extends EventEmitter { public readonly height: number = 19; diff --git a/src/GoEngine.ts b/src/GoEngine.ts index 161b18d3..86375949 100644 --- a/src/GoEngine.ts +++ b/src/GoEngine.ts @@ -491,7 +491,7 @@ export class GoEngine extends Board { ) { super( GoEngine.fillDefaults( - GoEngine.normalizeConfig( + GoEngine.migrateConfig( ((config: GoEngineConfig): GoEngineConfig => { /* We had a bug where we were filling in some initial state * data incorrectly when we were dealing with sgfs, so this @@ -1569,7 +1569,11 @@ export class GoEngine extends Board { return 0; } - private static normalizeConfig(config: GoEngineConfig): GoEngineConfig { + /** + * This function migrates old config's to whatever our current standard is + * for configs. + */ + private static migrateConfig(config: GoEngineConfig): GoEngineConfig { if (config.ladder !== config.ladder_id) { config.ladder_id = config.ladder; } @@ -1617,6 +1621,10 @@ export class GoEngine extends Board { return config; } + /** + * This function fills in default values for any missing fields in the + * config. + */ public static fillDefaults(game_obj: GoEngineConfig): GoEngineConfig { if (!("phase" in game_obj)) { game_obj.phase = "play"; From a7b5a62d0472eb559a7c0e1b41c53e9e792917db Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Wed, 12 Jun 2024 06:25:03 -0600 Subject: [PATCH 27/68] Refactor: Merged GoEngineState into BoardState Also removed unused udata system we had for GoEngineState --- src/{Board.ts => BoardState.ts} | 31 +++++++++++- src/GoEngine.ts | 60 +++++------------------- src/GobanCore.ts | 26 +++++----- src/MoveTree.ts | 7 +-- src/ScoreEstimator.ts | 4 +- src/StoneStringBuilder.ts | 6 +-- src/__tests__/GoMath.test.ts | 4 +- src/__tests__/StoneStringBuilder.test.ts | 4 +- src/autoscore.ts | 14 +++--- 9 files changed, 77 insertions(+), 79 deletions(-) rename src/{Board.ts => BoardState.ts} (91%) diff --git a/src/Board.ts b/src/BoardState.ts similarity index 91% rename from src/Board.ts rename to src/BoardState.ts index 08297140..30aa4259 100644 --- a/src/Board.ts +++ b/src/BoardState.ts @@ -30,13 +30,19 @@ export interface BoardConfig { height?: number; board?: JGOFNumericPlayerColor[][]; removal?: boolean[][]; + player?: JGOFNumericPlayerColor; + board_is_repeating?: boolean; + white_prisoners?: number; + black_prisoners?: number; + isobranch_hash?: string; + //udata_state?: any; } /* 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 Board extends EventEmitter { +export class BoardState extends EventEmitter implements BoardConfig { public readonly height: number = 19; //public readonly rules:GoEngineRules = 'japanese'; public readonly width: number = 19; @@ -44,6 +50,18 @@ export class Board extends EventEmitter { public removal: boolean[][]; protected goban_callback?: GobanCore; + public player: JGOFNumericPlayerColor; + public board_is_repeating: boolean; + public white_prisoners: number; + public black_prisoners: number; + + /** + * 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 not automatically + * populated, MoveTree uses it to store hash information as needed. + * */ + public isobranch_hash?: string; + /** * Constructs a new board with the given configuration. If height/width * are not provided, they will be inferred from the board array, or will @@ -75,6 +93,17 @@ export class Board extends EventEmitter { 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; + this.isobranch_hash = config.isobranch_hash; + } + + /** Clone the entire BoardState */ + public cloneBoardState(): BoardState { + return new BoardState(this, this.goban_callback); } /** Returns a clone of .board */ diff --git a/src/GoEngine.ts b/src/GoEngine.ts index 86375949..3b0630ed 100644 --- a/src/GoEngine.ts +++ b/src/GoEngine.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { Board, BoardConfig } from "./Board"; +import { BoardState, BoardConfig } from "./BoardState"; import { GobanMoveError } from "./GobanError"; import { MoveTree, MoveTreeJson } from "./MoveTree"; import { Move, encodeMove } from "./GoMath"; @@ -65,20 +65,6 @@ 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 { id: number; username: string; @@ -298,12 +284,11 @@ export type PuzzlePlacementSetting = export type PlayerColor = "black" | "white"; -export class GoEngine extends Board { +export class GoEngine extends BoardState { //public readonly players.black.id:number; //public readonly players.white.id:number; public throw_all_errors?: boolean; //public cur_review_move?: MoveTree; - public getState_callback?: () => any; public handicap_rank_difference?: number; public handicap: number = NaN; public initial_state: GoEngineInitialState = { black: "", white: "" }; @@ -334,7 +319,6 @@ export class GoEngine extends Board { public readonly config: GoEngineConfig; public readonly disable_analysis: boolean = false; //public readonly rules:GoEngineRules = 'japanese'; - public setState_callback?: (state: any) => void; public time_control: JGOFTimeControl = { system: "none", speed: "correspondence", @@ -468,9 +452,9 @@ export class GoEngine extends Board { 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; + public black_prisoners: number = 0; + public white_prisoners: number = 0; + public board_is_repeating: boolean; private dontStoreBoardHistory: boolean; public free_handicap_placement: boolean = false; private loading_sgf: boolean = false; @@ -709,35 +693,17 @@ export class GoEngine extends Board { ): 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), - }; - - 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; - } - - 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 } = {}; @@ -775,7 +741,7 @@ export class GoEngine extends Board { } return true; } - private boardStatesAreTheSame(state1: GoEngineState, state2: GoEngineState): boolean { + private boardsAreEqual(state1: BoardState, state2: BoardState): 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]) { @@ -1207,7 +1173,7 @@ export class GoEngine extends Board { const current_state = this.getState(); if ( !this.cur_move.edited && - this.boardStatesAreTheSame(current_state, this.cur_move.index(-1).state) + this.boardsAreEqual(current_state, this.cur_move.index(-1).state) ) { throw new GobanMoveError( this.game_id || this.review_id || 0, @@ -1282,7 +1248,7 @@ export class GoEngine extends Board { ) { if (t) { if (!check_situational || t.player === current_player_to_move) { - if (this.boardStatesAreTheSame(t.state, current_state)) { + if (this.boardsAreEqual(t.state, current_state)) { return true; } } diff --git a/src/GobanCore.ts b/src/GobanCore.ts index f2aed366..40b026e4 100644 --- a/src/GobanCore.ts +++ b/src/GobanCore.ts @@ -2147,12 +2147,6 @@ export abstract class GobanCore extends EventEmitter { 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; @@ -2827,7 +2821,20 @@ export abstract class GobanCore extends EventEmitter { }); } } - protected setState(state: any): void { + /** This is a callback that gets called by GoEngine.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; + } + */ + + /** This is a callback that gets called by GoEngine.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) { @@ -2838,11 +2845,6 @@ export abstract class GobanCore extends EventEmitter { 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 }); } diff --git a/src/MoveTree.ts b/src/MoveTree.ts index c71b612d..9450088c 100644 --- a/src/MoveTree.ts +++ b/src/MoveTree.ts @@ -15,7 +15,8 @@ */ import * as GoMath from "./GoMath"; -import { GoEngine, GoEngineState } from "./GoEngine"; +import { GoEngine } from "./GoEngine"; +import { BoardState } from "./BoardState"; import { encodeMove } from "./GoMath"; import { AdHocPackedMove } from "./AdHocFormat"; import { JGOFNumericPlayerColor, JGOFPlayerSummary } from "./JGOF"; @@ -103,7 +104,7 @@ export class MoveTree { public x: number; public y: number; public edited: boolean; - public state: GoEngineState; + public state: BoardState; public pen_marks: MoveTreePenMarks = []; public player_update: JGOFPlayerSummary | undefined; public played_by: number | undefined; @@ -134,7 +135,7 @@ export class MoveTree { player: JGOFNumericPlayerColor, move_number: number, parent: MoveTree | null, - state: GoEngineState, + state: BoardState, ) { this.id = ++__move_tree_id; this.x = x; diff --git a/src/ScoreEstimator.ts b/src/ScoreEstimator.ts index fc308cf9..d01be510 100644 --- a/src/ScoreEstimator.ts +++ b/src/ScoreEstimator.ts @@ -24,7 +24,7 @@ import { JGOFMove, JGOFNumericPlayerColor, JGOFSealingIntersection } from "./JGO import { _ } from "./translate"; import { estimateScoreWasm } from "./local_estimators/wasm_estimator"; import * as goscorer from "./goscorer/goscorer"; -import { Board } from "./Board"; +import { BoardState } from "./BoardState"; export { init_score_estimator, estimateScoreWasm } from "./local_estimators/wasm_estimator"; export { estimateScoreVoronoi } from "./local_estimators/voronoi"; @@ -100,7 +100,7 @@ export function set_local_scorer(scorer: LocalEstimator) { local_scorer = scorer; } -export class ScoreEstimator extends Board { +export class ScoreEstimator extends BoardState { white: PlayerScore = { total: 0, stones: 0, diff --git a/src/StoneStringBuilder.ts b/src/StoneStringBuilder.ts index 28011568..b6bf2e7a 100644 --- a/src/StoneStringBuilder.ts +++ b/src/StoneStringBuilder.ts @@ -16,15 +16,15 @@ import * as GoMath from "./GoMath"; import { StoneString } from "./StoneString"; -import { Board } from "./Board"; +import { BoardState } from "./BoardState"; import { JGOFNumericPlayerColor } from "./JGOF"; export class StoneStringBuilder { - private state: Board; + private state: BoardState; public readonly stone_string_id_map: number[][]; public readonly stone_strings: StoneString[]; - constructor(state: Board, original_board?: JGOFNumericPlayerColor[][]) { + 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 = GoMath.makeMatrix(state.width, state.height, 0); diff --git a/src/__tests__/GoMath.test.ts b/src/__tests__/GoMath.test.ts index 5bb7edae..49e9fc97 100644 --- a/src/__tests__/GoMath.test.ts +++ b/src/__tests__/GoMath.test.ts @@ -3,7 +3,7 @@ import { StoneStringBuilder } from "../StoneStringBuilder"; import { JGOFNumericPlayerColor } from "../JGOF"; import * as GoMath from "../GoMath"; -import { Board } from "../Board"; +import { BoardState } from "../BoardState"; describe("GoStoneGroups constructor", () => { test("basic board state", () => { @@ -19,7 +19,7 @@ describe("GoStoneGroups constructor", () => { ]; const stone_string_builder = new StoneStringBuilder( - new Board({ + new BoardState({ board: THREExTHREE_board, removal: THREExTHREE_removal, }), diff --git a/src/__tests__/StoneStringBuilder.test.ts b/src/__tests__/StoneStringBuilder.test.ts index 81597603..02e06dc6 100644 --- a/src/__tests__/StoneStringBuilder.test.ts +++ b/src/__tests__/StoneStringBuilder.test.ts @@ -1,6 +1,6 @@ import * as GoMath from "../GoMath"; import { StoneStringBuilder } from "../StoneStringBuilder"; -import { Board } from "../Board"; +import { BoardState } from "../BoardState"; // Here is a board displaying many of the features GoStoneGroup cares about. @@ -29,7 +29,7 @@ const REMOVAL = GoMath.makeMatrix(5, 5, false); function makeGoMathWithFeatureBoard() { return new StoneStringBuilder( - new Board({ + new BoardState({ board: FEATURE_BOARD, removal: REMOVAL, }), diff --git a/src/autoscore.ts b/src/autoscore.ts index 704e0103..5fe4660d 100644 --- a/src/autoscore.ts +++ b/src/autoscore.ts @@ -25,7 +25,7 @@ import { StoneStringBuilder } from "./StoneStringBuilder"; import { JGOFNumericPlayerColor, JGOFSealingIntersection, JGOFMove } from "./JGOF"; import { char2num, makeMatrix, num2char, pretty_coor_num2ch } from "./GoMath"; import { GoEngine, GoEngineInitialState, GoEngineRules } from "./GoEngine"; -import { Board } from "./Board"; +import { BoardState } from "./BoardState"; interface AutoscoreResults { result: JGOFNumericPlayerColor[][]; @@ -83,7 +83,7 @@ export function autoscore( debug_ownership_output("Average estimates", average_ownership); const groups = new StoneStringBuilder( - new Board({ + new BoardState({ board, removal: makeMatrix(width, height, false), }), @@ -138,7 +138,7 @@ export function autoscore( stage("Settling agreed upon territory"); const groups = new StoneStringBuilder( - new Board({ + new BoardState({ board, removal, }), @@ -274,7 +274,7 @@ export function autoscore( * their neighboring stones */ const groups = new StoneStringBuilder( - new Board({ + new BoardState({ board: is_settled, removal: makeMatrix(width, height, false), }), @@ -400,7 +400,7 @@ export function autoscore( stage(`Sealing territory`); { let groups = new StoneStringBuilder( - new Board({ + new BoardState({ board, removal, }), @@ -465,7 +465,7 @@ export function autoscore( }); groups = new StoneStringBuilder( - new Board({ + new BoardState({ board, removal, }), @@ -556,7 +556,7 @@ export function autoscore( } const groups = new StoneStringBuilder( - new Board({ + new BoardState({ board, removal, }), From 0a0e8e926ed194abc147b3184b22de6a1b3c55c1 Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Wed, 12 Jun 2024 07:44:05 -0600 Subject: [PATCH 28/68] Refactor: Specialize isobranch state for move tree, move matrix comparison code out to util --- src/BoardState.ts | 14 +++++-------- src/GoEngine.ts | 38 ++-------------------------------- src/GobanCore.ts | 7 ++----- src/MoveTree.ts | 11 +++++++++- src/__tests__/GoEngine.test.ts | 8 +++---- src/util.ts | 29 +++++++++++++++++++++++++- 6 files changed, 51 insertions(+), 56 deletions(-) diff --git a/src/BoardState.ts b/src/BoardState.ts index 30aa4259..d6d653cc 100644 --- a/src/BoardState.ts +++ b/src/BoardState.ts @@ -22,7 +22,7 @@ import * as goscorer from "./goscorer/goscorer"; import { StoneStringBuilder } from "./StoneStringBuilder"; import { GobanCore } from "./GobanCore"; import { RawStoneString } from "./StoneString"; -import { cloneMatrix } from "./util"; +import { cloneMatrix, matricesAreEqual } from "./util"; import { callbacks } from "./callbacks"; export interface BoardConfig { @@ -55,13 +55,6 @@ export class BoardState extends EventEmitter implements BoardConfig { public white_prisoners: number; public black_prisoners: number; - /** - * 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 not automatically - * populated, MoveTree uses it to store hash information as needed. - * */ - public isobranch_hash?: string; - /** * Constructs a new board with the given configuration. If height/width * are not provided, they will be inferred from the board array, or will @@ -98,7 +91,6 @@ export class BoardState extends EventEmitter implements BoardConfig { this.board_is_repeating = config.board_is_repeating ?? false; this.white_prisoners = config.white_prisoners ?? 0; this.black_prisoners = config.black_prisoners ?? 0; - this.isobranch_hash = config.isobranch_hash; } /** Clone the entire BoardState */ @@ -368,4 +360,8 @@ export class BoardState extends EventEmitter implements BoardConfig { } } } + + public boardEquals(other: BoardState): boolean { + return matricesAreEqual(this.board, other.board); + } } diff --git a/src/GoEngine.ts b/src/GoEngine.ts index 3b0630ed..b213991b 100644 --- a/src/GoEngine.ts +++ b/src/GoEngine.ts @@ -724,34 +724,6 @@ export class GoEngine extends BoardState { 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 boardsAreEqual(state1: BoardState, state2: BoardState): 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); @@ -1170,11 +1142,7 @@ export class GoEngine extends BoardState { } if (checkForKo && !this.allow_ko) { - const current_state = this.getState(); - if ( - !this.cur_move.edited && - this.boardsAreEqual(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, @@ -1235,8 +1203,6 @@ export class GoEngine extends BoardState { } public isBoardRepeating(superko_rule: GoEngineSuperKoAlgorithm): 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"; @@ -1248,7 +1214,7 @@ export class GoEngine extends BoardState { ) { if (t) { if (!check_situational || t.player === current_player_to_move) { - if (this.boardsAreEqual(t.state, current_state)) { + if (this.boardEquals(t.state)) { return true; } } diff --git a/src/GobanCore.ts b/src/GobanCore.ts index 40b026e4..2733bf0e 100644 --- a/src/GobanCore.ts +++ b/src/GobanCore.ts @@ -33,7 +33,7 @@ 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 "./util"; +import { deepEqual, dup, computeAverageMoveTime, niceInterval, matricesAreEqual } from "./util"; import { _, interpolate } from "./translate"; import { JGOFClock, @@ -2179,9 +2179,7 @@ export abstract class GobanCore extends EventEmitter { } } - if ( - !(old_engine && old_engine.boardMatricesAreTheSame(old_engine.board, this.engine.board)) - ) { + if (!(old_engine && matricesAreEqual(old_engine.board, this.engine.board))) { this.redraw(true); } @@ -3867,7 +3865,6 @@ export abstract class GobanCore extends EventEmitter { m: diff.moves, k: marks, }; - console.log("mARKS:", marks); const tmp = dup(msg); if (this.last_review_message.f === msg.f && this.last_review_message.m === msg.m) { diff --git a/src/MoveTree.ts b/src/MoveTree.ts index 9450088c..a0f831b2 100644 --- a/src/MoveTree.ts +++ b/src/MoveTree.ts @@ -79,6 +79,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; @@ -104,7 +113,7 @@ export class MoveTree { public x: number; public y: number; public edited: boolean; - public state: BoardState; + public state: BoardStateWithIsobranchHash; public pen_marks: MoveTreePenMarks = []; public player_update: JGOFPlayerSummary | undefined; public played_by: number | undefined; diff --git a/src/__tests__/GoEngine.test.ts b/src/__tests__/GoEngine.test.ts index 6fc9a4e8..0c8c35fb 100644 --- a/src/__tests__/GoEngine.test.ts +++ b/src/__tests__/GoEngine.test.ts @@ -5,9 +5,9 @@ import { movesFromBoardState } from "../test_utils"; import { GobanMoveError } from "../GobanError"; import { JGOFIntersection } from "../JGOF"; import { makeMatrix } from "../GoMath"; +import { matricesAreEqual } from "../util"; test("boardMatricesAreTheSame", () => { - const engine = new GoEngine({}); const a = [ [1, 2], [3, 4], @@ -24,9 +24,9 @@ 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", () => { diff --git a/src/util.ts b/src/util.ts index a740ff79..4c15e9fc 100644 --- a/src/util.ts +++ b/src/util.ts @@ -24,7 +24,34 @@ export function getRandomInt(min: number, max: number) { /** Returns a cloned copy of the provided matrix */ export function cloneMatrix(matrix: T[][]): T[][] { - return matrix.map((row) => row.slice()); + 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; } /** Takes a number of seconds and returns a string like "1d 3h 2m 52s" */ From c510809b3d5b9a889b85a58d7ce01982d5e7de51 Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Thu, 13 Jun 2024 06:28:12 -0600 Subject: [PATCH 29/68] Refactor: rename GobanCore autoScore to performStoneRemovalAutoScoring for clarity --- src/BoardState.ts | 1 + src/GobanCore.ts | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/BoardState.ts b/src/BoardState.ts index d6d653cc..13cabfb3 100644 --- a/src/BoardState.ts +++ b/src/BoardState.ts @@ -361,6 +361,7 @@ export class BoardState extends EventEmitter implements BoardConfig { } } + /** 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); } diff --git a/src/GobanCore.ts b/src/GobanCore.ts index 2733bf0e..2c02f7e3 100644 --- a/src/GobanCore.ts +++ b/src/GobanCore.ts @@ -1059,7 +1059,7 @@ export abstract class GobanCore extends EventEmitter { this.engine.phase = new_phase; if (this.engine.phase === "stone removal") { - this.autoScore(); + this.performStoneRemovalAutoScoring(); } else { delete this.auto_scoring_done; } @@ -3140,7 +3140,7 @@ export abstract class GobanCore extends EventEmitter { this.emit("score_estimate", this.score_estimate); } } - public autoScore(): void { + public performStoneRemovalAutoScoring(): void { try { if ( !(window as any)["user"] || From 2c6fbb0b9f104609bafefe5de92094a63de75d82 Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Thu, 13 Jun 2024 08:55:45 -0600 Subject: [PATCH 30/68] Refactor: nomenclature normalization --- src/GoEngine.ts | 6 +- src/GobanCanvas.ts | 104 +++++++++--------- src/GobanCore.ts | 45 ++++---- src/GobanSVG.ts | 104 +++++++++--------- src/ScoreEstimator.ts | 37 +++---- src/__tests__/ScoreEstimator.test.ts | 62 +++++------ src/engine.ts | 1 + src/goban.ts | 2 + .../__tests__/voronoi_estimator.test.ts} | 6 +- src/ownership_estimators/index.ts | 19 ++++ src/ownership_estimators/remote_estimator.ts | 28 +++++ .../voronoi_estimator.ts} | 2 +- .../wasm_estimator.ts | 22 ++-- 13 files changed, 240 insertions(+), 198 deletions(-) rename src/{local_estimators/__tests__/voronoi.test.ts => ownership_estimators/__tests__/voronoi_estimator.test.ts} (87%) create mode 100644 src/ownership_estimators/index.ts create mode 100644 src/ownership_estimators/remote_estimator.ts rename src/{local_estimators/voronoi.ts => ownership_estimators/voronoi_estimator.ts} (97%) rename src/{local_estimators => ownership_estimators}/wasm_estimator.ts (91%) diff --git a/src/GoEngine.ts b/src/GoEngine.ts index b213991b..bf4ae356 100644 --- a/src/GoEngine.ts +++ b/src/GoEngine.ts @@ -2391,15 +2391,15 @@ export class GoEngine extends BoardState { 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(); } diff --git a/src/GobanCanvas.ts b/src/GobanCanvas.ts index f148d276..99d45966 100644 --- a/src/GobanCanvas.ts +++ b/src/GobanCanvas.ts @@ -371,8 +371,8 @@ 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, @@ -1552,9 +1552,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] && @@ -1568,11 +1568,11 @@ export class GobanCanvas extends GobanCore implements GobanCanvasInterface { 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]; + color = this.score_estimator.board[j][i]; translucent = true; } else if ( this.engine && @@ -1742,9 +1742,9 @@ export class GobanCanvas extends GobanCore implements GobanCanvasInterface { this.engine.board[j][i] && this.engine.removal[j][i]) || (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]) || pos.stone_removed ) { draw_red_x = true; @@ -1787,10 +1787,10 @@ export class GobanCanvas extends GobanCore implements GobanCanvasInterface { if ( (pos.score && (this.engine.phase !== "finished" || this.mode === "play")) || (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 && @@ -1806,15 +1806,15 @@ export class GobanCanvas extends GobanCore implements GobanCanvasInterface { 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"; } @@ -2204,7 +2204,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") @@ -2212,7 +2212,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(); @@ -2321,9 +2321,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] && @@ -2335,11 +2335,11 @@ export class GobanCanvas extends GobanCore implements GobanCanvasInterface { 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]; + color = this.score_estimator.board[j][i]; translucent = true; } else if ( this.engine && @@ -2391,9 +2391,9 @@ export class GobanCanvas extends GobanCore implements GobanCanvasInterface { this.engine.board[j][i] && this.engine.removal[j][i]) || (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.mode === "analyze" && pos.stone_removed) pos.stone_removed ) { @@ -2454,7 +2454,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; } @@ -2472,10 +2472,10 @@ export class GobanCanvas extends GobanCore implements GobanCanvasInterface { if ( (pos.score && (this.engine.phase !== "finished" || this.mode === "play")) || (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 && @@ -2489,15 +2489,15 @@ export class GobanCanvas extends GobanCore implements GobanCanvasInterface { 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"; } @@ -2528,10 +2528,10 @@ export class GobanCanvas extends GobanCore implements GobanCanvasInterface { 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 + ","; } @@ -2640,7 +2640,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") @@ -2648,7 +2648,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) + ","; diff --git a/src/GobanCore.ts b/src/GobanCore.ts index 2c02f7e3..a1f84189 100644 --- a/src/GobanCore.ts +++ b/src/GobanCore.ts @@ -32,7 +32,8 @@ 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 { init_wasm_ownership_estimator } from "./ownership_estimators"; +import { ScoreEstimator } from "./ScoreEstimator"; import { deepEqual, dup, computeAverageMoveTime, niceInterval, matricesAreEqual } from "./util"; import { _, interpolate } from "./translate"; import { @@ -415,19 +416,19 @@ export abstract class GobanCore extends EventEmitter { this.emit("analyze_subtool", this.analyze_subtool); } - private _score_estimate: ScoreEstimator | null = null; - public get score_estimate(): ScoreEstimator | null { - return this._score_estimate; + private _score_estimator: ScoreEstimator | null = null; + public get score_estimator(): ScoreEstimator | null { + return this._score_estimator; } - public set score_estimate(score_estimate: ScoreEstimator | null) { - if (this._score_estimate === score_estimate) { + public set score_estimator(score_estimate: ScoreEstimator | null) { + if (this._score_estimator === score_estimate) { return; } - this._score_estimate = score_estimate; - this.emit("score_estimate", this.score_estimate); - this._score_estimate?.when_ready + this._score_estimator = score_estimate; + this.emit("score_estimate", this.score_estimator); + this._score_estimator?.when_ready .then(() => { - this.emit("score_estimate", this.score_estimate); + this.emit("score_estimate", this.score_estimator); }) .catch(() => { return; @@ -464,7 +465,7 @@ export abstract class GobanCore extends EventEmitter { 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 stone_removal_auto_scoring_done?: boolean = false; protected bounded_height: number; protected bounded_width: number; protected bounds: GobanBounds; @@ -1061,7 +1062,7 @@ export abstract class GobanCore extends EventEmitter { if (this.engine.phase === "stone removal") { this.performStoneRemovalAutoScoring(); } else { - delete this.auto_scoring_done; + delete this.stone_removal_auto_scoring_done; } this.updateTitleAndStonePlacement(); @@ -2206,7 +2207,7 @@ export abstract class GobanCore extends EventEmitter { !("auto_scoring_done" in this) && !("auto_scoring_done" in (this as any).engine) ) { - (this as any).autoScore(); + this.performStoneRemovalAutoScoring(); } this.emit("load", config); @@ -3129,15 +3130,15 @@ export abstract class GobanCore extends EventEmitter { } } public updateScoreEstimation(): void { - if (this.score_estimate) { - const est = this.score_estimate.estimated_hard_score - this.engine.komi; + 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_estimate); + this.emit("score_estimate", this.score_estimator); } } public performStoneRemovalAutoScoring(): void { @@ -3156,13 +3157,13 @@ export abstract class GobanCore extends EventEmitter { return; } - this.auto_scoring_done = true; + this.stone_removal_auto_scoring_done = true; this.showMessage("processing", undefined, -1); const do_score_estimation = () => { const se = new ScoreEstimator( - this, this.engine, + this, AUTOSCORE_TRIALS, AUTOSCORE_TOLERANCE, true /* prefer remote */, @@ -3219,7 +3220,7 @@ export abstract class GobanCore extends EventEmitter { }; setTimeout(() => { - init_score_estimator() + init_wasm_ownership_estimator() .then(do_score_estimation) .catch((err) => console.error(err)); }, 10); @@ -3911,12 +3912,12 @@ export abstract class GobanCore extends EventEmitter { this.showMessage("processing", undefined, -1); this.setMode("score estimation", true); this.clearMessage(); - const autoscore = false; - this.score_estimate = this.engine.estimateScore( + const should_autoscore = false; + this.score_estimator = this.engine.estimateScore( SCORE_ESTIMATION_TRIALS, SCORE_ESTIMATION_TOLERANCE, prefer_remote, - autoscore, + should_autoscore, ); this.enableStonePlacement(); this.redraw(true); diff --git a/src/GobanSVG.ts b/src/GobanSVG.ts index 277ca757..511fcce3 100644 --- a/src/GobanSVG.ts +++ b/src/GobanSVG.ts @@ -327,8 +327,8 @@ 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, @@ -1473,9 +1473,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] && @@ -1489,11 +1489,11 @@ export class GobanSVG extends GobanCore implements GobanSVGInterface { 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]; + color = this.score_estimator.board[j][i]; translucent = true; } else if ( this.engine && @@ -1657,9 +1657,9 @@ export class GobanSVG extends GobanCore implements GobanSVGInterface { this.engine.board[j][i] && this.engine.removal[j][i]) || (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.mode === "analyze" && pos.stone_removed) pos.stone_removed ) { @@ -1708,10 +1708,10 @@ export class GobanSVG extends GobanCore implements GobanSVGInterface { if ( (pos.score && (this.engine.phase !== "finished" || this.mode === "play")) || (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 && @@ -1726,15 +1726,15 @@ export class GobanSVG extends GobanCore implements GobanSVGInterface { 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"; } @@ -2142,7 +2142,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") @@ -2150,7 +2150,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; @@ -2264,9 +2264,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] && @@ -2278,11 +2278,11 @@ export class GobanSVG extends GobanCore implements GobanSVGInterface { 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]; + color = this.score_estimator.board[j][i]; translucent = true; } else if ( this.engine && @@ -2343,9 +2343,9 @@ export class GobanSVG extends GobanCore implements GobanSVGInterface { this.engine.board[j][i] && this.engine.removal[j][i]) || (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.mode === "analyze" && pos.stone_removed) pos.stone_removed ) { @@ -2406,7 +2406,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; } @@ -2424,10 +2424,10 @@ export class GobanSVG extends GobanCore implements GobanSVGInterface { if ( (pos.score && (this.engine.phase !== "finished" || this.mode === "play")) || (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 && @@ -2441,15 +2441,15 @@ export class GobanSVG extends GobanCore implements GobanSVGInterface { 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"; } @@ -2479,10 +2479,10 @@ export class GobanSVG extends GobanCore implements GobanSVGInterface { } 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 + ","; } @@ -2591,7 +2591,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") @@ -2599,7 +2599,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) + ","; diff --git a/src/ScoreEstimator.ts b/src/ScoreEstimator.ts index d01be510..8799a98a 100644 --- a/src/ScoreEstimator.ts +++ b/src/ScoreEstimator.ts @@ -22,13 +22,10 @@ import { GobanCore } from "./GobanCore"; import { GoEngine, PlayerScore, GoEngineRules } from "./GoEngine"; import { JGOFMove, JGOFNumericPlayerColor, JGOFSealingIntersection } from "./JGOF"; import { _ } from "./translate"; -import { estimateScoreWasm } from "./local_estimators/wasm_estimator"; +import { wasm_estimate_ownership, remote_estimate_ownership } from "./ownership_estimators"; import * as goscorer from "./goscorer/goscorer"; import { BoardState } from "./BoardState"; -export { init_score_estimator, estimateScoreWasm } from "./local_estimators/wasm_estimator"; -export { estimateScoreVoronoi } from "./local_estimators/voronoi"; - /* 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 * remote scoring enabled. @@ -71,14 +68,6 @@ export interface ScoreEstimateResponse { autoscored_needs_sealing?: JGOFSealingIntersection[]; } -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; -} - /** * The interface that local estimators should follow. * @@ -95,9 +84,9 @@ type LocalEstimator = ( trials: number, tolerance: number, ) => GoMath.NumberMatrix; -let local_scorer = estimateScoreWasm; -export function set_local_scorer(scorer: LocalEstimator) { - local_scorer = scorer; +let local_ownership_estimator = wasm_estimate_ownership; +export function set_local_ownership_estimator(estimator: LocalEstimator) { + local_ownership_estimator = estimator; } export class ScoreEstimator extends BoardState { @@ -138,9 +127,8 @@ export class ScoreEstimator extends BoardState { public autoscored_needs_sealing?: JGOFSealingIntersection[]; constructor( - /* REFACTOR TODO: Engine puts config first before callback, this should be the same */ - goban_callback: GobanCore | undefined, engine: GoEngine, + goban_callback: GobanCore | undefined, trials: number, tolerance: number, prefer_remote: boolean = false, @@ -170,7 +158,7 @@ export class ScoreEstimator extends BoardState { return this.estimateScoreLocal(trials, tolerance); } - if (remote_scorer) { + if (remote_estimate_ownership) { return this.estimateScoreRemote(autoscore); } else { return this.estimateScoreLocal(trials, tolerance); @@ -184,7 +172,7 @@ export class ScoreEstimator extends BoardState { : 0; return new Promise((resolve, reject) => { - if (!remote_scorer) { + if (!remote_estimate_ownership) { throw new Error("Remote scoring not setup"); } @@ -197,13 +185,13 @@ export class ScoreEstimator extends BoardState { 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) => { @@ -270,7 +258,12 @@ export class ScoreEstimator extends BoardState { } } - 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); diff --git a/src/__tests__/ScoreEstimator.test.ts b/src/__tests__/ScoreEstimator.test.ts index a001b559..bdc3a2ac 100644 --- a/src/__tests__/ScoreEstimator.test.ts +++ b/src/__tests__/ScoreEstimator.test.ts @@ -2,13 +2,11 @@ import { GoEngine } from "../GoEngine"; import { makeMatrix } from "../GoMath"; +import { ScoreEstimator, adjust_estimate, set_local_ownership_estimator } from "../ScoreEstimator"; 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 "../ownership_estimators"; describe("adjust_estimate", () => { const BOARD = [ @@ -65,7 +63,7 @@ describe("ScoreEstimator", () => { const tolerance = 0.25; beforeEach(() => { - set_remote_scorer(async () => { + init_remote_ownership_estimator(async () => { return { ownership: OWNERSHIP, score: -7.5, @@ -75,15 +73,15 @@ describe("ScoreEstimator", () => { }; }); - 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; @@ -94,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; @@ -121,7 +119,7 @@ describe("ScoreEstimator", () => { 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], @@ -137,7 +135,7 @@ describe("ScoreEstimator", () => { }); test("score() territory", 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; se.score(); @@ -170,7 +168,7 @@ describe("ScoreEstimator", () => { 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(); @@ -206,7 +204,7 @@ describe("ScoreEstimator", () => { 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(); @@ -232,7 +230,7 @@ describe("ScoreEstimator", () => { }); test("score() with removed stones", async () => { - const se = new ScoreEstimator(undefined, engine, 10, 0.5, false); + const se = new ScoreEstimator(engine, undefined, 10, 0.5, false); se.toggleSingleGroupRemoval(1, 0); se.toggleSingleGroupRemoval(2, 0); await se.when_ready; @@ -260,7 +258,7 @@ describe("ScoreEstimator", () => { }); test("getStoneRemovalString()", async () => { - const se = new ScoreEstimator(undefined, engine, 10, 0.5, false); + const se = new ScoreEstimator(engine, undefined, 10, 0.5, false); se.toggleSingleGroupRemoval(1, 0); se.toggleSingleGroupRemoval(2, 0); await se.when_ready; @@ -278,7 +276,7 @@ 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(); @@ -292,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 @@ -304,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(); @@ -329,14 +327,14 @@ describe("ScoreEstimator", () => { 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); @@ -350,8 +348,8 @@ 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, 0); @@ -365,8 +363,8 @@ describe("ScoreEstimator", () => { }); 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); @@ -377,8 +375,8 @@ describe("ScoreEstimator", () => { }); test("long press", 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, 1000); @@ -413,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/src/engine.ts b/src/engine.ts index 6df666ee..4ae1d369 100644 --- a/src/engine.ts +++ b/src/engine.ts @@ -26,6 +26,7 @@ export * from "./AdHocFormat"; export * from "./AIReview"; export * from "./util"; export * from "./test_utils"; +export * from "./BoardState"; export * from "./autoscore"; export * from "./GoMath"; diff --git a/src/goban.ts b/src/goban.ts index 27a3a0b9..4646b09d 100644 --- a/src/goban.ts +++ b/src/goban.ts @@ -37,7 +37,9 @@ export * from "./test_utils"; export * from "./GobanSocket"; export * from "./util"; export * from "./callbacks"; +export * from "./BoardState"; export * from "./autoscore"; +export * from "./ownership_estimators"; export * as GoMath from "./GoMath"; export * as protocol from "./protocol"; diff --git a/src/local_estimators/__tests__/voronoi.test.ts b/src/ownership_estimators/__tests__/voronoi_estimator.test.ts similarity index 87% rename from src/local_estimators/__tests__/voronoi.test.ts rename to src/ownership_estimators/__tests__/voronoi_estimator.test.ts index 930f1457..d93dd237 100644 --- a/src/local_estimators/__tests__/voronoi.test.ts +++ b/src/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/ownership_estimators/index.ts b/src/ownership_estimators/index.ts new file mode 100644 index 00000000..1782887d --- /dev/null +++ b/src/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/ownership_estimators/remote_estimator.ts b/src/ownership_estimators/remote_estimator.ts new file mode 100644 index 00000000..371d9c65 --- /dev/null +++ b/src/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/ownership_estimators/voronoi_estimator.ts similarity index 97% rename from src/local_estimators/voronoi.ts rename to src/ownership_estimators/voronoi_estimator.ts index 3ba95d22..bcbe10bb 100644 --- a/src/local_estimators/voronoi.ts +++ b/src/ownership_estimators/voronoi_estimator.ts @@ -21,7 +21,7 @@ import { dup } from "../util"; * 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); let points = getPoints(board, (pt) => pt !== 0); diff --git a/src/local_estimators/wasm_estimator.ts b/src/ownership_estimators/wasm_estimator.ts similarity index 91% rename from src/local_estimators/wasm_estimator.ts rename to src/ownership_estimators/wasm_estimator.ts index 785b71f7..2180ad62 100644 --- a/src/local_estimators/wasm_estimator.ts +++ b/src/ownership_estimators/wasm_estimator.ts @@ -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, @@ -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); From b6baf66b4f430aa3d5de07c4fcdbad745f7ca7eb Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Fri, 14 Jun 2024 05:14:39 -0600 Subject: [PATCH 31/68] Add support for automatically marking scores in analysis mode, and clearing them --- src/BoardState.ts | 71 ++++++++++++++++++++++++++++++ src/GobanCanvas.ts | 29 ------------- src/GobanCore.ts | 105 ++++++++++++++++++++++++++++++++++++++++++++- src/GobanSVG.ts | 29 ------------- 4 files changed, 174 insertions(+), 60 deletions(-) diff --git a/src/BoardState.ts b/src/BoardState.ts index 13cabfb3..cec273e2 100644 --- a/src/BoardState.ts +++ b/src/BoardState.ts @@ -38,6 +38,19 @@ export interface BoardConfig { //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); @@ -365,4 +378,62 @@ export class BoardState extends EventEmitter implements BoardConfig { 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/GobanCanvas.ts b/src/GobanCanvas.ts index 99d45966..0d882759 100644 --- a/src/GobanCanvas.ts +++ b/src/GobanCanvas.ts @@ -34,7 +34,6 @@ import { import { _ } from "./translate"; import { formatMessage, MessageID } from "./messages"; import { color_blend, getRandomInt } from "./util"; -import { StoneStringBuilder } from "./StoneStringBuilder"; import { callbacks } from "./callbacks"; const __theme_cache: { @@ -93,7 +92,6 @@ export interface GobanCanvasInterface { export class GobanCanvas extends GobanCore implements GobanCanvasInterface { public engine: GoEngine; - private parent: HTMLElement; //private board_div: HTMLElement; private board: HTMLCanvasElement; private __set_board_height: number = -1; @@ -124,8 +122,6 @@ export class GobanCanvas extends GobanCore implements GobanCanvasInterface { private analysis_scoring_color?: "black" | "white" | string; private analysis_scoring_last_position: { i: number; j: number } = { i: NaN, j: NaN }; - private analysis_removal_state?: boolean; - private analysis_removal_last_position: { i: number; j: number } = { i: NaN, j: NaN }; private drawing_enabled: boolean = true; private pen_ctx?: CanvasRenderingContext2D; @@ -3262,31 +3258,6 @@ export class GobanCanvas extends GobanCore implements GobanCanvasInterface { this.putAnalysisScoreColorAtLocation(cur.i, cur.j, this.analysis_scoring_color); } } - private 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 stone_group = new StoneStringBuilder(this.engine).getGroup(x, y); - - stone_group.map((loc) => { - this.putAnalysisRemovalAtLocation(loc.x, loc.y, this.analysis_removal_state); - }); - } protected setTitle(title: string): void { this.title = title; if (this.title_div) { diff --git a/src/GobanCore.ts b/src/GobanCore.ts index a1f84189..984a7863 100644 --- a/src/GobanCore.ts +++ b/src/GobanCore.ts @@ -28,7 +28,7 @@ import { Score, } from "./GoEngine"; import { GobanMoveError } from "./GobanError"; -import { Move, NumberMatrix, Intersection, encodeMove } from "./GoMath"; +import { Move, NumberMatrix, Intersection, encodeMove, makeMatrix } from "./GoMath"; import * as GoMath from "./GoMath"; import { GoConditionalMove, ConditionalMoveResponse } from "./GoConditionalMove"; import { MoveTree, MarkInterface, MoveTreePenMarks } from "./MoveTree"; @@ -53,6 +53,8 @@ import { GobanSocket, GobanSocketEvents } from "./GobanSocket"; import { ServerToClient, GameChatMessage, GameChatLine, StallingScoreEstimate } from "./protocol"; import { EventEmitter } from "eventemitter3"; import { callbacks } from "./callbacks"; +import { StoneStringBuilder } from "./StoneStringBuilder"; +import { getRelativeEventPosition } from "./canvas_utils"; declare let swal: any; @@ -324,6 +326,7 @@ export interface GobanMetrics { } export abstract class GobanCore extends EventEmitter { + protected parent!: HTMLElement; public conditional_starting_color: "black" | "white" | "invalid" = "invalid"; public conditional_tree: GoConditionalMove = new GoConditionalMove(null); public double_click_submit: boolean; @@ -354,6 +357,9 @@ export abstract class GobanCore extends EventEmitter { private last_paused_state: boolean | null = null; private last_paused_by_player_state: boolean | null = null; + private analysis_removal_state?: boolean; + private analysis_removal_last_position: { i: number; j: number } = { i: NaN, j: NaN }; + private marked_analysis_score?: boolean[][]; /* Properties that emit change events */ private _mode: GobanModes = "play"; @@ -1868,11 +1874,14 @@ export abstract class GobanCore extends EventEmitter { 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); - this.syncReviewMove(); + if (sync_review_move) { + this.syncReviewMove(); + } } protected putAnalysisRemovalAtLocation(x: number, y: number, removal?: boolean): void { const marks = this.getMarks(x, y); @@ -1881,6 +1890,98 @@ export abstract class GobanCore extends EventEmitter { 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(); + } + 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); diff --git a/src/GobanSVG.ts b/src/GobanSVG.ts index 511fcce3..7acdf9c5 100644 --- a/src/GobanSVG.ts +++ b/src/GobanSVG.ts @@ -30,7 +30,6 @@ import { getRelativeEventPosition } from "./canvas_utils"; import { _ } from "./translate"; import { formatMessage, MessageID } from "./messages"; import { color_blend, getRandomInt } from "./util"; -import { StoneStringBuilder } from "./StoneStringBuilder"; import { callbacks } from "./callbacks"; //import { GobanCanvasConfig, GobanCanvasInterface } from "./GobanCanvas"; @@ -93,7 +92,6 @@ interface GobanSVGInterface { export class GobanSVG extends GobanCore implements GobanSVGInterface { public engine: GoEngine; - private parent: HTMLElement; //private board_div: HTMLElement; private svg: SVGElement; private svg_defs: SVGDefsElement; @@ -131,8 +129,6 @@ export class GobanSVG extends GobanCore implements GobanSVGInterface { private analysis_scoring_color?: "black" | "white" | string; private analysis_scoring_last_position: { i: number; j: number } = { i: NaN, j: NaN }; - private analysis_removal_state?: boolean; - private analysis_removal_last_position: { i: number; j: number } = { i: NaN, j: NaN }; private drawing_enabled: boolean = true; protected title_div?: HTMLElement; @@ -3274,31 +3270,6 @@ export class GobanSVG extends GobanCore implements GobanSVGInterface { this.putAnalysisScoreColorAtLocation(cur.i, cur.j, this.analysis_scoring_color); } } - private 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 stone_group = new StoneStringBuilder(this.engine).getGroup(x, y); - - stone_group.map((loc) => { - this.putAnalysisRemovalAtLocation(loc.x, loc.y, this.analysis_removal_state); - }); - } protected setTitle(title: string): void { this.title = title; if (this.title_div) { From f9adeac094d51771f6efa27f5aecd639b16a6920 Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Fri, 14 Jun 2024 05:34:02 -0600 Subject: [PATCH 32/68] Refactor: beginning move to organize engine and front end code into separate directories --- jest.config.ts | 4 +- src/engine.ts | 33 ------ src/engine/index.ts | 36 +++++++ src/{goban.ts => goban/index.ts} | 50 +++------ tsconfig.json | 2 +- tsconfig.node.json | 9 +- webpack.config.js | 173 +++++++++++++++---------------- 7 files changed, 142 insertions(+), 165 deletions(-) delete mode 100644 src/engine.ts create mode 100644 src/engine/index.ts rename src/{goban.ts => goban/index.ts} (54%) diff --git a/jest.config.ts b/jest.config.ts index 5e88614e..ccba70e8 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -31,8 +31,8 @@ export default { // ], coveragePathIgnorePatterns: [ "/src/test.tsx", - "/src/goban.ts", - "/src/engine.ts", + "/src/goban/index.ts", + "/src/engine/index.ts", ".d.ts", "wasm_estimator.ts", ], diff --git a/src/engine.ts b/src/engine.ts deleted file mode 100644 index 4ae1d369..00000000 --- a/src/engine.ts +++ /dev/null @@ -1,33 +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. - */ - -export * from "./GobanError"; -export * from "./StoneString"; -export * from "./GoEngine"; -export * from "./GoConditionalMove"; -export * from "./MoveTree"; -export * from "./translate"; -export * from "./ScoreEstimator"; -export * from "./JGOF"; -export * from "./AdHocFormat"; -export * from "./AIReview"; -export * from "./util"; -export * from "./test_utils"; -export * from "./BoardState"; -export * from "./autoscore"; - -export * from "./GoMath"; -export * as GoMath from "./GoMath"; diff --git a/src/engine/index.ts b/src/engine/index.ts new file mode 100644 index 00000000..3a1e8050 --- /dev/null +++ b/src/engine/index.ts @@ -0,0 +1,36 @@ +/* + * 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 "../AIReview"; +export * from "../autoscore"; +export * from "../BoardState"; +export * from "../GobanError"; +export * from "../GoConditionalMove"; +export * from "../GoEngine"; +export * from "../JGOF"; +export * from "../GobanSocket"; +export * from "../MoveTree"; +export * from "../ScoreEstimator"; +export * from "../StoneString"; +export * from "../StoneStringBuilder"; +export * from "../test_utils"; +export * from "../translate"; +export * from "../ownership_estimators"; +export * from "../util"; + +export * from "../GoMath"; +export * as GoMath from "../GoMath"; diff --git a/src/goban.ts b/src/goban/index.ts similarity index 54% rename from src/goban.ts rename to src/goban/index.ts index 4646b09d..ec8ae757 100644 --- a/src/goban.ts +++ b/src/goban/index.ts @@ -14,41 +14,23 @@ * limitations under the License. */ -export * from "./GobanCore"; -export * from "./GobanCanvas"; -export * from "./GobanSVG"; -export * from "./GoConditionalMove"; -export * from "./GoEngine"; -export * from "./GobanError"; -export * from "./StoneString"; -export * from "./StoneStringBuilder"; -export * from "./GoTheme"; -export * from "./GoThemes"; -export * from "./util"; -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 "./util"; -export * from "./callbacks"; -export * from "./BoardState"; -export * from "./autoscore"; -export * from "./ownership_estimators"; - -export * as GoMath from "./GoMath"; -export * as protocol from "./protocol"; -export { placeRenderedImageStone, preRenderImageStone } from "./themes/image_stones"; +export * from "../engine"; +export * from "../callbacks"; +export * from "../canvas_utils"; +export * from "../GobanCanvas"; +export * from "../GobanCore"; +export * from "../GobanSVG"; +export * from "../GoTheme"; +export * from "../GoThemes"; +export * from "../TestGoban"; + +export * as protocol from "../protocol"; +export { placeRenderedImageStone, preRenderImageStone } from "../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, GobanCanvasConfig } from "../GobanCanvas"; +import { GobanSVG, GobanSVGConfig } from "../GobanSVG"; export type GobanRenderer = GobanCanvas | GobanSVG; export type GobanRendererConfig = GobanCanvasConfig | GobanSVGConfig; @@ -61,8 +43,8 @@ export function setGobanRenderer(_renderer: "svg" | "canvas") { renderer = _renderer; } -import { AdHocFormat } from "./AdHocFormat"; -import { JGOF } from "./JGOF"; +import { AdHocFormat } from "../AdHocFormat"; +import { JGOF } from "../JGOF"; export function createGoban( config: GobanRendererConfig, diff --git a/tsconfig.json b/tsconfig.json index 4085b2d7..38028771 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -30,5 +30,5 @@ "sourceMap": true, "jsx": "react" }, - "files": ["./src/goban.ts", "./src/engine.ts", "./test/test_autoscore.ts"] + "files": ["./src/goban/index.ts", "src/engine/index.ts", "./test/test_autoscore.ts"] } diff --git a/tsconfig.node.json b/tsconfig.node.json index 6335e130..f8395986 100644 --- a/tsconfig.node.json +++ b/tsconfig.node.json @@ -13,10 +13,7 @@ "lib": ["es2017", "dom"], "baseUrl": ".", "paths": { - "*": [ - "src/*", - "*" - ] + "*": ["src/*", "*"] }, "noImplicitAny": true, "noImplicitReturns": true, @@ -24,7 +21,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 a6aa5815..ac9f4df5 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"], extensions: [".webpack.js", ".web.js", ".ts", ".tsx", ".js"], }, @@ -53,31 +52,28 @@ module.exports = (env, argv) => { optimization: { minimizer: [ - new TerserPlugin({ - terserOptions: { - }, - }), + new TerserPlugin({ + terserOptions: {}, + }), ], }, - - devtool: 'source-map', + devtool: "source-map", }; - let ret = [ /* web */ Object.assign({}, common, { - 'target': 'web', + target: "web", entry: { - 'goban': './src/goban.ts', - 'engine': './src/engine.ts', - 'test': './src/test.tsx', + goban: "./src/goban/index.ts", + engine: "./src/engine/index.ts", + test: "./src/test.tsx", }, 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 +88,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,74 +112,75 @@ 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', - }, + /* node */ + Object.assign({}, common, { + target: "node", - 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', - } - } - ] - }, + entry: { + engine: "./src/engine/index.ts", + }, - output: { - path: __dirname + '/node', - filename: '[name].js' - }, + 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", + }, + }, + ], + }, - plugins: plugins.concat([ - new webpack.DefinePlugin({ - CLIENT: false, - SERVER: true, - }), - ]), + output: { + path: __dirname + "/node", + filename: "[name].js", + }, - optimization: { - minimizer: [ - new TerserPlugin({ - terserOptions: { - safari10: true, - }, + plugins: plugins.concat([ + new webpack.DefinePlugin({ + CLIENT: false, + SERVER: true, }), - ], - }, - })); + ]), + + optimization: { + minimizer: [ + new TerserPlugin({ + terserOptions: { + safari10: true, + }, + }), + ], + }, + }), + ); } return ret; -} +}; From b97c0c947db5588df6ba8c18ed582108239ec5c1 Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Fri, 14 Jun 2024 07:49:46 -0600 Subject: [PATCH 33/68] Refactor: Move around a lot of files. Unify build and switch to using dts-bundle-generator --- .vscode/cspell.json | 2 +- Makefile | 4 +- package.json | 1 + src/TestGoban.ts | 8 +- src/{ => engine}/AIReview.ts | 0 src/{ => engine}/AdHocFormat.ts | 0 src/{ => engine}/BoardState.ts | 6 +- src/{ => engine}/GoConditionalMove.ts | 0 src/{ => engine}/GoEngine.ts | 4 +- src/{ => engine}/GoMath.ts | 0 src/{ => engine}/GobanError.ts | 0 src/{ => engine}/GobanSocket.ts | 0 src/{ => engine}/JGOF.ts | 0 src/{ => engine}/MoveTree.ts | 0 src/engine/README.md | 5 + src/{ => engine}/ScoreEstimator.ts | 4 +- src/{ => engine}/StoneString.ts | 0 src/{ => engine}/StoneStringBuilder.ts | 0 src/{ => engine}/autoscore.ts | 0 src/{ => engine}/callbacks.ts | 2 +- src/engine/index.ts | 37 +- src/{ => engine}/messages.ts | 0 .../__tests__/voronoi_estimator.test.ts | 0 .../ownership_estimators/index.ts | 0 .../ownership_estimators/remote_estimator.ts | 0 .../ownership_estimators/voronoi_estimator.ts | 0 .../ownership_estimators/wasm_estimator.ts | 0 src/{ => engine}/protocol/AIServerToClient.ts | 0 src/{ => engine}/protocol/ClientToAIServer.ts | 0 src/{ => engine}/protocol/ClientToServer.ts | 2 +- src/{ => engine}/protocol/ServerToClient.ts | 0 src/{ => engine}/protocol/index.ts | 0 src/{ => engine}/translate.ts | 0 src/{ => engine}/util.ts | 0 src/{ => goban}/GoTheme.ts | 0 src/{ => goban}/GoThemes.ts | 0 src/{ => goban}/GobanCanvas.ts | 20 +- src/{ => goban}/GobanCore.ts | 45 +- src/{ => goban}/GobanSVG.ts | 20 +- src/{ => goban}/canvas_utils.ts | 2 +- src/goban/index.ts | 27 +- src/{ => goban}/themes/board_plain.ts | 4 +- src/{ => goban}/themes/board_woods.ts | 4 +- src/{ => goban}/themes/image_stones.ts | 8 +- src/{ => goban}/themes/plain_stones.ts | 2 +- src/{ => goban}/themes/rendered_stones.ts | 2 +- src/test.tsx | 690 ------------------ src/test_utils.ts | 4 +- src/{ => third_party}/goscorer/LICENSE.txt | 0 src/{ => third_party}/goscorer/README.md | 0 src/{ => third_party}/goscorer/goscorer.d.ts | 0 src/{ => third_party}/goscorer/goscorer.js | 0 test/test_autoscore.ts | 8 +- tsconfig.json | 6 +- tsconfig.node.json | 3 +- webpack.config.js | 9 +- yarn.lock | 12 +- 57 files changed, 141 insertions(+), 800 deletions(-) rename src/{ => engine}/AIReview.ts (100%) rename src/{ => engine}/AdHocFormat.ts (100%) rename src/{ => engine}/BoardState.ts (99%) rename src/{ => engine}/GoConditionalMove.ts (100%) rename src/{ => engine}/GoEngine.ts (99%) rename src/{ => engine}/GoMath.ts (100%) rename src/{ => engine}/GobanError.ts (100%) rename src/{ => engine}/GobanSocket.ts (100%) rename src/{ => engine}/JGOF.ts (100%) rename src/{ => engine}/MoveTree.ts (100%) create mode 100644 src/engine/README.md rename src/{ => engine}/ScoreEstimator.ts (99%) rename src/{ => engine}/StoneString.ts (100%) rename src/{ => engine}/StoneStringBuilder.ts (100%) rename src/{ => engine}/autoscore.ts (100%) rename src/{ => engine}/callbacks.ts (97%) rename src/{ => engine}/messages.ts (100%) rename src/{ => engine}/ownership_estimators/__tests__/voronoi_estimator.test.ts (100%) rename src/{ => engine}/ownership_estimators/index.ts (100%) rename src/{ => engine}/ownership_estimators/remote_estimator.ts (100%) rename src/{ => engine}/ownership_estimators/voronoi_estimator.ts (100%) rename src/{ => engine}/ownership_estimators/wasm_estimator.ts (100%) rename src/{ => engine}/protocol/AIServerToClient.ts (100%) rename src/{ => engine}/protocol/ClientToAIServer.ts (100%) rename src/{ => engine}/protocol/ClientToServer.ts (99%) rename src/{ => engine}/protocol/ServerToClient.ts (100%) rename src/{ => engine}/protocol/index.ts (100%) rename src/{ => engine}/translate.ts (100%) rename src/{ => engine}/util.ts (100%) rename src/{ => goban}/GoTheme.ts (100%) rename src/{ => goban}/GoThemes.ts (100%) rename src/{ => goban}/GobanCanvas.ts (99%) rename src/{ => goban}/GobanCore.ts (99%) rename src/{ => goban}/GobanSVG.ts (99%) rename src/{ => goban}/canvas_utils.ts (99%) rename src/{ => goban}/themes/board_plain.ts (98%) rename src/{ => goban}/themes/board_woods.ts (98%) rename src/{ => goban}/themes/image_stones.ts (98%) rename src/{ => goban}/themes/plain_stones.ts (99%) rename src/{ => goban}/themes/rendered_stones.ts (99%) delete mode 100644 src/test.tsx rename src/{ => third_party}/goscorer/LICENSE.txt (100%) rename src/{ => third_party}/goscorer/README.md (100%) rename src/{ => third_party}/goscorer/goscorer.d.ts (100%) rename src/{ => third_party}/goscorer/goscorer.js (100%) diff --git a/.vscode/cspell.json b/.vscode/cspell.json index 9893224c..7f7c6d90 100644 --- a/.vscode/cspell.json +++ b/.vscode/cspell.json @@ -190,5 +190,5 @@ "zoomable" ], "language": "en,en-GB", - "ignorePaths": ["test/autoscore_test_files", "src/goscorer"] + "ignorePaths": ["test/autoscore_test_files", "src/goscorer", "*.d.ts"] } diff --git a/Makefile b/Makefile index cac3c7d2..14efd8ee 100644 --- a/Makefile +++ b/Makefile @@ -7,7 +7,9 @@ all dev: build lib types: yarn run build-debug yarn run build-production - cp -Rp lib/src/* lib/ + npx dts-bundle-generator -o lib/goban.d.ts src/goban/index.ts + # npx dts-bundle-generator -o lib/engine.d.ts src/engine/index.ts + lint: yarn run lint diff --git a/package.json b/package.json index 2cacaf34..9785980b 100644 --- a/package.json +++ b/package.json @@ -56,6 +56,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", diff --git a/src/TestGoban.ts b/src/TestGoban.ts index 556b2528..5ba86f2f 100644 --- a/src/TestGoban.ts +++ b/src/TestGoban.ts @@ -24,10 +24,10 @@ // - [ASSERT] public state tracking: `is_pen_enabled`, `current_message`, // `current_title` etc. A way for testers to peer into the internals -import { GobanConfig, GobanCore, GobanSelectedThemes } from "./GobanCore"; -import { GoEngine } from "./GoEngine"; -import { MessageID } from "./messages"; -import { MoveTreePenMarks } from "./MoveTree"; +import { GobanConfig, GobanCore, GobanSelectedThemes } from "./goban/GobanCore"; +import { GoEngine } from "engine/GoEngine"; +import { MessageID } from "engine/messages"; +import { MoveTreePenMarks } from "engine/MoveTree"; export class TestGoban extends GobanCore { public engine: GoEngine; diff --git a/src/AIReview.ts b/src/engine/AIReview.ts similarity index 100% rename from src/AIReview.ts rename to src/engine/AIReview.ts diff --git a/src/AdHocFormat.ts b/src/engine/AdHocFormat.ts similarity index 100% rename from src/AdHocFormat.ts rename to src/engine/AdHocFormat.ts diff --git a/src/BoardState.ts b/src/engine/BoardState.ts similarity index 99% rename from src/BoardState.ts rename to src/engine/BoardState.ts index cec273e2..2cbd6319 100644 --- a/src/BoardState.ts +++ b/src/engine/BoardState.ts @@ -14,13 +14,13 @@ * limitations under the License. */ -import { Events } from "./GobanCore"; +import { Events } from "../goban/GobanCore"; import { EventEmitter } from "eventemitter3"; import { JGOFIntersection, JGOFNumericPlayerColor } from "./JGOF"; import { makeMatrix } from "./GoMath"; -import * as goscorer from "./goscorer/goscorer"; +import * as goscorer from "goscorer"; import { StoneStringBuilder } from "./StoneStringBuilder"; -import { GobanCore } from "./GobanCore"; +import type { GobanCore } from "../goban/GobanCore"; import { RawStoneString } from "./StoneString"; import { cloneMatrix, matricesAreEqual } from "./util"; import { callbacks } from "./callbacks"; diff --git a/src/GoConditionalMove.ts b/src/engine/GoConditionalMove.ts similarity index 100% rename from src/GoConditionalMove.ts rename to src/engine/GoConditionalMove.ts diff --git a/src/GoEngine.ts b/src/engine/GoEngine.ts similarity index 99% rename from src/GoEngine.ts rename to src/engine/GoEngine.ts index f2a527a3..a300fd2a 100644 --- a/src/GoEngine.ts +++ b/src/engine/GoEngine.ts @@ -21,7 +21,7 @@ import { Move, encodeMove } from "./GoMath"; import * as GoMath from "./GoMath"; import { RawStoneString } from "./StoneString"; import { ScoreEstimator } from "./ScoreEstimator"; -import { GobanCore, Events } from "./GobanCore"; +import { GobanCore, Events } from "../goban/GobanCore"; import { JGOFTimeControl, JGOFNumericPlayerColor, @@ -34,7 +34,7 @@ import { AdHocPackedMove } from "./AdHocFormat"; import { _ } from "./translate"; import { EventEmitter } from "eventemitter3"; import { GameClock, StallingScoreEstimate } from "./protocol"; -import * as goscorer from "./goscorer/goscorer"; +import * as goscorer from "goscorer"; declare const CLIENT: boolean; declare const SERVER: boolean; diff --git a/src/GoMath.ts b/src/engine/GoMath.ts similarity index 100% rename from src/GoMath.ts rename to src/engine/GoMath.ts diff --git a/src/GobanError.ts b/src/engine/GobanError.ts similarity index 100% rename from src/GobanError.ts rename to src/engine/GobanError.ts diff --git a/src/GobanSocket.ts b/src/engine/GobanSocket.ts similarity index 100% rename from src/GobanSocket.ts rename to src/engine/GobanSocket.ts diff --git a/src/JGOF.ts b/src/engine/JGOF.ts similarity index 100% rename from src/JGOF.ts rename to src/engine/JGOF.ts diff --git a/src/MoveTree.ts b/src/engine/MoveTree.ts similarity index 100% rename from src/MoveTree.ts rename to src/engine/MoveTree.ts 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 99% rename from src/ScoreEstimator.ts rename to src/engine/ScoreEstimator.ts index 8799a98a..02f7dd45 100644 --- a/src/ScoreEstimator.ts +++ b/src/engine/ScoreEstimator.ts @@ -18,12 +18,12 @@ import { encodeMove } from "./GoMath"; import * as GoMath from "./GoMath"; import { StoneString } from "./StoneString"; import { StoneStringBuilder } from "./StoneStringBuilder"; -import { GobanCore } from "./GobanCore"; +import type { GobanCore } from "../goban/GobanCore"; import { GoEngine, PlayerScore, GoEngineRules } from "./GoEngine"; import { JGOFMove, JGOFNumericPlayerColor, JGOFSealingIntersection } from "./JGOF"; import { _ } from "./translate"; import { wasm_estimate_ownership, remote_estimate_ownership } from "./ownership_estimators"; -import * as goscorer from "./goscorer/goscorer"; +import * as goscorer from "goscorer"; import { BoardState } from "./BoardState"; /* In addition to the local estimators, we have a RemoteScoring system diff --git a/src/StoneString.ts b/src/engine/StoneString.ts similarity index 100% rename from src/StoneString.ts rename to src/engine/StoneString.ts diff --git a/src/StoneStringBuilder.ts b/src/engine/StoneStringBuilder.ts similarity index 100% rename from src/StoneStringBuilder.ts rename to src/engine/StoneStringBuilder.ts diff --git a/src/autoscore.ts b/src/engine/autoscore.ts similarity index 100% rename from src/autoscore.ts rename to src/engine/autoscore.ts diff --git a/src/callbacks.ts b/src/engine/callbacks.ts similarity index 97% rename from src/callbacks.ts rename to src/engine/callbacks.ts index a280aa54..2c6cde94 100644 --- a/src/callbacks.ts +++ b/src/engine/callbacks.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import type { GobanCore, GobanSelectedThemes } from "./GobanCore"; +import type { GobanCore, GobanSelectedThemes } from "../goban/GobanCore"; export interface GobanCallbacks { defaultConfig?: () => any; diff --git a/src/engine/index.ts b/src/engine/index.ts index 3a1e8050..a17c7b3e 100644 --- a/src/engine/index.ts +++ b/src/engine/index.ts @@ -14,23 +14,24 @@ * limitations under the License. */ -export * from "../AdHocFormat"; -export * from "../AIReview"; -export * from "../autoscore"; -export * from "../BoardState"; -export * from "../GobanError"; -export * from "../GoConditionalMove"; -export * from "../GoEngine"; -export * from "../JGOF"; -export * from "../GobanSocket"; -export * from "../MoveTree"; -export * from "../ScoreEstimator"; -export * from "../StoneString"; -export * from "../StoneStringBuilder"; +export * from "./AdHocFormat"; +export * from "./AIReview"; +export * from "./autoscore"; +export * from "./BoardState"; +export * from "../goban/GobanCore"; +export * from "./GobanError"; +export * from "./GobanSocket"; +export * from "./GoConditionalMove"; +export * from "./GoEngine"; +export * from "./GoMath"; +export * from "./JGOF"; +export * from "./MoveTree"; +export * from "./ownership_estimators"; +export * from "./ScoreEstimator"; +export * from "./StoneString"; +export * from "./StoneStringBuilder"; export * from "../test_utils"; -export * from "../translate"; -export * from "../ownership_estimators"; -export * from "../util"; +export * from "./translate"; +export * from "./util"; -export * from "../GoMath"; -export * as GoMath from "../GoMath"; +export * as GoMath from "./GoMath"; diff --git a/src/messages.ts b/src/engine/messages.ts similarity index 100% rename from src/messages.ts rename to src/engine/messages.ts diff --git a/src/ownership_estimators/__tests__/voronoi_estimator.test.ts b/src/engine/ownership_estimators/__tests__/voronoi_estimator.test.ts similarity index 100% rename from src/ownership_estimators/__tests__/voronoi_estimator.test.ts rename to src/engine/ownership_estimators/__tests__/voronoi_estimator.test.ts diff --git a/src/ownership_estimators/index.ts b/src/engine/ownership_estimators/index.ts similarity index 100% rename from src/ownership_estimators/index.ts rename to src/engine/ownership_estimators/index.ts diff --git a/src/ownership_estimators/remote_estimator.ts b/src/engine/ownership_estimators/remote_estimator.ts similarity index 100% rename from src/ownership_estimators/remote_estimator.ts rename to src/engine/ownership_estimators/remote_estimator.ts diff --git a/src/ownership_estimators/voronoi_estimator.ts b/src/engine/ownership_estimators/voronoi_estimator.ts similarity index 100% rename from src/ownership_estimators/voronoi_estimator.ts rename to src/engine/ownership_estimators/voronoi_estimator.ts diff --git a/src/ownership_estimators/wasm_estimator.ts b/src/engine/ownership_estimators/wasm_estimator.ts similarity index 100% rename from src/ownership_estimators/wasm_estimator.ts rename to src/engine/ownership_estimators/wasm_estimator.ts 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 100% rename from src/protocol/ClientToAIServer.ts rename to src/engine/protocol/ClientToAIServer.ts diff --git a/src/protocol/ClientToServer.ts b/src/engine/protocol/ClientToServer.ts similarity index 99% rename from src/protocol/ClientToServer.ts rename to src/engine/protocol/ClientToServer.ts index cfe91b2b..ff3591e0 100644 --- a/src/protocol/ClientToServer.ts +++ b/src/engine/protocol/ClientToServer.ts @@ -259,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 GoEngine.gameCanBeCancelled for cancellation ) */ "game/cancel": (data: { /** The game id */ game_id: number; diff --git a/src/protocol/ServerToClient.ts b/src/engine/protocol/ServerToClient.ts similarity index 100% rename from src/protocol/ServerToClient.ts rename to src/engine/protocol/ServerToClient.ts 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/util.ts b/src/engine/util.ts similarity index 100% rename from src/util.ts rename to src/engine/util.ts diff --git a/src/GoTheme.ts b/src/goban/GoTheme.ts similarity index 100% rename from src/GoTheme.ts rename to src/goban/GoTheme.ts diff --git a/src/GoThemes.ts b/src/goban/GoThemes.ts similarity index 100% rename from src/GoThemes.ts rename to src/goban/GoThemes.ts diff --git a/src/GobanCanvas.ts b/src/goban/GobanCanvas.ts similarity index 99% rename from src/GobanCanvas.ts rename to src/goban/GobanCanvas.ts index 0d882759..67fc377c 100644 --- a/src/GobanCanvas.ts +++ b/src/goban/GobanCanvas.ts @@ -14,27 +14,27 @@ * limitations under the License. */ -import { JGOF, JGOFIntersection, JGOFNumericPlayerColor } from "./JGOF"; +import { JGOF, JGOFIntersection, JGOFNumericPlayerColor } from "engine/JGOF"; -import { AdHocFormat } from "./AdHocFormat"; +import { AdHocFormat } from "engine/AdHocFormat"; import { GobanCore, GobanConfig, GobanSelectedThemes, GobanMetrics, GOBAN_FONT } from "./GobanCore"; -import { GoEngine } from "./GoEngine"; -import * as GoMath from "./GoMath"; -import { MoveTree } from "./MoveTree"; +import { GoEngine } from "engine/GoEngine"; +import * as GoMath from "engine/GoMath"; +import { MoveTree } from "engine/MoveTree"; import { GoTheme } from "./GoTheme"; import { GoThemes } from "./GoThemes"; -import { MoveTreePenMarks } from "./MoveTree"; +import { MoveTreePenMarks } from "engine/MoveTree"; import { createDeviceScaledCanvas, resizeDeviceScaledCanvas, allocateCanvasOrError, getRelativeEventPosition, } from "./canvas_utils"; -import { _ } from "./translate"; -import { formatMessage, MessageID } from "./messages"; -import { color_blend, getRandomInt } from "./util"; -import { callbacks } from "./callbacks"; +import { _ } from "engine/translate"; +import { formatMessage, MessageID } from "engine/messages"; +import { color_blend, getRandomInt } from "engine/util"; +import { callbacks } from "../engine/callbacks"; const __theme_cache: { [bw: string]: { [name: string]: { [size: string]: any } }; diff --git a/src/GobanCore.ts b/src/goban/GobanCore.ts similarity index 99% rename from src/GobanCore.ts rename to src/goban/GobanCore.ts index 984a7863..1a7a462d 100644 --- a/src/GobanCore.ts +++ b/src/goban/GobanCore.ts @@ -26,16 +26,22 @@ import { PuzzleConfig, PuzzlePlacementSetting, Score, -} from "./GoEngine"; -import { GobanMoveError } from "./GobanError"; -import { Move, NumberMatrix, Intersection, encodeMove, makeMatrix } from "./GoMath"; -import * as GoMath from "./GoMath"; -import { GoConditionalMove, ConditionalMoveResponse } from "./GoConditionalMove"; -import { MoveTree, MarkInterface, MoveTreePenMarks } from "./MoveTree"; -import { init_wasm_ownership_estimator } from "./ownership_estimators"; -import { ScoreEstimator } from "./ScoreEstimator"; -import { deepEqual, dup, computeAverageMoveTime, niceInterval, matricesAreEqual } from "./util"; -import { _, interpolate } from "./translate"; +} from "engine/GoEngine"; +import { GobanMoveError } from "engine/GobanError"; +import { Move, NumberMatrix, Intersection, encodeMove, makeMatrix } from "engine/GoMath"; +import * as GoMath from "engine/GoMath"; +import { GoConditionalMove, ConditionalMoveResponse } from "engine/GoConditionalMove"; +import { MoveTree, MarkInterface, MoveTreePenMarks } from "engine/MoveTree"; +import { init_wasm_ownership_estimator } from "engine/ownership_estimators"; +import { ScoreEstimator } from "engine/ScoreEstimator"; +import { + deepEqual, + dup, + computeAverageMoveTime, + niceInterval, + matricesAreEqual, +} from "engine/util"; +import { _, interpolate } from "engine/translate"; import { JGOFClock, JGOFIntersection, @@ -46,14 +52,19 @@ import { JGOFPauseState, JGOFPlayerSummary, JGOFSealingIntersection, -} from "./JGOF"; -import { AdHocClock, AdHocPlayerClock, AdHocPauseControl } from "./AdHocFormat"; -import { MessageID } from "./messages"; -import { GobanSocket, GobanSocketEvents } from "./GobanSocket"; -import { ServerToClient, GameChatMessage, GameChatLine, StallingScoreEstimate } from "./protocol"; +} from "engine/JGOF"; +import { AdHocClock, AdHocPlayerClock, AdHocPauseControl } from "engine/AdHocFormat"; +import { MessageID } from "engine/messages"; +import { GobanSocket, GobanSocketEvents } from "engine/GobanSocket"; +import { + ServerToClient, + GameChatMessage, + GameChatLine, + StallingScoreEstimate, +} from "engine/protocol"; import { EventEmitter } from "eventemitter3"; -import { callbacks } from "./callbacks"; -import { StoneStringBuilder } from "./StoneStringBuilder"; +import { callbacks } from "../engine/callbacks"; +import { StoneStringBuilder } from "engine/StoneStringBuilder"; import { getRelativeEventPosition } from "./canvas_utils"; declare let swal: any; diff --git a/src/GobanSVG.ts b/src/goban/GobanSVG.ts similarity index 99% rename from src/GobanSVG.ts rename to src/goban/GobanSVG.ts index 7acdf9c5..2100f212 100644 --- a/src/GobanSVG.ts +++ b/src/goban/GobanSVG.ts @@ -14,23 +14,23 @@ * limitations under the License. */ -import { JGOF, JGOFIntersection, JGOFNumericPlayerColor } from "./JGOF"; +import { JGOF, JGOFIntersection, JGOFNumericPlayerColor } from "engine/JGOF"; -import { AdHocFormat } from "./AdHocFormat"; +import { AdHocFormat } from "engine/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 { MoveTree } from "./MoveTree"; +import { GoEngine } from "engine/GoEngine"; +import * as GoMath from "engine/GoMath"; +import { MoveTree } from "engine/MoveTree"; import { GoTheme } from "./GoTheme"; import { GoThemes } from "./GoThemes"; -import { MoveTreePenMarks } from "./MoveTree"; +import { MoveTreePenMarks } from "engine/MoveTree"; import { getRelativeEventPosition } from "./canvas_utils"; -import { _ } from "./translate"; -import { formatMessage, MessageID } from "./messages"; -import { color_blend, getRandomInt } from "./util"; -import { callbacks } from "./callbacks"; +import { _ } from "engine/translate"; +import { formatMessage, MessageID } from "engine/messages"; +import { color_blend, getRandomInt } from "engine/util"; +import { callbacks } from "../engine/callbacks"; //import { GobanCanvasConfig, GobanCanvasInterface } from "./GobanCanvas"; diff --git a/src/canvas_utils.ts b/src/goban/canvas_utils.ts similarity index 99% rename from src/canvas_utils.ts rename to src/goban/canvas_utils.ts index 0eb95985..56604cc5 100644 --- a/src/canvas_utils.ts +++ b/src/goban/canvas_utils.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { callbacks } from "./callbacks"; +import { callbacks } from "../engine/callbacks"; let __deviceCanvasScalingRatio = 0; let canvases_allocated = 0; diff --git a/src/goban/index.ts b/src/goban/index.ts index ec8ae757..f9937738 100644 --- a/src/goban/index.ts +++ b/src/goban/index.ts @@ -14,23 +14,23 @@ * limitations under the License. */ -export * from "../engine"; -export * from "../callbacks"; -export * from "../canvas_utils"; -export * from "../GobanCanvas"; -export * from "../GobanCore"; -export * from "../GobanSVG"; -export * from "../GoTheme"; -export * from "../GoThemes"; +export * from "engine"; +export * from "../engine/callbacks"; +export * from "./canvas_utils"; +export * from "./GobanCanvas"; +export * from "./GobanCore"; +export * from "./GobanSVG"; +export * from "./GoTheme"; +export * from "./GoThemes"; export * from "../TestGoban"; -export * as protocol from "../protocol"; -export { placeRenderedImageStone, preRenderImageStone } from "../themes/image_stones"; +export * as protocol from "engine/protocol"; +export { placeRenderedImageStone, preRenderImageStone } from "./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, GobanCanvasConfig } from "./GobanCanvas"; +import { GobanSVG, GobanSVGConfig } from "./GobanSVG"; export type GobanRenderer = GobanCanvas | GobanSVG; export type GobanRendererConfig = GobanCanvasConfig | GobanSVGConfig; @@ -43,8 +43,7 @@ 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, diff --git a/src/themes/board_plain.ts b/src/goban/themes/board_plain.ts similarity index 98% rename from src/themes/board_plain.ts rename to src/goban/themes/board_plain.ts index 84d7565c..390a4502 100644 --- a/src/themes/board_plain.ts +++ b/src/goban/themes/board_plain.ts @@ -16,8 +16,8 @@ import { GoTheme, GoThemeBackgroundCSS } from "../GoTheme"; import { GoThemesInterface } from "../GoThemes"; -import { callbacks } from "../callbacks"; -import { _ } from "../translate"; +import { callbacks } from "engine/callbacks"; +import { _ } from "engine/translate"; // Converts a six-digit hex string to rgba() notation function hexToRgba(raw: string, alpha: number = 1): string { diff --git a/src/themes/board_woods.ts b/src/goban/themes/board_woods.ts similarity index 98% rename from src/themes/board_woods.ts rename to src/goban/themes/board_woods.ts index 1fb87564..086a9938 100644 --- a/src/themes/board_woods.ts +++ b/src/goban/themes/board_woods.ts @@ -16,8 +16,8 @@ import { GoTheme, GoThemeBackgroundCSS } from "../GoTheme"; import { GoThemesInterface } from "../GoThemes"; -import { _ } from "../translate"; -import { callbacks } from "../callbacks"; +import { _ } from "engine/translate"; +import { callbacks } from "engine/callbacks"; function getCDNReleaseBase() { if (callbacks.getCDNReleaseBase) { diff --git a/src/themes/image_stones.ts b/src/goban/themes/image_stones.ts similarity index 98% rename from src/themes/image_stones.ts rename to src/goban/themes/image_stones.ts index e70fed9a..175a70bd 100644 --- a/src/themes/image_stones.ts +++ b/src/goban/themes/image_stones.ts @@ -16,14 +16,14 @@ import { GoTheme } from "../GoTheme"; import { GoThemesInterface } from "../GoThemes"; -import { _ } from "../translate"; +import { _ } from "engine/translate"; import { deviceCanvasScalingRatio, allocateCanvasOrError } from "../canvas_utils"; import { renderShadow } from "./rendered_stones"; import { renderPlainStone } from "./plain_stones"; -import { callbacks } from "../callbacks"; +import { callbacks } from "engine/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 (callbacks.getCDNReleaseBase) { diff --git a/src/themes/plain_stones.ts b/src/goban/themes/plain_stones.ts similarity index 99% rename from src/themes/plain_stones.ts rename to src/goban/themes/plain_stones.ts index 6ccf4fee..151552fd 100644 --- a/src/themes/plain_stones.ts +++ b/src/goban/themes/plain_stones.ts @@ -16,7 +16,7 @@ import { GoTheme } from "../GoTheme"; import { GoThemesInterface } from "../GoThemes"; -import { _ } from "../translate"; +import { _ } from "engine/translate"; export function renderPlainStone( ctx: CanvasRenderingContext2D, diff --git a/src/themes/rendered_stones.ts b/src/goban/themes/rendered_stones.ts similarity index 99% rename from src/themes/rendered_stones.ts rename to src/goban/themes/rendered_stones.ts index 43cf3ed8..a90854b4 100644 --- a/src/themes/rendered_stones.ts +++ b/src/goban/themes/rendered_stones.ts @@ -16,7 +16,7 @@ import { GoTheme } from "../GoTheme"; import { GoThemesInterface } from "../GoThemes"; -import { _ } from "../translate"; +import { _ } from "engine/translate"; import { deviceCanvasScalingRatio, allocateCanvasOrError } from "../canvas_utils"; type StoneType = { stone: HTMLCanvasElement; shadow: HTMLCanvasElement }; diff --git a/src/test.tsx b/src/test.tsx deleted file mode 100644 index f7b51db0..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, 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"; -import { callbacks, setCallbacks } from "./callbacks"; - -let stored_config: GobanConfig = {}; -try { - stored_config = JSON.parse(localStorage.getItem("config") || "{}"); -} catch (e) {} - -callbacks.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", -}); - -callbacks.customWhiteStoneUrl = () => { - return "https://cdn.online-go.com/goban/anime_white.svg"; -}; -callbacks.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, -); - -setCallbacks({ - //getCoordinateDisplaySystem: () => "1-1", - getCoordinateDisplaySystem: () => "A1", - getCDNReleaseBase: () => "", -}); - -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/test_utils.ts b/src/test_utils.ts index 6f0c5f4a..628d0fb0 100644 --- a/src/test_utils.ts +++ b/src/test_utils.ts @@ -15,8 +15,8 @@ * limitations under the License. */ -import { AdHocPackedMove } from "./AdHocFormat"; -import { JGOFNumericPlayerColor } from "./JGOF"; +import { AdHocPackedMove } from "engine/AdHocFormat"; +import { JGOFNumericPlayerColor } from "engine/JGOF"; type Coordinate = { x: number; y: number }; diff --git a/src/goscorer/LICENSE.txt b/src/third_party/goscorer/LICENSE.txt similarity index 100% rename from src/goscorer/LICENSE.txt rename to src/third_party/goscorer/LICENSE.txt diff --git a/src/goscorer/README.md b/src/third_party/goscorer/README.md similarity index 100% rename from src/goscorer/README.md rename to src/third_party/goscorer/README.md diff --git a/src/goscorer/goscorer.d.ts b/src/third_party/goscorer/goscorer.d.ts similarity index 100% rename from src/goscorer/goscorer.d.ts rename to src/third_party/goscorer/goscorer.d.ts diff --git a/src/goscorer/goscorer.js b/src/third_party/goscorer/goscorer.js similarity index 100% rename from src/goscorer/goscorer.js rename to src/third_party/goscorer/goscorer.js diff --git a/test/test_autoscore.ts b/test/test_autoscore.ts index 1b14a124..1aa331e6 100755 --- a/test/test_autoscore.ts +++ b/test/test_autoscore.ts @@ -27,11 +27,11 @@ */ import { existsSync, readFileSync, readdirSync } from "fs"; -import { autoscore } from "../src/autoscore"; +import { autoscore } from "../src/engine/autoscore"; import * as clc from "cli-color"; -import { GoEngine, GoEngineInitialState } from "../src/GoEngine"; -import { char2num, makeMatrix, num2char } from "../src/GoMath"; -import { JGOFMove, JGOFNumericPlayerColor, JGOFSealingIntersection } from "../src/JGOF"; +import { GoEngine, GoEngineInitialState } from "../src/engine/GoEngine"; +import { char2num, makeMatrix, num2char } from "../src/engine/GoMath"; +import { JGOFMove, JGOFNumericPlayerColor, JGOFSealingIntersection } from "../src/engine/JGOF"; function run_autoscore_tests() { const test_file_directory = "autoscore_test_files"; diff --git a/tsconfig.json b/tsconfig.json index 38028771..2b94c8ef 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -7,13 +7,15 @@ "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, diff --git a/tsconfig.node.json b/tsconfig.node.json index f8395986..124a38ba 100644 --- a/tsconfig.node.json +++ b/tsconfig.node.json @@ -13,7 +13,8 @@ "lib": ["es2017", "dom"], "baseUrl": ".", "paths": { - "*": ["src/*", "*"] + "*": ["src/*", "*"], + "goscorer": ["src/third_party/goscorer/goscorer"] }, "noImplicitAny": true, "noImplicitReturns": true, diff --git a/webpack.config.js b/webpack.config.js index ac9f4df5..ba4985fa 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -41,7 +41,7 @@ module.exports = (env, argv) => { 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"], }, @@ -67,8 +67,7 @@ module.exports = (env, argv) => { target: "web", entry: { goban: "./src/goban/index.ts", - engine: "./src/engine/index.ts", - test: "./src/test.tsx", + //engine: "./src/engine/index.ts", }, output: { @@ -133,9 +132,10 @@ module.exports = (env, argv) => { }), ]; + /* if (production) { ret.push( - /* node */ + // node Object.assign({}, common, { target: "node", @@ -181,6 +181,7 @@ module.exports = (env, argv) => { }), ); } + */ return ret; }; diff --git a/yarn.lock b/yarn.lock index 02be5d78..dc332344 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2699,6 +2699,14 @@ domexception@^4.0.0: dependencies: webidl-conversions "^7.0.0" +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: + 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" @@ -6260,7 +6268,7 @@ typedoc@^0.25.6: minimatch "^9.0.3" shiki "^0.14.7" -typescript@=5.4.5, 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== @@ -6681,7 +6689,7 @@ yargs-parser@^21.0.1, yargs-parser@^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== From b5967804f182fbe84f5550099479b2590713a6dd Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Fri, 14 Jun 2024 08:28:42 -0600 Subject: [PATCH 34/68] Fix bundling locations for engine vs browser --- Makefile | 3 +-- package.json | 6 +++++- webpack.config.js | 7 +++---- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/Makefile b/Makefile index 14efd8ee..f69d1cad 100644 --- a/Makefile +++ b/Makefile @@ -7,8 +7,7 @@ all dev: build lib types: yarn run build-debug yarn run build-production - npx dts-bundle-generator -o lib/goban.d.ts src/goban/index.ts - # npx dts-bundle-generator -o lib/engine.d.ts src/engine/index.ts + yarn run dts lint: diff --git a/package.json b/package.json index 9785980b..9661c0ab 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,10 @@ "name": "goban", "version": "0.7.50", "description": "", - "main": "lib/goban.js", + "main": "node/goban-engine.js", + "browser": { + "goban": "lib/goban-browser.js" + }, "types": "lib/goban.d.ts", "files": [ "lib/", @@ -22,6 +25,7 @@ "dev": "webpack-cli serve", "build-debug": "webpack", "build-production": "webpack --mode production", + "dts": "dts-bundle-generator -o lib/goban.d.ts src/goban/index.ts", "lint": "eslint src/ --ext=.ts,.tsx", "lint:fix": "eslint --fix src/ --ext=.ts,.tsx", "typedoc": "typedoc src/goban.ts", diff --git a/webpack.config.js b/webpack.config.js index ba4985fa..af619efd 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -66,7 +66,7 @@ module.exports = (env, argv) => { Object.assign({}, common, { target: "web", entry: { - goban: "./src/goban/index.ts", + "goban-browser": "./src/goban/index.ts", //engine: "./src/engine/index.ts", }, @@ -77,6 +77,7 @@ module.exports = (env, argv) => { name: "goban", type: "umd", }, + globalObject: "this", }, module: { @@ -132,7 +133,6 @@ module.exports = (env, argv) => { }), ]; - /* if (production) { ret.push( // node @@ -140,7 +140,7 @@ module.exports = (env, argv) => { target: "node", entry: { - engine: "./src/engine/index.ts", + "goban-engine": "./src/engine/index.ts", }, module: { @@ -181,7 +181,6 @@ module.exports = (env, argv) => { }), ); } - */ return ret; }; From ff69f9ed5f1ee2f27f9c9775daafcf81123fb8dd Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Fri, 14 Jun 2024 12:16:13 -0600 Subject: [PATCH 35/68] Fixed up node packaging --- package.json | 2 +- src/engine/GobanSocket.ts | 6 +++--- src/engine/index.ts | 4 ++-- src/goban/GobanSVG.ts | 14 +++++++------- tsconfig.node.json | 9 +++++---- webpack.config.js | 8 ++++++-- 6 files changed, 24 insertions(+), 19 deletions(-) diff --git a/package.json b/package.json index 9661c0ab..b7b08381 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "description": "", "main": "node/goban-engine.js", "browser": { - "goban": "lib/goban-browser.js" + "goban": "lib/goban.js" }, "types": "lib/goban.d.ts", "files": [ diff --git a/src/engine/GobanSocket.ts b/src/engine/GobanSocket.ts index e556df3a..aa1585b9 100644 --- a/src/engine/GobanSocket.ts +++ b/src/engine/GobanSocket.ts @@ -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/engine/index.ts b/src/engine/index.ts index a17c7b3e..86a175f3 100644 --- a/src/engine/index.ts +++ b/src/engine/index.ts @@ -14,15 +14,15 @@ * limitations under the License. */ +export * from "./BoardState"; +export * from "./GoEngine"; export * from "./AdHocFormat"; export * from "./AIReview"; export * from "./autoscore"; -export * from "./BoardState"; export * from "../goban/GobanCore"; export * from "./GobanError"; export * from "./GobanSocket"; export * from "./GoConditionalMove"; -export * from "./GoEngine"; export * from "./GoMath"; export * from "./JGOF"; export * from "./MoveTree"; diff --git a/src/goban/GobanSVG.ts b/src/goban/GobanSVG.ts index 2100f212..28c7b0a3 100644 --- a/src/goban/GobanSVG.ts +++ b/src/goban/GobanSVG.ts @@ -51,7 +51,7 @@ export interface GobanSVGConfig extends GobanConfig { last_move_opacity?: number; } -interface ViewPortInterface { +interface MoveTreeViewPortInterface { offset_x: number; offset_y: number; minx: number; @@ -3497,7 +3497,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; @@ -3600,7 +3600,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); @@ -3623,7 +3623,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 = @@ -3638,7 +3638,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; @@ -3675,7 +3675,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; @@ -3729,7 +3729,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/tsconfig.node.json b/tsconfig.node.json index 124a38ba..02bc10f7 100644 --- a/tsconfig.node.json +++ b/tsconfig.node.json @@ -2,15 +2,16 @@ "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/*", "*"], diff --git a/webpack.config.js b/webpack.config.js index af619efd..01228532 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -66,7 +66,7 @@ module.exports = (env, argv) => { Object.assign({}, common, { target: "web", entry: { - "goban-browser": "./src/goban/index.ts", + goban: "./src/goban/index.ts", //engine: "./src/engine/index.ts", }, @@ -77,7 +77,6 @@ module.exports = (env, argv) => { name: "goban", type: "umd", }, - globalObject: "this", }, module: { @@ -160,6 +159,11 @@ module.exports = (env, argv) => { output: { path: __dirname + "/node", filename: "[name].js", + globalObject: "this", + library: { + name: "goban", + type: "umd", + }, }, plugins: plugins.concat([ From a6985536250332f39c0f0757ece1b655ff27bde6 Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Sat, 15 Jun 2024 08:39:04 -0600 Subject: [PATCH 36/68] Refactor: File moving, split Goban into render base and light Goban interface for engine callbacks --- Makefile | 10 +- jest.config.ts | 6 +- package.json | 6 +- src/Goban.ts | 309 ++++++++++++++++++ src/__tests__/GoConditionalMove.test.ts | 2 +- src/__tests__/GoEngine.test.ts | 12 +- src/__tests__/GoEngine_sgf.test.ts | 4 +- src/__tests__/GoMath.test.ts | 5 +- src/__tests__/GoMath_positionId.test.ts | 4 +- src/__tests__/GobanCanvas.test.ts | 11 +- .../GobanCore_conditional_moves.test.ts | 2 +- src/__tests__/GobanSVG.test.ts | 11 +- src/__tests__/GobanSocket.test.ts | 4 +- src/__tests__/ScoreEstimator.test.ts | 8 +- src/__tests__/StoneStringBuilder.test.ts | 4 +- src/__tests__/autoscore.test.ts | 2 +- src/__tests__/util.test.ts | 4 +- src/engine/AIReview.ts | 2 +- src/engine/BoardState.ts | 14 +- src/engine/GoMath.ts | 4 +- src/engine/{GoEngine.ts => GobanEngine.ts} | 19 +- src/engine/MoveTree.ts | 6 +- src/engine/ScoreEstimator.ts | 8 +- src/engine/StoneString.ts | 2 +- src/engine/StoneStringBuilder.ts | 2 +- src/engine/autoscore.ts | 4 +- src/engine/{ => formats}/AdHocFormat.ts | 0 src/engine/{ => formats}/JGOF.ts | 4 +- src/engine/formats/index.ts | 18 + src/engine/index.ts | 8 +- src/engine/protocol/ClientToAIServer.ts | 2 +- src/engine/protocol/ClientToServer.ts | 4 +- src/engine/protocol/ServerToClient.ts | 6 +- src/engine/util.ts | 2 +- src/{goban => }/index.ts | 22 +- src/{goban => renderer}/GoTheme.ts | 6 +- src/{goban => renderer}/GoThemes.ts | 0 src/{goban => renderer}/GobanCanvas.ts | 18 +- .../GobanRendererBase.ts} | 111 ++----- src/{goban => renderer}/GobanSVG.ts | 13 +- src/{ => renderer}/TestGoban.ts | 19 +- src/{engine => renderer}/callbacks.ts | 7 +- src/{goban => renderer}/canvas_utils.ts | 2 +- src/{goban => renderer}/themes/board_plain.ts | 2 +- src/{goban => renderer}/themes/board_woods.ts | 2 +- .../themes/image_stones.ts | 2 +- .../themes/plain_stones.ts | 0 .../themes/rendered_stones.ts | 0 src/test_utils.ts | 3 +- test/test_autoscore.ts | 8 +- tsconfig.json | 2 +- tsconfig.node.json | 1 + webpack.config.js | 94 +++--- 53 files changed, 549 insertions(+), 272 deletions(-) create mode 100644 src/Goban.ts rename src/engine/{GoEngine.ts => GobanEngine.ts} (99%) rename src/engine/{ => formats}/AdHocFormat.ts (100%) rename src/engine/{ => formats}/JGOF.ts (98%) create mode 100644 src/engine/formats/index.ts rename src/{goban => }/index.ts (76%) rename src/{goban => renderer}/GoTheme.ts (99%) rename src/{goban => renderer}/GoThemes.ts (100%) rename src/{goban => renderer}/GobanCanvas.ts (99%) rename src/{goban/GobanCore.ts => renderer/GobanRendererBase.ts} (97%) rename src/{goban => renderer}/GobanSVG.ts (99%) rename src/{ => renderer}/TestGoban.ts (76%) rename src/{engine => renderer}/callbacks.ts (90%) rename src/{goban => renderer}/canvas_utils.ts (99%) rename src/{goban => renderer}/themes/board_plain.ts (99%) rename src/{goban => renderer}/themes/board_woods.ts (99%) rename src/{goban => renderer}/themes/image_stones.ts (99%) rename src/{goban => renderer}/themes/plain_stones.ts (100%) rename src/{goban => renderer}/themes/rendered_stones.ts (100%) diff --git a/Makefile b/Makefile index f69d1cad..c6be10e2 100644 --- a/Makefile +++ b/Makefile @@ -4,9 +4,17 @@ SLACK_WEBHOOK=$(shell cat ../ogs/.slack-webhook) all dev: yarn run dev -build lib types: +build: lib types + +lib: build-debug build-production + +build-debug: yarn run build-debug + +build-production: yarn run build-production + +types: yarn run dts diff --git a/jest.config.ts b/jest.config.ts index ccba70e8..c41d8660 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -31,7 +31,7 @@ export default { // ], coveragePathIgnorePatterns: [ "/src/test.tsx", - "/src/goban/index.ts", + "/src/renderer/index.ts", "/src/engine/index.ts", ".d.ts", "wasm_estimator.ts", @@ -82,9 +82,7 @@ export default { // maxWorkers: "50%", // An array of directory names to be searched recursively up from the requiring module's location - // moduleDirectories: [ - // "node_modules" - // ], + moduleDirectories: ["src", "src/third_party", "src/third_party/goscorer", "node_modules"], // An array of file extensions your modules use // moduleFileExtensions: [ diff --git a/package.json b/package.json index b7b08381..28c917f8 100644 --- a/package.json +++ b/package.json @@ -25,11 +25,11 @@ "dev": "webpack-cli serve", "build-debug": "webpack", "build-production": "webpack --mode production", - "dts": "dts-bundle-generator -o lib/goban.d.ts src/goban/index.ts", + "dts": "dts-bundle-generator -o lib/goban.d.ts src/index.ts", "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 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", diff --git a/src/Goban.ts b/src/Goban.ts new file mode 100644 index 00000000..ff2c173c --- /dev/null +++ b/src/Goban.ts @@ -0,0 +1,309 @@ +/* + * 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 { + GoEngine, + GoEngineConfig, + GoEnginePhase, + GoEngineRules, + 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, +} from "engine/formats/JGOF"; +import { 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 "./renderer/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 GoEngineConfig, PuzzleConfig { + display_width?: number; + + interactive?: boolean; + mode?: GobanModes; + square_size?: number | ((goban: Goban) => 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: 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 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: 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; + "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 Goban extends EventEmitter { + /* Classes, types, and enums */ + static Engine = GoEngine; + static setTranslations = setGobanTranslations; + static setCallbacks = setGobanCallbacks; + + /** Actual fields **/ + public readonly goban_id = ++last_goban_id; + + /* The rest of these fields are for subclasses of Goban, namely used by the renderers */ + public abstract engine: 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; + 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(); + } +} diff --git a/src/__tests__/GoConditionalMove.test.ts b/src/__tests__/GoConditionalMove.test.ts index d0f2b258..fc86cb4e 100644 --- a/src/__tests__/GoConditionalMove.test.ts +++ b/src/__tests__/GoConditionalMove.test.ts @@ -1,4 +1,4 @@ -import { GoConditionalMove } from "../GoConditionalMove"; +import { GoConditionalMove } from "../engine"; /** * ``` diff --git a/src/__tests__/GoEngine.test.ts b/src/__tests__/GoEngine.test.ts index 0c8c35fb..4f299900 100644 --- a/src/__tests__/GoEngine.test.ts +++ b/src/__tests__/GoEngine.test.ts @@ -1,11 +1,13 @@ //cspell: disable -import { GoEngine } from "../GoEngine"; +import { + GoEngine, + GobanMoveError, + JGOFIntersection, + makeMatrix, + matricesAreEqual, +} from "../engine"; import { movesFromBoardState } from "../test_utils"; -import { GobanMoveError } from "../GobanError"; -import { JGOFIntersection } from "../JGOF"; -import { makeMatrix } from "../GoMath"; -import { matricesAreEqual } from "../util"; test("boardMatricesAreTheSame", () => { const a = [ diff --git a/src/__tests__/GoEngine_sgf.test.ts b/src/__tests__/GoEngine_sgf.test.ts index 90b90399..fc36fe72 100644 --- a/src/__tests__/GoEngine_sgf.test.ts +++ b/src/__tests__/GoEngine_sgf.test.ts @@ -11,8 +11,8 @@ (global as any).CLIENT = true; -import { TestGoban } from "../TestGoban"; -import { MoveTree } from "../MoveTree"; +import { TestGoban } from "../renderer/TestGoban"; +import { MoveTree } from "../engine"; type SGFTestcase = { template: string; diff --git a/src/__tests__/GoMath.test.ts b/src/__tests__/GoMath.test.ts index 49e9fc97..edc9e579 100644 --- a/src/__tests__/GoMath.test.ts +++ b/src/__tests__/GoMath.test.ts @@ -1,9 +1,6 @@ //cspell: disable -import { StoneStringBuilder } from "../StoneStringBuilder"; -import { JGOFNumericPlayerColor } from "../JGOF"; -import * as GoMath from "../GoMath"; -import { BoardState } from "../BoardState"; +import { StoneStringBuilder, JGOFNumericPlayerColor, GoMath, BoardState } from "../engine"; describe("GoStoneGroups constructor", () => { test("basic board state", () => { diff --git a/src/__tests__/GoMath_positionId.test.ts b/src/__tests__/GoMath_positionId.test.ts index 4c484f69..f9cc5b38 100644 --- a/src/__tests__/GoMath_positionId.test.ts +++ b/src/__tests__/GoMath_positionId.test.ts @@ -1,7 +1,7 @@ // cspell: disable -import * as GoMath from "../GoMath"; -import { JGOFNumericPlayerColor } from "../JGOF"; +import { GoMath } from "../engine"; +import { JGOFNumericPlayerColor } from "../engine"; type Testcase = { height: number; diff --git a/src/__tests__/GobanCanvas.test.ts b/src/__tests__/GobanCanvas.test.ts index a64e1331..f7f1a4cf 100644 --- a/src/__tests__/GobanCanvas.test.ts +++ b/src/__tests__/GobanCanvas.test.ts @@ -6,11 +6,10 @@ (global as any).CLIENT = true; -import { GobanCanvas, GobanCanvasConfig } from "../GobanCanvas"; -import { SCORE_ESTIMATION_TOLERANCE, SCORE_ESTIMATION_TRIALS } from "../GobanCore"; -import { setCallbacks } from "../callbacks"; -import { GobanSocket } from "../GobanSocket"; -import * as GoMath from "../GoMath"; +import { GobanCanvas, GobanCanvasConfig } from "../renderer/GobanCanvas"; +import { SCORE_ESTIMATION_TOLERANCE, SCORE_ESTIMATION_TRIALS } from "../renderer/GobanRendererBase"; +import { GobanSocket, GoMath } from "../engine"; +import { Goban } from "../Goban"; import WS from "jest-websocket-mock"; let board_div: HTMLDivElement; @@ -367,7 +366,7 @@ describe("onTap", () => { const canvas = document.getElementById("board-canvas") as HTMLCanvasElement; const addCoordinatesToChatInput = jest.fn(); - setCallbacks({ addCoordinatesToChatInput }); + Goban.setCallbacks({ addCoordinatesToChatInput }); canvas.dispatchEvent( new MouseEvent("click", { diff --git a/src/__tests__/GobanCore_conditional_moves.test.ts b/src/__tests__/GobanCore_conditional_moves.test.ts index d76a2826..9c8c8be2 100644 --- a/src/__tests__/GobanCore_conditional_moves.test.ts +++ b/src/__tests__/GobanCore_conditional_moves.test.ts @@ -11,7 +11,7 @@ (global as any).CLIENT = true; -import { TestGoban } from "../TestGoban"; +import { TestGoban } from "../renderer/TestGoban"; test("call FollowConditionalPath", () => { const goban = new TestGoban({ moves: [] }); diff --git a/src/__tests__/GobanSVG.test.ts b/src/__tests__/GobanSVG.test.ts index 7bc295d7..7f483b1d 100644 --- a/src/__tests__/GobanSVG.test.ts +++ b/src/__tests__/GobanSVG.test.ts @@ -6,11 +6,10 @@ (global as any).CLIENT = true; -import { GobanSVG, GobanSVGConfig } from "../GobanSVG"; -import { SCORE_ESTIMATION_TOLERANCE, SCORE_ESTIMATION_TRIALS } from "../GobanCore"; -import { setCallbacks } from "../callbacks"; -import { GobanSocket } from "../GobanSocket"; -import * as GoMath from "../GoMath"; +import { GobanSVG, GobanSVGConfig } from "../renderer/GobanSVG"; +import { SCORE_ESTIMATION_TOLERANCE, SCORE_ESTIMATION_TRIALS } from "../renderer/GobanRendererBase"; +import { GobanSocket, GoMath } from "../engine"; +import { Goban } from "../Goban"; import WS from "jest-websocket-mock"; let board_div: HTMLDivElement; @@ -366,7 +365,7 @@ describe("onTap", () => { const event_layer = goban.event_layer; const addCoordinatesToChatInput = jest.fn(); - setCallbacks({ addCoordinatesToChatInput }); + Goban.setCallbacks({ addCoordinatesToChatInput }); event_layer.dispatchEvent( new MouseEvent("click", { diff --git a/src/__tests__/GobanSocket.test.ts b/src/__tests__/GobanSocket.test.ts index bca6fa24..ab278b23 100644 --- a/src/__tests__/GobanSocket.test.ts +++ b/src/__tests__/GobanSocket.test.ts @@ -4,9 +4,9 @@ (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/src/__tests__/ScoreEstimator.test.ts index bdc3a2ac..e0adcef2 100644 --- a/src/__tests__/ScoreEstimator.test.ts +++ b/src/__tests__/ScoreEstimator.test.ts @@ -1,12 +1,12 @@ //cspell: disable -import { GoEngine } from "../GoEngine"; -import { makeMatrix } from "../GoMath"; -import { ScoreEstimator, adjust_estimate, set_local_ownership_estimator } from "../ScoreEstimator"; +import { GoEngine } from "../engine"; +import { makeMatrix } from "../engine"; +import { ScoreEstimator, adjust_estimate, set_local_ownership_estimator } from "../engine"; import { init_remote_ownership_estimator, voronoi_estimate_ownership, -} from "../ownership_estimators"; +} from "../engine/ownership_estimators"; describe("adjust_estimate", () => { const BOARD = [ diff --git a/src/__tests__/StoneStringBuilder.test.ts b/src/__tests__/StoneStringBuilder.test.ts index 02e06dc6..d2538b5f 100644 --- a/src/__tests__/StoneStringBuilder.test.ts +++ b/src/__tests__/StoneStringBuilder.test.ts @@ -1,6 +1,4 @@ -import * as GoMath from "../GoMath"; -import { StoneStringBuilder } from "../StoneStringBuilder"; -import { BoardState } from "../BoardState"; +import { GoMath, StoneStringBuilder, BoardState } from "../engine"; // Here is a board displaying many of the features GoStoneGroup cares about. diff --git a/src/__tests__/autoscore.test.ts b/src/__tests__/autoscore.test.ts index 4ba64d7b..ab4fef90 100644 --- a/src/__tests__/autoscore.test.ts +++ b/src/__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"); diff --git a/src/__tests__/util.test.ts b/src/__tests__/util.test.ts index 261a4276..86601986 100644 --- a/src/__tests__/util.test.ts +++ b/src/__tests__/util.test.ts @@ -1,5 +1,5 @@ -import { escapeSGFText, newlines_to_spaces } from "../util"; -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) diff --git a/src/engine/AIReview.ts b/src/engine/AIReview.ts index 32e0b70e..096a9963 100644 --- a/src/engine/AIReview.ts +++ b/src/engine/AIReview.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { JGOFAIReview, JGOFIntersection, JGOFNumericPlayerColor } from "./JGOF"; +import { JGOFAIReview, JGOFIntersection, JGOFNumericPlayerColor } from "./formats/JGOF"; import { MoveTree } from "./MoveTree"; export interface AIReviewWorstMoveEntry { diff --git a/src/engine/BoardState.ts b/src/engine/BoardState.ts index 2cbd6319..72421912 100644 --- a/src/engine/BoardState.ts +++ b/src/engine/BoardState.ts @@ -14,16 +14,16 @@ * limitations under the License. */ -import { Events } from "../goban/GobanCore"; +import { GobanEvents } from "../Goban"; import { EventEmitter } from "eventemitter3"; -import { JGOFIntersection, JGOFNumericPlayerColor } from "./JGOF"; +import { JGOFIntersection, JGOFNumericPlayerColor } from "./formats/JGOF"; import { makeMatrix } from "./GoMath"; import * as goscorer from "goscorer"; import { StoneStringBuilder } from "./StoneStringBuilder"; -import type { GobanCore } from "../goban/GobanCore"; +import type { Goban } from "../Goban"; import { RawStoneString } from "./StoneString"; import { cloneMatrix, matricesAreEqual } from "./util"; -import { callbacks } from "./callbacks"; +import { callbacks } from "../renderer/callbacks"; export interface BoardConfig { width?: number; @@ -55,13 +55,13 @@ export interface ScoringLocations { let __current_flood_fill_value = 0; const __flood_fill_scratch_pad: number[] = Array(25 * 25).fill(0); -export class BoardState extends EventEmitter implements BoardConfig { +export class BoardState extends EventEmitter implements BoardConfig { public readonly height: number = 19; //public readonly rules:GoEngineRules = 'japanese'; public readonly width: number = 19; public board: JGOFNumericPlayerColor[][]; public removal: boolean[][]; - protected goban_callback?: GobanCore; + protected goban_callback?: Goban; public player: JGOFNumericPlayerColor; public board_is_repeating: boolean; @@ -76,7 +76,7 @@ export class BoardState extends EventEmitter implements BoardConfig { * Any state matrices (board, removal, etc..) provided will be cloned * and must have the same dimensionality. */ - constructor(config: BoardConfig, goban_callback?: GobanCore) { + constructor(config: BoardConfig, goban_callback?: Goban) { super(); this.goban_callback = goban_callback; diff --git a/src/engine/GoMath.ts b/src/engine/GoMath.ts index 269ba314..602465b5 100644 --- a/src/engine/GoMath.ts +++ b/src/engine/GoMath.ts @@ -14,8 +14,8 @@ * limitations under the License. */ -import { JGOFIntersection, JGOFMove, JGOFNumericPlayerColor } from "./JGOF"; -import { AdHocPackedMove } from "./AdHocFormat"; +import { JGOFIntersection, JGOFMove, JGOFNumericPlayerColor } from "./formats/JGOF"; +import { AdHocPackedMove } from "./formats/AdHocFormat"; export type Move = JGOFMove; export type Intersection = JGOFIntersection; diff --git a/src/engine/GoEngine.ts b/src/engine/GobanEngine.ts similarity index 99% rename from src/engine/GoEngine.ts rename to src/engine/GobanEngine.ts index a300fd2a..f57a67ac 100644 --- a/src/engine/GoEngine.ts +++ b/src/engine/GobanEngine.ts @@ -21,7 +21,7 @@ import { Move, encodeMove } from "./GoMath"; import * as GoMath from "./GoMath"; import { RawStoneString } from "./StoneString"; import { ScoreEstimator } from "./ScoreEstimator"; -import { GobanCore, Events } from "../goban/GobanCore"; +import { Goban, GobanEvents } from "../Goban"; import { JGOFTimeControl, JGOFNumericPlayerColor, @@ -29,8 +29,8 @@ import { JGOFPlayerSummary, JGOFIntersection, JGOFSealingIntersection, -} from "./JGOF"; -import { AdHocPackedMove } from "./AdHocFormat"; +} from "./formats/JGOF"; +import { AdHocPackedMove } from "./formats/AdHocFormat"; import { _ } from "./translate"; import { EventEmitter } from "eventemitter3"; import { GameClock, StallingScoreEstimate } from "./protocol"; @@ -468,11 +468,7 @@ export class GoEngine extends BoardState { public score_territory_in_seki: boolean = false; public territory_included_in_sgf: boolean = false; - constructor( - config: GoEngineConfig, - goban_callback?: GobanCore, - dontStoreBoardHistory?: boolean, - ) { + constructor(config: GoEngineConfig, goban_callback?: Goban, dontStoreBoardHistory?: boolean) { super( GoEngine.fillDefaults( GoEngine.migrateConfig( @@ -2477,8 +2473,11 @@ export class GoEngine extends BoardState { return ret; } - public parentEventEmitter?: EventEmitter; - emit(event: K, ...args: EventEmitter.EventArgs): boolean { + public parentEventEmitter?: EventEmitter; + 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/engine/MoveTree.ts b/src/engine/MoveTree.ts index a0f831b2..1d1baa7b 100644 --- a/src/engine/MoveTree.ts +++ b/src/engine/MoveTree.ts @@ -15,11 +15,11 @@ */ import * as GoMath from "./GoMath"; -import { GoEngine } from "./GoEngine"; +import { GoEngine } from "./GobanEngine"; import { BoardState } from "./BoardState"; import { encodeMove } from "./GoMath"; -import { AdHocPackedMove } from "./AdHocFormat"; -import { JGOFNumericPlayerColor, JGOFPlayerSummary } from "./JGOF"; +import { AdHocPackedMove } from "./formats/AdHocFormat"; +import { JGOFNumericPlayerColor, JGOFPlayerSummary } from "./formats/JGOF"; import { escapeSGFText, newlines_to_spaces } from "./util"; export interface MarkInterface { diff --git a/src/engine/ScoreEstimator.ts b/src/engine/ScoreEstimator.ts index 02f7dd45..82014ec0 100644 --- a/src/engine/ScoreEstimator.ts +++ b/src/engine/ScoreEstimator.ts @@ -18,9 +18,9 @@ import { encodeMove } from "./GoMath"; import * as GoMath from "./GoMath"; import { StoneString } from "./StoneString"; import { StoneStringBuilder } from "./StoneStringBuilder"; -import type { GobanCore } from "../goban/GobanCore"; -import { GoEngine, PlayerScore, GoEngineRules } from "./GoEngine"; -import { JGOFMove, JGOFNumericPlayerColor, JGOFSealingIntersection } from "./JGOF"; +import type { Goban } from "../Goban"; +import { GoEngine, PlayerScore, GoEngineRules } from "./GobanEngine"; +import { JGOFMove, JGOFNumericPlayerColor, JGOFSealingIntersection } from "./formats/JGOF"; import { _ } from "./translate"; import { wasm_estimate_ownership, remote_estimate_ownership } from "./ownership_estimators"; import * as goscorer from "goscorer"; @@ -128,7 +128,7 @@ export class ScoreEstimator extends BoardState { constructor( engine: GoEngine, - goban_callback: GobanCore | undefined, + goban_callback: Goban | undefined, trials: number, tolerance: number, prefer_remote: boolean = false, diff --git a/src/engine/StoneString.ts b/src/engine/StoneString.ts index d9a1444f..a16cbdfa 100644 --- a/src/engine/StoneString.ts +++ b/src/engine/StoneString.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { JGOFNumericPlayerColor, JGOFIntersection } from "./JGOF"; +import { JGOFNumericPlayerColor, JGOFIntersection } from "./formats/JGOF"; /** A raw stone string is simply an array of intersections */ export type RawStoneString = Array; diff --git a/src/engine/StoneStringBuilder.ts b/src/engine/StoneStringBuilder.ts index b6bf2e7a..8dae8a24 100644 --- a/src/engine/StoneStringBuilder.ts +++ b/src/engine/StoneStringBuilder.ts @@ -17,7 +17,7 @@ import * as GoMath from "./GoMath"; import { StoneString } from "./StoneString"; import { BoardState } from "./BoardState"; -import { JGOFNumericPlayerColor } from "./JGOF"; +import { JGOFNumericPlayerColor } from "./formats/JGOF"; export class StoneStringBuilder { private state: BoardState; diff --git a/src/engine/autoscore.ts b/src/engine/autoscore.ts index 5fe4660d..00c7d5d2 100644 --- a/src/engine/autoscore.ts +++ b/src/engine/autoscore.ts @@ -22,9 +22,9 @@ */ import { StoneStringBuilder } from "./StoneStringBuilder"; -import { JGOFNumericPlayerColor, JGOFSealingIntersection, JGOFMove } from "./JGOF"; +import { JGOFNumericPlayerColor, JGOFSealingIntersection, JGOFMove } from "./formats/JGOF"; import { char2num, makeMatrix, num2char, pretty_coor_num2ch } from "./GoMath"; -import { GoEngine, GoEngineInitialState, GoEngineRules } from "./GoEngine"; +import { GoEngine, GoEngineInitialState, GoEngineRules } from "./GobanEngine"; import { BoardState } from "./BoardState"; interface AutoscoreResults { diff --git a/src/engine/AdHocFormat.ts b/src/engine/formats/AdHocFormat.ts similarity index 100% rename from src/engine/AdHocFormat.ts rename to src/engine/formats/AdHocFormat.ts diff --git a/src/engine/JGOF.ts b/src/engine/formats/JGOF.ts similarity index 98% rename from src/engine/JGOF.ts rename to src/engine/formats/JGOF.ts index 58d721a3..ad70cd56 100644 --- a/src/engine/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 { 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/index.ts b/src/engine/index.ts index 86a175f3..ce2257fc 100644 --- a/src/engine/index.ts +++ b/src/engine/index.ts @@ -15,23 +15,21 @@ */ export * from "./BoardState"; -export * from "./GoEngine"; -export * from "./AdHocFormat"; +export * from "./GobanEngine"; export * from "./AIReview"; export * from "./autoscore"; -export * from "../goban/GobanCore"; +export * from "../Goban"; export * from "./GobanError"; export * from "./GobanSocket"; export * from "./GoConditionalMove"; export * from "./GoMath"; -export * from "./JGOF"; export * from "./MoveTree"; export * from "./ownership_estimators"; export * from "./ScoreEstimator"; export * from "./StoneString"; export * from "./StoneStringBuilder"; export * from "../test_utils"; -export * from "./translate"; +export * from "./formats"; export * from "./util"; export * as GoMath from "./GoMath"; diff --git a/src/engine/protocol/ClientToAIServer.ts b/src/engine/protocol/ClientToAIServer.ts index 4329accd..cabcce4d 100644 --- a/src/engine/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/engine/protocol/ClientToServer.ts b/src/engine/protocol/ClientToServer.ts index ff3591e0..ed04cd9e 100644 --- a/src/engine/protocol/ClientToServer.ts +++ b/src/engine/protocol/ClientToServer.ts @@ -14,8 +14,8 @@ * limitations under the License. */ -import type { JGOFMove, JGOFPlayerClock, JGOFSealingIntersection } from "../JGOF"; -import type { ReviewMessage } from "../GoEngine"; +import type { JGOFMove, JGOFPlayerClock, JGOFSealingIntersection } from "../formats/JGOF"; +import type { ReviewMessage } from "../GobanEngine"; import type { ConditionalMoveResponse } from "../GoConditionalMove"; /** Messages that clients send, regardless of target server */ diff --git a/src/engine/protocol/ServerToClient.ts b/src/engine/protocol/ServerToClient.ts index 25feecc2..eb48f5f6 100644 --- a/src/engine/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 { JGOFTimeControl } from "../formats/JGOF"; import type { ConditionalMoveResponse } from "../GoConditionalMove"; -import type { GoEngineConfig, Score, ReviewMessage } from "../GoEngine"; -import type { AdHocPackedMove } from "../AdHocFormat"; +import type { GoEngineConfig, 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, diff --git a/src/engine/util.ts b/src/engine/util.ts index 4c15e9fc..af0ec10b 100644 --- a/src/engine/util.ts +++ b/src/engine/util.ts @@ -15,7 +15,7 @@ */ import { _, interpolate } from "./translate"; -import type { JGOFTimeControl } from "./JGOF"; +import type { JGOFTimeControl } from "./formats/JGOF"; /** Returns a random integer between min (inclusive) and max (exclusive) */ export function getRandomInt(min: number, max: number) { diff --git a/src/goban/index.ts b/src/index.ts similarity index 76% rename from src/goban/index.ts rename to src/index.ts index f9937738..1efd732a 100644 --- a/src/goban/index.ts +++ b/src/index.ts @@ -15,22 +15,22 @@ */ export * from "engine"; -export * from "../engine/callbacks"; -export * from "./canvas_utils"; -export * from "./GobanCanvas"; -export * from "./GobanCore"; -export * from "./GobanSVG"; -export * from "./GoTheme"; -export * from "./GoThemes"; -export * from "../TestGoban"; +export * from "./renderer/callbacks"; +export * from "./renderer/canvas_utils"; +export * from "./renderer/GobanCanvas"; +export * from "./Goban"; +export * from "./renderer/GobanSVG"; +export * from "./renderer/GoTheme"; +export * from "./renderer/GoThemes"; +export * from "./renderer/TestGoban"; export * as protocol from "engine/protocol"; -export { placeRenderedImageStone, preRenderImageStone } from "./themes/image_stones"; +export { placeRenderedImageStone, preRenderImageStone } from "./renderer/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, GobanCanvasConfig } from "./renderer/GobanCanvas"; +import { GobanSVG, GobanSVGConfig } from "./renderer/GobanSVG"; export type GobanRenderer = GobanCanvas | GobanSVG; export type GobanRendererConfig = GobanCanvasConfig | GobanSVGConfig; diff --git a/src/goban/GoTheme.ts b/src/renderer/GoTheme.ts similarity index 99% rename from src/goban/GoTheme.ts rename to src/renderer/GoTheme.ts index 689fecd4..f155e339 100644 --- a/src/goban/GoTheme.ts +++ b/src/renderer/GoTheme.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { GobanCore } from "./GobanCore"; +import { Goban } from "../Goban"; export interface GoThemeBackgroundCSS { "background-color"?: string; @@ -251,7 +251,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 getStone(x: number, y: number, stones: any, _goban: GobanCore): any { + public getStone(x: number, y: number, stones: any, _goban: Goban): any { const ret = Array.isArray(stones) ? stones[((x + 1) * 53 * ((y + 1) * 97)) % stones.length] : stones; @@ -267,7 +267,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: Goban): string { if (Array.isArray(stones)) { return "" + (((x + 1) * 53 * ((y + 1) * 97)) % stones.length); } diff --git a/src/goban/GoThemes.ts b/src/renderer/GoThemes.ts similarity index 100% rename from src/goban/GoThemes.ts rename to src/renderer/GoThemes.ts diff --git a/src/goban/GobanCanvas.ts b/src/renderer/GobanCanvas.ts similarity index 99% rename from src/goban/GobanCanvas.ts rename to src/renderer/GobanCanvas.ts index 67fc377c..aa2a42ce 100644 --- a/src/goban/GobanCanvas.ts +++ b/src/renderer/GobanCanvas.ts @@ -14,12 +14,12 @@ * limitations under the License. */ -import { JGOF, JGOFIntersection, JGOFNumericPlayerColor } from "engine/JGOF"; +import { JGOF, JGOFIntersection, JGOFNumericPlayerColor } from "engine/formats/JGOF"; -import { AdHocFormat } from "engine/AdHocFormat"; +import { AdHocFormat } from "engine/formats/AdHocFormat"; -import { GobanCore, GobanConfig, GobanSelectedThemes, GobanMetrics, GOBAN_FONT } from "./GobanCore"; -import { GoEngine } from "engine/GoEngine"; +import { GobanConfig } from "../Goban"; +import { GoEngine } from "engine"; import * as GoMath from "engine/GoMath"; import { MoveTree } from "engine/MoveTree"; import { GoTheme } from "./GoTheme"; @@ -34,7 +34,13 @@ import { import { _ } from "engine/translate"; import { formatMessage, MessageID } from "engine/messages"; import { color_blend, getRandomInt } from "engine/util"; -import { callbacks } from "../engine/callbacks"; +import { callbacks } from "./callbacks"; +import { + GobanRendererBase, + GobanMetrics, + GobanSelectedThemes, + GOBAN_FONT, +} from "./GobanRendererBase"; const __theme_cache: { [bw: string]: { [name: string]: { [size: string]: any } }; @@ -90,7 +96,7 @@ export interface GobanCanvasInterface { destroy(): void; } -export class GobanCanvas extends GobanCore implements GobanCanvasInterface { +export class GobanCanvas extends GobanRendererBase implements GobanCanvasInterface { public engine: GoEngine; //private board_div: HTMLElement; private board: HTMLCanvasElement; diff --git a/src/goban/GobanCore.ts b/src/renderer/GobanRendererBase.ts similarity index 97% rename from src/goban/GobanCore.ts rename to src/renderer/GobanRendererBase.ts index 1a7a462d..6ac01f65 100644 --- a/src/goban/GobanCore.ts +++ b/src/renderer/GobanRendererBase.ts @@ -26,7 +26,7 @@ import { PuzzleConfig, PuzzlePlacementSetting, Score, -} from "engine/GoEngine"; +} from "engine"; import { GobanMoveError } from "engine/GobanError"; import { Move, NumberMatrix, Intersection, encodeMove, makeMatrix } from "engine/GoMath"; import * as GoMath from "engine/GoMath"; @@ -51,9 +51,8 @@ import { JGOFNumericPlayerColor, JGOFPauseState, JGOFPlayerSummary, - JGOFSealingIntersection, -} from "engine/JGOF"; -import { AdHocClock, AdHocPlayerClock, AdHocPauseControl } from "engine/AdHocFormat"; +} from "engine/formats/JGOF"; +import { AdHocClock, AdHocPlayerClock, AdHocPauseControl } from "engine/formats/AdHocFormat"; import { MessageID } from "engine/messages"; import { GobanSocket, GobanSocketEvents } from "engine/GobanSocket"; import { @@ -62,8 +61,7 @@ import { GameChatLine, StallingScoreEstimate, } from "engine/protocol"; -import { EventEmitter } from "eventemitter3"; -import { callbacks } from "../engine/callbacks"; +import { callbacks } from "./callbacks"; import { StoneStringBuilder } from "engine/StoneStringBuilder"; import { getRelativeEventPosition } from "./canvas_utils"; @@ -94,8 +92,6 @@ export type LabelPosition = | "bottom-right" | "bottom-left"; -let last_goban_id = 0; - interface JGOFPlayerClockWithTimedOut extends JGOFPlayerClock { timed_out: boolean; } @@ -138,7 +134,7 @@ export interface GobanConfig extends GoEngineConfig, PuzzleConfig { interactive?: boolean; mode?: GobanModes; - square_size?: number | ((goban: GobanCore) => number) | "auto"; + square_size?: number | ((goban: Goban) => number) | "auto"; getPuzzlePlacementSetting?: () => PuzzlePlacementSetting; @@ -255,80 +251,6 @@ export interface StateUpdateEvents { 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; - "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; -} - export interface GobanMetrics { width: number; height: number; @@ -336,7 +258,16 @@ export interface GobanMetrics { offset: number; } -export abstract class GobanCore extends EventEmitter { +import { Goban } from "../Goban"; + +/** + * 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 GobanRendererBase extends Goban { protected parent!: HTMLElement; public conditional_starting_color: "black" | "white" | "invalid" = "invalid"; public conditional_tree: GoConditionalMove = new GoConditionalMove(null); @@ -504,7 +435,6 @@ export abstract class GobanCore extends EventEmitter { 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; @@ -523,7 +453,7 @@ export abstract class GobanCore extends EventEmitter { protected no_display: boolean; protected onError?: (error: Error) => void; protected on_game_screen: boolean; - protected original_square_size: number | ((goban: GobanCore) => number) | "auto"; + protected original_square_size: number | ((goban: Goban) => number) | "auto"; protected player_id: number; protected puzzle_autoplace_delay: number; protected restrict_moves_to_movetree: boolean; @@ -577,8 +507,6 @@ export abstract class GobanCore extends EventEmitter { } }); - this.goban_id = ++last_goban_id; - /* Apply defaults */ const C: any = {}; const default_config = this.defaultConfig(); @@ -2091,6 +2019,10 @@ export abstract class GobanCore extends EventEmitter { } } + public set(x: number, y: number, player: JGOFNumericPlayerColor): void { + this.markDirty(); + } + protected updateMoveTree(): void { this.move_tree_redraw(); } @@ -2326,9 +2258,6 @@ export abstract class GobanCore extends EventEmitter { return this.engine; } - public set(x: number, y: number, player: JGOFNumericPlayerColor): void { - this.markDirty(); - } public setForRemoval( x: number, y: number, diff --git a/src/goban/GobanSVG.ts b/src/renderer/GobanSVG.ts similarity index 99% rename from src/goban/GobanSVG.ts rename to src/renderer/GobanSVG.ts index 28c7b0a3..f506ce54 100644 --- a/src/goban/GobanSVG.ts +++ b/src/renderer/GobanSVG.ts @@ -14,13 +14,13 @@ * limitations under the License. */ -import { JGOF, JGOFIntersection, JGOFNumericPlayerColor } from "engine/JGOF"; +import { JGOF, JGOFIntersection, JGOFNumericPlayerColor } from "engine/formats/JGOF"; -import { AdHocFormat } from "engine/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 "engine/GoEngine"; +import { GobanConfig } from "../Goban"; +import { GoEngine } from "engine"; import * as GoMath from "engine/GoMath"; import { MoveTree } from "engine/MoveTree"; import { GoTheme } from "./GoTheme"; @@ -30,7 +30,8 @@ import { getRelativeEventPosition } from "./canvas_utils"; import { _ } from "engine/translate"; import { formatMessage, MessageID } from "engine/messages"; import { color_blend, getRandomInt } from "engine/util"; -import { callbacks } from "../engine/callbacks"; +import { callbacks } from "./callbacks"; +import { GobanRendererBase, GobanMetrics, GobanSelectedThemes } from "./GobanRendererBase"; //import { GobanCanvasConfig, GobanCanvasInterface } from "./GobanCanvas"; @@ -90,7 +91,7 @@ interface GobanSVGInterface { destroy(): void; } -export class GobanSVG extends GobanCore implements GobanSVGInterface { +export class GobanSVG extends GobanRendererBase implements GobanSVGInterface { public engine: GoEngine; //private board_div: HTMLElement; private svg: SVGElement; diff --git a/src/TestGoban.ts b/src/renderer/TestGoban.ts similarity index 76% rename from src/TestGoban.ts rename to src/renderer/TestGoban.ts index 5ba86f2f..f8be5685 100644 --- a/src/TestGoban.ts +++ b/src/renderer/TestGoban.ts @@ -15,7 +15,7 @@ * limitations under the License. */ -// This is a minimal implementation of GobanCore. Currently it is just enough +// This is a minimal implementation of Goban. 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 @@ -24,12 +24,14 @@ // - [ASSERT] public state tracking: `is_pen_enabled`, `current_message`, // `current_title` etc. A way for testers to peer into the internals -import { GobanConfig, GobanCore, GobanSelectedThemes } from "./goban/GobanCore"; -import { GoEngine } from "engine/GoEngine"; +import { JGOFNumericPlayerColor } from "engine"; +import { GobanConfig } from "../Goban"; +import { GoEngine } from "engine/GobanEngine"; import { MessageID } from "engine/messages"; import { MoveTreePenMarks } from "engine/MoveTree"; +import { GobanRendererBase, GobanSelectedThemes } from "./GobanRendererBase"; -export class TestGoban extends GobanCore { +export class TestGoban extends GobanRendererBase { public engine: GoEngine; constructor(config: GobanConfig) { @@ -56,4 +58,13 @@ export class TestGoban extends GobanCore { protected setTitle(title: string): void {} protected enableDrawing(): void {} protected disableDrawing(): void {} + public set(x: number, y: number, color: JGOFNumericPlayerColor): void {} + public setForRemoval( + x: number, + y: number, + removed: boolean, + emit_stone_removal_updated: boolean, + ): void {} + public setState(): void {} + public updateScoreEstimation(): void {} } diff --git a/src/engine/callbacks.ts b/src/renderer/callbacks.ts similarity index 90% rename from src/engine/callbacks.ts rename to src/renderer/callbacks.ts index 2c6cde94..6b164b4a 100644 --- a/src/engine/callbacks.ts +++ b/src/renderer/callbacks.ts @@ -14,12 +14,13 @@ * limitations under the License. */ -import type { GobanCore, GobanSelectedThemes } from "../goban/GobanCore"; +import type { Goban } from "../Goban"; +import { GobanSelectedThemes } from "./GobanRendererBase"; export interface GobanCallbacks { defaultConfig?: () => any; getCoordinateDisplaySystem?: () => "A1" | "1-1"; - isAnalysisDisabled?: (goban: GobanCore, perGameSettingAppliesToNonPlayers: boolean) => boolean; + isAnalysisDisabled?: (goban: Goban, perGameSettingAppliesToNonPlayers: boolean) => boolean; getClockDrift?: () => number; getNetworkLatency?: () => number; @@ -72,7 +73,7 @@ export const callbacks: GobanCallbacks = { * 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 setCallbacks(newCallbacks: GobanCallbacks): void { +export function setGobanCallbacks(newCallbacks: GobanCallbacks): void { for (const key in newCallbacks) { if (newCallbacks[key as keyof GobanCallbacks] !== undefined) { callbacks[key as keyof GobanCallbacks] = newCallbacks[ diff --git a/src/goban/canvas_utils.ts b/src/renderer/canvas_utils.ts similarity index 99% rename from src/goban/canvas_utils.ts rename to src/renderer/canvas_utils.ts index 56604cc5..0eb95985 100644 --- a/src/goban/canvas_utils.ts +++ b/src/renderer/canvas_utils.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { callbacks } from "../engine/callbacks"; +import { callbacks } from "./callbacks"; let __deviceCanvasScalingRatio = 0; let canvases_allocated = 0; diff --git a/src/goban/themes/board_plain.ts b/src/renderer/themes/board_plain.ts similarity index 99% rename from src/goban/themes/board_plain.ts rename to src/renderer/themes/board_plain.ts index 390a4502..0bd7e6c0 100644 --- a/src/goban/themes/board_plain.ts +++ b/src/renderer/themes/board_plain.ts @@ -16,7 +16,7 @@ import { GoTheme, GoThemeBackgroundCSS } from "../GoTheme"; import { GoThemesInterface } from "../GoThemes"; -import { callbacks } from "engine/callbacks"; +import { callbacks } from "../callbacks"; import { _ } from "engine/translate"; // Converts a six-digit hex string to rgba() notation diff --git a/src/goban/themes/board_woods.ts b/src/renderer/themes/board_woods.ts similarity index 99% rename from src/goban/themes/board_woods.ts rename to src/renderer/themes/board_woods.ts index 086a9938..7b5723fb 100644 --- a/src/goban/themes/board_woods.ts +++ b/src/renderer/themes/board_woods.ts @@ -17,7 +17,7 @@ import { GoTheme, GoThemeBackgroundCSS } from "../GoTheme"; import { GoThemesInterface } from "../GoThemes"; import { _ } from "engine/translate"; -import { callbacks } from "engine/callbacks"; +import { callbacks } from "../callbacks"; function getCDNReleaseBase() { if (callbacks.getCDNReleaseBase) { diff --git a/src/goban/themes/image_stones.ts b/src/renderer/themes/image_stones.ts similarity index 99% rename from src/goban/themes/image_stones.ts rename to src/renderer/themes/image_stones.ts index 175a70bd..5c579029 100644 --- a/src/goban/themes/image_stones.ts +++ b/src/renderer/themes/image_stones.ts @@ -20,7 +20,7 @@ import { _ } from "engine/translate"; import { deviceCanvasScalingRatio, allocateCanvasOrError } from "../canvas_utils"; import { renderShadow } from "./rendered_stones"; import { renderPlainStone } from "./plain_stones"; -import { callbacks } from "engine/callbacks"; +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")); diff --git a/src/goban/themes/plain_stones.ts b/src/renderer/themes/plain_stones.ts similarity index 100% rename from src/goban/themes/plain_stones.ts rename to src/renderer/themes/plain_stones.ts diff --git a/src/goban/themes/rendered_stones.ts b/src/renderer/themes/rendered_stones.ts similarity index 100% rename from src/goban/themes/rendered_stones.ts rename to src/renderer/themes/rendered_stones.ts diff --git a/src/test_utils.ts b/src/test_utils.ts index 628d0fb0..4ad952f9 100644 --- a/src/test_utils.ts +++ b/src/test_utils.ts @@ -15,8 +15,7 @@ * limitations under the License. */ -import { AdHocPackedMove } from "engine/AdHocFormat"; -import { JGOFNumericPlayerColor } from "engine/JGOF"; +import { AdHocPackedMove, JGOFNumericPlayerColor } from "engine/formats"; type Coordinate = { x: number; y: number }; diff --git a/test/test_autoscore.ts b/test/test_autoscore.ts index 1aa331e6..780ee5b4 100755 --- a/test/test_autoscore.ts +++ b/test/test_autoscore.ts @@ -29,9 +29,13 @@ import { existsSync, readFileSync, readdirSync } from "fs"; import { autoscore } from "../src/engine/autoscore"; import * as clc from "cli-color"; -import { GoEngine, GoEngineInitialState } from "../src/engine/GoEngine"; +import { GoEngine, GoEngineInitialState } from "../src/engine/GobanEngine"; import { char2num, makeMatrix, num2char } from "../src/engine/GoMath"; -import { JGOFMove, JGOFNumericPlayerColor, JGOFSealingIntersection } from "../src/engine/JGOF"; +import { + JGOFMove, + JGOFNumericPlayerColor, + JGOFSealingIntersection, +} from "../src/engine/formats/JGOF"; function run_autoscore_tests() { const test_file_directory = "autoscore_test_files"; diff --git a/tsconfig.json b/tsconfig.json index 2b94c8ef..c80f8c5a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -32,5 +32,5 @@ "sourceMap": true, "jsx": "react" }, - "files": ["./src/goban/index.ts", "src/engine/index.ts", "./test/test_autoscore.ts"] + "files": ["src/index.ts", "src/engine/index.ts", "./test/test_autoscore.ts"] } diff --git a/tsconfig.node.json b/tsconfig.node.json index 02bc10f7..c80ecef4 100644 --- a/tsconfig.node.json +++ b/tsconfig.node.json @@ -15,6 +15,7 @@ "baseUrl": ".", "paths": { "*": ["src/*", "*"], + "engine": ["src/engine"], "goscorer": ["src/third_party/goscorer/goscorer"] }, "noImplicitAny": true, diff --git a/webpack.config.js b/webpack.config.js index 01228532..edc52ebd 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -66,7 +66,7 @@ module.exports = (env, argv) => { Object.assign({}, common, { target: "web", entry: { - goban: "./src/goban/index.ts", + goban: "./src/index.ts", //engine: "./src/engine/index.ts", }, @@ -132,59 +132,59 @@ module.exports = (env, argv) => { }), ]; - if (production) { - ret.push( - // node - Object.assign({}, common, { - target: "node", + //if (production) { + ret.push( + // node + Object.assign({}, common, { + target: "node", - entry: { - "goban-engine": "./src/engine/index.ts", - }, + 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", - }, + 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", }, + ], + }, + + output: { + path: __dirname + "/node", + filename: "[name].js", + globalObject: "this", + library: { + name: "goban", + type: "umd", }, + }, + + plugins: plugins.concat([ + new webpack.DefinePlugin({ + CLIENT: false, + SERVER: true, + }), + ]), - plugins: plugins.concat([ - new webpack.DefinePlugin({ - CLIENT: false, - SERVER: true, + optimization: { + minimizer: [ + new TerserPlugin({ + terserOptions: { + safari10: true, + }, }), - ]), - - optimization: { - minimizer: [ - new TerserPlugin({ - terserOptions: { - safari10: true, - }, - }), - ], - }, - }), - ); - } + ], + }, + }), + ); + //} return ret; }; From 3516319585b6020e64de4bd6ba233360ac0b308e Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Sat, 15 Jun 2024 12:38:30 -0600 Subject: [PATCH 37/68] Refactor: Split Goban functionailty into functionality layers Split the monolithic Goban into interactive functionality, ogs socket connectivity, and rendering base functionality. --- src/{Goban.ts => GobanBase.ts} | 12 +- src/__tests__/GobanCanvas.test.ts | 6 +- src/__tests__/GobanSVG.test.ts | 6 +- src/engine/BoardState.ts | 8 +- src/engine/GobanEngine.ts | 8 +- src/engine/ScoreEstimator.ts | 4 +- src/engine/index.ts | 3 +- src/index.ts | 3 +- src/renderer/GoTheme.ts | 6 +- src/renderer/Goban.ts | 299 ++ src/renderer/GobanCanvas.ts | 11 +- src/renderer/GobanInteractive.ts | 1800 +++++++++++ src/renderer/GobanOGSConnectivity.ts | 2051 +++++++++++++ src/renderer/GobanRendererBase.ts | 4186 -------------------------- src/renderer/GobanSVG.ts | 6 +- src/renderer/TestGoban.ts | 6 +- src/renderer/callbacks.ts | 6 +- src/renderer/focus_tracker.ts | 71 + 18 files changed, 4268 insertions(+), 4224 deletions(-) rename src/{Goban.ts => GobanBase.ts} (96%) create mode 100644 src/renderer/Goban.ts create mode 100644 src/renderer/GobanInteractive.ts create mode 100644 src/renderer/GobanOGSConnectivity.ts delete mode 100644 src/renderer/GobanRendererBase.ts create mode 100644 src/renderer/focus_tracker.ts diff --git a/src/Goban.ts b/src/GobanBase.ts similarity index 96% rename from src/Goban.ts rename to src/GobanBase.ts index ff2c173c..d819b1b7 100644 --- a/src/Goban.ts +++ b/src/GobanBase.ts @@ -70,7 +70,7 @@ export interface GobanConfig extends GoEngineConfig, PuzzleConfig { interactive?: boolean; mode?: GobanModes; - square_size?: number | ((goban: Goban) => number) | "auto"; + square_size?: number | ((goban: GobanBase) => number) | "auto"; getPuzzlePlacementSetting?: () => PuzzlePlacementSetting; @@ -260,7 +260,7 @@ export interface GobanEvents extends StateUpdateEvents { * 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 EventEmitter { +export abstract class GobanBase extends EventEmitter { /* Classes, types, and enums */ static Engine = GoEngine; static setTranslations = setGobanTranslations; @@ -268,6 +268,7 @@ export abstract class Goban extends EventEmitter { /** Actual fields **/ public readonly goban_id = ++last_goban_id; + public destroyed = false; /* The rest of these fields are for subclasses of Goban, namely used by the renderers */ public abstract engine: GoEngine; @@ -306,4 +307,11 @@ export abstract class Goban extends EventEmitter { constructor() { super(); } + + public destroy() { + this.emit("destroy"); + this.destroyed = true; + this.engine.removeAllListeners(); + this.removeAllListeners(); + } } diff --git a/src/__tests__/GobanCanvas.test.ts b/src/__tests__/GobanCanvas.test.ts index f7f1a4cf..4f8e5df3 100644 --- a/src/__tests__/GobanCanvas.test.ts +++ b/src/__tests__/GobanCanvas.test.ts @@ -7,9 +7,9 @@ (global as any).CLIENT = true; import { GobanCanvas, GobanCanvasConfig } from "../renderer/GobanCanvas"; -import { SCORE_ESTIMATION_TOLERANCE, SCORE_ESTIMATION_TRIALS } from "../renderer/GobanRendererBase"; +import { SCORE_ESTIMATION_TOLERANCE, SCORE_ESTIMATION_TRIALS } from "../renderer/GobanInteractive"; import { GobanSocket, GoMath } from "../engine"; -import { Goban } from "../Goban"; +import { GobanBase } from "../GobanBase"; import WS from "jest-websocket-mock"; let board_div: HTMLDivElement; @@ -366,7 +366,7 @@ describe("onTap", () => { const canvas = document.getElementById("board-canvas") as HTMLCanvasElement; const addCoordinatesToChatInput = jest.fn(); - Goban.setCallbacks({ addCoordinatesToChatInput }); + GobanBase.setCallbacks({ addCoordinatesToChatInput }); canvas.dispatchEvent( new MouseEvent("click", { diff --git a/src/__tests__/GobanSVG.test.ts b/src/__tests__/GobanSVG.test.ts index 7f483b1d..cabb762f 100644 --- a/src/__tests__/GobanSVG.test.ts +++ b/src/__tests__/GobanSVG.test.ts @@ -7,9 +7,9 @@ (global as any).CLIENT = true; import { GobanSVG, GobanSVGConfig } from "../renderer/GobanSVG"; -import { SCORE_ESTIMATION_TOLERANCE, SCORE_ESTIMATION_TRIALS } from "../renderer/GobanRendererBase"; +import { SCORE_ESTIMATION_TOLERANCE, SCORE_ESTIMATION_TRIALS } from "../renderer/GobanInteractive"; import { GobanSocket, GoMath } from "../engine"; -import { Goban } from "../Goban"; +import { GobanBase } from "../GobanBase"; import WS from "jest-websocket-mock"; let board_div: HTMLDivElement; @@ -365,7 +365,7 @@ describe("onTap", () => { const event_layer = goban.event_layer; const addCoordinatesToChatInput = jest.fn(); - Goban.setCallbacks({ addCoordinatesToChatInput }); + GobanBase.setCallbacks({ addCoordinatesToChatInput }); event_layer.dispatchEvent( new MouseEvent("click", { diff --git a/src/engine/BoardState.ts b/src/engine/BoardState.ts index 72421912..2b0f5dca 100644 --- a/src/engine/BoardState.ts +++ b/src/engine/BoardState.ts @@ -14,13 +14,13 @@ * limitations under the License. */ -import { GobanEvents } from "../Goban"; +import { GobanEvents } from "../GobanBase"; import { EventEmitter } from "eventemitter3"; import { JGOFIntersection, JGOFNumericPlayerColor } from "./formats/JGOF"; import { makeMatrix } from "./GoMath"; import * as goscorer from "goscorer"; import { StoneStringBuilder } from "./StoneStringBuilder"; -import type { Goban } from "../Goban"; +import type { GobanBase } from "../GobanBase"; import { RawStoneString } from "./StoneString"; import { cloneMatrix, matricesAreEqual } from "./util"; import { callbacks } from "../renderer/callbacks"; @@ -61,7 +61,7 @@ export class BoardState extends EventEmitter implements BoardConfig public readonly width: number = 19; public board: JGOFNumericPlayerColor[][]; public removal: boolean[][]; - protected goban_callback?: Goban; + protected goban_callback?: GobanBase; public player: JGOFNumericPlayerColor; public board_is_repeating: boolean; @@ -76,7 +76,7 @@ export class BoardState extends EventEmitter implements BoardConfig * Any state matrices (board, removal, etc..) provided will be cloned * and must have the same dimensionality. */ - constructor(config: BoardConfig, goban_callback?: Goban) { + constructor(config: BoardConfig, goban_callback?: GobanBase) { super(); this.goban_callback = goban_callback; diff --git a/src/engine/GobanEngine.ts b/src/engine/GobanEngine.ts index f57a67ac..7608004c 100644 --- a/src/engine/GobanEngine.ts +++ b/src/engine/GobanEngine.ts @@ -21,7 +21,7 @@ import { Move, encodeMove } from "./GoMath"; import * as GoMath from "./GoMath"; import { RawStoneString } from "./StoneString"; import { ScoreEstimator } from "./ScoreEstimator"; -import { Goban, GobanEvents } from "../Goban"; +import { GobanBase, GobanEvents } from "../GobanBase"; import { JGOFTimeControl, JGOFNumericPlayerColor, @@ -468,7 +468,11 @@ export class GoEngine extends BoardState { public score_territory_in_seki: boolean = false; public territory_included_in_sgf: boolean = false; - constructor(config: GoEngineConfig, goban_callback?: Goban, dontStoreBoardHistory?: boolean) { + constructor( + config: GoEngineConfig, + goban_callback?: GobanBase, + dontStoreBoardHistory?: boolean, + ) { super( GoEngine.fillDefaults( GoEngine.migrateConfig( diff --git a/src/engine/ScoreEstimator.ts b/src/engine/ScoreEstimator.ts index 82014ec0..966e9a15 100644 --- a/src/engine/ScoreEstimator.ts +++ b/src/engine/ScoreEstimator.ts @@ -18,7 +18,7 @@ import { encodeMove } from "./GoMath"; import * as GoMath from "./GoMath"; import { StoneString } from "./StoneString"; import { StoneStringBuilder } from "./StoneStringBuilder"; -import type { Goban } from "../Goban"; +import type { GobanBase } from "../GobanBase"; import { GoEngine, PlayerScore, GoEngineRules } from "./GobanEngine"; import { JGOFMove, JGOFNumericPlayerColor, JGOFSealingIntersection } from "./formats/JGOF"; import { _ } from "./translate"; @@ -128,7 +128,7 @@ export class ScoreEstimator extends BoardState { constructor( engine: GoEngine, - goban_callback: Goban | undefined, + goban_callback: GobanBase | undefined, trials: number, tolerance: number, prefer_remote: boolean = false, diff --git a/src/engine/index.ts b/src/engine/index.ts index ce2257fc..34977e05 100644 --- a/src/engine/index.ts +++ b/src/engine/index.ts @@ -18,7 +18,7 @@ export * from "./BoardState"; export * from "./GobanEngine"; export * from "./AIReview"; export * from "./autoscore"; -export * from "../Goban"; +export * from "../GobanBase"; export * from "./GobanError"; export * from "./GobanSocket"; export * from "./GoConditionalMove"; @@ -32,4 +32,5 @@ export * from "../test_utils"; export * from "./formats"; export * from "./util"; +export * as translate from "./translate"; export * as GoMath from "./GoMath"; diff --git a/src/index.ts b/src/index.ts index 1efd732a..5ab6cb91 100644 --- a/src/index.ts +++ b/src/index.ts @@ -18,10 +18,11 @@ export * from "engine"; export * from "./renderer/callbacks"; export * from "./renderer/canvas_utils"; export * from "./renderer/GobanCanvas"; -export * from "./Goban"; +export * from "./GobanBase"; export * from "./renderer/GobanSVG"; export * from "./renderer/GoTheme"; export * from "./renderer/GoThemes"; +export * from "./renderer/Goban"; export * from "./renderer/TestGoban"; export * as protocol from "engine/protocol"; diff --git a/src/renderer/GoTheme.ts b/src/renderer/GoTheme.ts index f155e339..f8b408a5 100644 --- a/src/renderer/GoTheme.ts +++ b/src/renderer/GoTheme.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { Goban } from "../Goban"; +import { GobanBase } from "../GobanBase"; export interface GoThemeBackgroundCSS { "background-color"?: string; @@ -251,7 +251,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 getStone(x: number, y: number, stones: any, _goban: Goban): 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 +267,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: Goban): 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); } diff --git a/src/renderer/Goban.ts b/src/renderer/Goban.ts new file mode 100644 index 00000000..a37f2b1a --- /dev/null +++ b/src/renderer/Goban.ts @@ -0,0 +1,299 @@ +/* + * 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 "./GobanInteractive"; +import { GobanOGSConnectivity } from "./GobanOGSConnectivity"; +import { GobanConfig } from "../GobanBase"; +import { callbacks } from "./callbacks"; +import { makeMatrix, StoneStringBuilder } from "engine"; +import { getRelativeEventPosition } from "./canvas_utils"; + +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 GobanOGSConnectivity { + protected abstract setThemes(themes: GobanSelectedThemes, dont_redraw: boolean): void; + + 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; + } +} diff --git a/src/renderer/GobanCanvas.ts b/src/renderer/GobanCanvas.ts index aa2a42ce..55aa2cc0 100644 --- a/src/renderer/GobanCanvas.ts +++ b/src/renderer/GobanCanvas.ts @@ -18,7 +18,7 @@ import { JGOF, JGOFIntersection, JGOFNumericPlayerColor } from "engine/formats/J import { AdHocFormat } from "engine/formats/AdHocFormat"; -import { GobanConfig } from "../Goban"; +import { GobanConfig } from "../GobanBase"; import { GoEngine } from "engine"; import * as GoMath from "engine/GoMath"; import { MoveTree } from "engine/MoveTree"; @@ -35,12 +35,7 @@ import { _ } from "engine/translate"; import { formatMessage, MessageID } from "engine/messages"; import { color_blend, getRandomInt } from "engine/util"; import { callbacks } from "./callbacks"; -import { - GobanRendererBase, - GobanMetrics, - GobanSelectedThemes, - GOBAN_FONT, -} from "./GobanRendererBase"; +import { Goban, GobanMetrics, GobanSelectedThemes, GOBAN_FONT } from "./Goban"; const __theme_cache: { [bw: string]: { [name: string]: { [size: string]: any } }; @@ -96,7 +91,7 @@ export interface GobanCanvasInterface { destroy(): void; } -export class GobanCanvas extends GobanRendererBase implements GobanCanvasInterface { +export class GobanCanvas extends Goban implements GobanCanvasInterface { public engine: GoEngine; //private board_div: HTMLElement; private board: HTMLCanvasElement; diff --git a/src/renderer/GobanInteractive.ts b/src/renderer/GobanInteractive.ts new file mode 100644 index 00000000..99da1e06 --- /dev/null +++ b/src/renderer/GobanInteractive.ts @@ -0,0 +1,1800 @@ +/* + * 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 { + GoEngine, + GoEnginePhase, + GoEngineRules, + ReviewMessage, + PlayerColor, + PuzzlePlacementSetting, + Score, +} from "engine"; +import { GobanMoveError } from "engine/GobanError"; +import { NumberMatrix, Intersection, encodeMove } from "engine/GoMath"; +import * as GoMath from "engine/GoMath"; +import { GoConditionalMove } from "engine/GoConditionalMove"; +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: 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 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; + + protected parent!: HTMLElement; + 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 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: 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 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?: GoConditionalMove; + 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?: 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: 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.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, 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(): 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 (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): 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") { + 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 = 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.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?: 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 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 GoEngine.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 = 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.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/renderer/GobanOGSConnectivity.ts b/src/renderer/GobanOGSConnectivity.ts new file mode 100644 index 00000000..707de0e2 --- /dev/null +++ b/src/renderer/GobanOGSConnectivity.ts @@ -0,0 +1,2051 @@ +/* + * 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 "./GobanInteractive"; +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, + dup, + encodeMove, + GobanSocket, + GobanSocketEvents, + GoConditionalMove, + GoEngine, + GoMath, + init_wasm_ownership_estimator, + JGOFIntersection, + JGOFPauseState, + JGOFPlayerClock, + JGOFPlayerSummary, + JGOFTimeControl, + MarkInterface, + Move, + niceInterval, + ReviewMessage, + ScoreEstimator, +} from "engine"; +import { + //ServerToClient, + GameChatMessage, + //GameChatLine, + //StallingScoreEstimate, +} from "engine/protocol"; + +declare let swal: any; + +interface JGOFPlayerClockWithTimedOut extends JGOFPlayerClock { + timed_out: boolean; +} +/** + * Provides the online connectivity functionality for a Goban to be able + * to connect to the Online-Go.com servers + */ +export abstract class GobanOGSConnectivity 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(): GoEngine { + 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( + 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 (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 = 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); + } + } + + 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 GoEngine.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 */, + ); + + 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/renderer/GobanRendererBase.ts b/src/renderer/GobanRendererBase.ts deleted file mode 100644 index 6ac01f65..00000000 --- a/src/renderer/GobanRendererBase.ts +++ /dev/null @@ -1,4186 +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 "engine"; -import { GobanMoveError } from "engine/GobanError"; -import { Move, NumberMatrix, Intersection, encodeMove, makeMatrix } from "engine/GoMath"; -import * as GoMath from "engine/GoMath"; -import { GoConditionalMove, ConditionalMoveResponse } from "engine/GoConditionalMove"; -import { MoveTree, MarkInterface, MoveTreePenMarks } from "engine/MoveTree"; -import { init_wasm_ownership_estimator } from "engine/ownership_estimators"; -import { ScoreEstimator } from "engine/ScoreEstimator"; -import { - deepEqual, - dup, - computeAverageMoveTime, - niceInterval, - matricesAreEqual, -} from "engine/util"; -import { _, interpolate } from "engine/translate"; -import { - JGOFClock, - JGOFIntersection, - JGOFTimeControl, - JGOFPlayerClock, - JGOFTimeControlSystem, - JGOFNumericPlayerColor, - JGOFPauseState, - JGOFPlayerSummary, -} from "engine/formats/JGOF"; -import { AdHocClock, AdHocPlayerClock, AdHocPauseControl } from "engine/formats/AdHocFormat"; -import { MessageID } from "engine/messages"; -import { GobanSocket, GobanSocketEvents } from "engine/GobanSocket"; -import { - ServerToClient, - GameChatMessage, - GameChatLine, - StallingScoreEstimate, -} from "engine/protocol"; -import { callbacks } from "./callbacks"; -import { StoneStringBuilder } from "engine/StoneStringBuilder"; -import { getRelativeEventPosition } from "./canvas_utils"; - -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", - "score", - "stone_removed", -]; -export type LabelPosition = - | "all" - | "none" - | "top-left" - | "top-right" - | "bottom-right" - | "bottom-left"; - -interface JGOFPlayerClockWithTimedOut extends JGOFPlayerClock { - timed_out: boolean; -} - -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 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: Goban) => 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 GobanMetrics { - width: number; - height: number; - mid: number; - offset: number; -} - -import { Goban } from "../Goban"; - -/** - * 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 GobanRendererBase extends Goban { - protected parent!: HTMLElement; - 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; - private analysis_removal_state?: boolean; - private analysis_removal_last_position: { i: number; j: number } = { i: NaN, j: NaN }; - private 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: 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 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?: 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 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: Goban) => 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; - - constructor(config: GobanConfig, preloaded_data?: GobanConfig) { - super(); - - 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.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]); - } - - 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 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; - } - 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 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( - 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 (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; - } - 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, - 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 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(); - } - 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 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); - } - - /* 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, - }); - } - 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); - } - } - - 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): 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.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"); - - if ( - this.engine.phase === "stone removal" && - !("auto_scoring_done" in this) && - !("auto_scoring_done" in (this as any).engine) - ) { - this.performStoneRemovalAutoScoring(); - } - - 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 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(), - }); - } - } - /** This is a callback that gets called by GoEngine.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; - } - */ - - /** This is a callback that gets called by GoEngine.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 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.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 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 */, - ); - - 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); - } - - 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 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 (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; - } - - 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 = 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 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; - } - - /* 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) { - try { - window.addEventListener("blur", this.onBlur); - window.addEventListener("focus", this.onFocus); - } catch (e) { - console.error(e); - } - } - } catch (e) { - // no CLIENT defined, no problem - } - } - - 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/renderer/GobanSVG.ts b/src/renderer/GobanSVG.ts index f506ce54..da4e249f 100644 --- a/src/renderer/GobanSVG.ts +++ b/src/renderer/GobanSVG.ts @@ -19,7 +19,7 @@ import { JGOF, JGOFIntersection, JGOFNumericPlayerColor } from "engine/formats/J import { AdHocFormat } from "engine/formats/AdHocFormat"; //import { GobanCore, GobanSelectedThemes, GobanMetrics, GOBAN_FONT } from "./GobanCore"; -import { GobanConfig } from "../Goban"; +import { GobanConfig } from "../GobanBase"; import { GoEngine } from "engine"; import * as GoMath from "engine/GoMath"; import { MoveTree } from "engine/MoveTree"; @@ -31,7 +31,7 @@ import { _ } from "engine/translate"; import { formatMessage, MessageID } from "engine/messages"; import { color_blend, getRandomInt } from "engine/util"; import { callbacks } from "./callbacks"; -import { GobanRendererBase, GobanMetrics, GobanSelectedThemes } from "./GobanRendererBase"; +import { Goban, GobanMetrics, GobanSelectedThemes } from "./Goban"; //import { GobanCanvasConfig, GobanCanvasInterface } from "./GobanCanvas"; @@ -91,7 +91,7 @@ interface GobanSVGInterface { destroy(): void; } -export class GobanSVG extends GobanRendererBase implements GobanSVGInterface { +export class GobanSVG extends Goban implements GobanSVGInterface { public engine: GoEngine; //private board_div: HTMLElement; private svg: SVGElement; diff --git a/src/renderer/TestGoban.ts b/src/renderer/TestGoban.ts index f8be5685..18324860 100644 --- a/src/renderer/TestGoban.ts +++ b/src/renderer/TestGoban.ts @@ -25,13 +25,13 @@ // `current_title` etc. A way for testers to peer into the internals import { JGOFNumericPlayerColor } from "engine"; -import { GobanConfig } from "../Goban"; +import { GobanConfig } from "../GobanBase"; import { GoEngine } from "engine/GobanEngine"; import { MessageID } from "engine/messages"; import { MoveTreePenMarks } from "engine/MoveTree"; -import { GobanRendererBase, GobanSelectedThemes } from "./GobanRendererBase"; +import { Goban, GobanSelectedThemes } from "./Goban"; -export class TestGoban extends GobanRendererBase { +export class TestGoban extends Goban { public engine: GoEngine; constructor(config: GobanConfig) { diff --git a/src/renderer/callbacks.ts b/src/renderer/callbacks.ts index 6b164b4a..d4cf714a 100644 --- a/src/renderer/callbacks.ts +++ b/src/renderer/callbacks.ts @@ -14,13 +14,13 @@ * limitations under the License. */ -import type { Goban } from "../Goban"; -import { GobanSelectedThemes } from "./GobanRendererBase"; +import type { GobanBase } from "../GobanBase"; +import { GobanSelectedThemes } from "./Goban"; export interface GobanCallbacks { defaultConfig?: () => any; getCoordinateDisplaySystem?: () => "A1" | "1-1"; - isAnalysisDisabled?: (goban: Goban, perGameSettingAppliesToNonPlayers: boolean) => boolean; + isAnalysisDisabled?: (goban: GobanBase, perGameSettingAppliesToNonPlayers: boolean) => boolean; getClockDrift?: () => number; getNetworkLatency?: () => number; diff --git a/src/renderer/focus_tracker.ts b/src/renderer/focus_tracker.ts new file mode 100644 index 00000000..8d12df56 --- /dev/null +++ b/src/renderer/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(); From 48c18e8e399315d09c7abd89b7c6fb30c19ebab0 Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Sat, 15 Jun 2024 12:53:04 -0600 Subject: [PATCH 38/68] Turn on noImplicitOverride's --- src/engine/GobanEngine.ts | 6 +- src/engine/ScoreEstimator.ts | 4 +- src/renderer/GobanCanvas.ts | 2 +- src/renderer/GobanSVG.ts | 2 +- src/renderer/TestGoban.ts | 10 -- src/renderer/themes/board_plain.ts | 108 ++++++++++----------- src/renderer/themes/board_woods.ts | 126 ++++++++++++------------- src/renderer/themes/image_stones.ts | 48 +++++----- src/renderer/themes/plain_stones.ts | 24 ++--- src/renderer/themes/rendered_stones.ts | 74 +++++++-------- tsconfig.json | 1 + 11 files changed, 196 insertions(+), 209 deletions(-) diff --git a/src/engine/GobanEngine.ts b/src/engine/GobanEngine.ts index 7608004c..dfcf53cc 100644 --- a/src/engine/GobanEngine.ts +++ b/src/engine/GobanEngine.ts @@ -300,7 +300,6 @@ export class GoEngine extends BoardState { {}; /* 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 latencies?: { [player_id: string]: number }; public players: { @@ -452,9 +451,6 @@ export class GoEngine extends BoardState { private allow_self_capture: boolean = false; private allow_superko: boolean = false; private superko_algorithm: GoEngineSuperKoAlgorithm = "psk"; - public black_prisoners: number = 0; - public white_prisoners: number = 0; - public board_is_repeating: boolean; private dontStoreBoardHistory: boolean; public free_handicap_placement: boolean = false; private loading_sgf: boolean = false; @@ -2478,7 +2474,7 @@ export class GoEngine extends BoardState { } public parentEventEmitter?: EventEmitter; - emit( + public override emit( event: K, ...args: EventEmitter.EventArgs ): boolean { diff --git a/src/engine/ScoreEstimator.ts b/src/engine/ScoreEstimator.ts index 966e9a15..809c4592 100644 --- a/src/engine/ScoreEstimator.ts +++ b/src/engine/ScoreEstimator.ts @@ -334,12 +334,12 @@ export class ScoreEstimator extends BoardState { }); } - setRemoved(x: number, y: number, removed: boolean): void { + public override setRemoved(x: number, y: number, removed: boolean): void { this.clearAutoScore(); super.setRemoved(x, y, removed); } - clearRemoved(): void { + public override clearRemoved(): void { this.clearAutoScore(); super.clearRemoved(); } diff --git a/src/renderer/GobanCanvas.ts b/src/renderer/GobanCanvas.ts index 55aa2cc0..68d16718 100644 --- a/src/renderer/GobanCanvas.ts +++ b/src/renderer/GobanCanvas.ts @@ -235,7 +235,7 @@ export class GobanCanvas extends Goban implements GobanCanvasInterface { this.detachPenCanvas(); } - public destroy(): void { + public override destroy(): void { super.destroy(); if (this.board && this.board.parentNode) { diff --git a/src/renderer/GobanSVG.ts b/src/renderer/GobanSVG.ts index da4e249f..5eee464b 100644 --- a/src/renderer/GobanSVG.ts +++ b/src/renderer/GobanSVG.ts @@ -238,7 +238,7 @@ export class GobanSVG extends Goban implements GobanSVGInterface { this.detachPenLayer(); } - public destroy(): void { + public override destroy(): void { super.destroy(); this.clearMessage(); diff --git a/src/renderer/TestGoban.ts b/src/renderer/TestGoban.ts index 18324860..d494519e 100644 --- a/src/renderer/TestGoban.ts +++ b/src/renderer/TestGoban.ts @@ -24,7 +24,6 @@ // - [ASSERT] public state tracking: `is_pen_enabled`, `current_message`, // `current_title` etc. A way for testers to peer into the internals -import { JGOFNumericPlayerColor } from "engine"; import { GobanConfig } from "../GobanBase"; import { GoEngine } from "engine/GobanEngine"; import { MessageID } from "engine/messages"; @@ -58,13 +57,4 @@ export class TestGoban extends Goban { protected setTitle(title: string): void {} protected enableDrawing(): void {} protected disableDrawing(): void {} - public set(x: number, y: number, color: JGOFNumericPlayerColor): void {} - public setForRemoval( - x: number, - y: number, - removed: boolean, - emit_stone_removal_updated: boolean, - ): void {} - public setState(): void {} - public updateScoreEstimation(): void {} } diff --git a/src/renderer/themes/board_plain.ts b/src/renderer/themes/board_plain.ts index 0bd7e6c0..a90cc023 100644 --- a/src/renderer/themes/board_plain.ts +++ b/src/renderer/themes/board_plain.ts @@ -33,34 +33,34 @@ function hexToRgba(raw: string, alpha: number = 1): string { export default function (GoThemes: GoThemesInterface) { class Plain extends GoTheme { - sort(): number { + override sort(): number { return 1; } - get theme_name(): string { + override get theme_name(): string { return "Plain"; } - getBackgroundCSS(): GoThemeBackgroundCSS { + override getBackgroundCSS(): GoThemeBackgroundCSS { 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); } } @@ -69,13 +69,13 @@ export default function (GoThemes: GoThemesInterface) { GoThemes["board"]["Plain"] = Plain; class Custom extends GoTheme { - sort(): number { + 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(): GoThemeBackgroundCSS { return { "background-color": callbacks.customBoardColor ? callbacks.customBoardColor() @@ -87,28 +87,28 @@ export default function (GoThemes: GoThemesInterface) { "background-size": "cover", }; } - getLineColor(): string { + override getLineColor(): string { return callbacks.customBoardLineColor ? callbacks.customBoardLineColor() : "#000000"; } - getFadedLineColor(): string { + override getFadedLineColor(): string { return hexToRgba( callbacks.customBoardLineColor ? callbacks.customBoardLineColor() : "#000000", 0.5, ); } - getStarColor(): string { + override getStarColor(): string { return callbacks.customBoardLineColor ? callbacks.customBoardLineColor() : "#000000"; } - getFadedStarColor(): string { + override getFadedStarColor(): string { return hexToRgba( callbacks.customBoardLineColor ? callbacks.customBoardLineColor() : "#000000", 0.5, ); } - getBlankTextColor(): string { + override getBlankTextColor(): string { return callbacks.customBoardLineColor ? callbacks.customBoardLineColor() : "#000000"; } - getLabelTextColor(): string { + override getLabelTextColor(): string { return hexToRgba( callbacks.customBoardLineColor ? callbacks.customBoardLineColor() : "#000000", 0.75, @@ -120,34 +120,34 @@ export default function (GoThemes: GoThemesInterface) { GoThemes["board"]["Custom"] = Custom; class Night extends GoTheme { - sort(): number { + override sort(): number { return 100; } - get theme_name(): string { + override get theme_name(): string { return "Night Play"; } - getBackgroundCSS(): GoThemeBackgroundCSS { + override getBackgroundCSS(): GoThemeBackgroundCSS { 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"; } } @@ -158,34 +158,34 @@ export default function (GoThemes: GoThemesInterface) { class HNG extends GoTheme { 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(): GoThemeBackgroundCSS { 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; } } @@ -195,34 +195,34 @@ export default function (GoThemes: GoThemesInterface) { class HNGNight extends GoTheme { 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(): GoThemeBackgroundCSS { 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"; } } @@ -231,34 +231,34 @@ export default function (GoThemes: GoThemesInterface) { GoThemes["board"]["HNG Night"] = HNGNight; class Book extends GoTheme { - sort(): number { + override sort(): number { return 110; } - get theme_name(): string { + override get theme_name(): string { return "Book"; } - getBackgroundCSS(): GoThemeBackgroundCSS { + override getBackgroundCSS(): GoThemeBackgroundCSS { 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"; } } diff --git a/src/renderer/themes/board_woods.ts b/src/renderer/themes/board_woods.ts index 7b5723fb..43e83167 100644 --- a/src/renderer/themes/board_woods.ts +++ b/src/renderer/themes/board_woods.ts @@ -28,34 +28,34 @@ function getCDNReleaseBase() { export default function (GoThemes: GoThemesInterface) { class Kaya extends GoTheme { - sort(): number { + override sort(): number { return 10; } - get theme_name(): string { + override get theme_name(): string { return "Kaya"; } - getBackgroundCSS(): GoThemeBackgroundCSS { + override getBackgroundCSS(): GoThemeBackgroundCSS { 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"; } } @@ -64,34 +64,34 @@ export default function (GoThemes: GoThemesInterface) { GoThemes["board"]["Kaya"] = Kaya; class RedOak extends GoTheme { - sort(): number { + override sort(): number { return 20; } - get theme_name(): string { + override get theme_name(): string { return "Red Oak"; } - getBackgroundCSS(): GoThemeBackgroundCSS { + override getBackgroundCSS(): GoThemeBackgroundCSS { 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"; } } @@ -100,34 +100,34 @@ export default function (GoThemes: GoThemesInterface) { GoThemes["board"]["Red Oak"] = RedOak; class Persimmon extends GoTheme { - sort(): number { + override sort(): number { return 30; } - get theme_name(): string { + override get theme_name(): string { return "Persimmon"; } - getBackgroundCSS(): GoThemeBackgroundCSS { + override getBackgroundCSS(): GoThemeBackgroundCSS { 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"; } } @@ -136,34 +136,34 @@ export default function (GoThemes: GoThemesInterface) { GoThemes["board"]["Persimmon"] = Persimmon; class BlackWalnut extends GoTheme { - sort(): number { + override sort(): number { return 40; } - get theme_name(): string { + override get theme_name(): string { return "Black Walnut"; } - getBackgroundCSS(): GoThemeBackgroundCSS { + override getBackgroundCSS(): GoThemeBackgroundCSS { 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"; } } @@ -172,34 +172,34 @@ export default function (GoThemes: GoThemesInterface) { GoThemes["board"]["Black Walnut"] = BlackWalnut; class Granite extends GoTheme { - sort(): number { + override sort(): number { return 40; } - get theme_name(): string { + override get theme_name(): string { return "Granite"; } - getBackgroundCSS(): GoThemeBackgroundCSS { + override getBackgroundCSS(): GoThemeBackgroundCSS { 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"; } } @@ -208,35 +208,35 @@ export default function (GoThemes: GoThemesInterface) { GoThemes["board"]["Granite"] = Granite; class Anime extends GoTheme { - sort(): number { + override sort(): number { return 10; } - get theme_name(): string { + override get theme_name(): string { return "Anime"; } - getBackgroundCSS(): GoThemeBackgroundCSS { + override getBackgroundCSS(): GoThemeBackgroundCSS { 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"; } } @@ -245,34 +245,34 @@ export default function (GoThemes: GoThemesInterface) { GoThemes["board"]["Anime"] = Anime; class BrightKaya extends GoTheme { - sort(): number { + override sort(): number { return 15; } - get theme_name(): string { + override get theme_name(): string { return "Bright Kaya"; } - getBackgroundCSS(): GoThemeBackgroundCSS { + override getBackgroundCSS(): GoThemeBackgroundCSS { 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"; } } diff --git a/src/renderer/themes/image_stones.ts b/src/renderer/themes/image_stones.ts index 5c579029..e6b2d76b 100644 --- a/src/renderer/themes/image_stones.ts +++ b/src/renderer/themes/image_stones.ts @@ -168,10 +168,10 @@ export default function (GoThemes: GoThemesInterface) { } class Common extends GoTheme { - stoneCastsShadow(radius: number): boolean { + 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, @@ -304,15 +304,15 @@ export default function (GoThemes: GoThemesInterface) { GoThemes["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, @@ -334,7 +334,7 @@ export default function (GoThemes: GoThemesInterface) { } } - preRenderBlack( + override preRenderBlack( radius: number, _seed: number, deferredRenderCallback: () => void, @@ -351,15 +351,15 @@ export default function (GoThemes: GoThemesInterface) { //return preRenderImageStone(radius, anime_black_imagedata); } - public getBlackStoneColor(): string { + public override getBlackStoneColor(): string { return callbacks.customBlackStoneColor ? callbacks.customBlackStoneColor() : "#000000"; } - public getBlackTextColor(): string { + public override getBlackTextColor(): string { return callbacks.customBlackTextColor ? callbacks.customBlackTextColor() : "#FFFFFF"; } - placeWhiteStone( + override placeWhiteStone( ctx: CanvasRenderingContext2D, shadow_ctx: CanvasRenderingContext2D | null, stone: StoneType, @@ -381,7 +381,7 @@ export default function (GoThemes: GoThemesInterface) { } } - preRenderWhite( + override preRenderWhite( radius: number, _seed: number, deferredRenderCallback: () => void, @@ -398,15 +398,15 @@ export default function (GoThemes: GoThemesInterface) { //return preRenderImageStone(radius, anime_white_imagedata); } - public getWhiteStoneColor(): string { + public override getWhiteStoneColor(): string { return callbacks.customWhiteStoneColor ? callbacks.customWhiteStoneColor() : "#FFFFFF"; } - public getWhiteTextColor(): string { + public override getWhiteTextColor(): string { return callbacks.customWhiteTextColor ? callbacks.customWhiteTextColor() : "#000000"; } - public preRenderBlackSVG( + public override preRenderBlackSVG( defs: SVGDefsElement, radius: number, _seed: number, @@ -429,7 +429,7 @@ export default function (GoThemes: GoThemesInterface) { return [`custom-black-${radius}`]; } - public preRenderWhiteSVG( + public override preRenderWhiteSVG( defs: SVGDefsElement, radius: number, _seed: number, diff --git a/src/renderer/themes/plain_stones.ts b/src/renderer/themes/plain_stones.ts index 151552fd..05277120 100644 --- a/src/renderer/themes/plain_stones.ts +++ b/src/renderer/themes/plain_stones.ts @@ -52,21 +52,21 @@ export function renderPlainStone( export default function (GoThemes: GoThemesInterface) { class Stone extends GoTheme { - sort(): number { + 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, diff --git a/src/renderer/themes/rendered_stones.ts b/src/renderer/themes/rendered_stones.ts index a90854b4..7c444a3f 100644 --- a/src/renderer/themes/rendered_stones.ts +++ b/src/renderer/themes/rendered_stones.ts @@ -479,10 +479,10 @@ function stoneCastsShadow(radius: number): boolean { export default function (GoThemes: GoThemesInterface) { class Common extends GoTheme { - stoneCastsShadow(radius: number): boolean { + 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, @@ -565,14 +565,14 @@ export default function (GoThemes: GoThemesInterface) { GoThemes["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,7 +707,7 @@ export default function (GoThemes: GoThemesInterface) { return ret; } - getWhiteTextColor(color: string): string { + override getWhiteTextColor(color: string): string { return "#000000"; } } @@ -717,14 +717,14 @@ export default function (GoThemes: GoThemesInterface) { /* 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, @@ -836,14 +836,14 @@ export default function (GoThemes: GoThemesInterface) { /* 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, @@ -954,14 +954,14 @@ export default function (GoThemes: GoThemesInterface) { /* 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, diff --git a/tsconfig.json b/tsconfig.json index c80f8c5a..7d38841b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -22,6 +22,7 @@ "noImplicitAny": true, "noImplicitReturns": true, "noImplicitThis": true, + "noImplicitOverride": true, "strictBindCallApply": true, "strictFunctionTypes": true, "strictNullChecks": true, From 29c43eee82101f408ca58fd93a484e50b51b55b7 Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Sat, 15 Jun 2024 14:14:43 -0600 Subject: [PATCH 39/68] Refactor: Simplifying names for clarity --- .../CanvasRenderer.ts} | 27 ++++---- src/{renderer => Goban}/Goban.ts | 15 +++-- .../InteractiveBase.ts} | 4 +- .../OGSConnectivity.ts} | 11 ++-- src/Goban/README.md | 51 ++++++++++++++++ .../GobanSVG.ts => Goban/SVGRenderer.ts} | 29 +++++---- src/{renderer => Goban}/TestGoban.ts | 2 +- src/{renderer => Goban}/callbacks.ts | 0 src/{renderer => Goban}/canvas_utils.ts | 0 src/{renderer => Goban}/focus_tracker.ts | 0 .../GoTheme.ts => Goban/themes/Theme.ts} | 20 +++--- src/{renderer => Goban}/themes/board_plain.ts | 42 ++++++------- src/{renderer => Goban}/themes/board_woods.ts | 48 +++++++-------- .../themes/image_stones.ts | 16 ++--- src/Goban/themes/index.ts | 61 +++++++++++++++++++ .../themes/plain_stones.ts | 12 ++-- .../themes/rendered_stones.ts | 24 ++++---- src/GobanBase.ts | 2 +- src/__tests__/GoEngine_sgf.test.ts | 2 +- src/__tests__/GobanCanvas.test.ts | 12 ++-- .../GobanCore_conditional_moves.test.ts | 2 +- src/__tests__/GobanSVG.test.ts | 42 +++++++------ src/__tests__/autoscore.test.ts | 2 +- src/engine/BoardState.ts | 2 +- src/index.ts | 27 ++++---- src/renderer/GoThemes.ts | 60 ------------------ 26 files changed, 289 insertions(+), 224 deletions(-) rename src/{renderer/GobanCanvas.ts => Goban/CanvasRenderer.ts} (99%) rename src/{renderer => Goban}/Goban.ts (96%) rename src/{renderer/GobanInteractive.ts => Goban/InteractiveBase.ts} (99%) rename src/{renderer/GobanOGSConnectivity.ts => Goban/OGSConnectivity.ts} (99%) create mode 100644 src/Goban/README.md rename src/{renderer/GobanSVG.ts => Goban/SVGRenderer.ts} (99%) rename src/{renderer => Goban}/TestGoban.ts (96%) rename src/{renderer => Goban}/callbacks.ts (100%) rename src/{renderer => Goban}/canvas_utils.ts (100%) rename src/{renderer => Goban}/focus_tracker.ts (100%) rename src/{renderer/GoTheme.ts => Goban/themes/Theme.ts} (96%) rename src/{renderer => Goban}/themes/board_plain.ts (88%) rename src/{renderer => Goban}/themes/board_woods.ts (86%) rename src/{renderer => Goban}/themes/image_stones.ts (97%) create mode 100644 src/Goban/themes/index.ts rename src/{renderer => Goban}/themes/plain_stones.ts (95%) rename src/{renderer => Goban}/themes/rendered_stones.ts (98%) delete mode 100644 src/renderer/GoThemes.ts diff --git a/src/renderer/GobanCanvas.ts b/src/Goban/CanvasRenderer.ts similarity index 99% rename from src/renderer/GobanCanvas.ts rename to src/Goban/CanvasRenderer.ts index 68d16718..4a0f12cb 100644 --- a/src/renderer/GobanCanvas.ts +++ b/src/Goban/CanvasRenderer.ts @@ -22,8 +22,7 @@ import { GobanConfig } from "../GobanBase"; import { GoEngine } from "engine"; import * as GoMath from "engine/GoMath"; import { MoveTree } from "engine/MoveTree"; -import { GoTheme } from "./GoTheme"; -import { GoThemes } from "./GoThemes"; +import { Theme, THEMES } from "./themes"; import { MoveTreePenMarks } from "engine/MoveTree"; import { createDeviceScaledCanvas, @@ -46,7 +45,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; @@ -134,22 +133,22 @@ export class GobanCanvas extends Goban implements GobanCanvasInterface { black: "Plain", white: "Plain", }; - private theme_black!: GoTheme; + private theme_black!: Theme; 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!: Theme; 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!: Theme; 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); @@ -200,7 +199,7 @@ export class GobanCanvas extends Goban 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 ( @@ -213,7 +212,7 @@ export class GobanCanvas extends Goban 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()); @@ -2717,7 +2716,7 @@ export class GobanCanvas extends Goban 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; @@ -2975,15 +2974,15 @@ export class GobanCanvas extends Goban 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); diff --git a/src/renderer/Goban.ts b/src/Goban/Goban.ts similarity index 96% rename from src/renderer/Goban.ts rename to src/Goban/Goban.ts index a37f2b1a..325fa5fd 100644 --- a/src/renderer/Goban.ts +++ b/src/Goban/Goban.ts @@ -14,12 +14,13 @@ * limitations under the License. */ -import { MARK_TYPES } from "./GobanInteractive"; -import { GobanOGSConnectivity } from "./GobanOGSConnectivity"; +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, Theme } from "./themes"; export const GOBAN_FONT = "Verdana,Arial,sans-serif"; export interface GobanSelectedThemes { @@ -49,8 +50,14 @@ export interface GobanMetrics { * 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 GobanOGSConnectivity { - protected abstract setThemes(themes: GobanSelectedThemes, dont_redraw: boolean): void; +export abstract class Goban extends OGSConnectivity { + static THEMES = THEMES; + static THEMES_SORTED = THEMES_SORTED; + static Theme = Theme; + + protected abstract setTheme(themes: GobanSelectedThemes, dont_redraw: boolean): void; + + protected parent!: HTMLElement; constructor(config: GobanConfig, preloaded_data?: GobanConfig) { super(config, preloaded_data); diff --git a/src/renderer/GobanInteractive.ts b/src/Goban/InteractiveBase.ts similarity index 99% rename from src/renderer/GobanInteractive.ts rename to src/Goban/InteractiveBase.ts index 99da1e06..7c0247ef 100644 --- a/src/renderer/GobanInteractive.ts +++ b/src/Goban/InteractiveBase.ts @@ -129,13 +129,15 @@ export interface StateUpdateEvents { } /** + * 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; - protected parent!: HTMLElement; public conditional_starting_color: "black" | "white" | "invalid" = "invalid"; public conditional_tree: GoConditionalMove = new GoConditionalMove(null); public double_click_submit: boolean; diff --git a/src/renderer/GobanOGSConnectivity.ts b/src/Goban/OGSConnectivity.ts similarity index 99% rename from src/renderer/GobanOGSConnectivity.ts rename to src/Goban/OGSConnectivity.ts index 707de0e2..df732175 100644 --- a/src/renderer/GobanOGSConnectivity.ts +++ b/src/Goban/OGSConnectivity.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { AudioClockEvent, GobanInteractive, MARK_TYPES, MoveCommand } from "./GobanInteractive"; +import { AudioClockEvent, GobanInteractive, MARK_TYPES, MoveCommand } from "./InteractiveBase"; import { GobanConfig, JGOFClockWithTransmitting } from "../GobanBase"; import { callbacks } from "./callbacks"; import { _, interpolate } from "../engine/translate"; @@ -59,10 +59,13 @@ interface JGOFPlayerClockWithTimedOut extends JGOFPlayerClock { timed_out: boolean; } /** - * Provides the online connectivity functionality for a Goban to be able - * to connect to the Online-Go.com servers + * 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 GobanOGSConnectivity extends GobanInteractive { +export abstract class OGSConnectivity extends GobanInteractive { public sent_timed_out_message: boolean = false; protected socket!: GobanSocket; protected socket_event_bindings: Array<[keyof GobanSocketEvents, () => void]> = []; 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/renderer/GobanSVG.ts b/src/Goban/SVGRenderer.ts similarity index 99% rename from src/renderer/GobanSVG.ts rename to src/Goban/SVGRenderer.ts index 5eee464b..62de85d7 100644 --- a/src/renderer/GobanSVG.ts +++ b/src/Goban/SVGRenderer.ts @@ -23,8 +23,7 @@ import { GobanConfig } from "../GobanBase"; import { GoEngine } from "engine"; import * as GoMath from "engine/GoMath"; import { MoveTree } from "engine/MoveTree"; -import { GoTheme } from "./GoTheme"; -import { GoThemes } from "./GoThemes"; +import { Theme, THEMES } from "./themes"; import { MoveTreePenMarks } from "engine/MoveTree"; import { getRelativeEventPosition } from "./canvas_utils"; import { _ } from "engine/translate"; @@ -45,7 +44,7 @@ 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; @@ -91,7 +90,7 @@ interface GobanSVGInterface { destroy(): void; } -export class GobanSVG extends Goban implements GobanSVGInterface { +export class SVGRenderer extends Goban implements GobanSVGInterface { public engine: GoEngine; //private board_div: HTMLElement; private svg: SVGElement; @@ -139,22 +138,22 @@ export class GobanSVG extends Goban implements GobanSVGInterface { black: "Plain", white: "Plain", }; - private theme_black!: GoTheme; + private theme_black!: Theme; 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!: Theme; 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!: Theme; 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); @@ -203,7 +202,7 @@ export class GobanSVG extends Goban 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 ( @@ -216,7 +215,7 @@ export class GobanSVG extends Goban 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()); @@ -2933,7 +2932,7 @@ export class GobanSVG extends Goban 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; @@ -3067,16 +3066,16 @@ export class GobanSVG extends Goban 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); diff --git a/src/renderer/TestGoban.ts b/src/Goban/TestGoban.ts similarity index 96% rename from src/renderer/TestGoban.ts rename to src/Goban/TestGoban.ts index d494519e..150b1d57 100644 --- a/src/renderer/TestGoban.ts +++ b/src/Goban/TestGoban.ts @@ -49,7 +49,7 @@ export class TestGoban extends Goban { 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/renderer/callbacks.ts b/src/Goban/callbacks.ts similarity index 100% rename from src/renderer/callbacks.ts rename to src/Goban/callbacks.ts diff --git a/src/renderer/canvas_utils.ts b/src/Goban/canvas_utils.ts similarity index 100% rename from src/renderer/canvas_utils.ts rename to src/Goban/canvas_utils.ts diff --git a/src/renderer/focus_tracker.ts b/src/Goban/focus_tracker.ts similarity index 100% rename from src/renderer/focus_tracker.ts rename to src/Goban/focus_tracker.ts diff --git a/src/renderer/GoTheme.ts b/src/Goban/themes/Theme.ts similarity index 96% rename from src/renderer/GoTheme.ts rename to src/Goban/themes/Theme.ts index f8b408a5..66d800c0 100644 --- a/src/renderer/GoTheme.ts +++ b/src/Goban/themes/Theme.ts @@ -14,15 +14,15 @@ * limitations under the License. */ -import { GobanBase } from "../GobanBase"; +import { GobanBase } from "../../GobanBase"; -export interface GoThemeBackgroundCSS { +export interface ThemeBackgroundCSS { "background-color"?: string; "background-image"?: string; "background-size"?: string; } -export interface GoThemeBackgroundReactStyles { +export interface ThemeBackgroundReactStyles { backgroundColor?: string; backgroundImage?: string; backgroundSize?: string; @@ -54,12 +54,12 @@ export interface SVGStoneParameters { url?: string; } -export class GoTheme { +export class Theme { public name: string; public styles: { [style_name: string]: string } = {}; - protected parent?: GoTheme; // An optional parent theme + protected parent?: Theme; // An optional parent theme - constructor(parent?: GoTheme) { + constructor(parent?: Theme) { this.name = `[ERROR theme missing name]`; this.parent = parent; } @@ -301,7 +301,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(): ThemeBackgroundCSS { return { "background-color": "#DCB35C", "background-image": "", @@ -309,9 +309,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(): ThemeBackgroundReactStyles { + const ret: ThemeBackgroundReactStyles = {}; + const css: ThemeBackgroundCSS = this.getBackgroundCSS(); ret.backgroundColor = css["background-color"]; ret.backgroundImage = css["background-image"]; diff --git a/src/renderer/themes/board_plain.ts b/src/Goban/themes/board_plain.ts similarity index 88% rename from src/renderer/themes/board_plain.ts rename to src/Goban/themes/board_plain.ts index a90cc023..e1dd7096 100644 --- a/src/renderer/themes/board_plain.ts +++ b/src/Goban/themes/board_plain.ts @@ -14,8 +14,8 @@ * limitations under the License. */ -import { GoTheme, GoThemeBackgroundCSS } from "../GoTheme"; -import { GoThemesInterface } from "../GoThemes"; +import { Theme, ThemeBackgroundCSS } from "./Theme"; +import { ThemesInterface } from "./"; import { callbacks } from "../callbacks"; import { _ } from "engine/translate"; @@ -31,15 +31,15 @@ function hexToRgba(raw: string, alpha: number = 1): string { return `rgba(${r}, ${g}, ${b}, ${alpha})`; } -export default function (GoThemes: GoThemesInterface) { - class Plain extends GoTheme { +export default function (THEMES: ThemesInterface) { + class Plain extends Theme { override sort(): number { return 1; } override get theme_name(): string { return "Plain"; } - override getBackgroundCSS(): GoThemeBackgroundCSS { + override getBackgroundCSS(): ThemeBackgroundCSS { return { "background-color": "#DCB35C", "background-image": "", @@ -66,16 +66,16 @@ export default function (GoThemes: GoThemesInterface) { } _("Plain"); // ensure translation exists - GoThemes["board"]["Plain"] = Plain; + THEMES["board"]["Plain"] = Plain; - class Custom extends GoTheme { + class Custom extends Theme { override sort(): number { return 200; //last, because this is the "customisable" one } override get theme_name(): string { return "Custom"; } - override getBackgroundCSS(): GoThemeBackgroundCSS { + override getBackgroundCSS(): ThemeBackgroundCSS { return { "background-color": callbacks.customBoardColor ? callbacks.customBoardColor() @@ -117,16 +117,16 @@ export default function (GoThemes: GoThemesInterface) { } _("Custom"); // ensure translation exists - GoThemes["board"]["Custom"] = Custom; + THEMES["board"]["Custom"] = Custom; - class Night extends GoTheme { + class Night extends Theme { override sort(): number { return 100; } override get theme_name(): string { return "Night Play"; } - override getBackgroundCSS(): GoThemeBackgroundCSS { + override getBackgroundCSS(): ThemeBackgroundCSS { return { "background-color": "#444444", "background-image": "", @@ -153,9 +153,9 @@ export default function (GoThemes: GoThemesInterface) { } _("Night Play"); // ensure translation exists - GoThemes["board"]["Night Play"] = Night; + THEMES["board"]["Night Play"] = Night; - class HNG extends GoTheme { + class HNG extends Theme { static C = "#00193E"; static C2 = "#004C75"; override sort(): number { @@ -164,7 +164,7 @@ export default function (GoThemes: GoThemesInterface) { override get theme_name(): string { return "HNG"; } - override getBackgroundCSS(): GoThemeBackgroundCSS { + override getBackgroundCSS(): ThemeBackgroundCSS { return { "background-color": "#00e7fc", "background-image": "", @@ -191,9 +191,9 @@ export default function (GoThemes: GoThemesInterface) { } _("HNG"); // ensure translation exists - GoThemes["board"]["HNG"] = HNG; + THEMES["board"]["HNG"] = HNG; - class HNGNight extends GoTheme { + class HNGNight extends Theme { static C = "#007591"; override sort(): number { return 105; @@ -201,7 +201,7 @@ export default function (GoThemes: GoThemesInterface) { override get theme_name(): string { return "HNG Night"; } - override getBackgroundCSS(): GoThemeBackgroundCSS { + override getBackgroundCSS(): ThemeBackgroundCSS { return { "background-color": "#090C1F", "background-image": "", @@ -228,16 +228,16 @@ export default function (GoThemes: GoThemesInterface) { } _("HNG Night"); // ensure translation exists - GoThemes["board"]["HNG Night"] = HNGNight; + THEMES["board"]["HNG Night"] = HNGNight; - class Book extends GoTheme { + class Book extends Theme { override sort(): number { return 110; } override get theme_name(): string { return "Book"; } - override getBackgroundCSS(): GoThemeBackgroundCSS { + override getBackgroundCSS(): ThemeBackgroundCSS { return { "background-color": "#ffffff", "background-image": "", @@ -264,5 +264,5 @@ export default function (GoThemes: GoThemesInterface) { } _("Book"); // ensure translation exists - GoThemes["board"]["Book"] = Book; + THEMES["board"]["Book"] = Book; } diff --git a/src/renderer/themes/board_woods.ts b/src/Goban/themes/board_woods.ts similarity index 86% rename from src/renderer/themes/board_woods.ts rename to src/Goban/themes/board_woods.ts index 43e83167..c4e8c826 100644 --- a/src/renderer/themes/board_woods.ts +++ b/src/Goban/themes/board_woods.ts @@ -14,8 +14,8 @@ * limitations under the License. */ -import { GoTheme, GoThemeBackgroundCSS } from "../GoTheme"; -import { GoThemesInterface } from "../GoThemes"; +import { Theme, ThemeBackgroundCSS } from "./Theme"; +import { ThemesInterface } from "./"; import { _ } from "engine/translate"; import { callbacks } from "../callbacks"; @@ -26,15 +26,15 @@ function getCDNReleaseBase() { return ""; } -export default function (GoThemes: GoThemesInterface) { - class Kaya extends GoTheme { +export default function (THEMES: ThemesInterface) { + class Kaya extends Theme { override sort(): number { return 10; } override get theme_name(): string { return "Kaya"; } - override getBackgroundCSS(): GoThemeBackgroundCSS { + override getBackgroundCSS(): ThemeBackgroundCSS { return { "background-color": "#DCB35C", "background-image": "url('" + getCDNReleaseBase() + "/img/kaya.jpg')", @@ -61,16 +61,16 @@ export default function (GoThemes: GoThemesInterface) { } _("Kaya"); // ensure translation - GoThemes["board"]["Kaya"] = Kaya; + THEMES["board"]["Kaya"] = Kaya; - class RedOak extends GoTheme { + class RedOak extends Theme { override sort(): number { return 20; } override get theme_name(): string { return "Red Oak"; } - override getBackgroundCSS(): GoThemeBackgroundCSS { + override getBackgroundCSS(): ThemeBackgroundCSS { return { "background-color": "#DCB35C", "background-image": "url('" + getCDNReleaseBase() + "/img/oak.jpg')", @@ -97,16 +97,16 @@ export default function (GoThemes: GoThemesInterface) { } _("Red Oak"); // ensure translation - GoThemes["board"]["Red Oak"] = RedOak; + THEMES["board"]["Red Oak"] = RedOak; - class Persimmon extends GoTheme { + class Persimmon extends Theme { override sort(): number { return 30; } override get theme_name(): string { return "Persimmon"; } - override getBackgroundCSS(): GoThemeBackgroundCSS { + override getBackgroundCSS(): ThemeBackgroundCSS { return { "background-color": "#DCB35C", "background-image": "url('" + getCDNReleaseBase() + "/img/persimmon.jpg')", @@ -133,16 +133,16 @@ export default function (GoThemes: GoThemesInterface) { } _("Persimmon"); // ensure translation - GoThemes["board"]["Persimmon"] = Persimmon; + THEMES["board"]["Persimmon"] = Persimmon; - class BlackWalnut extends GoTheme { + class BlackWalnut extends Theme { override sort(): number { return 40; } override get theme_name(): string { return "Black Walnut"; } - override getBackgroundCSS(): GoThemeBackgroundCSS { + override getBackgroundCSS(): ThemeBackgroundCSS { return { "background-color": "#DCB35C", "background-image": "url('" + getCDNReleaseBase() + "/img/black_walnut.jpg')", @@ -169,16 +169,16 @@ export default function (GoThemes: GoThemesInterface) { } _("Black Walnut"); // ensure translation - GoThemes["board"]["Black Walnut"] = BlackWalnut; + THEMES["board"]["Black Walnut"] = BlackWalnut; - class Granite extends GoTheme { + class Granite extends Theme { override sort(): number { return 40; } override get theme_name(): string { return "Granite"; } - override getBackgroundCSS(): GoThemeBackgroundCSS { + override getBackgroundCSS(): ThemeBackgroundCSS { return { "background-color": "#DCB35C", "background-image": "url('" + getCDNReleaseBase() + "/img/granite.jpg')", @@ -205,16 +205,16 @@ export default function (GoThemes: GoThemesInterface) { } _("Granite"); // ensure translation - GoThemes["board"]["Granite"] = Granite; + THEMES["board"]["Granite"] = Granite; - class Anime extends GoTheme { + class Anime extends Theme { override sort(): number { return 10; } override get theme_name(): string { return "Anime"; } - override getBackgroundCSS(): GoThemeBackgroundCSS { + override getBackgroundCSS(): ThemeBackgroundCSS { return { "background-color": "#DCB35C", "background-image": "url('" + getCDNReleaseBase() + "/img/anime_board.svg')", @@ -242,16 +242,16 @@ export default function (GoThemes: GoThemesInterface) { } _("Anime"); // ensure translation - GoThemes["board"]["Anime"] = Anime; + THEMES["board"]["Anime"] = Anime; - class BrightKaya extends GoTheme { + class BrightKaya extends Theme { override sort(): number { return 15; } override get theme_name(): string { return "Bright Kaya"; } - override getBackgroundCSS(): GoThemeBackgroundCSS { + override getBackgroundCSS(): ThemeBackgroundCSS { return { "background-color": "#DBB25B", "background-image": "url('" + getCDNReleaseBase() + "/img/kaya.jpg')", @@ -278,5 +278,5 @@ export default function (GoThemes: GoThemesInterface) { } _("Bright Kaya"); // ensure translation - GoThemes["board"]["Bright Kaya"] = BrightKaya; + THEMES["board"]["Bright Kaya"] = BrightKaya; } diff --git a/src/renderer/themes/image_stones.ts b/src/Goban/themes/image_stones.ts similarity index 97% rename from src/renderer/themes/image_stones.ts rename to src/Goban/themes/image_stones.ts index e6b2d76b..f392749e 100644 --- a/src/renderer/themes/image_stones.ts +++ b/src/Goban/themes/image_stones.ts @@ -14,8 +14,8 @@ * limitations under the License. */ -import { GoTheme } from "../GoTheme"; -import { GoThemesInterface } from "../GoThemes"; +import { Theme } from "./Theme"; +import { ThemesInterface } from "./"; import { _ } from "engine/translate"; import { deviceCanvasScalingRatio, allocateCanvasOrError } from "../canvas_utils"; import { renderShadow } from "./rendered_stones"; @@ -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,7 +167,7 @@ export default function (GoThemes: GoThemesInterface) { // ignore } - class Common extends GoTheme { + class Common extends Theme { override stoneCastsShadow(radius: number): boolean { return stoneCastsShadow(radius * deviceCanvasScalingRatio()); } @@ -300,8 +300,8 @@ 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 { override sort() { @@ -453,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..f1e08152 --- /dev/null +++ b/src/Goban/themes/index.ts @@ -0,0 +1,61 @@ +/* + * 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 { Theme } from "./Theme"; + +import { Theme } from "./Theme"; + +export interface ThemesInterface { + white: { [name: string]: typeof Theme }; + black: { [name: string]: typeof Theme }; + board: { [name: string]: typeof Theme }; +} + +export const THEMES: ThemesInterface = { + white: {}, + black: {}, + board: {}, +}; +export const THEMES_SORTED: { + white: Theme[]; + black: Theme[]; + board: Theme[]; +} = { 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: Theme, b: Theme) { + 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/renderer/themes/plain_stones.ts b/src/Goban/themes/plain_stones.ts similarity index 95% rename from src/renderer/themes/plain_stones.ts rename to src/Goban/themes/plain_stones.ts index 05277120..dd615f3d 100644 --- a/src/renderer/themes/plain_stones.ts +++ b/src/Goban/themes/plain_stones.ts @@ -14,8 +14,8 @@ * limitations under the License. */ -import { GoTheme } from "../GoTheme"; -import { GoThemesInterface } from "../GoThemes"; +import { Theme } from "./Theme"; +import { ThemesInterface } from "./"; import { _ } from "engine/translate"; export function renderPlainStone( @@ -50,8 +50,8 @@ export function renderPlainStone( ctx.fill(); } -export default function (GoThemes: GoThemesInterface) { - class Stone extends GoTheme { +export default function (THEMES: ThemesInterface) { + class Stone extends Theme { override sort(): number { return 1; } @@ -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/renderer/themes/rendered_stones.ts b/src/Goban/themes/rendered_stones.ts similarity index 98% rename from src/renderer/themes/rendered_stones.ts rename to src/Goban/themes/rendered_stones.ts index 7c444a3f..e5fe4de0 100644 --- a/src/renderer/themes/rendered_stones.ts +++ b/src/Goban/themes/rendered_stones.ts @@ -14,8 +14,8 @@ * limitations under the License. */ -import { GoTheme } from "../GoTheme"; -import { GoThemesInterface } from "../GoThemes"; +import { Theme } from "./Theme"; +import { ThemesInterface } from "./"; import { _ } from "engine/translate"; import { deviceCanvasScalingRatio, allocateCanvasOrError } from "../canvas_utils"; @@ -477,8 +477,8 @@ function stoneCastsShadow(radius: number): boolean { return radius >= 10; } -export default function (GoThemes: GoThemesInterface) { - class Common extends GoTheme { +export default function (THEMES: ThemesInterface) { + class Common extends Theme { override stoneCastsShadow(radius: number): boolean { return stoneCastsShadow(radius * deviceCanvasScalingRatio()); } @@ -562,7 +562,7 @@ export default function (GoThemes: GoThemesInterface) { } _("Slate"); // ensure translation - GoThemes["black"]["Slate"] = Slate; + THEMES["black"]["Slate"] = Slate; class Shell extends Common { override sort() { @@ -712,7 +712,7 @@ export default function (GoThemes: GoThemesInterface) { } } _("Shell"); // ensure translation - GoThemes["white"]["Shell"] = Shell; + THEMES["white"]["Shell"] = Shell; /* Glass { */ @@ -830,8 +830,8 @@ 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 { */ @@ -949,8 +949,8 @@ 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 { @@ -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 index d819b1b7..781e527e 100644 --- a/src/GobanBase.ts +++ b/src/GobanBase.ts @@ -41,7 +41,7 @@ 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 "./renderer/callbacks"; +import { setGobanCallbacks } from "./Goban/callbacks"; let last_goban_id = 0; diff --git a/src/__tests__/GoEngine_sgf.test.ts b/src/__tests__/GoEngine_sgf.test.ts index fc36fe72..145f933a 100644 --- a/src/__tests__/GoEngine_sgf.test.ts +++ b/src/__tests__/GoEngine_sgf.test.ts @@ -11,7 +11,7 @@ (global as any).CLIENT = true; -import { TestGoban } from "../renderer/TestGoban"; +import { TestGoban } from "../Goban/TestGoban"; import { MoveTree } from "../engine"; type SGFTestcase = { diff --git a/src/__tests__/GobanCanvas.test.ts b/src/__tests__/GobanCanvas.test.ts index 4f8e5df3..dbae8699 100644 --- a/src/__tests__/GobanCanvas.test.ts +++ b/src/__tests__/GobanCanvas.test.ts @@ -6,8 +6,8 @@ (global as any).CLIENT = true; -import { GobanCanvas, GobanCanvasConfig } from "../renderer/GobanCanvas"; -import { SCORE_ESTIMATION_TOLERANCE, SCORE_ESTIMATION_TRIALS } from "../renderer/GobanInteractive"; +import { GobanCanvas, CanvasRendererGobanConfig } from "../Goban/CanvasRenderer"; +import { SCORE_ESTIMATION_TOLERANCE, SCORE_ESTIMATION_TRIALS } from "../Goban/InteractiveBase"; import { GobanSocket, GoMath } from "../engine"; import { GobanBase } from "../GobanBase"; import WS from "jest-websocket-mock"; @@ -38,11 +38,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 +51,9 @@ function basic3x3Config(additionalOptions?: GobanCanvasConfig): GobanCanvasConfi }; } -function basicScorableBoardConfig(additionalOptions?: GobanCanvasConfig): GobanCanvasConfig { +function basicScorableBoardConfig( + additionalOptions?: CanvasRendererGobanConfig, +): CanvasRendererGobanConfig { return { ...commonConfig(), width: 4, diff --git a/src/__tests__/GobanCore_conditional_moves.test.ts b/src/__tests__/GobanCore_conditional_moves.test.ts index 9c8c8be2..a4152554 100644 --- a/src/__tests__/GobanCore_conditional_moves.test.ts +++ b/src/__tests__/GobanCore_conditional_moves.test.ts @@ -11,7 +11,7 @@ (global as any).CLIENT = true; -import { TestGoban } from "../renderer/TestGoban"; +import { TestGoban } from "../Goban/TestGoban"; test("call FollowConditionalPath", () => { const goban = new TestGoban({ moves: [] }); diff --git a/src/__tests__/GobanSVG.test.ts b/src/__tests__/GobanSVG.test.ts index cabb762f..9269d112 100644 --- a/src/__tests__/GobanSVG.test.ts +++ b/src/__tests__/GobanSVG.test.ts @@ -6,8 +6,8 @@ (global as any).CLIENT = true; -import { GobanSVG, GobanSVGConfig } from "../renderer/GobanSVG"; -import { SCORE_ESTIMATION_TOLERANCE, SCORE_ESTIMATION_TRIALS } from "../renderer/GobanInteractive"; +import { SVGRenderer, SVGRendererGobanConfig } from "../Goban/SVGRenderer"; +import { SCORE_ESTIMATION_TOLERANCE, SCORE_ESTIMATION_TRIALS } from "../Goban/InteractiveBase"; import { GobanSocket, GoMath } from "../engine"; import { GobanBase } from "../GobanBase"; import WS from "jest-websocket-mock"; @@ -38,11 +38,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 +51,9 @@ function basic3x3Config(additionalOptions?: GobanSVGConfig): GobanSVGConfig { }; } -function basicScorableBoardConfig(additionalOptions?: GobanSVGConfig): GobanSVGConfig { +function basicScorableBoardConfig( + additionalOptions?: SVGRendererGobanConfig, +): SVGRendererGobanConfig { return { ...commonConfig(), width: 4, @@ -100,7 +102,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 +115,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 +129,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 +143,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 +179,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 +195,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 +215,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 +251,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 +284,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 +304,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 @@ -326,7 +328,7 @@ describe("onTap", () => { }); test("Shift-Clicking during stone removal toggles the group ", 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 @@ -361,7 +363,7 @@ 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(); @@ -385,7 +387,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 }); @@ -406,7 +408,7 @@ describe("onTap", () => { }); 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 @@ -441,7 +443,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__/autoscore.test.ts b/src/__tests__/autoscore.test.ts index ab4fef90..938b01a2 100644 --- a/src/__tests__/autoscore.test.ts +++ b/src/__tests__/autoscore.test.ts @@ -1,5 +1,5 @@ import { readFileSync, readdirSync } from "fs"; -import { autoscore } from "engine"; +import { autoscore } from "../engine"; describe("Auto-score tests ", () => { const files = readdirSync("test/autoscore_test_files"); diff --git a/src/engine/BoardState.ts b/src/engine/BoardState.ts index 2b0f5dca..57fb3c91 100644 --- a/src/engine/BoardState.ts +++ b/src/engine/BoardState.ts @@ -23,7 +23,7 @@ import { StoneStringBuilder } from "./StoneStringBuilder"; import type { GobanBase } from "../GobanBase"; import { RawStoneString } from "./StoneString"; import { cloneMatrix, matricesAreEqual } from "./util"; -import { callbacks } from "../renderer/callbacks"; +import { callbacks } from "../Goban/callbacks"; export interface BoardConfig { width?: number; diff --git a/src/index.ts b/src/index.ts index 5ab6cb91..2a75a83d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -15,26 +15,25 @@ */ export * from "engine"; -export * from "./renderer/callbacks"; -export * from "./renderer/canvas_utils"; -export * from "./renderer/GobanCanvas"; export * from "./GobanBase"; -export * from "./renderer/GobanSVG"; -export * from "./renderer/GoTheme"; -export * from "./renderer/GoThemes"; -export * from "./renderer/Goban"; -export * from "./renderer/TestGoban"; +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"; export * as protocol from "engine/protocol"; -export { placeRenderedImageStone, preRenderImageStone } from "./renderer/themes/image_stones"; +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 "./renderer/GobanCanvas"; -import { GobanSVG, GobanSVGConfig } from "./renderer/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; @@ -51,7 +50,7 @@ export function createGoban( 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/renderer/GoThemes.ts b/src/renderer/GoThemes.ts deleted file mode 100644 index 2685a539..00000000 --- a/src/renderer/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); -} From faa21989c2703f24e227d259cb1539482d28ea04 Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Sat, 15 Jun 2024 14:29:07 -0600 Subject: [PATCH 40/68] Refactor: Goban.Theme -> GobanTheme because typescript doesn't like the former --- src/Goban/CanvasRenderer.ts | 8 +- src/Goban/Goban.ts | 3 +- src/Goban/SVGRenderer.ts | 8 +- src/Goban/themes/Theme.ts | 436 ---------------------------- src/Goban/themes/board_plain.ts | 26 +- src/Goban/themes/board_woods.ts | 30 +- src/Goban/themes/image_stones.ts | 4 +- src/Goban/themes/index.ts | 24 +- src/Goban/themes/plain_stones.ts | 4 +- src/Goban/themes/rendered_stones.ts | 4 +- 10 files changed, 58 insertions(+), 489 deletions(-) delete mode 100644 src/Goban/themes/Theme.ts diff --git a/src/Goban/CanvasRenderer.ts b/src/Goban/CanvasRenderer.ts index 4a0f12cb..9f8e72ab 100644 --- a/src/Goban/CanvasRenderer.ts +++ b/src/Goban/CanvasRenderer.ts @@ -22,7 +22,7 @@ import { GobanConfig } from "../GobanBase"; import { GoEngine } from "engine"; import * as GoMath from "engine/GoMath"; import { MoveTree } from "engine/MoveTree"; -import { Theme, THEMES } from "./themes"; +import { GobanTheme, THEMES } from "./themes"; import { MoveTreePenMarks } from "engine/MoveTree"; import { createDeviceScaledCanvas, @@ -133,18 +133,18 @@ export class GobanCanvas extends Goban implements GobanCanvasInterface { black: "Plain", white: "Plain", }; - private theme_black!: Theme; + 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!: Theme; + 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!: Theme; + private theme_white!: GobanTheme; private theme_white_stones: Array = []; private theme_white_text_color: string = HOT_PINK; diff --git a/src/Goban/Goban.ts b/src/Goban/Goban.ts index 325fa5fd..0b33a8a2 100644 --- a/src/Goban/Goban.ts +++ b/src/Goban/Goban.ts @@ -20,7 +20,7 @@ import { GobanConfig } from "../GobanBase"; import { callbacks } from "./callbacks"; import { makeMatrix, StoneStringBuilder } from "engine"; import { getRelativeEventPosition } from "./canvas_utils"; -import { THEMES, THEMES_SORTED, Theme } from "./themes"; +import { THEMES, THEMES_SORTED } from "./themes"; export const GOBAN_FONT = "Verdana,Arial,sans-serif"; export interface GobanSelectedThemes { @@ -53,7 +53,6 @@ export interface GobanMetrics { export abstract class Goban extends OGSConnectivity { static THEMES = THEMES; static THEMES_SORTED = THEMES_SORTED; - static Theme = Theme; protected abstract setTheme(themes: GobanSelectedThemes, dont_redraw: boolean): void; diff --git a/src/Goban/SVGRenderer.ts b/src/Goban/SVGRenderer.ts index 62de85d7..e0402072 100644 --- a/src/Goban/SVGRenderer.ts +++ b/src/Goban/SVGRenderer.ts @@ -23,7 +23,7 @@ import { GobanConfig } from "../GobanBase"; import { GoEngine } from "engine"; import * as GoMath from "engine/GoMath"; import { MoveTree } from "engine/MoveTree"; -import { Theme, THEMES } from "./themes"; +import { GobanTheme, THEMES } from "./themes"; import { MoveTreePenMarks } from "engine/MoveTree"; import { getRelativeEventPosition } from "./canvas_utils"; import { _ } from "engine/translate"; @@ -138,18 +138,18 @@ export class SVGRenderer extends Goban implements GobanSVGInterface { black: "Plain", white: "Plain", }; - private theme_black!: Theme; + 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!: Theme; + 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!: Theme; + private theme_white!: GobanTheme; private theme_white_stones: Array = []; private theme_white_text_color: string = HOT_PINK; diff --git a/src/Goban/themes/Theme.ts b/src/Goban/themes/Theme.ts deleted file mode 100644 index 66d800c0..00000000 --- a/src/Goban/themes/Theme.ts +++ /dev/null @@ -1,436 +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 { GobanBase } from "../../GobanBase"; - -export interface ThemeBackgroundCSS { - "background-color"?: string; - "background-image"?: string; - "background-size"?: string; -} - -export interface ThemeBackgroundReactStyles { - backgroundColor?: string; - backgroundImage?: string; - backgroundSize?: string; -} - -export interface SVGStop { - offset: number; - color: string; -} - -export interface SVGStoneParameters { - id: string; - fill?: string; - stroke?: string; - stroke_scale?: number; // scale the radius by this amount - gradient?: { - stops: SVGStop[]; - type?: "radial" | "linear"; // default radial - x1?: number; - x2?: number; - y1?: number; - y2?: number; - cx?: number; - cy?: number; - r?: number; - fx?: number; - fy?: number; - }; - url?: string; -} - -export class Theme { - public name: string; - public styles: { [style_name: string]: string } = {}; - protected parent?: Theme; // An optional parent theme - - constructor(parent?: Theme) { - this.name = `[ERROR theme missing name]`; - this.parent = parent; - } - - get theme_name(): string { - return "ERROR missing theme_name"; - } - public sort(): number { - return 0; - } - - /* Returns an array of black stone objects. The structure - * of the array elements is up to the implementor, as they are passed - * verbatim to the placeBlackStone method */ - public preRenderBlack( - _radius: number, - _seed: number, - _deferredRenderCallback: () => void, - ): any { - return { black: "stone" }; - } - - /* Returns an array of white stone objects. The structure - * of the array elements is up to the implementor, as they are passed - * verbatim to the placeWhiteStone method */ - public preRenderWhite( - _radius: number, - _seed: number, - _deferredRenderCallback: () => void, - ): any { - return { white: "stone" }; - } - - /* Returns an array of black stone objects. The structure - * of the array elements is up to the implementor, as they are passed - * verbatim to the placeBlackStone method */ - public preRenderBlackSVG( - defs: SVGDefsElement, - radius: number, - _seed: number, - _deferredRenderCallback: () => void, - ): string[] { - const ret = []; - const key = `black-${radius}`; - ret.push(key); - - defs.appendChild( - this.renderSVG( - { - id: key, - //fill: "hsl(8, 7%, 10%)", - //stroke: "hsl(8, 7%, 10%)", - fill: this.getBlackStoneColor(), - stroke: this.getBlackStoneColor(), - }, - radius, - ), - ); - return ret; - } - - /* Returns an array of white stone objects. The structure - * of the array elements is up to the implementor, as they are passed - * verbatim to the placeWhiteStone method */ - public preRenderWhiteSVG( - defs: SVGDefsElement, - radius: number, - _seed: number, - _deferredRenderCallback: () => void, - ): string[] { - const ret = []; - const key = `white-${radius}`; - ret.push(key); - defs.appendChild( - this.renderSVG( - { - id: key, - //fill: "hsl(8, 7%, 90%)", - //stroke: "hsl(8, 7%, 30%)", - fill: this.getWhiteStoneColor(), - stroke: this.getBlackStoneColor(), - }, - radius, - ), - ); - return ret; - } - - /* Places a pre rendered stone onto the canvas, centered at cx, cy */ - public placeWhiteStone( - ctx: CanvasRenderingContext2D, - _shadow_ctx: CanvasRenderingContext2D | null, - _stone: any, - cx: number, - cy: number, - radius: number, - ) { - //if (shadow_ctx) do something - ctx.fillStyle = this.getWhiteStoneColor(); - ctx.beginPath(); - ctx.arc(cx, cy, Math.max(0.1, radius), 0, 2 * Math.PI, true); - ctx.fill(); - } - - public placeBlackStone( - ctx: CanvasRenderingContext2D, - _shadow_ctx: CanvasRenderingContext2D | null, - _stone: any, - cx: number, - cy: number, - radius: number, - ) { - //if (shadow_ctx) do something - ctx.fillStyle = this.getBlackStoneColor(); - ctx.beginPath(); - ctx.arc(cx, cy, Math.max(0.1, radius), 0, 2 * Math.PI, true); - ctx.fill(); - } - - public placeStoneShadowSVG( - shadow_cell: SVGGraphicsElement | undefined, - cx: number, - cy: number, - radius: number, - color: string, - ): SVGElement | undefined { - if (!shadow_cell) { - return; - } - - const invisible_circle_to_cast_shadow = document.createElementNS( - "http://www.w3.org/2000/svg", - "circle", - ); - invisible_circle_to_cast_shadow.setAttribute("class", "shadow"); - invisible_circle_to_cast_shadow.setAttribute("fill", color); - invisible_circle_to_cast_shadow.setAttribute("cx", cx.toString()); - invisible_circle_to_cast_shadow.setAttribute("cy", cy.toString()); - invisible_circle_to_cast_shadow.setAttribute("r", (radius * 0.97).toString()); - const sx = radius * 0.15; - const sy = radius * 0.15; - const softness = radius * 0.1; - invisible_circle_to_cast_shadow.setAttribute( - "style", - `filter: drop-shadow(${sx}px ${sy}px ${softness}px rgba(0,0,0,0.4)`, - ); - shadow_cell.appendChild(invisible_circle_to_cast_shadow); - return invisible_circle_to_cast_shadow; - } - - public placeWhiteStoneSVG( - cell: SVGGraphicsElement, - shadow_cell: SVGGraphicsElement | undefined, - stone: string, - cx: number, - cy: number, - radius: number, - ): [SVGElement, SVGElement | undefined] { - const shadow = this.placeStoneShadowSVG(shadow_cell, cx, cy, radius, "#eeeeee"); - - 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 [ref, shadow]; - } - - public placeBlackStoneSVG( - cell: SVGGraphicsElement, - shadow_cell: SVGGraphicsElement | undefined, - stone: string, - cx: number, - 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 [ref, shadow]; - } - - /* 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: GobanBase): any { - const ret = Array.isArray(stones) - ? stones[((x + 1) * 53 * ((y + 1) * 97)) % stones.length] - : stones; - - if (!ret) { - console.error("No stone returned for ", x, y, stones); - throw new Error("Failed to get stone for " + x + ", " + y); - } - - return ret; - } - - /* 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: GobanBase): string { - if (Array.isArray(stones)) { - return "" + (((x + 1) * 53 * ((y + 1) * 97)) % stones.length); - } - return ""; - } - - /* Should return true if you would like the shadow layer to be present. False - * speeds up rendering typically */ - public stoneCastsShadow(_radius: number): boolean { - return false; - } - - /* Returns the color that should be used for white stones */ - public getWhiteStoneColor(): string { - return "#ffffff"; - } - - /* Returns the color that should be used for black stones */ - public getBlackStoneColor(): string { - return "#000000"; - } - - /* Returns the color that should be used for text over white stones */ - public getWhiteTextColor(_color?: string): string { - return "#000000"; - } - - /* Returns the color that should be used for text over black stones */ - public getBlackTextColor(_color?: string): string { - return "#ffffff"; - } - - /* Returns a set of CSS styles that should be applied to the background layer (ie the board) */ - public getBackgroundCSS(): ThemeBackgroundCSS { - return { - "background-color": "#DCB35C", - "background-image": "", - }; - } - - /* Returns a set of CSS styles (for react) that should be applied to the background layer (ie the board) */ - public getReactStyles(): ThemeBackgroundReactStyles { - const ret: ThemeBackgroundReactStyles = {}; - const css: ThemeBackgroundCSS = this.getBackgroundCSS(); - - ret.backgroundColor = css["background-color"]; - ret.backgroundImage = css["background-image"]; - - return ret; - } - - /* Returns the color that should be used for lines */ - public getLineColor(): string { - return "#000000"; - } - - /* Returns the color that should be used for lines * when there is text over the square */ - public getFadedLineColor(): string { - return "#888888"; - } - - /* Returns the color that should be used for star points */ - public getStarColor(): string { - return "#000000"; - } - - /* Returns the color that should be used for star points - * when there is text over the square */ - public getFadedStarColor(): string { - return "#888888"; - } - - /* Returns the color that text should be over empty intersections */ - public getBlankTextColor(): string { - return "#000000"; - } - - /** Returns the color that should be used for labels */ - public getLabelTextColor(): string { - return "#000000"; - } - - public renderSVG(params: SVGStoneParameters, radius: number): SVGGraphicsElement { - const cx = radius; - const cy = radius; - - const stone = document.createElementNS("http://www.w3.org/2000/svg", "g"); - stone.setAttribute("id", params.id); - stone.setAttribute("class", "stone"); - - if (params.fill || params.stroke || params.gradient) { - const circle = document.createElementNS("http://www.w3.org/2000/svg", "circle"); - stone.appendChild(circle); - if (params.fill) { - circle.setAttribute("fill", params.fill); - } - let stroke_width = 0.0; - if (params.stroke) { - circle.setAttribute("stroke", params.stroke); - if (params.stroke_scale) { - stroke_width = radius * params.stroke_scale; - } else { - stroke_width = radius / 20; - } - circle.setAttribute("stroke-width", `${stroke_width.toFixed(1)}px`); - } - circle.setAttribute("cx", cx.toString()); - circle.setAttribute("cy", cy.toString()); - circle.setAttribute("r", (radius - stroke_width * 0.5).toString()); - circle.setAttribute("shape-rendering", "geometricPrecision"); - - // gradient - if (params.gradient) { - const grad = params.gradient; - const defs = document.createElementNS("http://www.w3.org/2000/svg", "defs"); - - let gradient; - - if (grad.type === "linear") { - gradient = document.createElementNS( - "http://www.w3.org/2000/svg", - "linearGradient", - ); - gradient.setAttribute("x1", (grad.x1 ?? 0.0).toFixed(2)); - gradient.setAttribute("y1", (grad.y1 ?? 0.0).toFixed(2)); - gradient.setAttribute("x2", (grad.x2 ?? 1.0).toFixed(2)); - gradient.setAttribute("y2", (grad.y2 ?? 1.0).toFixed(2)); - } else { - gradient = document.createElementNS( - "http://www.w3.org/2000/svg", - params.gradient.type === "linear" ? "linearGradient" : "radialGradient", - ); - gradient.setAttribute("cx", (grad.cx ?? 0.5).toFixed(2)); - gradient.setAttribute("cy", (grad.cy ?? 0.5).toFixed(2)); - gradient.setAttribute("r", (grad.r ?? 0.5).toFixed(2)); - gradient.setAttribute("fx", (grad.fx ?? 0.3).toFixed(2)); - gradient.setAttribute("fy", (grad.fy ?? 0.2).toFixed(2)); - } - gradient.setAttribute("id", params.id + "-gradient"); - - for (const stop of params.gradient.stops) { - const s = document.createElementNS("http://www.w3.org/2000/svg", "stop"); - s.setAttribute("offset", `${stop.offset}%`); - s.setAttribute("stop-color", stop.color); - gradient.appendChild(s); - } - defs.appendChild(gradient); - stone.appendChild(defs); - circle.setAttribute("fill", `url(#${params.id}-gradient)`); - } - } - - if (params.url) { - const stone_image = document.createElementNS("http://www.w3.org/2000/svg", "image"); - stone_image.setAttribute("class", "stone"); - stone_image.setAttribute("x", `${cx - radius}`); - stone_image.setAttribute("y", `${cy - radius}`); - stone_image.setAttribute("width", `${radius * 2}`); - stone_image.setAttribute("height", `${radius * 2}`); - stone_image.setAttributeNS("http://www.w3.org/1999/xlink", "href", params.url); - stone.appendChild(stone_image); - } - - return stone; - } -} diff --git a/src/Goban/themes/board_plain.ts b/src/Goban/themes/board_plain.ts index e1dd7096..942f4e8e 100644 --- a/src/Goban/themes/board_plain.ts +++ b/src/Goban/themes/board_plain.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { Theme, ThemeBackgroundCSS } from "./Theme"; +import { GobanTheme, GobanThemeBackgroundCSS } from "./GobanTheme"; import { ThemesInterface } from "./"; import { callbacks } from "../callbacks"; import { _ } from "engine/translate"; @@ -32,14 +32,14 @@ function hexToRgba(raw: string, alpha: number = 1): string { } export default function (THEMES: ThemesInterface) { - class Plain extends Theme { + class Plain extends GobanTheme { override sort(): number { return 1; } override get theme_name(): string { return "Plain"; } - override getBackgroundCSS(): ThemeBackgroundCSS { + override getBackgroundCSS(): GobanThemeBackgroundCSS { return { "background-color": "#DCB35C", "background-image": "", @@ -68,14 +68,14 @@ export default function (THEMES: ThemesInterface) { _("Plain"); // ensure translation exists THEMES["board"]["Plain"] = Plain; - class Custom extends Theme { + class Custom extends GobanTheme { override sort(): number { return 200; //last, because this is the "customisable" one } override get theme_name(): string { return "Custom"; } - override getBackgroundCSS(): ThemeBackgroundCSS { + override getBackgroundCSS(): GobanThemeBackgroundCSS { return { "background-color": callbacks.customBoardColor ? callbacks.customBoardColor() @@ -119,14 +119,14 @@ export default function (THEMES: ThemesInterface) { _("Custom"); // ensure translation exists THEMES["board"]["Custom"] = Custom; - class Night extends Theme { + class Night extends GobanTheme { override sort(): number { return 100; } override get theme_name(): string { return "Night Play"; } - override getBackgroundCSS(): ThemeBackgroundCSS { + override getBackgroundCSS(): GobanThemeBackgroundCSS { return { "background-color": "#444444", "background-image": "", @@ -155,7 +155,7 @@ export default function (THEMES: ThemesInterface) { _("Night Play"); // ensure translation exists THEMES["board"]["Night Play"] = Night; - class HNG extends Theme { + class HNG extends GobanTheme { static C = "#00193E"; static C2 = "#004C75"; override sort(): number { @@ -164,7 +164,7 @@ export default function (THEMES: ThemesInterface) { override get theme_name(): string { return "HNG"; } - override getBackgroundCSS(): ThemeBackgroundCSS { + override getBackgroundCSS(): GobanThemeBackgroundCSS { return { "background-color": "#00e7fc", "background-image": "", @@ -193,7 +193,7 @@ export default function (THEMES: ThemesInterface) { _("HNG"); // ensure translation exists THEMES["board"]["HNG"] = HNG; - class HNGNight extends Theme { + class HNGNight extends GobanTheme { static C = "#007591"; override sort(): number { return 105; @@ -201,7 +201,7 @@ export default function (THEMES: ThemesInterface) { override get theme_name(): string { return "HNG Night"; } - override getBackgroundCSS(): ThemeBackgroundCSS { + override getBackgroundCSS(): GobanThemeBackgroundCSS { return { "background-color": "#090C1F", "background-image": "", @@ -230,14 +230,14 @@ export default function (THEMES: ThemesInterface) { _("HNG Night"); // ensure translation exists THEMES["board"]["HNG Night"] = HNGNight; - class Book extends Theme { + class Book extends GobanTheme { override sort(): number { return 110; } override get theme_name(): string { return "Book"; } - override getBackgroundCSS(): ThemeBackgroundCSS { + override getBackgroundCSS(): GobanThemeBackgroundCSS { return { "background-color": "#ffffff", "background-image": "", diff --git a/src/Goban/themes/board_woods.ts b/src/Goban/themes/board_woods.ts index c4e8c826..a8d4426d 100644 --- a/src/Goban/themes/board_woods.ts +++ b/src/Goban/themes/board_woods.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { Theme, ThemeBackgroundCSS } from "./Theme"; +import { GobanTheme, GobanThemeBackgroundCSS } from "./GobanTheme"; import { ThemesInterface } from "./"; import { _ } from "engine/translate"; import { callbacks } from "../callbacks"; @@ -27,14 +27,14 @@ function getCDNReleaseBase() { } export default function (THEMES: ThemesInterface) { - class Kaya extends Theme { + class Kaya extends GobanTheme { override sort(): number { return 10; } override get theme_name(): string { return "Kaya"; } - override getBackgroundCSS(): ThemeBackgroundCSS { + override getBackgroundCSS(): GobanThemeBackgroundCSS { return { "background-color": "#DCB35C", "background-image": "url('" + getCDNReleaseBase() + "/img/kaya.jpg')", @@ -63,14 +63,14 @@ export default function (THEMES: ThemesInterface) { _("Kaya"); // ensure translation THEMES["board"]["Kaya"] = Kaya; - class RedOak extends Theme { + class RedOak extends GobanTheme { override sort(): number { return 20; } override get theme_name(): string { return "Red Oak"; } - override getBackgroundCSS(): ThemeBackgroundCSS { + override getBackgroundCSS(): GobanThemeBackgroundCSS { return { "background-color": "#DCB35C", "background-image": "url('" + getCDNReleaseBase() + "/img/oak.jpg')", @@ -99,14 +99,14 @@ export default function (THEMES: ThemesInterface) { _("Red Oak"); // ensure translation THEMES["board"]["Red Oak"] = RedOak; - class Persimmon extends Theme { + class Persimmon extends GobanTheme { override sort(): number { return 30; } override get theme_name(): string { return "Persimmon"; } - override getBackgroundCSS(): ThemeBackgroundCSS { + override getBackgroundCSS(): GobanThemeBackgroundCSS { return { "background-color": "#DCB35C", "background-image": "url('" + getCDNReleaseBase() + "/img/persimmon.jpg')", @@ -135,14 +135,14 @@ export default function (THEMES: ThemesInterface) { _("Persimmon"); // ensure translation THEMES["board"]["Persimmon"] = Persimmon; - class BlackWalnut extends Theme { + class BlackWalnut extends GobanTheme { override sort(): number { return 40; } override get theme_name(): string { return "Black Walnut"; } - override getBackgroundCSS(): ThemeBackgroundCSS { + override getBackgroundCSS(): GobanThemeBackgroundCSS { return { "background-color": "#DCB35C", "background-image": "url('" + getCDNReleaseBase() + "/img/black_walnut.jpg')", @@ -171,14 +171,14 @@ export default function (THEMES: ThemesInterface) { _("Black Walnut"); // ensure translation THEMES["board"]["Black Walnut"] = BlackWalnut; - class Granite extends Theme { + class Granite extends GobanTheme { override sort(): number { return 40; } override get theme_name(): string { return "Granite"; } - override getBackgroundCSS(): ThemeBackgroundCSS { + override getBackgroundCSS(): GobanThemeBackgroundCSS { return { "background-color": "#DCB35C", "background-image": "url('" + getCDNReleaseBase() + "/img/granite.jpg')", @@ -207,14 +207,14 @@ export default function (THEMES: ThemesInterface) { _("Granite"); // ensure translation THEMES["board"]["Granite"] = Granite; - class Anime extends Theme { + class Anime extends GobanTheme { override sort(): number { return 10; } override get theme_name(): string { return "Anime"; } - override getBackgroundCSS(): ThemeBackgroundCSS { + override getBackgroundCSS(): GobanThemeBackgroundCSS { return { "background-color": "#DCB35C", "background-image": "url('" + getCDNReleaseBase() + "/img/anime_board.svg')", @@ -244,14 +244,14 @@ export default function (THEMES: ThemesInterface) { _("Anime"); // ensure translation THEMES["board"]["Anime"] = Anime; - class BrightKaya extends Theme { + class BrightKaya extends GobanTheme { override sort(): number { return 15; } override get theme_name(): string { return "Bright Kaya"; } - override getBackgroundCSS(): ThemeBackgroundCSS { + override getBackgroundCSS(): GobanThemeBackgroundCSS { return { "background-color": "#DBB25B", "background-image": "url('" + getCDNReleaseBase() + "/img/kaya.jpg')", diff --git a/src/Goban/themes/image_stones.ts b/src/Goban/themes/image_stones.ts index f392749e..b6c1d980 100644 --- a/src/Goban/themes/image_stones.ts +++ b/src/Goban/themes/image_stones.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { Theme } from "./Theme"; +import { GobanTheme } from "./GobanTheme"; import { ThemesInterface } from "./"; import { _ } from "engine/translate"; import { deviceCanvasScalingRatio, allocateCanvasOrError } from "../canvas_utils"; @@ -167,7 +167,7 @@ export default function (THEMES: ThemesInterface) { // ignore } - class Common extends Theme { + class Common extends GobanTheme { override stoneCastsShadow(radius: number): boolean { return stoneCastsShadow(radius * deviceCanvasScalingRatio()); } diff --git a/src/Goban/themes/index.ts b/src/Goban/themes/index.ts index f1e08152..93c80787 100644 --- a/src/Goban/themes/index.ts +++ b/src/Goban/themes/index.ts @@ -14,14 +14,17 @@ * limitations under the License. */ -export { Theme } from "./Theme"; +export { GobanTheme } from "./GobanTheme"; -import { Theme } from "./Theme"; +import { GobanTheme } from "./GobanTheme"; export interface ThemesInterface { - white: { [name: string]: typeof Theme }; - black: { [name: string]: typeof Theme }; - board: { [name: string]: typeof Theme }; + 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 = { @@ -30,9 +33,12 @@ export const THEMES: ThemesInterface = { board: {}, }; export const THEMES_SORTED: { - white: Theme[]; - black: Theme[]; - board: Theme[]; + 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"; @@ -47,7 +53,7 @@ init_plain_stones(THEMES); init_rendered(THEMES); init_image_stones(THEMES); -function theme_sort(a: Theme, b: Theme) { +function theme_sort(a: GobanTheme, b: GobanTheme) { return a.sort() - b.sort(); } diff --git a/src/Goban/themes/plain_stones.ts b/src/Goban/themes/plain_stones.ts index dd615f3d..ec276050 100644 --- a/src/Goban/themes/plain_stones.ts +++ b/src/Goban/themes/plain_stones.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { Theme } from "./Theme"; +import { GobanTheme } from "./GobanTheme"; import { ThemesInterface } from "./"; import { _ } from "engine/translate"; @@ -51,7 +51,7 @@ export function renderPlainStone( } export default function (THEMES: ThemesInterface) { - class Stone extends Theme { + class Stone extends GobanTheme { override sort(): number { return 1; } diff --git a/src/Goban/themes/rendered_stones.ts b/src/Goban/themes/rendered_stones.ts index e5fe4de0..a9d4291a 100644 --- a/src/Goban/themes/rendered_stones.ts +++ b/src/Goban/themes/rendered_stones.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { Theme } from "./Theme"; +import { GobanTheme } from "./GobanTheme"; import { ThemesInterface } from "./"; import { _ } from "engine/translate"; import { deviceCanvasScalingRatio, allocateCanvasOrError } from "../canvas_utils"; @@ -478,7 +478,7 @@ function stoneCastsShadow(radius: number): boolean { } export default function (THEMES: ThemesInterface) { - class Common extends Theme { + class Common extends GobanTheme { override stoneCastsShadow(radius: number): boolean { return stoneCastsShadow(radius * deviceCanvasScalingRatio()); } From 90b75c701ed1a7162a4fa97ea928d4c5e490a38d Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Sat, 15 Jun 2024 14:30:41 -0600 Subject: [PATCH 41/68] Add missing file --- src/Goban/themes/GobanTheme.ts | 436 +++++++++++++++++++++++++++++++++ 1 file changed, 436 insertions(+) create mode 100644 src/Goban/themes/GobanTheme.ts diff --git a/src/Goban/themes/GobanTheme.ts b/src/Goban/themes/GobanTheme.ts new file mode 100644 index 00000000..ade23705 --- /dev/null +++ b/src/Goban/themes/GobanTheme.ts @@ -0,0 +1,436 @@ +/* + * 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 { GobanBase } from "../../GobanBase"; + +export interface GobanThemeBackgroundCSS { + "background-color"?: string; + "background-image"?: string; + "background-size"?: string; +} + +export interface GobanThemeBackgroundReactStyles { + backgroundColor?: string; + backgroundImage?: string; + backgroundSize?: string; +} + +export interface SVGStop { + offset: number; + color: string; +} + +export interface SVGStoneParameters { + id: string; + fill?: string; + stroke?: string; + stroke_scale?: number; // scale the radius by this amount + gradient?: { + stops: SVGStop[]; + type?: "radial" | "linear"; // default radial + x1?: number; + x2?: number; + y1?: number; + y2?: number; + cx?: number; + cy?: number; + r?: number; + fx?: number; + fy?: number; + }; + url?: string; +} + +export class GobanTheme { + public name: string; + public styles: { [style_name: string]: string } = {}; + protected parent?: GobanTheme; // An optional parent theme + + constructor(parent?: GobanTheme) { + this.name = `[ERROR theme missing name]`; + this.parent = parent; + } + + get theme_name(): string { + return "ERROR missing theme_name"; + } + public sort(): number { + return 0; + } + + /* Returns an array of black stone objects. The structure + * of the array elements is up to the implementor, as they are passed + * verbatim to the placeBlackStone method */ + public preRenderBlack( + _radius: number, + _seed: number, + _deferredRenderCallback: () => void, + ): any { + return { black: "stone" }; + } + + /* Returns an array of white stone objects. The structure + * of the array elements is up to the implementor, as they are passed + * verbatim to the placeWhiteStone method */ + public preRenderWhite( + _radius: number, + _seed: number, + _deferredRenderCallback: () => void, + ): any { + return { white: "stone" }; + } + + /* Returns an array of black stone objects. The structure + * of the array elements is up to the implementor, as they are passed + * verbatim to the placeBlackStone method */ + public preRenderBlackSVG( + defs: SVGDefsElement, + radius: number, + _seed: number, + _deferredRenderCallback: () => void, + ): string[] { + const ret = []; + const key = `black-${radius}`; + ret.push(key); + + defs.appendChild( + this.renderSVG( + { + id: key, + //fill: "hsl(8, 7%, 10%)", + //stroke: "hsl(8, 7%, 10%)", + fill: this.getBlackStoneColor(), + stroke: this.getBlackStoneColor(), + }, + radius, + ), + ); + return ret; + } + + /* Returns an array of white stone objects. The structure + * of the array elements is up to the implementor, as they are passed + * verbatim to the placeWhiteStone method */ + public preRenderWhiteSVG( + defs: SVGDefsElement, + radius: number, + _seed: number, + _deferredRenderCallback: () => void, + ): string[] { + const ret = []; + const key = `white-${radius}`; + ret.push(key); + defs.appendChild( + this.renderSVG( + { + id: key, + //fill: "hsl(8, 7%, 90%)", + //stroke: "hsl(8, 7%, 30%)", + fill: this.getWhiteStoneColor(), + stroke: this.getBlackStoneColor(), + }, + radius, + ), + ); + return ret; + } + + /* Places a pre rendered stone onto the canvas, centered at cx, cy */ + public placeWhiteStone( + ctx: CanvasRenderingContext2D, + _shadow_ctx: CanvasRenderingContext2D | null, + _stone: any, + cx: number, + cy: number, + radius: number, + ) { + //if (shadow_ctx) do something + ctx.fillStyle = this.getWhiteStoneColor(); + ctx.beginPath(); + ctx.arc(cx, cy, Math.max(0.1, radius), 0, 2 * Math.PI, true); + ctx.fill(); + } + + public placeBlackStone( + ctx: CanvasRenderingContext2D, + _shadow_ctx: CanvasRenderingContext2D | null, + _stone: any, + cx: number, + cy: number, + radius: number, + ) { + //if (shadow_ctx) do something + ctx.fillStyle = this.getBlackStoneColor(); + ctx.beginPath(); + ctx.arc(cx, cy, Math.max(0.1, radius), 0, 2 * Math.PI, true); + ctx.fill(); + } + + public placeStoneShadowSVG( + shadow_cell: SVGGraphicsElement | undefined, + cx: number, + cy: number, + radius: number, + color: string, + ): SVGElement | undefined { + if (!shadow_cell) { + return; + } + + const invisible_circle_to_cast_shadow = document.createElementNS( + "http://www.w3.org/2000/svg", + "circle", + ); + invisible_circle_to_cast_shadow.setAttribute("class", "shadow"); + invisible_circle_to_cast_shadow.setAttribute("fill", color); + invisible_circle_to_cast_shadow.setAttribute("cx", cx.toString()); + invisible_circle_to_cast_shadow.setAttribute("cy", cy.toString()); + invisible_circle_to_cast_shadow.setAttribute("r", (radius * 0.97).toString()); + const sx = radius * 0.15; + const sy = radius * 0.15; + const softness = radius * 0.1; + invisible_circle_to_cast_shadow.setAttribute( + "style", + `filter: drop-shadow(${sx}px ${sy}px ${softness}px rgba(0,0,0,0.4)`, + ); + shadow_cell.appendChild(invisible_circle_to_cast_shadow); + return invisible_circle_to_cast_shadow; + } + + public placeWhiteStoneSVG( + cell: SVGGraphicsElement, + shadow_cell: SVGGraphicsElement | undefined, + stone: string, + cx: number, + cy: number, + radius: number, + ): [SVGElement, SVGElement | undefined] { + const shadow = this.placeStoneShadowSVG(shadow_cell, cx, cy, radius, "#eeeeee"); + + 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 [ref, shadow]; + } + + public placeBlackStoneSVG( + cell: SVGGraphicsElement, + shadow_cell: SVGGraphicsElement | undefined, + stone: string, + cx: number, + 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 [ref, shadow]; + } + + /* 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: GobanBase): any { + const ret = Array.isArray(stones) + ? stones[((x + 1) * 53 * ((y + 1) * 97)) % stones.length] + : stones; + + if (!ret) { + console.error("No stone returned for ", x, y, stones); + throw new Error("Failed to get stone for " + x + ", " + y); + } + + return ret; + } + + /* 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: GobanBase): string { + if (Array.isArray(stones)) { + return "" + (((x + 1) * 53 * ((y + 1) * 97)) % stones.length); + } + return ""; + } + + /* Should return true if you would like the shadow layer to be present. False + * speeds up rendering typically */ + public stoneCastsShadow(_radius: number): boolean { + return false; + } + + /* Returns the color that should be used for white stones */ + public getWhiteStoneColor(): string { + return "#ffffff"; + } + + /* Returns the color that should be used for black stones */ + public getBlackStoneColor(): string { + return "#000000"; + } + + /* Returns the color that should be used for text over white stones */ + public getWhiteTextColor(_color?: string): string { + return "#000000"; + } + + /* Returns the color that should be used for text over black stones */ + public getBlackTextColor(_color?: string): string { + return "#ffffff"; + } + + /* Returns a set of CSS styles that should be applied to the background layer (ie the board) */ + public getBackgroundCSS(): GobanThemeBackgroundCSS { + return { + "background-color": "#DCB35C", + "background-image": "", + }; + } + + /* Returns a set of CSS styles (for react) that should be applied to the background layer (ie the board) */ + public getReactStyles(): GobanThemeBackgroundReactStyles { + const ret: GobanThemeBackgroundReactStyles = {}; + const css: GobanThemeBackgroundCSS = this.getBackgroundCSS(); + + ret.backgroundColor = css["background-color"]; + ret.backgroundImage = css["background-image"]; + + return ret; + } + + /* Returns the color that should be used for lines */ + public getLineColor(): string { + return "#000000"; + } + + /* Returns the color that should be used for lines * when there is text over the square */ + public getFadedLineColor(): string { + return "#888888"; + } + + /* Returns the color that should be used for star points */ + public getStarColor(): string { + return "#000000"; + } + + /* Returns the color that should be used for star points + * when there is text over the square */ + public getFadedStarColor(): string { + return "#888888"; + } + + /* Returns the color that text should be over empty intersections */ + public getBlankTextColor(): string { + return "#000000"; + } + + /** Returns the color that should be used for labels */ + public getLabelTextColor(): string { + return "#000000"; + } + + public renderSVG(params: SVGStoneParameters, radius: number): SVGGraphicsElement { + const cx = radius; + const cy = radius; + + const stone = document.createElementNS("http://www.w3.org/2000/svg", "g"); + stone.setAttribute("id", params.id); + stone.setAttribute("class", "stone"); + + if (params.fill || params.stroke || params.gradient) { + const circle = document.createElementNS("http://www.w3.org/2000/svg", "circle"); + stone.appendChild(circle); + if (params.fill) { + circle.setAttribute("fill", params.fill); + } + let stroke_width = 0.0; + if (params.stroke) { + circle.setAttribute("stroke", params.stroke); + if (params.stroke_scale) { + stroke_width = radius * params.stroke_scale; + } else { + stroke_width = radius / 20; + } + circle.setAttribute("stroke-width", `${stroke_width.toFixed(1)}px`); + } + circle.setAttribute("cx", cx.toString()); + circle.setAttribute("cy", cy.toString()); + circle.setAttribute("r", (radius - stroke_width * 0.5).toString()); + circle.setAttribute("shape-rendering", "geometricPrecision"); + + // gradient + if (params.gradient) { + const grad = params.gradient; + const defs = document.createElementNS("http://www.w3.org/2000/svg", "defs"); + + let gradient; + + if (grad.type === "linear") { + gradient = document.createElementNS( + "http://www.w3.org/2000/svg", + "linearGradient", + ); + gradient.setAttribute("x1", (grad.x1 ?? 0.0).toFixed(2)); + gradient.setAttribute("y1", (grad.y1 ?? 0.0).toFixed(2)); + gradient.setAttribute("x2", (grad.x2 ?? 1.0).toFixed(2)); + gradient.setAttribute("y2", (grad.y2 ?? 1.0).toFixed(2)); + } else { + gradient = document.createElementNS( + "http://www.w3.org/2000/svg", + params.gradient.type === "linear" ? "linearGradient" : "radialGradient", + ); + gradient.setAttribute("cx", (grad.cx ?? 0.5).toFixed(2)); + gradient.setAttribute("cy", (grad.cy ?? 0.5).toFixed(2)); + gradient.setAttribute("r", (grad.r ?? 0.5).toFixed(2)); + gradient.setAttribute("fx", (grad.fx ?? 0.3).toFixed(2)); + gradient.setAttribute("fy", (grad.fy ?? 0.2).toFixed(2)); + } + gradient.setAttribute("id", params.id + "-gradient"); + + for (const stop of params.gradient.stops) { + const s = document.createElementNS("http://www.w3.org/2000/svg", "stop"); + s.setAttribute("offset", `${stop.offset}%`); + s.setAttribute("stop-color", stop.color); + gradient.appendChild(s); + } + defs.appendChild(gradient); + stone.appendChild(defs); + circle.setAttribute("fill", `url(#${params.id}-gradient)`); + } + } + + if (params.url) { + const stone_image = document.createElementNS("http://www.w3.org/2000/svg", "image"); + stone_image.setAttribute("class", "stone"); + stone_image.setAttribute("x", `${cx - radius}`); + stone_image.setAttribute("y", `${cy - radius}`); + stone_image.setAttribute("width", `${radius * 2}`); + stone_image.setAttribute("height", `${radius * 2}`); + stone_image.setAttributeNS("http://www.w3.org/1999/xlink", "href", params.url); + stone.appendChild(stone_image); + } + + return stone; + } +} From 55589d2bd4987aa71c0c7a2fc1561feea11eea80 Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Sat, 15 Jun 2024 16:56:39 -0600 Subject: [PATCH 42/68] Move test specific code to __tests__ directory, use jsdom environment everywhere --- jest.config.ts | 2 +- src/__tests__/GoEngine.test.ts | 2 +- src/__tests__/GoEngine_sgf.test.ts | 11 +---------- src/__tests__/GobanCanvas.test.ts | 6 +----- .../GobanCore_conditional_moves.test.ts | 11 +---------- src/__tests__/GobanSVG.test.ts | 4 ---- src/__tests__/GobanSocket.test.ts | 4 ---- src/{Goban => __tests__}/TestGoban.ts | 12 ++++++++---- src/__tests__/autoscore.test.ts | 2 +- src/{ => __tests__}/test_utils.ts | 18 ++++++++++++++++++ src/engine/index.ts | 2 +- src/index.ts | 1 - tsconfig.json | 3 ++- 13 files changed, 35 insertions(+), 43 deletions(-) rename src/{Goban => __tests__}/TestGoban.ts (89%) rename src/{ => __tests__}/test_utils.ts (87%) diff --git a/jest.config.ts b/jest.config.ts index c41d8660..062666e6 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -153,7 +153,7 @@ export default { // snapshotSerializers: [], // The test environment that will be used for testing - // testEnvironment: "jest-environment-node", + testEnvironment: "jsdom", // Options that will be passed to the testEnvironment // testEnvironmentOptions: {}, diff --git a/src/__tests__/GoEngine.test.ts b/src/__tests__/GoEngine.test.ts index 4f299900..954681ff 100644 --- a/src/__tests__/GoEngine.test.ts +++ b/src/__tests__/GoEngine.test.ts @@ -7,7 +7,7 @@ import { makeMatrix, matricesAreEqual, } from "../engine"; -import { movesFromBoardState } from "../test_utils"; +import { movesFromBoardState } from "./test_utils"; test("boardMatricesAreTheSame", () => { const a = [ diff --git a/src/__tests__/GoEngine_sgf.test.ts b/src/__tests__/GoEngine_sgf.test.ts index 145f933a..540e3076 100644 --- a/src/__tests__/GoEngine_sgf.test.ts +++ b/src/__tests__/GoEngine_sgf.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 "../Goban/TestGoban"; +import { TestGoban } from "./TestGoban"; import { MoveTree } from "../engine"; type SGFTestcase = { diff --git a/src/__tests__/GobanCanvas.test.ts b/src/__tests__/GobanCanvas.test.ts index dbae8699..1b27d9f4 100644 --- a/src/__tests__/GobanCanvas.test.ts +++ b/src/__tests__/GobanCanvas.test.ts @@ -1,14 +1,10 @@ -/** - * @jest-environment jsdom - */ - // cspell: disable (global as any).CLIENT = true; import { GobanCanvas, CanvasRendererGobanConfig } from "../Goban/CanvasRenderer"; import { SCORE_ESTIMATION_TOLERANCE, SCORE_ESTIMATION_TRIALS } from "../Goban/InteractiveBase"; -import { GobanSocket, GoMath } from "../engine"; +import { GobanSocket, GoMath } from "engine"; import { GobanBase } from "../GobanBase"; import WS from "jest-websocket-mock"; diff --git a/src/__tests__/GobanCore_conditional_moves.test.ts b/src/__tests__/GobanCore_conditional_moves.test.ts index a4152554..32f3fb69 100644 --- a/src/__tests__/GobanCore_conditional_moves.test.ts +++ b/src/__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 "../Goban/TestGoban"; +import { TestGoban } from "./TestGoban"; test("call FollowConditionalPath", () => { const goban = new TestGoban({ moves: [] }); diff --git a/src/__tests__/GobanSVG.test.ts b/src/__tests__/GobanSVG.test.ts index 9269d112..0240ccee 100644 --- a/src/__tests__/GobanSVG.test.ts +++ b/src/__tests__/GobanSVG.test.ts @@ -1,7 +1,3 @@ -/** - * @jest-environment jsdom - */ - // cspell: disable (global as any).CLIENT = true; diff --git a/src/__tests__/GobanSocket.test.ts b/src/__tests__/GobanSocket.test.ts index ab278b23..8f1185dc 100644 --- a/src/__tests__/GobanSocket.test.ts +++ b/src/__tests__/GobanSocket.test.ts @@ -1,7 +1,3 @@ -/** - * @jest-environment jsdom - */ - (global as any).CLIENT = true; import { GobanSocket, closeErrorCodeToString } from "../engine"; diff --git a/src/Goban/TestGoban.ts b/src/__tests__/TestGoban.ts similarity index 89% rename from src/Goban/TestGoban.ts rename to src/__tests__/TestGoban.ts index 150b1d57..ca976e7c 100644 --- a/src/Goban/TestGoban.ts +++ b/src/__tests__/TestGoban.ts @@ -25,10 +25,10 @@ // `current_title` etc. A way for testers to peer into the internals import { GobanConfig } from "../GobanBase"; -import { GoEngine } from "engine/GobanEngine"; -import { MessageID } from "engine/messages"; -import { MoveTreePenMarks } from "engine/MoveTree"; -import { Goban, GobanSelectedThemes } from "./Goban"; +import { GoEngine } from "../engine/GobanEngine"; +import { MessageID } from "../engine/messages"; +import { MoveTreePenMarks } from "../engine/MoveTree"; +import { Goban, GobanSelectedThemes } from "../Goban/Goban"; export class TestGoban extends Goban { public engine: GoEngine; @@ -58,3 +58,7 @@ export class TestGoban extends Goban { protected enableDrawing(): void {} protected disableDrawing(): void {} } + +test("TestGoban", () => { + new TestGoban({}); +}); diff --git a/src/__tests__/autoscore.test.ts b/src/__tests__/autoscore.test.ts index 938b01a2..ab4fef90 100644 --- a/src/__tests__/autoscore.test.ts +++ b/src/__tests__/autoscore.test.ts @@ -1,5 +1,5 @@ import { readFileSync, readdirSync } from "fs"; -import { autoscore } from "../engine"; +import { autoscore } from "engine"; describe("Auto-score tests ", () => { const files = readdirSync("test/autoscore_test_files"); diff --git a/src/test_utils.ts b/src/__tests__/test_utils.ts similarity index 87% rename from src/test_utils.ts rename to src/__tests__/test_utils.ts index 4ad952f9..e58a3a62 100644 --- a/src/test_utils.ts +++ b/src/__tests__/test_utils.ts @@ -68,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/engine/index.ts b/src/engine/index.ts index 34977e05..1b79c9be 100644 --- a/src/engine/index.ts +++ b/src/engine/index.ts @@ -28,7 +28,7 @@ export * from "./ownership_estimators"; export * from "./ScoreEstimator"; export * from "./StoneString"; export * from "./StoneStringBuilder"; -export * from "../test_utils"; +export * from "../__tests__/test_utils"; export * from "./formats"; export * from "./util"; diff --git a/src/index.ts b/src/index.ts index 2a75a83d..3aeaaf58 100644 --- a/src/index.ts +++ b/src/index.ts @@ -22,7 +22,6 @@ export * from "./Goban/CanvasRenderer"; export * from "./Goban/SVGRenderer"; export * from "./Goban/themes"; export * from "./Goban/Goban"; -export * from "./Goban/TestGoban"; export * as protocol from "engine/protocol"; export { placeRenderedImageStone, preRenderImageStone } from "./Goban/themes/image_stones"; diff --git a/tsconfig.json b/tsconfig.json index 7d38841b..0ca8abd4 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -33,5 +33,6 @@ "sourceMap": true, "jsx": "react" }, - "files": ["src/index.ts", "src/engine/index.ts", "./test/test_autoscore.ts"] + "files": ["src/index.ts", "src/engine/index.ts", "./test/test_autoscore.ts"], + "include": ["src/__tests__/*.ts"] } From 12cfbb87e5e3ef2582f89cdef99880fa78701cef Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Sat, 15 Jun 2024 17:04:32 -0600 Subject: [PATCH 43/68] Move unit tests to dedicated test directory --- jest.config.ts | 7 ++++++- src/Goban/OGSConnectivity.ts | 2 +- src/engine/index.ts | 2 +- .../unit_tests}/GoConditionalMove.test.ts | 2 +- {src/__tests__ => test/unit_tests}/GoEngine.test.ts | 8 +------- .../unit_tests}/GoEngine_sgf.test.ts | 2 +- {src/__tests__ => test/unit_tests}/GoMath.test.ts | 2 +- .../unit_tests}/GoMath_positionId.test.ts | 4 ++-- .../__tests__ => test/unit_tests}/GobanCanvas.test.ts | 9 ++++++--- .../unit_tests}/GobanCore_conditional_moves.test.ts | 0 {src/__tests__ => test/unit_tests}/GobanSVG.test.ts | 11 +++++++---- .../__tests__ => test/unit_tests}/GobanSocket.test.ts | 4 ++-- .../unit_tests}/ScoreEstimator.test.ts | 8 ++++---- .../unit_tests}/StoneStringBuilder.test.ts | 2 +- {src/__tests__ => test/unit_tests}/TestGoban.ts | 10 +++++----- {src/__tests__ => test/unit_tests}/autoscore.test.ts | 0 {src/__tests__ => test/unit_tests}/test_utils.ts | 0 {src/__tests__ => test/unit_tests}/util.test.ts | 4 ++-- tsconfig.json | 2 +- 19 files changed, 42 insertions(+), 37 deletions(-) rename {src/__tests__ => test/unit_tests}/GoConditionalMove.test.ts (98%) rename {src/__tests__ => test/unit_tests}/GoEngine.test.ts (99%) rename {src/__tests__ => test/unit_tests}/GoEngine_sgf.test.ts (99%) rename {src/__tests__ => test/unit_tests}/GoMath.test.ts (99%) rename {src/__tests__ => test/unit_tests}/GoMath_positionId.test.ts (97%) rename {src/__tests__ => test/unit_tests}/GobanCanvas.test.ts (98%) rename {src/__tests__ => test/unit_tests}/GobanCore_conditional_moves.test.ts (100%) rename {src/__tests__ => test/unit_tests}/GobanSVG.test.ts (98%) rename {src/__tests__ => test/unit_tests}/GobanSocket.test.ts (97%) rename {src/__tests__ => test/unit_tests}/ScoreEstimator.test.ts (98%) rename {src/__tests__ => test/unit_tests}/StoneStringBuilder.test.ts (94%) rename {src/__tests__ => test/unit_tests}/TestGoban.ts (89%) rename {src/__tests__ => test/unit_tests}/autoscore.test.ts (100%) rename {src/__tests__ => test/unit_tests}/test_utils.ts (100%) rename {src/__tests__ => test/unit_tests}/util.test.ts (91%) diff --git a/jest.config.ts b/jest.config.ts index 062666e6..803f0deb 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -162,7 +162,12 @@ export default { // testLocationInResults: false, // The glob patterns Jest uses to detect test files - testMatch: ["/src/**/__tests__/**/*.[jt]s?(x)", "/src/**/?(*.)+(spec|test).ts"], + testMatch: [ + "/src/**/unit_tests/**/*.[jt]s?(x)", + "/test/**/unit_tests/**/*.[jt]s?(x)", + "/src/**/?(*.)+(spec|test).ts", + "/test/**/?(*.)+(spec|test).ts", + ], // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped // testPathIgnorePatterns: [ diff --git a/src/Goban/OGSConnectivity.ts b/src/Goban/OGSConnectivity.ts index df732175..88a2315d 100644 --- a/src/Goban/OGSConnectivity.ts +++ b/src/Goban/OGSConnectivity.ts @@ -17,7 +17,7 @@ import { AudioClockEvent, GobanInteractive, MARK_TYPES, MoveCommand } from "./InteractiveBase"; import { GobanConfig, JGOFClockWithTransmitting } from "../GobanBase"; import { callbacks } from "./callbacks"; -import { _, interpolate } from "../engine/translate"; +import { _, interpolate } from "engine/translate"; import { focus_tracker } from "./focus_tracker"; import { AdHocClock, diff --git a/src/engine/index.ts b/src/engine/index.ts index 1b79c9be..b198da84 100644 --- a/src/engine/index.ts +++ b/src/engine/index.ts @@ -28,7 +28,7 @@ export * from "./ownership_estimators"; export * from "./ScoreEstimator"; export * from "./StoneString"; export * from "./StoneStringBuilder"; -export * from "../__tests__/test_utils"; +export * from "../../test/unit_tests/test_utils"; export * from "./formats"; export * from "./util"; diff --git a/src/__tests__/GoConditionalMove.test.ts b/test/unit_tests/GoConditionalMove.test.ts similarity index 98% rename from src/__tests__/GoConditionalMove.test.ts rename to test/unit_tests/GoConditionalMove.test.ts index fc86cb4e..cf9b1bcf 100644 --- a/src/__tests__/GoConditionalMove.test.ts +++ b/test/unit_tests/GoConditionalMove.test.ts @@ -1,4 +1,4 @@ -import { GoConditionalMove } from "../engine"; +import { GoConditionalMove } from "engine"; /** * ``` diff --git a/src/__tests__/GoEngine.test.ts b/test/unit_tests/GoEngine.test.ts similarity index 99% rename from src/__tests__/GoEngine.test.ts rename to test/unit_tests/GoEngine.test.ts index 954681ff..5a53cb4e 100644 --- a/src/__tests__/GoEngine.test.ts +++ b/test/unit_tests/GoEngine.test.ts @@ -1,12 +1,6 @@ //cspell: disable -import { - GoEngine, - GobanMoveError, - JGOFIntersection, - makeMatrix, - matricesAreEqual, -} from "../engine"; +import { GoEngine, GobanMoveError, JGOFIntersection, makeMatrix, matricesAreEqual } from "engine"; import { movesFromBoardState } from "./test_utils"; test("boardMatricesAreTheSame", () => { diff --git a/src/__tests__/GoEngine_sgf.test.ts b/test/unit_tests/GoEngine_sgf.test.ts similarity index 99% rename from src/__tests__/GoEngine_sgf.test.ts rename to test/unit_tests/GoEngine_sgf.test.ts index 540e3076..1681a4f3 100644 --- a/src/__tests__/GoEngine_sgf.test.ts +++ b/test/unit_tests/GoEngine_sgf.test.ts @@ -3,7 +3,7 @@ (global as any).CLIENT = true; import { TestGoban } from "./TestGoban"; -import { MoveTree } from "../engine"; +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 99% rename from src/__tests__/GoMath.test.ts rename to test/unit_tests/GoMath.test.ts index edc9e579..09d93d4c 100644 --- a/src/__tests__/GoMath.test.ts +++ b/test/unit_tests/GoMath.test.ts @@ -1,6 +1,6 @@ //cspell: disable -import { StoneStringBuilder, JGOFNumericPlayerColor, GoMath, BoardState } from "../engine"; +import { StoneStringBuilder, JGOFNumericPlayerColor, GoMath, BoardState } from "engine"; describe("GoStoneGroups constructor", () => { test("basic board state", () => { diff --git a/src/__tests__/GoMath_positionId.test.ts b/test/unit_tests/GoMath_positionId.test.ts similarity index 97% rename from src/__tests__/GoMath_positionId.test.ts rename to test/unit_tests/GoMath_positionId.test.ts index f9cc5b38..834e0662 100644 --- a/src/__tests__/GoMath_positionId.test.ts +++ b/test/unit_tests/GoMath_positionId.test.ts @@ -1,7 +1,7 @@ // cspell: disable -import { GoMath } from "../engine"; -import { JGOFNumericPlayerColor } from "../engine"; +import { GoMath } from "engine"; +import { JGOFNumericPlayerColor } from "engine"; type Testcase = { height: number; diff --git a/src/__tests__/GobanCanvas.test.ts b/test/unit_tests/GobanCanvas.test.ts similarity index 98% rename from src/__tests__/GobanCanvas.test.ts rename to test/unit_tests/GobanCanvas.test.ts index 1b27d9f4..98a34112 100644 --- a/src/__tests__/GobanCanvas.test.ts +++ b/test/unit_tests/GobanCanvas.test.ts @@ -2,10 +2,13 @@ (global as any).CLIENT = true; -import { GobanCanvas, CanvasRendererGobanConfig } from "../Goban/CanvasRenderer"; -import { SCORE_ESTIMATION_TOLERANCE, SCORE_ESTIMATION_TRIALS } from "../Goban/InteractiveBase"; +import { GobanCanvas, CanvasRendererGobanConfig } from "../../src/Goban/CanvasRenderer"; +import { + SCORE_ESTIMATION_TOLERANCE, + SCORE_ESTIMATION_TRIALS, +} from "../../src/Goban/InteractiveBase"; import { GobanSocket, GoMath } from "engine"; -import { GobanBase } from "../GobanBase"; +import { GobanBase } from "../../src/GobanBase"; import WS from "jest-websocket-mock"; let board_div: HTMLDivElement; diff --git a/src/__tests__/GobanCore_conditional_moves.test.ts b/test/unit_tests/GobanCore_conditional_moves.test.ts similarity index 100% rename from src/__tests__/GobanCore_conditional_moves.test.ts rename to test/unit_tests/GobanCore_conditional_moves.test.ts diff --git a/src/__tests__/GobanSVG.test.ts b/test/unit_tests/GobanSVG.test.ts similarity index 98% rename from src/__tests__/GobanSVG.test.ts rename to test/unit_tests/GobanSVG.test.ts index 0240ccee..5b52f4ca 100644 --- a/src/__tests__/GobanSVG.test.ts +++ b/test/unit_tests/GobanSVG.test.ts @@ -2,10 +2,13 @@ (global as any).CLIENT = true; -import { SVGRenderer, SVGRendererGobanConfig } from "../Goban/SVGRenderer"; -import { SCORE_ESTIMATION_TOLERANCE, SCORE_ESTIMATION_TRIALS } from "../Goban/InteractiveBase"; -import { GobanSocket, GoMath } from "../engine"; -import { GobanBase } from "../GobanBase"; +import { SVGRenderer, SVGRendererGobanConfig } from "../../src/Goban/SVGRenderer"; +import { + SCORE_ESTIMATION_TOLERANCE, + SCORE_ESTIMATION_TRIALS, +} from "../../src/Goban/InteractiveBase"; +import { GobanSocket, GoMath } from "engine"; +import { GobanBase } from "../../src/GobanBase"; import WS from "jest-websocket-mock"; let board_div: HTMLDivElement; 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 8f1185dc..50bb1914 100644 --- a/src/__tests__/GobanSocket.test.ts +++ b/test/unit_tests/GobanSocket.test.ts @@ -1,8 +1,8 @@ (global as any).CLIENT = true; -import { GobanSocket, closeErrorCodeToString } from "../engine"; +import { GobanSocket, closeErrorCodeToString } from "engine"; import WS from "jest-websocket-mock"; -import * as protocol from "../engine/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 98% rename from src/__tests__/ScoreEstimator.test.ts rename to test/unit_tests/ScoreEstimator.test.ts index e0adcef2..f36cba8d 100644 --- a/src/__tests__/ScoreEstimator.test.ts +++ b/test/unit_tests/ScoreEstimator.test.ts @@ -1,12 +1,12 @@ //cspell: disable -import { GoEngine } from "../engine"; -import { makeMatrix } from "../engine"; -import { ScoreEstimator, adjust_estimate, set_local_ownership_estimator } from "../engine"; +import { GoEngine } from "engine"; +import { makeMatrix } from "engine"; +import { ScoreEstimator, adjust_estimate, set_local_ownership_estimator } from "engine"; import { init_remote_ownership_estimator, voronoi_estimate_ownership, -} from "../engine/ownership_estimators"; +} from "engine/ownership_estimators"; describe("adjust_estimate", () => { const BOARD = [ diff --git a/src/__tests__/StoneStringBuilder.test.ts b/test/unit_tests/StoneStringBuilder.test.ts similarity index 94% rename from src/__tests__/StoneStringBuilder.test.ts rename to test/unit_tests/StoneStringBuilder.test.ts index d2538b5f..bcd29cba 100644 --- a/src/__tests__/StoneStringBuilder.test.ts +++ b/test/unit_tests/StoneStringBuilder.test.ts @@ -1,4 +1,4 @@ -import { GoMath, StoneStringBuilder, BoardState } from "../engine"; +import { GoMath, StoneStringBuilder, BoardState } from "engine"; // Here is a board displaying many of the features GoStoneGroup cares about. diff --git a/src/__tests__/TestGoban.ts b/test/unit_tests/TestGoban.ts similarity index 89% rename from src/__tests__/TestGoban.ts rename to test/unit_tests/TestGoban.ts index ca976e7c..5d40b891 100644 --- a/src/__tests__/TestGoban.ts +++ b/test/unit_tests/TestGoban.ts @@ -24,11 +24,11 @@ // - [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 { GoEngine } from "../engine/GobanEngine"; -import { MessageID } from "../engine/messages"; -import { MoveTreePenMarks } from "../engine/MoveTree"; -import { Goban, GobanSelectedThemes } from "../Goban/Goban"; +import { GobanConfig } from "../../src/GobanBase"; +import { GoEngine } from "engine/GobanEngine"; +import { MessageID } from "engine/messages"; +import { MoveTreePenMarks } from "engine/MoveTree"; +import { Goban, GobanSelectedThemes } from "../../src/Goban/Goban"; export class TestGoban extends Goban { public engine: GoEngine; diff --git a/src/__tests__/autoscore.test.ts b/test/unit_tests/autoscore.test.ts similarity index 100% rename from src/__tests__/autoscore.test.ts rename to test/unit_tests/autoscore.test.ts diff --git a/src/__tests__/test_utils.ts b/test/unit_tests/test_utils.ts similarity index 100% rename from src/__tests__/test_utils.ts rename to test/unit_tests/test_utils.ts diff --git a/src/__tests__/util.test.ts b/test/unit_tests/util.test.ts similarity index 91% rename from src/__tests__/util.test.ts rename to test/unit_tests/util.test.ts index 86601986..06617bd4 100644 --- a/src/__tests__/util.test.ts +++ b/test/unit_tests/util.test.ts @@ -1,5 +1,5 @@ -import { escapeSGFText, newlines_to_spaces } from "../engine"; -import * as AdHoc from "../engine/formats/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) diff --git a/tsconfig.json b/tsconfig.json index 0ca8abd4..36e337a5 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -34,5 +34,5 @@ "jsx": "react" }, "files": ["src/index.ts", "src/engine/index.ts", "./test/test_autoscore.ts"], - "include": ["src/__tests__/*.ts"] + "include": ["test/unit_tests/*.ts"] } From 269fec2f915dac87336b1c9e04ba3cc3608eecbf Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Sat, 15 Jun 2024 17:06:26 -0600 Subject: [PATCH 44/68] Add jest.config.ts to tsconfig.json --- jest.config.ts | 16 ++++++++++++++++ tsconfig.json | 2 +- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/jest.config.ts b/jest.config.ts index 803f0deb..425d624c 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -1,3 +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. + */ + /* * For a detailed explanation regarding each configuration property and type check, visit: * https://jestjs.io/docs/configuration diff --git a/tsconfig.json b/tsconfig.json index 36e337a5..8873804f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -33,6 +33,6 @@ "sourceMap": true, "jsx": "react" }, - "files": ["src/index.ts", "src/engine/index.ts", "./test/test_autoscore.ts"], + "files": ["src/index.ts", "src/engine/index.ts", "./test/test_autoscore.ts", "jest.config.ts"], "include": ["test/unit_tests/*.ts"] } From f642ce5fa2a719c444651147c2b5bb81cb1b31ca Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Sat, 15 Jun 2024 18:34:47 -0600 Subject: [PATCH 45/68] Refactor: GoConditionalMove -> ConditionalMoveTree --- src/Goban/InteractiveBase.ts | 14 +++++----- src/Goban/OGSConnectivity.ts | 4 +-- {test/unit_tests => src/Goban}/TestGoban.ts | 20 ++++--------- ...ditionalMove.ts => ConditionalMoveTree.ts} | 28 +++++++++---------- src/engine/index.ts | 3 +- src/engine/protocol/ClientToServer.ts | 2 +- src/engine/protocol/ServerToClient.ts | 2 +- src/index.ts | 1 + test/unit_tests/GoConditionalMove.test.ts | 24 ++++++++-------- test/unit_tests/GoEngine_sgf.test.ts | 2 +- .../GobanCore_conditional_moves.test.ts | 2 +- tsconfig.json | 4 +-- 12 files changed, 48 insertions(+), 58 deletions(-) rename {test/unit_tests => src/Goban}/TestGoban.ts (72%) rename src/engine/{GoConditionalMove.ts => ConditionalMoveTree.ts} (68%) diff --git a/src/Goban/InteractiveBase.ts b/src/Goban/InteractiveBase.ts index 7c0247ef..4fb3a52a 100644 --- a/src/Goban/InteractiveBase.ts +++ b/src/Goban/InteractiveBase.ts @@ -22,11 +22,11 @@ import { PlayerColor, PuzzlePlacementSetting, Score, + ConditionalMoveTree, + GobanMoveError, } from "engine"; -import { GobanMoveError } from "engine/GobanError"; import { NumberMatrix, Intersection, encodeMove } from "engine/GoMath"; import * as GoMath from "engine/GoMath"; -import { GoConditionalMove } from "engine/GoConditionalMove"; import { MoveTree, MarkInterface } from "engine/MoveTree"; import { ScoreEstimator } from "engine/ScoreEstimator"; import { computeAverageMoveTime, niceInterval, matricesAreEqual } from "engine/util"; @@ -139,7 +139,7 @@ export abstract class GobanInteractive extends GobanBase { protected abstract sendMove(mv: MoveCommand, cb?: () => void): boolean; public conditional_starting_color: "black" | "white" | "invalid" = "invalid"; - public conditional_tree: GoConditionalMove = new GoConditionalMove(null); + public conditional_tree: ConditionalMoveTree = new ConditionalMoveTree(null); public double_click_submit: boolean; public variation_stone_opacity: number; public draw_bottom_labels: boolean; @@ -286,7 +286,7 @@ export abstract class GobanInteractive extends GobanBase { protected bounds: GobanBounds; protected conditional_path: string = ""; public config: GobanConfig; - protected current_cmove?: GoConditionalMove; + protected current_cmove?: ConditionalMoveTree; protected currently_my_cmove: boolean = false; protected dirty_redraw: any = null; // timer protected disconnectedFromGame: boolean = true; @@ -1174,9 +1174,9 @@ export abstract class GobanInteractive extends GobanBase { } } - public setConditionalTree(conditional_tree?: GoConditionalMove): void { + public setConditionalTree(conditional_tree?: ConditionalMoveTree): void { if (typeof conditional_tree === "undefined") { - this.conditional_tree = new GoConditionalMove(null); + this.conditional_tree = new ConditionalMoveTree(null); } else { this.conditional_tree = conditional_tree; } @@ -1211,7 +1211,7 @@ export abstract class GobanInteractive extends GobanBase { if (mv in this.current_cmove.children) { cmove = this.current_cmove.children[mv]; } else { - cmove = new GoConditionalMove(null, this.current_cmove); + cmove = new ConditionalMoveTree(null, this.current_cmove); this.current_cmove.children[mv] = cmove; } this.current_cmove = cmove; diff --git a/src/Goban/OGSConnectivity.ts b/src/Goban/OGSConnectivity.ts index 88a2315d..823927fd 100644 --- a/src/Goban/OGSConnectivity.ts +++ b/src/Goban/OGSConnectivity.ts @@ -31,7 +31,7 @@ import { encodeMove, GobanSocket, GobanSocketEvents, - GoConditionalMove, + ConditionalMoveTree, GoEngine, GoMath, init_wasm_ownership_estimator, @@ -620,7 +620,7 @@ export abstract class OGSConnectivity extends GobanInteractive { if (cmoves.moves == null) { this.setConditionalTree(); } else { - this.setConditionalTree(GoConditionalMove.decode(cmoves.moves)); + this.setConditionalTree(ConditionalMoveTree.decode(cmoves.moves)); } }, ); diff --git a/test/unit_tests/TestGoban.ts b/src/Goban/TestGoban.ts similarity index 72% rename from test/unit_tests/TestGoban.ts rename to src/Goban/TestGoban.ts index 5d40b891..a4220a57 100644 --- a/test/unit_tests/TestGoban.ts +++ b/src/Goban/TestGoban.ts @@ -15,21 +15,15 @@ * limitations under the License. */ -// This is a minimal implementation of Goban. 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 "../../src/GobanBase"; +import { GobanConfig } from "../GobanBase"; import { GoEngine } from "engine/GobanEngine"; import { MessageID } from "engine/messages"; import { MoveTreePenMarks } from "engine/MoveTree"; -import { Goban, GobanSelectedThemes } from "../../src/Goban/Goban"; +import { Goban, GobanSelectedThemes } from "./Goban"; +/** + * This is a minimal implementation of Goban, primarily used for unit tests. + */ export class TestGoban extends Goban { public engine: GoEngine; @@ -58,7 +52,3 @@ export class TestGoban extends Goban { protected enableDrawing(): void {} protected disableDrawing(): void {} } - -test("TestGoban", () => { - new TestGoban({}); -}); diff --git a/src/engine/GoConditionalMove.ts b/src/engine/ConditionalMoveTree.ts similarity index 68% rename from src/engine/GoConditionalMove.ts rename to src/engine/ConditionalMoveTree.ts index 8539715b..c471e16d 100644 --- a/src/engine/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/engine/index.ts b/src/engine/index.ts index b198da84..02ff286b 100644 --- a/src/engine/index.ts +++ b/src/engine/index.ts @@ -21,14 +21,13 @@ export * from "./autoscore"; export * from "../GobanBase"; export * from "./GobanError"; export * from "./GobanSocket"; -export * from "./GoConditionalMove"; +export * from "./ConditionalMoveTree"; export * from "./GoMath"; export * from "./MoveTree"; export * from "./ownership_estimators"; export * from "./ScoreEstimator"; export * from "./StoneString"; export * from "./StoneStringBuilder"; -export * from "../../test/unit_tests/test_utils"; export * from "./formats"; export * from "./util"; diff --git a/src/engine/protocol/ClientToServer.ts b/src/engine/protocol/ClientToServer.ts index ed04cd9e..7e1da6bd 100644 --- a/src/engine/protocol/ClientToServer.ts +++ b/src/engine/protocol/ClientToServer.ts @@ -16,7 +16,7 @@ import type { JGOFMove, JGOFPlayerClock, JGOFSealingIntersection } from "../formats/JGOF"; import type { ReviewMessage } from "../GobanEngine"; -import type { ConditionalMoveResponse } from "../GoConditionalMove"; +import type { ConditionalMoveResponse } from "../ConditionalMoveTree"; /** Messages that clients send, regardless of target server */ export interface ClientToServerBase { diff --git a/src/engine/protocol/ServerToClient.ts b/src/engine/protocol/ServerToClient.ts index eb48f5f6..56f131e4 100644 --- a/src/engine/protocol/ServerToClient.ts +++ b/src/engine/protocol/ServerToClient.ts @@ -21,7 +21,7 @@ import type { RemoteStorageReplication, } from "./ClientToServer"; import type { JGOFTimeControl } from "../formats/JGOF"; -import type { ConditionalMoveResponse } from "../GoConditionalMove"; +import type { ConditionalMoveResponse } from "../ConditionalMoveTree"; import type { GoEngineConfig, Score, ReviewMessage } from "../GobanEngine"; import type { AdHocPackedMove } from "../formats/AdHocFormat"; diff --git a/src/index.ts b/src/index.ts index 3aeaaf58..10eba92e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -22,6 +22,7 @@ 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"; diff --git a/test/unit_tests/GoConditionalMove.test.ts b/test/unit_tests/GoConditionalMove.test.ts index cf9b1bcf..713d7a7a 100644 --- a/test/unit_tests/GoConditionalMove.test.ts +++ b/test/unit_tests/GoConditionalMove.test.ts @@ -1,4 +1,4 @@ -import { GoConditionalMove } from "engine"; +import { ConditionalMoveTree } from "engine"; /** * ``` @@ -11,7 +11,7 @@ import { GoConditionalMove } from "engine"; * ``` */ 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/test/unit_tests/GoEngine_sgf.test.ts b/test/unit_tests/GoEngine_sgf.test.ts index 1681a4f3..0d6f36a6 100644 --- a/test/unit_tests/GoEngine_sgf.test.ts +++ b/test/unit_tests/GoEngine_sgf.test.ts @@ -2,7 +2,7 @@ (global as any).CLIENT = true; -import { TestGoban } from "./TestGoban"; +import { TestGoban } from "../../src/Goban/TestGoban"; import { MoveTree } from "engine"; type SGFTestcase = { diff --git a/test/unit_tests/GobanCore_conditional_moves.test.ts b/test/unit_tests/GobanCore_conditional_moves.test.ts index 32f3fb69..e3260725 100644 --- a/test/unit_tests/GobanCore_conditional_moves.test.ts +++ b/test/unit_tests/GobanCore_conditional_moves.test.ts @@ -2,7 +2,7 @@ (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/tsconfig.json b/tsconfig.json index 8873804f..e0298563 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -33,6 +33,6 @@ "sourceMap": true, "jsx": "react" }, - "files": ["src/index.ts", "src/engine/index.ts", "./test/test_autoscore.ts", "jest.config.ts"], - "include": ["test/unit_tests/*.ts"] + "files": ["src/index.ts", "src/engine/index.ts", "jest.config.ts"], + "include": ["test/**/*.ts"] } From 607ec5df9c3c923191ef81b75fd635c70371c335 Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Sat, 15 Jun 2024 18:43:18 -0600 Subject: [PATCH 46/68] Fix path resolution for ts-node executed manual tests --- package.json | 1 + tsconfig.json | 5 ++++- yarn.lock | 16 +++++++++++++++- 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 28c917f8..6bea7b4f 100644 --- a/package.json +++ b/package.json @@ -84,6 +84,7 @@ "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.4.5", diff --git a/tsconfig.json b/tsconfig.json index e0298563..c549ba2a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -34,5 +34,8 @@ "jsx": "react" }, "files": ["src/index.ts", "src/engine/index.ts", "jest.config.ts"], - "include": ["test/**/*.ts"] + "include": ["test/**/*.ts"], + "ts-node": { + "require": ["tsconfig-paths/register"] + } } diff --git a/yarn.lock b/yarn.lock index dc332344..e2fe82e9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4467,7 +4467,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== @@ -5928,6 +5928,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" @@ -6177,6 +6182,15 @@ 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" From ee215d432d1bbab424075f9aa6fba58d0af41120 Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Sun, 16 Jun 2024 06:35:01 -0600 Subject: [PATCH 47/68] Refactor: coordinate nomenclature normalization --- .vscode/cspell.json | 7 ++- Makefile | 2 +- src/Goban/CanvasRenderer.ts | 4 +- src/Goban/InteractiveBase.ts | 19 +++--- src/Goban/OGSConnectivity.ts | 4 +- src/Goban/SVGRenderer.ts | 4 +- src/GobanBase.ts | 61 ++++++++++++++++-- src/engine/GoMath.ts | 112 ++++++++++++++------------------- src/engine/GobanEngine.ts | 76 +++++++++++++++++----- src/engine/MoveTree.ts | 12 ++-- src/engine/autoscore.ts | 4 +- test/test_autoscore.ts | 14 ++--- test/unit_tests/GoMath.test.ts | 75 +++++++++------------- 13 files changed, 230 insertions(+), 164 deletions(-) diff --git a/.vscode/cspell.json b/.vscode/cspell.json index 7f7c6d90..1923ff15 100644 --- a/.vscode/cspell.json +++ b/.vscode/cspell.json @@ -1,5 +1,6 @@ { "words": [ + "aabc", "abcdefghjklmnopqrstuvwxyz", "AILR", "aireview", @@ -190,5 +191,9 @@ "zoomable" ], "language": "en,en-GB", - "ignorePaths": ["test/autoscore_test_files", "src/goscorer", "*.d.ts"] + "ignorePaths": [ + "test/autoscore_test_files", + "src/goscorer", + "*.d.ts" + ] } diff --git a/Makefile b/Makefile index c6be10e2..26a55571 100644 --- a/Makefile +++ b/Makefile @@ -6,7 +6,7 @@ all dev: build: lib types -lib: build-debug build-production +lib: build-debug build-production types build-debug: yarn run build-debug diff --git a/src/Goban/CanvasRenderer.ts b/src/Goban/CanvasRenderer.ts index 9f8e72ab..94d1e72e 100644 --- a/src/Goban/CanvasRenderer.ts +++ b/src/Goban/CanvasRenderer.ts @@ -386,7 +386,7 @@ export class GobanCanvas extends Goban implements GobanCanvasInterface { const pt = this.xy2ij(pos.x, pos.y); if (callbacks.addCoordinatesToChatInput) { callbacks.addCoordinatesToChatInput( - this.engine.prettyCoords(pt.i, pt.j), + this.engine.prettyCoordinates(pt.i, pt.j), ); } } catch (e) { @@ -2764,7 +2764,7 @@ export class GobanCanvas extends Goban 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(GoMath.encodePrettyXCoordinate(c), x, y); } break; case "1-1": diff --git a/src/Goban/InteractiveBase.ts b/src/Goban/InteractiveBase.ts index 4fb3a52a..0535927e 100644 --- a/src/Goban/InteractiveBase.ts +++ b/src/Goban/InteractiveBase.ts @@ -362,7 +362,6 @@ export abstract class GobanInteractive extends GobanBase { /* 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; @@ -1476,17 +1475,17 @@ export abstract class GobanInteractive extends GobanBase { } } - public editPlaceByPrettyCoord( - coord: string, + public editPlaceByPrettyCoordinates( + coordinates: string, color: JGOFNumericPlayerColor, isTrunkMove?: boolean, ): void { - for (const mv of this.engine.decodeMoves(coord)) { + for (const mv of this.engine.decodeMoves(coordinates)) { this.engine.editPlace(mv.x, mv.y, color, isTrunkMove); } } - public placeByPrettyCoord(coord: string): void { - for (const mv of this.engine.decodeMoves(coord)) { + 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, @@ -1508,8 +1507,12 @@ export abstract class GobanInteractive extends GobanBase { } } } - public setMarkByPrettyCoord(coord: string, mark: number | string, dont_draw?: boolean): void { - for (const mv of this.engine.decodeMoves(coord)) { + 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); } } diff --git a/src/Goban/OGSConnectivity.ts b/src/Goban/OGSConnectivity.ts index 823927fd..132fd194 100644 --- a/src/Goban/OGSConnectivity.ts +++ b/src/Goban/OGSConnectivity.ts @@ -41,10 +41,10 @@ import { JGOFPlayerSummary, JGOFTimeControl, MarkInterface, - Move, niceInterval, ReviewMessage, ScoreEstimator, + JGOFMove, } from "engine"; import { //ServerToClient, @@ -636,7 +636,7 @@ export abstract class OGSConnectivity extends GobanInteractive { } else { const removed = cfg.removed; const stones = cfg.stones; - let moves: Array; + let moves: JGOFMove[]; if (!stones) { moves = []; } else { diff --git a/src/Goban/SVGRenderer.ts b/src/Goban/SVGRenderer.ts index e0402072..4de0a164 100644 --- a/src/Goban/SVGRenderer.ts +++ b/src/Goban/SVGRenderer.ts @@ -342,7 +342,7 @@ export class SVGRenderer extends Goban implements GobanSVGInterface { const pt = this.xy2ij(pos.x, pos.y); if (callbacks.addCoordinatesToChatInput) { callbacks.addCoordinatesToChatInput( - this.engine.prettyCoords(pt.i, pt.j), + this.engine.prettyCoordinates(pt.i, pt.j), ); } } catch (e) { @@ -2799,7 +2799,7 @@ export class SVGRenderer extends Goban 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(GoMath.encodePrettyXCoordinate(c), x, y); } break; case "1-1": diff --git a/src/GobanBase.ts b/src/GobanBase.ts index 781e527e..b857a31e 100644 --- a/src/GobanBase.ts +++ b/src/GobanBase.ts @@ -35,8 +35,9 @@ import { JGOFPlayerSummary, JGOFSealingIntersection, JGOFNumericPlayerColor, + JGOFMove, } from "engine/formats/JGOF"; -import { AdHocPauseControl } from "engine/formats/AdHocFormat"; +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"; @@ -260,15 +261,19 @@ export interface GobanEvents extends StateUpdateEvents { * 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 { - /* Classes, types, and enums */ - static Engine = GoEngine; + /* Static functions */ static setTranslations = setGobanTranslations; static setCallbacks = setGobanCallbacks; - /** Actual fields **/ + /** Base fields **/ public readonly goban_id = ++last_goban_id; - public destroyed = false; + + 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: GoEngine; @@ -310,8 +315,52 @@ export abstract class GobanBase extends EventEmitter { public destroy() { this.emit("destroy"); - this.destroyed = true; + 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/engine/GoMath.ts b/src/engine/GoMath.ts index 602465b5..98702e39 100644 --- a/src/engine/GoMath.ts +++ b/src/engine/GoMath.ts @@ -17,7 +17,6 @@ import { JGOFIntersection, JGOFMove, JGOFNumericPlayerColor } from "./formats/JGOF"; import { AdHocPackedMove } from "./formats/AdHocFormat"; -export type Move = JGOFMove; export type Intersection = JGOFIntersection; export type Matrix = T[][]; export type NumberMatrix = Matrix; @@ -66,66 +65,79 @@ export function makeEmptyObjectMatrix(width: number, height: number): Array= 0) { - return pretty_coor_num2ch(x) + ("" + (board_height - y)); + return encodePrettyXCoordinate(x) + ("" + (board_height - y)); } return "pass"; } -export function decodeGTPCoordinate(move: string, width: number, height: number): 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 = pretty_coor_ch2num(move[0]); + const x = decodePrettyXCoordinate(move[0]); if (x === -1) { y = -1; } return { x, y }; } +export function decodePrettyCoordinates(move: string, height: number): JGOFMove { + return decodeGTPCoordinates(move, -1, height); +} -// 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) +/** + * 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: - | AdHocPackedMove | string - | Array - | [object] - | Array + | AdHocPackedMove + | AdHocPackedMove[] | JGOFMove + | JGOFMove[] + | [object] | undefined, width: number, height: number, -): Array { - const ret: Array = []; +): JGOFMove[] { + const ret: Array = []; + // undefined or empty string? return empty array. if (!move_obj) { return []; } - function decodeSingleMoveArray(arr: [number, number, number, number?, object?]): Move { - const obj: Move = { + 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, @@ -234,29 +246,29 @@ export function char2num(ch: string): number { if (ch === ".") { return -1; } - return coor_ch2num(ch); + return decodeCoordinate(ch); } function pretty_char2num(ch: string): number { if (ch === ".") { return -1; } - return pretty_coor_ch2num(ch); + return decodePrettyXCoordinate(ch); } export function num2char(num: number): string { if (num === -1) { return "."; } - return coor_num2ch(num); + return encodeCoordinate(num); } -export function encodeMove(x: number | Move, y?: number): string { +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: Move = x; + const mv: JGOFMove = x; if (!mv.edited) { return num2char(mv.x) + num2char(mv.y); @@ -266,7 +278,9 @@ export function encodeMove(x: number | Move, y?: number): string { } } -export function encodeMoves(lst: Array): string { +/* 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]); @@ -274,14 +288,7 @@ export function encodeMoves(lst: Array): string { 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 { +export function encodeMoveToArray(mv: JGOFMove): AdHocPackedMove { // Note: despite the name here, AdHocPackedMove became a tuple at some point! let extra: any = {}; if (mv.blur) { @@ -316,7 +323,7 @@ export function encodeMoveToArray(mv: Move): AdHocPackedMove { } return arr; } -export function encodeMovesToArray(moves: Array): Array { +export function encodeMovesToArray(moves: Array): Array { const ret: Array = []; for (let i = 0; i < moves.length; ++i) { ret.push(encodeMoveToArray(moves[i])); @@ -324,29 +331,6 @@ export function encodeMovesToArray(moves: Array): Array { 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. @@ -407,7 +391,7 @@ export function ojeSequenceToMoves(sequence: string): Array { if (play === "pass") { return { x: -1, y: -1 }; } - return decodeGTPCoordinate(play, 19, 19); + return decodeGTPCoordinates(play, 19, 19); }); return moves; diff --git a/src/engine/GobanEngine.ts b/src/engine/GobanEngine.ts index dfcf53cc..0a1165a4 100644 --- a/src/engine/GobanEngine.ts +++ b/src/engine/GobanEngine.ts @@ -17,7 +17,13 @@ import { BoardState, BoardConfig } from "./BoardState"; import { GobanMoveError } from "./GobanError"; import { MoveTree, MoveTreeJson } from "./MoveTree"; -import { Move, encodeMove } from "./GoMath"; +import { + decodeMoves, + decodePrettyCoordinates, + encodeMove, + encodeMoves, + prettyCoordinates, +} from "./GoMath"; import * as GoMath from "./GoMath"; import { RawStoneString } from "./StoneString"; import { ScoreEstimator } from "./ScoreEstimator"; @@ -325,7 +331,7 @@ export class GoEngine extends BoardState { }; 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; @@ -629,7 +635,7 @@ export class GoEngine extends BoardState { 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]); @@ -684,11 +690,54 @@ export class GoEngine extends BoardState { } } + /** + * 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); + move_obj: + | string + | AdHocPackedMove + | AdHocPackedMove[] + | JGOFMove + | JGOFMove[] + | [object] + | undefined, + ): JGOFMove[] { + return decodeMoves(move_obj, this.width, this.height); + } + + /* 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); } + private getState(): BoardState { return this.cloneBoardState(); } @@ -902,7 +951,7 @@ export class GoEngine extends BoardState { 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({ @@ -983,9 +1032,6 @@ export class GoEngine extends BoardState { private opponent(): JGOFNumericPlayerColor { return this.player === 1 ? 2 : 1; } - public prettyCoords(x: number, y: number): string { - return GoMath.prettyCoords(x, y, this.height); - } private captureGroup(group: RawStoneString): number { for (let i = 0; i < group.length; ++i) { @@ -1086,7 +1132,7 @@ export class GoEngine extends BoardState { 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: " + @@ -1102,7 +1148,7 @@ export class GoEngine extends BoardState { 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", ); } @@ -1130,7 +1176,7 @@ export class GoEngine extends BoardState { throw new GobanMoveError( this.game_id || this.review_id || 0, this.cur_move?.move_number ?? -1, - this.prettyCoords(x, y), + this.prettyCoordinates(x, y), "move_is_suicidal", ); } @@ -1142,7 +1188,7 @@ export class GoEngine extends BoardState { 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", ); } @@ -1156,7 +1202,7 @@ export class GoEngine extends BoardState { 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", ); } diff --git a/src/engine/MoveTree.ts b/src/engine/MoveTree.ts index 1d1baa7b..e509f660 100644 --- a/src/engine/MoveTree.ts +++ b/src/engine/MoveTree.ts @@ -17,7 +17,7 @@ import * as GoMath from "./GoMath"; import { GoEngine } from "./GobanEngine"; import { BoardState } from "./BoardState"; -import { encodeMove } from "./GoMath"; +import { encodeCoordinate, encodeMove } from "./GoMath"; import { AdHocPackedMove } from "./formats/AdHocFormat"; import { JGOFNumericPlayerColor, JGOFPlayerSummary } from "./formats/JGOF"; import { escapeSGFText, newlines_to_spaces } from "./util"; @@ -149,7 +149,7 @@ export class MoveTree { 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; @@ -680,8 +680,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); @@ -708,7 +708,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 = GoMath.encodeCoordinate(x) + GoMath.encodeCoordinate(y); if (m.triangle) { ret.push("TR[" + pos + "]"); } @@ -1061,7 +1061,7 @@ export class MoveTree { const moves = GoMath.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 += GoMath.prettyCoordinates(moves[i].x, moves[i].y, height) + " "; } return message.name + ". From move " + message.from + ": " + move_str; diff --git a/src/engine/autoscore.ts b/src/engine/autoscore.ts index 00c7d5d2..e33c9c43 100644 --- a/src/engine/autoscore.ts +++ b/src/engine/autoscore.ts @@ -23,7 +23,7 @@ import { StoneStringBuilder } from "./StoneStringBuilder"; import { JGOFNumericPlayerColor, JGOFSealingIntersection, JGOFMove } from "./formats/JGOF"; -import { char2num, makeMatrix, num2char, pretty_coor_num2ch } from "./GoMath"; +import { char2num, makeMatrix, num2char, encodePrettyXCoordinate } from "./GoMath"; import { GoEngine, GoEngineInitialState, GoEngineRules } from "./GobanEngine"; import { BoardState } from "./BoardState"; @@ -123,7 +123,7 @@ export function autoscore( removed.push({ x, y, removal_reason }); board[y][x] = JGOFNumericPlayerColor.EMPTY; removal[y][x] = true; - stage_log(`Removing ${pretty_coor_num2ch(x)}${height - y}: ${removal_reason}`); + stage_log(`Removing ${encodePrettyXCoordinate(x)}${height - y}: ${removal_reason}`); } /* diff --git a/test/test_autoscore.ts b/test/test_autoscore.ts index 780ee5b4..d6025566 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,15 +27,11 @@ */ import { existsSync, readFileSync, readdirSync } from "fs"; -import { autoscore } from "../src/engine/autoscore"; +import { autoscore } from "engine/autoscore"; import * as clc from "cli-color"; -import { GoEngine, GoEngineInitialState } from "../src/engine/GobanEngine"; -import { char2num, makeMatrix, num2char } from "../src/engine/GoMath"; -import { - JGOFMove, - JGOFNumericPlayerColor, - JGOFSealingIntersection, -} from "../src/engine/formats/JGOF"; +import { GoEngine, GoEngineInitialState } from "engine/GobanEngine"; +import { char2num, makeMatrix, num2char } from "engine/GoMath"; +import { JGOFMove, JGOFNumericPlayerColor, JGOFSealingIntersection } from "engine/formats/JGOF"; function run_autoscore_tests() { const test_file_directory = "autoscore_test_files"; diff --git a/test/unit_tests/GoMath.test.ts b/test/unit_tests/GoMath.test.ts index 09d93d4c..9708b5e2 100644 --- a/test/unit_tests/GoMath.test.ts +++ b/test/unit_tests/GoMath.test.ts @@ -1,6 +1,13 @@ //cspell: disable -import { StoneStringBuilder, JGOFNumericPlayerColor, GoMath, BoardState } from "engine"; +import { + StoneStringBuilder, + JGOFNumericPlayerColor, + GoMath, + BoardState, + decodePrettyCoordinates, + encodeMove, +} from "engine"; describe("GoStoneGroups constructor", () => { test("basic board state", () => { @@ -85,41 +92,41 @@ describe("matrices", () => { describe("prettyCoords", () => { test("pass", () => { - expect(GoMath.prettyCoords(-1, -1, 19)).toBe("pass"); + expect(GoMath.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(GoMath.prettyCoordinates(25, 9, 19)).toBe("undefined10"); + expect(GoMath.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(GoMath.prettyCoordinates(0, 0, 19)).toBe("A19"); + expect(GoMath.prettyCoordinates(2, 15, 19)).toBe("C4"); + expect(GoMath.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(GoMath.decodeGTPCoordinates("pass", 19, 19)).toEqual({ x: -1, y: -1 }); + expect(GoMath.decodeGTPCoordinates("..", 19, 19)).toEqual({ x: -1, y: -1 }); }); test("nonsense", () => { - expect(GoMath.decodeGTPCoordinate("&%", 19, 19)).toEqual({ x: -1, y: -1 }); + expect(GoMath.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(GoMath.decodeGTPCoordinates("a1", 19, 19)).toEqual({ x: 0, y: 18 }); + expect(GoMath.decodeGTPCoordinates("c4", 19, 19)).toEqual({ x: 2, y: 15 }); + expect(GoMath.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(GoMath.decodeGTPCoordinates("A1", 19, 19)).toEqual({ x: 0, y: 18 }); + expect(GoMath.decodeGTPCoordinates("C4", 19, 19)).toEqual({ x: 2, y: 15 }); + expect(GoMath.decodeGTPCoordinates("K10", 19, 19)).toEqual({ x: 9, y: 9 }); }); }); @@ -271,26 +278,26 @@ describe("encodeMove", () => { }); }); -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(".."); }); }); @@ -359,30 +366,6 @@ 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([]); From bdaf3d20465700f82a0fc1196a9b668f3c2bde97 Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Sun, 16 Jun 2024 07:21:33 -0600 Subject: [PATCH 48/68] Refactor: Split GoMath code into smaller util files --- src/Goban/CanvasRenderer.ts | 18 +- src/Goban/InteractiveBase.ts | 13 +- src/Goban/OGSConnectivity.ts | 7 +- src/Goban/SVGRenderer.ts | 18 +- src/engine/BoardState.ts | 2 +- src/engine/GobanEngine.ts | 25 +- src/engine/MoveTree.ts | 17 +- src/engine/ScoreEstimator.ts | 25 +- src/engine/StoneStringBuilder.ts | 4 +- src/engine/autoscore.ts | 2 +- src/engine/index.ts | 3 - .../ownership_estimators/voronoi_estimator.ts | 4 +- .../ownership_estimators/wasm_estimator.ts | 6 +- src/engine/util.ts | 292 ------------------ .../{AIReview.ts => util/ai_review_utils.ts} | 4 +- src/engine/util/color.ts | 45 +++ src/engine/util/computeAverageMoveTime.ts | 74 +++++ src/engine/util/coordinates.ts | 69 +++++ src/engine/util/duration_strings.ts | 37 +++ src/engine/util/getRandomInt.ts | 19 ++ src/engine/util/index.ts | 29 ++ src/engine/util/matrix.ts | 96 ++++++ .../{GoMath.ts => util/move_encoding.ts} | 214 +------------ src/engine/util/niceInterval.ts | 35 +++ src/engine/util/object_utils.ts | 77 +++++ src/engine/util/positionId.ts | 79 +++++ src/engine/util/sgf_utils.ts | 63 ++++ src/engine/util/sortMoves.ts | 43 +++ test/test_autoscore.ts | 3 +- test/unit_tests/GoMath.test.ts | 160 +++++----- test/unit_tests/GoMath_positionId.test.ts | 5 +- test/unit_tests/GobanCanvas.test.ts | 10 +- test/unit_tests/GobanSVG.test.ts | 10 +- test/unit_tests/StoneStringBuilder.test.ts | 4 +- tsconfig.json | 7 +- 35 files changed, 858 insertions(+), 661 deletions(-) delete mode 100644 src/engine/util.ts rename src/engine/{AIReview.ts => util/ai_review_utils.ts} (98%) create mode 100644 src/engine/util/color.ts create mode 100644 src/engine/util/computeAverageMoveTime.ts create mode 100644 src/engine/util/coordinates.ts create mode 100644 src/engine/util/duration_strings.ts create mode 100644 src/engine/util/getRandomInt.ts create mode 100644 src/engine/util/index.ts create mode 100644 src/engine/util/matrix.ts rename src/engine/{GoMath.ts => util/move_encoding.ts} (53%) create mode 100644 src/engine/util/niceInterval.ts create mode 100644 src/engine/util/object_utils.ts create mode 100644 src/engine/util/positionId.ts create mode 100644 src/engine/util/sgf_utils.ts create mode 100644 src/engine/util/sortMoves.ts diff --git a/src/Goban/CanvasRenderer.ts b/src/Goban/CanvasRenderer.ts index 94d1e72e..f1658427 100644 --- a/src/Goban/CanvasRenderer.ts +++ b/src/Goban/CanvasRenderer.ts @@ -20,7 +20,6 @@ import { AdHocFormat } from "engine/formats/AdHocFormat"; import { GobanConfig } from "../GobanBase"; import { GoEngine } from "engine"; -import * as GoMath from "engine/GoMath"; import { MoveTree } from "engine/MoveTree"; import { GobanTheme, THEMES } from "./themes"; import { MoveTreePenMarks } from "engine/MoveTree"; @@ -32,7 +31,14 @@ import { } from "./canvas_utils"; import { _ } from "engine/translate"; import { formatMessage, MessageID } from "engine/messages"; -import { color_blend, getRandomInt } from "engine/util"; +import { + color_blend, + encodeMove, + encodeMoves, + encodePrettyXCoordinate, + getRandomInt, + makeStringMatrix, +} from "engine/util"; import { callbacks } from "./callbacks"; import { Goban, GobanMetrics, GobanSelectedThemes, GOBAN_FONT } from "./Goban"; @@ -830,7 +836,7 @@ export class GobanCanvas extends Goban implements GobanCanvasInterface { } const sent = this.sendMove({ game_id: this.game_id, - move: GoMath.encodeMove(x, y), + move: encodeMove(x, y), }); if (sent) { this.playMovementSound(); @@ -866,7 +872,7 @@ export class GobanCanvas extends Goban implements GobanCanvasInterface { this.socket.send("game/removed_stones/set", { game_id: this.game_id, removed: removed, - stones: GoMath.encodeMoves(group), + stones: encodeMoves(group), }); } } else if (this.mode === "puzzle") { @@ -2764,7 +2770,7 @@ export class GobanCanvas extends Goban implements GobanCanvasInterface { this.square_size + this.square_size / 2; const y = j * this.square_size + this.square_size / 2; - place(GoMath.encodePrettyXCoordinate(c), x, y); + place(encodePrettyXCoordinate(c), x, y); } break; case "1-1": @@ -2888,7 +2894,7 @@ export class GobanCanvas extends Goban 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 = makeStringMatrix(this.width, this.height); } /* Set font for text overlay */ diff --git a/src/Goban/InteractiveBase.ts b/src/Goban/InteractiveBase.ts index 0535927e..a5001a55 100644 --- a/src/Goban/InteractiveBase.ts +++ b/src/Goban/InteractiveBase.ts @@ -25,8 +25,7 @@ import { ConditionalMoveTree, GobanMoveError, } from "engine"; -import { NumberMatrix, Intersection, encodeMove } from "engine/GoMath"; -import * as GoMath from "engine/GoMath"; +import { NumberMatrix, encodeMove, makeStringMatrix, makeEmptyObjectMatrix } from "engine/util"; import { MoveTree, MarkInterface } from "engine/MoveTree"; import { ScoreEstimator } from "engine/ScoreEstimator"; import { computeAverageMoveTime, niceInterval, matricesAreEqual } from "engine/util"; @@ -309,13 +308,13 @@ export abstract class GobanInteractive extends GobanBase { protected isPlayerOwner: () => boolean; protected label_character: string; protected label_mark: string = "[UNSET]"; - protected last_hover_square?: Intersection; + protected last_hover_square?: JGOFIntersection; 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 move_selected?: JGOFIntersection; protected no_display: boolean; protected onError?: (error: Error) => void; protected on_game_screen: boolean; @@ -400,7 +399,7 @@ export abstract class GobanInteractive extends GobanBase { this.pen_marks = []; this.config = repair_config(config); - this.__draw_state = GoMath.makeStringMatrix(this.width, this.height); + this.__draw_state = 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; @@ -767,7 +766,7 @@ export abstract class GobanInteractive extends GobanBase { 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 = makeStringMatrix(this.width, this.height); } this.chat_log = []; @@ -1423,7 +1422,7 @@ export abstract class GobanInteractive extends GobanBase { return; } - this.colored_circles = GoMath.makeEmptyObjectMatrix(this.width, this.height); + this.colored_circles = makeEmptyObjectMatrix(this.width, this.height); for (const circle of circles) { const mv = circle.move; this.colored_circles[mv.y][mv.x] = circle; diff --git a/src/Goban/OGSConnectivity.ts b/src/Goban/OGSConnectivity.ts index 132fd194..22ef92e0 100644 --- a/src/Goban/OGSConnectivity.ts +++ b/src/Goban/OGSConnectivity.ts @@ -27,13 +27,12 @@ import { AUTOSCORE_TRIALS, ConditionalMoveResponse, deepEqual, - dup, + deepClone, encodeMove, GobanSocket, GobanSocketEvents, ConditionalMoveTree, GoEngine, - GoMath, init_wasm_ownership_estimator, JGOFIntersection, JGOFPauseState, @@ -487,7 +486,7 @@ export abstract class OGSConnectivity extends GobanInteractive { if (this.engine.playerToMove() !== this.player_id) { const t = this.conditional_tree.getChild( - GoMath.encodeMove(the_move.x, the_move.y), + encodeMove(the_move.x, the_move.y), ); t.move = null; this.setConditionalTree(t); @@ -1110,7 +1109,7 @@ export abstract class OGSConnectivity extends GobanInteractive { m: diff.moves, k: marks, }; - const tmp = dup(msg); + const tmp = deepClone(msg); if (this.last_review_message.f === msg.f && this.last_review_message.m === msg.m) { delete msg["f"]; diff --git a/src/Goban/SVGRenderer.ts b/src/Goban/SVGRenderer.ts index 4de0a164..5fa9349c 100644 --- a/src/Goban/SVGRenderer.ts +++ b/src/Goban/SVGRenderer.ts @@ -21,14 +21,20 @@ import { AdHocFormat } from "engine/formats/AdHocFormat"; //import { GobanCore, GobanSelectedThemes, GobanMetrics, GOBAN_FONT } from "./GobanCore"; import { GobanConfig } from "../GobanBase"; import { GoEngine } from "engine"; -import * as GoMath from "engine/GoMath"; import { MoveTree } from "engine/MoveTree"; import { GobanTheme, THEMES } from "./themes"; import { MoveTreePenMarks } from "engine/MoveTree"; import { getRelativeEventPosition } from "./canvas_utils"; import { _ } from "engine/translate"; import { formatMessage, MessageID } from "engine/messages"; -import { color_blend, getRandomInt } from "engine/util"; +import { + color_blend, + encodeMove, + encodeMoves, + encodePrettyXCoordinate, + getRandomInt, + makeStringMatrix, +} from "engine/util"; import { callbacks } from "./callbacks"; import { Goban, GobanMetrics, GobanSelectedThemes } from "./Goban"; @@ -802,7 +808,7 @@ export class SVGRenderer extends Goban implements GobanSVGInterface { } const sent = this.sendMove({ game_id: this.game_id, - move: GoMath.encodeMove(x, y), + move: encodeMove(x, y), }); if (sent) { this.playMovementSound(); @@ -838,7 +844,7 @@ export class SVGRenderer extends Goban implements GobanSVGInterface { this.socket.send("game/removed_stones/set", { game_id: this.game_id, removed: removed, - stones: GoMath.encodeMoves(group), + stones: encodeMoves(group), }); } } else if (this.mode === "puzzle") { @@ -2799,7 +2805,7 @@ export class SVGRenderer extends Goban implements GobanSVGInterface { this.square_size + this.square_size / 2; const y = j * this.square_size + this.square_size / 2; - place(GoMath.encodePrettyXCoordinate(c), x, y); + place(encodePrettyXCoordinate(c), x, y); } break; case "1-1": @@ -2980,7 +2986,7 @@ export class SVGRenderer extends Goban 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 = makeStringMatrix(this.width, this.height); } for (let j = this.bounds.top; j <= this.bounds.bottom; ++j) { diff --git a/src/engine/BoardState.ts b/src/engine/BoardState.ts index 57fb3c91..fe3edb38 100644 --- a/src/engine/BoardState.ts +++ b/src/engine/BoardState.ts @@ -17,7 +17,7 @@ import { GobanEvents } from "../GobanBase"; import { EventEmitter } from "eventemitter3"; import { JGOFIntersection, JGOFNumericPlayerColor } from "./formats/JGOF"; -import { makeMatrix } from "./GoMath"; +import { makeMatrix } from "./util"; import * as goscorer from "goscorer"; import { StoneStringBuilder } from "./StoneStringBuilder"; import type { GobanBase } from "../GobanBase"; diff --git a/src/engine/GobanEngine.ts b/src/engine/GobanEngine.ts index 0a1165a4..99688924 100644 --- a/src/engine/GobanEngine.ts +++ b/src/engine/GobanEngine.ts @@ -22,9 +22,10 @@ import { decodePrettyCoordinates, encodeMove, encodeMoves, + positionId, prettyCoordinates, -} from "./GoMath"; -import * as GoMath from "./GoMath"; + sortMoves, +} from "./util"; import { RawStoneString } from "./StoneString"; import { ScoreEstimator } from "./ScoreEstimator"; import { GobanBase, GobanEvents } from "../GobanBase"; @@ -771,7 +772,7 @@ export class GoEngine extends BoardState { } public currentPositionId(): string { - return GoMath.positionId(this.board, this.height, this.width); + return positionId(this.board, this.height, this.width); } public followPath( @@ -964,7 +965,7 @@ export class GoEngine extends BoardState { } 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) { @@ -1322,7 +1323,7 @@ export class GoEngine extends BoardState { 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) { @@ -1331,7 +1332,7 @@ export class GoEngine extends BoardState { break; } } - this.initial_state.white = GoMath.encodeMoves(moves); + this.initial_state.white = encodeMoves(moves); /* Then add if applicable */ if (color) { @@ -1339,7 +1340,7 @@ export class GoEngine extends BoardState { 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); } } @@ -1401,7 +1402,7 @@ export class GoEngine extends BoardState { ret += arr[i]; } - return GoMath.sortMoves(ret, this.width, this.height); + return sortMoves(ret, this.width, this.height); } public getMoveNumber(): number { @@ -1474,14 +1475,14 @@ export class GoEngine extends BoardState { } else { ret.black.territory += 1; } - ret.black.scoring_positions += GoMath.encodeMove(x, y); + 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 += GoMath.encodeMove(x, y); + ret.white.scoring_positions += encodeMove(x, y); } } } @@ -1491,10 +1492,10 @@ export class GoEngine extends BoardState { for (let x = 0; x < this.width; ++x) { if (scoring[y][x].isTerritoryFor === goscorer.BLACK) { ret.black.territory += 1; - ret.black.scoring_positions += GoMath.encodeMove(x, y); + ret.black.scoring_positions += encodeMove(x, y); } else if (scoring[y][x].isTerritoryFor === goscorer.WHITE) { ret.white.territory += 1; - ret.white.scoring_positions += GoMath.encodeMove(x, y); + ret.white.scoring_positions += encodeMove(x, y); } } } diff --git a/src/engine/MoveTree.ts b/src/engine/MoveTree.ts index e509f660..f2c450f8 100644 --- a/src/engine/MoveTree.ts +++ b/src/engine/MoveTree.ts @@ -14,10 +14,15 @@ * limitations under the License. */ -import * as GoMath from "./GoMath"; import { GoEngine } from "./GobanEngine"; import { BoardState } from "./BoardState"; -import { encodeCoordinate, encodeMove } from "./GoMath"; +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"; @@ -596,7 +601,7 @@ export class MoveTree { this.marks = marks; } clearMarks(): Array> { - this.marks = GoMath.makeObjectMatrix(this.engine.width, this.engine.height); + this.marks = makeObjectMatrix(this.engine.width, this.engine.height); return this.marks; } hasMarks(): boolean { @@ -708,7 +713,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.encodeCoordinate(x) + GoMath.encodeCoordinate(y); + const pos = encodeCoordinate(x) + encodeCoordinate(y); if (m.triangle) { ret.push("TR[" + pos + "]"); } @@ -1058,10 +1063,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.prettyCoordinates(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/ScoreEstimator.ts b/src/engine/ScoreEstimator.ts index 809c4592..c3735cc3 100644 --- a/src/engine/ScoreEstimator.ts +++ b/src/engine/ScoreEstimator.ts @@ -14,8 +14,7 @@ * limitations under the License. */ -import { encodeMove } from "./GoMath"; -import * as GoMath from "./GoMath"; +import { encodeMove, makeMatrix, NumberMatrix } from "./util"; import { StoneString } from "./StoneString"; import { StoneStringBuilder } from "./StoneStringBuilder"; import type { GobanBase } from "../GobanBase"; @@ -83,7 +82,7 @@ type LocalEstimator = ( color_to_move: "black" | "white", trials: number, tolerance: number, -) => GoMath.NumberMatrix; +) => NumberMatrix; let local_ownership_estimator = wasm_estimate_ownership; export function set_local_ownership_estimator(estimator: LocalEstimator) { local_ownership_estimator = estimator; @@ -139,15 +138,15 @@ export class ScoreEstimator extends BoardState { this.engine = engine; this.color_to_move = engine.colorToMove(); this.board = engine.cloneBoard(); - this.ownership = GoMath.makeMatrix(this.width, this.height, 0); - this.territory = GoMath.makeMatrix(this.width, this.height, 0); + 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.territory = makeMatrix(this.width, this.height, 0); this.groups = new StoneStringBuilder(this); this.when_ready = this.estimateScore(this.trials, this.tolerance, autoscore); @@ -248,7 +247,7 @@ export class ScoreEstimator extends BoardState { tolerance = 0.25; } - const board = GoMath.makeMatrix(this.width, this.height, 0); + 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]; @@ -438,14 +437,14 @@ export class ScoreEstimator extends BoardState { } else { this.black.territory += 1; } - this.black.scoring_positions += GoMath.encodeMove(x, y); + 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 += GoMath.encodeMove(x, y); + this.white.scoring_positions += encodeMove(x, y); } } } @@ -463,10 +462,10 @@ export class ScoreEstimator extends BoardState { } else { if (scoring[y][x].isTerritoryFor === goscorer.BLACK) { this.black.territory += 1; - this.black.scoring_positions += GoMath.encodeMove(x, y); + this.black.scoring_positions += encodeMove(x, y); } else if (scoring[y][x].isTerritoryFor === goscorer.WHITE) { this.white.territory += 1; - this.white.scoring_positions += GoMath.encodeMove(x, y); + this.white.scoring_positions += encodeMove(x, y); } } } @@ -507,7 +506,7 @@ export function adjust_estimate( ) { let adjusted_score = score - engine.getHandicapPointAdjustmentForWhite(); const { width, height } = get_dimensions(board); - const ownership = GoMath.makeMatrix(width, height, 0); + 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 @@ -560,7 +559,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/StoneStringBuilder.ts b/src/engine/StoneStringBuilder.ts index 8dae8a24..b5aa757e 100644 --- a/src/engine/StoneStringBuilder.ts +++ b/src/engine/StoneStringBuilder.ts @@ -14,10 +14,10 @@ * limitations under the License. */ -import * as GoMath from "./GoMath"; import { StoneString } from "./StoneString"; import { BoardState } from "./BoardState"; import { JGOFNumericPlayerColor } from "./formats/JGOF"; +import { makeMatrix } from "./util"; export class StoneStringBuilder { private state: BoardState; @@ -26,7 +26,7 @@ export class StoneStringBuilder { 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 = GoMath.makeMatrix(state.width, state.height, 0); + const group_id_map = makeMatrix(state.width, state.height, 0); this.state = state; this.stone_string_id_map = group_id_map; diff --git a/src/engine/autoscore.ts b/src/engine/autoscore.ts index e33c9c43..49f7dc31 100644 --- a/src/engine/autoscore.ts +++ b/src/engine/autoscore.ts @@ -23,7 +23,7 @@ import { StoneStringBuilder } from "./StoneStringBuilder"; import { JGOFNumericPlayerColor, JGOFSealingIntersection, JGOFMove } from "./formats/JGOF"; -import { char2num, makeMatrix, num2char, encodePrettyXCoordinate } from "./GoMath"; +import { char2num, makeMatrix, num2char, encodePrettyXCoordinate } from "./util"; import { GoEngine, GoEngineInitialState, GoEngineRules } from "./GobanEngine"; import { BoardState } from "./BoardState"; diff --git a/src/engine/index.ts b/src/engine/index.ts index 02ff286b..a7162b11 100644 --- a/src/engine/index.ts +++ b/src/engine/index.ts @@ -16,13 +16,11 @@ export * from "./BoardState"; export * from "./GobanEngine"; -export * from "./AIReview"; export * from "./autoscore"; export * from "../GobanBase"; export * from "./GobanError"; export * from "./GobanSocket"; export * from "./ConditionalMoveTree"; -export * from "./GoMath"; export * from "./MoveTree"; export * from "./ownership_estimators"; export * from "./ScoreEstimator"; @@ -32,4 +30,3 @@ export * from "./formats"; export * from "./util"; export * as translate from "./translate"; -export * as GoMath from "./GoMath"; diff --git a/src/engine/ownership_estimators/voronoi_estimator.ts b/src/engine/ownership_estimators/voronoi_estimator.ts index bcbe10bb..48244e49 100644 --- a/src/engine/ownership_estimators/voronoi_estimator.ts +++ b/src/engine/ownership_estimators/voronoi_estimator.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { dup } from "../util"; +import { cloneMatrix } from "engine/util"; /** * This estimator simply marks territory for whichever color has a @@ -23,7 +23,7 @@ import { dup } from "../util"; */ 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/engine/ownership_estimators/wasm_estimator.ts b/src/engine/ownership_estimators/wasm_estimator.ts index 2180ad62..e24842cc 100644 --- a/src/engine/ownership_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; @@ -98,7 +98,7 @@ export function wasm_estimate_ownership( ) { 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()); diff --git a/src/engine/util.ts b/src/engine/util.ts deleted file mode 100644 index af0ec10b..00000000 --- a/src/engine/util.ts +++ /dev/null @@ -1,292 +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 "./formats/JGOF"; - -/** 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; -} - -/** 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; -} - -/** 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]) : "") - ); -} - -/** Deep clones an object */ -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; -} - -/** 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; - } -} - -/** - * 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); -} - -/** - * 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; - } -} - -/** - * 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); -} - -/* - * SPEC: https://www.red-bean.com/sgf/sgf4.html#text - * - * in sgf (as per spec): - * - slash is an escape char - * - closing bracket is a special symbol - * - whitespaces other than space & newline should be converted to space - * - in compose data type, we should also escape ':' - * (but that is only used in special SGF properties) - * - * so we gotta: - * - escape (double) all slashes in the text (so that they do not have the special meaning) - * - escape any closing brackets ] (as it closes e.g. the comment section) - * - replace whitespace - * - [opt] handle colon - */ -export function escapeSGFText(txt: string, escapeColon: boolean = false): string { - // escape slashes first - // 'blah\blah' -> 'blah\\blah' - txt = txt.replace(/\\/g, "\\\\"); - - // escape closing square bracket ] - // 'blah[9dan]' -> 'blah[9dan\]' - txt = txt.replace(/]/g, "\\]"); - - // no need to escape opening bracket, SGF grammar handles that - // 'C[[[[[[blah blah]' - // ^ after it finds the first [, it is only looking for the closing bracket - // parsing SGF properties, so the remaining [ are safely treated as text - //txt = txt.replace(/[/g, "\\["); - - // sub whitespace except newline & carriage return by space - txt = txt.replace(/[^\S\r\n]/g, " "); - - if (escapeColon) { - txt = txt.replace(/:/g, "\\:"); - } - return txt; -} - -/** - * 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, " "); -} - -/** 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/AIReview.ts b/src/engine/util/ai_review_utils.ts similarity index 98% rename from src/engine/AIReview.ts rename to src/engine/util/ai_review_utils.ts index 096a9963..f16f47f3 100644 --- a/src/engine/AIReview.ts +++ b/src/engine/util/ai_review_utils.ts @@ -14,8 +14,8 @@ * limitations under the License. */ -import { JGOFAIReview, JGOFIntersection, JGOFNumericPlayerColor } from "./formats/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..de5643b7 --- /dev/null +++ b/src/engine/util/matrix.ts @@ -0,0 +1,96 @@ +/* + * 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 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; +} diff --git a/src/engine/GoMath.ts b/src/engine/util/move_encoding.ts similarity index 53% rename from src/engine/GoMath.ts rename to src/engine/util/move_encoding.ts index 98702e39..2db31642 100644 --- a/src/engine/GoMath.ts +++ b/src/engine/util/move_encoding.ts @@ -14,104 +14,13 @@ * limitations under the License. */ -import { JGOFIntersection, JGOFMove, JGOFNumericPlayerColor } from "./formats/JGOF"; -import { AdHocPackedMove } from "./formats/AdHocFormat"; - -export type Intersection = JGOFIntersection; -export type Matrix = T[][]; -export type NumberMatrix = Matrix; -export type StringMatrix = Matrix; - -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 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; -} - -/* Lower case, includes i, used for our string encoding of moves */ -const COORDINATE_SEQUENCE = "abcdefghijklmnopqrstuvwxyz"; - -/** 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]; -} - -/* Upper case, and doesn't have I */ -const PRETTY_COORDINATE_SEQUENCE = "ABCDEFGHJKLMNOPQRSTUVWXYZ"; - -/** 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]; -} - -export function prettyCoordinates(x: number, y: number, board_height: number): string { - if (x >= 0) { - return encodePrettyXCoordinate(x) + ("" + (board_height - y)); - } - return "pass"; -} -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 }; -} -export function decodePrettyCoordinates(move: string, height: number): JGOFMove { - return decodeGTPCoordinates(move, -1, height); -} +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 @@ -331,54 +240,6 @@ export function encodeMovesToArray(moves: Array): 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(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((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; - }); - } else { - const arr = decodeMoves(moves, width, height); - arr.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(arr); - } -} - // OJE Sequence format is '.root.K10.Q1' ... export function ojeSequenceToMoves(sequence: string): Array { const plays = sequence.split("."); @@ -396,64 +257,3 @@ export function ojeSequenceToMoves(sequence: string): Array { 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/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/engine/util/sgf_utils.ts b/src/engine/util/sgf_utils.ts new file mode 100644 index 00000000..2aa6abe7 --- /dev/null +++ b/src/engine/util/sgf_utils.ts @@ -0,0 +1,63 @@ +/* + * 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. + */ + +/* + * SPEC: https://www.red-bean.com/sgf/sgf4.html#text + * + * in sgf (as per spec): + * - slash is an escape char + * - closing bracket is a special symbol + * - whitespaces other than space & newline should be converted to space + * - in compose data type, we should also escape ':' + * (but that is only used in special SGF properties) + * + * so we gotta: + * - escape (double) all slashes in the text (so that they do not have the special meaning) + * - escape any closing brackets ] (as it closes e.g. the comment section) + * - replace whitespace + * - [opt] handle colon + */ +export function escapeSGFText(txt: string, escapeColon: boolean = false): string { + // escape slashes first + // 'blah\blah' -> 'blah\\blah' + txt = txt.replace(/\\/g, "\\\\"); + + // escape closing square bracket ] + // 'blah[9dan]' -> 'blah[9dan\]' + txt = txt.replace(/]/g, "\\]"); + + // no need to escape opening bracket, SGF grammar handles that + // 'C[[[[[[blah blah]' + // ^ after it finds the first [, it is only looking for the closing bracket + // parsing SGF properties, so the remaining [ are safely treated as text + //txt = txt.replace(/[/g, "\\["); + + // sub whitespace except newline & carriage return by space + txt = txt.replace(/[^\S\r\n]/g, " "); + + if (escapeColon) { + txt = txt.replace(/:/g, "\\:"); + } + return txt; +} + +/** + * 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..954dfeb7 --- /dev/null +++ b/src/engine/util/sortMoves.ts @@ -0,0 +1,43 @@ +/* + * 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((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; + }); + } else { + const arr = decodeMoves(moves, width, height); + arr.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(arr); + } +} diff --git a/test/test_autoscore.ts b/test/test_autoscore.ts index d6025566..bb020343 100755 --- a/test/test_autoscore.ts +++ b/test/test_autoscore.ts @@ -29,8 +29,7 @@ import { existsSync, readFileSync, readdirSync } from "fs"; import { autoscore } from "engine/autoscore"; import * as clc from "cli-color"; -import { GoEngine, GoEngineInitialState } from "engine/GobanEngine"; -import { char2num, makeMatrix, num2char } from "engine/GoMath"; +import { GoEngine, GoEngineInitialState, char2num, makeMatrix, num2char } from "engine"; import { JGOFMove, JGOFNumericPlayerColor, JGOFSealingIntersection } from "engine/formats/JGOF"; function run_autoscore_tests() { diff --git a/test/unit_tests/GoMath.test.ts b/test/unit_tests/GoMath.test.ts index 9708b5e2..3982ffaa 100644 --- a/test/unit_tests/GoMath.test.ts +++ b/test/unit_tests/GoMath.test.ts @@ -3,10 +3,20 @@ import { StoneStringBuilder, JGOFNumericPlayerColor, - GoMath, BoardState, decodePrettyCoordinates, encodeMove, + decodeGTPCoordinates, + decodeMoves, + encodeMovesToArray, + encodeMoveToArray, + makeEmptyObjectMatrix, + makeMatrix, + makeObjectMatrix, + makeStringMatrix, + ojeSequenceToMoves, + prettyCoordinates, + sortMoves, } from "engine"; describe("GoStoneGroups constructor", () => { @@ -50,89 +60,89 @@ describe("GoStoneGroups constructor", () => { describe("matrices", () => { test("makeMatrix", () => { - expect(GoMath.makeMatrix(3, 2, 0)).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, 0)).toEqual([]); + expect(makeMatrix(0, 0, 0)).toEqual([]); }); test("makeStringMatrix", () => { - expect(GoMath.makeStringMatrix(3, 2)).toEqual([ + expect(makeStringMatrix(3, 2)).toEqual([ ["", "", ""], ["", "", ""], ]); - expect(GoMath.makeStringMatrix(3, 2, "asdf")).toEqual([ + expect(makeStringMatrix(3, 2, "asdf")).toEqual([ ["asdf", "asdf", "asdf"], ["asdf", "asdf", "asdf"], ]); - expect(GoMath.makeStringMatrix(0, 0)).toEqual([]); + expect(makeStringMatrix(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(makeEmptyObjectMatrix(3, 2)).toEqual([ [undefined, undefined, undefined], [undefined, undefined, undefined], ]); - expect(GoMath.makeEmptyObjectMatrix(0, 0)).toEqual([]); + expect(makeEmptyObjectMatrix(0, 0)).toEqual([]); }); }); describe("prettyCoords", () => { test("pass", () => { - expect(GoMath.prettyCoordinates(-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.prettyCoordinates(25, 9, 19)).toBe("undefined10"); - expect(GoMath.prettyCoordinates(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.prettyCoordinates(0, 0, 19)).toBe("A19"); - expect(GoMath.prettyCoordinates(2, 15, 19)).toBe("C4"); - expect(GoMath.prettyCoordinates(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.decodeGTPCoordinates("pass", 19, 19)).toEqual({ x: -1, y: -1 }); - expect(GoMath.decodeGTPCoordinates("..", 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.decodeGTPCoordinates("&%", 19, 19)).toEqual({ x: -1, y: -1 }); + expect(decodeGTPCoordinates("&%", 19, 19)).toEqual({ x: -1, y: -1 }); }); test("regular moves (lowercase)", () => { - expect(GoMath.decodeGTPCoordinates("a1", 19, 19)).toEqual({ x: 0, y: 18 }); - expect(GoMath.decodeGTPCoordinates("c4", 19, 19)).toEqual({ x: 2, y: 15 }); - expect(GoMath.decodeGTPCoordinates("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.decodeGTPCoordinates("A1", 19, 19)).toEqual({ x: 0, y: 18 }); - expect(GoMath.decodeGTPCoordinates("C4", 19, 19)).toEqual({ x: 2, y: 15 }); - expect(GoMath.decodeGTPCoordinates("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 }, @@ -140,76 +150,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 }, @@ -225,7 +233,7 @@ describe("decodeMoves", () => { test("Array", () => { expect( - GoMath.decodeMoves( + decodeMoves( [ [1, 2, 2048, 2, { blur: 1234 }], [3, 4, 2048, 1], @@ -241,39 +249,39 @@ 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"); }); }); @@ -303,28 +311,26 @@ describe("decodePrettyCoord", () => { 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, @@ -356,7 +362,7 @@ describe("encodeMoveToArray", () => { test("encodeMovesToArray", () => { expect( - GoMath.encodeMovesToArray([ + encodeMovesToArray([ { x: 4, y: 4, timedelta: 2048 }, { x: 3, y: 3, timedelta: 1024 }, ]), @@ -366,14 +372,15 @@ test("encodeMovesToArray", () => { ]); }); +/* 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, @@ -395,7 +402,7 @@ describe("trimJGOFMoves", () => { test("trims played_by", () => { expect( - GoMath.trimJGOFMoves([ + trimJGOFMoves([ { x: 1, y: 1, @@ -412,7 +419,7 @@ describe("trimJGOFMoves", () => { test("trims edited=false", () => { expect( - GoMath.trimJGOFMoves([ + trimJGOFMoves([ { x: 1, y: 1, @@ -429,7 +436,7 @@ describe("trimJGOFMoves", () => { test("trims color=0", () => { expect( - GoMath.trimJGOFMoves([ + trimJGOFMoves([ { x: 1, y: 1, @@ -452,7 +459,7 @@ describe("trimJGOFMoves", () => { edited: false, }, ]; - GoMath.trimJGOFMoves(arr); + trimJGOFMoves(arr); expect(arr).toEqual([ { x: 1, @@ -462,41 +469,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"); }); @@ -512,6 +520,6 @@ describe("ojeSequenceToMoves", () => { ], ], ])("id of %s", (sequence, id) => { - expect(GoMath.ojeSequenceToMoves(sequence)).toStrictEqual(id); + expect(ojeSequenceToMoves(sequence)).toStrictEqual(id); }); }); diff --git a/test/unit_tests/GoMath_positionId.test.ts b/test/unit_tests/GoMath_positionId.test.ts index 834e0662..da71be0c 100644 --- a/test/unit_tests/GoMath_positionId.test.ts +++ b/test/unit_tests/GoMath_positionId.test.ts @@ -1,7 +1,6 @@ // cspell: disable -import { GoMath } from "engine"; -import { JGOFNumericPlayerColor } from "engine"; +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/test/unit_tests/GobanCanvas.test.ts b/test/unit_tests/GobanCanvas.test.ts index 98a34112..40d694cd 100644 --- a/test/unit_tests/GobanCanvas.test.ts +++ b/test/unit_tests/GobanCanvas.test.ts @@ -7,7 +7,7 @@ import { SCORE_ESTIMATION_TOLERANCE, SCORE_ESTIMATION_TRIALS, } from "../../src/Goban/InteractiveBase"; -import { GobanSocket, GoMath } from "engine"; +import { GobanSocket, makeMatrix } from "engine"; import { GobanBase } from "../../src/GobanBase"; import WS from "jest-websocket-mock"; @@ -417,10 +417,10 @@ describe("onTap", () => { const mock_score_estimate = { handleClick: jest.fn(), when_ready: Promise.resolve(), - board: GoMath.makeMatrix(4, 2, 0), - removal: GoMath.makeMatrix(4, 2, false), - territory: GoMath.makeMatrix(4, 2, 0), - ownership: GoMath.makeMatrix(4, 2, 0), + 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); diff --git a/test/unit_tests/GobanSVG.test.ts b/test/unit_tests/GobanSVG.test.ts index 5b52f4ca..74301e2c 100644 --- a/test/unit_tests/GobanSVG.test.ts +++ b/test/unit_tests/GobanSVG.test.ts @@ -7,7 +7,7 @@ import { SCORE_ESTIMATION_TOLERANCE, SCORE_ESTIMATION_TRIALS, } from "../../src/Goban/InteractiveBase"; -import { GobanSocket, GoMath } from "engine"; +import { GobanSocket, makeMatrix } from "engine"; import { GobanBase } from "../../src/GobanBase"; import WS from "jest-websocket-mock"; @@ -416,10 +416,10 @@ describe("onTap", () => { const mock_score_estimate = { handleClick: jest.fn(), when_ready: Promise.resolve(), - board: GoMath.makeMatrix(4, 2, 0), - removal: GoMath.makeMatrix(4, 2, 0), - territory: GoMath.makeMatrix(4, 2, 0), - ownership: GoMath.makeMatrix(4, 2, 0), + 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); diff --git a/test/unit_tests/StoneStringBuilder.test.ts b/test/unit_tests/StoneStringBuilder.test.ts index bcd29cba..a83f5f9c 100644 --- a/test/unit_tests/StoneStringBuilder.test.ts +++ b/test/unit_tests/StoneStringBuilder.test.ts @@ -1,4 +1,4 @@ -import { GoMath, StoneStringBuilder, BoardState } from "engine"; +import { StoneStringBuilder, BoardState, makeMatrix } from "engine"; // Here is a board displaying many of the features GoStoneGroup cares about. @@ -23,7 +23,7 @@ const FEATURE_BOARD = [ [2, 2, 1, 2, 0], ]; -const REMOVAL = GoMath.makeMatrix(5, 5, false); +const REMOVAL = makeMatrix(5, 5, false); function makeGoMathWithFeatureBoard() { return new StoneStringBuilder( diff --git a/tsconfig.json b/tsconfig.json index c549ba2a..5fbf6724 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -33,7 +33,12 @@ "sourceMap": true, "jsx": "react" }, - "files": ["src/index.ts", "src/engine/index.ts", "jest.config.ts"], + "files": [ + "src/index.ts", + "src/engine/index.ts", + "jest.config.ts", + "./src/engine/util/getRandomInt.ts" + ], "include": ["test/**/*.ts"], "ts-node": { "require": ["tsconfig-paths/register"] From 8e4b17b75d0e66ca439dfe2900dd1b0582704fea Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Sun, 16 Jun 2024 07:26:51 -0600 Subject: [PATCH 49/68] Refactor: GoEngine -> GobanEngine --- src/Goban/CanvasRenderer.ts | 6 +- src/Goban/InteractiveBase.ts | 24 +++---- src/Goban/OGSConnectivity.ts | 6 +- src/Goban/SVGRenderer.ts | 6 +- src/Goban/TestGoban.ts | 6 +- src/GobanBase.ts | 18 +++--- src/engine/BoardState.ts | 2 +- src/engine/GobanEngine.ts | 88 +++++++++++++------------- src/engine/MoveTree.ts | 10 +-- src/engine/ScoreEstimator.ts | 10 +-- src/engine/autoscore.ts | 10 +-- src/engine/protocol/ClientToServer.ts | 2 +- src/engine/protocol/ServerToClient.ts | 4 +- test/test_autoscore.ts | 8 +-- test/unit_tests/GoEngine.test.ts | 84 +++++++++++++----------- test/unit_tests/ScoreEstimator.test.ts | 18 +++--- 16 files changed, 155 insertions(+), 147 deletions(-) diff --git a/src/Goban/CanvasRenderer.ts b/src/Goban/CanvasRenderer.ts index f1658427..ed2ec4fd 100644 --- a/src/Goban/CanvasRenderer.ts +++ b/src/Goban/CanvasRenderer.ts @@ -19,7 +19,7 @@ import { JGOF, JGOFIntersection, JGOFNumericPlayerColor } from "engine/formats/J import { AdHocFormat } from "engine/formats/AdHocFormat"; import { GobanConfig } from "../GobanBase"; -import { GoEngine } from "engine"; +import { GobanEngine } from "engine"; import { MoveTree } from "engine/MoveTree"; import { GobanTheme, THEMES } from "./themes"; import { MoveTreePenMarks } from "engine/MoveTree"; @@ -70,7 +70,7 @@ interface ViewPortInterface { const HOT_PINK = "#ff69b4"; export interface GobanCanvasInterface { - engine: GoEngine; + engine: GobanEngine; move_tree_container?: HTMLElement; clearAnalysisDrawing(): void; @@ -97,7 +97,7 @@ export interface GobanCanvasInterface { } export class GobanCanvas extends Goban implements GobanCanvasInterface { - public engine: GoEngine; + public engine: GobanEngine; //private board_div: HTMLElement; private board: HTMLCanvasElement; private __set_board_height: number = -1; diff --git a/src/Goban/InteractiveBase.ts b/src/Goban/InteractiveBase.ts index a5001a55..0bd788a8 100644 --- a/src/Goban/InteractiveBase.ts +++ b/src/Goban/InteractiveBase.ts @@ -15,9 +15,9 @@ */ import { - GoEngine, - GoEnginePhase, - GoEngineRules, + GobanEngine, + GobanEnginePhase, + GobanEngineRules, ReviewMessage, PlayerColor, PuzzlePlacementSetting, @@ -107,7 +107,7 @@ export interface MoveCommand { export interface StateUpdateEvents { mode: (d: GobanModes) => void; title: (d: string) => void; - phase: (d: GoEnginePhase) => void; + phase: (d: GobanEnginePhase) => void; cur_move: (d: MoveTree) => void; cur_review_move: (d: MoveTree | undefined) => void; last_official_move: (d: MoveTree) => void; @@ -116,7 +116,7 @@ export interface StateUpdateEvents { analyze_subtool: (d: AnalysisSubTool) => void; score_estimate: (d: ScoreEstimator | null) => void; strict_seki_mode: (d: boolean) => void; - rules: (d: GoEngineRules) => 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; @@ -310,7 +310,7 @@ export abstract class GobanInteractive extends GobanBase { protected label_mark: string = "[UNSET]"; protected last_hover_square?: JGOFIntersection; protected last_move?: MoveTree; - protected last_phase?: GoEnginePhase; + protected last_phase?: GobanEnginePhase; protected last_review_message: ReviewMessage; protected last_sound_played_for_a_stone_placement?: string; protected last_stone_sound: number; @@ -502,8 +502,8 @@ export abstract class GobanInteractive extends GobanBase { * 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 { - let ret: GoEngine; + protected post_config_constructor(): GobanEngine { + let ret: GobanEngine; delete this.current_cmove; /* set in setConditionalTree */ this.currently_my_cmove = false; @@ -720,7 +720,7 @@ export abstract class GobanInteractive extends GobanBase { } } - public load(config: GobanConfig): GoEngine { + public load(config: GobanConfig): GobanEngine { config = repair_config(config); for (const k in config) { (this.config as any)[k] = (config as any)[k]; @@ -819,7 +819,7 @@ export abstract class GobanInteractive extends GobanBase { // 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); + const new_engine = new GobanEngine(config); /* if (old_engine) { @@ -848,7 +848,7 @@ export abstract class GobanInteractive extends GobanBase { // 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.engine = new GobanEngine(config, this); this.emit("engine.updated", this.engine); this.engine.parentEventEmitter = this; } @@ -1391,7 +1391,7 @@ export abstract class GobanInteractive extends GobanBase { }); } } - /** This is a callback that gets called by GoEngine.getState to save and + /** 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. */ /* diff --git a/src/Goban/OGSConnectivity.ts b/src/Goban/OGSConnectivity.ts index 22ef92e0..b9646ad9 100644 --- a/src/Goban/OGSConnectivity.ts +++ b/src/Goban/OGSConnectivity.ts @@ -32,7 +32,7 @@ import { GobanSocket, GobanSocketEvents, ConditionalMoveTree, - GoEngine, + GobanEngine, init_wasm_ownership_estimator, JGOFIntersection, JGOFPauseState, @@ -85,7 +85,7 @@ export abstract class OGSConnectivity extends GobanInteractive { }); } - protected override post_config_constructor(): GoEngine { + protected override post_config_constructor(): GobanEngine { const ret = super.post_config_constructor(); if ("server_socket" in this.config && this.config["server_socket"]) { @@ -1336,7 +1336,7 @@ export abstract class OGSConnectivity extends GobanInteractive { } } - /** This is a callback that gets called by GoEngine.setState to load + /** This is a callback that gets called by GobanEngine.setState to load * previously saved board state. */ //public setState(state: any): void { public setState(): void { diff --git a/src/Goban/SVGRenderer.ts b/src/Goban/SVGRenderer.ts index 5fa9349c..cd1f80b4 100644 --- a/src/Goban/SVGRenderer.ts +++ b/src/Goban/SVGRenderer.ts @@ -20,7 +20,7 @@ import { AdHocFormat } from "engine/formats/AdHocFormat"; //import { GobanCore, GobanSelectedThemes, GobanMetrics, GOBAN_FONT } from "./GobanCore"; import { GobanConfig } from "../GobanBase"; -import { GoEngine } from "engine"; +import { GobanEngine } from "engine"; import { MoveTree } from "engine/MoveTree"; import { GobanTheme, THEMES } from "./themes"; import { MoveTreePenMarks } from "engine/MoveTree"; @@ -70,7 +70,7 @@ const HOT_PINK = "#ff69b4"; //interface GobanCanvasInterface { interface GobanSVGInterface { - engine: GoEngine; + engine: GobanEngine; move_tree_container?: HTMLElement; clearAnalysisDrawing(): void; @@ -97,7 +97,7 @@ interface GobanSVGInterface { } export class SVGRenderer extends Goban implements GobanSVGInterface { - public engine: GoEngine; + public engine: GobanEngine; //private board_div: HTMLElement; private svg: SVGElement; private svg_defs: SVGDefsElement; diff --git a/src/Goban/TestGoban.ts b/src/Goban/TestGoban.ts index a4220a57..d8621acb 100644 --- a/src/Goban/TestGoban.ts +++ b/src/Goban/TestGoban.ts @@ -16,7 +16,7 @@ */ import { GobanConfig } from "../GobanBase"; -import { GoEngine } from "engine/GobanEngine"; +import { GobanEngine } from "engine/GobanEngine"; import { MessageID } from "engine/messages"; import { MoveTreePenMarks } from "engine/MoveTree"; import { Goban, GobanSelectedThemes } from "./Goban"; @@ -25,12 +25,12 @@ import { Goban, GobanSelectedThemes } from "./Goban"; * This is a minimal implementation of Goban, primarily used for unit tests. */ export class TestGoban extends Goban { - public engine: GoEngine; + public engine: GobanEngine; constructor(config: GobanConfig) { super(config); - this.engine = new GoEngine(config); + this.engine = new GobanEngine(config); } public enablePen(): void {} diff --git a/src/GobanBase.ts b/src/GobanBase.ts index b857a31e..404dafee 100644 --- a/src/GobanBase.ts +++ b/src/GobanBase.ts @@ -15,10 +15,10 @@ */ import { - GoEngine, - GoEngineConfig, - GoEnginePhase, - GoEngineRules, + GobanEngine, + GobanEngineConfig, + GobanEnginePhase, + GobanEngineRules, PlayerColor, PuzzleConfig, PuzzlePlacementSetting, @@ -66,7 +66,7 @@ export interface GobanBounds { export type GobanChatLog = Array; -export interface GobanConfig extends GoEngineConfig, PuzzleConfig { +export interface GobanConfig extends GobanEngineConfig, PuzzleConfig { display_width?: number; interactive?: boolean; @@ -160,7 +160,7 @@ export interface JGOFClockWithTransmitting extends JGOFClock { export interface StateUpdateEvents { mode: (d: GobanModes) => void; title: (d: string) => void; - phase: (d: GoEnginePhase) => void; + phase: (d: GobanEnginePhase) => void; cur_move: (d: MoveTree) => void; cur_review_move: (d: MoveTree | undefined) => void; last_official_move: (d: MoveTree) => void; @@ -169,7 +169,7 @@ export interface StateUpdateEvents { analyze_subtool: (d: AnalysisSubTool) => void; score_estimate: (d: ScoreEstimator | null) => void; strict_seki_mode: (d: boolean) => void; - rules: (d: GoEngineRules) => 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; @@ -187,7 +187,7 @@ export interface GobanEvents extends StateUpdateEvents { "error": (d: any) => void; "gamedata": (d: any) => void; "chat": (d: any) => void; - "engine.updated": (engine: GoEngine) => void; + "engine.updated": (engine: GobanEngine) => void; "load": (config: GobanConfig) => void; "show-message": (message: { formatted: string; @@ -276,7 +276,7 @@ export abstract class GobanBase extends EventEmitter { } /* The rest of these fields are for subclasses of Goban, namely used by the renderers */ - public abstract engine: GoEngine; + public abstract engine: GobanEngine; public abstract enablePen(): void; public abstract disablePen(): void; diff --git a/src/engine/BoardState.ts b/src/engine/BoardState.ts index fe3edb38..0cd7d7ac 100644 --- a/src/engine/BoardState.ts +++ b/src/engine/BoardState.ts @@ -57,7 +57,7 @@ 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:GoEngineRules = 'japanese'; + //public readonly rules:GobanEngineRules = 'japanese'; public readonly width: number = 19; public board: JGOFNumericPlayerColor[][]; public removal: boolean[][]; diff --git a/src/engine/GobanEngine.ts b/src/engine/GobanEngine.ts index 99688924..a717ea9f 100644 --- a/src/engine/GobanEngine.ts +++ b/src/engine/GobanEngine.ts @@ -49,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" @@ -72,7 +72,7 @@ export interface Score { black: PlayerScore; } -export interface GoEnginePlayerEntry { +export interface GobanEnginePlayerEntry { id: number; username: string; country?: string; @@ -92,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 extends BoardConfig { +export interface GobanEngineConfig extends BoardConfig { game_id?: number | string; review_id?: number; game_name?: string; @@ -107,24 +107,24 @@ export interface GoEngineConfig extends BoardConfig { 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; @@ -156,13 +156,13 @@ export interface GoEngineConfig extends BoardConfig { 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. */ @@ -203,7 +203,7 @@ export interface GoEngineConfig extends BoardConfig { white_player_id?: number; } -export interface GoEngineInitialState { +export interface GobanEngineInitialState { black?: string; white?: string; } @@ -257,7 +257,7 @@ 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. */ @@ -269,7 +269,7 @@ export interface PuzzleConfig extends BoardConfig { mode?: string; name?: string; puzzle_type?: string; - initial_state?: GoEngineInitialState; + initial_state?: GobanEngineInitialState; marks?: { [mark: string]: string }; puzzle_autoplace_delay?: number; puzzle_opponent_move_mode?: PuzzleOpponentMoveMode; @@ -291,14 +291,14 @@ export type PuzzlePlacementSetting = export type PlayerColor = "black" | "white"; -export class GoEngine extends BoardState { +export class GobanEngine extends BoardState { //public readonly players.black.id:number; //public readonly players.white.id:number; public throw_all_errors?: boolean; //public cur_review_move?: MoveTree; 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 = @@ -307,11 +307,11 @@ export class GoEngine extends BoardState { {}; /* 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_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 }, @@ -322,9 +322,9 @@ export class GoEngine extends BoardState { 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 rules:GoEngineRules = 'japanese'; + //public readonly rules:GobanEngineRules = 'japanese'; public time_control: JGOFTimeControl = { system: "none", speed: "correspondence", @@ -337,17 +337,17 @@ export class GoEngine extends BoardState { 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; } @@ -403,11 +403,11 @@ export class GoEngine extends BoardState { 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; } @@ -457,7 +457,7 @@ export class GoEngine extends BoardState { private allow_ko: boolean = false; private allow_self_capture: boolean = false; private allow_superko: boolean = false; - private superko_algorithm: GoEngineSuperKoAlgorithm = "psk"; + private superko_algorithm: GobanEngineSuperKoAlgorithm = "psk"; private dontStoreBoardHistory: boolean; public free_handicap_placement: boolean = false; private loading_sgf: boolean = false; @@ -472,14 +472,14 @@ export class GoEngine extends BoardState { public territory_included_in_sgf: boolean = false; constructor( - config: GoEngineConfig, + config: GobanEngineConfig, goban_callback?: GobanBase, dontStoreBoardHistory?: boolean, ) { super( - GoEngine.fillDefaults( - GoEngine.migrateConfig( - ((config: GoEngineConfig): GoEngineConfig => { + 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.. @@ -1244,7 +1244,7 @@ export class GoEngine extends BoardState { 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_player_to_move = this.player; const check_situational = superko_rule === "ssk"; @@ -1548,7 +1548,7 @@ export class GoEngine extends BoardState { * This function migrates old config's to whatever our current standard is * for configs. */ - private static migrateConfig(config: GoEngineConfig): GoEngineConfig { + private static migrateConfig(config: GobanEngineConfig): GobanEngineConfig { if (config.ladder !== config.ladder_id) { config.ladder_id = config.ladder; } @@ -1600,7 +1600,7 @@ export class GoEngine extends BoardState { * This function fills in default values for any missing fields in the * config. */ - public static fillDefaults(game_obj: GoEngineConfig): GoEngineConfig { + public static fillDefaults(game_obj: GobanEngineConfig): GobanEngineConfig { if (!("phase" in game_obj)) { game_obj.phase = "play"; } @@ -1608,7 +1608,7 @@ export class GoEngine extends BoardState { game_obj.rules = "japanese"; } - const defaults: GoEngineConfig = {}; + const defaults: GobanEngineConfig = {}; //defaults.history = []; defaults.game_id = 0; @@ -1902,7 +1902,7 @@ export class GoEngine extends BoardState { 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; @@ -2232,7 +2232,7 @@ export class GoEngine extends BoardState { case "RU": { instructions.push(() => { - let rules: GoEngineRules = "japanese"; + let rules: GobanEngineRules = "japanese"; switch (val.toLowerCase()) { case "japanese": diff --git a/src/engine/MoveTree.ts b/src/engine/MoveTree.ts index f2c450f8..3b189ad4 100644 --- a/src/engine/MoveTree.ts +++ b/src/engine/MoveTree.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { GoEngine } from "./GobanEngine"; +import { GobanEngine } from "./GobanEngine"; import { BoardState } from "./BoardState"; import { decodeMoves, @@ -114,7 +114,7 @@ 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; @@ -141,7 +141,7 @@ export class MoveTree { private isobranch_hash?: string; constructor( - engine: GoEngine, + engine: GobanEngine, trunk: boolean, x: number, y: number, @@ -213,8 +213,8 @@ export class MoveTree { } 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) { diff --git a/src/engine/ScoreEstimator.ts b/src/engine/ScoreEstimator.ts index c3735cc3..ad7a6718 100644 --- a/src/engine/ScoreEstimator.ts +++ b/src/engine/ScoreEstimator.ts @@ -18,7 +18,7 @@ import { encodeMove, makeMatrix, NumberMatrix } from "./util"; import { StoneString } from "./StoneString"; import { StoneStringBuilder } from "./StoneStringBuilder"; import type { GobanBase } from "../GobanBase"; -import { GoEngine, PlayerScore, GoEngineRules } from "./GobanEngine"; +import { GobanEngine, PlayerScore, GobanEngineRules } from "./GobanEngine"; import { JGOFMove, JGOFNumericPlayerColor, JGOFSealingIntersection } from "./formats/JGOF"; import { _ } from "./translate"; import { wasm_estimate_ownership, remote_estimate_ownership } from "./ownership_estimators"; @@ -35,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; @@ -108,7 +108,7 @@ export class ScoreEstimator extends BoardState { komi: 0, }; - engine: GoEngine; + engine: GobanEngine; private groups: StoneStringBuilder; tolerance: number; amount: number = NaN; @@ -126,7 +126,7 @@ export class ScoreEstimator extends BoardState { public autoscored_needs_sealing?: JGOFSealingIntersection[]; constructor( - engine: GoEngine, + engine: GobanEngine, goban_callback: GobanBase | undefined, trials: number, tolerance: number, @@ -499,7 +499,7 @@ export class ScoreEstimator extends BoardState { * @param score estimated score (not accounting for captures) */ export function adjust_estimate( - engine: GoEngine, + engine: GobanEngine, board: Array>, area_map: number[][], score: number, diff --git a/src/engine/autoscore.ts b/src/engine/autoscore.ts index 49f7dc31..8337ef3c 100644 --- a/src/engine/autoscore.ts +++ b/src/engine/autoscore.ts @@ -24,7 +24,7 @@ import { StoneStringBuilder } from "./StoneStringBuilder"; import { JGOFNumericPlayerColor, JGOFSealingIntersection, JGOFMove } from "./formats/JGOF"; import { char2num, makeMatrix, num2char, encodePrettyXCoordinate } from "./util"; -import { GoEngine, GoEngineInitialState, GoEngineRules } from "./GobanEngine"; +import { GobanEngine, GobanEngineInitialState, GobanEngineRules } from "./GobanEngine"; import { BoardState } from "./BoardState"; interface AutoscoreResults { @@ -51,7 +51,7 @@ function isBlack(ownership: number): boolean { export function autoscore( board: JGOFNumericPlayerColor[][], - rules: GoEngineRules, + rules: GobanEngineRules, black_plays_first_ownership: number[][], white_plays_first_ownership: number[][], ): [AutoscoreResults, DebugOutput] { @@ -508,11 +508,11 @@ export function autoscore( .map((p) => num2char(p.x) + num2char(p.y)) .join(""); - const real_initial_state: GoEngineInitialState = { + const real_initial_state: GobanEngineInitialState = { black: black_state, white: white_state, }; - const sealed_initial_state: GoEngineInitialState = { + const sealed_initial_state: GobanEngineInitialState = { black: sealed_black_state, white: sealed_white_state, }; @@ -520,7 +520,7 @@ export function autoscore( for (const initial_state of [sealed_initial_state, real_initial_state]) { const cur_ownership = makeMatrix(width, height, 0); - const engine = new GoEngine({ + const engine = new GobanEngine({ width: original_board[0].length, height: original_board.length, initial_state, diff --git a/src/engine/protocol/ClientToServer.ts b/src/engine/protocol/ClientToServer.ts index 7e1da6bd..12188c23 100644 --- a/src/engine/protocol/ClientToServer.ts +++ b/src/engine/protocol/ClientToServer.ts @@ -259,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 cancellation ) */ + * moves of the game. (See GobanEngine.gameCanBeCancelled for cancellation ) */ "game/cancel": (data: { /** The game id */ game_id: number; diff --git a/src/engine/protocol/ServerToClient.ts b/src/engine/protocol/ServerToClient.ts index 56f131e4..f3bb8cdf 100644 --- a/src/engine/protocol/ServerToClient.ts +++ b/src/engine/protocol/ServerToClient.ts @@ -22,7 +22,7 @@ import type { } from "./ClientToServer"; import type { JGOFTimeControl } from "../formats/JGOF"; import type { ConditionalMoveResponse } from "../ConditionalMoveTree"; -import type { GoEngineConfig, Score, ReviewMessage } from "../GobanEngine"; +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 @@ -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/test/test_autoscore.ts b/test/test_autoscore.ts index bb020343..64056249 100755 --- a/test/test_autoscore.ts +++ b/test/test_autoscore.ts @@ -29,7 +29,7 @@ import { existsSync, readFileSync, readdirSync } from "fs"; import { autoscore } from "engine/autoscore"; import * as clc from "cli-color"; -import { GoEngine, GoEngineInitialState, char2num, makeMatrix, num2char } from "engine"; +import { GobanEngine, GobanEngineInitialState, char2num, makeMatrix, num2char } from "engine"; import { JGOFMove, JGOFNumericPlayerColor, JGOFSealingIntersection } from "engine/formats/JGOF"; function run_autoscore_tests() { @@ -230,7 +230,7 @@ function test_file(path: string, quiet: boolean): boolean { } if (!quiet) { - // Double check that when we run everything through our normal GoEngine.computeScore function, + // 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") { @@ -257,12 +257,12 @@ function test_file(path: string, quiet: boolean): boolean { } } - const initial_state: GoEngineInitialState = { + const initial_state: GobanEngineInitialState = { black: black_state, white: white_state, }; - const engine = new GoEngine({ + const engine = new GobanEngine({ width: board[0].length, height: board.length, initial_state, diff --git a/test/unit_tests/GoEngine.test.ts b/test/unit_tests/GoEngine.test.ts index 5a53cb4e..18cbcf7c 100644 --- a/test/unit_tests/GoEngine.test.ts +++ b/test/unit_tests/GoEngine.test.ts @@ -1,6 +1,12 @@ //cspell: disable -import { GoEngine, GobanMoveError, JGOFIntersection, makeMatrix, matricesAreEqual } from "engine"; +import { + GobanEngine, + GobanMoveError, + JGOFIntersection, + makeMatrix, + matricesAreEqual, +} from "engine"; import { movesFromBoardState } from "./test_utils"; test("boardMatricesAreTheSame", () => { @@ -26,8 +32,8 @@ test("boardMatricesAreTheSame", () => { }); describe("computeScore", () => { - test("GoEngine defaults", () => { - const engine = new GoEngine({}); + test("GobanEngine defaults", () => { + const engine = new GobanEngine({}); expect(engine.computeScore()).toEqual({ black: { handicap: 0, @@ -50,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, @@ -75,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, @@ -93,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: // @@ -116,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({ @@ -142,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), @@ -173,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), @@ -204,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; @@ -214,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); @@ -250,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); @@ -260,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); @@ -273,7 +279,7 @@ describe("GoEngine.place()", () => { }); test("ko", () => { - const engine = new GoEngine({ + const engine = new GobanEngine({ width: 4, height: 3, initial_state: { @@ -297,7 +303,7 @@ describe("GoEngine.place()", () => { }); test("superko", () => { - const engine = new GoEngine({ + const engine = new GobanEngine({ rules: "chinese", initial_state: { black: "dabbcbdbccadbdcd", @@ -325,7 +331,7 @@ describe("GoEngine.place()", () => { }); test("suicide", () => { - const engine = new GoEngine({ + const engine = new GobanEngine({ width: 2, height: 2, initial_state: { @@ -349,7 +355,7 @@ describe("GoEngine.place()", () => { set: jest.fn(), }; - const engine = new GoEngine( + const engine = new GobanEngine( { width: 2, height: 2, @@ -372,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); @@ -385,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); @@ -408,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); @@ -424,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); @@ -448,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); @@ -462,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], @@ -477,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], @@ -500,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", () => { @@ -517,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], @@ -546,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); @@ -567,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); @@ -576,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], @@ -586,7 +594,7 @@ describe("moves", () => { }); test("deleteCurMove", () => { - const engine = new GoEngine({ + const engine = new GobanEngine({ width: 4, height: 2, }); @@ -605,7 +613,7 @@ describe("moves", () => { describe("groups", () => { test("toggleSingleGroupRemoval", () => { - const engine = new GoEngine({ + const engine = new GobanEngine({ width: 4, height: 4, initial_state: { black: "aabbdd", white: "cacbcccd" }, @@ -638,7 +646,7 @@ describe("groups", () => { }); test("toggleSingleGroupRemoval out-of-bounds", () => { - const engine = new GoEngine({ + const engine = new GobanEngine({ width: 4, height: 4, initial_state: { black: "aabbdd", white: "cacbcccd" }, @@ -659,7 +667,7 @@ describe("groups", () => { }); test("toggleSingleGroupRemoval empty area doesn't do anything", () => { - const engine = new GoEngine({ + const engine = new GobanEngine({ width: 4, height: 2, initial_state: { black: "aabb", white: "cacb" }, @@ -678,7 +686,7 @@ describe("groups", () => { }); test("clearRemoved", () => { - const engine = new GoEngine({ + const engine = new GobanEngine({ width: 4, height: 2, initial_state: { black: "aabb", white: "cacb" }, @@ -699,7 +707,7 @@ describe("groups", () => { }); test("clearRemoved", () => { - const engine = new GoEngine({ + const engine = new GobanEngine({ width: 4, height: 2, initial_state: { black: "aabb", white: "cacb" }, diff --git a/test/unit_tests/ScoreEstimator.test.ts b/test/unit_tests/ScoreEstimator.test.ts index f36cba8d..dfa8f169 100644 --- a/test/unit_tests/ScoreEstimator.test.ts +++ b/test/unit_tests/ScoreEstimator.test.ts @@ -1,6 +1,6 @@ //cspell: disable -import { GoEngine } from "engine"; +import { GobanEngine } from "engine"; import { makeMatrix } from "engine"; import { ScoreEstimator, adjust_estimate, set_local_ownership_estimator } from "engine"; import { @@ -25,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, @@ -39,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, @@ -53,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); @@ -115,7 +115,7 @@ 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); } @@ -162,7 +162,7 @@ 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); @@ -197,7 +197,7 @@ 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); @@ -321,7 +321,7 @@ 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); @@ -388,7 +388,7 @@ describe("ScoreEstimator", () => { 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" }, From e94b499c3fc30d0f74ef0bf82c1ffc16a07b196a Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Sun, 16 Jun 2024 07:47:15 -0600 Subject: [PATCH 50/68] Replaced suicide terminology with self capture --- src/Goban/InteractiveBase.ts | 2 +- src/engine/GobanEngine.ts | 12 ++++++------ src/engine/GobanError.ts | 2 +- src/engine/messages.ts | 2 +- test/unit_tests/GoEngine.test.ts | 4 ++-- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/Goban/InteractiveBase.ts b/src/Goban/InteractiveBase.ts index 0bd788a8..7ffecef8 100644 --- a/src/Goban/InteractiveBase.ts +++ b/src/Goban/InteractiveBase.ts @@ -446,7 +446,7 @@ export abstract class GobanInteractive extends GobanBase { return; } */ - if (e instanceof GobanMoveError && e.message_id === "move_is_suicidal") { + if (e instanceof GobanMoveError && e.message_id === "illegal_self_capture") { this.showMessage("self_capture_not_allowed", { error: e }, 5000); return; } else { diff --git a/src/engine/GobanEngine.ts b/src/engine/GobanEngine.ts index a717ea9f..b07cfb5a 100644 --- a/src/engine/GobanEngine.ts +++ b/src/engine/GobanEngine.ts @@ -1120,7 +1120,7 @@ export class GobanEngine extends BoardState { checkForKo?: boolean, errorOnSuperKo?: boolean, dontCheckForSuperKo?: boolean, - dontCheckForSuicide?: boolean, + dontCheckForSelfCapture?: boolean, isTrunkMove?: boolean, removed_stones?: Array, ): number { @@ -1155,7 +1155,7 @@ export class GobanEngine extends BoardState { } this.board[y][x] = this.player; - let suicide_move = false; + let self_capture_move = false; const player_group = this.getRawStoneString(x, y, true); const opponent_groups = this.getNeighboringRawStoneStrings(player_group); @@ -1169,16 +1169,16 @@ export class GobanEngine extends BoardState { } 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.prettyCoordinates(x, y), - "move_is_suicidal", + "illegal_self_capture", ); } } @@ -1210,7 +1210,7 @@ export class GobanEngine extends BoardState { } } - if (!suicide_move) { + if (!self_capture_move) { if (this.goban_callback) { this.goban_callback.set(x, y, this.player); } diff --git a/src/engine/GobanError.ts b/src/engine/GobanError.ts index d618736e..03f63730 100644 --- a/src/engine/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/engine/messages.ts b/src/engine/messages.ts index 87924f84..cdd59ef6 100644 --- a/src/engine/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/test/unit_tests/GoEngine.test.ts b/test/unit_tests/GoEngine.test.ts index 18cbcf7c..ead4325a 100644 --- a/test/unit_tests/GoEngine.test.ts +++ b/test/unit_tests/GoEngine.test.ts @@ -330,7 +330,7 @@ describe("GobanEngine.place()", () => { ); }); - test("suicide", () => { + test("self capture", () => { const engine = new GobanEngine({ width: 2, height: 2, @@ -346,7 +346,7 @@ describe("GobanEngine.place()", () => { */ expect(() => engine.place(0, 0)).toThrow( - new GobanMoveError(0, 0, "A2", "move_is_suicidal"), + new GobanMoveError(0, 0, "A2", "illegal_self_capture"), ); }); From 721a5dc3e9fa79b12f0b01cb9dfb8592b9a4d4b6 Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Sun, 16 Jun 2024 14:00:07 -0600 Subject: [PATCH 51/68] Fixed analysis scoring mode to set by color if the existing color didn't match the brush color; hoisted out of renders to Goban to de-duplicate. --- src/Goban/CanvasRenderer.ts | 66 ------------------------------------- src/Goban/Goban.ts | 66 +++++++++++++++++++++++++++++++++++++ src/Goban/SVGRenderer.ts | 66 ------------------------------------- 3 files changed, 66 insertions(+), 132 deletions(-) diff --git a/src/Goban/CanvasRenderer.ts b/src/Goban/CanvasRenderer.ts index ed2ec4fd..1f9d1da5 100644 --- a/src/Goban/CanvasRenderer.ts +++ b/src/Goban/CanvasRenderer.ts @@ -126,9 +126,6 @@ export class GobanCanvas extends Goban implements GobanCanvasInterface { private last_pen_position?: [number, number]; protected metrics: GobanMetrics = { width: NaN, height: NaN, mid: NaN, offset: NaN }; - private analysis_scoring_color?: "black" | "white" | string; - private analysis_scoring_last_position: { i: number; j: number } = { i: NaN, j: NaN }; - private drawing_enabled: boolean = true; private pen_ctx?: CanvasRenderingContext2D; private pen_layer?: HTMLCanvasElement; @@ -3201,69 +3198,6 @@ export class GobanCanvas extends Goban implements GobanCanvasInterface { } } - private 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.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, - ); - } - private 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); - } - } protected setTitle(title: string): void { this.title = title; if (this.title_div) { diff --git a/src/Goban/Goban.ts b/src/Goban/Goban.ts index 0b33a8a2..8953866a 100644 --- a/src/Goban/Goban.ts +++ b/src/Goban/Goban.ts @@ -57,6 +57,8 @@ export abstract class Goban extends OGSConnectivity { 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); @@ -302,4 +304,68 @@ export abstract class Goban extends OGSConnectivity { 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/SVGRenderer.ts b/src/Goban/SVGRenderer.ts index cd1f80b4..d641f51e 100644 --- a/src/Goban/SVGRenderer.ts +++ b/src/Goban/SVGRenderer.ts @@ -133,9 +133,6 @@ export class SVGRenderer extends Goban implements GobanSVGInterface { private last_pen_position?: [number, number]; protected metrics: GobanMetrics = { width: NaN, height: NaN, mid: NaN, offset: NaN }; - private analysis_scoring_color?: "black" | "white" | string; - private analysis_scoring_last_position: { i: number; j: number } = { i: NaN, j: NaN }; - private drawing_enabled: boolean = true; protected title_div?: HTMLElement; @@ -3213,69 +3210,6 @@ export class SVGRenderer extends Goban implements GobanSVGInterface { this.setLabelCharacterFromMarks(); } } - private 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.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, - ); - } - private 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); - } - } protected setTitle(title: string): void { this.title = title; if (this.title_div) { From efa2456a1317d0becec7b2ee05a66f622d08d25a Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Sun, 16 Jun 2024 14:39:53 -0600 Subject: [PATCH 52/68] Add mark stashing functions to MoveTree --- src/engine/MoveTree.ts | 42 ++++++++++++++++++++++++++++++++---------- 1 file changed, 32 insertions(+), 10 deletions(-) diff --git a/src/engine/MoveTree.ts b/src/engine/MoveTree.ts index 3b189ad4..fe505036 100644 --- a/src/engine/MoveTree.ts +++ b/src/engine/MoveTree.ts @@ -123,7 +123,7 @@ export class MoveTree { 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; @@ -136,7 +136,8 @@ 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; @@ -174,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, @@ -211,7 +213,9 @@ 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 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 @@ -238,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"); } @@ -585,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> { + 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; @@ -620,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; } From 95203c5a358188c5c88ba239087ef800acf45d84 Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Mon, 17 Jun 2024 06:12:05 -0600 Subject: [PATCH 53/68] Add code duplication detection npm/make target --- Makefile | 8 +- package.json | 7 +- yarn.lock | 420 +++++++++++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 420 insertions(+), 15 deletions(-) diff --git a/Makefile b/Makefile index 26a55571..a5f1b5eb 100644 --- a/Makefile +++ b/Makefile @@ -23,14 +23,20 @@ lint: test: yarn run test + +detect-duplicate-code: + 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 diff --git a/package.json b/package.json index 6bea7b4f..93079e33 100644 --- a/package.json +++ b/package.json @@ -26,9 +26,10 @@ "build-debug": "webpack", "build-production": "webpack --mode production", "dts": "dts-bundle-generator -o lib/goban.d.ts src/index.ts", + "detect-duplicate-code": "jscpd --min-tokens 50 src/", "lint": "eslint src/ --ext=.ts,.tsx", "lint:fix": "eslint --fix src/ --ext=.ts,.tsx", - "typedoc": "typedoc src/index.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}\"", @@ -74,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", @@ -86,7 +88,8 @@ "ts-node": "^10.9.1", "tsconfig-paths": "^4.2.0", "tslint": "^6.1.3", - "typedoc": "^0.25.6", + "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", diff --git a/yarn.lock b/yarn.lock index e2fe82e9..c388944e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -159,7 +159,7 @@ 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.7": +"@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== @@ -287,7 +287,7 @@ debug "^4.3.1" globals "^11.1.0" -"@babel/types@^7.0.0", "@babel/types@^7.20.7", "@babel/types@^7.24.7", "@babel/types@^7.3.3": +"@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== @@ -301,6 +301,11 @@ resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== +"@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" @@ -995,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" @@ -1320,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" @@ -1670,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" @@ -1845,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" @@ -1910,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" @@ -1930,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" @@ -2029,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== @@ -2119,6 +2200,13 @@ char-regex@^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" @@ -2187,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" @@ -2257,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" @@ -2279,6 +2381,11 @@ commander@^2.12.1, commander@^2.20.0: 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" @@ -2335,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" @@ -2395,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== @@ -2692,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" @@ -2742,6 +2862,13 @@ 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.17.0" resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.17.0.tgz#d037603789dd9555b89aaec7eb78845c49089bc5" @@ -3066,6 +3193,21 @@ events@^3.2.0: resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== +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.0" + get-stream "^5.0.0" + human-signals "^1.1.1" + is-stream "^2.0.0" + merge-stream "^2.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" resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd" @@ -3351,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" @@ -3434,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" @@ -3444,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" @@ -3567,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" @@ -3679,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" @@ -3813,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" @@ -3874,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" @@ -4358,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" @@ -4378,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" @@ -4486,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" @@ -4690,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" @@ -4934,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" @@ -4956,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== @@ -5012,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== @@ -5247,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" @@ -5268,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" @@ -5404,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" @@ -5451,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== @@ -5783,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" @@ -6111,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" @@ -6272,7 +6648,12 @@ type@^2.7.2: resolved "https://registry.yarnpkg.com/type/-/type-2.7.3.tgz#436981652129285cc3ba94f392886c2637ea0486" integrity sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ== -typedoc@^0.25.6: +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.13: version "0.25.13" resolved "https://registry.yarnpkg.com/typedoc/-/typedoc-0.25.13.tgz#9a98819e3b2d155a6d78589b46fa4c03768f0922" integrity sha512-pQqiwiJ+Z4pigfOnnysObszLiU3mVLWAExSPf+Mu06G/qsc3wzbuM56SZQvONhHLncLUhYzOVkjFFpFfL5AzhQ== @@ -6379,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" @@ -6603,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== @@ -6622,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" From 5d418ac70e596904e078dcea34c00b46d3e67eb7 Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Mon, 17 Jun 2024 06:13:14 -0600 Subject: [PATCH 54/68] Code de-duplication --- src/engine/util/sortMoves.ts | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/src/engine/util/sortMoves.ts b/src/engine/util/sortMoves.ts index 954dfeb7..6670d1ae 100644 --- a/src/engine/util/sortMoves.ts +++ b/src/engine/util/sortMoves.ts @@ -26,18 +26,16 @@ export function sortMoves( height: number, ): string | JGOFMove[] { if (moves instanceof Array) { - return 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 moves.sort(compare_moves); } else { const arr = decodeMoves(moves, width, height); - arr.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; - }); + 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; +} From 7307ee8cdbc3c005852b9af67be46b61e1839da7 Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Mon, 17 Jun 2024 06:15:46 -0600 Subject: [PATCH 55/68] Fixed typing for empty matrix creation --- src/Goban/CanvasRenderer.ts | 4 ++-- src/Goban/InteractiveBase.ts | 6 +++--- src/Goban/SVGRenderer.ts | 4 ++-- src/engine/util/matrix.ts | 5 ++--- test/unit_tests/GoMath.test.ts | 6 +++--- 5 files changed, 12 insertions(+), 13 deletions(-) diff --git a/src/Goban/CanvasRenderer.ts b/src/Goban/CanvasRenderer.ts index 1f9d1da5..4a592ce1 100644 --- a/src/Goban/CanvasRenderer.ts +++ b/src/Goban/CanvasRenderer.ts @@ -1498,8 +1498,8 @@ export class GobanCanvas extends Goban implements GobanCanvasInterface { /* Colored stones */ if (this.colored_circles) { - if (this.colored_circles[j][i]) { - const circle = this.colored_circles[j][i]; + const circle = this.colored_circles[j][i]; + if (circle) { const color = circle.color; ctx.save(); diff --git a/src/Goban/InteractiveBase.ts b/src/Goban/InteractiveBase.ts index 7ffecef8..629b794c 100644 --- a/src/Goban/InteractiveBase.ts +++ b/src/Goban/InteractiveBase.ts @@ -25,7 +25,7 @@ import { ConditionalMoveTree, GobanMoveError, } from "engine"; -import { NumberMatrix, encodeMove, makeStringMatrix, makeEmptyObjectMatrix } from "engine/util"; +import { NumberMatrix, encodeMove, makeStringMatrix, makeEmptyMatrix } from "engine/util"; import { MoveTree, MarkInterface } from "engine/MoveTree"; import { ScoreEstimator } from "engine/ScoreEstimator"; import { computeAverageMoveTime, niceInterval, matricesAreEqual } from "engine/util"; @@ -297,7 +297,7 @@ export abstract class GobanInteractive extends GobanBase { protected edit_color?: "black" | "white"; protected errorHandler: (e: Error) => void; protected heatmap?: NumberMatrix; - protected colored_circles?: Array>; + protected colored_circles?: Array>; protected game_type: string; protected getPuzzlePlacementSetting?: () => PuzzlePlacementSetting; protected highlight_movetree_moves: boolean; @@ -1422,7 +1422,7 @@ export abstract class GobanInteractive extends GobanBase { return; } - this.colored_circles = makeEmptyObjectMatrix(this.width, this.height); + 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; diff --git a/src/Goban/SVGRenderer.ts b/src/Goban/SVGRenderer.ts index d641f51e..dc10f9be 100644 --- a/src/Goban/SVGRenderer.ts +++ b/src/Goban/SVGRenderer.ts @@ -1427,8 +1427,8 @@ export class SVGRenderer extends Goban implements GobanSVGInterface { /* Colored stones */ if (this.colored_circles) { - if (this.colored_circles[j][i]) { - const circle = this.colored_circles[j][i]; + 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) { diff --git a/src/engine/util/matrix.ts b/src/engine/util/matrix.ts index de5643b7..0178ab1a 100644 --- a/src/engine/util/matrix.ts +++ b/src/engine/util/matrix.ts @@ -86,11 +86,10 @@ export function makeObjectMatrix(width: number, height: number): Array(width: number, height: number): Array> { +export function makeEmptyMatrix(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; + ret[y] = new Array(width); } return ret; } diff --git a/test/unit_tests/GoMath.test.ts b/test/unit_tests/GoMath.test.ts index 3982ffaa..187db61d 100644 --- a/test/unit_tests/GoMath.test.ts +++ b/test/unit_tests/GoMath.test.ts @@ -10,7 +10,7 @@ import { decodeMoves, encodeMovesToArray, encodeMoveToArray, - makeEmptyObjectMatrix, + makeEmptyMatrix, makeMatrix, makeObjectMatrix, makeStringMatrix, @@ -92,11 +92,11 @@ describe("matrices", () => { }); test("makeEmptyObjectMatrix", () => { - expect(makeEmptyObjectMatrix(3, 2)).toEqual([ + expect(makeEmptyMatrix(3, 2)).toEqual([ [undefined, undefined, undefined], [undefined, undefined, undefined], ]); - expect(makeEmptyObjectMatrix(0, 0)).toEqual([]); + expect(makeEmptyMatrix(0, 0)).toEqual([]); }); }); From 9ac475b6434541e26b476320ad1f22829a284a56 Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Mon, 17 Jun 2024 06:27:19 -0600 Subject: [PATCH 56/68] Replaced makeStringMatrix with makeMatrix calls. Cleaned up colored stone undefined checks --- src/Goban/CanvasRenderer.ts | 80 +++++++++++++++------------------- src/Goban/InteractiveBase.ts | 8 ++-- src/Goban/SVGRenderer.ts | 67 ++++++++++++---------------- src/engine/util/matrix.ts | 14 ------ test/unit_tests/GoMath.test.ts | 9 ++-- 5 files changed, 72 insertions(+), 106 deletions(-) diff --git a/src/Goban/CanvasRenderer.ts b/src/Goban/CanvasRenderer.ts index 4a592ce1..bfd98075 100644 --- a/src/Goban/CanvasRenderer.ts +++ b/src/Goban/CanvasRenderer.ts @@ -37,7 +37,7 @@ import { encodeMoves, encodePrettyXCoordinate, getRandomInt, - makeStringMatrix, + makeMatrix, } from "engine/util"; import { callbacks } from "./callbacks"; import { Goban, GobanMetrics, GobanSelectedThemes, GOBAN_FONT } from "./Goban"; @@ -1497,39 +1497,37 @@ export class GobanCanvas extends Goban implements GobanCanvasInterface { /* Colored stones */ - if (this.colored_circles) { - const circle = this.colored_circles[j][i]; - if (circle) { - 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 */ @@ -1698,14 +1696,8 @@ export class GobanCanvas extends Goban 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; @@ -2263,11 +2255,9 @@ export class GobanCanvas extends Goban 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 */ @@ -2891,7 +2881,7 @@ export class GobanCanvas extends Goban implements GobanCanvasInterface { this.__draw_state.length !== this.height || this.__draw_state[0].length !== this.width ) { - this.__draw_state = makeStringMatrix(this.width, this.height); + this.__draw_state = makeMatrix(this.width, this.height, ""); } /* Set font for text overlay */ diff --git a/src/Goban/InteractiveBase.ts b/src/Goban/InteractiveBase.ts index 629b794c..66a55093 100644 --- a/src/Goban/InteractiveBase.ts +++ b/src/Goban/InteractiveBase.ts @@ -25,7 +25,7 @@ import { ConditionalMoveTree, GobanMoveError, } from "engine"; -import { NumberMatrix, encodeMove, makeStringMatrix, makeEmptyMatrix } from "engine/util"; +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"; @@ -275,7 +275,7 @@ export abstract class GobanInteractive extends GobanBase { protected __board_redraw_pen_layer_timer: any = null; protected __clock_timer?: ReturnType; - protected __draw_state: Array>; + 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; @@ -399,7 +399,7 @@ export abstract class GobanInteractive extends GobanBase { this.pen_marks = []; this.config = repair_config(config); - this.__draw_state = makeStringMatrix(this.width, this.height); + 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; @@ -766,7 +766,7 @@ export abstract class GobanInteractive extends GobanBase { this.__draw_state.length !== this.height || this.__draw_state[0].length !== this.width ) { - this.__draw_state = makeStringMatrix(this.width, this.height); + this.__draw_state = makeMatrix(this.width, this.height, ""); } this.chat_log = []; diff --git a/src/Goban/SVGRenderer.ts b/src/Goban/SVGRenderer.ts index dc10f9be..8bdb6b2d 100644 --- a/src/Goban/SVGRenderer.ts +++ b/src/Goban/SVGRenderer.ts @@ -33,7 +33,7 @@ import { encodeMoves, encodePrettyXCoordinate, getRandomInt, - makeStringMatrix, + makeMatrix, } from "engine/util"; import { callbacks } from "./callbacks"; import { Goban, GobanMetrics, GobanSelectedThemes } from "./Goban"; @@ -1426,31 +1426,29 @@ export class SVGRenderer extends Goban implements GobanSVGInterface { } /* Colored stones */ - if (this.colored_circles) { - 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); + 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 */ @@ -1619,13 +1617,8 @@ export class SVGRenderer extends Goban 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]; + 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) { @@ -2206,11 +2199,9 @@ export class SVGRenderer extends Goban 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 */ @@ -2983,7 +2974,7 @@ export class SVGRenderer extends Goban implements GobanSVGInterface { this.__draw_state.length !== this.height || this.__draw_state[0].length !== this.width ) { - this.__draw_state = makeStringMatrix(this.width, this.height); + this.__draw_state = makeMatrix(this.width, this.height, ""); } for (let j = this.bounds.top; j <= this.bounds.bottom; ++j) { diff --git a/src/engine/util/matrix.ts b/src/engine/util/matrix.ts index 0178ab1a..c3094a80 100644 --- a/src/engine/util/matrix.ts +++ b/src/engine/util/matrix.ts @@ -60,20 +60,6 @@ export function makeMatrix(width: number, height: number, initialVal } 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) { diff --git a/test/unit_tests/GoMath.test.ts b/test/unit_tests/GoMath.test.ts index 187db61d..c8a2f4ba 100644 --- a/test/unit_tests/GoMath.test.ts +++ b/test/unit_tests/GoMath.test.ts @@ -13,7 +13,6 @@ import { makeEmptyMatrix, makeMatrix, makeObjectMatrix, - makeStringMatrix, ojeSequenceToMoves, prettyCoordinates, sortMoves, @@ -71,16 +70,16 @@ describe("matrices", () => { expect(makeMatrix(0, 0, 0)).toEqual([]); }); - test("makeStringMatrix", () => { - expect(makeStringMatrix(3, 2)).toEqual([ + test("makeMatrix", () => { + expect(makeMatrix(3, 2, "")).toEqual([ ["", "", ""], ["", "", ""], ]); - expect(makeStringMatrix(3, 2, "asdf")).toEqual([ + expect(makeMatrix(3, 2, "asdf")).toEqual([ ["asdf", "asdf", "asdf"], ["asdf", "asdf", "asdf"], ]); - expect(makeStringMatrix(0, 0)).toEqual([]); + expect(makeMatrix(0, 0, "")).toEqual([]); }); test("makeObjectMatrix", () => { From 5edab70e00ed0a45d98bba32689ae9769276c0c4 Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Mon, 17 Jun 2024 06:29:07 -0600 Subject: [PATCH 57/68] Move engine build before with-renderer builds for easier to scan error output in my build terminal --- webpack.config.js | 107 ++++++++++++++++++++++------------------------ 1 file changed, 51 insertions(+), 56 deletions(-) diff --git a/webpack.config.js b/webpack.config.js index edc52ebd..3659475f 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -62,12 +62,61 @@ module.exports = (env, argv) => { }; 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", entry: { goban: "./src/index.ts", - //engine: "./src/engine/index.ts", }, output: { @@ -132,59 +181,5 @@ module.exports = (env, argv) => { }), ]; - //if (production) { - ret.push( - // node - 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, - }, - }), - ], - }, - }), - ); - //} - return ret; }; From b6ee1a71fdd0a0f8eb0aabe8377e407ab8815b1f Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Mon, 17 Jun 2024 06:39:50 -0600 Subject: [PATCH 58/68] Code de-duplication --- Makefile | 2 +- package.json | 2 +- src/Goban/themes/GobanTheme.ts | 26 +++++++++------ src/engine/protocol/ClientToServer.ts | 48 ++++++++++----------------- 4 files changed, 35 insertions(+), 43 deletions(-) diff --git a/Makefile b/Makefile index a5f1b5eb..87813970 100644 --- a/Makefile +++ b/Makefile @@ -24,7 +24,7 @@ lint: test: yarn run test -detect-duplicate-code: +detect-duplicate-code duplicate-code-detection: yarn run detect-duplicate-code doc docs typedoc: diff --git a/package.json b/package.json index 93079e33..930e3729 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "build-debug": "webpack", "build-production": "webpack --mode production", "dts": "dts-bundle-generator -o lib/goban.d.ts src/index.ts", - "detect-duplicate-code": "jscpd --min-tokens 50 src/", + "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 --plugin typedoc-plugin-missing-exports src/index.ts ", diff --git a/src/Goban/themes/GobanTheme.ts b/src/Goban/themes/GobanTheme.ts index ade23705..f129d244 100644 --- a/src/Goban/themes/GobanTheme.ts +++ b/src/Goban/themes/GobanTheme.ts @@ -210,15 +210,16 @@ export class GobanTheme { 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 GobanTheme { return [ref, shadow]; } - public placeBlackStoneSVG( + public placeWhiteStoneSVG( cell: SVGGraphicsElement, shadow_cell: SVGGraphicsElement | undefined, stone: string, @@ -237,15 +238,18 @@ export class GobanTheme { 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 diff --git a/src/engine/protocol/ClientToServer.ts b/src/engine/protocol/ClientToServer.ts index 12188c23..4123a75e 100644 --- a/src/engine/protocol/ClientToServer.ts +++ b/src/engine/protocol/ClientToServer.ts @@ -677,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; @@ -690,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: { From 334b4697432750ff2774f0bfe2e3712f3da1d765 Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Mon, 17 Jun 2024 07:31:36 -0600 Subject: [PATCH 59/68] Fixed analysis scores not being visible after a game was over --- src/Goban/CanvasRenderer.ts | 10 ++++++++-- src/Goban/SVGRenderer.ts | 10 ++++++++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/Goban/CanvasRenderer.ts b/src/Goban/CanvasRenderer.ts index bfd98075..ab29b42d 100644 --- a/src/Goban/CanvasRenderer.ts +++ b/src/Goban/CanvasRenderer.ts @@ -1776,7 +1776,10 @@ export class GobanCanvas extends Goban 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_estimator && (this.score_estimator.territory[j][i] || @@ -2459,7 +2462,10 @@ export class GobanCanvas extends Goban 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_estimator && (this.score_estimator.territory[j][i] || diff --git a/src/Goban/SVGRenderer.ts b/src/Goban/SVGRenderer.ts index 8bdb6b2d..db37e60e 100644 --- a/src/Goban/SVGRenderer.ts +++ b/src/Goban/SVGRenderer.ts @@ -1698,7 +1698,10 @@ export class SVGRenderer extends Goban 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_estimator && (this.score_estimator.territory[j][i] || @@ -2412,7 +2415,10 @@ export class SVGRenderer extends Goban 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_estimator && (this.score_estimator.territory[j][i] || From 39ddc6aa5537b291d5eb01f7f8d501d2f6d90e64 Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Mon, 17 Jun 2024 08:45:20 -0600 Subject: [PATCH 60/68] v0.8.0-beta.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 930e3729..2f4072c5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "goban", - "version": "0.7.50", + "version": "0.8.0-beta.1", "description": "", "main": "node/goban-engine.js", "browser": { From 9c12f61267b9df5b07a162a8cb79ab564a581b59 Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Tue, 18 Jun 2024 06:19:39 -0600 Subject: [PATCH 61/68] Don't upload engine.js to our cdn, no need --- Makefile | 2 -- 1 file changed, 2 deletions(-) diff --git a/Makefile b/Makefile index 87813970..3bda5ec0 100644 --- a/Makefile +++ b/Makefile @@ -56,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 build docs test clean all dev typedoc publich push lib types From 3bbc530f41e90e7975df3fe92684941a8cd0b659 Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Tue, 18 Jun 2024 06:45:59 -0600 Subject: [PATCH 62/68] Fix blacking out blue move in our SVG renderer when hovering over the blue move --- src/Goban/SVGRenderer.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Goban/SVGRenderer.ts b/src/Goban/SVGRenderer.ts index db37e60e..def47cb8 100644 --- a/src/Goban/SVGRenderer.ts +++ b/src/Goban/SVGRenderer.ts @@ -1617,6 +1617,7 @@ export class SVGRenderer extends Goban implements GobanSVGInterface { } } + /** 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; @@ -1635,6 +1636,7 @@ export class SVGRenderer extends Goban 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()); From 3c0253d8575d9ad30bbc5b067718ec0f742b7062 Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Wed, 19 Jun 2024 06:59:58 -0600 Subject: [PATCH 63/68] Mark snapback locations as settled and don't touch them for autoscoring --- .vscode/cspell.json | 1 + src/engine/StoneString.ts | 10 +- src/engine/autoscore.ts | 115 +++++++++++++++++++++- test/autoscore_test_files/game_17150.json | 47 +++++++++ 4 files changed, 166 insertions(+), 7 deletions(-) create mode 100644 test/autoscore_test_files/game_17150.json diff --git a/.vscode/cspell.json b/.vscode/cspell.json index 1923ff15..67c68ab8 100644 --- a/.vscode/cspell.json +++ b/.vscode/cspell.json @@ -143,6 +143,7 @@ "shownotesindicator", "Sitewide", "slowstrobe", + "snapbacks", "sodos", "stdev", "styl", diff --git a/src/engine/StoneString.ts b/src/engine/StoneString.ts index a16cbdfa..8e9aa042 100644 --- a/src/engine/StoneString.ts +++ b/src/engine/StoneString.ts @@ -33,13 +33,13 @@ export class StoneString { private __added_neighbors: { [group_id: number]: boolean }; private neighboring_space: StoneString[]; - private neighboring_enemy: StoneString[]; + private neighboring_stone_strings: StoneString[]; constructor(id: number, color: JGOFNumericPlayerColor) { this.intersections = []; this.neighbors = []; this.neighboring_space = []; - this.neighboring_enemy = []; + this.neighboring_stone_strings = []; this.id = id; this.color = color; @@ -61,8 +61,8 @@ export class StoneString { } } public foreachNeighboringStoneString(fn: (stone_string: StoneString) => void): void { - for (let i = 0; i < this.neighbors.length; ++i) { - fn(this.neighboring_enemy[i]); + for (let i = 0; i < this.neighboring_stone_strings.length; ++i) { + fn(this.neighboring_stone_strings[i]); } } public size(): number { @@ -84,7 +84,7 @@ export class StoneString { if (group.color === JGOFNumericPlayerColor.EMPTY) { this.neighboring_space.push(group); } else { - this.neighboring_enemy.push(group); + this.neighboring_stone_strings.push(group); } } } diff --git a/src/engine/autoscore.ts b/src/engine/autoscore.ts index 8337ef3c..ebf0e8ca 100644 --- a/src/engine/autoscore.ts +++ b/src/engine/autoscore.ts @@ -92,6 +92,8 @@ export function autoscore( 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(); @@ -126,6 +128,112 @@ export function autoscore( 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); + } + */ + + 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 * @@ -229,6 +337,9 @@ export function autoscore( 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]) && @@ -1010,11 +1121,11 @@ function finalize_debug_output(): string { final_output = ""; let legend = ""; - legend += "Legend:\n"; + 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 += " " + yellow("Territory in Seki") + "\n"; legend += " " + magenta("Undecided territory") + "\n"; legend += " " + red("Error") + "\n"; diff --git a/test/autoscore_test_files/game_17150.json b/test/autoscore_test_files/game_17150.json new file mode 100644 index 00000000..0ac1d4cd --- /dev/null +++ b/test/autoscore_test_files/game_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 " + ] +} From 1e83e3c415ab0f1b9bef61c29f999e76a4705384 Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Wed, 19 Jun 2024 07:35:15 -0600 Subject: [PATCH 64/68] Comments, fix autoscore test game fetcher script, include seal points in final autoscore debug output --- scripts/fetch_game_for_autoscore_testing.ts | 2 +- src/engine/autoscore.ts | 55 ++++++++++++++++++- .../{game_17150.json => game_beta_17150.json} | 0 tsconfig.json | 9 +-- 4 files changed, 57 insertions(+), 9 deletions(-) rename test/autoscore_test_files/{game_17150.json => game_beta_17150.json} (100%) diff --git a/scripts/fetch_game_for_autoscore_testing.ts b/scripts/fetch_game_for_autoscore_testing.ts index ec2ccc33..93d0b98a 100755 --- a/scripts/fetch_game_for_autoscore_testing.ts +++ b/scripts/fetch_game_for_autoscore_testing.ts @@ -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]; diff --git a/src/engine/autoscore.ts b/src/engine/autoscore.ts index ebf0e8ca..7436e529 100644 --- a/src/engine/autoscore.ts +++ b/src/engine/autoscore.ts @@ -102,7 +102,20 @@ export function autoscore( score_positions(); stage("Final state"); - debug_board_output("Final ownership", final_ownership); + 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); @@ -172,6 +185,15 @@ export function autoscore( } */ + /** + * 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"); @@ -821,6 +843,37 @@ function debug_board_output(title: string, board: JGOFNumericPlayerColor[][]) { 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); diff --git a/test/autoscore_test_files/game_17150.json b/test/autoscore_test_files/game_beta_17150.json similarity index 100% rename from test/autoscore_test_files/game_17150.json rename to test/autoscore_test_files/game_beta_17150.json diff --git a/tsconfig.json b/tsconfig.json index 5fbf6724..3fdbbeb9 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -33,13 +33,8 @@ "sourceMap": true, "jsx": "react" }, - "files": [ - "src/index.ts", - "src/engine/index.ts", - "jest.config.ts", - "./src/engine/util/getRandomInt.ts" - ], - "include": ["test/**/*.ts"], + "files": ["src/index.ts", "src/engine/index.ts", "jest.config.ts"], + "include": ["test/**/*.ts", "scripts/**/*.ts"], "ts-node": { "require": ["tsconfig-paths/register"] } From 27da7f0d35422eefa115fb0958ad4eb5a1dd3cac Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Wed, 19 Jun 2024 07:45:09 -0600 Subject: [PATCH 65/68] Add autoscore test case --- src/engine/autoscore.ts | 3 + .../game_dev_51750014.json | 63 +++++++++++++++++++ 2 files changed, 66 insertions(+) create mode 100644 test/autoscore_test_files/game_dev_51750014.json diff --git a/src/engine/autoscore.ts b/src/engine/autoscore.ts index 7436e529..3657189f 100644 --- a/src/engine/autoscore.ts +++ b/src/engine/autoscore.ts @@ -564,12 +564,15 @@ export function autoscore( 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; 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" + ] +} From de97310c974a7e669d908069e68a51b12fd939b6 Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Wed, 19 Jun 2024 07:55:39 -0600 Subject: [PATCH 66/68] v0.8.0-beta.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2f4072c5..543abcdf 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "goban", - "version": "0.8.0-beta.1", + "version": "0.8.0-beta.2", "description": "", "main": "node/goban-engine.js", "browser": { From a603762079423f974b53ee09a703e0a50a81374f Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Wed, 19 Jun 2024 08:34:52 -0600 Subject: [PATCH 67/68] Fix auto-score clicking toggling dead stones --- src/Goban/OGSConnectivity.ts | 3 +++ src/engine/ScoreEstimator.ts | 5 +++++ 2 files changed, 8 insertions(+) diff --git a/src/Goban/OGSConnectivity.ts b/src/Goban/OGSConnectivity.ts index b9646ad9..c647e7ee 100644 --- a/src/Goban/OGSConnectivity.ts +++ b/src/Goban/OGSConnectivity.ts @@ -44,6 +44,7 @@ import { ReviewMessage, ScoreEstimator, JGOFMove, + makeMatrix, } from "engine"; import { //ServerToClient, @@ -1392,6 +1393,8 @@ export abstract class OGSConnectivity extends GobanInteractive { 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 diff --git a/src/engine/ScoreEstimator.ts b/src/engine/ScoreEstimator.ts index ad7a6718..91dadb71 100644 --- a/src/engine/ScoreEstimator.ts +++ b/src/engine/ScoreEstimator.ts @@ -132,9 +132,14 @@ export class ScoreEstimator extends BoardState { tolerance: number, prefer_remote: boolean = false, autoscore: boolean = false, + removal?: boolean[][], ) { super(engine, goban_callback); + if (removal) { + this.removal = removal; + } + this.engine = engine; this.color_to_move = engine.colorToMove(); this.board = engine.cloneBoard(); From b8b5cced9346b9b41fa80f4de5eeee66b7b1209e Mon Sep 17 00:00:00 2001 From: Akita Noek Date: Wed, 19 Jun 2024 08:37:59 -0600 Subject: [PATCH 68/68] v0.8.0-beta.3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 543abcdf..ecdfa6dd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "goban", - "version": "0.8.0-beta.2", + "version": "0.8.0-beta.3", "description": "", "main": "node/goban-engine.js", "browser": {