├── .python-version ├── .gitignore ├── favicon.ico ├── donate_katago_qr.png ├── katago_gui ├── static │ ├── eng.png │ ├── kata.png │ ├── kor.png │ ├── china.png │ ├── favicon.ico │ ├── japan.png │ ├── kata-gray.png │ ├── kata-red.png │ ├── large │ │ ├── black.png │ │ ├── mono.jpg │ │ ├── shadow.png │ │ ├── walnut.jpg │ │ ├── white.png │ │ ├── black43.png │ │ ├── shinkaya.jpg │ │ ├── white43.png │ │ ├── shadow_dark.png │ │ ├── shadow_dark43.png │ │ └── board.js │ ├── small │ │ ├── black.png │ │ ├── mono.jpg │ │ ├── shadow.png │ │ ├── walnut.jpg │ │ ├── white.png │ │ ├── shinkaya.jpg │ │ ├── shadow_dark.png │ │ └── board.js │ ├── main.css │ ├── reconnecting-websocket.min.js │ ├── js.cookie.js │ └── sgf.js ├── templates │ ├── ttest.tmpl │ ├── flash_only.tmpl │ ├── watch_select_game.tmpl │ ├── about.tmpl │ ├── reset_request.tmpl │ ├── reset_token.tmpl │ ├── find_game.tmpl │ ├── login.tmpl │ ├── layout_watch.tmpl │ ├── account.tmpl │ ├── register.tmpl │ ├── watch_mobile.tmpl │ ├── watch.tmpl │ ├── layout.tmpl │ ├── settings_overlay.tmpl │ └── settings_overlay_mobile.tmpl ├── gotypes.py ├── dbmodel.py ├── __init__.py ├── go_utils.py ├── auth.py ├── forms.py ├── create_tables.py ├── scoring.py ├── routes_watch.py ├── routes.py ├── helpers.py └── translations.py ├── scripts ├── delete_old_guests.sql └── all_game_urls_for_user.sql ├── Procfile ├── pre-commit ├── sgf_examples ├── h4.sgf ├── fox-handicap.sgf ├── go4go-1.sgf ├── fox-no-komi.sgf ├── pandanet-H2.sgf ├── ogs-handicap.sgf ├── pandanet-even.sgf ├── go4go-2.sgf ├── kgs-H2.sgf ├── fox-even.sgf ├── kifucam-H2.sgf ├── ogs-even.sgf ├── kifucam-even.sgf └── kgs-even.sgf ├── paypal_donations.txt ├── requirements.txt ├── heroku_app.py ├── fill_zobrist.py ├── checksame.py └── README.md /.python-version: -------------------------------------------------------------------------------- 1 | 3.12 2 | 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | venv 3 | .DS_Store 4 | change_pwd.py 5 | 6 | 7 | -------------------------------------------------------------------------------- /favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hauensteina/katagui/HEAD/favicon.ico -------------------------------------------------------------------------------- /donate_katago_qr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hauensteina/katagui/HEAD/donate_katago_qr.png -------------------------------------------------------------------------------- /katago_gui/static/eng.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hauensteina/katagui/HEAD/katago_gui/static/eng.png -------------------------------------------------------------------------------- /katago_gui/static/kata.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hauensteina/katagui/HEAD/katago_gui/static/kata.png -------------------------------------------------------------------------------- /katago_gui/static/kor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hauensteina/katagui/HEAD/katago_gui/static/kor.png -------------------------------------------------------------------------------- /katago_gui/static/china.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hauensteina/katagui/HEAD/katago_gui/static/china.png -------------------------------------------------------------------------------- /katago_gui/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hauensteina/katagui/HEAD/katago_gui/static/favicon.ico -------------------------------------------------------------------------------- /katago_gui/static/japan.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hauensteina/katagui/HEAD/katago_gui/static/japan.png -------------------------------------------------------------------------------- /katago_gui/static/kata-gray.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hauensteina/katagui/HEAD/katago_gui/static/kata-gray.png -------------------------------------------------------------------------------- /katago_gui/static/kata-red.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hauensteina/katagui/HEAD/katago_gui/static/kata-red.png -------------------------------------------------------------------------------- /katago_gui/static/large/black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hauensteina/katagui/HEAD/katago_gui/static/large/black.png -------------------------------------------------------------------------------- /katago_gui/static/large/mono.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hauensteina/katagui/HEAD/katago_gui/static/large/mono.jpg -------------------------------------------------------------------------------- /katago_gui/static/large/shadow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hauensteina/katagui/HEAD/katago_gui/static/large/shadow.png -------------------------------------------------------------------------------- /katago_gui/static/large/walnut.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hauensteina/katagui/HEAD/katago_gui/static/large/walnut.jpg -------------------------------------------------------------------------------- /katago_gui/static/large/white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hauensteina/katagui/HEAD/katago_gui/static/large/white.png -------------------------------------------------------------------------------- /katago_gui/static/small/black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hauensteina/katagui/HEAD/katago_gui/static/small/black.png -------------------------------------------------------------------------------- /katago_gui/static/small/mono.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hauensteina/katagui/HEAD/katago_gui/static/small/mono.jpg -------------------------------------------------------------------------------- /katago_gui/static/small/shadow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hauensteina/katagui/HEAD/katago_gui/static/small/shadow.png -------------------------------------------------------------------------------- /katago_gui/static/small/walnut.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hauensteina/katagui/HEAD/katago_gui/static/small/walnut.jpg -------------------------------------------------------------------------------- /katago_gui/static/small/white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hauensteina/katagui/HEAD/katago_gui/static/small/white.png -------------------------------------------------------------------------------- /katago_gui/static/large/black43.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hauensteina/katagui/HEAD/katago_gui/static/large/black43.png -------------------------------------------------------------------------------- /katago_gui/static/large/shinkaya.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hauensteina/katagui/HEAD/katago_gui/static/large/shinkaya.jpg -------------------------------------------------------------------------------- /katago_gui/static/large/white43.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hauensteina/katagui/HEAD/katago_gui/static/large/white43.png -------------------------------------------------------------------------------- /katago_gui/static/small/shinkaya.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hauensteina/katagui/HEAD/katago_gui/static/small/shinkaya.jpg -------------------------------------------------------------------------------- /katago_gui/static/large/shadow_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hauensteina/katagui/HEAD/katago_gui/static/large/shadow_dark.png -------------------------------------------------------------------------------- /katago_gui/static/small/shadow_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hauensteina/katagui/HEAD/katago_gui/static/small/shadow_dark.png -------------------------------------------------------------------------------- /katago_gui/static/large/shadow_dark43.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hauensteina/katagui/HEAD/katago_gui/static/large/shadow_dark43.png -------------------------------------------------------------------------------- /scripts/delete_old_guests.sql: -------------------------------------------------------------------------------- 1 | 2 | delete from t_user where username like 'guest_%' and extract(epoch from now() - ts_last_seen)/3600 > 24; 3 | 4 | 5 | -------------------------------------------------------------------------------- /katago_gui/templates/ttest.tmpl: -------------------------------------------------------------------------------- 1 | {% extends 'layout.tmpl' %} 2 | 3 | 4 | {% block content %} 5 |
6 | {{ msg }} 7 |
8 | {% endblock content %} 9 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | 2 | web: gunicorn -k flask_sockets.worker heroku_app:app --workers 2 --threads 4 --limit-request-line 8000 --max-requests 1000 --max-requests-jitter 200 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | sed -i '' 's/katago_server_test/katago_server/' heroku_app.py 4 | sed -i '' 's/const STRONG = 1/const STRONG = 0/' katago_gui/static/main.js 5 | git add heroku_app.py 6 | -------------------------------------------------------------------------------- /scripts/all_game_urls_for_user.sql: -------------------------------------------------------------------------------- 1 | 2 | -- Dump all games by one player 3 | -- AHN, Jan 2021 4 | 5 | select 6 | ts_started, 7 | handicap, 8 | 'https://katagui.herokuapp.com/watch_game?live=0&game_hash=' || game_hash as url 9 | from 10 | t_game 11 | where 12 | username = 'dfs' 13 | order by 1 14 | ; 15 | -------------------------------------------------------------------------------- /sgf_examples/h4.sgf: -------------------------------------------------------------------------------- 1 | (;FF[4]SZ[19] 2 | SO[katago-one-playout.herokuapp.com] 3 | PB[] 4 | PW[] 5 | RE[] 6 | KM[0.5] 7 | DT[2020-10-17] 8 | ;B[dp]C[P:0.00 S:0.0];W[tt];B[pd]C[P:0.00 S:0.0];W[tt];B[pp]C[P:0.00 S:0.0];W[tt];B[dd]C[P:0.00 S:0.0];W[fq]C[P:1.00 S:33.1];B[jq]C[P:1.00 S:33.4];W[dn]C[P:1.00 S:33.4];B[en]C[P:1.00 S:34.1];W[em]C[P:1.00 S:34.1];B[eo]C[P:1.00 S:33.0];W[cq]C[P:1.00 S:33.0]) -------------------------------------------------------------------------------- /paypal_donations.txt: -------------------------------------------------------------------------------- 1 | paypal business 2 | ahauens@gmail.com 3 | T...5! 4 | 5 |
6 | 7 | 8 | 9 | 10 |
11 | 12 | https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=T322ZZH9TKMMN&source=url 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | bcrypt==4.1.3 2 | blinker==1.8.2 3 | certifi==2024.6.2 4 | charset-normalizer==3.3.2 5 | click==8.1.7 6 | dnspython==2.6.1 7 | email_validator==2.2.0 8 | Flask==3.0.3 9 | Flask-Bcrypt==1.0.1 10 | Flask-Login==0.6.3 11 | Flask-Mail==0.10.0 12 | Flask-Sockets==0.2.1 13 | Flask-WTF==1.2.1 14 | gevent==24.2.1 15 | gevent-websocket==0.10.1 16 | greenlet==3.0.3 17 | gunicorn==22.0.0 18 | idna==3.7 19 | itsdangerous==2.2.0 20 | Jinja2==3.1.4 21 | MarkupSafe==2.1.5 22 | numpy==2.0.0 23 | packaging==24.1 24 | psycopg2-binary==2.9.10 25 | redis==5.0.6 26 | requests==2.32.3 27 | setuptools==70.1.0 28 | six==1.16.0 29 | urllib3==2.2.2 30 | Werkzeug==3.0.3 31 | WTForms==3.1.2 32 | zope.event==5.0 33 | zope.interface==6.4.post2 34 | -------------------------------------------------------------------------------- /heroku_app.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # /******************************************************************** 4 | # Filename: katago-gui/katago_gui/heroku_app.py 5 | # Author: AHN 6 | # Creation Date: Jul, 2020 7 | # **********************************************************************/ 8 | # 9 | # A web front end for REST Api katago-server 10 | # 11 | 12 | from pdb import set_trace as BP 13 | from katago_gui import app 14 | 15 | #---------------------------- 16 | if __name__ == '__main__': 17 | # This won't work with websockets. 18 | app.run( host='0.0.0.0', port=8000, debug=True) 19 | # Instead when debugging: 20 | # $ source .env; gunicorn -k flask_sockets.worker heroku_app:app -w 1 -b 0.0.0.0:8000 --reload --timeout 1000 21 | -------------------------------------------------------------------------------- /katago_gui/templates/flash_only.tmpl: -------------------------------------------------------------------------------- 1 | {% extends 'layout.tmpl' %} 2 | 3 | {% block css %} 4 | 5 | {% endblock %} 6 | 7 | {% block content %} 8 |
9 | 10 | 23 |
11 | {% with messages = get_flashed_messages( with_categories=true) %} 12 | {% if messages %} 13 | {% for category, message in messages %} 14 |
15 | {{ message }} 16 |
17 | {% endfor %} 18 | {% else %} 19 |

20 | {% endif %} 21 | {% endwith %} 22 |
24 |
25 | {% endblock content %} 26 | -------------------------------------------------------------------------------- /sgf_examples/fox-handicap.sgf: -------------------------------------------------------------------------------- 1 | (;GM[1]FF[4] 2 | SZ[19] 3 | GN[] 4 | DT[2020-03-21] 5 | PB[卡比獸偶尼] 6 | PW[偶兔] 7 | BR[3段] 8 | WR[9段] 9 | KM[0]HA[4]RU[Japanese]AP[GNU Go:3.8]RE[draw]TM[5400]TC[10]TT[30]AP[foxwq]RL[0] 10 | ;AB[pd][dp][dd][pp]; 11 | W[qf];B[qe];W[pf];B[nc];W[qn];B[ql];W[on];B[qi];W[np];B[nq];W[oq];B[mq];W[op];B[oi];W[mf];B[kd];W[jq];B[hq];W[od];B[oc];W[qc];B[pc];W[re];B[qd];W[rg];B[rn];W[ro];B[rm];W[rc];B[rp];W[qp];B[so];W[qo];B[dj];W[ri];B[rj];W[qh];B[pi];W[rk];B[si];W[qk];B[pl];W[pk];B[ok];W[ol];B[rl];W[md];B[mc];W[sh];B[rh];W[lc];B[ld];W[ri];B[sj];W[qj];B[rh];W[oj];B[nk];W[ri];B[rq];W[sk];B[qq];W[pq];B[pr];W[or];B[qs];W[nl];B[lp];W[sr];B[rr];W[jo];B[ln];W[os];B[ps];W[rs];B[ss];W[jd];B[kc];W[rs];B[po];W[pm];B[ss];W[cc];B[cd];W[dc];B[ec];W[eb];B[fc];W[fb];B[gc];W[rs];B[pn];W[oo];B[ss];W[gb];B[sq];W[fq];B[fp];W[il];B[bc];W[bb];B[as];W[jm];B[ll];W[mk];B[lk];W[cl];B[dl];W[dm];B[cm];W[ck]) -------------------------------------------------------------------------------- /katago_gui/gotypes.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # /******************************************************************** 4 | # Filename: gotypes.py 5 | # Author: AHN 6 | # Creation Date: Mar, 2019 7 | # **********************************************************************/ 8 | # 9 | # Types for implementing a go board 10 | # 11 | 12 | import enum 13 | from collections import namedtuple 14 | 15 | # Whose turn is it 16 | #============================= 17 | class Player( enum.Enum): 18 | black = 1 19 | white = 2 20 | 21 | @property 22 | def other( self): 23 | return Player.black if self == Player.white else Player.white 24 | 25 | # Board intersection 26 | #================================================= 27 | class Point( namedtuple( 'Point', 'row col')): 28 | # Neighbors in tblr order 29 | #-------------------------- 30 | def neighbors( self): 31 | return [ 32 | Point( self.row - 1, self.col), 33 | Point( self.row + 1, self.col), 34 | Point( self.row, self.col - 1), 35 | Point( self.row, self.col + 1) 36 | ] 37 | 38 | #------------ 39 | def main(): 40 | p = Player( Player.white) 41 | print( p) 42 | print( p.other) 43 | print( Point(0,1).neighbors()) 44 | -------------------------------------------------------------------------------- /sgf_examples/go4go-1.sgf: -------------------------------------------------------------------------------- 1 | (;DT[2020-09-08]EV[9th Ing Cup]RO[round 1] 2 | PB[Shin Jinseo]BR[9p]PW[Xie Erhao]WR[9p] 3 | KM[8]RE[B+R]SO[Go4Go.net] 4 | ;B[qd];W[pp];B[dc];W[cp];B[eq];W[do];B[de];W[oc];B[pf];W[kd];B[qn];W[qo];B[pn];W[np];B[lq];W[qk];B[mo];W[no];B[qq];W[ro];B[oq];W[pq];B[pr];W[nr];B[or];W[nq];B[rr];W[op];B[pk];W[pj];B[qj];W[qi];B[rj];W[ok];B[pl];W[ri];B[rk];W[ol];B[pm];W[oj];B[nn];W[sq];B[sr];W[os];B[qs];W[lp];B[mm];W[ln];B[mn];W[kq];B[ph];W[pi];B[hq];W[qc];B[rc];W[pd];B[qb];W[qe];B[pc];W[rd];B[od];W[qc];B[mp];W[mq];B[qd];W[pe];B[nc];W[ob];B[oe];W[qc];B[pb];W[of];B[nb];W[dq];B[er];W[me];B[nf];W[og];B[lo];W[kp];B[ko];W[ip];B[jr];W[hp];B[iq];W[ns];B[ps];W[jo];B[cj];W[cg];B[dh];W[ch];B[di];W[cd];B[cc];W[ce];B[dd];W[dg];B[eg];W[bj];B[bk];W[bi];B[cl];W[dr];B[bc];W[bf];B[bd];W[be];B[df];W[ah];B[qf];W[rf];B[id];W[fp];B[ep];W[eo];B[fq];W[dk];B[ck];W[kn];B[qd];W[re];B[gp];W[fo];B[go];W[kr];B[jn];W[ll];B[ho];W[ls];B[ml];W[kl];B[mk];W[in];B[jm];W[hm];B[io];W[jp];B[im];W[lj];B[mj];W[li];B[hl];W[gm];B[gl];W[fm];B[ik];W[kh];B[jj];W[kj];B[oh];W[ne];B[nd];W[pg];B[rm];W[ng];B[el];W[em];B[ig];W[ih];B[hh];W[ii];B[hi];W[ie];B[he];W[ic];B[hc];W[jd];B[hd];W[if];B[hg];W[hn];B[gr];W[dl];B[ek];W[jg];B[ni];W[mi];B[nj];W[nh];B[oi];W[qh];B[aj];W[bg];B[bn];W[ib];B[hb];W[dm];B[dj];W[bo];B[cn];W[co];B[ds];W[cs];B[es];W[br];B[kb];W[kc];B[ja];W[sj];B[ql];W[qc];B[rb]) 5 | -------------------------------------------------------------------------------- /sgf_examples/fox-no-komi.sgf: -------------------------------------------------------------------------------- 1 | (;GM[1]FF[4] 2 | SZ[19] 3 | GN[] 4 | DT[2025-10-23] 5 | PB[V350010278] 6 | PW[不知火七] 7 | BR[3段] 8 | WR[4段] 9 | KM[0]HA[1]RU[Chinese]AP[GNU Go:3.8]RN[1]RE[W+2.00]TM[60]TC[3]TT[20]AP[foxwq]RL[0] 10 | ;B[dp];W[pd];B[pp];W[dd];B[jc];W[qn];B[qd];W[qc];B[qe];W[pc];B[qh];W[lc];B[cd];W[cc];B[ce];W[dc];B[ch];W[hc];B[hd];W[gd];B[ld];W[mc];B[jq];W[id];B[jd];W[he];B[cl];W[jf];B[kf];W[kg];B[lf];W[je];B[lg];W[kh];B[lh];W[ki];B[eh];W[ke];B[ro];W[nq];B[oo];W[pr];B[ql];W[qq];B[om];W[cq];B[cp];W[dq];B[fq];W[fr];B[gr];W[er];B[hq];W[li];B[bq];W[br];B[bp];W[ar];B[nh];W[dk];B[ck];W[dj];B[ci];W[gj];B[em];W[pe];B[pf];W[of];B[pg];W[fm];B[fn];W[gm];B[gn];W[el];B[dl];W[hn];B[ho];W[hm];B[ek];W[ej];B[fl];W[fk];B[el];W[gl];B[mr];W[nr];B[mq];W[ni];B[oi];W[nj];B[oj];W[nk];B[ok];W[nl];B[in];W[im];B[jn];W[kl];B[jm];W[jl];B[np];W[rp];B[qp];W[rq];B[rn];W[ln];B[nm];W[mm];B[ko];W[ms];B[kr];W[eq];B[ep];W[gh];B[le];W[kd];B[rc];W[rb];B[rd];W[de];B[cf];W[bc];B[ef];W[fe];B[gg];W[hg];B[fh];W[gi];B[bd];W[sb];B[nf];W[fg];B[eg];W[ff];B[oe];W[od];B[ne];W[ls];B[lo];W[mn];B[oq];W[or];B[ac];W[ab];B[ad];W[ba];B[nd];W[nc];B[gs];W[fs];B[mi];W[mj];B[mh];W[nn];B[on];W[sd];B[re];W[sc];B[se];W[pq];B[mo];W[km];B[ol];W[df];B[dg];W[cj];B[bj];W[ee];B[di];W[aq];B[ap];W[op];B[qb];W[pb];B[oq];W[md];B[me];W[op];B[qa];W[pa];B[oq];W[fp];B[gq];W[op];B[cs];W[cr];B[oq];W[kn];B[op];W[ks];B[js];W[ns];B[lr];W[so];B[sn];W[sp];B[ei];W[fj];B[go]) -------------------------------------------------------------------------------- /sgf_examples/pandanet-H2.sgf: -------------------------------------------------------------------------------- 1 | (; 2 | GM[1]EV[Internet Go Server game: coco7 vs gobaby] 3 | US[Brought to you by IGS PANDANET] 4 | CoPyright[ 5 | Copyright (c) PANDANET Inc. 2025 6 | Permission to reproduce this game is given, provided proper credit is given. 7 | No warrantee, implied or explicit, is understood. 8 | Use of this game is an understanding and agreement of this notice. 9 | ] 10 | GN[coco7-gobaby(B) IGS]RE[W+Time] 11 | PW[coco7]WR[6d?]NW[34] 12 | PB[gobaby]BR[4d ]NB[32] 13 | PC[IGS: igs.joyjoy.net 6969]DT[2025-10-15] 14 | SZ[19]TM[60]KM[-5.500000]LT[] 15 | RR[Normal] 16 | HA[2]AB[pd][dp]C[ 17 | coco7 6d?: Let's begin and enjoy a great game. 18 | ] 19 | ;W[pp]WL[60] 20 | ;B[dc]BL[57] 21 | ;W[ce]WL[57] 22 | ;B[cd]BL[53] 23 | ;W[de]WL[56] 24 | ;B[fc]BL[51] 25 | ;W[di]WL[56] 26 | ;B[ck]BL[30] 27 | ;W[ed]WL[55] 28 | ;B[ec]BL[27] 29 | ;W[bd]WL[53] 30 | ;B[bc]BL[25] 31 | ;W[dd]WL[53] 32 | ;B[cc]BL[24] 33 | ;W[ge]WL[51] 34 | ;B[fd]BL[17] 35 | ;W[fe]WL[49] 36 | ;B[id]BL[10] 37 | ;W[do]WL[46] 38 | ;B[ep]BL[60] 39 | ;W[co]WL[34] 40 | ;B[ek]BL[51] 41 | ;W[cp]WL[27] 42 | ;B[cq]BL[49] 43 | ;W[bq]WL[25] 44 | ;B[cr]BL[47] 45 | ;W[fn]WL[24] 46 | ;B[gp]BL[41] 47 | ;W[dl]WL[22] 48 | ;B[dk]BL[35] 49 | ;W[gk]WL[13] 50 | ;B[ci]BL[30] 51 | ;W[ch]WL[12] 52 | ;B[dh]BL[16] 53 | ;W[bi]WL[11] 54 | ;B[cj]BL[14] 55 | ;W[ei]WL[60] 56 | ;B[bh]BL[11] 57 | ;W[cg]WL[59] 58 | ;B[bg]BL[9] 59 | ;W[bf]WL[59] 60 | ;B[ai]BL[7] 61 | ;W[cl]WL[57] 62 | ;B[el]BL[2] 63 | ;W[bl]WL[55]; 64 | 65 | OS[] 66 | 67 | ) 68 | -------------------------------------------------------------------------------- /sgf_examples/ogs-handicap.sgf: -------------------------------------------------------------------------------- 1 | (;FF[4] 2 | CA[UTF-8] 3 | GM[1] 4 | DT[2025-10-18] 5 | PC[OGS: https://online-go.com/game/80395664] 6 | GN[Friendly Match] 7 | PB[yijun.m.xiao] 8 | PW[HumZhang] 9 | BR[3k] 10 | WR[1k] 11 | TM[300]OT[5x30 byo-yomi] 12 | RE[B+R] 13 | SZ[19] 14 | KM[0.5] 15 | RU[Japanese] 16 | HA[2] 17 | AB[pd][dp] 18 | ;W[qp] 19 | (;B[oq] 20 | (;W[pn] 21 | (;B[fq] 22 | (;W[cd] 23 | (;B[ec] 24 | (;W[df] 25 | (;B[hd] 26 | (;W[qf] 27 | (;B[nd] 28 | (;W[qc] 29 | (;B[qd] 30 | (;W[rd] 31 | (;B[re] 32 | (;W[rb] 33 | (;B[sd] 34 | (;W[rc] 35 | (;B[qe] 36 | (;W[nb] 37 | (;B[mb] 38 | (;W[ob] 39 | (;B[mc] 40 | (;W[qh] 41 | (;B[pi] 42 | (;W[ph] 43 | (;B[pk] 44 | (;W[oi] 45 | (;B[qi] 46 | (;W[ng] 47 | (;B[oj] 48 | (;W[ni] 49 | (;B[qr] 50 | (;W[np] 51 | (;B[op] 52 | (;W[no] 53 | (;B[mr] 54 | (;W[nj] 55 | (;B[ol] 56 | (;W[nm] 57 | (;B[nk] 58 | (;W[mk] 59 | (;B[nl] 60 | (;W[ml] 61 | (;B[mm] 62 | (;W[om] 63 | (;B[pm] 64 | (;W[qm] 65 | (;B[pl] 66 | (;W[oo] 67 | (;B[lq] 68 | (;W[lm] 69 | (;B[ch] 70 | (;W[rk] 71 | (;B[qk] 72 | (;W[bo] 73 | (;B[cn] 74 | (;W[bn] 75 | (;B[cm] 76 | (;W[cq] 77 | (;B[cb] 78 | (;W[dh] 79 | (;B[di] 80 | (;W[eh] 81 | (;B[cg] 82 | (;W[cf] 83 | (;B[dq] 84 | (;W[cr] 85 | (;B[ei] 86 | (;W[ff] 87 | (;B[gi] 88 | (;W[hf] 89 | (;B[kn] 90 | (;W[ln] 91 | (;B[ko] 92 | (;W[il] 93 | (;B[hm] 94 | (;W[hl] 95 | (;B[gm] 96 | (;W[im] 97 | (;B[in] 98 | (;W[gl] 99 | (;B[fm] 100 | (;W[gs] 101 | (;B[es] 102 | )))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))) -------------------------------------------------------------------------------- /katago_gui/templates/watch_select_game.tmpl: -------------------------------------------------------------------------------- 1 | {% extends 'layout_watch.tmpl' %} 2 | 3 | {% block css %} 4 | 5 | {% endblock %} 6 | 7 | {% block content %} 8 | 9 |
10 | 11 |
12 |

{{ tr('Games Today') }}

13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | {% for game in games %} 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | {% endfor %} 36 |
{{tr('User')}}{{tr('Handicap')}}{{tr('Moves')}}{{tr('Idle')}}{{tr('Live')}}{{tr('Link')}}
{{ game.username }} {{ game.handicap }} {{ game.nmoves }} {{ game.t_idle }} {{ '\u2713' if game.live else '\u2717' }} {{tr('Observe')}}
37 |
38 | {% endblock content %} 39 | -------------------------------------------------------------------------------- /katago_gui/templates/about.tmpl: -------------------------------------------------------------------------------- 1 | {% extends 'layout.tmpl' %} 2 | 3 | {% block pre %} 4 | 10 | {% endblock pre %} 11 | 12 | {% block css %} 13 | 14 | {% endblock %} 15 | 16 | {% block content %} 17 | 18 | 19 | 20 | 56 |
 

About

21 | This is the TensorRT version of KataGo v1.14.1
22 | running on an NVIDIA RTX 3080.
23 | 
24 | The Pro Network is the strongest stable net as of Mar 12, 2024:
25 | kata1-b18c384nbt-s9131461376-d4087399203.bin.gz 
26 | 
27 | The Guest network is still the old 10 block version:
28 | g170e-b10c128-s1141046784-d204142634.bin.gz 
29 | 
30 | Credits:
31 | ---------
32 | 
33 | KataGo, by David J. Wu, is here:
34 | https://github.com/lightvector/KataGo
35 | 
36 | Go data structures were pilfered from the
37 | book 'Deep Learning and the Game of Go'
38 | by Max Pumperla and Kevin Ferguson.
39 | 
40 | The Go board is jGoBoard by
41 | Joonas Pihlajamaa.
42 | 
43 | Chinese translations were provided by
44 | Xiaocheng Stephen Hu.
45 | 
46 | Japanese translations were provided by
47 | Satoshi Banya.
48 | 
49 | Plumbing and glue is by me:
50 | 
51 | Andreas Hauenstein
52 | hauensteina@gmail.com
53 | https://github.com/hauensteina/katagui
54 | 
55 |
57 | {% endblock content %} 58 | -------------------------------------------------------------------------------- /sgf_examples/pandanet-even.sgf: -------------------------------------------------------------------------------- 1 | (;GM[1]FF[4]CA[UTF-8]AP[GoPanda2.8.0]ST[1] 2 | SZ[19]HA[0]KM[0.5] 3 | PW[gaffel]WR[2d+]PB[a1006b]BR[2d]RE[B+R]DT[2025-10-24]PC[IGS-PandaNet] 4 | 5 | ;B[po] 6 | ;W[dd] 7 | ;B[co] 8 | ;W[qd] 9 | ;B[cf] 10 | ;W[dq] 11 | ;B[ef] 12 | ;W[qq] 13 | ;B[ep] 14 | ;W[eq] 15 | ;B[fp] 16 | ;W[fq] 17 | ;B[gp] 18 | ;W[hq] 19 | ;B[io] 20 | ;W[ck] 21 | ;B[fd] 22 | ;W[bd] 23 | ;B[be] 24 | ;W[ce] 25 | ;B[bf] 26 | ;W[fc] 27 | ;B[gc] 28 | ;W[ec] 29 | ;B[gb] 30 | ;W[gd] 31 | ;B[fe] 32 | ;W[hd] 33 | ;B[ic] 34 | ;W[id] 35 | ;B[jc] 36 | ;W[df] 37 | ;B[dg] 38 | ;W[de] 39 | ;B[dh] 40 | ;W[bh] 41 | ;B[cg] 42 | ;W[cc] 43 | ;B[jd] 44 | ;W[hf] 45 | ;B[jf] 46 | ;W[hh] 47 | ;B[fg] 48 | ;W[gi] 49 | ;B[ci] 50 | ;W[bi] 51 | ;B[cj] 52 | ;W[bj] 53 | ;B[dk] 54 | ;W[cl] 55 | ;B[dl] 56 | ;W[cm] 57 | ;B[dm] 58 | ;W[ej] 59 | ;B[dj] 60 | ;W[cn] 61 | ;B[dn] 62 | ;W[jg] 63 | ;B[bn] 64 | ;W[bm] 65 | ;B[bo] 66 | ;W[ak] 67 | ;B[kf] 68 | ;W[kg] 69 | ;B[lg] 70 | ;W[lf] 71 | ;B[ig] 72 | ;W[ih] 73 | ;B[if] 74 | ;W[hg] 75 | ;B[jh] 76 | ;W[kh] 77 | ;B[ji] 78 | ;W[ki] 79 | ;B[jj] 80 | ;W[ik] 81 | ;B[kj] 82 | ;W[mh] 83 | ;B[le] 84 | ;W[mf] 85 | ;B[mi] 86 | ;W[mg] 87 | ;B[li] 88 | ;W[lh] 89 | ;B[hj] 90 | ;W[ij] 91 | ;B[ii] 92 | ;W[hi] 93 | ;B[hk] 94 | ;W[hl] 95 | ;B[gk] 96 | ;W[fl] 97 | ;B[gl] 98 | ;W[lk] 99 | ;B[il] 100 | ;W[jk] 101 | ;B[kk] 102 | ;W[jl] 103 | ;B[kl] 104 | ;W[jm] 105 | ;B[gm] 106 | ;W[km] 107 | ;B[ll] 108 | ;W[ld] 109 | ;B[me] 110 | ;W[mc] 111 | ;B[ne] 112 | ;W[kd] 113 | ;B[ke] 114 | ;W[ie] 115 | ;B[je] 116 | ) 117 | -------------------------------------------------------------------------------- /sgf_examples/go4go-2.sgf: -------------------------------------------------------------------------------- 1 | (;DT[2020-09-01]EV[22nd Chinese League A]RO[round 8] 2 | PB[Shin Jinseo]BR[9p]PW[Chen Yichun]WR[4p] 3 | KM[7.5]RE[B+R]SO[Go4Go.net] 4 | ;B[qd];W[dd];B[pp];W[dp];B[cc];W[dc];B[cd];W[ce];B[be];W[bf];B[cf];W[de];B[bg];W[bd];B[af];W[bc];B[fq];W[cn];B[od];W[nq];B[qn];W[eq];B[fp];W[jq];B[lq];W[jo];B[mp];W[gn];B[en];W[cl];B[hq];W[ho];B[dm];W[dn];B[em];W[bk];B[ip];W[jp];B[eo];W[dr];B[hm];W[io];B[gm];W[jm];B[dk];W[jk];B[df];W[pq];B[qq];W[qr];B[qp];W[np];B[no];W[mo];B[lo];W[mn];B[ln];W[oo];B[nn];W[mm];B[lm];W[on];B[kl];W[nm];B[jl];W[rr];B[qk];W[gr];B[fr];W[hp];B[kr];W[iq];B[hr];W[ir];B[gs];W[im];B[il];W[km];B[ml];W[hn];B[nl];W[ne];B[nd];W[me];B[lc];W[qe];B[oe];W[rd];B[qc];W[qg];B[rf];W[qj];B[rj];W[qi];B[pk];W[ri];B[re];W[rk];B[rl];W[sj];B[nh];W[ef];B[eg];W[ff];B[fg];W[gf];B[gg];W[hf];B[hg];W[if];B[ic];W[kd];B[kc];W[mq];B[pm];W[rg];B[fb];W[eb];B[jg];W[cj];B[dj];W[mh];B[kh];W[ni];B[oi];W[ng];B[oh];W[li];B[og];W[kf];B[fd];W[hd];B[hc];W[gd];B[gc];W[lk];B[ll];W[md];B[mc];W[ki];B[ji];W[kk];B[sl];W[qf];B[rc];W[sk];B[ld];W[le];B[jd];W[ke];B[lp];W[ik];B[mj];W[lh];B[nf];W[mf];B[mg];W[lg];B[ci];W[bi];B[bh];W[hj];B[er];W[cq];B[rq];W[mr];B[pr];W[oq];B[sr];W[or];B[ep];W[dq];B[gi];W[gj];B[fj];W[hi];B[hh];W[fi];B[ck];W[bj];B[cm];W[bm];B[dl];W[bl];B[gh];W[fk];B[ej];W[sf];B[sd];W[lr];B[kq];W[ad];B[id];W[ie];B[ge];W[he];B[ec];W[db];B[fe];W[ng];B[do];W[co];B[mg];W[mk];B[nj];W[ng];B[of];W[nk];B[ok];W[mi];B[fl];W[gk];B[jj];W[ee];B[ah];W[om];B[ol];W[ae];B[bf];W[ks];B[js];W[ls];B[jr];W[gp];B[gq];W[es];B[ai];W[aj];B[kn];W[jn];B[ea];W[da];B[fa];W[pe];B[ph];W[pd];B[pc];W[pn];B[qo];W[qh];B[rs];W[ps];B[fn];W[gl];B[hl];W[ek];B[el];W[op];B[se];W[sg];B[is];W[kg];B[jh]) 5 | -------------------------------------------------------------------------------- /sgf_examples/kgs-H2.sgf: -------------------------------------------------------------------------------- 1 | (;GM[1]FF[4]CA[UTF-8]AP[CGoban:3]ST[2] 2 | RU[Japanese]SZ[19]HA[2]KM[0.50]TM[600]OT[5x20 byo-yomi] 3 | PW[Hasselhoff]PB[legon]WR[1d]BR[2k]DT[2025-10-24]PC[The KGS Go Server at http://www.gokgs.com/]AB[pd][dp]C[Hasselhoff [1d\]: hi 4 | Hasselhoff [1d\]: gg 5 | ] 6 | ;W[pp]WL[598.087] 7 | ;B[dd]BL[598.621] 8 | ;W[fq]WL[595.899] 9 | ;B[eq]BL[594.512] 10 | ;W[fp]WL[594.286] 11 | ;B[qn]BL[589.684] 12 | ;W[pn]WL[585.644] 13 | ;B[po]BL[587.305] 14 | ;W[oo]WL[574.485] 15 | ;B[qo]BL[573.556] 16 | ;W[qp]WL[562.545] 17 | ;B[op]BL[560.424] 18 | ;W[pm]WL[555.716] 19 | ;B[qm]BL[558.1] 20 | ;W[rp]WL[525.009] 21 | ;B[pl]BL[552.183] 22 | ;W[no]WL[496.465] 23 | ;B[ol]BL[543.158] 24 | ;W[nm]WL[492.532] 25 | ;B[oq]BL[541.45] 26 | ;W[pr]WL[478.316] 27 | ;B[or]BL[539.098] 28 | ;W[mq]WL[455.423] 29 | ;B[mr]BL[530.801] 30 | ;W[lq]WL[442.499] 31 | ;B[lr]BL[525.78] 32 | ;W[ql]WL[441.033] 33 | ;B[rl]BL[521.04] 34 | ;W[qk]WL[439.115] 35 | ;B[rk]BL[520.371] 36 | ;W[qj]WL[436.691] 37 | ;B[rj]BL[519.13] 38 | ;W[ro]WL[431.742] 39 | ;B[rm]BL[516.296] 40 | ;W[qi]WL[423.058] 41 | ;B[ri]BL[513.346] 42 | ;W[qh]WL[404.759] 43 | ;B[rg]BL[509.087]C[legon [2k\]: gg 44 | ] 45 | ;W[kq]WL[344.352] 46 | ;B[kr]BL[505.528] 47 | ;W[pq]WL[326.962] 48 | ;B[jq]BL[500.942] 49 | ;W[nl]WL[324.299] 50 | ;B[oj]BL[492.548] 51 | ;W[nk]WL[290.886] 52 | ;B[pj]BL[486.059] 53 | ;W[ni]WL[278.211] 54 | ;B[ph]BL[476.021] 55 | ;W[qg]WL[271.231] 56 | ;B[rf]BL[471.055] 57 | ;W[rh]WL[262.684] 58 | ;B[sh]BL[417.898] 59 | ;W[og]WL[219.947] 60 | ;B[oh]BL[414.043] 61 | ;W[qe]WL[118.259] 62 | ;B[qf]BL[357.003] 63 | ;W[pg]WL[115.255] 64 | ;B[nh]BL[348.058] 65 | ;W[ng]WL[91.633] 66 | ;B[mh]BL[341.345] 67 | ;W[pe]WL[60.442] 68 | ;B[re]BL[330.928] 69 | ;W[mg]WL[20]OW[5]) 70 | -------------------------------------------------------------------------------- /katago_gui/templates/reset_request.tmpl: -------------------------------------------------------------------------------- 1 | {% extends "layout.tmpl" %} 2 | 3 | {% block css %} 4 | 5 | {% endblock %} 6 | 7 | {% block content %} 8 |
9 | 10 | 23 | 46 |
11 | {% with messages = get_flashed_messages( with_categories=true) %} 12 | {% if messages %} 13 | {% for category, message in messages %} 14 |
15 | {{ message }} 16 |
17 | {% endfor %} 18 | {% else %} 19 |

20 | {% endif %} 21 | {% endwith %} 22 |
24 |
25 | {{ form.hidden_tag() }} 26 |
27 |
28 | {{ form.email.label(class="form-control-label") }} 29 | {% if form.email.errors %} 30 | {{ form.email(class="form-control form-control-lg is-invalid") }} 31 |
32 | {% for error in form.email.errors %} 33 | {{ error }} 34 | {% endfor %} 35 |
36 | {% else %} 37 | {{ form.email(class="form-control form-control-lg") }} 38 | {% endif %} 39 |
40 |
41 |
42 | {{ form.submit(class="btn btn-outline-info") }} 43 |
44 |
45 |
47 |
48 | {% endblock content %} 49 | -------------------------------------------------------------------------------- /katago_gui/static/main.css: -------------------------------------------------------------------------------- 1 | body { 2 | /*background:#cccccc;*/ 3 | /*background:#e8e8e8;*/ 4 | /* background:#faf0e6; */ /* linen */ 5 | background: #f8f8f8; 6 | color:#000000; 7 | font-family: Helvetica Neue, Helvetica, Arial, sans-serif; 8 | text-align: center; 9 | margin-top: 5rem; 10 | } 11 | .center { 12 | margin-left:auto; 13 | margin-right:auto; 14 | } 15 | .text-center { 16 | text-align:center; 17 | } 18 | 19 | .hlink { 20 | text-decoration: none; 21 | color: white; 22 | } 23 | 24 | .hlink:visited { 25 | text-decoration: none; 26 | color: white; 27 | } 28 | 29 | .hlink:hover { 30 | text-decoration: none; 31 | color: #cbd5db; 32 | } 33 | 34 | .hlink:active { 35 | text-decoration: none; 36 | color: #cbd5db; 37 | } 38 | 39 | .glink { 40 | text-decoration: none; 41 | color: green; 42 | } 43 | 44 | .glink:visited { 45 | text-decoration: none; 46 | color: green; 47 | } 48 | 49 | .glink:hover { 50 | text-decoration: none; 51 | color: #444444; 52 | } 53 | 54 | .glink:active { 55 | text-decoration: none; 56 | color: #444444; 57 | } 58 | 59 | .ahx-overlay { 60 | position: fixed; 61 | user-select: auto; 62 | /*left: 0;*/ 63 | left: 50%; /* anchor at the horizontal center */ 64 | transform: translateX(-50%); 65 | top: 5%; 66 | z-index: 20; 67 | padding-left: 10px; 68 | padding-right: 10px; 69 | max-height: 80vh; 70 | overflow-y: auto; 71 | } 72 | 73 | .red-border { border-style:solid; border-color:red;} 74 | .green-border { border-style:solid; border-color:green; } 75 | .blue-border { border-style:solid; border-color:blue; } 76 | .yellow-border { border-style:solid; border-color:yellow; } 77 | .magenta-border { border-style:solid; border-color:magenta; } 78 | .cyan-border { border-style:solid; border-color:cyan; } 79 | -------------------------------------------------------------------------------- /sgf_examples/fox-even.sgf: -------------------------------------------------------------------------------- 1 | (;GM[1]FF[4] 2 | SZ[19] 3 | GN[] 4 | DT[2024-07-17] 5 | PB[gaffel1] 6 | PW[abiaode] 7 | BR[6段] 8 | WR[6段] 9 | KM[375]HA[0]RU[Chinese]AP[GNU Go:3.8]RN[1]RE[B+0.75]TM[300]TC[3]TT[30]AP[foxwq]RL[0] 10 | ;B[pd];W[dp];B[pq];W[dd];B[cc];W[cd];B[dc];W[ed];B[ec];W[fc];B[fb];W[bc];B[bb];W[gb];B[fd];W[gc];B[bd];W[be];B[ac];W[df];B[cq];W[cp];B[dq];W[fq];B[eq];W[ep];B[fr];W[gq];B[gr];W[hq];B[qn];W[pp];B[qp];W[oq];B[po];W[op];B[pr];W[oo];B[on];W[nn];B[om];W[nm];B[ol];W[lp];B[nc];W[qd];B[qc];W[qe];B[rc];W[pe];B[od];W[qi];B[nf];W[nl];B[ok];W[mj];B[ck];W[ci];B[di];W[dj];B[cj];W[ei];B[dh];W[ch];B[bi];W[bh];B[ej];W[dk];B[dl];W[ek];B[fj];W[cl];B[eh];W[fk];B[gk];W[gj];B[fi];W[hk];B[gl];W[bk];B[hj];W[gi];B[gh];W[hi];B[ij];W[hh];B[gg];W[ji];B[jj];W[ki];B[kj];W[hg];B[hf];W[gf];B[ii];W[fg];B[fh];W[ig];B[ff];W[ge];B[ef];W[kg];B[jm];W[hm];B[hl];W[kl];B[im];W[jl];B[il];W[hn];B[jo];W[jq];B[kp];W[kq];B[lo];W[mo];B[km];W[ll];B[mp];W[lq];B[fl];W[bj];B[gn];W[ho];B[ln];W[jp];B[ko];W[rk];B[gd];W[hd];B[fe];W[he];B[hc];W[dg];B[eg];W[jd];B[hb];W[bq];B[br];W[cr];B[cs];W[ar];B[bs];W[lc];B[og];W[qg];B[bp];W[bo];B[aq];W[or];B[qq];W[rm];B[rn];W[mb];B[jc];W[kc];B[jb];W[md];B[oe];W[ob];B[nb];W[na];B[lf];W[ke];B[pb];W[kf];B[rd];W[re];B[os];W[ns];B[ps];W[mr];B[bn];W[co];B[ao];W[bm];B[cn];W[dn];B[rh];W[qh];B[se];W[sf];B[sd];W[rg];B[qm];W[rl];B[hr];W[ir];B[ae];W[bf];B[sm];W[cm];B[oc];W[oa];B[pa];W[ma];B[mc];W[lb];B[jh];W[kh];B[en];W[gm];B[fm];W[go];B[me];W[fn];B[dm];W[an];B[jg];W[if];B[ih];W[ap];B[bq];W[eo];B[jf];W[je];B[le];W[ld];B[id];W[mg];B[mh];W[nh];B[mi];W[ni];B[lj];W[li];B[mk];W[nj];B[ml];W[mm];B[lm];W[nk];B[lk];W[em];B[el];W[sl];B[sn];W[ic];B[mn];W[no];B[ql];W[qk];B[hs];W[ka];B[ja];W[kb];B[is];W[ie];B[ib];W[js];B[io];W[ip];B[pk];W[af];B[ad];W[pj];B[lg];W[lh];B[de];W[ce];B[ee];W[in];B[jn];W[pf];B[ng];W[oh];B[pg];W[ph];B[oj];W[oi];B[rb];W[en];B[mf];W[mh];B[ao];W[ne];B[of];W[nd];B[pc];W[id];B[ap];W[er];B[dr]) -------------------------------------------------------------------------------- /katago_gui/static/small/board.js: -------------------------------------------------------------------------------- 1 | // Import or create JGO namespace 2 | var JGO = JGO || {}; 3 | 4 | JGO.BOARD = JGO.BOARD || {}; 5 | 6 | JGO.BOARD.large = { 7 | textures: { 8 | black: 'static/small/black.png', 9 | white: 'static/small/white.png', 10 | shadow:'static/small/shadow.png', 11 | board: 'static/small/shinkaya.jpg' 12 | }, 13 | 14 | // Margins around the board, both on normal edges and clipped ones 15 | //margin: {normal: 20, clipped: 20}, 16 | margin: {normal: 0, clipped: 0}, 17 | 18 | // Shadow color, blur and offset 19 | boardShadow: {color: '#ffe0a8', blur: 30, offX: 5, offY: 5}, 20 | 21 | // Lighter border around the board makes it more photorealistic 22 | border: {color: 'rgba(255, 255, 255, 0.3)', lineWidth: 2}, 23 | 24 | // Amount of "extra wood" around the grid where stones lie 25 | padding: {normal: 10, clipped: 5}, 26 | 27 | // Grid color and size, line widths 28 | grid: {color: '#202020', x: 25, y: 25, smooth: 0.0, 29 | borderWidth: 0.75, lineWidth: 0.6}, 30 | 31 | // Star point radius 32 | stars: {radius: 3}, 33 | 34 | // Coordinate color and font 35 | //coordinates: {color: '#808080', font: 'normal 9px sanf-serif'}, 36 | coordinates: {top:false, bottom:false, left:false, right:false}, 37 | 38 | // Stone radius and alpha for semi-transparent stones 39 | stone: {radius: 12, dimAlpha:0.6}, 40 | 41 | // Shadow offset from center 42 | shadow: {xOff: -2, yOff: 2}, 43 | 44 | // Mark base size and line width, color and font settings 45 | mark: {lineWidth: 1.5, blackColor: 'white', whiteColor: 'black', 46 | clearColor: 'black', font: 'normal 12px sans-serif'} 47 | }; 48 | 49 | JGO.BOARD.largeWalnut = JGO.util.extend(JGO.util.extend({}, JGO.BOARD.large), { 50 | textures: {board: 'static/small/mono.jpg', shadow: 'static/small/shadow_dark.png'}, 51 | boardShadow: {color: '#e2baa0'}, 52 | grid: {color: '#101010', borderWidth: 1.8, lineWidth: 1.5} 53 | }); 54 | 55 | JGO.BOARD.largeBW = JGO.util.extend(JGO.util.extend({}, JGO.BOARD.large), { 56 | textures: false 57 | }); 58 | -------------------------------------------------------------------------------- /katago_gui/dbmodel.py: -------------------------------------------------------------------------------- 1 | # /******************************************************************** 2 | # Filename: katago_gui/dbmodel.py 3 | # Author: AHN 4 | # Creation Date: Aug, 2020 5 | # **********************************************************************/ 6 | # 7 | # Wrapper classes for the DB tables 8 | # 9 | 10 | import json 11 | import uuid 12 | from katago_gui import db, app 13 | from pdb import set_trace as BP 14 | 15 | class Game: 16 | """ A game record as defined in create_tables.py """ 17 | def __init__( self, game_hash=None): 18 | self.valid = True 19 | self.data = {} 20 | #if not game_hash: app.logger.info('>>>>>>>>>> Game without game_hash') 21 | self.id = game_hash if game_hash else uuid.uuid4().hex[:16] 22 | rows = db.find( 't_game', 'game_hash', self.id) 23 | if not rows: 24 | self.valid = False 25 | return 26 | self.data = rows[0] 27 | if self.data['game_record']: # Convert json string to a python object 28 | self.data['game_record'] = json.loads( self.data['game_record']) 29 | 30 | def update_db( self, data): 31 | """ Create or update game in DB """ 32 | self.data.update( data) 33 | self.data['game_hash'] = self.id 34 | rows = db.find( 't_game', 'game_hash', self.id) 35 | if not rows: 36 | db.insert( 't_game', (self.data,)) 37 | db.tstamp( 't_game', 'game_hash', self.id, 'ts_started') 38 | self.valid = True 39 | return 'inserted' 40 | db.update_row( 't_game', 'game_hash', self.id, self.data) 41 | db.tstamp( 't_game', 'game_hash', self.id, 'ts_latest_move') 42 | if self.data['game_record']: # Convert json string to a python object 43 | self.data['game_record'] = json.loads( self.data['game_record']) 44 | self.valid = True 45 | return 'updated' 46 | 47 | def read_db( self): 48 | """ Read our data from the db """ 49 | data = db.find( 't_game', 'game_hash', self.id)[0] 50 | self.data.update( data) 51 | -------------------------------------------------------------------------------- /katago_gui/static/large/board.js: -------------------------------------------------------------------------------- 1 | // Import or create JGO namespace 2 | var JGO = JGO || {}; 3 | 4 | JGO.BOARD = JGO.BOARD || {}; 5 | 6 | JGO.BOARD.large = { 7 | textures: { 8 | black: 'static/large/black43.png', 9 | white: 'static/large/white43.png', 10 | shadow:'static/large/shadow.png', 11 | //board: 'static/large/shinkaya.jpg' 12 | }, 13 | 14 | // Margins around the board, both on normal edges and clipped ones 15 | //margin: {normal: 40, clipped: 40}, 16 | margin: {normal: 0, clipped: 0, color: '#cba45e'}, // board color 17 | 18 | // Shadow color, blur and offset 19 | boardShadow: {color: '#ffe0a8', blur: 30, offX: 5, offY: 5}, 20 | 21 | // Lighter border around the board makes it more photorealistic 22 | border: {color: 'rgba(255, 255, 255, 0.3)', lineWidth: 2}, 23 | 24 | // Amount of "extra wood" around the grid where stones lie 25 | padding: {normal: 20, clipped: 10}, 26 | 27 | // Grid color and size, line widths 28 | grid: {color: '#202020', x: 45, y: 45, smooth: 0.0, 29 | //grid: {color: '#202020', x: 50, y: 50, smooth: 0.0, 30 | borderWidth: 1.5, lineWidth: 1.2}, 31 | 32 | // Star point radius 33 | stars: {radius: 4}, 34 | 35 | // Coordinate color and font 36 | //coordinates: {color: '#808080', font: 'normal 18px sanf-serif'}, 37 | coordinates: {top:false, bottom:false, left:false, right:false}, 38 | 39 | 40 | // Stone radius and alpha for semi-transparent stones 41 | stone: {radius: 22, dimAlpha:0.6}, 42 | 43 | // Shadow offset from center 44 | shadow: {xOff: -2, yOff: 2}, 45 | 46 | // Mark base size and line width, color and font settings 47 | mark: {lineWidth: 1.5, blackColor: 'white', whiteColor: 'black', 48 | clearColor: 'black', font: 'normal 30px sans-serif'} 49 | }; 50 | 51 | JGO.BOARD.largeWalnut = JGO.util.extend(JGO.util.extend({}, JGO.BOARD.large), { 52 | textures: { /* board: 'static/large/mono.jpg', */ shadow: 'static/large/shadow_dark43.png' }, 53 | boardShadow: {color: '#e2baa0'}, 54 | grid: {color: '#101010', borderWidth: 1.8, lineWidth: 1.5} 55 | }); 56 | 57 | JGO.BOARD.largeBW = JGO.util.extend(JGO.util.extend({}, JGO.BOARD.large), { 58 | textures: false 59 | }); 60 | -------------------------------------------------------------------------------- /katago_gui/templates/reset_token.tmpl: -------------------------------------------------------------------------------- 1 | {% extends "layout.tmpl" %} 2 | 3 | {% block css %} 4 | 5 | {% endblock %} 6 | 7 | {% block content %} 8 |
9 | 10 | 23 | 60 |
11 | {% with messages = get_flashed_messages( with_categories=true) %} 12 | {% if messages %} 13 | {% for category, message in messages %} 14 |
15 | {{ message }} 16 |
17 | {% endfor %} 18 | {% else %} 19 |

20 | {% endif %} 21 | {% endwith %} 22 |
24 |
25 | {{ form.hidden_tag() }} 26 |
27 | {{tr('Reset Password')}} 28 |
29 | {{ form.password.label(class="form-control-label") }} 30 | {% if form.password.errors %} 31 | {{ form.password(class="form-control form-control-lg is-invalid") }} 32 |
33 | {% for error in form.password.errors %} 34 | {{ error }} 35 | {% endfor %} 36 |
37 | {% else %} 38 | {{ form.password(class="form-control form-control-lg") }} 39 | {% endif %} 40 |
41 |
42 | {{ form.confirm_password.label(class="form-control-label") }} 43 | {% if form.confirm_password.errors %} 44 | {{ form.confirm_password(class="form-control form-control-lg is-invalid") }} 45 |
46 | {% for error in form.confirm_password.errors %} 47 | {{ error }} 48 | {% endfor %} 49 |
50 | {% else %} 51 | {{ form.confirm_password(class="form-control form-control-lg") }} 52 | {% endif %} 53 |
54 |
55 |
56 | {{ form.submit(class="btn btn-outline-info") }} 57 |
58 |
59 |
61 |
62 | {% endblock content %} 63 | -------------------------------------------------------------------------------- /katago_gui/templates/find_game.tmpl: -------------------------------------------------------------------------------- 1 | {% extends 'layout_watch.tmpl' %} 2 | 3 | {% block css %} 4 | 5 | {% endblock %} 6 | 7 | {% block content %} 8 |
9 |
10 |

{{ tr('Find a Game Played on KataGui') }}

11 |
12 | {% if action == 'choose_file' %} 13 |
14 | 17 |
18 | {% else %} 19 | 20 | {% if games %} 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | {% for game in games %} 29 | 30 | 31 | 32 | 33 | 34 | 35 | {% endfor %} 36 |
{{tr('User')}}{{tr('Started')}}{{tr('Ended')}}{{tr('Link')}}
{{ game.username }} {{ game.ts_started }} {{ game.ts_latest_move }} {{tr('Observe')}}
37 | {% else %} 38 | Game Not Found 39 | {% endif %} 40 |

41 | {{tr('Try Again')}} 42 | {% endif %} 43 |
44 | {% endblock content %} 45 | 46 | {% block js %} 47 | 72 | {% endblock js %} 73 | -------------------------------------------------------------------------------- /katago_gui/templates/login.tmpl: -------------------------------------------------------------------------------- 1 | {% extends 'layout.tmpl' %} 2 | 3 | {% block css %} 4 | 5 | {% endblock %} 6 | 7 | {% block content %} 8 |
9 | 10 | 23 | 68 |
11 | {% with messages = get_flashed_messages( with_categories=true) %} 12 | {% if messages %} 13 | {% for category, message in messages %} 14 |
15 | {{ message }} 16 |
17 | {% endfor %} 18 | {% else %} 19 |

20 | {% endif %} 21 | {% endwith %} 22 |
24 |
25 | {{ form.hidden_tag() }} 26 |
27 |
28 | {{ form.email.label( class='form-control-label') }} 29 | {% if form.email.errors %} 30 | {{ form.email( class='form-control form-control-lg is-invalid') }} 31 |
32 | {% for error in form.email.errors %} 33 | {{ error }} 34 | {% endfor %} 35 |
36 | {% else %} 37 | {{ form.email( class='form-control form-control-lg') }} 38 | {% endif %} 39 |
40 |
41 | {{ form.password.label( class='form-control-label') }} 42 | {% if form.password.errors %} 43 | {{ form.password( class='form-control form-control-lg is-invalid') }} 44 |
45 | {% for error in form.password.errors %} 46 | {{ error }} 47 | {% endfor %} 48 |
49 | {% else %} 50 | {{ form.password( class='form-control form-control-lg') }} 51 | {% endif %} 52 |
53 |
54 | {{ form.remember() }} {{ form.remember.label() }} 55 |
56 |
57 |
58 | {{ form.submit( class='btn btn-outline-info') }} 59 |

60 |
63 |
64 | {{tr('Need An Account?')}} {{tr('Sign Up Now.')}} 65 |
66 |
67 |
69 |
70 | {% endblock content %} 71 | -------------------------------------------------------------------------------- /katago_gui/static/reconnecting-websocket.min.js: -------------------------------------------------------------------------------- 1 | !function(a,b){"function"==typeof define&&define.amd?define([],b):"undefined"!=typeof module&&module.exports?module.exports=b():a.ReconnectingWebSocket=b()}(this,function(){function a(b,c,d){function l(a,b){var c=document.createEvent("CustomEvent");return c.initCustomEvent(a,!1,!1,b),c}var e={debug:!1,automaticOpen:!0,reconnectInterval:1e3,maxReconnectInterval:3e4,reconnectDecay:1.5,timeoutInterval:2e3};d||(d={});for(var f in e)this[f]="undefined"!=typeof d[f]?d[f]:e[f];this.url=b,this.reconnectAttempts=0,this.readyState=WebSocket.CONNECTING,this.protocol=null;var h,g=this,i=!1,j=!1,k=document.createElement("div");k.addEventListener("open",function(a){g.onopen(a)}),k.addEventListener("close",function(a){g.onclose(a)}),k.addEventListener("connecting",function(a){g.onconnecting(a)}),k.addEventListener("message",function(a){g.onmessage(a)}),k.addEventListener("error",function(a){g.onerror(a)}),this.addEventListener=k.addEventListener.bind(k),this.removeEventListener=k.removeEventListener.bind(k),this.dispatchEvent=k.dispatchEvent.bind(k),this.open=function(b){h=new WebSocket(g.url,c||[]),b||k.dispatchEvent(l("connecting")),(g.debug||a.debugAll)&&console.debug("ReconnectingWebSocket","attempt-connect",g.url);var d=h,e=setTimeout(function(){(g.debug||a.debugAll)&&console.debug("ReconnectingWebSocket","connection-timeout",g.url),j=!0,d.close(),j=!1},g.timeoutInterval);h.onopen=function(){clearTimeout(e),(g.debug||a.debugAll)&&console.debug("ReconnectingWebSocket","onopen",g.url),g.protocol=h.protocol,g.readyState=WebSocket.OPEN,g.reconnectAttempts=0;var d=l("open");d.isReconnect=b,b=!1,k.dispatchEvent(d)},h.onclose=function(c){if(clearTimeout(e),h=null,i)g.readyState=WebSocket.CLOSED,k.dispatchEvent(l("close"));else{g.readyState=WebSocket.CONNECTING;var d=l("connecting");d.code=c.code,d.reason=c.reason,d.wasClean=c.wasClean,k.dispatchEvent(d),b||j||((g.debug||a.debugAll)&&console.debug("ReconnectingWebSocket","onclose",g.url),k.dispatchEvent(l("close")));var e=g.reconnectInterval*Math.pow(g.reconnectDecay,g.reconnectAttempts);setTimeout(function(){g.reconnectAttempts++,g.open(!0)},e>g.maxReconnectInterval?g.maxReconnectInterval:e)}},h.onmessage=function(b){(g.debug||a.debugAll)&&console.debug("ReconnectingWebSocket","onmessage",g.url,b.data);var c=l("message");c.data=b.data,k.dispatchEvent(c)},h.onerror=function(b){(g.debug||a.debugAll)&&console.debug("ReconnectingWebSocket","onerror",g.url,b),k.dispatchEvent(l("error"))}},1==this.automaticOpen&&this.open(!1),this.send=function(b){if(h)return(g.debug||a.debugAll)&&console.debug("ReconnectingWebSocket","send",g.url,b),h.send(b);throw"INVALID_STATE_ERR : Pausing to reconnect websocket"},this.close=function(a,b){"undefined"==typeof a&&(a=1e3),i=!0,h&&h.close(a,b)},this.refresh=function(){h&&h.close()}}return a.prototype.onopen=function(){},a.prototype.onclose=function(){},a.prototype.onconnecting=function(){},a.prototype.onmessage=function(){},a.prototype.onerror=function(){},a.debugAll=!1,a.CONNECTING=WebSocket.CONNECTING,a.OPEN=WebSocket.OPEN,a.CLOSING=WebSocket.CLOSING,a.CLOSED=WebSocket.CLOSED,a}); 2 | -------------------------------------------------------------------------------- /fill_zobrist.py: -------------------------------------------------------------------------------- 1 | # /******************************************************************** 2 | # Filename: katago-gui/katago_gui/fill_zobrist.py 3 | # Author: AHN 4 | # Creation Date: Oct, 2020 5 | # **********************************************************************/ 6 | # 7 | # Fill zobrist column for all games older than 5 minutes. 8 | # 9 | 10 | from pdb import set_trace as BP 11 | 12 | import os, sys, re, json 13 | from katago_gui import db, dbmodel 14 | from katago_gui import go_utils 15 | from katago_gui import BOARD_SIZE, ZOBRIST_MOVES 16 | 17 | 18 | #--------------------------- 19 | def usage(printmsg=False): 20 | name = os.path.basename(__file__) 21 | msg = ''' 22 | 23 | Name: 24 | %s: Recomupte the zobrist hash for all games inactive for five minutes or more. 25 | Synopsis: 26 | %s --run 27 | Description: 28 | Recomupte the zobrist hash for all games inactive for five minutes or more. 29 | Zobrist has is the max across all 8 symmetries, and thus invariant. 30 | Examples: 31 | $ time %s --run 32 | -- 33 | ''' % (name,name,name) 34 | if printmsg: 35 | print(msg) 36 | exit(1) 37 | else: 38 | return msg 39 | 40 | #------------ 41 | def main(): 42 | if len(sys.argv) != 2: 43 | usage( True) 44 | 45 | n = 0 46 | total = db.select( 'select count(*) from v_games_no_zobrist')[0]['count'] 47 | 48 | while(1): 49 | rows = db.select( 'select * from v_games_no_zobrist limit 10') 50 | #rows = db.select( "select * from t_game where game_hash = 'a7161d79bb484e77'") 51 | #BP() 52 | if len(rows) == 0: break 53 | n += len(rows) 54 | print( 'Updating zobrist hashes %d/%d' % (n,total)) 55 | for row in rows: 56 | try: 57 | game_hash = row['game_hash'] 58 | game = dbmodel.Game( game_hash) 59 | if not game.data['game_record']: 60 | print( 'No game record for game hash %s; ignoring' % game_hash) 61 | continue 62 | rec = game.data['game_record']['record'] 63 | if not rec: 64 | print( 'No moves for game hash %s; erasing game' % game_hash) 65 | db.update_row( 't_game', 'game_hash', game_hash, {'game_record':''}) 66 | continue 67 | if len( game.data['game_record']['var_record']) > len(rec): 68 | rec = game.data['game_record']['var_record'] 69 | moves = [x['mv'] for x in rec] 70 | rc0s = [] 71 | # Convert to 0 based 0-18 (row,col) pairs 72 | for move in moves: 73 | coord = move 74 | if coord in ('pass','resign'): 75 | rc0s.append( coord) 76 | continue 77 | point = go_utils.point_from_coords( coord) 78 | rc0 = (point.row - 1, point.col - 1) 79 | rc0s.append( rc0) 80 | zobrist = go_utils.game_zobrist( rc0s, ZOBRIST_MOVES) 81 | db.update_row( 't_game', 'game_hash', game_hash, {'zobrist':str(zobrist)}) 82 | db.tstamp( 't_game', 'game_hash', game_hash, 'ts_zobrist') 83 | print( 'updated %s' % game_hash) 84 | except Exception as e: 85 | print( 'Exception updating zobrist for game hash %s; erasing game' % game_hash) 86 | db.update_row( 't_game', 'game_hash', game_hash, {'game_record':''}) 87 | 88 | print( 'Done') 89 | 90 | 91 | if __name__ == '__main__': 92 | main() 93 | -------------------------------------------------------------------------------- /sgf_examples/kifucam-H2.sgf: -------------------------------------------------------------------------------- 1 | (;GM[1] GN[] FF[4] CA[UTF-8] AP[KifuCam] RU[Chinese] PB[Black] PW[White] BS[0]WS[0] SZ[19] PL[B] DT[2025-10-29] KM[0.5] HA[2] GC[intersections:((34,86),(49,86),(64,86),(78,86),(93,85),(106,85),(120,85),(134,85),(149,85),(163,85),(178,85),(191,85),(206,84),(219,84),(235,84),(248,84),(264,84),(279,84),(293,84),(35,101),(50,101),(65,101),(79,100),(93,100),(107,100),(120,100),(135,100),(149,100),(163,100),(178,100),(191,99),(206,99),(219,99),(235,99),(248,99),(263,99),(278,99),(293,98),(36,116),(51,116),(65,115),(79,115),(94,115),(107,115),(121,115),(135,115),(149,115),(163,115),(178,114),(191,114),(206,114),(219,114),(234,114),(248,114),(263,114),(277,114),(292,113),(37,130),(51,130),(66,130),(80,130),(94,130),(107,130),(121,130),(135,129),(149,129),(163,129),(178,129),(191,129),(206,129),(219,129),(234,128),(247,128),(262,128),(277,128),(291,128),(38,145),(52,145),(66,145),(80,144),(95,144),(108,144),(121,144),(136,144),(150,144),(164,144),(178,144),(191,143),(206,143),(219,143),(234,143),(247,143),(262,143),(276,143),(291,143),(39,160),(53,160),(67,160),(81,159),(95,159),(108,159),(122,159),(136,159),(150,159),(164,159),(178,158),(191,158),(206,158),(219,158),(233,158),(247,158),(262,158),(276,158),(290,157),(39,174),(54,174),(67,173),(82,173),(95,173),(109,173),(122,173),(136,173),(150,173),(164,173),(178,172),(191,172),(206,172),(219,172),(233,172),(247,172),(261,172),(275,172),(290,171),(40,187),(54,187),(68,187),(82,187),(96,187),(109,187),(123,187),(136,187),(150,187),(164,187),(178,187),(191,187),(206,187),(219,187),(233,187),(246,187),(261,187),(275,187),(289,187),(41,201),(55,201),(69,201),(83,201),(96,201),(110,200),(123,200),(137,200),(151,200),(164,200),(178,200),(191,200),(206,200),(218,199),(233,199),(246,199),(260,199),(274,199),(289,199),(42,214),(56,214),(69,214),(83,214),(97,214),(110,214),(123,214),(137,214),(151,214),(164,214),(178,214),(191,214),(206,214),(218,214),(232,214),(246,214),(260,214),(274,214),(288,214),(42,227),(56,227),(70,227),(84,227),(97,227),(111,227),(124,227),(137,227),(151,227),(164,227),(178,227),(192,227),(206,227),(218,227),(232,227),(246,227),(259,227),(273,227),(287,227),(43,241),(57,241),(70,241),(84,240),(97,240),(111,240),(124,240),(138,240),(151,240),(164,240),(178,240),(192,240),(205,239),(218,239),(232,239),(245,239),(259,239),(273,239),(287,239),(44,254),(58,254),(71,254),(85,254),(98,254),(112,254),(125,254),(138,254),(152,254),(164,254),(178,254),(192,254),(205,254),(218,254),(231,254),(245,254),(259,254),(272,254),(286,254),(45,267),(59,267),(71,267),(86,267),(98,267),(112,267),(125,267),(138,267),(152,267),(164,267),(178,267),(192,267),(205,267),(218,267),(231,267),(245,267),(258,267),(272,267),(286,267),(46,280),(59,280),(72,280),(86,280),(98,280),(112,280),(125,280),(139,280),(152,280),(164,280),(178,280),(192,280),(205,280),(218,280),(231,280),(245,280),(258,280),(271,280),(285,280),(46,292),(60,292),(72,292),(87,292),(99,292),(113,292),(126,292),(139,292),(152,292),(164,292),(178,292),(192,292),(205,292),(218,292),(231,292),(245,292),(257,292),(271,292),(285,292),(47,305),(61,305),(73,305),(87,305),(99,305),(113,305),(126,305),(139,306),(153,306),(164,306),(178,306),(192,306),(205,306),(218,306),(230,306),(244,307),(257,307),(270,307),(284,307),(48,317),(61,317),(73,317),(88,317),(100,317),(114,317),(126,317),(139,318),(153,318),(164,318),(178,318),(192,318),(205,318),(218,318),(230,318),(244,318),(257,319),(270,319),(284,319),(48,330),(62,330),(74,330),(88,330),(100,330),(114,330),(127,330),(140,330),(153,330),(164,330),(178,330),(192,330),(205,330),(218,330),(230,330),(244,330),(256,330),(270,330),(283,330))#phi:82.25#theta:0.00#]AB[cb]AB[db]AB[eb]AW[bc]AW[cc]AB[dc]AW[ec]AB[fc]AW[dd]AW[ed]AB[pd]AW[cf]AB[cp]AB[dp]AB[ep]AB[pp]AW[cq]AW[dq]AW[eq]AB[fq]AW[fr]) 2 | -------------------------------------------------------------------------------- /katago_gui/templates/layout_watch.tmpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | {% block pre %} 4 | 10 | {% endblock pre %} 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | Study With KataGo 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 42 | 43 | 44 | {% block css %} 45 | {% endblock css %} 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | {% if logged_in() %} 65 | 66 | {% else %} 67 | 68 | {% endif %} 69 | 70 | 71 | 72 | 76 | 77 |
  55 | 56 | KataGui 0.0.0 57 |
{{ current_user.data["username"] }}  {{tr('Guest')}}  
73 | {% block content %} 74 | {% endblock content %} 75 |
78 | 94 | 95 | 96 | {% block js %} 97 | {% endblock js %} 98 | 99 | 100 | 101 | -------------------------------------------------------------------------------- /katago_gui/__init__.py: -------------------------------------------------------------------------------- 1 | # /******************************************************************** 2 | # Filename: katago-gui/katago_gui/__init__.py 3 | # Author: AHN 4 | # Creation Date: Jul, 2020 5 | # **********************************************************************/ 6 | # 7 | # Imports and Globals 8 | # 9 | 10 | from pdb import set_trace as BP 11 | 12 | import os,random 13 | import logging 14 | import redis 15 | import gevent 16 | from urllib.parse import urlparse 17 | 18 | from flask import Flask, session 19 | from flask_bcrypt import Bcrypt 20 | from flask_login import LoginManager, user_logged_out, user_logged_in, current_user 21 | from flask_mail import Mail 22 | from flask_sockets import Sockets 23 | from katago_gui.postgres import Postgres 24 | from katago_gui.translations import translate, donation_blurb 25 | 26 | ZOBRIST_MOVES = 40 27 | BOARD_SIZE = 19 28 | 29 | app = Flask( __name__) 30 | sockets = Sockets(app) 31 | 32 | #---------------- 33 | def logged_in(): 34 | return current_user.is_authenticated and not current_user.data['username'].startswith('guest_') 35 | 36 | #------------- 37 | def rrand(): 38 | return str(random.uniform(0,1)) 39 | 40 | # Make some functions available in the jinja templates. 41 | # Black Magic. 42 | @app.context_processor 43 | def inject_template_funcs(): 44 | return {'tr':translate # {{ tr('Play') }} now puts the result of translate('Play') into the template 45 | ,'donation_blurb':donation_blurb 46 | ,'logged_in':logged_in 47 | ,'rrand':rrand 48 | } 49 | 50 | app.config.update( 51 | DEBUG = True, 52 | SECRET_KEY = 'secret_xxx' 53 | ) 54 | 55 | app.config['MAX_CONTENT_LENGTH'] = 1 * 1024 * 1024 56 | 57 | DEMO_KATAGO_SERVER = 'https://my-katago-server.herokuapp.com' 58 | # 20b 256 playouts 59 | #KATAGO_SERVER = 'http://www.ahaux.com/katago_server/' 60 | # 40b 1024 playouts 61 | #KATAGO_SERVER_X = 'http://www.ahaux.com/katago_server_x/' 62 | # 10b 256 playouts 63 | #KATAGO_SERVER_GUEST = 'http://www.ahaux.com/katago_server_guest/' 64 | 65 | db_url = os.environ['DATABASE_URL'] 66 | db = Postgres( db_url) 67 | 68 | # Make some functions available in the jinja templates. 69 | # Black Magic. 70 | @app.context_processor 71 | def inject_template_funcs(): 72 | return { 73 | 'is_mobile':is_mobile 74 | } 75 | 76 | def is_mobile(): 77 | return session.get('is_mobile', False) 78 | 79 | 80 | bcrypt = Bcrypt( app) # Our password hasher 81 | login_manager = LoginManager( app) 82 | login_manager.login_view = 'login' # The route if you should be logged in but aren't 83 | login_manager.login_message_category = 'info' # Flash category for 'Please log in' message 84 | 85 | app.config['MAIL_SERVER'] = 'mail.hover.com' 86 | app.config['MAIL_PORT'] = 587 87 | app.config['MAIL_USE_TLS'] = False 88 | app.config['MAIL_USERNAME'] = os.environ.get('KATAGUI_EMAIL_USER') 89 | app.config['MAIL_PASSWORD'] = os.environ.get('KATAGUI_EMAIL_PASS') 90 | mail = Mail(app) 91 | 92 | if os.environ.get('FLASK_DEBUG') == '1': 93 | REDIS_URL='redis://localhost:6379' 94 | redis = redis.from_url(REDIS_URL) # when running locally 95 | else: 96 | # Redis and websockets for server push needed to watch games 97 | REDIS_URL = os.environ['REDIS_URL'] 98 | url = urlparse(REDIS_URL) 99 | redis = redis.Redis( host=url.hostname, port=url.port, username=url.username, password=url.password, ssl=True, ssl_cert_reqs=None) 100 | 101 | REDIS_CHAN = 'watch' 102 | 103 | from katago_gui.create_tables import create_tables 104 | create_tables( db) 105 | 106 | from katago_gui import routes 107 | from katago_gui import routes_watch 108 | from katago_gui import routes_api 109 | 110 | -------------------------------------------------------------------------------- /katago_gui/go_utils.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # /******************************************************************** 4 | # Filename: go_utils.py 5 | # Author: AHN 6 | # Creation Date: Mar, 2019 7 | # **********************************************************************/ 8 | # 9 | # Various Go utility funcs 10 | # 11 | 12 | from pdb import set_trace as BP 13 | import katago_gui.gotypes as gotypes 14 | import katago_gui.goboard_fast as goboard 15 | 16 | COLS = 'ABCDEFGHJKLMNOPQRST' 17 | STONE_TO_CHAR = { 18 | None: ' . ', 19 | gotypes.Player.black: ' x ', 20 | gotypes.Player.white: ' o ' 21 | } 22 | BOARD_SIZE = 19 23 | 24 | #------------------------------- 25 | def print_move( player, move): 26 | if move.is_pass: 27 | move_str = 'passes' 28 | elif move.is_resign: 29 | move_str = 'resigns' 30 | else: 31 | move_str = '%s%d' % (COLS[move.point.col - 1], move.point.row) 32 | print( '%s %s' % (player, move_str)) 33 | 34 | #------------------------- 35 | def print_board( board): 36 | for row in range( board.num_rows, 0, -1): 37 | bump = ' ' if row <= 9 else '' 38 | line = [] 39 | for col in range( 1, board.num_cols + 1): 40 | stone = board.get( gotypes.Point( row, col)) 41 | line.append( STONE_TO_CHAR[stone]) 42 | print( '%s%d %s' % (bump, row, ''.join(line))) 43 | print(' ' + ' '.join( COLS[:board.num_cols])) 44 | 45 | # Convert from sgf coords to Point 46 | #----------------------------------- 47 | def point_from_coords(coords): 48 | col = COLS.index(coords[0]) + 1 49 | row = int(coords[1:]) 50 | return gotypes.Point(row=row, col=col) 51 | 52 | # Convert from Point to sgf coords 53 | #---------------------------------- 54 | def coords_from_point(point): 55 | return '%s%d' % ( 56 | COLS[point.col - 1], 57 | point.row 58 | ) 59 | 60 | #------------------------------------------- 61 | def board_transform( move, transform_key): 62 | 'Rotate or mirrir a 0 based (r,c) pair' 63 | if not transform_key: return move 64 | if transform_key == '0': return move 65 | if move == 'pass': return move 66 | if len( transform_key) == 4: 67 | return board_transform( board_transform( move, transform_key[:2]), transform_key[2:]) 68 | (r,c) = move 69 | if transform_key == 'lr': 70 | return (r, 18 - c) 71 | elif transform_key == 'td': 72 | return (18 - r, c) 73 | elif transform_key == 'le': 74 | return (c, 18 - r) 75 | elif transform_key == 'ri': 76 | return (18 - c, r) 77 | else: 78 | print( 'ERROR: unknown transform %s' % transform_key) 79 | exit(1) 80 | 81 | #------------------------------------------------------------------------ 82 | def game_zobrist( moves, zobrist_moves = 40): 83 | 'Get orientation invariant zobrist hash from 0 based (r,c) pairs' 84 | zobrist = 0 85 | for transform_key in ['0','lr','td','ri','le','letd','ritd','lrtd']: 86 | game_state = goboard.GameState.new_game( BOARD_SIZE) 87 | for idx,move in enumerate(moves): 88 | if idx >= zobrist_moves: break 89 | if move == 'pass': 90 | next_move = goboard.Move.pass_turn() 91 | elif move == 'resign': 92 | next_move = goboard.Move.resign() 93 | else: 94 | move = board_transform( move, transform_key) 95 | next_move = goboard.Move.play( gotypes.Point( move[0]+1, move[1]+1)) 96 | game_state = game_state.apply_move( next_move) 97 | #print( game_state.board.zobrist_hash()) 98 | zobrist = max( game_state.board.zobrist_hash(), zobrist) 99 | return zobrist 100 | -------------------------------------------------------------------------------- /katago_gui/auth.py: -------------------------------------------------------------------------------- 1 | # /******************************************************************** 2 | # Filename: katago-gui/katago_gui/auth.py 3 | # Author: AHN 4 | # Creation Date: Aug, 2020 5 | # **********************************************************************/ 6 | # 7 | # Flask login with postgres 8 | # 9 | 10 | from flask_login import UserMixin 11 | from katago_gui import db, login_manager, bcrypt 12 | from pdb import set_trace as BP 13 | 14 | class User(UserMixin): 15 | def __init__( self, email): 16 | self.valid = True 17 | self.id = email.strip().lower() 18 | self.data = {} 19 | rows = db.find( 't_user', 'email', self.id) 20 | if not rows: 21 | self.valid = False 22 | return 23 | self.data = rows[0] 24 | 25 | def createdb( self, data): 26 | """ Create User in DB """ 27 | User.delete_old_guests() 28 | self.data = data 29 | self.data['email'] = self.id 30 | self.data['username'] = self.data['username'].strip() 31 | self.data['password'] = '' 32 | self.data['fname'] = self.data['fname'].strip() 33 | self.data['lname'] = self.data['lname'].strip() 34 | self.data['lang'] = 'eng' 35 | rows = db.find( 't_user', 'username', self.data['username']) 36 | if rows: 37 | return 'err_user_exists' 38 | rows = db.find( 't_user', 'email', self.id) 39 | if rows: 40 | return 'err_user_exists' 41 | db.insert( 't_user', (data,)) 42 | db.tstamp( 't_user', 'email', self.id, 'ts_created') 43 | db.tstamp( 't_user', 'email', self.id, 'ts_last_seen') 44 | self.read_from_db() 45 | self.valid = True 46 | return 'ok' 47 | 48 | def update_db( self): 49 | """ Write our data back to the db """ 50 | db.update_row( 't_user', 'email', self.id, self.data) 51 | 52 | def read_from_db( self): 53 | """ Read our data from the db """ 54 | data = db.find( 't_user', 'email', self.id)[0] 55 | self.data.update( data) 56 | 57 | def record_activity( self): 58 | """ Set ts_last_seen to now() """ 59 | db.tstamp( 't_user', 'email', self.id, 'ts_last_seen') 60 | 61 | def count_selfplay_move( self): 62 | """ Count a selfplay move in the db """ 63 | db.incr( 't_user', 'email', self.id, 'self_move_count') 64 | 65 | def count_move( self): 66 | """ Count a non-selfplay move in the db """ 67 | db.incr( 't_user', 'email', self.id, 'move_count') 68 | 69 | def password_matches( self, password): 70 | """ Check password """ 71 | return bcrypt.check_password_hash( self.data['password'], password) 72 | 73 | def set_password( self, password): 74 | """ Update password """ 75 | hashed_password = bcrypt.generate_password_hash( password).decode('utf-8') 76 | self.data['password'] = hashed_password 77 | db.update_row( 't_user', 'email', self.id, { 'password':hashed_password }) 78 | 79 | def set_email_verified( self): 80 | """ Mark email as verified """ 81 | self.data['email_verified'] = True 82 | db.update_row( 't_user', 'email', self.id, { 'email_verified':True }) 83 | 84 | def email_verified( self): 85 | return self.data.get( 'email_verified', False) 86 | 87 | @classmethod 88 | def delete_old_guests( clazz): 89 | sql = ''' 90 | delete from t_user where username like 'guest_%%' and extract(epoch from now() - ts_last_seen)/3600 > 24 91 | ''' 92 | db.run(sql) 93 | 94 | # flask_login needs this callback 95 | @login_manager.user_loader 96 | def load_user( email): 97 | #print('################# load user') 98 | user = User( email) 99 | if not user.valid: 100 | return None 101 | elif not user.email_verified(): 102 | return None 103 | return user 104 | -------------------------------------------------------------------------------- /katago_gui/templates/account.tmpl: -------------------------------------------------------------------------------- 1 | {% extends 'layout.tmpl' %} 2 | 3 | {% block css %} 4 | 5 | {% endblock %} 6 | 7 | {% block content %} 8 |
9 | 10 | 23 | 86 |
11 | {% with messages = get_flashed_messages( with_categories=true) %} 12 | {% if messages %} 13 | {% for category, message in messages %} 14 |
15 | {{ message }} 16 |
17 | {% endfor %} 18 | {% else %} 19 |

20 | {% endif %} 21 | {% endwith %} 22 |
24 |
25 | {{ form.hidden_tag() }} 26 |
27 | {{tr('Account Info')}} 28 |
29 | {{ form.username.label(class="form-control-label") }} 30 | {% if form.username.errors %} 31 | {{ form.username(class="form-control form-control-lg is-invalid") }} 32 |
33 | {% for error in form.username.errors %} 34 | {{ error }} 35 | {% endfor %} 36 |
37 | {% else %} 38 | {{ form.username(class="form-control form-control-lg", disabled=True) }} 39 | {% endif %} 40 |
41 |
42 | {{ form.email.label(class="form-control-label") }} 43 | {% if form.email.errors %} 44 | {{ form.email(class="form-control form-control-lg is-invalid") }} 45 |
46 | {% for error in form.email.errors %} 47 | {{ error }} 48 | {% endfor %} 49 |
50 | {% else %} 51 | {{ form.email(class="form-control form-control-lg", disabled=True) }} 52 | {% endif %} 53 |
54 |
55 | {{ form.fname.label( class='form-control-label') }} 56 | {% if form.fname.errors %} 57 | {{ form.fname( class='form-control form-control-lg is-invalid') }} 58 |
59 | {% for error in form.fname.errors %} 60 | {{ error }} 61 | {% endfor %} 62 |
63 | {% else %} 64 | {{ form.fname( class='form-control form-control-lg') }} 65 | {% endif %} 66 |
67 |
68 | {{ form.lname.label( class='form-control-label') }} 69 | {% if form.lname.errors %} 70 | {{ form.lname( class='form-control form-control-lg is-invalid') }} 71 |
72 | {% for error in form.lname.errors %} 73 | {{ error }} 74 | {% endfor %} 75 |
76 | {% else %} 77 | {{ form.lname( class='form-control form-control-lg') }} 78 | {% endif %} 79 |
80 |
81 |
82 | {{ form.submit(class="btn btn-outline-info") }} 83 |
84 |
85 |
87 |
88 | {% endblock content %} 89 | -------------------------------------------------------------------------------- /sgf_examples/ogs-even.sgf: -------------------------------------------------------------------------------- 1 | (;FF[4] 2 | CA[UTF-8] 3 | GM[1] 4 | DT[2025-10-22] 5 | PC[OGS: https://online-go.com/game/80524639] 6 | GN[GoGameFan vs. tufei905] 7 | PB[GoGameFan] 8 | PW[tufei905] 9 | BR[2k] 10 | WR[1k] 11 | TM[300]OT[7 fischer] 12 | RE[B+3.5] 13 | SZ[19] 14 | KM[6.5] 15 | RU[Japanese] 16 | ;B[dp] 17 | (;W[qd] 18 | (;B[dd] 19 | (;W[pq] 20 | (;B[od] 21 | (;W[lc] 22 | (;B[pf] 23 | (;W[pc] 24 | (;B[oc] 25 | (;W[qf] 26 | (;B[pg] 27 | (;W[qg] 28 | (;B[ph] 29 | (;W[qi] 30 | (;B[pb] 31 | (;W[qb] 32 | (;B[pd] 33 | (;W[qc] 34 | (;B[jc] 35 | (;W[le] 36 | (;B[gc] 37 | (;W[cc] 38 | (;B[dc] 39 | (;W[cd] 40 | (;B[ce] 41 | (;W[be] 42 | (;B[bf] 43 | (;W[cf] 44 | (;B[de] 45 | (;W[bg] 46 | (;B[bd] 47 | (;W[af] 48 | (;B[bc] 49 | (;W[ic] 50 | (;B[jd] 51 | (;W[kf] 52 | (;B[mc] 53 | (;W[mb] 54 | (;B[md] 55 | (;W[ld] 56 | (;B[nb] 57 | (;W[kb] 58 | (;B[id] 59 | (;W[if] 60 | (;B[df] 61 | (;W[dg] 62 | (;B[eg] 63 | (;W[eh] 64 | (;B[dh] 65 | (;W[cg] 66 | (;B[di] 67 | (;W[fg] 68 | (;B[ef] 69 | (;W[bi] 70 | (;B[cj] 71 | (;W[fi] 72 | (;B[fj] 73 | (;W[gj] 74 | (;B[ei] 75 | (;W[fh] 76 | (;B[fk] 77 | (;W[gk] 78 | (;B[bj] 79 | (;W[fl] 80 | (;B[el] 81 | (;W[em] 82 | (;B[dl] 83 | (;W[dm] 84 | (;B[cm] 85 | (;W[cn] 86 | (;B[fm] 87 | (;W[gl] 88 | (;B[en] 89 | (;W[dn] 90 | (;B[cl] 91 | (;W[eo] 92 | (;B[ep] 93 | (;W[fo] 94 | (;B[fp] 95 | (;W[bn] 96 | (;B[ci] 97 | (;W[bh] 98 | (;B[bm] 99 | (;W[cq] 100 | (;B[go] 101 | (;W[fn] 102 | (;B[cp] 103 | (;W[bp] 104 | (;B[dr] 105 | (;W[gp] 106 | (;B[hp] 107 | (;W[gq] 108 | (;B[gn] 109 | (;W[hq] 110 | (;B[gm] 111 | (;W[do] 112 | (;B[bq] 113 | (;W[ap] 114 | (;B[cr] 115 | (;W[an] 116 | (;B[aq] 117 | (;W[co] 118 | (;B[ip] 119 | (;W[iq] 120 | (;B[jp] 121 | (;W[jq] 122 | (;B[kq] 123 | (;W[kr] 124 | (;B[lq] 125 | (;W[fr] 126 | (;B[dq] 127 | (;W[hm] 128 | (;B[in] 129 | (;W[lr] 130 | (;B[mq] 131 | (;W[qo] 132 | (;B[mr] 133 | (;W[pi] 134 | (;B[ma] 135 | (;W[oh] 136 | (;B[lb] 137 | (;W[mf] 138 | (;B[oi] 139 | (;W[nh] 140 | (;B[oj] 141 | (;W[qk] 142 | (;B[mj] 143 | (;W[li] 144 | (;B[mi] 145 | (;W[mh] 146 | (;B[lj] 147 | (;W[kj] 148 | (;B[kk] 149 | (;W[jj] 150 | (;B[jk] 151 | (;W[ik] 152 | (;B[il] 153 | (;W[hk] 154 | (;B[hl] 155 | (;W[hi] 156 | (;B[om] 157 | (;W[no] 158 | (;B[oo] 159 | (;W[op] 160 | (;B[po] 161 | (;W[nq] 162 | (;B[nn] 163 | (;W[mo] 164 | (;B[ln] 165 | (;W[lo] 166 | (;B[ko] 167 | (;W[mn] 168 | (;B[mm] 169 | (;W[qp] 170 | (;B[qm] 171 | (;W[lm] 172 | (;B[kn] 173 | (;W[ml] 174 | (;B[nm] 175 | (;W[jl] 176 | (;B[im] 177 | (;W[kc] 178 | (;B[jb] 179 | (;W[ja] 180 | (;B[ia] 181 | (;W[ka] 182 | (;B[ib] 183 | (;W[pe] 184 | (;B[oe] 185 | (;W[qe] 186 | (;B[ng] 187 | (;W[nf] 188 | (;B[of] 189 | (;W[mg] 190 | (;B[qa] 191 | (;W[ra] 192 | (;B[pa] 193 | (;W[sb] 194 | (;B[qh] 195 | (;W[rh] 196 | (;B[ls] 197 | (;W[js] 198 | (;B[ps] 199 | (;W[qs] 200 | (;B[or] 201 | (;W[pr] 202 | (;B[os] 203 | (;W[rr] 204 | (;B[qn] 205 | (;W[ge] 206 | (;B[he] 207 | (;W[hf] 208 | (;B[gd] 209 | (;W[fe] 210 | (;B[ff] 211 | (;W[gf] 212 | (;B[ad] 213 | (;W[ae] 214 | (;B[nr] 215 | (;W[pp] 216 | (;B[cb] 217 | (;W[pl] 218 | (;B[pm] 219 | (;W[lk] 220 | (;B[kl] 221 | (;W[ok] 222 | (;B[nk] 223 | (;W[nl] 224 | (;B[ol] 225 | (;W[pj] 226 | (;B[pk] 227 | (;W[br] 228 | (;B[ar] 229 | (;W[ok] 230 | (;B[ag] 231 | (;W[ah] 232 | (;B[pk] 233 | (;W[ek] 234 | (;B[ej] 235 | (;W[ok] 236 | (;B[mk] 237 | (;W[ni] 238 | (;B[pk] 239 | (;W[ql] 240 | (;B[ok] 241 | (;W[rj] 242 | (;B[es] 243 | (;W[fs] 244 | (;B[fd] 245 | (;W[ie] 246 | (;B[hd] 247 | (;W[ms] 248 | (;B[ns] 249 | (;W[rn] 250 | (;B[rm] 251 | (;W[sn] 252 | (;B[sm] 253 | (;W[rp] 254 | (;B[rk] 255 | (;W[rl] 256 | (;B[sk] 257 | (;W[ri] 258 | (;B[sl] 259 | (;W[qj] 260 | (;B[sj] 261 | (;W[rg] 262 | (;B[je] 263 | (;W[jf] 264 | (;B[og] 265 | (;W[ne] 266 | (;B[nd] 267 | (;W[la] 268 | (;B[mb] 269 | (;W[am] 270 | (;B[bk] 271 | (;W[al] 272 | (;B[dk] 273 | (;W[er] 274 | (;B[ds] 275 | (;W[si] 276 | (;B[fq] 277 | (;W[hr] 278 | (;B[ee] 279 | (;W[np] 280 | (;B[ll] 281 | (;W[ke] 282 | (;B[bs] 283 | (;W[ks] 284 | (;B[is] 285 | (;W[jr] 286 | (;B[kp] 287 | (;W[ms] 288 | (;B[bo] 289 | (;W[ao] 290 | (;B[ls] 291 | (;W[on] 292 | (;B[pn] 293 | (;W[ms] 294 | (;B[ro] 295 | (;W[so] 296 | (;B[ls] 297 | (;W[hs] 298 | (;B[aj] 299 | (;W[ms] 300 | (;B[sp] 301 | (;W[sq] 302 | (;B[ls] 303 | (;W[] 304 | (;B[ms] 305 | (;W[] 306 | (;B[] 307 | ))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))) -------------------------------------------------------------------------------- /katago_gui/forms.py: -------------------------------------------------------------------------------- 1 | from flask_wtf import FlaskForm 2 | from wtforms import StringField, PasswordField, BooleanField, SubmitField 3 | from wtforms.fields import EmailField 4 | from wtforms.validators import InputRequired, DataRequired, Length, Email, EqualTo 5 | from katago_gui.translations import translate as tr 6 | 7 | from pdb import set_trace as BP 8 | 9 | class RegistrationForm( FlaskForm): 10 | username = StringField( 'Username', 11 | validators=[DataRequired(), Length(min=2, max=20)]) 12 | email = EmailField( 'Email', validators=[DataRequired(), Email()]) 13 | fname = StringField( 'First Name', validators=[DataRequired()]) 14 | lname = StringField( 'Last Name', validators=[DataRequired()]) 15 | password = PasswordField( 'Password', validators=[DataRequired()]) 16 | confirm_password = PasswordField( 'Confirm Password', validators=[DataRequired(), EqualTo('password')]) 17 | submit = SubmitField('Sign Up') 18 | 19 | @classmethod 20 | def translate( cclass): 21 | ''' Call this from routes.py to translate to current language setting ''' 22 | cclass.username = StringField( tr('Username'), 23 | validators=[DataRequired(), Length(min=2, max=20)]) 24 | cclass.email = EmailField( tr('Email'), validators=[DataRequired(), Email()]) 25 | cclass.fname = StringField( tr('First Name'), validators=[DataRequired()]) 26 | cclass.lname = StringField( tr('Last Name'), validators=[DataRequired()]) 27 | cclass.password = PasswordField( tr('Password'), validators=[DataRequired()]) 28 | cclass.confirm_password = PasswordField( tr('Confirm Password'), validators=[DataRequired(), EqualTo('password')]) 29 | cclass.submit = SubmitField(tr('Sign Up')) 30 | 31 | class LoginForm( FlaskForm): 32 | email = EmailField( 'Email', validators=[DataRequired(), Email()]) 33 | password = PasswordField( 'Password', validators=[DataRequired()]) 34 | remember = BooleanField( 'Remember Me') 35 | submit = SubmitField('Login') 36 | 37 | @classmethod 38 | def translate( cclass): 39 | ''' Call this from routes.py to translate to current language setting ''' 40 | cclass.email = EmailField( tr('Email'), validators=[DataRequired(), Email()]) 41 | cclass.password = PasswordField( tr('Password'), validators=[DataRequired()]) 42 | cclass.remember = BooleanField( tr('Remember Me')) 43 | cclass.submit = SubmitField( tr('Login')) 44 | 45 | class RequestResetForm(FlaskForm): 46 | email = EmailField('Email', 47 | validators=[DataRequired(), Email()]) 48 | submit = SubmitField('Request Password Reset') 49 | 50 | @classmethod 51 | def translate( cclass): 52 | ''' Call this from routes.py to translate to current language setting ''' 53 | cclass.email = EmailField( tr('Email'), validators=[DataRequired(), Email()]) 54 | cclass.submit = SubmitField( tr('Request Password Reset')) 55 | 56 | class ResetPasswordForm(FlaskForm): 57 | password = PasswordField('Password', validators=[DataRequired()]) 58 | confirm_password = PasswordField('Confirm Password', 59 | validators=[DataRequired(), EqualTo('password')]) 60 | submit = SubmitField('Reset Password') 61 | 62 | @classmethod 63 | def translate( cclass, lang='eng'): 64 | ''' Call this from routes.py to translate to current language setting ''' 65 | cclass.password = PasswordField( tr('Password', lang), validators=[DataRequired()]) 66 | cclass.confirm_password = PasswordField( tr('Confirm Password'), 67 | validators=[DataRequired(), EqualTo('password')]) 68 | cclass.submit = SubmitField( tr('Reset Password')) 69 | 70 | class UpdateAccountForm(FlaskForm): 71 | username = StringField('Username') 72 | email = EmailField('Email') 73 | fname = StringField( 'First Name', validators=[DataRequired()]) 74 | lname = StringField( 'Last Name', validators=[DataRequired()]) 75 | submit = SubmitField('Update') 76 | 77 | @classmethod 78 | def translate( cclass): 79 | ''' Call this from routes.py to translate to current language setting ''' 80 | cclass.username = StringField( tr('Username')) 81 | cclass.email = EmailField( tr('Email')) 82 | cclass.fname = StringField( tr('First Name'), validators=[DataRequired()]) 83 | cclass.lname = StringField( tr('Last Name'), validators=[DataRequired()]) 84 | cclass.submit = SubmitField( tr('Save')) 85 | -------------------------------------------------------------------------------- /katago_gui/static/js.cookie.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * JavaScript Cookie v2.2.0 3 | * https://github.com/js-cookie/js-cookie 4 | * 5 | * Copyright 2006, 2015 Klaus Hartl & Fagner Brack 6 | * Released under the MIT license 7 | */ 8 | ;(function (factory) { 9 | var registeredInModuleLoader; 10 | if (typeof define === 'function' && define.amd) { 11 | define(factory); 12 | registeredInModuleLoader = true; 13 | } 14 | if (typeof exports === 'object') { 15 | module.exports = factory(); 16 | registeredInModuleLoader = true; 17 | } 18 | if (!registeredInModuleLoader) { 19 | var OldCookies = window.Cookies; 20 | var api = window.Cookies = factory(); 21 | api.noConflict = function () { 22 | window.Cookies = OldCookies; 23 | return api; 24 | }; 25 | } 26 | }(function () { 27 | function extend () { 28 | var i = 0; 29 | var result = {}; 30 | for (; i < arguments.length; i++) { 31 | var attributes = arguments[ i ]; 32 | for (var key in attributes) { 33 | result[key] = attributes[key]; 34 | } 35 | } 36 | return result; 37 | } 38 | 39 | function decode (s) { 40 | return s.replace(/(%[0-9A-Z]{2})+/g, decodeURIComponent); 41 | } 42 | 43 | function init (converter) { 44 | function api() {} 45 | 46 | function set (key, value, attributes) { 47 | if (typeof document === 'undefined') { 48 | return; 49 | } 50 | 51 | attributes = extend({ 52 | path: '/' 53 | }, api.defaults, attributes); 54 | 55 | if (typeof attributes.expires === 'number') { 56 | attributes.expires = new Date(new Date() * 1 + attributes.expires * 864e+5); 57 | } 58 | 59 | // We're using "expires" because "max-age" is not supported by IE 60 | attributes.expires = attributes.expires ? attributes.expires.toUTCString() : ''; 61 | 62 | try { 63 | var result = JSON.stringify(value); 64 | if (/^[\{\[]/.test(result)) { 65 | value = result; 66 | } 67 | } catch (e) {} 68 | 69 | value = converter.write ? 70 | converter.write(value, key) : 71 | encodeURIComponent(String(value)) 72 | .replace(/%(23|24|26|2B|3A|3C|3E|3D|2F|3F|40|5B|5D|5E|60|7B|7D|7C)/g, decodeURIComponent); 73 | 74 | key = encodeURIComponent(String(key)) 75 | .replace(/%(23|24|26|2B|5E|60|7C)/g, decodeURIComponent) 76 | .replace(/[\(\)]/g, escape); 77 | 78 | var stringifiedAttributes = ''; 79 | for (var attributeName in attributes) { 80 | if (!attributes[attributeName]) { 81 | continue; 82 | } 83 | stringifiedAttributes += '; ' + attributeName; 84 | if (attributes[attributeName] === true) { 85 | continue; 86 | } 87 | 88 | // Considers RFC 6265 section 5.2: 89 | // ... 90 | // 3. If the remaining unparsed-attributes contains a %x3B (";") 91 | // character: 92 | // Consume the characters of the unparsed-attributes up to, 93 | // not including, the first %x3B (";") character. 94 | // ... 95 | stringifiedAttributes += '=' + attributes[attributeName].split(';')[0]; 96 | } 97 | 98 | return (document.cookie = key + '=' + value + stringifiedAttributes); 99 | } 100 | 101 | function get (key, json) { 102 | if (typeof document === 'undefined') { 103 | return; 104 | } 105 | 106 | var jar = {}; 107 | // To prevent the for loop in the first place assign an empty array 108 | // in case there are no cookies at all. 109 | var cookies = document.cookie ? document.cookie.split('; ') : []; 110 | var i = 0; 111 | 112 | for (; i < cookies.length; i++) { 113 | var parts = cookies[i].split('='); 114 | var cookie = parts.slice(1).join('='); 115 | 116 | if (!json && cookie.charAt(0) === '"') { 117 | cookie = cookie.slice(1, -1); 118 | } 119 | 120 | try { 121 | var name = decode(parts[0]); 122 | cookie = (converter.read || converter)(cookie, name) || 123 | decode(cookie); 124 | 125 | if (json) { 126 | try { 127 | cookie = JSON.parse(cookie); 128 | } catch (e) {} 129 | } 130 | 131 | jar[name] = cookie; 132 | 133 | if (key === name) { 134 | break; 135 | } 136 | } catch (e) {} 137 | } 138 | 139 | return key ? jar[key] : jar; 140 | } 141 | 142 | api.set = set; 143 | api.get = function (key) { 144 | return get(key, false /* read as raw */); 145 | }; 146 | api.getJSON = function (key) { 147 | return get(key, true /* read as json */); 148 | }; 149 | api.remove = function (key, attributes) { 150 | set(key, '', extend(attributes, { 151 | expires: -1 152 | })); 153 | }; 154 | 155 | api.defaults = {}; 156 | 157 | api.withConverter = init; 158 | 159 | return api; 160 | } 161 | 162 | return init(function () {}); 163 | })); 164 | -------------------------------------------------------------------------------- /katago_gui/create_tables.py: -------------------------------------------------------------------------------- 1 | 2 | from pdb import set_trace as BP 3 | from katago_gui import bcrypt 4 | 5 | def create_tables(db): 6 | create_t_user(db) 7 | create_t_game(db) 8 | create_v_games_24hours(db) 9 | create_v_games_no_zobrist(db) 10 | create_v_recent_users(db) 11 | create_v_registered(db) 12 | create_v_guests(db) 13 | 14 | def create_t_user(db): 15 | if db.table_exists( 't_user'): return 16 | sql = ''' 17 | create table t_user ( 18 | id bigserial not null primary key 19 | ,username text 20 | ,email text 21 | ,password text 22 | ,fname text 23 | ,lname text 24 | ,ts_created timestamptz 25 | ,ts_last_seen timestamptz 26 | ,email_verified boolean 27 | ,lang text 28 | ,game_hash text 29 | ,move_count integer not null default 0 30 | ,self_move_count integer not null default 0 31 | ,watch_game_hash text 32 | ) ''' 33 | db.run( sql) 34 | 35 | def create_t_game(db): 36 | if db.table_exists( 't_game'): return 37 | sql = ''' 38 | create table t_game ( 39 | game_hash text not null primary key 40 | ,username text 41 | ,handicap integer 42 | ,komi real 43 | ,ts_started timestamptz 44 | ,ts_latest_move timestamptz 45 | ,client_timestamp bigint 46 | ,game_record text 47 | ,zobrist text 48 | ,ts_zobrist timestamptz 49 | ,ip_addr text 50 | ) ''' 51 | db.run( sql) 52 | 53 | def create_v_games_24hours(db): 54 | if db.table_exists( 'v_games_24hours'): return 55 | sql = """ 56 | create view v_games_24hours as 57 | with obs_by_game as ( 58 | select 59 | watch_game_hash, count(*) as n_obs 60 | from 61 | t_user 62 | where 63 | watch_game_hash is not null 64 | group by watch_game_hash 65 | ), 66 | games as ( 67 | select 68 | g.game_hash game_hash, u.game_hash as uhash, g.ts_latest_move, g.username 69 | ,g.handicap, g.komi, g.game_record 70 | ,coalesce( o.n_obs, 0) as n_obs 71 | ,extract( epoch from now() - g.ts_latest_move) as idle_secs 72 | ,case when u.game_hash is not null then 1 else 0 end as active 73 | ,case when extract( epoch from now() - g.ts_latest_move) > 10 * 60 then 1 else 0 end as old 74 | from 75 | t_game g 76 | left outer join t_user u 77 | on g.game_hash = u.game_hash 78 | left outer join obs_by_game o 79 | on g.game_hash = o.watch_game_hash 80 | where 81 | g.username is not null 82 | and g.game_record is not NULL 83 | and not g.game_record = '' 84 | and g.ts_latest_move is not null 85 | and extract( epoch from now() - g.ts_latest_move) < 3600 * 24 86 | ) 87 | select 88 | *, 89 | case when active > 0 and not old > 0 then 1 else 0 end as live 90 | from games 91 | order by idle_secs 92 | """ 93 | db.run( sql) 94 | 95 | def create_v_games_no_zobrist(db): 96 | if db.table_exists( 'v_games_no_zobrist'): return 97 | sql = """ 98 | create view v_games_no_zobrist as 99 | WITH games AS ( 100 | SELECT g.game_hash, 101 | COALESCE(date_part('epoch'::text, (g.ts_zobrist - g.ts_latest_move)), ('-300'::integer)::double precision) AS age 102 | FROM t_game g 103 | WHERE ((g.game_record IS NOT NULL) AND (NOT (g.game_record = ''::text))) 104 | ) 105 | SELECT games.game_hash, 106 | games.age 107 | FROM games 108 | WHERE (games.age <= ('-300'::integer)::double precision) 109 | """ 110 | db.run(sql) 111 | 112 | def create_v_recent_users(db): 113 | if db.table_exists( 'v_recent_users'): return 114 | sql = """ 115 | create view v_recent_users as 116 | select t_user.username, 117 | t_user.email, 118 | (now() - t_user.ts_last_seen) AS t_idle 119 | FROM t_user 120 | ORDER BY (now() - t_user.ts_last_seen) 121 | """ 122 | db.run(sql) 123 | 124 | def create_v_registered(db): 125 | if db.table_exists( 'v_registered'): return 126 | sql = """ 127 | create view v_registered as 128 | SELECT t_user.username, 129 | t_user.email, 130 | t_user.ts_created, 131 | t_user.email_verified, 132 | (now() - t_user.ts_last_seen) AS t_idle 133 | FROM t_user 134 | WHERE (t_user.username !~~ 'guest_%'::text) 135 | ORDER BY (now() - t_user.ts_last_seen) 136 | """ 137 | db.run(sql) 138 | 139 | def create_v_guests(db): 140 | if db.table_exists( 'v_guests'): return 141 | sql = """ 142 | create view v_guests as 143 | SELECT t_user.username, 144 | t_user.email, 145 | t_user.ts_created, 146 | (now() - t_user.ts_last_seen) AS t_idle 147 | FROM t_user 148 | WHERE (t_user.username ~~ 'guest_%'::text) 149 | ORDER BY (now() - t_user.ts_last_seen) 150 | """ 151 | db.run(sql) 152 | -------------------------------------------------------------------------------- /sgf_examples/kifucam-even.sgf: -------------------------------------------------------------------------------- 1 | (;GM[1] GN[] FF[4] CA[UTF-8] AP[KifuCam] RU[Chinese] PB[Black] PW[White] BS[0]WS[0] SZ[19] PL[W] DT[2025-10-24] KM[7.5] HA[0] 2 | GC[intersections:((60,132),(74,132),(88,132),(102,132),(116,132),(131,132),(145,132),(159,133),(172,133),(186,133),(199,133),(213,133),(228,133), 3 | (242,133),(256,133),(269,134),(283,134),(298,134),(311,134),(60,146),(74,146),(88,146),(102,146),(116,146),(131,146),(145,147),(159,147),(172,147), 4 | (186,147),(199,147),(213,147),(228,147),(242,147),(255,148),(269,148),(283,148),(298,148),(311,148),(60,160),(74,160),(88,160),(102,160), 5 | (116,161),(131,161),(145,161),(159,161),(172,161),(186,161),(199,161),(213,161),(228,161),(242,162),(255,162),(269,162),(283,162),(298,162), 6 | (310,162),(60,174),(74,174),(88,174),(102,174),(116,175),(131,175),(144,175),(159,175),(172,175),(186,175),(199,175),(213,175),(228,176),(242,176), 7 | (255,176),(269,176),(283,176),(298,176),(310,176),(60,189),(74,189),(88,189),(102,189),(116,189),(131,189),(144,189),(159,189),(172,190),(186,190),(199,190), 8 | (213,190),(227,190),(241,190),(255,190),(269,190),(283,191),(298,191),(310,191),(59,202),(74,202),(88,203),(102,203),(116,203),(131,203),(144,203),(159,203), 9 | (172,203),(186,203),(199,203),(213,204),(227,204),(241,204),(255,204),(269,204),(283,204),(297,204),(310,204),(59,217),(73,217),(87,217),(102,217),(116,217), 10 | (131,217),(144,217),(159,217),(172,217),(186,217),(199,217),(213,217),(227,217),(241,217),(255,217),(269,217),(283,217),(297,217),(310,217),(59,231),(73,231), 11 | (87,231),(102,231),(116,231),(130,231),(144,231),(158,231),(172,231),(186,231),(199,231),(213,231),(227,231),(241,231),(255,231),(268,231),(282,231),(297,231), 12 | (310,231),(59,244),(73,244),(87,244),(102,244),(116,244),(130,244),(144,244),(158,245),(172,245),(186,245),(199,245),(213,245),(227,245),(241,245),(255,245), 13 | (268,245),(282,246),(297,246),(310,246),(59,257),(73,257),(87,257),(102,257),(116,258),(130,258),(144,258),(158,258),(172,258),(186,258),(199,258),(213,258), 14 | (227,258),(240,259),(255,259),(268,259),(282,259),(296,259),(310,259),(59,273),(73,273),(87,273),(102,273),(116,273),(130,273),(144,273),(158,273),(172,273), 15 | (186,273),(199,273),(213,273),(226,273),(240,273),(255,273),(268,273),(282,273),(296,273),(310,273),(59,286),(73,286),(87,286),(102,286),(116,286),(130,286), 16 | (144,286),(158,286),(172,286),(186,286),(199,286),(213,286),(226,286),(240,286),(255,286),(268,286),(282,286),(296,286),(309,286),(59,301),(73,301),(87,301), 17 | (102,301),(116,301),(130,301),(144,301),(158,301),(172,300),(186,300),(199,300),(213,300),(226,300),(240,300),(255,300),(268,300),(282,300),(296,299),(309,299), 18 | (59,314),(73,314),(87,314),(102,314),(115,314),(130,314),(143,314),(158,314),(172,314),(186,314),(199,314),(213,314),(226,314),(240,314),(255,314),(268,314), 19 | (282,314),(295,314),(309,314),(59,329),(73,329),(87,329),(101,329),(115,329),(130,329),(143,328),(158,328),(172,328),(186,328),(199,328),(213,328),(226,328), 20 | (240,328),(255,327),(268,327),(282,327),(295,327),(309,327),(59,343),(73,343),(87,343),(101,343),(115,343),(130,343),(143,343),(158,343),(172,343),(186,343), 21 | (199,343),(213,343),(225,343),(239,343),(255,343),(268,343),(282,343),(295,343),(309,343),(59,357),(73,357),(87,357),(101,357),(115,357),(130,356),(143,356), 22 | (158,356),(172,356),(186,356),(199,356),(213,356),(225,356),(239,355),(254,355),(268,355),(282,355),(295,355),(309,355),(59,371),(73,370),(87,370),(101,370), 23 | (115,370),(130,370),(143,370),(157,370),(172,370),(186,369),(199,369),(213,369),(225,369),(239,369),(254,369),(268,369),(282,369),(295,369),(309,368),(59,384), 24 | (73,384),(87,384),(101,384),(115,384),(130,384),(143,384),(157,383),(172,383),(186,383),(199,383),(213,383),(225,383),(239,383),(254,383),(268,383),(281,382), 25 | (294,382),(309,382))#phi:88.50#theta:0.00#] 26 | AW[ca]AW[ea]AB[fa]AB[ja]AB[la]AW[qa]AB[ra]AW[cb]AW[db]AB[eb]AW[fb]AB[gb]AB[hb]AB[ib]AW[jb]AB[kb]AB[lb]AB[mb]AB[nb]AB[ob]AW[pb]AW[qb]AB[rb]AW[sb]AB[ec]AW[fc]AW[gc]AB[hc]AW[ic]AW[jc]AB[kc]AW[lc]AW[mc]AB[oc]AB[qc]AW[rc]AW[ad]AW[bd]AW[dd]AW[ed]AW[fd]AB[gd]AB[hd]AB[id]AW[jd]AW[ld]AB[nd]AB[qd]AW[rd]AB[ae]AW[be]AW[ce]AW[de]AW[ee]AB[fe]AB[ge]AB[he]AW[ie]AW[je]AW[ke]AW[le]AB[me]AB[oe]AB[pe]AW[qe]AW[re]AB[af]AB[bf]AB[cf]AW[df]AB[ef]AB[gf]AW[jf]AB[kf]AB[lf]AB[mf]AB[of]AW[pf]AW[rf]AB[cg]AW[dg]AW[eg]AB[fg]AB[gg]AW[hg]AW[jg]AW[kg]AW[lg]AB[mg]AB[og]AB[pg]AW[qg]AB[bh]AB[ch]AB[dh]AB[eh]AW[fh]AW[gh]AW[hh]AW[ih]AW[jh]AW[kh]AB[lh]AB[oh]AW[ph]AW[qh]AB[ai]AB[ci]AW[di]AW[ei]AW[fi]AB[hi]AB[ii]AW[ji]AB[ki]AB[oi]AB[pi]AW[qi]AB[bj]AB[cj]AB[dj]AB[ej]AW[fj]AB[gj]AB[ij]AB[jj]AB[kj]AB[oj]AW[pj]AB[ck]AB[dk]AW[ek]AW[fk]AW[gk]AB[hk]AB[lk]AB[mk]AB[nk]AB[ok]AW[pk]AW[qk]AW[rk]AB[bl]AB[dl]AB[el]AW[fl]AW[gl]AW[hl]AW[il]AW[kl]AB[ll]AW[ml]AW[nl]AW[ol]AW[ql]AW[sl]AB[bm]AB[cm]AB[dm]AW[em]AB[gm]AW[jm]AB[km]AB[lm]AB[mm]AB[om]AW[pm]AW[qm]AW[rm]AB[bn]AW[cn]AB[dn]AW[en]AB[gn]AW[hn]AW[in]AW[kn]AB[ln]AB[nn]AW[pn]AW[qn]AW[rn]AB[bo]AW[go]AW[ho]AB[io]AW[ko]AB[lo]AB[mo]AW[no]AW[oo]AW[po]AB[qo]AB[ro]AB[ap]AW[bp]AW[cp]AW[dp]AW[ep]AB[fp]AW[gp]AW[hp]AB[ip]AB[jp]AW[kp]AB[lp]AW[mp]AB[np]AB[op]AW[pp]AB[qp]AB[bq]AB[cq]AB[dq]AB[eq]AW[fq]AW[gq]AB[hq]AB[iq]AW[jq]AW[kq]AW[lq]AW[mq]AB[nq]AB[pq]AB[qq]AB[ar]AB[fr]AB[gr]AB[hr]AW[ir]AW[jr]AW[lr]AB[mr]AB[nr]AB[bs]AW[hs]) 27 | -------------------------------------------------------------------------------- /katago_gui/templates/register.tmpl: -------------------------------------------------------------------------------- 1 | {% extends 'layout.tmpl' %} 2 | 3 | {% block css %} 4 | 5 | {% endblock %} 6 | 7 | {% block content %} 8 |
9 | 10 | 23 | 114 |
11 | {% with messages = get_flashed_messages( with_categories=true) %} 12 | {% if messages %} 13 | {% for category, message in messages %} 14 |
15 | {{ message }} 16 |
17 | {% endfor %} 18 | {% else %} 19 |
20 | {% endif %} 21 | {% endwith %} 22 |
24 |
25 | {{ form.hidden_tag() }} 26 |
27 |
28 | {{ form.username.label( class='form-control-label') }} 29 | {% if form.username.errors %} 30 | {{ form.username( class='form-control form-control-lg is-invalid') }} 31 |
32 | {% for error in form.username.errors %} 33 | {{ error }} 34 | {% endfor %} 35 |
36 | {% else %} 37 | {{ form.username( class='form-control form-control-lg') }} 38 | {% endif %} 39 |
40 |
41 | {{ form.fname.label( class='form-control-label') }} 42 | {% if form.fname.errors %} 43 | {{ form.fname( class='form-control form-control-lg is-invalid') }} 44 |
45 | {% for error in form.fname.errors %} 46 | {{ error }} 47 | {% endfor %} 48 |
49 | {% else %} 50 | {{ form.fname( class='form-control form-control-lg') }} 51 | {% endif %} 52 |
53 |
54 | {{ form.lname.label( class='form-control-label') }} 55 | {% if form.lname.errors %} 56 | {{ form.lname( class='form-control form-control-lg is-invalid') }} 57 |
58 | {% for error in form.lname.errors %} 59 | {{ error }} 60 | {% endfor %} 61 |
62 | {% else %} 63 | {{ form.lname( class='form-control form-control-lg') }} 64 | {% endif %} 65 |
66 |
67 | {{ form.email.label( class='form-control-label') }} 68 | {% if form.email.errors %} 69 | {{ form.email( class='form-control form-control-lg is-invalid') }} 70 |
71 | {% for error in form.email.errors %} 72 | {{ error }} 73 | {% endfor %} 74 |
75 | {% else %} 76 | {{ form.email( class='form-control form-control-lg') }} 77 | {% endif %} 78 |
79 |
80 | {{ form.password.label( class='form-control-label') }} 81 | {% if form.password.errors %} 82 | {{ form.password( class='form-control form-control-lg is-invalid') }} 83 |
84 | {% for error in form.password.errors %} 85 | {{ error }} 86 | {% endfor %} 87 |
88 | {% else %} 89 | {{ form.password( class='form-control form-control-lg') }} 90 | {% endif %} 91 |
92 |
93 | {{ form.confirm_password.label( class='form-control-label') }} 94 | {% if form.confirm_password.errors %} 95 | {{ form.confirm_password( class='form-control form-control-lg is-invalid') }} 96 |
97 | {% for error in form.confirm_password.errors %} 98 | {{ error }} 99 | {% endfor %} 100 |
101 | {% else %} 102 | {{ form.confirm_password( class='form-control form-control-lg') }} 103 | {% endif %} 104 |
105 |
106 |
107 | {{ form.submit( class='btn btn-outline-info') }} 108 |

109 |
110 | {{ tr('Already Have An Account?') }} {{tr('Sign In.')}} 111 |
112 |
113 |
115 |
116 | {% endblock content %} 117 | -------------------------------------------------------------------------------- /checksame.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # /******************************************************************** 4 | # Filename: katago-gui/katago_gui/checksame.py 5 | # Author: AHN 6 | # Creation Date: Oct, 2020 7 | # **********************************************************************/ 8 | # 9 | # A script to check whether the first ZOBRIST_MOVES moves of to sgf files are the same, 10 | # taking care of all symmetries, using zobrist hashing. 11 | 12 | # 13 | from pdb import set_trace as BP 14 | 15 | import os, sys, re 16 | from datetime import datetime 17 | #import argparse 18 | from katago_gui.sgf import Sgf_game 19 | import katago_gui.goboard_fast as goboard 20 | from katago_gui.go_utils import point_from_coords, game_zobrist, board_transform 21 | from katago_gui.gotypes import Player,Point 22 | from katago_gui.zobrist import HASH_CODE, EMPTY_BOARD 23 | from katago_gui.helpers import moves2sgf 24 | 25 | from katago_gui import BOARD_SIZE, ZOBRIST_MOVES 26 | 27 | #--------------------------- 28 | def usage(printmsg=False): 29 | name = os.path.basename(__file__) 30 | msg = ''' 31 | 32 | Name: 33 | %s: Check if two sgf files agree on the first %d moves, after eliminating symmetries. 34 | Synopsis: 35 | %s 36 | Description: 37 | Transform file1.sgf with one of ['0','lr','td','ri','le','letd','ritd','lrtd'] 38 | and save to checksame1.sgf. Then check if it is the same as file2.sgf . 39 | Examples: 40 | %s lr first.sgf second.sgf 41 | %s td first.sgf first.sgf 42 | -- 43 | ''' % (name,ZOBRIST_MOVES,name,name,name) 44 | if printmsg: 45 | print(msg) 46 | exit(1) 47 | else: 48 | return msg 49 | 50 | #------------ 51 | def main(): 52 | if len(sys.argv) != 4: 53 | usage( True) 54 | 55 | transform = sys.argv[1] 56 | sgfname1 = sys.argv[2] 57 | sgfname2 = sys.argv[3] 58 | with open(sgfname1) as x: sgfstr1 = x.read() 59 | with open(sgfname2) as x: sgfstr2 = x.read() 60 | moves1 = getmoves( sgfstr1) 61 | moves2 = getmoves( sgfstr2) 62 | moves1 = rotmoves( moves1, transform) 63 | rotsgf = moves2sgf( moves1) 64 | with open( 'checksame1.sgf', 'w') as x: x.write( rotsgf) 65 | 66 | zob1 = game_zobrist( moves1) 67 | zob2 = game_zobrist( moves2) 68 | if zob1 == zob2: 69 | print( 'same') 70 | else: 71 | print( 'different') 72 | 73 | # #------------------------------------------------------ 74 | # def zobrist( moves, zobrist_moves = ZOBRIST_MOVES): 75 | # zobrist = 0 76 | # for transform_key in ['0','lr','td','ri','le','letd','ritd','lrtd']: 77 | # game_state = goboard.GameState.new_game( BOARD_SIZE) 78 | # for idx,move in enumerate(moves): 79 | # if move == 'pass': 80 | # next_move = goboard.Move.pass_turn() 81 | # elif move == 'resign': 82 | # next_move = goboard.Move.resign() 83 | # else: 84 | # move = transform( move, transform_key) 85 | # next_move = goboard.Move.play( Point( move[0]+1, move[1]+1)) 86 | # game_state = game_state.apply_move( next_move) 87 | # zobrist = max( game_state.board.zobrist_hash(), zobrist) 88 | # return zobrist 89 | 90 | #-------------------------------------- 91 | def rotmoves( moves, transform_key): 92 | res = [] 93 | for idx,move in enumerate( moves): 94 | res.append( board_transform( move, transform_key)) 95 | return res 96 | 97 | #-------------------------------------------------------------------------- 98 | def getmoves( sgfstr): 99 | 'Turn an sgf string into a list of moves like [(3,3), "pass", ...]' 100 | sgf = Sgf_game.from_string( sgfstr) 101 | moves = [] 102 | for idx,item in enumerate(sgf.main_sequence_iter()): 103 | color, move = item.get_move() 104 | if not color: continue 105 | if not move: 106 | moves.append( 'pass') 107 | else: 108 | moves.append( move) 109 | return moves 110 | 111 | 112 | 'Convert a list of moves like [(2,3), ...] to sgf' 113 | #--------------------------------------------------- 114 | def moves2sgf( moves): 115 | 'Convert a list of moves like [(2,3), ...] to sgf' 116 | sgf = '(;FF[4]SZ[19]\n' 117 | sgf += 'SO[checksame.py]\n' 118 | dtstr = datetime.now().strftime('%Y-%m-%d') 119 | km = '7.5' 120 | 121 | sgf += 'PB[black]\n' 122 | sgf += 'PW[white]\n' 123 | sgf += 'KM[%s]\n' % km 124 | sgf += 'DT[%s]\n' % dtstr 125 | 126 | movestr = '' 127 | result = '' 128 | color = 'B' 129 | for idx,move in enumerate(moves): 130 | othercol = 'W' if color == 'B' else 'B' 131 | if move == 'resign': 132 | result = 'RE[%s+R]' % othercol 133 | elif move == 'pass': 134 | movestr += ';%s[tt]' % color 135 | else: 136 | col_s = 'abcdefghijklmnopqrstuvwxy'[move[1]] 137 | row_s = 'abcdefghijklmnopqrstuvwxy'[18 - move[0]] 138 | movestr += ';%s[%s%s]' % (color,col_s,row_s) 139 | color = othercol 140 | 141 | sgf += result 142 | sgf += movestr 143 | sgf += ')' 144 | return sgf 145 | 146 | if __name__ == '__main__': 147 | main() 148 | -------------------------------------------------------------------------------- /katago_gui/templates/watch_mobile.tmpl: -------------------------------------------------------------------------------- 1 | {% extends 'layout_watch.tmpl' %} 2 | 3 | {% block pre %} 4 | 11 | 12 | {% endblock pre %} 13 | 14 | {% block css %} 15 | 34 | {% endblock css %} 35 | 36 | {% block content %} 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 61 | 62 | 63 | 84 | 85 | 92 | 93 | 94 | 102 | 103 | 104 | 113 |
 
 
 
 
46 |
47 | 48 | 49 | 50 | 56 | 57 | 58 |
  51 | 52 | 53 | 54 |
yy
55 |
 
59 |
60 |
64 |
65 | 66 | {% if live == '1' %} 67 | 71 | {% endif %} 72 | 81 |
68 | 69 |    70 | 73 | 74 |    75 | 76 | 77 |    78 | 79 | 80 |
82 |
83 |
86 |
87 | 88 |   89 | 90 |
91 |
 
95 |
96 | 97 | 98 | 99 | 100 |
101 |
 
105 |
106 | 107 | 110 |
108 | 109 |
111 |
112 |
114 | 115 | 116 | 131 | {% endblock content %} 132 | 133 | {% block js %} 134 | 135 | 136 | 141 | {% endblock js %} 142 | -------------------------------------------------------------------------------- /katago_gui/templates/watch.tmpl: -------------------------------------------------------------------------------- 1 | {% extends 'layout_watch.tmpl' %} 2 | 3 | {% block pre %} 4 | 11 | {% endblock pre %} 12 | 13 | {% block css %} 14 | 54 | {% endblock css %} 55 | 56 | {% block content %} 57 | {% if 0 and current_user.data['username'].strip().lower() in ['acm','ahn'] %} 58 |
59 | debug 60 |
61 | {% endif %} 62 |
63 | 64 | 65 | 66 |
67 | 68 |
70 |
71 | 72 |
73 | 75 | 76 |
77 |
78 | 79 | 80 | 81 |
82 |
83 |
84 |
85 | 86 |
87 |
88 |
89 | 90 |
91 |
92 |
93 |
94 | 95 |
96 |
97 |
98 |
99 | 100 | 101 | 102 |
103 |
104 | 105 |
106 | KataGo 20 blocks
2020-05-30 107 |
108 |
109 | 110 |
111 | {% if live == '1' %} 112 | 113 | {% endif %} 114 |
115 |
116 |   117 | 118 |
119 |
120 |   122 |   124 |   126 | 128 |
129 |
130 |   131 | 132 |
133 |
134 |   135 |
136 |
137 | 139 |
140 |
 
141 |
142 |
143 | 144 | 145 | 160 | {% endblock content %} 161 | 162 | {% block js %} 163 | 164 | 165 | 170 | {% endblock js %} -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | A front end to play against KataGo via a REST API, ready to deploy to Heroku. 3 | ================================================================================ 4 | 5 | Try it at https://katagui.herokuapp.com . 6 | 7 | The REST API for the AI back end is at https://github.com/hauensteina/katago-server . 8 | 9 | What exactly does this do 10 | ----------------------------- 11 | 12 | KataGui has all the basic features of a complete Go Server, similar to OGS or some such, with the notable difference that you don't play other people. It is meant to help you study using KataGo, without the need for expensive hardware. You can watch other people and study and chat with them, or search for games via Sgf upload to catch people who use KataGui to cheat on another server. 13 | 14 | The design goal is simplicity and ease of use on mobile devices. We want power without features. 15 | 16 | You can use it from any device that has a web browser. Personally, I mostly use it from a tablet, which feels a lot like an intelligent Go board if you are using KataGui. 17 | 18 | The technology behind KataGui is the usual mess: A Postgres database, Websockets and Redis, REST API calls, Javascript, Python, and an additional back end server running the AI, which is obviously written in C++. It runs on Heroku, and the instructions below should be sufficient to run it inside your own Heroku project, if you are persistent. 19 | 20 | You will need a back end server running KataGo. There is a basic one at https://my-katago.herokuapp.com, with a 10 block network and 8 playouts. If you want to set up your own, follow the steps [here](https://github.com/hauensteina/katago-server). Look at the file `__init__.py` to see where the server URL is configured. 21 | 22 | 23 | How to get KataGui to run on Heroku 24 | -------------------------------------- 25 | 26 | Heroku is a platform which allows you to easily deploy a web app in a serverless, scalable way. 27 | And best of all, if you do not have a lot of traffic, it is totally free. 28 | 29 | The instructions below work on a Mac or in a Linux environment. If you are on Windows, it is probably easiest to just get yourself a [Ubuntu VM ](https://brb.nci.nih.gov/seqtools/installUbuntu.html). 30 | 31 | Get a Heroku login at https://heroku.com . 32 | 33 | Install the Heroku CLI. Instructions can be found [here](https://devcenter.heroku.com/articles/heroku-cli) . 34 | 35 | Open a terminal window and log into Heroku: 36 | 37 | ``` 38 | $ heroku login -i 39 | ``` 40 | 41 | Create a new Heroku project with 42 | 43 | ``` 44 | $ mkdir my-katagui 45 | $ cd my-katagui 46 | $ git init 47 | $ heroku apps:create my-katagui 48 | ``` 49 | 50 | You have to change the name `my-katagui` to something less generic to 51 | avoid name collisions. 52 | 53 | To get the code from github, let's add a second remote repo `github` to pull from: 54 | 55 | `$ git remote add github https://github.com/hauensteina/katago-gui.git` 56 | 57 | Then pull the code for the project to your local file system: 58 | 59 | `$ git pull github master` 60 | 61 | Make sure you are on python 3: 62 | 63 | ``` 64 | $ python --version 65 | Python 3.7.1 66 | ``` 67 | 68 | Make a virtual environment to manage our package dependencies: 69 | 70 | `$ python -m venv venv` 71 | 72 | Activate the venv: 73 | 74 | `$ source ./venv/bin/activate` 75 | 76 | Install the postgres addon: 77 | 78 | `$ heroku addons:create heroku-postgresql:hobby-dev` 79 | 80 | Postgres is a relational database, which means it uses SQL as query language. 81 | KataGui uses it to store users, logins, games, etc. 82 | It is initially empty. Tables will automatically be created on app startup. 83 | 84 | Install the Redis To Go addon: 85 | 86 | `$ heroku addons:create redistogo:nano` 87 | 88 | We use Redis channels to push messages to game observers when the game position changes. 89 | 90 | Now that Postgres and Redis exist, we need to make sure we locally know how to access them in the cloud. The easiest way to set the necessary environment variables is to get their values from Heroku: 91 | 92 | `$ heroku config > .env` 93 | 94 | The format of that output is not quite what we need. Edit `.env` to look like this: 95 | 96 | ``` 97 | $ vi .env 98 | # Use vi as best as you can 99 | $ cat .env 100 | 101 | export DATABASE_URL=postgres://epaotthtelivhp:e52af25836a477b22e3b418b870b688b8615aa5704225b6e7923f9fa5802fbe9@ec2-107-20-104-234.compute-1.amazonaws.com:5432/dcs3pjc3lov71m 102 | export REDISTOGO_URL=redis://redistogo:d320d3e67a6c50998be36022a98c4185@scat.redistogo.com:10501/ 103 | 104 | ``` 105 | 106 | Notice that .env has two lines only, and we had to insert the `export` keyword and the `=` assignments. 107 | Make sure there are no spaces around the `=` . 108 | 109 | Install the necessary python packages: 110 | 111 | ``` 112 | $ pip install --upgrade pip 113 | $ pip install -r requirements.txt 114 | ``` 115 | 116 | Let's give it a spin locally: 117 | 118 | ``` 119 | $ source .env; ./venv/bin/gunicorn -k flask_sockets.worker heroku_app:app -w 1 -b 0.0.0.0:8000 --reload --timeout 1000 120 | [2020-10-23 14:40:23 -0700] [90807] [INFO] Starting gunicorn 19.9.0 121 | [2020-10-23 14:40:23 -0700] [90807] [INFO] Listening at: http://0.0.0.0:8000 (90807) 122 | [2020-10-23 14:40:23 -0700] [90807] [INFO] Using worker: flask_sockets.worker 123 | [2020-10-23 14:40:23 -0700] [90810] [INFO] Booting worker with pid: 90810 124 | ``` 125 | 126 | Open a browser, type `http://127.0.0.1:8000` in the address bar. 127 | 128 | With any luck, you will see a Go board. Now if you click play, you will hopefully get a move out of the 129 | demo KataGo server at https://my-katago-server.herokuapp.com, which is described 130 | [here](https://github.com/hauensteina/katago-server). 131 | 132 | Now show it to the world: 133 | 134 | ``` 135 | $ git add -u 136 | $ git commit -m 'push to heroku' 137 | $ git push heroku master 138 | $ heroku logs -t --app my-katagui 139 | ... 140 | 2020-10-23T22:37:13.762732+00:00 heroku[web.1]: State changed from starting to up 141 | 2020-10-23T22:37:18.000000+00:00 app[api]: Build succeeded 142 | ``` 143 | 144 | Point your browser at `https://my-katagui.herokuapp.com`. 145 | 146 | 147 | Most things will work, except anything requiring email, like registering non-guest accounts. 148 | If you have a working SMTP account somewhere other than gmail (they will block login attempts by flask_mail), 149 | you can set the environment variables KATAGUI_EMAIL_USER and KATAGUI_EMAIL_PASS in the my-katagui Heroku project 150 | to make user registration work, too. 151 | 152 | If you got here, congratulations. Drop me a note at hauensteina@gmail.com. 153 | 154 | === The End === 155 | -------------------------------------------------------------------------------- /sgf_examples/kgs-even.sgf: -------------------------------------------------------------------------------- 1 | (;GM[1]FF[4]CA[UTF-8]AP[CGoban:3]ST[2] 2 | RU[Japanese]SZ[19]KM[0.50]TM[60]OT[5x10 byo-yomi] 3 | PW[GroSeigen]PB[ivan88911]WR[3d]BR[2d]DT[2025-10-14]PC[The KGS Go Server at http://www.gokgs.com/]RE[W+11.50] 4 | ;B[pd]BL[58.234]C[GroSeigen [3d\]: Hi. GG: 5 | ] 6 | ;W[dp]WL[55.092] 7 | ;B[pq]BL[50.461] 8 | ;W[dd]WL[49.268]C[ivan88911 [2d\]: gg 9 | ] 10 | ;B[fc]BL[44.581] 11 | ;W[cf]WL[47.693] 12 | ;B[dc]BL[28.156] 13 | ;W[cc]WL[43.373] 14 | ;B[cb]BL[27.064] 15 | ;W[bb]WL[42.161] 16 | ;B[bc]BL[10]OB[5] 17 | ;W[cd]WL[35.079] 18 | ;B[db]BL[10]OB[5] 19 | ;W[ba]WL[33.828] 20 | ;B[hd]BL[10]OB[5] 21 | ;W[qf]WL[31.884] 22 | ;B[qh]BL[10]OB[5] 23 | ;W[qc]WL[29.935] 24 | ;B[qd]BL[10]OB[5] 25 | ;W[pc]WL[28.735] 26 | ;B[od]BL[10]OB[5] 27 | ;W[rd]WL[28.089] 28 | ;B[re]BL[10]OB[5] 29 | ;W[rc]WL[27.198] 30 | ;B[qe]BL[10]OB[5] 31 | ;W[nc]WL[26.175] 32 | ;B[pf]BL[10]OB[5] 33 | ;W[po]WL[22.939] 34 | ;B[qm]BL[10]OB[5] 35 | ;W[qq]WL[21.432] 36 | ;B[qr]BL[10]OB[5] 37 | ;W[qp]WL[20.478] 38 | ;B[mq]BL[10]OB[4] 39 | ;W[on]WL[18.242] 40 | ;B[pl]BL[10]OB[4] 41 | ;W[or]WL[15.745] 42 | ;B[oq]BL[10]OB[4] 43 | ;W[pr]WL[13.628] 44 | ;B[rr]BL[10]OB[4] 45 | ;W[nr]WL[12.752] 46 | ;B[nq]BL[10]OB[4] 47 | ;W[mr]WL[11.362] 48 | ;B[kq]BL[10]OB[4] 49 | ;W[lr]WL[9.665] 50 | ;B[lq]BL[10]OB[4] 51 | ;W[kr]WL[8.31] 52 | ;B[iq]BL[10]OB[4] 53 | ;W[jr]WL[6.219] 54 | ;B[ir]BL[10]OB[4] 55 | ;W[fq]WL[0.176] 56 | ;B[mn]BL[10]OB[4] 57 | ;W[nm]WL[10]OW[5] 58 | ;B[ho]BL[10]OB[4] 59 | ;W[nk]WL[10]OW[5] 60 | ;B[oj]BL[10]OB[4] 61 | ;W[co]WL[10]OW[5] 62 | ;B[dg]BL[10]OB[4] 63 | ;W[cg]WL[10]OW[5] 64 | ;B[di]BL[10]OB[4] 65 | ;W[ch]WL[10]OW[5] 66 | ;B[dh]BL[10]OB[4] 67 | ;W[ll]WL[10]OW[5] 68 | ;B[cm]BL[10]OB[4] 69 | ;W[cj]WL[10]OW[5] 70 | ;B[dk]BL[10]OB[4] 71 | ;W[dj]WL[10]OW[5] 72 | ;B[ej]BL[10]OB[4] 73 | ;W[ek]WL[10]OW[5] 74 | ;B[dl]BL[10]OB[4] 75 | ;W[fk]WL[10]OW[5] 76 | ;B[ci]BL[10]OB[4] 77 | ;W[bi]WL[10]OW[5] 78 | ;B[bj]BL[10]OB[4] 79 | ;W[bh]WL[10]OW[5] 80 | ;B[fj]BL[10]OB[4] 81 | ;W[il]WL[10]OW[5] 82 | ;B[lc]BL[10]OB[4] 83 | ;W[mc]WL[10]OW[5] 84 | ;B[ld]BL[10]OB[4] 85 | ;W[id]WL[10]OW[5] 86 | ;B[ie]BL[10]OB[4] 87 | ;W[je]WL[10]OW[5] 88 | ;B[jd]BL[10]OB[4] 89 | ;W[ic]WL[10]OW[5] 90 | ;B[jc]BL[10]OB[4] 91 | ;W[he]WL[10]OW[5] 92 | ;B[if]BL[10]OB[4] 93 | ;W[gd]WL[10]OW[5] 94 | ;B[hc]BL[10]OB[4] 95 | ;W[gc]WL[10]OW[5] 96 | ;B[ib]BL[10]OB[4] 97 | ;W[gb]WL[10]OW[5] 98 | ;B[fe]BL[10]OB[3] 99 | ;W[fd]WL[10]OW[5] 100 | ;B[dr]BL[10]OB[3] 101 | ;W[dq]WL[10]OW[5] 102 | ;B[er]BL[10]OB[3] 103 | ;W[cr]WL[10]OW[5] 104 | ;B[br]BL[10]OB[3] 105 | ;W[cs]WL[10]OW[5] 106 | ;B[fr]BL[10]OB[3] 107 | ;W[bn]WL[10]OW[5] 108 | ;B[ge]BL[10]OB[3] 109 | ;W[ee]WL[10]OW[5] 110 | ;B[ef]BL[10]OB[3] 111 | ;W[mi]WL[10]OW[5] 112 | ;B[ro]BL[10]OB[3] 113 | ;W[sp]WL[10]OW[5] 114 | ;B[qo]BL[10]OB[3] 115 | ;W[pp]WL[10]OW[5] 116 | ;B[km]BL[10]OB[3] 117 | ;W[kl]WL[10]OW[5] 118 | ;B[gk]BL[10]OB[3] 119 | ;W[ii]WL[10]OW[5] 120 | ;B[ik]BL[10]OB[3] 121 | ;W[jk]WL[10]OW[5] 122 | ;B[ij]BL[10]OB[3] 123 | ;W[jj]WL[10]OW[5] 124 | ;B[jm]BL[10]OB[3] 125 | ;W[jl]WL[10]OW[5] 126 | ;B[bm]BL[10]OB[2] 127 | ;W[hi]WL[10]OW[5] 128 | ;B[bp]BL[10]OB[2] 129 | ;W[bq]WL[10]OW[4] 130 | ;B[aq]BL[10]OB[2] 131 | ;W[cq]WL[10]OW[4] 132 | ;B[bo]BL[10]OB[2] 133 | ;W[cn]WL[10]OW[4] 134 | ;B[an]BL[10]OB[2] 135 | ;W[ar]WL[10]OW[4] 136 | ;B[as]BL[10]OB[2] 137 | ;W[bs]WL[10]OW[4] 138 | ;B[gj]BL[10]OB[2] 139 | ;W[gq]WL[10]OW[4] 140 | ;B[gr]BL[10]OB[2] 141 | ;W[fo]WL[10]OW[4] 142 | ;B[hq]BL[10]OB[1] 143 | ;W[df]WL[10]OW[4] 144 | ;B[fg]BL[10]OB[1] 145 | ;W[se]WL[10]OW[4] 146 | ;B[sf]BL[10]OB[1] 147 | ;W[sd]WL[10]OW[4] 148 | ;B[rf]BL[10]OB[1] 149 | ;W[so]WL[10]OW[4] 150 | ;B[sn]BL[10]OB[1] 151 | ;W[rp]WL[10]OW[4] 152 | ;B[qn]BL[10]OB[1] 153 | ;W[in]WL[10]OW[4] 154 | ;B[im]BL[10]OB[1] 155 | ;W[hn]WL[10]OW[4] 156 | ;B[gn]BL[10]OB[1] 157 | ;W[gm]WL[10]OW[4] 158 | ;B[hm]BL[10]OB[1] 159 | ;W[go]WL[10]OW[4] 160 | ;B[fn]BL[10]OB[1] 161 | ;W[io]WL[10]OW[4] 162 | ;B[hp]BL[10]OB[1] 163 | ;W[fm]WL[10]OW[4] 164 | ;B[en]BL[10]OB[1] 165 | ;W[em]WL[10]OW[4] 166 | ;B[eq]BL[10]OB[1] 167 | ;W[ep]WL[10]OW[4] 168 | ;B[fp]BL[10]OB[1] 169 | ;W[gp]WL[10]OW[4] 170 | ;B[jo]BL[10]OB[1] 171 | ;W[dn]WL[10]OW[4] 172 | ;B[jq]BL[10]OB[1] 173 | ;W[lm]WL[10]OW[4] 174 | ;B[ln]BL[10]OB[1] 175 | ;W[hg]WL[10]OW[4] 176 | ;B[hf]BL[10]OB[1] 177 | ;W[kh]WL[10]OW[4] 178 | ;B[mh]BL[10]OB[1] 179 | ;W[nh]WL[10]OW[4] 180 | ;B[mg]BL[10]OB[1] 181 | ;W[ni]WL[10]OW[4] 182 | ;B[lb]BL[10]OB[1] 183 | ;W[ma]WL[10]OW[4] 184 | ;B[kg]BL[10]OB[1] 185 | ;W[ph]WL[10]OW[4] 186 | ;B[pi]BL[10]OB[1] 187 | ;W[pg]WL[10]OW[4] 188 | ;B[qg]BL[10]OB[1] 189 | ;W[ng]WL[10]OW[4] 190 | ;B[jh]BL[10]OB[1] 191 | ;W[ji]WL[10]OW[4] 192 | ;B[ki]BL[10]OB[1] 193 | ;W[li]WL[10]OW[4] 194 | ;B[lh]BL[10]OB[1] 195 | ;W[kj]WL[10]OW[4] 196 | ;B[oc]BL[10]OB[1] 197 | ;W[ob]WL[10]OW[4] 198 | ;B[nf]BL[10]OB[1] 199 | ;W[oi]WL[10]OW[4] 200 | ;B[pj]BL[10]OB[1] 201 | ;W[ok]WL[10]OW[4] 202 | ;B[pk]BL[10]OB[1] 203 | ;W[aj]WL[10]OW[4] 204 | ;B[bk]BL[10]OB[1] 205 | ;W[ar]WL[10]OW[4] 206 | ;B[am]BL[10]OB[1] 207 | ;W[gh]WL[10]OW[4] 208 | ;B[mb]BL[10]OB[1] 209 | ;W[nb]WL[10]OW[4] 210 | ;B[la]BL[10]OB[1] 211 | ;W[fh]WL[10]OW[4] 212 | ;B[ei]BL[10]OB[1] 213 | ;W[ne]WL[10]OW[4] 214 | ;B[nd]BL[10]OB[1] 215 | ;W[me]WL[10]OW[4] 216 | ;B[md]BL[10]OB[1] 217 | ;W[of]WL[10]OW[4] 218 | ;B[mf]BL[10]OB[1] 219 | ;W[oe]WL[10]OW[4] 220 | ;B[og]BL[10]OB[1] 221 | ;W[oh]WL[10]OW[4] 222 | ;B[le]BL[10]OB[1] 223 | ;W[nj]WL[10]OW[4] 224 | ;B[pe]BL[10]OB[1] 225 | ;W[og]WL[10]OW[4] 226 | ;B[mm]BL[10]OB[1] 227 | ;W[ml]WL[10]OW[4] 228 | ;B[ol]BL[10]OB[1] 229 | ;W[nl]WL[10]OW[4] 230 | ;B[nn]BL[10]OB[1] 231 | ;W[om]WL[10]OW[4] 232 | ;B[jn]BL[10]OB[1] 233 | ;W[eo]WL[10]OW[4] 234 | ;B[na]BL[10]OB[1] 235 | ;W[pb]WL[10]OW[4] 236 | ;B[oa]BL[10]OB[1] 237 | ;W[pa]WL[10]OW[4] 238 | ;B[oo]BL[10]OB[1] 239 | ;W[op]WL[10]OW[4] 240 | ;B[no]BL[10]OB[1] 241 | ;W[pn]WL[10]OW[4] 242 | ;B[js]BL[10]OB[1] 243 | ;W[ks]WL[10]OW[4] 244 | ;B[is]BL[10]OB[1] 245 | ;W[pm]WL[10]OW[4] 246 | ;B[rm]BL[10]OB[1] 247 | ;W[ma]WL[10]OW[4] 248 | ;B[na]BL[10]OB[1] 249 | ;W[ra]WL[10]OW[4] 250 | ;B[ha]BL[10]OB[1] 251 | ;W[ga]WL[10]OW[4] 252 | ;B[gg]BL[10]OB[1] 253 | ;W[ig]WL[10]OW[4] 254 | ;B[jg]BL[10]OB[1] 255 | ;W[eh]WL[10]OW[4] 256 | ;B[eg]BL[10]OB[1] 257 | ;W[hb]WL[10]OW[4] 258 | ;B[ia]BL[10]OB[1] 259 | ;W[np]WL[10]OW[4] 260 | ;B[mp]BL[10]OB[1] 261 | ;W[ds]WL[10]OW[4] 262 | ;B[ap]BL[10]OB[1] 263 | ;W[es]WL[10]OW[4] 264 | ;B[fs]BL[10]OB[1] 265 | ;W[ip]WL[10]OW[4] 266 | ;B[jp]BL[10]OB[1] 267 | ;W[gn]WL[10]OW[4] 268 | ;B[ak]BL[10]OB[1] 269 | ;W[ai]WL[10]OW[4] 270 | ;B[ih]BL[10]OB[1] 271 | ;W[hh]WL[10]OW[4] 272 | ;B[kh]BL[10]OB[1] 273 | ;W[dm]WL[10]OW[4] 274 | ;B[gl]BL[10]OB[1] 275 | ;W[hl]WL[10]OW[4] 276 | ;B[ma]BL[10]OB[1] 277 | ;W[]WL[10]OW[4] 278 | ;B[]BL[10]OB[1]TW[aa][ca][da][ea][fa][qa][sa][ab][cb][db][eb][fb][qb][rb][sb][ac][bc][dc][ec][fc][sc][ad][bd][ed][ae][be][ce][de][af][bf][ag][bg][ah][lj][mj][kk][lk][mk][en][fn][do][fp][rq][sq][br][qr][rr][sr][as][ls][ms][ns][os][ps][qs][rs][ss]TB[ja][ka][jb][kb][ic][kc][id][kd][he][je][ke][ff][gf][jf][kf][lf][qf][lg][rg][sg][rh][sh][qi][ri][si][cj][dj][qj][rj][sj][ck][qk][rk][sk][al][bl][cl][ql][rl][sl][sm][kn][rn][ko][lo][mo][kp][lp][hr][gs][hs]C[GroSeigen [3d\]: Thx 279 | ivan88911 [2d\]: tx 280 | ]) 281 | -------------------------------------------------------------------------------- /katago_gui/templates/layout.tmpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | {% block pre %} 4 | 10 | {% endblock pre %} 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | Study With KataGo 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 42 | 43 | 44 | {% block css %} 45 | {% endblock css %} 46 | 54 | 55 | 56 | 57 | 63 | 64 | 65 | {% if not home %} 66 | 67 | 68 | 69 | 70 | 74 | 75 | 76 |
  71 | 72 | KataGui 0.0.0 73 |
77 | {% else %} 78 | 79 | 80 | 81 | 82 | 86 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 134 | 135 | 136 | 137 | 142 | 143 |
  87 | {% if logged_in() %} 88 | {{tr('Account')}}    89 | {{tr('Logout')}}    90 | 91 | {% else %} 92 | {{tr('Login')}}    93 | {{tr('Register')}}    94 | 95 | {% endif %} 96 | {{tr('Search')}}    97 | {{tr('About')}} 98 |   
106 |
107 | 121 | {% if logged_in() %} 122 |
124 | {{ current_user.data["username"] }} 125 |
126 | {% else %} 127 |
129 | {{tr('Guest')}} 130 |
131 | {% endif %} 132 |
133 |
138 | {% endif %} 139 | {% block content %} 140 | {% endblock content %} 141 |
144 | 163 | 164 | 165 | {% block js %} 166 | {% endblock js %} 167 | 168 | 169 | 170 | -------------------------------------------------------------------------------- /katago_gui/templates/settings_overlay.tmpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | 101 | 102 | 103 | 104 | -------------------------------------------------------------------------------- /katago_gui/scoring.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # /******************************************************************** 4 | # Filename: scoring.py 5 | # Author: AHN 6 | # Creation Date: Mar, 2019 7 | # **********************************************************************/ 8 | # 9 | # Count a go position. All dead stones must have been removed already. 10 | # 11 | 12 | from __future__ import absolute_import 13 | from collections import namedtuple 14 | from katago_gui.gotypes import Player, Point 15 | import numpy as np 16 | from pdb import set_trace as BP 17 | 18 | #=================== 19 | class Territory: 20 | 21 | #---------------------------------------- 22 | def __init__( self, territory_map): 23 | self.n_intersections = len( territory_map) 24 | self.num_black_territory = 0 25 | self.num_white_territory = 0 26 | self.num_black_stones = 0 27 | self.num_white_stones = 0 28 | self.num_dame = 0 29 | self.dame_points = [] 30 | self.black_points = [] 31 | self.white_points = [] 32 | for point, status in territory_map.items(): 33 | if status == Player.black: 34 | self.num_black_stones += 1 35 | self.black_points.append( point) 36 | elif status == Player.white: 37 | self.num_white_stones += 1 38 | self.white_points.append( point) 39 | elif status == 'territory_b': 40 | self.num_black_territory += 1 41 | self.black_points.append( point) 42 | elif status == 'territory_w': 43 | self.num_white_territory += 1 44 | self.white_points.append( point) 45 | elif status == 'dame': 46 | self.num_dame += 1 47 | self.dame_points.append( point) 48 | 49 | # Turn yourself into a 1D np array of 0 for b territory or stone, else 1. 50 | # So 1 denotes w territory or dame. 51 | # This is the label we use to train a territory estimator using 52 | # sigmoid activation. 53 | #------------------------------------------------------------ 54 | def encode_sigmoid( self): 55 | bsz = int( round( np.sqrt( self.n_intersections))) 56 | res = np.full( (bsz, bsz), 1, dtype='int8') 57 | for p in self.black_points: 58 | res[p.row - 1, p.col - 1] = 0 59 | return res 60 | 61 | #========================================================= 62 | class GameResult( namedtuple( 'GameResult', 'b w komi')): 63 | @property 64 | def winner(self): 65 | if self.b > self.w + self.komi: 66 | return Player.black 67 | return Player.white 68 | 69 | @property 70 | def winning_margin(self): 71 | w = self.w + self.komi 72 | return abs(self.b - w) 73 | 74 | def __str__(self): 75 | w = self.w + self.komi 76 | if self.b > w: 77 | return 'B+%.1f' % (self.b - w,) 78 | return 'W+%.1f' % (w - self.b,) 79 | 80 | # Map a board into territory and dame. 81 | # Any points that are completely surrounded by a single color are 82 | # counted as territory; it makes no attempt to identify even 83 | # trivially dead groups. 84 | #------------------------------- 85 | def evaluate_territory( board): 86 | status = {} 87 | for r in range( 1, board.num_rows + 1): 88 | for c in range( 1, board.num_cols + 1): 89 | p = Point(row=r, col=c) 90 | if p in status: # <1> 91 | continue 92 | stone = board.get(p) 93 | if stone is not None: # <2> 94 | status[p] = board.get(p) 95 | else: 96 | group, neighbors = _collect_region(p, board) 97 | if len(neighbors) == 1: # <3> 98 | neighbor_stone = neighbors.pop() 99 | stone_str = 'b' if neighbor_stone == Player.black else 'w' 100 | fill_with = 'territory_' + stone_str 101 | else: 102 | fill_with = 'dame' # <4> 103 | for pos in group: 104 | status[pos] = fill_with 105 | return Territory( status) 106 | 107 | # <1> Skip the point, if you already visited this as part of a different group. 108 | # <2> If the point is a stone, add it as status. 109 | # <3> If a point is completely surrounded by black or white stones, count it as territory. 110 | # <4> Otherwise the point has to be a neutral point, so we add it to dame. 111 | # end::scoring_evaluate_territory[] 112 | 113 | 114 | # Find the contiguous section of a board containing a point. Also 115 | # identify all the boundary points. 116 | # This is like finding strings, but also for empty intersections. 117 | #------------------------------------------------------------------ 118 | def _collect_region( start_pos, board, visited=None): 119 | if visited is None: 120 | visited = {} 121 | if start_pos in visited: 122 | return [], set() 123 | all_points = [start_pos] 124 | all_borders = set() 125 | visited[start_pos] = True 126 | here = board.get( start_pos) 127 | deltas = [(-1, 0), (1, 0), (0, -1), (0, 1)] 128 | for delta_r, delta_c in deltas: 129 | next_p = Point( row=start_pos.row + delta_r, col=start_pos.col + delta_c) 130 | if not board.is_on_grid(next_p): 131 | continue 132 | neighbor = board.get( next_p) 133 | if neighbor == here: 134 | points, borders = _collect_region( next_p, board, visited) 135 | all_points += points 136 | all_borders |= borders 137 | else: 138 | all_borders.add( neighbor) 139 | return all_points, all_borders 140 | 141 | # Naive Tromp Taylor result 142 | #-------------------------------------- 143 | def compute_game_result( game_state): 144 | territory = evaluate_territory( game_state.board) 145 | return (territory, 146 | GameResult( 147 | territory.num_black_territory + territory.num_black_stones, 148 | territory.num_white_territory + territory.num_white_stones, 149 | komi=7.5) 150 | ) 151 | 152 | # Turn nn output into the expected scoring format. 153 | # Called from server.py 154 | #---------------------------------------------------- 155 | def compute_nn_game_result( labels, next_player): 156 | mid = 0.5 # Between B and W 157 | #tol = 0.075 # Closer to 0.5 than tol is dame. Smaller means less dame. 158 | tol = 0.15 # Closer to 0.5 than tol is dame. Smaller means less dame. 159 | labels = labels[0,:] 160 | n_isecs = len(labels) 161 | boardsize = int(round(np.sqrt(n_isecs))) 162 | terrmap = {} 163 | bpoints = 0 164 | wpoints = 0 165 | dame = 0 166 | ssum = 0 167 | for r in range( 1, boardsize+1): 168 | for c in range( 1, boardsize+1): 169 | p = Point( row=r, col=c) 170 | prob_white = labels[ (r-1)*boardsize + c - 1] 171 | wpoints += prob_white 172 | if prob_white < mid - tol: 173 | terrmap[p] = 'territory_b' 174 | #bpoints += 1 175 | elif prob_white > mid + tol: 176 | terrmap[p] = 'territory_w' 177 | #wpoints += 1 178 | else: 179 | terrmap[p] = 'dame' 180 | dame += 1 181 | territory = Territory( terrmap) 182 | # bpoints += int( dame / 2) 183 | # wpoints += int( dame / 2) 184 | wpoints = int(round(wpoints)) 185 | bpoints = n_isecs - wpoints 186 | # if dame % 2: 187 | # if next_player == Player.white: 188 | # wpoints += 1 189 | # else: 190 | # bpoints += 1 191 | 192 | return (territory, 193 | GameResult( 194 | bpoints, 195 | wpoints, 196 | # territory.num_black_territory, 197 | # territory.num_white_territory, 198 | komi=0) 199 | ) 200 | -------------------------------------------------------------------------------- /katago_gui/templates/settings_overlay_mobile.tmpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | 136 | 137 | 138 | 139 | -------------------------------------------------------------------------------- /katago_gui/routes_watch.py: -------------------------------------------------------------------------------- 1 | # /******************************************************************** 2 | # Filename: katago-gui/katago_gui/routes_watch.py 3 | # Author: AHN 4 | # Creation Date: Aug, 2020 5 | # **********************************************************************/ 6 | # 7 | # URL Endpoints for observing games 8 | # 9 | 10 | from pdb import set_trace as BP 11 | 12 | import os, sys, re, json 13 | from datetime import datetime, timedelta 14 | import gevent 15 | 16 | from flask import jsonify, request, send_file, render_template, flash, redirect, url_for 17 | from flask_login import current_user 18 | from flask_sockets import Rule 19 | 20 | from katago_gui import app, logged_in 21 | from katago_gui import auth, db, sockets, redis, REDIS_CHAN 22 | from katago_gui.translations import translate as tr 23 | from katago_gui import go_utils 24 | from katago_gui import ZOBRIST_MOVES 25 | 26 | 27 | 28 | @app.route('/watch_select_game') 29 | @app.route('/watch_select_game_mobile') 30 | #--------------------------------------- 31 | def watch_select_game(): 32 | """ Show the screen to choose the game to watch """ 33 | rows = db.slurp( 'v_games_24hours') 34 | games = [] 35 | try: 36 | for row in rows: 37 | g = {} 38 | g['username'] = row['username'] 39 | g['handicap'] = row['handicap'] 40 | g['komi'] = row['komi'] 41 | g['live'] = row['live'] 42 | # Format seconds to hhmmss 43 | g['t_idle'] = re.sub( r'[.].*', '' , str( timedelta( seconds=row['idle_secs']))) 44 | g['nmoves'] = json.loads( row['game_record'])['n_visible'] 45 | g['n_obs'] = row['n_obs'] 46 | if 'mobile' in request.url_rule.rule: 47 | g['link'] = url_for( 'watch_game_mobile',game_hash=row['game_hash'], live=row['live']) 48 | else: 49 | g['link'] = url_for( 'watch_game',game_hash=row['game_hash'], live=row['live']) 50 | if g['live'] or g['nmoves'] > 20: 51 | games.append( g) 52 | except Exception as e: 53 | print( 'ERROR: Exception in watch_select_game(): %s' % str(e)) 54 | 55 | res = render_template( 'watch_select_game.tmpl', games=games) 56 | return res 57 | 58 | # a9429eb8b90e4794 59 | @app.route('/find_game', methods=['GET']) 60 | #-------------------------------------------------------------------------------- 61 | def find_game(): 62 | """ Upload Sgf to find (GET) or find the game and show matches (POST) """ 63 | 64 | # Pick sgf file we are trying to find 65 | if not 'action' in request.args: 66 | return render_template( 'find_game.tmpl', action='choose_file') 67 | 68 | # With the moves from the sgf, find the game in the DB, display link 69 | try: 70 | rc0s = [] 71 | # Convert to 0 based 0-18 (row,col) pairs 72 | moves = json.loads(request.args['moves']) 73 | for move in moves: 74 | coord = move 75 | if coord in ('pass','resign'): 76 | rc0s.append( coord) 77 | continue 78 | point = go_utils.point_from_coords( coord) 79 | rc0 = (point.row - 1, point.col - 1) 80 | rc0s.append( rc0) 81 | zobrist = go_utils.game_zobrist( rc0s, ZOBRIST_MOVES) 82 | rows = db.find( 't_game', 'zobrist', str(zobrist)) 83 | 84 | games = [] 85 | for row in rows: 86 | g = {} 87 | g['username'] = row['username'] 88 | #g['handicap'] = row['handicap'] 89 | #g['komi'] = row['komi'] 90 | g['ts_started'] = row['ts_started'].strftime("%Y-%m-%d %H:%M") 91 | g['ts_latest_move'] = row['ts_latest_move'].strftime("%Y-%m-%d %H:%M") 92 | #g['live'] = row['live'] 93 | # Format seconds to hhmmss 94 | #g['t_idle'] = re.sub( r'[.].*', '' , str( timedelta( seconds=row['idle_secs']))) 95 | #g['nmoves'] = json.loads( row['game_record'])['n_visible'] 96 | #g['n_obs'] = row['n_obs'] 97 | if 'mobile' in request.url_rule.rule: 98 | g['link'] = url_for( 'watch_game_mobile',game_hash=row['game_hash'],live=0) 99 | else: 100 | g['link'] = url_for( 'watch_game',game_hash=row['game_hash'],live=0) 101 | games.append( g) 102 | except Exception as e: 103 | print( 'ERROR: Exception in find_game(): %s' % str(e)) 104 | 105 | res = render_template( 'find_game.tmpl', action='show_games', games=games) 106 | return res 107 | 108 | @app.route('/watch_game') 109 | #----------------------------------------------------- 110 | def watch_game(): 111 | """ User clicks on the game he wants to watch """ 112 | try: 113 | gh = request.args['game_hash'] 114 | live = request.args['live'] 115 | # Remember which game we are watching 116 | db.update_row( 't_user', 'email', current_user.id, {'watch_game_hash':gh}) 117 | return render_template( 'watch.tmpl', game_hash=gh, live=live) 118 | except Exception as e: 119 | app.logger.info( 'ERROR: Exception in watch_game(): %s' % str(e)) 120 | return redirect( url_for('index')) 121 | 122 | @app.route('/watch_game_mobile') 123 | #----------------------------------------------------- 124 | def watch_game_mobile(): 125 | """ User clicks on the game he wants to watch """ 126 | try: 127 | gh = request.args['game_hash'] 128 | live = request.args['live'] 129 | # Remember which game we are watching 130 | db.update_row( 't_user', 'email', current_user.id, {'watch_game_hash':gh}) 131 | return render_template( 'watch_mobile.tmpl', game_hash=gh, live=live) 132 | except: 133 | app.logger.info( 'ERROR: Exception in watch_game_mobile()') 134 | return redirect( url_for('index')) 135 | 136 | @app.route('/clear_watch_game', methods=['POST']) 137 | #-------------------------------------------------- 138 | def clear_watch_game(): 139 | """ Clear watched game before unload """ 140 | try: 141 | db.update_row( 't_user', 'email', current_user.id, {'watch_game_hash':''}) 142 | return jsonify( {'result': 'ok' }) 143 | except: 144 | app.logger.info( 'ERROR: Exception in clear_watch_game()') 145 | return jsonify( {'result': 'error: exception in clear_watch_game()' }) 146 | 147 | 148 | @sockets.route('/register_socket/') 149 | #----------------------------------------------- 150 | def register_socket( ws, game_hash): 151 | """ Client js registers to receive live pushes when game progresses """ 152 | app.logger.info( '>>>>>>>>>>>>>>>>> register_socket ' + game_hash) 153 | watchers.register( ws, game_hash) 154 | 155 | while not ws.closed: 156 | # Context switch while `WatcherSockets.start` is running in the background. 157 | gevent.sleep(0.1) 158 | 159 | sockets.url_map.add(Rule('/register_socket/', endpoint=register_socket, websocket=True)) 160 | 161 | #======================= 162 | class WatcherSockets: 163 | """ Class to keep track of all game watchers and their websockets. """ 164 | """ Black Magic involving Redis, Websockets, Python. """ 165 | 166 | #---------------------- 167 | def __init__( self): 168 | self.sockets_by_hash = {} 169 | self.pubsub = redis.pubsub() 170 | self.pubsub.subscribe( REDIS_CHAN) 171 | 172 | #------------------------ 173 | def __iter_data( self): 174 | for message in self.pubsub.listen(): 175 | data = message.get('data') 176 | if message['type'] == 'message': 177 | #app.logger.info('Sending message: {}'.format(data)) 178 | yield data 179 | 180 | #-------------------------------------------- 181 | def register( self, websocket, game_hash): 182 | """ Register a WebSocket connection for Redis updates. """ 183 | if not game_hash in self.sockets_by_hash: 184 | self.sockets_by_hash[game_hash] = [] 185 | print('new game hash ' + game_hash) 186 | self.sockets_by_hash[game_hash].append( websocket) 187 | #print( str(self.sockets_by_hash)) 188 | 189 | #---------------------------------- 190 | def send( self, websocket, data): 191 | """ Send given data to the registered clients. Automatically discards invalid connections. """ 192 | try: 193 | websocket.send( data) 194 | print( 'sent to %s' % str(websocket)) 195 | except Exception: # remove dead sockets 196 | print( 'send failed to %s' % str(websocket)) 197 | for game_hash in self.sockets_by_hash: 198 | for ws in self.sockets_by_hash[game_hash]: 199 | if ws is websocket: 200 | self.sockets_by_hash[game_hash].remove( ws) 201 | 202 | #----------------- 203 | def run( self): 204 | """ Listens for new messages in Redis, and sends them to clients. """ 205 | for data in self.__iter_data(): 206 | msg = data.decode('utf-8') 207 | game_hash = json.loads( data)['game_hash'] 208 | # Send to all who are watching this game 209 | if not game_hash in self.sockets_by_hash: 210 | pass 211 | #app.logger.info( '>>>>>>>>>>>>>>>>> no observers for game ' + str(game_hash)) 212 | else: 213 | app.logger.info( '>>>>>>>>>>>>>>>>> sending to observers for game ' + str(game_hash)) 214 | for ws in self.sockets_by_hash[game_hash]: 215 | gevent.spawn( self.send, ws, data.decode('utf-8')) 216 | 217 | #-------------------- 218 | def start( self): 219 | """ Maintains Redis subscription in the background. """ 220 | gevent.spawn(self.run) 221 | 222 | watchers = WatcherSockets() 223 | watchers.start() 224 | -------------------------------------------------------------------------------- /katago_gui/routes.py: -------------------------------------------------------------------------------- 1 | # /******************************************************************** 2 | # Filename: katago-gui/katago_gui/routes.py 3 | # Author: AHN 4 | # Creation Date: Jul, 2020 5 | # **********************************************************************/ 6 | # 7 | # Template and static file routes 8 | # 9 | from pdb import set_trace as BP 10 | 11 | import os, sys, re, json 12 | 13 | from flask import request, render_template, flash, redirect, url_for, session 14 | from flask_login import login_user, current_user, login_required 15 | from flask_mail import Message 16 | #from itsdangerous import TimedJSONWebSignatureSerializer as Serializer 17 | from itsdangerous.url_safe import URLSafeTimedSerializer as Serializer 18 | 19 | from katago_gui import app, bcrypt, mail, logged_in 20 | from katago_gui import auth 21 | from katago_gui.translations import translate as tr 22 | from katago_gui.forms import LoginForm, RegistrationForm, RequestResetForm, ResetPasswordForm, UpdateAccountForm 23 | from katago_gui.helpers import check_https, login_as_guest, send_reset_email, send_register_email, moves2sgf, moves2arr 24 | 25 | @app.route('/ttest') 26 | #------------------------------- 27 | def ttest(): 28 | """ Try things here """ 29 | # msg = Message('Python test', 30 | # sender='hauensteina@ahaux.com', 31 | # recipients=['hauensteina@gmail.com']) 32 | # msg.body = 'hi there from python' 33 | # ret = mail.send(msg) 34 | return render_template( 'ttest.tmpl', msg='ttest') 35 | 36 | @app.route('/') 37 | @app.route('/index') 38 | @app.route('/home') 39 | #------------------------------- 40 | def index(): 41 | """ Main entry point """ 42 | if not check_https(): return redirect( 'https://katagui.baduk.club') 43 | if not current_user.is_authenticated: login_as_guest() 44 | return render_template( 'index.tmpl', home=True) 45 | 46 | @app.route('/index_mobile') 47 | #------------------------------- 48 | def index_mobile(): 49 | """ Main entry point for mobile devices """ 50 | if not check_https(): return redirect( 'https://katagui.baduk.club') 51 | if not current_user.is_authenticated: login_as_guest() 52 | return render_template( 'index_mobile.tmpl', home=True) 53 | 54 | @app.route('/about') 55 | #------------------------------- 56 | def about(): 57 | return render_template( 'about.tmpl') 58 | 59 | @app.route('/account', methods=['GET', 'POST']) 60 | @login_required 61 | #------------------- 62 | def account(): 63 | UpdateAccountForm.translate() 64 | form = UpdateAccountForm() 65 | if form.validate_on_submit(): 66 | current_user.data['fname'] = form.fname.data.strip().strip() 67 | current_user.data['lname'] = form.lname.data.strip().strip() 68 | current_user.update_db() 69 | flash( tr( 'account_updated'), 'success') 70 | return redirect(url_for('account')) 71 | elif request.method == 'GET': 72 | form.username.data = current_user.data['username'] 73 | form.email.data = current_user.data['email'] 74 | form.fname.data = current_user.data['fname'] 75 | form.lname.data = current_user.data['lname'] 76 | return render_template('account.tmpl', title='Account', form=form) 77 | 78 | @app.route('/export_diagram') 79 | #------------------------------- 80 | def export_diagram(): 81 | """ Export part of the board as a diagram """ 82 | parms = dict(request.args) 83 | stones = parms['stones'] 84 | marks = parms['marks'] 85 | moves = parms['moves'] 86 | moves = moves2arr( moves) 87 | pb = parms['pb'] 88 | pw = parms['pw'] 89 | km = parms['km'] 90 | re = parms['re'] 91 | dt = parms['dt'] 92 | meta = {'pb':pb, 'pw':pw, 'km':km, 're':re, 'dt':dt} 93 | sgf = moves2sgf( moves, [], [], meta) 94 | return render_template( 'export_diagram.tmpl', stones=stones, marks=marks, sgf=sgf) 95 | 96 | @app.route('/favicon.ico') 97 | #------------------------------- 98 | def favicon(): 99 | return app.send_static_file( 'favicon.ico') 100 | 101 | @app.route('/flash_only', methods=['GET']) 102 | #-------------------------------------------------------- 103 | def flash_only(): 104 | return render_template('flash_only.tmpl') 105 | 106 | @app.route('/login', methods=['GET','POST']) 107 | #--------------------------------------------- 108 | def login(): 109 | if logged_in(): 110 | return redirect( url_for('index')) 111 | LoginForm.translate() 112 | form = LoginForm() 113 | if form.validate_on_submit(): 114 | user = auth.User( form.email.data) 115 | if user.valid and user.password_matches( form.password.data): 116 | if not user.email_verified(): 117 | flash(tr('not_verified'), 'danger') 118 | return redirect( url_for('login')) 119 | login_user(user, remember=form.remember.data) 120 | next_page = request.args.get('next') # Magically populated to where we came from 121 | return redirect(next_page) if next_page else redirect(url_for('index')) 122 | else: 123 | flash(tr('login_failed'), 'danger') 124 | res = render_template('login.tmpl', title='Login', form=form) 125 | return res 126 | 127 | @app.route('/register', methods=['GET','POST']) 128 | #------------------------------------------------ 129 | def register(): 130 | if logged_in(): 131 | return redirect(url_for('index')) 132 | RegistrationForm.translate() 133 | form = RegistrationForm() 134 | if form.validate_on_submit(): 135 | formname = form.username.data.strip() 136 | formemail = form.email.data.strip().lower() 137 | user = auth.User( formemail) 138 | if formname.lower().startswith('guest'): 139 | flash( tr('guest_invalid'), 'danger') 140 | return render_template('register.tmpl', title='Register', form=form) 141 | if user.valid: 142 | if user.data['username'] != formname: 143 | flash( tr('account_exists'), 'danger') 144 | return render_template('register.tmpl', title='Register', form=form) 145 | 146 | hashed_password = bcrypt.generate_password_hash(form.password.data).decode('utf-8') 147 | user_data = {'username':form.username.data 148 | ,'fname':form.fname.data 149 | ,'lname':form.lname.data 150 | ,'email_verified':False 151 | } 152 | ret = user.createdb( user_data) 153 | if ret == 'err_user_exists': 154 | user.update_db() 155 | #flash( tr('name_taken'), 'danger') 156 | #return render_template('register.tmpl', title='Register', form=form) 157 | elif ret != 'ok': 158 | flash( tr('err_create_user'), 'danger') 159 | return render_template('register.tmpl', title='Register', form=form) 160 | user.set_password( form.password.data) 161 | send_register_email( user) 162 | flash( tr('email_sent'), 'info') 163 | return redirect(url_for('flash_only')) 164 | return render_template('register.tmpl', title='Register', form=form) 165 | 166 | @app.route('/reset_request', methods=['GET', 'POST']) 167 | #-------------------------------------------------------- 168 | def reset_request(): 169 | if logged_in(): 170 | return redirect( url_for('home')) 171 | RequestResetForm.translate() 172 | form = RequestResetForm() 173 | if form.validate_on_submit(): 174 | user = auth.User( form.email.data) 175 | if not user.valid: 176 | flash( tr('email_not_exists'), 'danger') 177 | return render_template('register.tmpl', title='Register', form=form) 178 | send_reset_email( user) 179 | flash( tr('reset_email_sent'), 'info') 180 | return redirect(url_for('login')) 181 | return render_template('reset_request.tmpl', title='Reset Password', form=form) 182 | 183 | @app.route('/reset_token/', methods=['GET', 'POST']) 184 | #---------------------------------------------------------------- 185 | def reset_token(token): 186 | if logged_in(): 187 | return redirect( url_for('index')) 188 | s = Serializer(app.config['SECRET_KEY']) 189 | user_id = s.loads(token)['user_id'] 190 | lang = s.loads(token)['lang'] 191 | user = auth.User( user_id) 192 | if not user.valid: 193 | flash( tr( 'invalid_token', 'warning')) 194 | return redirect( url_for('reset_request')) 195 | ResetPasswordForm.translate( lang) 196 | form = ResetPasswordForm() 197 | if form.validate_on_submit(): 198 | user.set_password( form.password.data) 199 | flash( tr( 'password_updated'), 'success') 200 | return redirect(url_for('login')) 201 | return render_template('reset_token.tmpl', title='Reset Password', form=form) 202 | 203 | @app.route("/set_mobile", methods=['GET','POST']) 204 | #--------------------------------------------------- 205 | def set_mobile(): 206 | parms = get_parms() 207 | url = parms['url'] 208 | mobile_flag = True if parms['mobile_flag'].lower() == 'true' else False 209 | session['is_mobile'] = mobile_flag 210 | return redirect(url) 211 | 212 | # @app.route('/settings') 213 | # #------------------------------- 214 | # def settings(): 215 | # return render_template( 'settings.tmpl') 216 | 217 | @app.route('/verify_email/', methods=['GET', 'POST']) 218 | #------------------------------------------------------------- 219 | def verify_email(token): 220 | """ User clicked on email verification link. """ 221 | s = Serializer(app.config['SECRET_KEY']) 222 | user_id = s.loads(token)['user_id'] 223 | user = auth.User( user_id) 224 | user.set_email_verified() 225 | flash( tr( 'email_verified'), 'success') 226 | return redirect(url_for('flash_only')) 227 | 228 | #------------------ 229 | def get_parms(): 230 | if request.method == 'POST': # Form submit 231 | parms = dict(request.form) 232 | else: 233 | parms = dict(request.args) 234 | # strip all parameters 235 | parms = { k:v.strip() for k, v in parms.items()} 236 | print(f'>>>>>>>>>PARMS:{parms}') 237 | return parms 238 | 239 | -------------------------------------------------------------------------------- /katago_gui/static/sgf.js: -------------------------------------------------------------------------------- 1 | // SGF main-variation parser for Go (FF[4]) — zero deps 2 | // ------------------------------------------------------- 3 | // Parses only the **main line** (first variation) from an SGF collection. 4 | // Returns a flat array of nodes along the main variation, preserving props. 5 | // 6 | // AHN, Oct 2025 7 | 8 | 'use strict'; 9 | 10 | /** @typedef {{ [k: string]: string[] }} SGFProps */ 11 | /** @typedef {{ props: SGFProps }} SGFNode */ 12 | 13 | //---------------------------------- 14 | export function sgf2list(sgf) { 15 | sgf = svg2sgf(sgf) 16 | sgf = sgf.replace('CoPyright', 'CP') 17 | var RE = getSgfTag(sgf, 'RE') 18 | var winner = '' 19 | if (RE.toLowerCase().startsWith('w')) winner = 'w' 20 | if (RE.length > 10) RE = '' 21 | else if (RE.toLowerCase().startsWith('b')) winner = 'b' 22 | var DT = getSgfTag(sgf, 'DT') 23 | if (DT.length > 15) DT = '' 24 | var player_white = getSgfTag(sgf, 'PW') 25 | var player_black = getSgfTag(sgf, 'PB') 26 | var komi = parseFloat(getSgfTag(sgf, 'KM')) || 0.0 27 | if (komi > 100) { komi = 2 * komi / 100.0 } // Fox anomaly 375 -> 7.5 28 | 29 | var moves = [] 30 | var handicap_setup_done = false 31 | const nodes = parseMainLine(sgf) 32 | for (const [index, n] of nodes.entries()) { 33 | if (n.props.AB || n.props.AW) { 34 | //debugger 35 | addSetupStones(moves, n) 36 | continue 37 | } 38 | var p = '0.00' 39 | var score = '0.00' 40 | var turn = 'B' 41 | if (moves.length % 2) turn = 'W' 42 | let [color, mv] = getMove(n) // ['B', 'Q16'] 43 | if (color) { 44 | if (color != turn) { 45 | moves.push({ 'mv': 'pass', 'p': '0.00', 'score': '0.00' }) 46 | } 47 | if (!mv || mv.length < 2) mv = 'pass' 48 | var move = { 'mv': mv, 'p': '0.00', 'score': '0.00' } 49 | moves.push(move) 50 | } 51 | } // for nodes 52 | const probs = moves.map(m => m.p) 53 | const scores = moves.map(m => m.score) 54 | moves = moves.map(m => m.mv) 55 | const res = { 56 | 'moves': moves, 'probs': probs, 'scores': scores, 57 | 'pb': player_black, 'pw': player_white, 58 | 'winner': winner, 'komi': komi, 'RE': RE, 'DT': DT 59 | } 60 | //debugger 61 | return res 62 | } // sgf2list() 63 | 64 | //--------------------------- 65 | function svg2sgf(tstr) { 66 | // Katagui SVG export embeds SGF in the SVG as a comment. 67 | const matches = tstr.match(/(.*?)<\/katagui>/s); 68 | if (matches) { 69 | const jsonstr = matches[1]; 70 | const meta = JSON.parse(jsonstr); 71 | const sgfstr = meta.sgf; 72 | return sgfstr; 73 | } else { 74 | return tstr; 75 | } 76 | } // svg2sgf(tstr) 77 | 78 | //----------------------------------------------------------------------------------------------- 79 | function getMove(node) { // pq -> ['B|W', 'Q3'] tt -> ['B|W', 'pass'] '' -. -> ['B|W', 'pass'] 80 | let color 81 | if (node.props.B) color = 'B' 82 | else if (node.props.W) color = 'W' 83 | if (!color) return ['', ''] 84 | let point 85 | if (node.props.B && node.props.B.length) { point = node.props.B[0] } 86 | else if (node.props.W && node.props.W.length) { point = node.props.W[0] } 87 | if (point == 'tt' || point == '') { 88 | return [color, 'pass'] 89 | } 90 | return [color, coordsFromPoint(point)] 91 | } // getMove() 92 | 93 | //------------------------------------------- 94 | function coordsFromPoint(p) { // pq -> Q3 95 | const letters = 'ABCDEFGHJKLMNOPQRST' 96 | var col = p[0].toLowerCase().charCodeAt(0) - 'a'.charCodeAt(0) 97 | var row = 19 - (p[1].toLowerCase().charCodeAt(0) - 'a'.charCodeAt(0)) 98 | return `${letters[col]}${row}` 99 | } // coordsFromPoint() 100 | 101 | //----------------------------------------- 102 | function addSetupStones(moves, node) { 103 | let bp = node.props.AB || [] 104 | let wp = node.props.AW || [] 105 | shuffle(bp) 106 | shuffle(wp) 107 | for (let i = 0; i < Math.max(bp.length, wp.length); i++) { 108 | if (i < bp.length) { 109 | moves.push({ 'mv': coordsFromPoint(bp[i]), 'p': '0.00', 'score': '0.00' }) 110 | } else { 111 | moves.push({ 'mv': 'pass', 'p': '0.00', 'score': '0.00' }) 112 | } 113 | if (i < wp.length) { 114 | moves.push({ 'mv': coordsFromPoint(wp[i]), 'p': '0.00', 'score': '0.00' }) 115 | } else { 116 | moves.push({ 'mv': 'pass', 'p': '0.00', 'score': '0.00' }) 117 | } 118 | } // for 119 | // Remove trailing passes from moves 120 | while (moves.length) { 121 | const last = moves[moves.length - 1] 122 | if (last.mv === 'pass') { moves.pop() } else { break } 123 | } // while 124 | } // addSetupStones() 125 | 126 | // Shuffle array in place. Fisher–Yates (Knuth) shuffle. 127 | //----------------------------------------------------------- 128 | function shuffle(array) { 129 | let m = array.length; 130 | let i = 0; 131 | 132 | while (m > 0) { 133 | // Pick a remaining element… 134 | i = Math.floor(Math.random() * m); 135 | m -= 1; 136 | 137 | // And swap it with the current element. 138 | [array[m], array[i]] = [array[i], array[m]]; 139 | } 140 | return array; 141 | } // shuffle() 142 | 143 | //------------------------------------ 144 | function getSgfTag(sgfstr, tag) { 145 | const rexp = new RegExp(`.*${tag}\\[([^\\[]*)\\].*`, 's') 146 | const res = sgfstr.replace(rexp, '$1') 147 | if (res === sgfstr) return '' // tag not found 148 | return res 149 | } // getSgfTag() 150 | 151 | //----------------------------------------- 152 | function parseMainLine(sgf) { 153 | const s = normalizeInput(sgf) 154 | let i = 0 155 | 156 | // Skip to first game-tree 157 | while (i < s.length && s[i] !== '(') i++ 158 | if (i >= s.length) return { nodes: [], collectionCount: 0 } 159 | 160 | let collectionCount = 0 161 | let nodes = [] 162 | 163 | // Parse only the first game-tree's main line 164 | const [treeNodes, nextIdx] = parseGameTreeMain(s, i) 165 | nodes = treeNodes 166 | i = nextIdx 167 | 168 | return nodes 169 | } // parseMainLine() 170 | 171 | // ----------------------- 172 | // Parsing internals 173 | // ----------------------- 174 | //------------------------------- 175 | function normalizeInput(s) { 176 | // Strip unicode Byte Order Mark, unify line endings 177 | if (s.charCodeAt(0) === 0xFEFF) s = s.slice(1) 178 | return s.replace(/\r\n?|\f/g, '\n') 179 | } // normalizeInput() 180 | 181 | //------------------------------------ 182 | function parseGameTreeMain(s, i) { 183 | // grammar: '(' sequence game-tree* ')' 184 | if (s[i] !== '(') throw new Error(`Expected '(' at ${i}`) 185 | i++ 186 | 187 | let nodes = [] 188 | // sequence: ;node ;node ... 189 | while (true) { 190 | i = skipWS(s, i) 191 | if (s[i] === ';') { 192 | const [node, j] = parseNode(s, i + 1) 193 | nodes.push(node) 194 | i = j 195 | continue 196 | } 197 | break 198 | } 199 | // game-tree*: multiple variations — follow only the FIRST one, skip the rest 200 | i = skipWS(s, i) 201 | if (s[i] === '(') { 202 | // parse first child main line 203 | const [childNodes, k] = parseGameTreeMain(s, i) 204 | nodes = nodes.concat(childNodes) 205 | i = k 206 | // skip any additional sibling variations at this level to get to the closing paren 207 | i = skipWS(s, i) 208 | while (s[i] === '(') { 209 | i = skipGameTree(s, i) 210 | i = skipWS(s, i) 211 | } // while 212 | } // if 213 | 214 | if (s[i] !== ')') throw new Error(`Expected ')' at ${i}`) 215 | return [nodes, i + 1] 216 | } // parseGameTreeMain() 217 | 218 | //--------------------------------- 219 | function skipGameTree(s, i) { 220 | // Skip a balanced game-tree starting at '(' without parsing its content 221 | if (s[i] !== '(') throw new Error(`Expected '(' at ${i}`) 222 | let depth = 0 223 | while (i < s.length) { 224 | const ch = s[i++] 225 | if (ch === '(') depth++ 226 | else if (ch === ')') { 227 | depth-- 228 | if (depth === 0) break 229 | } else if (ch === '[') { 230 | // Skip bracket content with escapes 231 | while (i < s.length) { 232 | if (s[i] === '\\') { i += 2; continue } 233 | if (s[i] === ']') { i++; break } 234 | i++ 235 | } 236 | } 237 | } 238 | return i 239 | } // skipGameTree() 240 | 241 | //------------------------------ 242 | function parseNode(s, i) { 243 | // node: one or more properties: IDENT '[' value ']' (value may repeat) 244 | /** @type {SGFProps} */ 245 | const props = {} 246 | while (i < s.length) { 247 | i = skipWS(s, i) 248 | const idStart = i 249 | while (i < s.length && s[i] >= 'A' && s[i] <= 'Z') i++ 250 | if (i === idStart) break // no more properties 251 | const ident = s.slice(idStart, i) 252 | const values = [] 253 | i = skipWS(s, i) 254 | if (s[i] !== '[') { 255 | throw new Error(`Expected '[' after ${ident} at ${i}`) 256 | } 257 | while (s[i] === '[') { 258 | const [val, j] = parseValue(s, i + 1) 259 | values.push(val) 260 | i = j 261 | i = skipWS(s, i) 262 | } 263 | if (props[ident]) { // append to existing 264 | props[ident] = props[ident].concat(values) 265 | } else { 266 | props[ident] = values 267 | } 268 | } // while 269 | return [{ props }, i] 270 | } // parseNode() 271 | 272 | //--------------------------------- 273 | function parseValue(s, i) { 274 | // value content until matching ']' with SGF escapes 275 | let out = '' 276 | while (i < s.length) { 277 | const ch = s[i] 278 | if (ch === ']') return [out, i + 1] 279 | if (ch === '\\') { 280 | // Escape: include next char literally (including newline) 281 | if (i + 1 < s.length) { 282 | const next = s[i + 1] 283 | // SGF line continuation: backslash followed by newline => remove both 284 | if (next === '\n') { i += 2; continue } 285 | out += next 286 | i += 2 287 | continue 288 | } else { 289 | i++ 290 | continue 291 | } 292 | } 293 | out += ch 294 | i++ 295 | } 296 | throw new Error('Unterminated value') 297 | } // parseValue() 298 | 299 | //-------------------------- 300 | function skipWS(s, i) { 301 | while (i < s.length) { 302 | const c = s[i] 303 | if (c === ' ' || c === '\t' || c === '\n' || c === '\r') i++ 304 | else break 305 | } 306 | return i 307 | } // skipWS() 308 | 309 | 310 | 311 | 312 | -------------------------------------------------------------------------------- /katago_gui/helpers.py: -------------------------------------------------------------------------------- 1 | # /******************************************************************** 2 | # Filename: katago-gui/katago_gui/helpers.py 3 | # Author: AHN 4 | # Creation Date: Jul, 2020 5 | # **********************************************************************/ 6 | # 7 | # Various utility functions 8 | # 9 | 10 | from pdb import set_trace as BP 11 | import os, sys, re, uuid 12 | import random 13 | import requests 14 | from datetime import datetime 15 | #from itsdangerous import TimedJSONWebSignatureSerializer as Serializer 16 | from itsdangerous.url_safe import URLSafeTimedSerializer as Serializer 17 | 18 | from flask import request, url_for 19 | from flask_login import current_user, login_user 20 | from flask_mail import Message 21 | 22 | from katago_gui import app, auth, mail, db 23 | from katago_gui import DEMO_KATAGO_SERVER 24 | from katago_gui.go_utils import point_from_coords 25 | from katago_gui.translations import translate as tr 26 | 27 | #------------------------- 28 | def check_https(): 29 | protocol = request.headers.get('X-Forwarded-Proto', 'http') 30 | if protocol != 'https' and 'HEROKU_FLAG' in os.environ: 31 | return False 32 | return True 33 | 34 | #-------------------------------- 35 | def get_sgf_tag( sgfstr, tag): 36 | rexp = r'.*' + tag + r'\[([^\[]*)\].*' 37 | #tstr = sgfstr.decode('utf8') 38 | res = re.sub(rexp, r'\1', sgfstr ,flags=re.DOTALL) 39 | if res == tag: return '' # tag not found 40 | return res 41 | 42 | # Forward fast request to katago server 43 | #---------------------------------------- 44 | def fwd_to_katago( endpoint, args): 45 | return 46 | try: 47 | ip = db.get_parm( 'server_ip') 48 | url = 'http://' + ip + ':2819/' + endpoint 49 | except: 50 | url = DEMO_KATAGO_SERVER + '/' + endpoint 51 | # local testing 52 | #ip = '192.168.0.190' 53 | #url = 'http://' + ip + ':2718/' + endpoint 54 | resp = requests.post( url, json=args) 55 | try: 56 | res = resp.json() 57 | except Exception as e: 58 | print( 'Exception in fwd_to_katago()') 59 | print( 'args %s' % str(args)) 60 | return res 61 | 62 | # Forward strong request to katago server 63 | #------------------------------------------ 64 | def fwd_to_katago_x( endpoint, args): 65 | ports = [3820, 3822] 66 | port = random.choice(ports) 67 | try: 68 | ip = db.get_parm( 'server_ip') 69 | # Locally, use black directly 70 | if not 'HEROKU_FLAG' in os.environ: ip = '10.0.0.137' 71 | url = 'http://' + ip + f':{port}/' + endpoint 72 | except: 73 | url = DEMO_KATAGO_SERVER + '/' + endpoint 74 | # local testing 75 | #ip = '192.168.0.190' 76 | #url = 'http://' + ip + ':2718/' + endpoint 77 | resp = requests.post( url, json=args) 78 | try: 79 | res = resp.json() 80 | except Exception as e: 81 | print( 'Exception in fwd_to_katago_x()') 82 | print( 'args %s' % str(args)) 83 | return res 84 | 85 | #----------------------------------------------------------------- 86 | def fwd_to_katago_x_marfa( endpoint, args): 87 | """ # Forward strong request to katago server on marfa """ 88 | port = 2820 89 | try: 90 | ip = db.get_parm( 'server_ip') 91 | # Locally, use marfa directly 92 | if not 'HEROKU_FLAG' in os.environ: ip = '10.0.0.135' 93 | url = 'http://' + ip + f':{port}/' + endpoint 94 | except: 95 | url = DEMO_KATAGO_SERVER + '/' + endpoint 96 | resp = requests.post( url, json=args) 97 | try: 98 | res = resp.json() 99 | except Exception as e: 100 | print( 'Exception in fwd_to_katago_x_marfa()') 101 | print( 'args %s' % str(args)) 102 | return res 103 | 104 | #--------------------------------------------------------------------- 105 | def fwd_to_katago_xx_marfa( endpoint, args): 106 | """ Forward extra strong request to katago server on marfa """ 107 | port = 2821 108 | try: 109 | ip = db.get_parm( 'server_ip') 110 | # Locally, use marfa directly 111 | if not 'HEROKU_FLAG' in os.environ: ip = '10.0.0.135' 112 | url = 'http://' + ip + f':{port}/' + endpoint 113 | except: 114 | url = DEMO_KATAGO_SERVER + '/' + endpoint 115 | resp = requests.post( url, json=args) 116 | try: 117 | res = resp.json() 118 | except Exception as e: 119 | print( 'Exception in fwd_to_katago_x_marfa()') 120 | print( 'args %s' % str(args)) 121 | return res 122 | 123 | # Forward guest request to katago server 124 | #------------------------------------------ 125 | def fwd_to_katago_guest( endpoint, args): 126 | ports = [3821, 3823] 127 | port = random.choice(ports) 128 | try: 129 | ip = db.get_parm( 'server_ip') 130 | # Locally, use blackstatic 131 | if not 'HEROKU_FLAG' in os.environ: ip = '10.0.0.137' 132 | url = 'http://' + ip + f':{port}/' + endpoint 133 | except: 134 | url = DEMO_KATAGO_SERVER + '/' + endpoint 135 | 136 | resp = requests.post( url, json=args) 137 | try: 138 | res = resp.json() 139 | except Exception as e: 140 | print( 'Exception in fwd_to_katago_guest()') 141 | print( 'args %s' % str(args)) 142 | return res 143 | 144 | # Forward 10b 1 playout request to katago server 145 | #-------------------------------------------------- 146 | def fwd_to_katago_one10( endpoint, args): 147 | try: 148 | ip = db.get_parm( 'server_ip') 149 | # Locally, use blackstatic 150 | if not 'HEROKU_FLAG' in os.environ: ip = '10.0.0.137' 151 | url = 'http://' + ip + ':3801/' + endpoint 152 | except: 153 | url = DEMO_KATAGO_SERVER + '/' + endpoint 154 | 155 | resp = requests.post( url, json=args) 156 | try: 157 | res = resp.json() 158 | except Exception as e: 159 | print( 'Exception in fwd_to_katago_one10()') 160 | print( 'args %s' % str(args)) 161 | return res 162 | 163 | # Forward 9x9 request to katago server 164 | #------------------------------------------ 165 | def fwd_to_katago_9( endpoint, args): 166 | return 167 | try: 168 | ip = db.get_parm( 'server_ip') 169 | url = 'http://' + ip + ':2822/' + endpoint 170 | except: 171 | url = DEMO_KATAGO_SERVER + '/' + endpoint 172 | 173 | resp = requests.post( url, json=args) 174 | try: 175 | res = resp.json() 176 | except Exception as e: 177 | print( 'Exception in fwd_to_katago_9()') 178 | print( 'args %s' % str(args)) 179 | return res 180 | 181 | # Forward 13x13 request to katago server 182 | #------------------------------------------ 183 | def fwd_to_katago_13( endpoint, args): 184 | return 185 | try: 186 | ip = db.get_parm( 'server_ip') 187 | url = 'http://' + ip + ':2823/' + endpoint 188 | except: 189 | url = DEMO_KATAGO_SERVER + '/' + endpoint 190 | 191 | resp = requests.post( url, json=args) 192 | try: 193 | res = resp.json() 194 | except Exception as e: 195 | print( 'Exception in fwd_to_katago_13()') 196 | print( 'args %s' % str(args)) 197 | return res 198 | 199 | # Convert a list of moves like ['Q16',...] to sgf 200 | #--------------------------------------------------- 201 | def moves2sgf( moves, probs, scores, meta): 202 | meta = { k : ('' if v == 'undefined' else v) for (k,v) in meta.items() } 203 | sgf = '(;FF[4]SZ[19]\n' 204 | sgf += 'SO[katagui.baduk.club]\n' 205 | dtstr = meta['dt'] 206 | if not dtstr: dtstr = datetime.now().strftime('%Y-%m-%d') 207 | km = meta['km'] 208 | if not km: km = '7.5' 209 | 210 | sgf += 'PB[%s]\n' % meta['pb'] 211 | sgf += 'PW[%s]\n' % meta['pw'] 212 | sgf += 'RE[%s]\n' % meta['re'] 213 | sgf += 'KM[%s]\n' % km 214 | sgf += 'DT[%s]\n' % dtstr 215 | 216 | movestr = '' 217 | result = '' 218 | color = 'B' 219 | for idx,move in enumerate(moves): 220 | othercol = 'W' if color == 'B' else 'B' 221 | if move == 'resign': 222 | result = 'RE[%s+R]' % othercol 223 | elif move == 'pass': 224 | movestr += ';%s[tt]' % color 225 | elif move == 'A0': 226 | movestr += ';%s[tt]' % color 227 | else: 228 | #BP() 229 | try: 230 | p = point_from_coords( move) 231 | col_s = 'abcdefghijklmnopqrstuvwxy'[p.col - 1] 232 | row_s = 'abcdefghijklmnopqrstuvwxy'[19 - p.row] 233 | movestr += ';%s[%s%s]' % (color,col_s,row_s) 234 | if idx < len(probs): 235 | movestr += 'C[P:%s S:%s]' % (probs[idx], scores[idx]) 236 | except: 237 | print( 'Exception in moves2sgf()') 238 | print( 'move %s' % move) 239 | break 240 | color = othercol 241 | 242 | sgf += result 243 | sgf += movestr 244 | sgf += ')' 245 | return sgf 246 | 247 | #------------------------ 248 | def moves2arr(moves): 249 | """ Q16D4 -> ['Q16','D4'] """ 250 | movearr = [] 251 | m = '' 252 | for c in moves: 253 | if c > '9': # a letter 254 | if m: 255 | movearr.append(m) 256 | m = c 257 | else: 258 | m += c 259 | if m: 260 | movearr.append(m) 261 | return movearr 262 | 263 | #--------------------------- 264 | def login_as_guest(): 265 | """ If we are not logged in, log in as guest """ 266 | print('>>>>>>>>>>>>>>> login guest') 267 | if current_user.is_authenticated:return 268 | print('>>>>>>>>>>>>>>> create guest') 269 | uname = 'guest_' + uuid.uuid4().hex[:7] 270 | email = uname + '@guest.guest' 271 | user = auth.User( email) 272 | user.createdb( { 'fname':'f', 'lname':'l', 'username':uname, 'email_verified':True }) 273 | login_user( user, remember=False) 274 | 275 | #------------------------------- 276 | def send_register_email( user): 277 | ''' User register. Send him an email to verify email address before creating account. ''' 278 | expires_sec = 3600 * 24 * 7 279 | s = Serializer( app.config['SECRET_KEY']) 280 | token = s.dumps( {'user_id': user.id}) 281 | msg = Message('Katagui Email Verification', 282 | sender='hauensteina@ahaux.com', 283 | recipients=[user.data['email']]) 284 | msg.body = f''' 285 | {tr( 'visit_link_activate')} 286 | 287 | {url_for('verify_email', token=token, _external=True)} 288 | 289 | {tr( 'register_ignore')} 290 | ''' 291 | mail.send(msg) 292 | 293 | #------------------------------ 294 | def send_reset_email( user): 295 | ''' User requested a password reset. Send him an email with a reset link. ''' 296 | expires_sec = 3600 * 24 * 7 297 | s = Serializer( app.config['SECRET_KEY']) 298 | token = s.dumps( {'user_id': user.id, 'lang':user.data.get('lang','eng') }) 299 | msg = Message('Password Reset Request', 300 | sender='noreply@ahaux.com', 301 | recipients=[user.data['email']]) 302 | #msg.body = 'hi there testing a reset' 303 | tstr = f''' 304 | {tr( 'visit_link_password')} 305 | 306 | {url_for('reset_token', token=token, _external=True)} 307 | 308 | {tr( 'password_ignore')} 309 | ''' 310 | msg.body = tstr 311 | mail.send(msg) 312 | -------------------------------------------------------------------------------- /katago_gui/translations.py: -------------------------------------------------------------------------------- 1 | 2 | from pdb import set_trace as BP 3 | from flask_login import current_user 4 | 5 | def translate( txt, lang=None): 6 | ''' 7 | Translate txt into the current user's language. 8 | Used in Jinja templates. 9 | ''' 10 | if not lang: 11 | u = current_user 12 | try: 13 | lang = u.data.get( 'lang', 'eng') 14 | except: 15 | lang = 'eng' 16 | try: 17 | tab = _langdict.get( lang, _langdict['eng']) 18 | res = tab.get( txt, txt) 19 | except: 20 | res = txt 21 | if not res: res = txt 22 | return res 23 | 24 | def donation_blurb( mobile): 25 | ''' 26 | Return html asking people for money. 27 | Used in Jinja templates. 28 | ''' 29 | DONATED = 55+26+15+5+21+10+50+10+20+21+5+10+21+30+5+5+25+10+100+20+19+29+21+20+20+31+20+20+10+120+25+25+100+10+20+5+5+36+50+100+20+20 30 | DONATED += 20+20+5+20+10+5+25+130+20+15+10+20 31 | LIMIT = 2000 32 | frac = DONATED / LIMIT 33 | pct = round( 100 * frac) 34 | restpct = str(100 - pct) + '%' 35 | tstr = str(DONATED) + ' / ' + str(LIMIT) + ' ' + translate( 'dollars') 36 | fontsize = '10pt' 37 | height = '20px' 38 | if mobile : 39 | fontsize = '20pt' 40 | height = '40px' 41 | 42 | res = f''' 43 | 48 | 49 | 50 | 55 | 56 | 57 |
44 | {translate( 'donation_blurb')} 45 |

46 | {translate( 'donation_status')} {tstr} 47 |
51 |
52 |
53 | {pct}
54 |
58 | ''' 59 | res = res.replace( "#AMOUNT", str(LIMIT)) 60 | res = res.replace( "#REST", restpct) 61 | return res 62 | 63 | 64 | def get_translation_table(): 65 | ''' The translation table itself, to be sent to the browser. ''' 66 | return _langdict 67 | 68 | _eng = { 69 | # Buttons 70 | 'Play':'AI Play' 71 | # Donation 72 | ,'donation_blurb': "To keep KataGo up and running, we need a dedicated server. A total of #AMOUNT dollars will do it. Only #REST to go! You know you want this. If you donate over 20 dollars, I'll buy you a beer when you visit me in California." 73 | ,'donation_status':'Status (updated daily):' 74 | # Registration dance 75 | ,'visit_link_activate':'To activate your Katagui account, visit the following link:' 76 | ,'register_ignore':'If you did not register, you can safely ignore this email.' 77 | ,'visit_link_password':'To reset your Katagui password, visit the following link:' 78 | ,'password_ignore':'If you did not request a password change, you can safely ignore this email.' 79 | ,'not_verified':'This email has not been verified.' 80 | ,'login_failed':'Login Unsuccessful. Please check email and password.' 81 | ,'account_exists':'An account with this email already exists.' 82 | ,'guest_invalid':'Guest is not a valid username.' 83 | ,'name_taken':'That username is taken.' 84 | ,'err_create_user':'Error creating user.' 85 | ,'email_sent':'An email has been sent to verify your address.
Make sure to check your Spam folder.' 86 | ,'email_verified':'Your email has been verified!
You are now able to log in.' 87 | ,'email_not_exists':'An account with this email does not exist.' 88 | ,'reset_email_sent':'An email has been sent with instructions to reset your password.' 89 | ,'password_updated':'Your password has been updated!
You are now able to log in' 90 | ,'account_updated':'Your account has been updated.' 91 | ,'invalid_token':'That is an invalid or expired token.' 92 | ,'dollars':'dollars' 93 | # Watching 94 | ,'Refresh':'Back to the Game' 95 | } 96 | 97 | _kor = { 98 | # Top Bar 99 | 'Account':'계정' 100 | ,'Account Info':'계정 정보' 101 | ,'Logout':'로그아웃' 102 | ,'Login':'로그인' 103 | ,'Register':'등록' 104 | ,'About':'어바웃' 105 | # Buttons 106 | ,'New Game':'새로운 게임' 107 | ,'Play':'AI 플레이' 108 | ,'Best':'베스트' 109 | ,'Undo':'무르기' 110 | ,'Pass':'패스' 111 | ,'Score':'스코어' 112 | ,'Back':'뒤로' 113 | ,'Back 10':'뒤로 10' 114 | ,'Prev':'이전' 115 | ,'Next':'다음' 116 | ,'Fwd 10':'앞으로 10' 117 | ,'First':'처음' 118 | ,'Last':'마지막' 119 | ,'Save Sgf':'Sgf 파일 저장' 120 | ,'Load Sgf':'Sgf 파일 업로드' 121 | ,'Back to Main Line':'메인으로 돌아가기' 122 | ,'Self Play':'셀프 플레이' 123 | ,'Self Play Speed:':'셀프 플레이 속도:' 124 | ,'Fast':'빠른' 125 | ,'Medium':'매질' 126 | ,'Slow':'느린' 127 | # Starting a Game 128 | ,'Handicap':'접바둑' 129 | ,'Komi':'덤' 130 | ,'Guest (10 blocks)':'게스트 (10 블럭)' 131 | ,'Fast (20 blocks)':'패스트 (20 블럭)' 132 | ,'Strong (40 blocks)':'스트롱 (40 블럭)' 133 | # Login and Registration 134 | ,'Guest':'게스트' 135 | ,'Please Log In':'로그인 해주세요' 136 | ,'Email':'이메일' 137 | ,'Password':'비밀번호' 138 | ,'Request Password Reset':'비밀번호 재설정 요청' 139 | ,'Password':'비밀번호' 140 | ,'Confirm Password':'비밀번호 확인' 141 | ,'Remember Me':'로그인 정보 저장' 142 | ,'Forgot Password?':'비밀번호 복구?' 143 | ,'Need An Account?':'계정이 필요하세요?' 144 | ,'Sign Up Now.':'지금 등록하세요.' 145 | ,'Already Have An Account?':'계정이 있으신가요?' 146 | ,'Sign In.': '로그인 해주세요.' 147 | ,'Username':'사용자 이름' 148 | ,'First Name':'성을 제외한 이름' 149 | ,'Last Name':'성' 150 | ,'Email':'이메일' 151 | ,'Sign Up':'등록' 152 | # Misc 153 | ,'Settings':'셋팅' 154 | ,'Show best 10 (A-J)':'베스트 10 보이기 (A-J)' 155 | ,'Show best moves':'최선의 수 보여주기' 156 | ,'Show emoji':'이모지 표시' 157 | ,'Show probability':'확률 표시' 158 | ,'Enable Diagrams':'다이어그램 활성화' 159 | ,'Disable AI':'AI 비활성화' 160 | ,'Save':'저장' 161 | ,'P(B wins)':'P(흑 기준)' 162 | ,'KataGo resigns. You beat Katago!':'카타고가 불계를 선언했습니다. 카타고에계 승리했습니다!' 163 | ,'KataGo resigns.':'카타고가 불계를 선언했습니다.' 164 | ,'KataGo passes. Click on the Score button.':'카타고가 패스 하였습니다. 스코어 버튼을 클릭해주세요.' 165 | ,'KataGo is thinking ...':'카타고가 생각 중입니다.' 166 | ,'KataGo is counting ...':'카타고가 스코어 중입니다.' 167 | ,'dollars':'달러' 168 | # Donation 169 | ,'donation_blurb': '카타고 운영을 위해 전용 서버를 구축하려면 총 #AMOUNT 달러 정도의 비용이 소모됩니다. 목표 달성까지 #REST가 남았습니다! 카타고 운영을 위해 후원을 고려해주세요. 20달러 이상 후원시 캘리포니아에 방문하면 맥주 한 잔을 사드립니다.' 170 | ,'donation_status':'누적 (하루 1회 업데이트 됨)' 171 | ,'Top Three Donors':'상위 3 명의 기부자' 172 | # Registration dance 173 | ,'visit_link_activate':'Katagui 계정을 활성화하려면 다음 링크를 방문하십시오.' 174 | ,'register_ignore':'등록하지 않은 경우이 이메일을 무시해도됩니다.' 175 | ,'visit_link_password':'Katagui 비밀번호를 재설정하려면 다음 링크를 방문하십시오.' 176 | ,'password_ignore':'이 요청을하지 않은 경우이 이메일을 무시하면 아무런 변화가 없습니다.' 177 | ,'not_verified':'이메일이 활성화 되지 않았습니다.' 178 | ,'login_failed':'정보가 정확하지 않습니다. 이메일과 비밀번호를 확인해주세요.' 179 | ,'account_exists':'이미 등록된 이메일 입니다.' 180 | ,'guest_invalid':'Guest를 사용자 이름으로 사용할 수 없습니다.' 181 | ,'name_taken':'사용자 이름이 이미 존재합니다.' 182 | ,'err_create_user':'계정 생성에 에러 발생.' 183 | ,'email_sent':'귀하의 주소를 확인하는 이메일이 발송되었습니다.' 184 | ,'email_verified':'계정이 활성화 되었습니다. 로그인 할 수 있습니다.' 185 | ,'email_not_exists':'이미 등록된 이메일 입니다.' 186 | ,'reset_email_sent':'비밀번호 복구 이메일이 전송 되었습니다.' 187 | ,'password_updated':'새로운 비밀번호가 저장 되었습니다. 로그인 할 수 있습니다.' 188 | ,'Reset Password':'새로운 비밀번호 설정' 189 | ,'account_updated':'계정이 업데이트 되었습니다.' 190 | ,'invalid_token':'유효하지 않거나 만료 된 토큰입니다' 191 | ,'W':'백' 192 | ,'B':'흑' 193 | ,'Result':'결과' 194 | ,'Date':'날짜' 195 | # Watching games 196 | ,'Games Today':'오늘 대국' 197 | ,'User':'사용자' 198 | ,'Moves':'수' 199 | ,'Idle':'대기중' 200 | ,'Live':'라이브' 201 | ,'Observers':'관전자' 202 | ,'Observe':'관전' 203 | ,'Link':'링크' 204 | ,'Watch':'보기' 205 | ,'Refresh':'재시작' 206 | ,'Type here':'이곳에 입력' 207 | # Search Game 208 | ,'Search':'검색' 209 | ,'Find a Game Played on KataGui':'KataGui에서 기보 찾기' 210 | ,'Started':'시작' 211 | ,'Ended':'종료' 212 | ,'Try Again':'다시 시도' 213 | ,'Game Not Found':'기보를 찾을 수 없음' 214 | ,'KataGo always plays at A':'KataGo는 항상 A에서 플레이합니다' 215 | } 216 | 217 | _chinese = { 218 | # Top Bar 219 | 'Account':'我的帐号' 220 | ,'Account Info':'帐号个人信息' 221 | ,'Logout':'退出登录' 222 | ,'Login':'登录' 223 | ,'Register':'注册' 224 | ,'About':'关于KataGui' 225 | # Buttons 226 | ,'New Game':'新对局' 227 | ,'Play':'AI落子' 228 | ,'Best':'最佳推荐' 229 | ,'Undo':'悔棋' 230 | ,'Pass':'停一手' 231 | ,'Score':'点目' 232 | ,'Back':'返回' 233 | ,'Back 10':'上10手' 234 | ,'Prev':'上一手' 235 | ,'Next':'下一手' 236 | ,'Fwd 10':'下10手' 237 | ,'First':'始' 238 | ,'Last':'终' 239 | ,'Save Sgf':'保存SGF' 240 | ,'Load Sgf':'载入' 241 | ,'Back to Main Line':'返回主分支' 242 | ,'Self Play':'自动落子' 243 | ,'Self Play Speed:':'自动落子速度:' 244 | ,'Fast':'快' 245 | ,'Medium':'中' 246 | ,'Slow':'慢' 247 | # Starting a Game 248 | ,'Handicap':'让子' 249 | ,'Komi':'贴目' 250 | ,'Guest (10 blocks)':'预览版 (10b)' 251 | ,'Fast (20 blocks)':'轻巧版 (20b)' 252 | ,'Strong (40 blocks)':'强力版 (40b)' 253 | # Login and Registration 254 | ,'Guest':'游客' 255 | ,'Please Log In':'请登录' 256 | ,'Email':'电子邮箱' 257 | ,'Password':'密码' 258 | ,'Request Password Reset':'请求重置密码' 259 | ,'Password':'密码' 260 | ,'Confirm Password':'确认密码' 261 | ,'Remember Me':'记住我' 262 | ,'Forgot Password?':'忘记密码?' 263 | ,'Need An Account?':'需要帐号?' 264 | ,'Sign Up Now.':'现在就可以注册。' 265 | ,'Already Have An Account?':'已经有了帐号?' 266 | ,'Sign In.': '请登录。' 267 | ,'Username':'用户名' 268 | ,'First Name':'名字' 269 | ,'Last Name':'姓氏' 270 | ,'Sign Up':'注册新帐号' 271 | # Misc 272 | ,'Settings':'设置' 273 | ,'Show best 10 (A-J)':'显示最佳的10点 (A-J)' 274 | ,'Show emoji':'显示表情' 275 | ,'Show probability':'显示概率' 276 | ,'Show best moves':'显示最佳棋步' 277 | ,'Enable Diagrams':'启用图表' 278 | ,'Disable AI':'禁用AI' 279 | ,'Save':'保存' 280 | ,'P(B wins)':'黑方胜率' 281 | ,'KataGo resigns. You beat Katago!':'KataGo认输了。你击败了KataGo!' 282 | ,'KataGo resigns.':'KataGo认输了。' 283 | ,'KataGo passes. Click on the Score button.':'KataGo停了一手,请开始数目。' 284 | ,'KataGo is thinking ...':'KataGo正在思考中...' 285 | ,'KataGo is counting ...':'KataGo正在计算中...' 286 | ,'dollars':'美元' 287 | # Donation 288 | ,'donation_blurb': '为了长期维持KataGo的运行,我们需要购买专用的服务器。现在距离 #AMOUNT 美元的目标只剩 #REST 了!希望您也能帮助我们实现这个愿望。如果您的捐助超过20美元,您下次来加州拜访我时我一定会热情款待!' 289 | ,'donation_status':'目前筹得捐款(每日更新):' 290 | ,'Top Three Donors':'前三名捐助者' 291 | # Registration dance 292 | ,'visit_link_activate':'请点击以下链接激活您的KataGui帐号:' 293 | ,'register_ignore':'如果您没有注册,请放心忽略这封邮件' 294 | ,'visit_link_password':'请点击以下链接重置您的KataGui密码:' 295 | ,'password_ignore':'如果您没有请求重置密码,请放心忽略这封邮件' 296 | ,'not_verified':'本电子邮箱尚未被验证' 297 | ,'login_failed':'登录失败,请检查您的电子邮箱或密码是否正确' 298 | ,'account_exists':'此电子邮箱已经被另一帐号注册' 299 | ,'guest_invalid':'用户名不能为Guest' 300 | ,'name_taken':'该用户名已被注册' 301 | ,'err_create_user':'创建用户时出错' 302 | ,'email_sent':'我们已向您的电子邮箱发送了一封确认邮件。
如未收到,请检查您的垃圾邮件箱。' 303 | ,'email_verified':'电子邮箱验证成功!
现在您可以登录了。' 304 | ,'email_not_exists':'不存在该电子邮箱相关的帐户。' 305 | ,'reset_email_sent':'我们已发送了一封邮件,其中包含重置密码的具体步骤。' 306 | ,'password_updated':'密码重置成功!
现在您可以登录了。' 307 | ,'account_updated':'帐号更新成功。' 308 | ,'invalid_token':'token不存在或已失效' 309 | ,'W':'白' 310 | ,'B':'黑' 311 | ,'Result':'结果' 312 | ,'Date':'日期' 313 | # Watching games 314 | ,'Games Today':'本日对局' 315 | ,'User':'用户' 316 | ,'Moves':'手数' 317 | ,'Idle':'空闲' 318 | ,'Live':'进行中' 319 | ,'Observers':'观众人数' 320 | ,'Observe':'进入' 321 | ,'Link':'链接' 322 | ,'Watch':'观战' 323 | ,'Refresh':'回到棋局' 324 | #,'Refresh':'刷新' 325 | ,'Type here':'在此输入...' 326 | # Search Game 327 | ,'Search':'搜索' 328 | ,'Find a Game Played on KataGui':'寻找KataGui上的对局' 329 | ,'Started':'已开始' 330 | ,'Ended':'已结束' 331 | ,'Try Again':'重试' 332 | ,'Game Not Found':'找不到对局' 333 | ,'KataGo always plays at A':'KataGo 总是在 A 处比赛' 334 | 335 | } 336 | 337 | _japanese = { 338 | # Top Bar 339 | 'Account':'アカウント' 340 | ,'Account Info':'アカウント情報' 341 | ,'Logout':'ログアウト' 342 | ,'Login':'ログイン' 343 | ,'Register':'登録' 344 | ,'About':'KataGoについて' 345 | # Buttons 346 | ,'New Game':'新規対局' 347 | ,'Play':'AI' 348 | ,'Best':'最善手' 349 | ,'Undo':'取り消し' 350 | ,'Pass':'パス' 351 | ,'Score':'形勢' 352 | ,'Back 10':'10手戻る' 353 | ,'Back':'戻る' 354 | ,'Prev':'1手戻る' 355 | ,'Next':'次の手' 356 | ,'Fwd 10':'10手進む' 357 | ,'First':'初手' 358 | ,'Last':'最終手' 359 | ,'Save Sgf':'SGFで保存' 360 | ,'Load Sgf':'SGFを読込み' 361 | ,'Back to Main Line':'本戦図に戻る' 362 | ,'Self Play':'自動対局' 363 | ,'Self Play Speed:':'自動対局スピード' 364 | ,'Fast':'早い' 365 | ,'Medium':'中' 366 | ,'Slow':'遅い' 367 | # Starting a Game 368 | ,'Handicap':'置き石' 369 | ,'Komi':'コミ' 370 | ,'Guest (10 blocks)':'ゲスト (10 blocks)' 371 | ,'Fast (20 blocks)':'早い(20 blocks)' 372 | ,'Strong (40 blocks)':'強い(40 blocks)' 373 | # Login and Registration 374 | ,'Guest':'ゲスト' 375 | ,'Please Log In':'ログインしてください' 376 | ,'Email':'Eメール' 377 | ,'Password':'パスワード' 378 | ,'Request Password Reset':'パスワードのリセットを申請する' 379 | ,'Password':'パスワード' 380 | ,'Confirm Password':'パスワードを確認' 381 | ,'Remember Me':'記憶する' 382 | ,'Forgot Password?':'パスワードを忘れましたか?' 383 | ,'Need An Account?':'アカウントを作りますか?' 384 | ,'Sign Up Now.':'すぐ加入する' 385 | ,'Already Have An Account?':'もうアカウントをお持ちの場合' 386 | ,'Sign In.': 'サインインする' 387 | ,'Username':'ユーザーネーム' 388 | ,'First Name':'名' 389 | ,'Last Name':'姓' 390 | ,'Sign Up':'サインアップ' 391 | # Misc 392 | ,'Settings':'設定' 393 | ,'Show best 10 (A-J)':'最善手を10個まで表示する' 394 | ,'Show emoji':'絵文字表示' 395 | ,'Show probability':'確率表示' 396 | ,'Show best moves':"最善手を表示する" 397 | ,'Enable Diagrams':'ダイアグラムを有効にする' 398 | ,'Disable AI':'AIを無効にする' 399 | ,'Save':'保存する' 400 | ,'P(B wins)':'勝率(黒の勝ち)' 401 | ,'KataGo resigns. You beat Katago!':'KataGoが投了しました。貴方の勝ちです' 402 | ,'KataGo resigns.':'KataGoが投了しました' 403 | ,'KataGo passes. Click on the Score button.':'KataGoがパスしました。形勢ボタンを押してください' 404 | ,'KataGo is thinking ...':'KataGoが検討中です' 405 | ,'KataGo is counting ...':'KataGoが計算中です' 406 | ,'dollars':'ドル' 407 | # Donation 408 | ,'donation_blurb': 'KataGoを維持運営して行くには、専用のサーバーが必要です。それに掛かる費用は#AMOUNTドルです。あと#REST不足しています。寄付をお願いします。20ドル以上、ご協力いただけた方には、カリフォルニアにおいでの際に、ビールを一杯ごちそうします。' 409 | ,'donation_status':'現状(毎日更新):' 410 | ,'Top Three Donors':'寄付金トップ3' 411 | # Registration dance 412 | ,'visit_link_activate':'登録を有効にするためにリンクをクリックしてください' 413 | ,'register_ignore':'登録を無視する' 414 | ,'visit_link_password':'リンクを押してパスワードを更新' 415 | ,'password_ignore':'パスワードを無視する' 416 | ,'not_verified':'確認できませんでした' 417 | ,'login_failed':'ログインに失敗しました' 418 | ,'account_exists':'そのアカウント名は既に存在しています' 419 | ,'guest_invalid':'そのゲスト名は無効です' 420 | ,'name_taken':'その名前は既に取得されています' 421 | ,'err_create_user':'ユーザー作成エラー' 422 | ,'email_sent':'Eメールが送られました' 423 | ,'email_verified':'Eメールが確認されました' 424 | ,'email_not_exists':'Eメールが存在しません' 425 | ,'reset_email_sent':'リセットのEメールが送られました' 426 | ,'password_updated':'パスワードが更新されました' 427 | ,'account_updated':'アカウントが更新されました' 428 | ,'invalid_token':'無効な識別子です' 429 | ,'W':'白' 430 | ,'B':'黒' 431 | ,'Result':'結果' 432 | ,'Date':'日付' 433 | # Watching games 434 | ,'Games Today':'今日の対局' 435 | ,'User':'ユーザー' 436 | ,'Moves':'手数' 437 | ,'Idle':'秒読み' 438 | ,'Live':'中継中' 439 | ,'Observers':'観戦者数' 440 | ,'Observe':'観戦する' 441 | ,'Link':'リンク' 442 | ,'Watch':'観戦する' 443 | # Back to the Game 444 | ,'Refresh':'再読み込み' 445 | ,'Type here':'ここに入力' 446 | # Search Game 447 | ,'Search':'検索する' 448 | ,'Find a Game Played on KataGui':'KataGuiで対局したゲームを見つける' 449 | ,'Started':'開始しました' 450 | ,'Ended':'終了しました' 451 | ,'Try Again':'もう一度' 452 | ,'Game Not Found':'対局が見つかりません' 453 | ,'KataGo always plays at A':'KataGoは常にAでプレイします' 454 | } 455 | 456 | _langdict = { 'eng':_eng, 'kor':_kor, 'chinese':_chinese, 'japanese':_japanese } 457 | --------------------------------------------------------------------------------