Last active
July 12, 2021 14:05
-
-
Save 9beach/2e1860eca4d99333ffb9b94f054e2ca9 to your computer and use it in GitHub Desktop.
SGF utilities
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env node | |
/** | |
* @fileOverview Converts Tygem's GIB format to SGF. | |
*/ | |
// GIB format example. | |
// | |
// \HS | |
// ... | |
// \[GAMECOMMENT=\] | |
// \[GAMETAG=S1,R3,D5,G0,W255,Z0,T30-3-1200,C2016:03:26:17:29, ...\] | |
// \HE | |
// \GS | |
// 2 1 0 | |
// 119 0 &4 | |
// INI 0 1 3 &4 | |
// STO 0 2 2 15 15 | |
// ... | |
// STO 0 119 1 10 8 | |
// \GE | |
// ('EV', 'World Cup') => 'EV[World Cup]' | |
const mkProp = (p, v) => (v ? `${p}[${v}]` : ''); | |
// Converts GIB to SGF. | |
function convert(gib) { | |
// Gets RootNode from HS~HE and INI. | |
// Properties: FF, GM, SZ, AP, PB, BR, PW, WR, EV, RE, KM, DT, HA, AB. | |
const root = `;FF[3]GM[1]SZ[19]AP[https://github.com/9beach/analyze-sgf]${ | |
pbFromGIB(gib) + | |
pwFromGIB(gib) + | |
mkProp('EV', propValueFromGIB(gib, 'GAMENAME')) + | |
reFromGIB(gib) + | |
kmFromGIB(gib) + | |
dtFromGIB(gib) + | |
haFromGIB(gib) | |
}`; | |
// Gets SGF NodeSequence from STOs. | |
const seq = gib | |
.substring(gib.indexOf('STO')) | |
.split('\n') | |
.filter((line) => line.indexOf('STO ') !== -1) | |
.reduce((acc, cur) => acc + nodeFromSTO(cur.trim()), ''); | |
return `(${root}${seq})`; | |
} | |
// '1' => 'A' | |
const oneToA = (x) => String.fromCharCode(97 + parseInt(x, 10)); | |
// 'STO 0 2 2 15 15' => ';W[pp]' | |
function nodeFromSTO(line) { | |
const ns = line.split(/\s+/); | |
const pl = ns[3] === '1' ? 'B' : 'W'; | |
return `;${pl}[${oneToA(ns[4])}${oneToA(ns[5])}]`; | |
} | |
// Gets PB, BR. | |
function pbFromGIB(gib) { | |
const v = propValueFromGIB(gib, 'GAMEBLACKNAME'); | |
if (!v) return ''; | |
const pair = parsePlRank(v); | |
return mkProp('PB', pair[0]) + mkProp('BR', pair[1]); | |
} | |
// Gets PW, WR. | |
function pwFromGIB(gib) { | |
const v = propValueFromGIB(gib, 'GAMEWHITENAME'); | |
if (!v) return ''; | |
const pair = parsePlRank(v); | |
return mkProp('PW', pair[0]) + mkProp('WR', pair[1]); | |
} | |
// Gets DT. | |
function dtFromGIB(gib) { | |
const v = propValueFromGIB(gib, 'GAMETAG'); | |
if (!v) return ''; | |
const w = v.match(/C(\d\d\d\d):(\d\d):(\d\d)/); | |
return w ? mkProp('DT', w.slice(1).join('-')) : ''; | |
} | |
// Gets RE. | |
function reFromGIB(gib) { | |
const ginfo = propValueFromGIB(gib, 'GAMEINFOMAIN'); | |
if (ginfo) return mkProp('RE', getRE(ginfo, /GRLT:(\d+),/, /ZIPSU:(\d+),/)); | |
const gtag = propValueFromGIB(gib, 'GAMETAG'); | |
return gtag ? mkProp('RE', getRE(gtag, /,W(\d+),/, /,Z(\d+),/)) : ''; | |
} | |
// Gets KM. | |
function kmFromGIB(gib) { | |
const ginfo = propValueFromGIB(gib, 'GAMEINFOMAIN'); | |
if (ginfo) { | |
const v = ginfo.match(/GONGJE:(\d+),/); | |
if (v) { | |
const komi = parseInt(v[1], 10) / 10; | |
if (!Number.isNaN(komi)) return mkProp('KM', komi.toString()); | |
} | |
} | |
const gtag = propValueFromGIB(gib, 'GAMETAG'); | |
if (gtag) { | |
const v = gtag.match(/,G(\d+),/); | |
if (v) { | |
const komi = parseInt(v[1], 10) / 10; | |
if (!Number.isNaN(komi)) return mkProp('KM', komi.toString()); | |
} | |
} | |
return ''; | |
} | |
const handicapStones = [ | |
null, | |
null, | |
'dp][pd', | |
'dp][pd][dd', | |
'dp][pd][dd][pp', | |
'dp][pd][dd][pp][jj', | |
'dp][pd][dd][pp][dj][pj', | |
'dp][pd][dd][pp][dj][pj][jj', | |
'dp][pd][dd][pp][dj][pj][jd][jp', | |
'dp][pd][dd][pp][dj][pj][jd][jp][jj', | |
]; | |
// Gets HA, AB. | |
function haFromGIB(gib) { | |
const v = valueOfINI(gib); | |
if (!v) return ''; | |
const setup = v.split(/\s+/); | |
const ha = parseInt(setup[2], 10); | |
return ha >= 2 && ha <= 9 | |
? mkProp('HA', ha) + mkProp('AB', handicapStones[ha]) | |
: ''; | |
} | |
// 'lee(8k)' => ['lee', '8k'] | |
function parsePlRank(v) { | |
const index = v.lastIndexOf('('); | |
return [ | |
v.substring(0, index).trim(), | |
v.substring(index + 1, v.length - 1).trim(), | |
]; | |
} | |
// ('...\[GAMEWHITENICK=xyz\]...', 'GAMEWHITENICK') => 'xyz' | |
function propValueFromGIB(gib, prop) { | |
const start = gib.indexOf(`\\[${prop}=`); | |
if (start === -1) return ''; | |
const end = gib.indexOf('\\]', start + prop.length); | |
return end === -1 ? '' : gib.substring(start + prop.length + 3, end).trim(); | |
} | |
// '...\nINI 0 1 5 ...\n...' => '0 1 5 ...' | |
function valueOfINI(gib) { | |
const start = gib.indexOf('INI '); | |
if (start === -1) return ''; | |
const end = gib.indexOf('\n', start + 4); | |
return end === -1 ? '' : gib.substring(start + 4, end).trim(); | |
} | |
// Gets RE. | |
function getRE(v, grltRegex, zipsuRegex) { | |
const gmatch = grltRegex.exec(v); | |
if (!gmatch) return ''; | |
const grlt = parseFloat(gmatch[1]); | |
const zmatch = zipsuRegex.exec(v); | |
return zmatch ? parseRE(grlt, parseFloat(zmatch[1])) : ''; | |
} | |
function parseRE(grlt, zipsu) { | |
const easycases = { 3: 'B+R', 4: 'W+R', 7: 'B+T', 8: 'W+T' }; | |
if (easycases[grlt] !== undefined) return easycases[grlt]; | |
return grlt === 0 || grlt === 1 | |
? `${grlt === 0 ? 'B' : 'W'}+${zipsu / 10}` | |
: ''; | |
} | |
module.exports = convert; | |
if (require.main === module) { | |
if (process.argv.length !== 2) { | |
console.log('Usage: gib2sgf < FILE'); | |
process.exit(1); | |
} | |
const data = []; | |
process.stdin.on('data', (chunk) => data.push(chunk)); | |
process.stdin.on('end', () => console.log(convert(data.join('')))); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env node | |
/** | |
* @fileOverview Downloads SGF from URL (Tygem and CyberORO). | |
*/ | |
const { XMLHttpRequest } = require('xmlhttprequest'); | |
const TYGEM_URL_BEGINS = 'http://service.tygem.com/service/gibo2/?seq='; | |
const ORO_URL_BEGINS = 'https://www.cyberoro.com/gibo_new/giboviewer/gibovie'; | |
const isValidURL = (url) => | |
url.indexOf(TYGEM_URL_BEGINS) === 0 || url.indexOf(ORO_URL_BEGINS) === 0; | |
function httpgetRawSGF(url) { | |
if (!isValidURL(url)) throw Error('URL not supported.'); | |
const http = new XMLHttpRequest(); | |
http.open('GET', url, false); | |
http.send(null); | |
return url.indexOf(TYGEM_URL_BEGINS) === 0 | |
? http.responseText | |
.replace(/\r\n/g, '') | |
.replace(/.*var sgf = "/, '') | |
.replace(/\).*/, ')') | |
: http.responseText | |
.replace(/\r\n/g, '') | |
.replace(/.*"hidden" name = "gibo_txt" id = "gibo_txt"[^(]*/, '') | |
.replace(/\).*/, ')'); | |
} | |
function httpgetSGF(url) { | |
const sgf = httpgetRawSGF(url); | |
if ( | |
sgf[sgf.length - 1] !== ')' || | |
(sgf.indexOf('(TE[') !== 0 && sgf.indexOf('(;GM[') !== 0) | |
) | |
throw Error('Invalid response from URL'); | |
// Fixes SGF dialects (KO/TE/RD) for other SGF editors. | |
// Fixes bad Tygem SGF. e.g., '대주배 16강 .' | |
// Fixes bad Tygem SGF. e.g., '신진서 ' | |
// Fixes bad Tygem SGF. e.g., '김미리:김미리:4단'. | |
return sgf | |
.replace(/\([;]*TE\[/, '(;GM[1]FF[4]EV[') | |
.replace(/\bRD\[/, 'DT[') | |
.replace(/\bK[OM]\[\]/, '') | |
.replace(/\bKO\[/, 'KM[') | |
.replace(/ \.\]/g, ']') | |
.replace(/ *\]/g, ']') | |
.replace(/\[ */g, '[') | |
.replace(/(P[BW]\[[^\]:]*):[^\]]*\]/g, '$1]'); | |
} | |
module.exports = { isValidURL, httpgetSGF }; | |
if (require.main === module) { | |
try { | |
if (process.argv.length !== 3) | |
console.log('Usage: httpget-sgf URL > FILE'); | |
else console.log(httpgetSGF(process.argv[2])); | |
} catch (error) { | |
console.log(error.message); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment