diff --git a/src/GoStoneGroup.ts b/src/GoStoneGroup.ts index fb6b4a31..daad7f42 100644 --- a/src/GoStoneGroup.ts +++ b/src/GoStoneGroup.ts @@ -41,11 +41,15 @@ export class GoStoneGroup { is_territory_in_seki: boolean = false; private __added_neighbors: { [group_id: number]: boolean }; + private neighboring_space: GoStoneGroup[]; + private neighboring_enemy: GoStoneGroup[]; constructor(board_state: BoardState, id: number, color: JGOFNumericPlayerColor) { this.board_state = board_state; this.points = []; this.neighbors = []; + this.neighboring_space = []; + this.neighboring_enemy = []; this.id = id; this.color = color; this.is_strong_eye = false; @@ -60,6 +64,14 @@ export class GoStoneGroup { if (!(group.id in this.__added_neighbors)) { this.neighbors.push(group); this.__added_neighbors[group.id] = true; + + if (group.color !== this.color) { + if (group.color === JGOFNumericPlayerColor.EMPTY) { + this.neighboring_space.push(group); + } else { + this.neighboring_enemy.push(group); + } + } } } addCornerGroup(x: number, y: number, group: GoStoneGroup): void { @@ -78,6 +90,16 @@ export class GoStoneGroup { fn(this.neighbors[i]); } } + foreachNeighborSpaceGroup(fn: (group: GoStoneGroup) => void): void { + for (let i = 0; i < this.neighboring_space.length; ++i) { + fn(this.neighboring_space[i]); + } + } + foreachNeighborEnemyGroup(fn: (group: GoStoneGroup) => void): void { + for (let i = 0; i < this.neighbors.length; ++i) { + fn(this.neighboring_enemy[i]); + } + } computeIsEye(): void { this.is_eye = false; diff --git a/src/ScoreEstimator.ts b/src/ScoreEstimator.ts index 07471d3e..32ef3655 100644 --- a/src/ScoreEstimator.ts +++ b/src/ScoreEstimator.ts @@ -80,111 +80,6 @@ export function set_local_scorer(scorer: LocalEstimator) { local_scorer = scorer; } -interface SEPoint { - x: number; - y: number; -} - -class SEGroup { - points: Array; - neighboring_enemy: Array; - neighboring_space: Array; - se: ScoreEstimator; - id: number; - color: JGOFNumericPlayerColor; - removed: boolean; - neighbors: Array; - neighbor_map: { [group_id: string]: boolean }; - - constructor(se: ScoreEstimator, color: JGOFNumericPlayerColor, id: number) { - this.points = []; - this.se = se; - this.id = id; - this.color = color; - this.neighbors = []; - this.neighboring_space = []; - this.neighboring_enemy = []; - this.neighbor_map = {}; - this.removed = false; - } - add(i: number, j: number) { - this.points.push({ x: i, y: j }); - } - foreachPoint(fn: (pt: SEPoint) => void) { - for (let i = 0; i < this.points.length; ++i) { - fn(this.points[i]); - } - } - foreachNeighboringPoint(fn: (pt: SEPoint) => void) { - const self = this; - const points = this.points; - const done_array = new Array(this.se.height * this.se.width); - for (let i = 0; i < points.length; ++i) { - done_array[points[i].x + points[i].y * this.se.width] = true; - } - - function checkAndDo(x: number, y: number): void { - const idx = x + y * self.se.width; - if (done_array[idx]) { - return; - } - done_array[idx] = true; - - fn({ x: x, y: y }); - } - - for (let i = 0; i < points.length; ++i) { - const pt = points[i]; - if (pt.x - 1 >= 0) { - checkAndDo(pt.x - 1, pt.y); - } - if (pt.x + 1 !== this.se.width) { - checkAndDo(pt.x + 1, pt.y); - } - if (pt.y - 1 >= 0) { - checkAndDo(pt.x, pt.y - 1); - } - if (pt.y + 1 !== this.se.height) { - checkAndDo(pt.x, pt.y + 1); - } - } - } - addNeighbor(group: SEGroup): void { - if (!(group.id in this.neighbor_map)) { - this.neighbors.push(group); - this.neighbor_map[group.id] = true; - - if (group.color === 0) { - this.neighboring_space.push(group); - } else { - this.neighboring_enemy.push(group); - } - } - } - foreachNeighborGroup(fn: (group: SEGroup) => void): void { - for (let i = 0; i < this.neighbors.length; ++i) { - fn(this.neighbors[i]); - } - } - foreachNeighborSpaceGroup(fn: (group: SEGroup) => void): void { - for (let i = 0; i < this.neighboring_space.length; ++i) { - fn(this.neighboring_space[i]); - } - } - foreachNeighborEnemyGroup(fn: (group: SEGroup) => void): void { - for (let i = 0; i < this.neighboring_enemy.length; ++i) { - fn(this.neighboring_enemy[i]); - } - } - setRemoved(removed: boolean): void { - this.removed = removed; - for (let i = 0; i < this.points.length; ++i) { - const pt = this.points[i]; - this.se.setRemoved(pt.x, pt.y, removed ? 1 : 0); - } - } -} - export class ScoreEstimator { width: number; height: number; @@ -209,11 +104,10 @@ export class ScoreEstimator { }; engine: GoEngine; - groups: Array>; + private groups: GoStoneGroups; removal: Array>; goban_callback?: GobanCore; tolerance: number; - group_list: Array; amount: number = NaN; ownership: Array>; territory: Array>; @@ -240,15 +134,15 @@ export class ScoreEstimator { this.board = dup(engine.board); this.removal = GoMath.makeMatrix(this.width, this.height, 0); this.ownership = GoMath.makeMatrix(this.width, this.height, 0); - this.groups = GoMath.makeEmptyObjectMatrix(this.width, this.height); this.territory = GoMath.makeMatrix(this.width, this.height, 0); this.estimated_hard_score = 0.0; - this.group_list = []; this.trials = trials; this.tolerance = tolerance; this.prefer_remote = prefer_remote; - this.resetGroups(); + this.territory = GoMath.makeMatrix(this.width, this.height, 0); + this.groups = new GoStoneGroups(this); + this.when_ready = this.estimateScore(this.trials, this.tolerance); } @@ -390,32 +284,6 @@ export class ScoreEstimator { } return ret; } - resetGroups(): void { - this.territory = GoMath.makeMatrix(this.width, this.height, 0); - this.groups = GoMath.makeEmptyObjectMatrix(this.width, this.height); - this.group_list = []; - - const go_stone_groups = new GoStoneGroups(this); - - go_stone_groups.foreachGroup((gs_grp) => { - const se_grp = make_se_group_from_gs_group(gs_grp, this); - gs_grp.points.forEach((pt) => { - this.groups[pt.y][pt.x] = se_grp; - }); - this.group_list.push(se_grp); - }); - - for (const grp of this.group_list) { - for (const gs_neighbor of go_stone_groups.groups[grp.id].neighbors) { - grp.addNeighbor(this.group_list[gs_neighbor.id - 1]); - } - } - } - foreachGroup(fn: (group: SEGroup) => void): void { - for (let i = 0; i < this.group_list.length; ++i) { - fn(this.group_list[i]); - } - } handleClick(i: number, j: number, modkey: boolean) { if (modkey) { this.setRemoved(i, j, !this.removal[j][i] ? 1 : 0); @@ -427,16 +295,21 @@ export class ScoreEstimator { /* empty */ }); } + + private removeGroup(g: GoStoneGroup, removing: boolean) { + 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 = []; + const space_groups: Array = []; let group_color: JGOFNumericPlayerColor; try { if (x >= 0 && y >= 0) { const removing = !this.removal[y][x]; const group = this.getGroup(x, y); - group.setRemoved(removing); + this.removeGroup(group, removing); group_color = this.board[y][x]; if (group_color === 0) { @@ -458,7 +331,7 @@ export class ScoreEstimator { if (!already_done[g.id]) { already_done[g.id] = true; if (g.color === group_color) { - g.setRemoved(removing); + this.removeGroup(g, removing); g.foreachNeighborSpaceGroup((gspace) => { if (!already_done[gspace.id]) { space_groups.push(gspace); @@ -506,8 +379,8 @@ export class ScoreEstimator { } return ret; } - getGroup(x: number, y: number): SEGroup { - return this.groups[y][x]; + getGroup(x: number, y: number): GoStoneGroup { + return this.groups.groups[this.groups.group_id_map[y][x]]; } /** @@ -683,12 +556,3 @@ function sum_board(board: GoMath.NumberMatrix) { } return sum; } - -/** - * SE Group and GoStoneGroup have a slightly different interface. - */ -function make_se_group_from_gs_group(gsg: GoStoneGroup, se: ScoreEstimator) { - const se_group = new SEGroup(se, gsg.color, gsg.id); - se_group.points = gsg.points.map((pt) => pt); - return se_group; -} diff --git a/src/__tests__/ScoreEstimator.test.ts b/src/__tests__/ScoreEstimator.test.ts index b06d3b6a..efba3375 100644 --- a/src/__tests__/ScoreEstimator.test.ts +++ b/src/__tests__/ScoreEstimator.test.ts @@ -59,10 +59,6 @@ describe("ScoreEstimator", () => { engine.place(1, 1); engine.place(2, 1); - // It might seem weird to set prefer_remote = true just to test - // resetGroups, but is a necessary hack to bypass initialization of - // the OGSScoreEstimation library - const prefer_remote = true; const trials = 10; const tolerance = 0.25; @@ -78,33 +74,6 @@ describe("ScoreEstimator", () => { set_remote_scorer(undefined as any); }); - test("resetGroups", async () => { - const se = new ScoreEstimator(undefined, engine, trials, tolerance, prefer_remote); - - await se.when_ready; - - expect(se.group_list).toHaveLength(4); - expect(se.group_list.map((group) => group.points)).toEqual([ - [ - { x: 0, y: 0 }, - { x: 0, y: 1 }, - ], - [ - { x: 1, y: 0 }, - { x: 1, y: 1 }, - ], - [ - { x: 2, y: 0 }, - { x: 2, y: 1 }, - ], - [ - { x: 3, y: 0 }, - { x: 3, y: 1 }, - ], - ]); - expect(se.group_list.map((group) => group.color)).toEqual([0, 1, 2, 0]); - }); - test("amount and winner", async () => { const se = new ScoreEstimator(undefined, engine, trials, tolerance, false);