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 " + ] +}