Skip to content

Commit 364e5af

Browse files
committed
support score-at-point in client
1 parent bf466c1 commit 364e5af

3 files changed

Lines changed: 112 additions & 10 deletions

File tree

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,11 @@ of each game starting with an `[Event "?"]` tagpair.
115115

116116
* `pygn-mode-dependency-check` — check Python and python-chess dependencies
117117

118+
### Engine Commands
119+
120+
* `pygn-mode-display-score-at-point` — display engine evaluation corresponding to the position of the point
121+
* `pygn-mode-display-variation-score-at-point` — display engine evaluation respecting variations
122+
118123
## Minor Mode
119124

120125
Enabling `pygn-mode-follow-minor-mode` causes a GUI board to be displayed and

pygn-mode.el

Lines changed: 100 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,16 @@
149149
:group 'pygn-mode
150150
:type 'boolean)
151151

152+
(defcustom pygn-mode-engine-executable "stockfish"
153+
"Path to a UCI engine executable."
154+
:group 'pygn-mode
155+
:type 'string)
156+
157+
(defcustom pygn-mode-default-engine-depth 20
158+
"Default depth for engine analysis."
159+
:group 'pygn-mode
160+
:type 'int)
161+
152162
;;;###autoload
153163
(defgroup pygn-mode-faces nil
154164
"Faces used by pygn-mode."
@@ -200,6 +210,9 @@
200210
(defvar pygn-mode-dependency-check-buffer-name "*pygn-mode-dependency-check*"
201211
"Buffer name used to display a dependency check.")
202212

213+
(defvar pygn-mode-score-buffer-name "*pygn-mode-score*"
214+
"Buffer name used to engine scores.")
215+
203216
(defvar pygn-mode--server-process nil
204217
"Python-based server which powers many `pygn-mode' features.")
205218

@@ -215,6 +228,9 @@
215228
(defvar pygn-mode--server-receive-max-seconds 0.5
216229
"The maximum amount of time `pygn-mode--server-receive' should check the server for output when polling.")
217230

231+
(defvar pygn-mode--server-receive-engine-max-seconds 60
232+
"The maximum amount of time `pygn-mode--server-receive' should check the server for output when polling for an engine response.")
233+
218234
;;; Syntax table
219235

220236
(defvar pygn-mode-syntax-table
@@ -281,6 +297,9 @@
281297
'(menu-item "Next Move" pygn-mode-next-move
282298
:help "Navigate to the next move"))
283299
(define-key map [menu-bar PyGN sep-2] menu-bar-separator)
300+
(define-key map [menu-bar PyGN pygn-mode-display-score-at-point]
301+
'(menu-item "Score at point" pygn-mode-display-score-at-point
302+
:help "Display engine score at point in separate window"))
284303
(define-key map [menu-bar PyGN pygn-mode-display-fen-at-point]
285304
'(menu-item "FEN at Point" pygn-mode-display-fen-at-point
286305
:help "Display FEN at point in separate window"))
@@ -409,36 +428,49 @@ data payload, and PAYLOAD may contain arbitrary data."
409428
payload)
410429
" ")))
411430

412-
(defun pygn-mode--server-receive ()
431+
(defun pygn-mode--server-receive (needs-engine)
413432
"Receive a response after `pygn-mode--server-send'.
414433
415-
Respects the variables `pygn-mode--server-receive-every-seconds' and
416-
`pygn-mode--server-receive-max-seconds'."
434+
When NEEDS-ENGINE is non-nil, wait longer, and show a progress reporter.
435+
436+
Respects the variables `pygn-mode--server-receive-every-seconds',
437+
`pygn-mode--server-receive-max-seconds', and
438+
`pygn-mode--server-receive-engine-max-seconds'."
417439
(unless (pygn-mode--server-running-p)
418440
(error "The pygn-mode server is not running -- cannot receive a response"))
419441
(unless (get-buffer pygn-mode--server-buffer)
420442
(error "The pygn-mode server output buffer does not exist -- cannot receive a response"))
421443
(with-current-buffer pygn-mode--server-buffer
422444
(erase-buffer)
423-
(let ((tries 0))
445+
(let ((tries 0)
446+
(reporter (when needs-engine (make-progress-reporter "Waiting for engine"))))
424447
(goto-char (point-min))
425448
(while (and (not (eq ?\n (char-before (point-max))))
426-
(< (* tries pygn-mode--server-receive-every-seconds) pygn-mode--server-receive-max-seconds))
449+
(< (* tries pygn-mode--server-receive-every-seconds)
450+
(if needs-engine pygn-mode--server-receive-engine-max-seconds pygn-mode--server-receive-max-seconds)))
427451
(accept-process-output pygn-mode--server-process pygn-mode--server-receive-every-seconds nil 1)
428-
(cl-incf tries))
452+
(cl-incf tries)
453+
(when needs-engine
454+
(progress-reporter-update reporter)
455+
(sit-for 0)))
456+
(when needs-engine
457+
(progress-reporter-done reporter))
429458
(buffer-substring-no-properties (point-min) (point-max)))))
430459

431460
(cl-defun pygn-mode--server-query (&key
432461
command
433462
options
434463
payload-type
435464
payload
465+
needs-engine
436466
force)
437467
"Send a request to `pygn-mode--server-process', await, and return the response.
438468
439469
COMMAND, OPTIONS, PAYLOAD-TYPE, and PAYLOAD are as documented at
440470
`pygn-mode--server-send'.
441471
472+
NEEDS-ENGINE is as documented at `pygn-mode--server-receive'.
473+
442474
FORCE forces a new server process to be created."
443475
(unless (pygn-mode--server-running-p)
444476
(pygn-mode--server-start force))
@@ -447,7 +479,7 @@ FORCE forces a new server process to be created."
447479
:options options
448480
:payload-type payload-type
449481
:payload payload)
450-
(pygn-mode--server-receive))
482+
(pygn-mode--server-receive needs-engine))
451483

452484
(defun pygn-mode--parse-response (response)
453485
"Parse RESPONSE string into a list of payload-id and payload."
@@ -602,13 +634,15 @@ Does not work for nested variations."
602634
(cl-defun pygn-mode--send-pgn-and-fetch (&key
603635
command
604636
options
637+
needs-engine
605638
pos)
606639
"Get PGN string preceding POS, send a `pygn-mode--server-process' request denoted by COMMAND, and return the response."
607640
(cl-callf or pos (point))
608641
(save-excursion
609642
(let ((pgn (buffer-substring-no-properties (pygn-mode-game-start-position) pos)))
610643
(pygn-mode--server-query
611644
:command command
645+
:needs-engine needs-engine
612646
:options options
613647
:payload-type :pgn
614648
:payload pgn))))
@@ -648,6 +682,19 @@ Does not work for nested variations."
648682

649683
;;; Font-lock
650684

685+
(defun pygn-mode-score-at-pos (pos &optional depth)
686+
"Get engine score for PGN string preceding POS."
687+
(let ((response (pygn-mode--send-pgn-and-fetch
688+
:needs-engine t
689+
:command :pgn-to-score
690+
:options `(:engine ,pygn-mode-engine-executable
691+
:depth ,(or depth pygn-mode-default-engine-depth))
692+
:pos pos)))
693+
(cl-callf pygn-mode--parse-response response)
694+
(unless (eq :score (car response))
695+
(error "Bad response from `pygn-mode' server"))
696+
(cadr response)))
697+
651698
(defun pygn-mode-after-change-function (beg end old-len)
652699
"Help refontify multi-line variations during edits."
653700
(let ((syn (syntax-ppss beg)))
@@ -838,6 +885,14 @@ Recenters buffer afterwards."
838885
"[ ] Bad. The executable '%s' cannot import the python-chess library. Try installing python-chess, and/or customizing the value of pygn-mode-pythonpath.\n\n"
839886
pygn-mode-python-executable))
840887
(cl-return-from pygn-mode-dependency-check))
888+
(if (executable-find pygn-mode-engine-executable)
889+
(insert (format "[x] Good. The pygn-mode-engine-executable exists at '%s'\n\n" (executable-find pygn-mode-engine-executable)))
890+
;; else
891+
(insert
892+
(format
893+
"[ ] Bad. The engine executable '%s' cannot be found. Try installing a chess engine and/or customizing the value of pygn-mode-engine-executable.\n\n"
894+
pygn-mode-engine-executable))
895+
(cl-return-from pygn-mode-dependency-check))
841896
(insert (format "------------------------------------\n\n"))
842897
(insert (format "All pygn-mode dependencies verified.\n")))))
843898

@@ -995,6 +1050,44 @@ When called non-interactively, display the board corresponding to POS."
9951050
(insert pgn)
9961051
(pygn-mode-display-fen-at-point (point-max)))))
9971052

1053+
(defun pygn-mode-display-score-at-point (pos &optional arg)
1054+
"Display an engine score corresponding to the point in a separate buffer.
1055+
1056+
When called non-interactively, display the score corresponding to POS.
1057+
1058+
With optional universal prefix ARG, prompt for the desired engine depth.
1059+
With optional numeric prefix ARG, ARG specifies the desired depth.
1060+
1061+
The UCI engine is defined by `pygn-mode-engine-executable'. The
1062+
default depth is defined by `pygn-mode-default-engine-depth'."
1063+
(interactive "d\nP")
1064+
(let* ((depth (cond
1065+
((numberp arg) arg)
1066+
(arg (completing-read "Depth: " nil))
1067+
(t pygn-mode-default-engine-depth)))
1068+
(score (pygn-mode-score-at-pos pos depth))
1069+
(buf (get-buffer-create pygn-mode-score-buffer-name))
1070+
(win (get-buffer-window buf)))
1071+
(with-current-buffer buf
1072+
(erase-buffer)
1073+
(insert score)
1074+
(goto-char (point-min))
1075+
(display-buffer buf '(display-buffer-reuse-window))
1076+
(unless win
1077+
(setq win (get-buffer-window buf))
1078+
(set-window-dedicated-p win t)
1079+
(resize-temp-buffer-window win)))))
1080+
1081+
(defun pygn-mode-display-variation-score-at-point (pos)
1082+
"Respecting variations, display and engine score corresponding to the point.
1083+
1084+
When called non-interactively, display the score corresponding to POS."
1085+
(interactive "d")
1086+
(let ((pgn (pygn-mode-pgn-as-if-variation pos)))
1087+
(with-temp-buffer
1088+
(insert pgn)
1089+
(pygn-mode-display-score-at-point (point-max)))))
1090+
9981091
;; interactive helper
9991092
(defun pygn-mode--save-gui-board-at-point (pos)
10001093
"Save the board image corresponding to POS to a file."

pygn_server.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,11 @@
4949

5050
def instantiate_engine(engine_path):
5151
if not engine_path in ENGINES:
52-
ENGINES[engine_path] = chess.engine.SimpleEngine.popen_uci(engine_path)
52+
ENGINES[engine_path] = chess.engine.SimpleEngine.popen_uci(engine_path, timeout=None)
53+
try:
54+
ENGINES[engine_path].ping()
55+
except:
56+
ENGINES[engine_path] = chess.engine.SimpleEngine.popen_uci(engine_path, timeout=None)
5357
return ENGINES[engine_path]
5458

5559
def cleanup():
@@ -171,8 +175,8 @@ def generate_argparser():
171175
argparser.add_argument('-depth', '--depth',
172176
nargs=1,
173177
type=int,
174-
default=[10],
175-
help='set depth for depth-limited to UCI evaluations. Default is 10.')
178+
default=[20],
179+
help='set depth for depth-limited to UCI evaluations. Default is 20.')
176180
return argparser
177181

178182
###

0 commit comments

Comments
 (0)