1 ;;; chess-ics.el --- Play on Internet Chess Servers
3 ;; Copyright (C) 2002, 2003, 2004, 2014 Free Software Foundation, Inc.
5 ;; Author: John Wiegley
6 ;; Maintainer: Mario Lang <mlang@delysid.org>
7 ;; Keywords: games, processes
9 ;; This program is free software; you can redistribute it and/or modify
10 ;; it under the terms of the GNU General Public License as published by
11 ;; the Free Software Foundation, either version 3 of the License, or
12 ;; (at your option) any later version.
14 ;; This program is distributed in the hope that it will be useful,
15 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
16 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 ;; GNU General Public License for more details.
19 ;; You should have received a copy of the GNU General Public License
20 ;; along with this program. If not, see <http://www.gnu.org/licenses/>.
24 ;; This module allows to play chess on an Internet Chess Server.
25 ;; Contrary to other chess engine modules for chess.el, you are not supposed to
26 ;; use `chess-ics' as an engine for `M-x chess', rather, you call
27 ;; `M-x chess-ics' directly to play chess on the internet.
29 ;; The two major Internet Chess Servers, freechess.org and chessclub.com
30 ;; are both supported. See `chess-ics-server-list' for the supported servers.
38 (require 'chess-network)
45 (defgroup chess-ics nil
46 "Engine for interacting with Internet Chess Servers."
49 (defcustom chess-ics-server-list '(("freechess.org" 5000)
50 ("chess.unix-ag.uni-kl.de" 5000)
51 ("chessclub.com" 5000)
53 ("oics.olympuschess.com" 5000))
54 "A list of servers to connect to.
55 The format of each entry is:
57 (SERVER PORT [HANDLE] [PASSWORD-OR-FILENAME] [HELPER] [HELPER ARGS...])"
58 :type '(repeat (list (string :tag "Server")
60 (choice (const :tag "Login as guest" nil)
61 (string :tag "Handle"))
62 (choice (const :tag "No password or ask" nil)
63 (string :tag "Password")
64 (file :tag "Filename"))
65 (choice (const :tag "Direct connection" nil)
66 (file :tag "Command"))
67 (choice (const :tag "No arguments" nil)
73 (defcustom chess-ics-initial-commands
76 "iset defprompt 1" ; So we can't be supprised by a user setting
77 (format "set interface emacs-chess %s" chess-version)
78 "iset seekremove 1" ; For real-time sought display
79 "iset startpos 1" ; Sends initial position before movelist
80 "set style 12" ; So we can parse the board "easily"
81 "set bell 0") ; We have our own way of announcing events
83 (format "/set-quietly interface emacs-chess %s" chess-version)
84 "/set-quietly style 12" ; So we can parse the board "easily"
85 "/set-quietly bell 0")
87 (format "set interface emacs-chess %s" chess-version)
88 "set style 12" ; So we can parse the board "easily"
90 "A list of commands to send automatically upon successful login.
91 The format is (SERVER COMMANDS...) where SERVER is either the server-name
92 \(see `chess-ics-server-list') or nil, which is the default to use for all
93 servers which do not have a specialized entry in this list. COMMAND is a
94 string which should be sent (newline characters will be added automatically.)"
97 (list :tag "Initialisation for"
98 (choice (string :tag "Server Name") (const :tag "Default" nil))
99 (repeat :inline t (string :tag "Command")))))
101 (defcustom chess-ics-prompt-regexp "\\(?:[0-2][0-9]:[0-6][0-9]_\\)?[af]ics% $"
102 "*Regexp which matches an ICS prompt."
106 (defvar chess-ics-server nil
107 "The ICS server name of this connection.")
108 (make-variable-buffer-local 'chess-ics-server)
110 (defvar chess-ics-handle nil
111 "The ICS handle of this connection.")
112 (make-variable-buffer-local 'chess-ics-handle)
114 (defvar chess-ics-password nil
115 "Password to use to identify to the server.")
116 (make-variable-buffer-local 'chess-ics-password)
118 (defvar chess-ics-handling-login nil
119 "Non-nil if we are currently handling the ICS login sequence.")
120 (make-variable-buffer-local 'chess-ics-handling-login)
122 (defvar chess-ics-server-type 'FICS
123 "The type of chss server we are about to connect too.
124 Possible values are currently FICS (the default, and best supported)
126 (make-variable-buffer-local 'chess-ics-server-type)
128 (defcustom chess-ics-icc-datagrams '(22 23 26 33 50 51 56 110 111)
129 "*A list of datagrams to request when connecting to ICC."
131 :type '(repeat (choice (const :tag "DG_SEND_MOVES" 24)
132 (const :tag "DG_KIBITZ" 26)
133 (const :tag "DG_MOVE_ALGEBRAIC" 33)
134 (const :tag "DG_SEEK" 50)
135 (const :tag "DG_SEEK_REMOVED" 51)
136 (const :tag "DG_MSEC" 56)
137 (const :tag "DG_POSITION_BEGIN" 101)
138 (const :tag "DG_POSITION_BEGIN2" 110)
139 (const :tag "DG_PAST_MOVE" 111))))
141 (defvar chess-ics-movelist-game-number nil
142 "If we are about to receive a movelist, this variable is set to the
144 (make-variable-buffer-local 'chess-ics-movelist-game-number)
146 (defvar chess-ics-movelist-game nil
147 "If we are receiving a movelist, this variable is set to the game object.")
148 (make-variable-buffer-local 'chess-ics-movelist-game)
150 (defvar chess-ics-movelist-start-position chess-starting-position
151 "The starting position to use upon receiving of a movelist.
152 It is possible to configure certain servers to automatically send a
153 style12 board before sending a movelist, to allow retrieval of
154 the movelist for a non-standard game (one which does not start at the
155 standard position). In those cases, this variable should be set to nil.")
156 (make-variable-buffer-local 'chess-ics-movelist-start-position)
158 (defsubst chess-ics-send (string &optional buffer)
159 "Send STRING to the ICS server."
160 (comint-send-string (get-buffer-process (or buffer (current-buffer)))
161 (concat string "\n")))
163 (chess-message-catalog 'english
164 '((ics-server-prompt . "Connect to chess server: ")
165 (ics-connecting . "Connecting to Internet Chess Server '%s'...")
166 (ics-connected . "Connecting to Internet Chess Server '%s'...done")
167 (ics-anon-login . "Logging in on Internet Chess Server '%s' as anonymous user...")
168 (ics-logging-in . "Logging in on Internet Chess Server '%s' as '%s'...")
169 (ics-logged-in . "Logging in on Internet Chess Server '%s' as '%s'...done")
170 (challenge-whom . "Whom would you like challenge? ")
171 (failed-ics-parse . "Failed to parse ICS move string (%s): ")))
173 (defconst chess-ics-style12-regexp
175 (group (repeat 8 (in "-pnbrqkPNBRQK"))) " "
176 (group (repeat 8 (in "-pnbrqkPNBRQK"))) " "
177 (group (repeat 8 (in "-pnbrqkPNBRQK"))) " "
178 (group (repeat 8 (in "-pnbrqkPNBRQK"))) " "
179 (group (repeat 8 (in "-pnbrqkPNBRQK"))) " "
180 (group (repeat 8 (in "-pnbrqkPNBRQK"))) " "
181 (group (repeat 8 (in "-pnbrqkPNBRQK"))) " "
182 (group (repeat 8 (in "-pnbrqkPNBRQK"))) " "
183 (group (in "BW")) " "
184 (group (and (? ?-) (in "0-7"))) " "
185 (group (and (? ?-) digit)) " "
186 (group (and (? ?-) digit)) " "
187 (group (and (? ?-) digit)) " "
188 (group (and (? ?-) digit)) " "
189 (group (+ digit)) " "
190 (group (+ digit)) " "
191 (group (+ (not (in " ")))) " "
192 (group (+ (not (in " ")))) " "
193 (group (and (? ?-) digit)) " "
194 (group (+ digit)) " "
195 (group (+ digit)) " "
196 (group (+ digit)) " "
197 (group (+ digit)) " "
198 (group (and (? ?-) (+ digit))) " "
199 (group (and (? ?-) (+ digit))) " "
200 (group (+ digit)) " "
201 (group (+ (not (in " ")))) " "
202 "(" (group (+ (not (in " )")))) ") "
203 (group (+ (not (in " ")))) " "
204 (group (and (? ?-) digit))
205 (optional (and " " (group (and (? ?-) digit)) " "
206 (group (and (? ?-) (+ digit)))))))
207 "A regular expression matching a style12 board string.")
209 (defvar chess-ics-matcher-alist
211 (cons "www.chessclub.com"
214 (when chess-ics-handling-login
215 (setq chess-ics-server-type 'ICC
216 comint-preoutput-filter-functions
217 '(chess-icc-preoutput-filter)))
219 (cons "\\(ogin\\|name\\):"
222 (when (eq chess-ics-server-type 'ICC)
224 (format "level2settings=%s"
225 (let ((str (make-string
226 (1+ (apply 'max chess-ics-icc-datagrams))
228 (dolist (dg chess-ics-icc-datagrams str)
229 (aset str dg ?1))))))
230 (if (string= "guest" chess-ics-handle)
231 (chess-message 'ics-anon-login chess-ics-server)
233 'ics-logging-in chess-ics-server chess-ics-handle))
234 (chess-ics-send chess-ics-handle)
239 (when chess-ics-handling-login
240 (chess-ics-send chess-ics-password))
242 (cons "\\(Logging you in as\\|Your name for this session will be\\) \"\\([^\"]+\\)\""
245 (setq chess-ics-handle (match-string 2))
247 (cons "Press return to enter the server as"
252 (cons "Press return to enter chess.net as \"\\([^\"]+\\)\":"
255 (setq chess-ics-handle (match-string 1))
265 (assoc chess-ics-server chess-ics-initial-commands)
266 (assoc nil chess-ics-initial-commands))) "\n"))
267 (setq chess-ics-handling-login nil)
268 (chess-message 'ics-logged-in chess-ics-server chess-ics-handle)
270 (cons "fics%\\s-+startpos set.$"
273 (setq chess-ics-movelist-start-position nil)
275 (cons (concat "^Game [0-9]+: \\S-+ moves: " chess-algebraic-regexp-entire)
279 (while (and (forward-line -1)
280 (or (looking-at "^[ \t]*$")
282 (concat "^" chess-ics-prompt-regexp))))
283 (delete-region (match-beginning 0) (1+ (match-end 0)))))
285 (cons "^\\([A-Za-z0-9]+\\)\\((\\*)\\|(B)\\|(CA?)\\|(H)\\|(DM)\\|(T[DM]?)\\|(SR)\\|(FM)\\|(W?[GI]M)\\|(U)\\|([0-9-]+)\\)*\\((\\([0-9]+\\))\\| tells you\\| s-shouts\\|\\[\\([0-9]+\\)\\] kibitzes\\): \\(.+\\)$"
288 (let ((fill-prefix (make-string
289 (- (match-end 1) (match-beginning 1)) ? ))
290 (game-num (match-string 5))
291 (text-begin (match-beginning 6)))
292 (goto-char (match-beginning 0))
294 (while (and (forward-line 1)
295 (looking-at "^\\\\\\s-+"))
296 (delete-region (1- (match-beginning 0)) (match-end 0))))
298 (chess-game-run-hooks
299 (chess-ics-game (string-to-number game-num))
300 'kibitz (buffer-substring text-begin (line-end-position))))
301 (when (> (- (line-end-position) (line-beginning-position))
304 (fill-region (point) (line-end-position))))
306 (while (and (forward-line -1)
307 (or (looking-at "^[ \t]*$")
308 (looking-at "^[af]ics%\\s-*$")))
309 (delete-region (match-beginning 0) (1+ (match-end 0)))))))))
310 (cons "{Game \\([0-9]+\\) (\\(\\S-+\\) vs\\. \\(\\S-+\\)) Creating [^ ]+ \\([^ ]+\\).*}"
313 (let ((game-number (string-to-number (match-string 1)))
314 (white (match-string-no-properties 2))
315 (black (match-string-no-properties 3)))
316 (message "Creating game %d (%s vs. %s)" game-number white black)
317 (chess-ics-game game-number :White white :Black black)))))
318 (cons "^<10>$" (function (lambda () (chess-ics-send "style 12\nrefresh"))))
319 (cons "^Game \\([0-9]+\\): \\S-+ backs up \\([0-9]+\\).$"
322 (chess-game-undo (chess-ics-game (string-to-number (match-string 1)))
323 (string-to-number (match-string 2))))))
324 (cons chess-ics-style12-regexp #'chess-ics-handle-style12)
325 (cons "Removing game \\([0-9]+\\) from observation list.$"
328 (chess-ics-game-destroy (string-to-number (match-string 1))))))
329 (cons "You are no longer examining game \\([0-9]+\\).$"
332 (chess-ics-game-destroy (string-to-number (match-string 1))))))
333 (cons "^Movelist for game \\([0-9]+\\):$"
336 (if (or chess-ics-movelist-game-number
337 chess-ics-movelist-game)
338 (message "[movelist] left-over movelist-game[-number]")
339 (setq chess-ics-movelist-game-number
340 (string-to-number (match-string 1)))))))
341 (cons "^Move\\s-+\\*?\\(\\S-+\\)\\s-+\\*?\\(\\S-+\\)\\s-*$"
344 (if (not chess-ics-movelist-game-number)
346 (goto-char (match-beginning 0))
347 (insert "(no game# known) "))
348 (setq chess-ics-movelist-game
349 (chess-ics-game chess-ics-movelist-game-number
350 :White (match-string 1)
351 :Black (match-string 2)))
352 (when chess-ics-movelist-start-position
353 (chess-game-set-start-position
354 chess-ics-movelist-game chess-ics-movelist-start-position)))
357 (cons (concat "^\\s-*\\([0-9]+\\)\\.\\s-+\\(" chess-algebraic-regexp "\\)"
358 "\\s-+\\(([0-9][0-9]?:[0-9][0-9])\\)\\s-*"
359 "\\(\\(" chess-algebraic-regexp "\\)\\s-+"
360 "\\(([0-9][0-9]?:[0-9][0-9])\\)\\s-*\\)?$")
361 #'chess-ics-handle-movelist-item)
362 (cons "\\s-+{Still in progress}\\s-+\\*$"
365 (if (integerp chess-ics-movelist-game-number)
366 (setq chess-ics-movelist-game-number nil
367 chess-ics-movelist-game nil)
368 (message "[movelist] end of movelist seen where no game known about")))))
369 (cons "\\S-+ would like to take back \\([0-9]+\\) half move(s)."
372 (funcall chess-engine-response-handler 'undo
373 (string-to-number (match-string 1))))))
374 (cons "The game has been aborted on move [^.]+\\."
377 (let ((chess-engine-pending-offer 'abort))
378 (funcall chess-engine-response-handler 'accept)))))
379 (cons "\\S-+ accepts the takeback request\\."
382 (funcall chess-engine-response-handler 'accept))))
383 (cons ;; resign announcement
384 "{Game \\([0-9]+\\) (\\(\\S-+\\) vs\\. \\(\\S-+\\)) \\(\\S-+\\) resigns}"
387 (let ((chess-engine-handling-event t)
388 (opponent-p (not (string= chess-ics-handle (match-string 4))))
389 (game (chess-ics-game (string-to-number (match-string 1))
390 :White (match-string 2)
391 :Black (match-string 3))))
392 (with-current-buffer (chess-game-data game 'engine)
394 (funcall chess-engine-response-handler 'resign)
395 (unless (chess-game-status game)
396 (chess-game-end game :resign))))
398 (cons "\\(\\S-+\\) forfeits on time}"
401 (if (string= (match-string 1) chess-engine-opponent-name)
402 (funcall chess-engine-response-handler 'flag-fell)
403 (funcall chess-engine-response-handler 'call-flag t)))))
404 (cons "Illegal move (\\([^)]+\\))\\."
407 (funcall chess-engine-response-handler 'illegal
409 (cons "Challenge: \\(\\S-+\\) \\S-+ \\S-+ \\S-+ .+"
412 (let ((opponent (match-string 1)))
413 (if (y-or-n-p (chess-string 'want-to-play opponent))
414 (chess-ics-send (concat "accept " opponent))
415 (chess-ics-send "decline match"))))))
417 (cons "\"?\\(\\(https?\\|ftp\\)://[^ \t\n\r\"]+\\)\"?"
420 (make-button (match-beginning 1) (match-end 1)
421 'action (lambda (button)
422 (browse-url (button-label button))))))))
423 "An alist of regular expressions to use to scan ICS server output.
424 The car of each element is the regexp to try, and the cdr is a function
425 to run whenever the regexp matches.")
427 (defvar chess-ics-sessions nil
428 "A list of chess-sessions spawned from an Internet Chess Server connection.
429 See `chess-ics-game'.")
430 (make-variable-buffer-local 'chess-ics-sessions)
432 (defun chess-ics-game (game-number &rest tags)
433 "Either create, or retrieve an existing game object with GAME-NUMBER."
434 (cl-assert (integerp game-number))
435 (cl-assert (or (zerop (logand (length tags) 1)) (eq (car tags) t)))
437 ;; First try to find a game which matches the constraints in TAGS
439 (let ((sessions chess-ics-sessions))
441 (if (not (buffer-live-p (caar sessions)))
442 (message "Found dead engine session in `chess-ics-sessions'")
443 (let ((game (chess-engine-game (caar sessions)))
445 (when (= game-number (chess-game-data game 'ics-game-number))
446 (if (or (null tags) (eq (car tags) t))
447 (throw 'ics-game game)
449 (cl-assert (symbolp (car tag-pairs)))
450 (let ((tag (substring (symbol-name (car tag-pairs)) 1))
451 (val (cadr tag-pairs)))
452 (cl-assert (stringp val))
453 (if (string= (chess-game-tag game tag) val)
454 (setq tag-pairs (cddr tag-pairs))
455 (if (not (string= (chess-game-tag game tag) "?"))
456 (message "Game %d %s %s != %s"
457 game-number tag (chess-game-tag game tag) val))
458 ;; Update tag and proceed
459 (chess-game-set-tag game tag val)
460 (setq tags (cddr tags)))))
461 (throw 'ics-game game)))))
462 (setq sessions (cdr sessions)))))
463 ;; if we are allowed to, create a new session for this game number
464 (unless (eq (car tags) t)
465 (push (let (chess-engine-handling-event)
466 (chess-session 'chess-ics))
468 (cl-assert (caar chess-ics-sessions))
469 (with-current-buffer (caar chess-ics-sessions)
470 (setq chess-ply-allow-interactive-query t))
471 (let ((game (chess-engine-game (caar chess-ics-sessions))))
472 (chess-game-set-data game 'ics-game-number game-number)
473 (chess-game-set-data game 'ics-buffer (current-buffer))
474 (chess-game-set-tag game "Site" chess-ics-server)
476 (cl-assert (keywordp (car tags)))
478 game (substring (symbol-name (car tags)) 1) (cadr tags))
479 (setq tags (cddr tags)))
482 (defun chess-ics-game-destroy (game-number &rest tags)
483 (let ((sessions chess-ics-sessions)
486 (if (not (buffer-live-p (caar sessions)))
487 (message "Found dead engine session in `chess-ics-sessions'")
488 (let ((game (chess-display-game (cl-cadar sessions)))
491 (when (= game-number (chess-game-data game 'ics-game-number))
494 (chess-display-destroy (cl-cadar sessions))
496 (setcdr last-session (cdr sessions))
497 (setq chess-ics-sessions (cdr sessions))))
498 (while (and tag-pairs found)
499 (cl-assert (symbolp (car tag-pairs)))
500 (let ((tag (substring (symbol-name (car tag-pairs)) 1))
501 (val (cadr tag-pairs)))
502 (cl-assert (stringp val))
503 (if (string= (chess-game-tag game tag) val)
504 (setq tag-pairs (cddr tag-pairs))
507 (error "Game not found")
508 (chess-engine-destroy (cl-cadar sessions))
510 (setcdr last-session (cdr sessions))
511 (setq chess-ics-sessions (cdr sessions))))))))
512 (setq last-session sessions
513 sessions (cdr sessions)))))
515 (defun chess-ics-handle-movelist-item ()
516 ;; TBD: time taken per ply
517 (let ((chess-engine-handling-event t)
518 (seq (string-to-number (match-string 1)))
519 (wmove (match-string 2))
520 (bmove (match-string 14))
521 (game chess-ics-movelist-game))
523 (if (/= (chess-game-seq game) seq)
525 (goto-char (match-beginning 0))
526 (insert (format "SeqNr. unmatched (%d): " seq)))
527 (when (chess-pos-side-to-move (chess-game-pos game))
529 game (chess-algebraic-to-ply (chess-game-pos game) wmove))
532 game (chess-algebraic-to-ply (chess-game-pos game) bmove))))))
535 ;; ICS style12 format (with artificial line breaks):
537 ;; <12> rnbqkbnr pppppppp -------- -------- \
538 ;; -------- -------- PPPPPPPP RNBQKBNR W -1 1 1 1 1 0 \
539 ;; 65 jwiegley GuestZYNJ 1 5 0 39 39 300 300 1 P/e2-e4 (0:00) e4 0 0 0
541 (defun chess-ics-handle-style12 ()
542 "Handle an ICS Style12 board string."
543 (let* ((chess-engine-handling-event t)
544 (begin (match-beginning 0))
546 (position (let ((pos (chess-pos-create t)))
548 (let ((rank (match-string (1+ r))))
550 (unless (= (aref rank f) ?-)
552 pos (chess-rf-to-index r f) (aref rank f))))))
553 (chess-pos-set-side-to-move pos (string= (match-string 9) "W"))
554 (let ((file (string-to-number (match-string 10))))
556 (chess-pos-set-en-passant
557 pos (chess-rf-to-index
558 (if (chess-pos-side-to-move pos) 3 4) file))))
560 (if (string= (match-string (cdr info)) "1")
561 (chess-pos-set-can-castle pos (car info) t)))
562 '((?K . 11) (?Q . 12) (?k . 13) (?q . 14))) pos))
563 (game (save-match-data
564 (chess-ics-game (string-to-number (match-string 16))
565 :White (match-string 17)
566 :Black (match-string 18))))
568 ;; my relation to this game:
569 ;; -3 isolated position, such as for "ref 3" or the "sposition"
571 ;; -2 I am observing game being examined
572 ;; 2 I am the examiner of this game
573 ;; -1 I am playing, it is my opponent's move
574 ;; 1 I am playing and it is my move
575 ;; 0 I am observing a game being played
576 (string-to-number (match-string 19))))
577 (when (or (= status 2) (= status -2) (= status 0))
578 (chess-game-set-data game 'my-color (chess-pos-side-to-move position)))
579 ;; initial time and increment (in seconds) of the match
581 game "TimeControl" (format "%s/%s" (match-string 20) (match-string 21)))
582 ;; material values for each side
583 (let ((centipawn (* 100 (- (string-to-number (match-string 22))
584 (string-to-number (match-string 23))))))
585 (chess-pos-set-epd position 'ce (if (chess-pos-side-to-move position)
586 centipawn (- centipawn))))
587 ;; White's and Black's remaining time
588 (chess-game-set-data game 'white-remaining (string-to-number (match-string 24)))
589 (chess-game-set-data game 'black-remaining (string-to-number (match-string 25)))
590 (let ((index (- (* (string-to-number (match-string 26)) 2)
591 (if (eq (chess-game-data game 'black-moved-first) t)
592 (if (chess-pos-side-to-move position) 3 2)
593 (if (chess-pos-side-to-move position) 2 1))))
594 (move (unless (string= (match-string 29) "none")
595 (cl-case (aref (match-string 29) (1- (length (match-string 29))))
596 (?+ (chess-pos-set-status position :check))
597 (?# (chess-pos-set-status position :checkmate)
598 (chess-pos-set-epd position 'ce 32767)))
599 ;; jww (2002-04-30): what about stalemate? do I need to
600 ;; calculate this each time?
602 (chess-pos-set-status position :stalemate))
607 (if (progn (setq error 'comparing-index)
608 (= (1- index) (chess-game-index game)))
609 (let ((ply (progn (setq error 'converting-ply)
610 (chess-algebraic-to-ply
611 (chess-game-pos game) move t))))
612 ;; each move gives the _position occurring after the ply_
613 (if (progn (setq error 'comparing-colors)
614 (eq (chess-pos-side-to-move position)
615 (chess-game-data game 'my-color)))
616 (setq error 'applying-opponent-move)
617 (setq error 'applying-my-move))
618 ;; save us from generating a position we already have
619 (chess-ply-set-keyword ply :next-pos position)
620 (chess-pos-set-preceding-ply position ply)
622 (chess-game-move game ply)
624 (if (= index (chess-game-index game))
625 ;; this is a refresh, which we can use to verify that our
626 ;; notion of the game's current position is correct
627 (let ((their-fen (chess-pos-to-fen position))
628 (our-fen (chess-pos-to-fen (chess-game-pos game))))
629 (if (string= their-fen our-fen)
630 (setq error nil) ; ignore the refresh
632 (format "comparing-position (%s != %s)"
633 their-fen our-fen))))
634 (if (and (> index (1+ (chess-game-index game)))
635 (= 1 (chess-game-seq game)))
636 ;; we lack a complete game, try to get it via the
642 (chess-game-data game 'ics-game-number))))
644 (format "comparing-index (%d:%d)"
645 index (chess-game-index game))))))
646 ;; no preceeding ply supplied, so this is a starting position
647 (let ((chess-game-inhibit-events t)
648 (color (chess-pos-side-to-move position)))
649 (when (or (= 1 status) (= -1 status))
650 (chess-game-set-data game 'my-color (if (= 1 status)
652 (chess-game-set-data game 'active t))
653 (setq error 'setting-start-position)
654 (chess-game-set-start-position game position)
655 (chess-game-set-data game 'black-moved-first (not color)))
656 (setq error 'orienting-board)
657 (chess-game-run-hooks game 'orient)
661 (insert (chess-string 'failed-ics-parse error))
662 (delete-region begin end)
664 (while (and (forward-line -1)
665 (or (looking-at "^[ \t]*$")
666 (looking-at "^[^% \t\n\r]+%\\s-*$")))
667 (delete-region (match-beginning 0) (1+ (match-end 0)))))
668 ;; we need to counter the forward-line in chess-engine-filter
672 (defvar chess-ics-sought-parent-buffer nil
673 "Contains the buffer from which this seektable originates.")
674 (make-variable-buffer-local 'chess-ics-sought-parent-buffer)
676 (defun chess-ics-sought-accept (button)
677 "Perform the action specified by a BUTTON."
678 (let ((buffer (button-get button 'ics-buffer))
679 (command (button-get button 'ics-command)))
680 (when (and (buffer-live-p buffer) (stringp command))
681 (chess-ics-send command buffer)
684 (defcustom chess-ics-popup-sought t
685 "If non-nil, display the sought buffer automatically."
689 (defcustom chess-ics-sought-buffer-name "*chess-ics-sought*"
690 "The name of the buffer which accumulates seek ads."
694 (define-derived-mode chess-ics-ads-mode tabulated-list-mode "ICSAds"
695 "Mode for displaying sought games from Internet Chess Servers."
697 (setq tabulated-list-format [("Player" 20 t)
698 ("Rating" 10 t :right-align t)
699 ("Rated" 5 nil :right-align t)
700 ("Time" 4 t :right-align t)
703 (setq tabulated-list-entries nil)
704 (tabulated-list-init-header)
705 (tabulated-list-print))
707 (defun chess-ics-sought-add (id name rating rated time inc variant
709 (let ((inhibit-redisplay t))
711 (or (get-buffer chess-ics-sought-buffer-name)
712 (with-current-buffer (get-buffer-create
713 chess-ics-sought-buffer-name)
715 (and chess-ics-popup-sought (display-buffer (current-buffer)))
717 (setq chess-ics-sought-parent-buffer ics-buffer)
718 (add-to-list 'tabulated-list-entries
721 'ics-buffer ics-buffer
723 'action #'chess-ics-sought-accept)
724 (number-to-string rating)
726 (number-to-string time)
727 (number-to-string inc)
729 (tabulated-list-revert))))
731 (defun chess-ics-seeking (string)
732 ;; jww (2008-09-02): we should use rx for this regular expression also
734 (concat "[\n\r]+\\(\\S-+\\) (\\([0-9+ -]+\\)) seeking \\([a-z]\\S-+ \\)?\\([0-9]+\\) \\([0-9]+\\) \\(\\(un\\)?rated\\) \\([^(]*\\)(\"\\([^\"]+\\)\" to respond)\\s-*[\n\r]+"
735 chess-ics-prompt-regexp)
737 (let* ((pre (substring string 0 (match-beginning 0)))
738 (post (substring string (match-end 0))))
739 (chess-ics-sought-add (string-to-number (substring (match-string 9 string) 5))
740 (match-string 1 string)
741 (string-to-number (match-string 2 string))
742 (if (string= (match-string 6 string) "rated")
744 (string-to-number (match-string 4 string))
745 (string-to-number (match-string 5 string))
747 (if (match-string 3 string)
748 (concat (match-string 3 string) " ") "")
749 (match-string 8 string))
751 (match-string 9 string))
752 (setq string (concat pre post))))
755 (defun chess-ics-ads-removed (string)
756 "Look for Seek ad removal announcements in the output stream.
757 This function should be put on `comint-preoutput-filter-functions'."
760 (concat "[\n\r]+Ads removed: \\([0-9 ]+\\)\\s-*[\n\r]+"
761 chess-ics-prompt-regexp)
763 (setq ids (append (mapcar #'string-to-number
765 (split-string (match-string 1 string) " +")))
767 string (concat (substring string 0 (match-beginning 0))
768 (substring string (match-end 0)))))
770 (let ((buf (get-buffer chess-ics-sought-buffer-name))
771 (inhibit-redisplay t))
772 (when (buffer-live-p buf)
773 (with-current-buffer buf
774 (let ((old-length (length tabulated-list-entries)))
775 (setq tabulated-list-entries
776 (cl-remove-if (lambda (entry) (member (car entry) ids))
777 tabulated-list-entries))
778 (when (/= (length tabulated-list-entries) old-length)
779 (tabulated-list-revert))))))))
782 (make-variable-buffer-local 'comint-preoutput-filter-functions)
785 (defun chess-ics (server port &optional handle password-or-filename
786 helper &rest helper-args)
787 "Connect to an Internet Chess Server."
789 (let ((args (if (= (length chess-ics-server-list) 1)
790 (car chess-ics-server-list)
791 (assoc (completing-read (chess-string 'ics-server-prompt)
792 chess-ics-server-list
793 nil t (caar chess-ics-server-list))
794 chess-ics-server-list))))
795 (if (and (nth 2 args) (not (nth 3 args)))
796 (append (list (nth 0 args) (nth 1 args) (nth 2 args)
797 (read-passwd "Password: ")
802 (setq handle "guest"))
803 (chess-message 'ics-connecting server)
804 (let ((buf (if helper
805 (apply 'make-comint "chess-ics" helper nil helper-args)
806 (make-comint "chess-ics" (cons server port)))))
807 (chess-message 'ics-connected server)
809 (setq chess-ics-server server
810 chess-ics-handle handle
812 (if (and password-or-filename
813 (file-readable-p password-or-filename))
815 (insert-file-contents password-or-filename)
817 password-or-filename)
818 chess-ics-handling-login t
819 chess-engine-regexp-alist (copy-alist chess-ics-matcher-alist)
820 comint-prompt-regexp "^[^%\n]*% *"
821 comint-scroll-show-maximum-output t)
822 (add-hook 'comint-output-filter-functions 'chess-engine-filter t t)
823 (setq comint-preoutput-filter-functions
824 '(chess-ics-ads-removed chess-ics-seeking))
826 (while (and chess-ics-handling-login
827 (> (setq ntimes (1- ntimes)) 0))
828 (accept-process-output (get-buffer-process (current-buffer)) 0 100)))
829 (switch-to-buffer buf)))
833 ;; See http://www.chessclub.com/resources/formats/formats.txt
835 (defvar chess-icc-unprocessed nil)
837 (defun chess-icc-datagram-handler (string)
838 (if (not (string-match "^\\([0-9]+\\) \\(.*\\)$" string))
839 (format "\nUnknown datagram format: %s\n" string)
840 (let ((chess-engine-handling-event t)
841 (dg (string-to-number (match-string 1 string)))
842 (args (match-string 2 string)))
844 ((and (or (= dg 22) (= dg 23))
845 (string-match "\\([0-9]+\\) \\([1-9][0-9]*\\)" args))
846 (chess-game-undo (chess-ics-game (string-to-number (match-string 1 args)))
847 (string-to-number (match-string 2 args)))
849 ((and (or (= dg 101) (= dg 110))
850 (string-match "\\([0-9]+\\) {\\(.+\\) \\(?:[0-9]+\\) \\(?:[0-9]+\\)} \\([0-9]+\\)" args))
851 (let ((pos (chess-fen-to-pos (match-string 2 args))))
852 (chess-game-set-start-position
853 (chess-ics-game (string-to-number (match-string 1 args))) pos))
855 ((and (or (= dg 24) (= dg 111))
856 (string-match "^\\([0-9]+\\) \\(.+\\)$" args))
857 (let* ((move (match-string 2 args))
858 (game (chess-ics-game (string-to-number (match-string 1 args))))
859 (pos (chess-game-pos game))
860 (ply (chess-algebraic-to-ply pos move)))
861 (chess-game-move game ply)
864 (string-match "^\\([0-9]+\\) \\(\\S-+\\) {\\([^}]*\\)} \\([01]\\)
\19{\\(.*\\)
\19}"
866 (let ((game-number (match-string 1 args))
867 (action (if (string= (match-string 4 args) "1")
868 "kibitzes" "whispers"))
869 (name (match-string 2 args))
870 (titles (match-string 3 args))
871 (text (match-string 5 args)))
874 (mapconcat (lambda (title)
875 (concat "(" title ")"))
876 (split-string titles " ") "")))
877 (format "\n%s[%s] %s: %s\n" name game-number action text)))
879 (string-match "^\\([0-9]+\\) \\([WB]\\) \\([0-9]+\\) \\([01]\\)"
881 (let ((sec (/ (string-to-number (match-string 3 args)) 1000))
882 (color (if (string= (match-string 2 args) "W")
883 'white-remaining 'black-remaining))
884 (game (chess-ics-game (string-to-number (match-string 1 args)))))
885 (chess-game-set-data game color sec))
888 (string-match "^\\([0-9]+\\) \\(\\S-+\\) {\\([^}]*\\)} \\([0-9]+\\) \\([0-2]\\) \\([0-9]+\\) \\(\\S-+\\) \\([0-9]+\\) \\([0-9]+\\) \\([01]\\) \\(-?[01]\\) \\([0-9]+\\) \\([0-9]+\\) \\([01]\\) \\([01]\\) {\\([^}]*\\)}" args))
889 (chess-ics-sought-add
890 (string-to-number (match-string 1 args))
891 (concat (match-string 2 args)
892 (if (not (string= (match-string 3 args) ""))
893 (format "(%s)" (match-string 3 args))
895 (string-to-number (match-string 4 args))
896 (if (string= (match-string 10 args) "1") "yes" "no")
897 (string-to-number (match-string 8 args))
898 (string-to-number (match-string 9 args))
899 (concat (match-string 7 args)
900 (if (not (string= (match-string 6 args) "0"))
901 (concat " " (match-string 6 args)) "")
902 (if (string= (match-string 14 args) "0")
904 (if (string= (match-string 15 args) "1")
907 (concat "play " (match-string 1 args)))
910 (let ((id (string-to-number (car (split-string args " +"))))
911 (buf (get-buffer chess-ics-sought-buffer-name)))
912 (when (buffer-live-p buf)
913 (with-current-buffer buf
914 (setq tabulated-list-entries
915 (cl-remove-if (lambda (entry) (equal (car entry) id))
916 tabulated-list-entries))
917 (tabulated-list-revert))))
920 (format "\nIgnoring unhandled datagram DG%03d: %s\n" dg args))))))
922 (defun chess-icc-preoutput-filter (string)
923 (if chess-icc-unprocessed
924 (let ((string (concat chess-icc-unprocessed string)))
925 (if (string-match "
\19)" string)
926 (let ((newstr (unwind-protect
927 (chess-icc-datagram-handler
928 (substring string 0 (match-beginning 0)))
929 (setq chess-icc-unprocessed nil))))
930 (chess-icc-preoutput-filter (concat (or newstr "")
933 (setq chess-icc-unprocessed string)
935 (if (string-match "
\19(" string)
936 (let ((pre (substring string 0 (match-beginning 0)))
937 (substr (substring string (match-end 0))))
938 (if (string-match "
\19)" substr)
939 (let ((post (substring substr (match-end 0)))
940 (newstr (chess-icc-datagram-handler
941 (substring substr 0 (match-beginning 0)))))
942 (chess-icc-preoutput-filter (concat pre newstr post)))
943 (setq chess-icc-unprocessed substr)
947 (defun chess-ics-icc-preoutput-filter (string)
948 (while (string-match "
\19(\\([0-9]+\\) \\(.*?\\)
\19)" string)
949 (let ((dg (string-to-number (match-string 1 string)))
950 (args (match-string 2 string))
951 (pre (substring string 0 (match-beginning 0)))
952 (post (substring string (match-end 0))))
954 ((and (or (= dg 101) (= dg 110))
955 (string-match "\\([0-9]+\\) {\\(.+\\) \\(?:[0-9]+\\) \\(?:[0-9]+\\)} \\([0-9]+\\)" args))
956 (let ((pos (chess-fen-to-pos (match-string 2 args))))
957 (chess-game-set-start-position
958 (chess-ics-game (string-to-number (match-string 1 args))) pos))
959 (setq string (concat pre post)))
960 ((and (or (= dg 24) (= dg 111))
961 (string-match "\\([0-9]+\\) \\(.+\\)$" args))
962 (let* ((chess-engine-handling-event t)
963 (move (match-string 2 args))
964 (game (chess-ics-game (string-to-number (match-string 1 args))))
965 (pos (chess-game-pos game))
966 (ply (chess-algebraic-to-ply pos move)))
968 (chess-game-move game ply)
969 (setq pre (format "%s\nunable to apply move %s\n" pre move))))
970 (setq string (concat pre post)))
972 (string-match "\\([0-9]+\\) \\(\\S-+\\) {\\([^}]*\\)} \\([01]\\)
\19{\\(.*\\)
\19}"
974 (let ((game-number (match-string 1 args))
975 (action (if (string= (match-string 4 args) "1")
976 "kibitzes" "whispers"))
977 (name (match-string 2 args))
978 (titles (match-string 3 args))
979 (text (match-string 5 args)))
982 (mapconcat (lambda (title)
983 (concat "(" title ")"))
984 (split-string titles " ") "")))
986 (format "%s\n%s[%s] %s: %s\n%s"
987 pre name game-number action text post))))
989 (string-match "\\([0-9]+\\) \\([WB]\\) \\([0-9]+\\) \\([01]\\)"
991 (let ((sec (/ (string-to-number (match-string 3 args)) 1000))
992 (color (if (string= (match-string 2 args) "W")
993 'white-remaining 'black-remaining))
994 (game (chess-ics-game (string-to-number (match-string 1 args)))))
995 (chess-game-set-data game color sec))
996 (setq string (concat pre post)))
998 (string-match "\\([0-9]+\\) \\(\\S-+\\) {\\([^}]*\\)} \\([0-9]+\\) \\([0-2]\\) \\([0-9]+\\) \\(\\S-+\\) \\([0-9]+\\) \\([0-9]+\\) \\([01]\\) \\(-?[01]\\) \\([0-9]+\\) \\([0-9]+\\) \\([01]\\) \\([01]\\) {\\([^}]*\\)}" args))
999 (chess-ics-sought-add
1000 (match-string 1 args)
1001 (concat (match-string 2 args)
1002 (if (not (string= (match-string 3 args) ""))
1003 (format "(%s)" (match-string 3 args))
1005 (string-to-number (match-string 4 args))
1006 (if (string= (match-string 10 args) "1")
1008 (string-to-number (match-string 8 args))
1009 (string-to-number (match-string 9 args))
1010 (concat (match-string 7 args)
1011 (if (not (string= (match-string 6 args) "0"))
1012 (concat " " (match-string 6 args)) "")
1013 (if (string= (match-string 14 args) "0")
1015 (if (string= (match-string 15 args) "1")
1018 (concat "play " (match-string 1 args)))
1019 (setq string (concat pre post)))
1021 (let ((id (car (split-string args " ")))
1022 (buf (get-buffer chess-ics-sought-buffer-name)))
1023 (when (buffer-live-p buf)
1024 (with-current-buffer buf
1025 (let ((here (point)))
1026 (goto-char (point-min))
1027 (when (re-search-forward (concat "^" id " ") nil t)
1028 (delete-region (line-beginning-position)
1029 (1+ (line-end-position))))
1030 (goto-char here)))))
1031 (setq string (concat pre post)))
1033 (message "Ignoring Datagram %03d: %s" dg args)
1034 (setq string (concat pre post))))))
1037 (defun chess-ics-handler (game event &rest args)
1038 (unless chess-engine-handling-event
1040 ((eq event 'initialize))
1043 (chess-game-run-hooks game 'announce-autosave))
1045 ((eq event 'busy)) ; ICS will inform them
1048 (setq chess-engine-pending-offer 'match)
1050 nil (format "match %s\n"
1051 (read-string (chess-string 'challenge-whom)))))
1053 ;; we need to send long algebraic notation to the ICS server, not short
1055 (let ((ply (car args)))
1057 (if (chess-ply-any-keyword ply :castle :long-castle)
1058 (chess-ply-to-algebraic ply)
1059 (concat (chess-index-to-coord (chess-ply-source ply))
1061 (chess-index-to-coord (chess-ply-target ply))
1062 (if (characterp (chess-ply-keyword ply :promote))
1063 (format "=%c" (chess-ply-keyword ply :promote))
1065 (chess-game-data game 'ics-buffer)))
1066 (if (chess-game-over-p game)
1067 (chess-game-set-data game 'active nil)))
1069 ((eq event 'flag-fell)
1070 (chess-common-handler game 'flag-fell))
1072 ((eq event 'forward)
1073 (chess-ics-send "forward" (chess-game-data game 'ics-buffer)))
1076 (chess-ics-send (format "takeback %d" (car args))
1077 (chess-game-data game 'ics-buffer)))
1080 (chess-ics-send "abort" (chess-game-data game 'ics-buffer)))
1082 ((eq event 'call-flag)
1083 (chess-ics-send "flag" (chess-game-data game 'ics-buffer)))
1086 (chess-ics-send "draw" (chess-game-data game 'ics-buffer)))
1089 (chess-ics-send "resign" (chess-game-data game 'ics-buffer)))
1092 (apply 'chess-network-handler game event args)))))
1094 (provide 'chess-ics)
1096 ;;; chess-ics.el ends here