Skip to content

Commit

Permalink
Merge pull request #161 from online-go/autoscore
Browse files Browse the repository at this point in the history
Autoscore
  • Loading branch information
anoek authored Jun 1, 2024
2 parents 6f32207 + dd131d9 commit 2df7879
Show file tree
Hide file tree
Showing 32 changed files with 2,824 additions and 12 deletions.
13 changes: 8 additions & 5 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ module.exports = {
"@typescript-eslint/no-this-alias": "off", // 11 errors
"@typescript-eslint/no-unused-vars": "off", // 38 warnings
"@typescript-eslint/no-empty-interface": "off", // 1 error
eqeqeq: "off", // 2 errors
"eqeqeq": "off", // 2 errors
"no-case-declarations": "off", // 1 error
"no-constant-condition": "off", // 2 errors
"no-empty": "off", // 2 errors
Expand All @@ -44,9 +44,9 @@ module.exports = {
"@typescript-eslint/semi": "error",
"@typescript-eslint/type-annotation-spacing": "error",
"computed-property-spacing": ["error", "never"],
curly: "error",
"curly": "error",
"eol-last": "error",
eqeqeq: ["error", "smart"],
"eqeqeq": ["error", "smart"],
"id-denylist": [
"error",
"any",
Expand Down Expand Up @@ -92,9 +92,12 @@ module.exports = {
"error",
{
rules: {
"file-header": [true, "[Cc]opyright ([(][Cc][)])?\\s*[Oo]nline-[gG]o.com"], // cspell: disable-line
"file-header": [
true,
"([Cc]opyright ([(][Cc][)])?\\s*[Oo]nline-[gG]o.com)|(bin/env)", // cspell: disable-line
],
"import-spacing": true,
whitespace: [
"whitespace": [
true,
"check-branch",
"check-decl",
Expand Down
6 changes: 5 additions & 1 deletion .vscode/cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"autoplace",
"autoplaying",
"autoscore",
"autoscored",
"Autoscoring",
"autoscroll",
"autoscrolling",
Expand Down Expand Up @@ -185,5 +186,8 @@
"yomi",
"zoomable"
],
"language": "en,en-GB"
"language": "en,en-GB",
"ignorePaths": [
"test/autoscore_test_files"
]
}
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "goban",
"version": "0.7.46",
"version": "0.7.47",
"description": "",
"main": "lib/goban.js",
"types": "lib/goban.d.ts",
Expand Down Expand Up @@ -48,12 +48,14 @@
"@types/node": "^18.15.5",
"@types/react": "^18.0.28",
"@types/react-dom": "^18.0.11",
"@types/cli-color": "^2.0.6",
"@typescript-eslint/eslint-plugin": "^6.14.0",
"@typescript-eslint/eslint-plugin-tslint": "^6.14.0",
"@typescript-eslint/parser": "^6.14.0",
"bufferutil": "^4.0.7",
"canvas": "^2.10.2",
"cspell": "^8.3.2",
"cli-color": "^2.0.4",
"eslint": "^8.56.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-jsdoc": "^46.9.1",
Expand Down
1 change: 1 addition & 0 deletions scripts/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
*.jwt
158 changes: 158 additions & 0 deletions scripts/fetch_game_for_autoscore_testing.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
#!/usr/bin/env ts-node

/*
This script fetches a game, scores it, and writes it to a test file.
Presumably you are fetching a game because the autoscoring function is not
working for that game, so you'll need to manually score the game.
Note: this script requires a JWT to be provided in the file "user.jwt"
*/

import { readFileSync, writeFileSync } from "fs";
import { ScoreEstimateRequest } from "../src/ScoreEstimator";

const jwt = readFileSync("user.jwt").toString().replace(/"/g, "").trim();
const game_id = process.argv[2];

if (!jwt) {
console.log(
'user.jwt file missing. Please open your javascript console and run data.get("config.user_jwt"), then ' +
'put the contents in a file called "user.jwt" in the same directory as this script.',
);
process.exit(1);
}
if (!game_id) {
console.log("Usage: ts-node fetch_game.ts <game_id>");
process.exit(1);
}
console.log(`Fetching game ${game_id}...`);

(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 json = await res.json();
const board_state = json.board;

const ser_black: ScoreEstimateRequest = {
player_to_move: "black",
width: board_state[0].length,
height: board_state.length,
board_state,
rules: "chinese",
jwt,
};
const ser_white = { ...ser_black, player_to_move: "white" };

const estimate_responses = await Promise.all([
// post to https://ai.online-go.com/api/score

fetch("https://ai.online-go.com/api/score", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(ser_black),
}),

fetch("https://ai.online-go.com/api/score", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(ser_white),
}),
]);

const estimates = await Promise.all(estimate_responses.map((r) => r.json()));

let output = "{\n";

output += ` "game_id": ${game_id},\n`;
output += ` "board": [\n`;
for (let row of board_state) {
output +=
` "` +
row
.map((c: number) => {
switch (c) {
case 0:
return " ";
case 1:
return "b";
case 2:
return "W";
}
return "?";
})
.join("");
if (row === board_state[board_state.length - 1]) {
output += `"\n`;
} else {
output += `",\n`;
}
}
output += " ],\n";

output += ' "black": [\n';
for (let row of estimates[0].ownership) {
output += ` [${row.map((x: number) => (" " + x.toFixed(1)).substr(-4)).join(", ")}]`;
if (row === estimates[0].ownership[estimates[0].ownership.length - 1]) {
output += `\n`;
} else {
output += `,\n`;
}
}
output += " ],\n";

output += ' "white": [\n';
for (let row of estimates[1].ownership) {
output += ` [${row.map((x: number) => (" " + x.toFixed(1)).substr(-4)).join(", ")}]`;
if (row === estimates[1].ownership[estimates[1].ownership.length - 1]) {
output += `\n`;
} else {
output += `,\n`;
}
}

output += " ],\n";

output += ' "correct_ownership": [\n';

const avg_ownership = estimates[0].ownership.map((row: number[], i: number) =>
row.map((x: number, j: number) => (x + estimates[1].ownership[i][j]) / 2),
);

for (let row of avg_ownership) {
output +=
` "` +
row
.map((c: number) => {
if (c < -0.5) {
return "W";
}
if (c > 0.5) {
return "B";
}
return "?";
})
.join("");

if (row === avg_ownership[avg_ownership.length - 1]) {
output += `"\n`;
} else {
output += `",\n`;
}
}
output += " ]\n";

output += "}\n";

console.log(output);
console.log(`Writing to game_${game_id}.json`);
writeFileSync(`game_${game_id}.json`, output);
})()
.then(() => console.log("Done, exiting"))
.catch(console.error);
70 changes: 66 additions & 4 deletions src/ScoreEstimator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,18 +37,34 @@ export interface ScoreEstimateRequest {
player_to_move: "black" | "white";
width: number;
height: number;
board_state: Array<Array<number>>;
board_state: JGOFNumericPlayerColor[][];
rules: GoEngineRules;
black_prisoners?: number;
white_prisoners?: number;
komi?: number;
jwt: string;

/** Whether to run autoscoring logic. If true, player_to_move is
* essentially ignored as we compute estimates with each player moving
* first in turn. */
autoscore?: boolean;
}

export interface ScoreEstimateResponse {
ownership: Array<Array<number>>;
/** Matrix of ownership estimates ranged from -1 (white) to 1 (black) */
ownership: number[][];

/** Estimated score */
score?: number;

/** Estimated win rate */
win_rate?: number;

/** Board state after autoscoring logic has been run. Only defined if autoscore was true in the request. */
autoscored_board_state?: JGOFNumericPlayerColor[][];

/** Intersections that are dead or dame. Only defined if autoscore was true in the request. */
autoscored_removed?: string;
}

let remote_scorer: ((req: ScoreEstimateRequest) => Promise<ScoreEstimateResponse>) | undefined;
Expand Down Expand Up @@ -117,6 +133,8 @@ export class ScoreEstimator {
estimated_hard_score: number;
when_ready: Promise<void>;
prefer_remote: boolean;
autoscored_state?: JGOFNumericPlayerColor[][];
autoscored_removed?: string;

constructor(
goban_callback: GobanCore | undefined,
Expand Down Expand Up @@ -184,6 +202,7 @@ export class ScoreEstimator {
height: this.engine.height,
rules: this.engine.rules,
board_state: board_state,
autoscore: true,
jwt: "", // this gets set by the remote_scorer method
})
.then((res: ScoreEstimateResponse) => {
Expand All @@ -201,8 +220,26 @@ export class ScoreEstimator {
res.score += 7.5 - komi; // we always ask katago to use 7.5 komi, so correct if necessary
res.score += captures_delta;
res.score -= this.engine.getHandicapPointAdjustmentForWhite();

this.updateEstimate(score_estimate, res.ownership, res.score);
this.autoscored_removed = res.autoscored_removed;
this.autoscored_state = res.autoscored_board_state;

if (this.autoscored_state) {
this.updateEstimate(
score_estimate,
this.autoscored_state.map((row) =>
row.map((cell) => (cell === 2 ? -1 : cell)),
),
res.score,
);
} else {
console.error(
"Remote scorer didn't have an autoscore board state, this should be unreachable",
);
// this was the old code, probably still works in case
// we have messed something up. Eventually this should
// be removed. - anoek 2024-06-01
this.updateEstimate(score_estimate, res.ownership, res.score);
}
resolve();
})
.catch((err: any) => {
Expand Down Expand Up @@ -260,6 +297,13 @@ export class ScoreEstimator {
}

getProbablyDead(): string {
if (this.autoscored_removed) {
console.info("Returning autoscored_removed for getProbablyDead");
return this.autoscored_removed;
} else {
console.warn("Not able to use autoscored_removed for getProbablyDead");
}

let ret = "";
const arr = [];

Expand Down Expand Up @@ -349,12 +393,15 @@ export class ScoreEstimator {
}
}
setRemoved(x: number, y: number, removed: number): void {
this.clearAutoScore();

this.removal[y][x] = removed;
if (this.goban_callback) {
this.goban_callback.setForRemoval(x, y, this.removal[y][x]);
}
}
clearRemoved(): void {
this.clearAutoScore();
for (let y = 0; y < this.height; ++y) {
for (let x = 0; x < this.width; ++x) {
if (this.removal[y][x]) {
Expand All @@ -363,7 +410,22 @@ export class ScoreEstimator {
}
}
}
clearAutoScore(): void {
if (this.autoscored_removed || this.autoscored_state) {
this.autoscored_removed = undefined;
this.autoscored_state = undefined;
console.warn("Clearing autoscored state");
}
}

getStoneRemovalString(): string {
if (this.autoscored_removed) {
console.info("Returning autoscored_removed for getStoneRemovalString");
return this.autoscored_removed;
} else {
console.warn("Not able to use autoscored_removed for getStoneRemovalString");
}

let ret = "";
const arr = [];
for (let y = 0; y < this.height; ++y) {
Expand Down
Loading

0 comments on commit 2df7879

Please sign in to comment.