├── jigo.png
├── ci
└── extractInfo.js
├── .github
└── workflows
│ └── create-release.yml
├── LICENSE
├── package.json
├── README.md
├── .gitignore
└── src
└── main.js
/jigo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yishn/KataJigo/HEAD/jigo.png
--------------------------------------------------------------------------------
/ci/extractInfo.js:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 | const {version} = require('../package.json')
3 |
4 | function printSetOutputs(outputs) {
5 | for (let [name, value] of Object.entries(outputs)) {
6 | console.log(`::set-output name=${name}::${value}`)
7 | }
8 | }
9 |
10 | printSetOutputs({
11 | version,
12 | tag: (process.env.GITHUB_REF || '').replace('refs/tags/', ''),
13 | ci: path.resolve(process.cwd(), './ci')
14 | })
15 |
--------------------------------------------------------------------------------
/.github/workflows/create-release.yml:
--------------------------------------------------------------------------------
1 | name: Create Release
2 |
3 | on:
4 | push:
5 | tags:
6 | - v*
7 |
8 | jobs:
9 | create-release:
10 | runs-on: windows-latest
11 |
12 | steps:
13 | - uses: actions/checkout@v1
14 | - uses: actions/setup-node@v1
15 | with:
16 | node-version: 12.x
17 | - uses: actions/setup-go@v1
18 | with:
19 | go-version: 1.13.x
20 | - name: Extract info
21 | id: info
22 | run: |
23 | node ./ci/extractInfo.js
24 | env:
25 | GITHUB_REF: ${{ github.ref }}
26 | - name: Create & upload artifact
27 | run: |
28 | npm install
29 | npm run dist:all
30 | go get -u github.com/tcnksm/ghr
31 | ./ci/bin/ghr -n "KataJigo v${{ steps.info.outputs.version }}" -draft -replace ${{ steps.info.outputs.tag }} ./dist
32 | env:
33 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
34 | GOPATH: ${{ steps.info.outputs.ci }}
35 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Yichuan Shen
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "katajigo",
3 | "version": "1.0.2",
4 | "description": "",
5 | "bin": "src/main.js",
6 | "main": "src/main.js",
7 | "scripts": {
8 | "test": "echo \"Error: no test specified\" && exit 1",
9 | "format": "prettier \"./**/*.{js,md}\" --write",
10 | "format-watch": "onchange \"./**/*.{js,md}\" -- prettier --write {{changed}}",
11 | "build": "pkg . --targets node12 --out-path ./bin",
12 | "build:win": "pkg . --targets node12-windows-x64 --out-path ./bin/katajigo-win",
13 | "build:mac": "pkg . --targets node12-macos-x64 --out-path ./bin/katajigo-mac",
14 | "build:linux": "pkg . --targets node12-linux-x64 --out-path ./bin/katajigo-linux",
15 | "dist:win": "npm run build:win && mkdirp ./dist && cross-zip ./bin/katajigo-win ./dist/katajigo-win.zip",
16 | "dist:mac": "npm run build:mac && mkdirp ./dist && cross-zip ./bin/katajigo-mac ./dist/katajigo-mac.zip",
17 | "dist:linux": "npm run build:linux && mkdirp ./dist && cross-zip ./bin/katajigo-linux ./dist/katajigo-linux.zip",
18 | "dist:all": "npm run dist:win && npm run dist:mac && npm run dist:linux"
19 | },
20 | "repository": {
21 | "type": "git",
22 | "url": "git+https://github.com/yishn/KataJigo.git"
23 | },
24 | "author": "Yichuan Shen",
25 | "license": "MIT",
26 | "bugs": {
27 | "url": "https://github.com/yishn/KataJigo/issues"
28 | },
29 | "homepage": "https://github.com/yishn/KataJigo#readme",
30 | "prettier": {
31 | "semi": false,
32 | "singleQuote": true,
33 | "bracketSpacing": false,
34 | "proseWrap": "always"
35 | },
36 | "dependencies": {
37 | "@sabaki/gtp": "^3.0.0"
38 | },
39 | "devDependencies": {
40 | "cross-zip-cli": "^1.0.0",
41 | "mkdirp": "^1.0.3",
42 | "onchange": "^6.1.0",
43 | "pkg": "^4.4.4",
44 | "prettier": "1.19.1"
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # KataJigo
2 |
3 | Play against a KataGo that strives for a Jigo instead of opponent destruction.
4 |
5 | Playing against [KataGo](https://github.com/lightvector/KataGo) can be
6 | particularly frustrating. Even before midgame starts, KataGo may have
7 | accumulated an overwhelming lead in points in the best case, or the game could
8 | have turned into a merciless, bloody slaughter of your groups in the worst case,
9 | leading to your inevitable demise, leaving you sad and empty inside.
10 |
11 | KataJigo is an experiment. It will let KataGo always aim for a half point win,
12 | or a draw if playing with an integer komi, slipping into the role of a capable
13 | teacher who's looking down from far above, playing a teaching game (in theory,
14 | at least).
15 |
16 |
21 |
22 | Jigo game of GnuGo (B) against KataJigo (W) with 7 point komi
23 |
24 | ## Installation
25 |
26 | 1. Download both [KataGo](https://github.com/lightvector/KataGo) and
27 | [KataJigo](https://github.com/yishn/KataJigo/releases/latest).
28 | 2. Install and set up KataGo according to the instructions.
29 | 3. In the same folder as KataGo, drop in the executable of KataJigo.
30 | 4. Now, you can use `katajigo` as a drop-in replacement of `katago`.
31 |
32 | If you already set up KataGo in Sabaki, all you need to do is replace the path
33 | to KataGo with the path to KataJigo in the same directory.
34 |
35 | ## How does this work?
36 |
37 | Using the analysis feature of KataGo, we'll consider all the moves with
38 | non-negative `scoreLead` and a `winrate` that is better than 50%. Out of those
39 | moves we'll pick the move with the lowest `scoreLead` and the highest `winrate`.
40 |
41 | ## Building
42 |
43 | Make sure you have Node.js installed. First, clone the repository and install
44 | all dependencies with npm:
45 |
46 | ```
47 | $ git clone https://github.com/yishn/KataJigo
48 | $ cd KataJigo
49 | $ npm install
50 | ```
51 |
52 | Run the following command to create an executable in the `./bin` directory:
53 |
54 | ```
55 | $ npm run build
56 | ```
57 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | bin/
2 | dist/
3 |
4 | # Logs
5 | logs
6 | *.log
7 | npm-debug.log*
8 | yarn-debug.log*
9 | yarn-error.log*
10 | lerna-debug.log*
11 |
12 | # Diagnostic reports (https://nodejs.org/api/report.html)
13 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
14 |
15 | # Runtime data
16 | pids
17 | *.pid
18 | *.seed
19 | *.pid.lock
20 |
21 | # Directory for instrumented libs generated by jscoverage/JSCover
22 | lib-cov
23 |
24 | # Coverage directory used by tools like istanbul
25 | coverage
26 | *.lcov
27 |
28 | # nyc test coverage
29 | .nyc_output
30 |
31 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
32 | .grunt
33 |
34 | # Bower dependency directory (https://bower.io/)
35 | bower_components
36 |
37 | # node-waf configuration
38 | .lock-wscript
39 |
40 | # Compiled binary addons (https://nodejs.org/api/addons.html)
41 | build/Release
42 |
43 | # Dependency directories
44 | node_modules/
45 | jspm_packages/
46 |
47 | # TypeScript v1 declaration files
48 | typings/
49 |
50 | # TypeScript cache
51 | *.tsbuildinfo
52 |
53 | # Optional npm cache directory
54 | .npm
55 |
56 | # Optional eslint cache
57 | .eslintcache
58 |
59 | # Microbundle cache
60 | .rpt2_cache/
61 | .rts2_cache_cjs/
62 | .rts2_cache_es/
63 | .rts2_cache_umd/
64 |
65 | # Optional REPL history
66 | .node_repl_history
67 |
68 | # Output of 'npm pack'
69 | *.tgz
70 |
71 | # Yarn Integrity file
72 | .yarn-integrity
73 |
74 | # dotenv environment variables file
75 | .env
76 | .env.test
77 |
78 | # parcel-bundler cache (https://parceljs.org/)
79 | .cache
80 |
81 | # Next.js build output
82 | .next
83 |
84 | # Nuxt.js build / generate output
85 | .nuxt
86 | dist
87 |
88 | # Gatsby files
89 | .cache/
90 | # Comment in the public line in if your project uses Gatsby and *not* Next.js
91 | # https://nextjs.org/blog/next-9-1#public-directory-support
92 | # public
93 |
94 | # vuepress build output
95 | .vuepress/dist
96 |
97 | # Serverless directories
98 | .serverless/
99 |
100 | # FuseBox cache
101 | .fusebox/
102 |
103 | # DynamoDB Local files
104 | .dynamodb/
105 |
106 | # TernJS port file
107 | .tern-port
108 |
--------------------------------------------------------------------------------
/src/main.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | const {dirname, join} = require('path')
4 | const {Controller, Engine} = require('@sabaki/gtp')
5 | const {version} = require('../package.json')
6 |
7 | function parseAnalysis(line) {
8 | return line
9 | .split(/\s*info\s+/)
10 | .slice(1)
11 | .map(x => x.trim().replace(/ownership\s+(\d+(\.\d+)?\s+)+/g, ''))
12 | .map(x => {
13 | let matchPV = x.match(/(pass|[A-Za-z]\d+)(\s+(pass|[A-Za-z]\d+))*$/)
14 | if (matchPV == null) return null
15 |
16 | let passIndex = matchPV[0].indexOf('pass')
17 | if (passIndex < 0) passIndex = Infinity
18 |
19 | return [
20 | x
21 | .slice(0, matchPV.index)
22 | .trim()
23 | .split(/\s+/)
24 | .slice(0, -1),
25 | matchPV[0]
26 | .slice(0, passIndex)
27 | .split(/\s+/)
28 | .filter(x => x.length >= 2)
29 | ]
30 | })
31 | .filter(x => x != null)
32 | .map(([tokens, pv]) => {
33 | let keys = tokens.filter((_, i) => i % 2 === 0)
34 | let values = tokens.filter((_, i) => i % 2 === 1)
35 |
36 | keys.push('pv')
37 | values.push(pv)
38 |
39 | return keys.reduce((acc, x, i) => ((acc[x] = values[i]), acc), {})
40 | })
41 | .filter(({move}) => move.match(/^[A-Za-z]\d+$/))
42 | .map(result => ({
43 | ...result,
44 | winrate: +result.winrate,
45 | scoreLead: +result.scoreLead
46 | }))
47 | }
48 |
49 | function stringifyAnalysis(analysis) {
50 | return analysis
51 | .map(
52 | entry =>
53 | `info ${Object.entries(entry)
54 | .map(
55 | ([key, value]) =>
56 | `${key} ${Array.isArray(value) ? value.join(' ') : value}`
57 | )
58 | .join(' ')}`
59 | )
60 | .join(' ')
61 | }
62 |
63 | async function main() {
64 | let args = process.argv.slice(2)
65 | let gtpMode = args[0] === 'gtp'
66 | let katagoPath = join(dirname(process.execPath), 'katago')
67 |
68 | let controller = new Controller(katagoPath, args)
69 | let engine = new Engine('KataJigo', version)
70 |
71 | controller.on('started', () => {
72 | if (!gtpMode) {
73 | controller.process.stdout.on('data', chunk => {
74 | process.stdout.write(chunk)
75 | })
76 |
77 | process.stdin.on('data', chunk => {
78 | controller.process.stdin.write(chunk)
79 | })
80 | }
81 | })
82 |
83 | controller.on('stopped', () => {
84 | process.exit()
85 | })
86 |
87 | controller.on('stderr', ({content}) => {
88 | process.stderr.write(content + '\n')
89 | })
90 |
91 | async function genmoveAnalyze(args, subscriber = () => {}) {
92 | let lastAnalysis = null
93 | let originalMove = null
94 | let foundMove = null
95 | let response = await controller.sendCommand(
96 | {
97 | name: 'kata-genmove_analyze',
98 | args
99 | },
100 | evt => {
101 | if (evt.line.startsWith('info ')) {
102 | lastAnalysis = parseAnalysis(evt.line)
103 | } else if (evt.line.startsWith('play ') && lastAnalysis != null) {
104 | originalMove = evt.line.slice('play '.length).trim()
105 |
106 | lastAnalysis = lastAnalysis.filter(
107 | // Ensure we're still winning
108 | variation => variation.winrate >= 0.5 && variation.scoreLead >= 0
109 | )
110 |
111 | let minScoreLead = Math.min(
112 | ...lastAnalysis.map(variation => variation.scoreLead)
113 | )
114 |
115 | lastAnalysis = lastAnalysis.filter(
116 | variation => variation.scoreLead === minScoreLead
117 | )
118 |
119 | let maxWinrate = Math.max(
120 | ...lastAnalysis.map(variation => variation.winrate)
121 | )
122 |
123 | lastAnalysis = lastAnalysis.filter(
124 | variation => variation.winrate === maxWinrate
125 | )
126 |
127 | if (lastAnalysis.length > 0) {
128 | let variation = lastAnalysis[0]
129 |
130 | for (let [key, value] of Object.entries(variation)) {
131 | console.error(
132 | `${key}: ${Array.isArray(value) ? value.join(' ') : value}`
133 | )
134 | }
135 |
136 | foundMove = variation.move
137 | evt.line = `play ${variation.move}`
138 | }
139 | }
140 |
141 | if (originalMove != null && foundMove != null) {
142 | evt.response.content = evt.response.content.replace(
143 | `play ${originalMove}`,
144 | `play ${foundMove}`
145 | )
146 | }
147 |
148 | subscriber(evt)
149 | }
150 | )
151 |
152 | if (!response.error && foundMove != null) {
153 | await controller.sendCommand({name: 'undo'})
154 | await controller.sendCommand({name: 'play', args: [args[0], foundMove]})
155 | }
156 |
157 | return response
158 | }
159 |
160 | engine.on('command-received', ({command}) => {
161 | controller.process.stdin.write('\n')
162 |
163 | if (
164 | !['name', 'version', 'genmove', 'lz-genmove_analyze'].includes(
165 | command.name
166 | )
167 | ) {
168 | engine.command(command.name, async (command, out) => {
169 | let firstWrite = true
170 | let subscriber = ({response, end, line}) => {
171 | if (!response.error && !end) {
172 | if (!firstWrite) out.write('\n')
173 | out.write(firstWrite ? response.content : line)
174 | firstWrite = false
175 | }
176 | }
177 |
178 | let response =
179 | command.name === 'kata-genmove_analyze'
180 | ? await genmoveAnalyze(command.args, subscriber)
181 | : await controller.sendCommand(command, subscriber)
182 |
183 | if (response.error) out.err(response.content)
184 | out.end()
185 | })
186 | }
187 | })
188 |
189 | engine.command('genmove', async (command, out) => {
190 | let response = await genmoveAnalyze(command.args.slice(0, 1), ({line}) => {
191 | if (line.startsWith('play ')) {
192 | let move = line.slice('play '.length).trim()
193 | out.send(move)
194 | }
195 | })
196 |
197 | if (response.error) out.err(response.content)
198 | })
199 |
200 | engine.command('lz-genmove_analyze', async (command, out) => {
201 | let firstWrite = true
202 |
203 | await genmoveAnalyze(command.args, ({response, line}) => {
204 | if (!firstWrite) out.write('\n')
205 |
206 | if (line.startsWith('info ')) {
207 | let analysis = parseAnalysis(line)
208 | let keys = ['move', 'visits', 'winrate', 'prior', 'lcb', 'order', 'pv']
209 |
210 | analysis = analysis.map(entry =>
211 | keys.reduce((acc, key) => ((acc[key] = entry[key]), acc), {})
212 | )
213 |
214 | for (let entry of analysis) {
215 | entry.winrate = Math.round(+entry.winrate * 10000)
216 | entry.prior = Math.round(+entry.prior * 10000)
217 | entry.lcb = Math.round(+entry.lcb * 10000)
218 | }
219 |
220 | out.write(stringifyAnalysis(analysis))
221 | } else {
222 | out.write(firstWrite ? response.content : line)
223 | }
224 |
225 | firstWrite = false
226 | })
227 |
228 | out.end()
229 | })
230 |
231 | controller.start()
232 | if (gtpMode) engine.start()
233 | }
234 |
235 | main().catch(console.error)
236 |
--------------------------------------------------------------------------------