76 |
77 | {% else %}
78 |
79 |
80 |
81 | |   |
82 |
83 |
84 | KataGui 0.0.0
85 | |
86 |
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 | |
99 | |
100 |
101 |
102 | |
103 |
104 |
105 |
106 |
107 |
121 | {% if logged_in() %}
122 |
124 | {{ current_user.data["username"] }}
125 |
126 | {% else %}
127 |
129 | {{tr('Guest')}}
130 |
131 | {% endif %}
132 |
133 | |
134 | |
135 |
136 |
137 | |
138 | {% endif %}
139 | {% block content %}
140 | {% endblock content %}
141 | |
142 |
143 |
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 | |
44 | {translate( 'donation_blurb')}
45 | |
46 | {translate( 'donation_status')} {tstr}
47 | |
48 |
49 | |
50 |
51 |
54 | |
55 | |
56 |
57 |
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 |
--------------------------------------------------------------------------------
|