;;; chess-ics.el --- Play on Internet Chess Servers -*- lexical-binding: t; -*- ;; Copyright (C) 2002-2020 Free Software Foundation, Inc. ;; Author: John Wiegley ;; Maintainer: Mario Lang ;; Keywords: games, processes ;; This program is free software; you can redistribute it and/or modify ;; it under the terms of the GNU General Public License as published by ;; the Free Software Foundation, either version 3 of the License, or ;; (at your option) any later version. ;; This program is distributed in the hope that it will be useful, ;; but WITHOUT ANY WARRANTY; without even the implied warranty of ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ;; GNU General Public License for more details. ;; You should have received a copy of the GNU General Public License ;; along with this program. If not, see . ;;; Commentary: ;; This module allows to play chess on an Internet Chess Server. ;; Contrary to other chess engine modules for chess.el, you are not supposed to ;; use `chess-ics' as an engine for `M-x chess', rather, you call ;; `M-x chess-ics' directly to play chess on the internet. ;; ;; The two major Internet Chess Servers, freechess.org and chessclub.com ;; are both supported. See `chess-ics-server-list' for the supported servers. ;;; Code: (require 'cl-lib) (require 'comint) (require 'chess) (require 'chess-network) (require 'chess-pos) (eval-when-compile (require 'rx) (require 'sort)) (defgroup chess-ics nil "Engine for interacting with Internet Chess Servers." :group 'chess :link '(custom-manual "(chess)Internet Chess Servers")) (defcustom chess-ics-server-list '(("freechess.org" 5000) ("chess.unix-ag.uni-kl.de" 5000) ("chessclub.com" 5000) ("chess.net" 5000) ("oics.olympuschess.com" 5000)) "A list of servers to connect to. The format of each entry is: (SERVER PORT [HANDLE] [PASSWORD-OR-FILENAME] [HELPER] [HELPER ARGS...])" :type '(repeat (list (string :tag "Server") (integer :tag "Port") (choice (const :tag "Login as guest" nil) (string :tag "Handle")) (choice (const :tag "No password or ask" nil) (string :tag "Password") (file :tag "Filename")) (choice (const :tag "Direct connection" nil) (file :tag "Command")) (choice (const :tag "No arguments" nil) (repeat string))))) (defcustom chess-ics-initial-commands (list (list "freechess.org" "iset defprompt 1" ; So we can't be supprised by a user setting (format "set interface emacs-chess %s" chess-version) "iset seekremove 1" ; For real-time sought display "iset startpos 1" ; Sends initial position before movelist "set style 12" ; So we can parse the board "easily" "set bell 0") ; We have our own way of announcing events (list "chessclub.com" (format "/set-quietly interface emacs-chess %s" chess-version) "/set-quietly style 12" ; So we can parse the board "easily" "/set-quietly bell 0") (list nil (format "set interface emacs-chess %s" chess-version) "set style 12" ; So we can parse the board "easily" "set bell 0")) "A list of commands to send automatically upon successful login. The format is (SERVER COMMANDS...) where SERVER is either the server-name \(see `chess-ics-server-list') or nil, which is the default to use for all servers which do not have a specialized entry in this list. COMMAND is a string which should be sent (newline characters will be added automatically.)" :type '(repeat (list :tag "Initialisation for" (choice (string :tag "Server Name") (const :tag "Default" nil)) (repeat :inline t (string :tag "Command"))))) (defcustom chess-ics-prompt-regexp "\\(?:[0-2][0-9]:[0-6][0-9]_\\)?[af]ics% $" "Regexp which matches an ICS prompt." :type 'regexp) (defvar chess-ics-server nil "The ICS server name of this connection.") (make-variable-buffer-local 'chess-ics-server) (defvar chess-ics-handle nil "The ICS handle of this connection.") (make-variable-buffer-local 'chess-ics-handle) (defvar chess-ics-password nil "Password to use to identify to the server.") (make-variable-buffer-local 'chess-ics-password) (defvar chess-ics-handling-login nil "Non-nil if we are currently handling the ICS login sequence.") (make-variable-buffer-local 'chess-ics-handling-login) (defvar chess-ics-server-type 'FICS "The type of chss server we are about to connect too. Possible values are currently FICS (the default, and best supported) and ICC.") (make-variable-buffer-local 'chess-ics-server-type) (defcustom chess-ics-icc-datagrams '(22 23 26 33 50 51 56 110 111) "A list of datagrams to request when connecting to ICC." :type '(repeat (choice (const :tag "DG_SEND_MOVES" 24) (const :tag "DG_KIBITZ" 26) (const :tag "DG_MOVE_ALGEBRAIC" 33) (const :tag "DG_SEEK" 50) (const :tag "DG_SEEK_REMOVED" 51) (const :tag "DG_MSEC" 56) (const :tag "DG_POSITION_BEGIN" 101) (const :tag "DG_POSITION_BEGIN2" 110) (const :tag "DG_PAST_MOVE" 111)))) (defvar chess-ics-movelist-game-number nil "If we are about to receive a movelist, this variable is set to the game number.") (make-variable-buffer-local 'chess-ics-movelist-game-number) (defvar chess-ics-movelist-game nil "If we are receiving a movelist, this variable is set to the game object.") (make-variable-buffer-local 'chess-ics-movelist-game) (defvar chess-ics-movelist-start-position chess-starting-position "The starting position to use upon receiving of a movelist. It is possible to configure certain servers to automatically send a style12 board before sending a movelist, to allow retrieval of the movelist for a non-standard game (one which does not start at the standard position). In those cases, this variable should be set to nil.") (make-variable-buffer-local 'chess-ics-movelist-start-position) (defsubst chess-ics-send (string &optional buffer) "Send STRING to the ICS server." (comint-send-string (get-buffer-process (or buffer (current-buffer))) (concat string "\n"))) (chess-message-catalog 'english '((ics-server-prompt . "Connect to chess server: ") (ics-connecting . "Connecting to Internet Chess Server '%s'...") (ics-connected . "Connecting to Internet Chess Server '%s'...done") (ics-anon-login . "Logging in on Internet Chess Server '%s' as anonymous user...") (ics-logging-in . "Logging in on Internet Chess Server '%s' as '%s'...") (ics-logged-in . "Logging in on Internet Chess Server '%s' as '%s'...done") (challenge-whom . "Whom would you like challenge? ") (failed-ics-parse . "Failed to parse ICS move string (%s): "))) (defconst chess-ics-style12-regexp (rx (and "<12> " (group (repeat 8 (in "-pnbrqkPNBRQK"))) " " (group (repeat 8 (in "-pnbrqkPNBRQK"))) " " (group (repeat 8 (in "-pnbrqkPNBRQK"))) " " (group (repeat 8 (in "-pnbrqkPNBRQK"))) " " (group (repeat 8 (in "-pnbrqkPNBRQK"))) " " (group (repeat 8 (in "-pnbrqkPNBRQK"))) " " (group (repeat 8 (in "-pnbrqkPNBRQK"))) " " (group (repeat 8 (in "-pnbrqkPNBRQK"))) " " (group (in "BW")) " " (group (and (? ?-) (in "0-7"))) " " (group (and (? ?-) digit)) " " (group (and (? ?-) digit)) " " (group (and (? ?-) digit)) " " (group (and (? ?-) digit)) " " (group (+ digit)) " " (group (+ digit)) " " (group (+ (not (in " ")))) " " (group (+ (not (in " ")))) " " (group (and (? ?-) digit)) " " (group (+ digit)) " " (group (+ digit)) " " (group (+ digit)) " " (group (+ digit)) " " (group (and (? ?-) (+ digit))) " " (group (and (? ?-) (+ digit))) " " (group (+ digit)) " " (group (+ (not (in " ")))) " " "(" (group (+ (not (in " )")))) ") " (group (+ (not (in " ")))) " " (group (and (? ?-) digit)) (optional (and " " (group (and (? ?-) digit)) " " (group (and (? ?-) (+ digit))))))) "A regular expression matching a style12 board string.") (defvar chess-ics-matcher-alist (list (cons "www.chessclub.com" (function (lambda () (when chess-ics-handling-login (setq chess-ics-server-type 'ICC) (add-hook 'comint-preoutput-filter-functions #'chess-icc-preoutput-filter nil t)) 'once))) (cons "\\(ogin\\|name\\):" (function (lambda () (when (eq chess-ics-server-type 'ICC) (chess-ics-send (format "level2settings=%s" (let ((str (make-string (1+ (apply #'max chess-ics-icc-datagrams)) ?0))) (dolist (dg chess-ics-icc-datagrams str) (aset str dg ?1)))))) (if (string= "guest" chess-ics-handle) (chess-message 'ics-anon-login chess-ics-server) (chess-message 'ics-logging-in chess-ics-server chess-ics-handle)) (chess-ics-send chess-ics-handle) 'once))) (cons "[Pp]assword:" (function (lambda () (when chess-ics-handling-login (chess-ics-send chess-ics-password)) 'once))) (cons "\\(Logging you in as\\|Your name for this session will be\\) \"\\([^\"]+\\)\"" (function (lambda () (setq chess-ics-handle (match-string 2)) 'once))) (cons "Press return to enter the server as" (function (lambda () (chess-ics-send "") 'once))) (cons "Press return to enter chess.net as \"\\([^\"]+\\)\":" (function (lambda () (setq chess-ics-handle (match-string 1)) (chess-ics-send "") 'once))) (cons "%\\s-*$" (function (lambda () (chess-ics-send (mapconcat #'identity (cdr (or (assoc chess-ics-server chess-ics-initial-commands) (assoc nil chess-ics-initial-commands))) "\n")) (setq chess-ics-handling-login nil) (chess-message 'ics-logged-in chess-ics-server chess-ics-handle) 'once))) (cons "fics%\\s-+startpos set.$" (function (lambda () (setq chess-ics-movelist-start-position nil) 'once))) (cons (concat "^Game [0-9]+: \\S-+ moves: " chess-algebraic-regexp-entire) (function (lambda () (save-excursion (while (and (forward-line -1) (or (looking-at "^[ \t]*$") (looking-at (concat "^" chess-ics-prompt-regexp)))) (delete-region (match-beginning 0) (1+ (match-end 0))))) t))) (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\\): \\(.+\\)$" (function (lambda () (let ((fill-prefix (make-string (- (match-end 1) (match-beginning 1)) ? )) (game-num (match-string 5)) (text-begin (match-beginning 6))) (goto-char (match-beginning 0)) (save-excursion (while (and (forward-line 1) (looking-at "^\\\\\\s-+")) (delete-region (1- (match-beginning 0)) (match-end 0)))) (when game-num (chess-game-run-hooks (chess-ics-game (string-to-number game-num)) 'kibitz (buffer-substring text-begin (line-end-position)))) (when (> (- (line-end-position) (line-beginning-position)) fill-column) (save-excursion (fill-region (point) (line-end-position)))) (save-excursion (while (and (forward-line -1) (or (looking-at "^[ \t]*$") (looking-at "^[af]ics%\\s-*$"))) (delete-region (match-beginning 0) (1+ (match-end 0))))))))) (cons "{Game \\([0-9]+\\) (\\(\\S-+\\) vs\\. \\(\\S-+\\)) Creating [^ ]+ \\([^ ]+\\).*}" (function (lambda () (let ((game-number (string-to-number (match-string 1))) (white (match-string-no-properties 2)) (black (match-string-no-properties 3))) (message "Creating game %d (%s vs. %s)" game-number white black) (chess-ics-game game-number :White white :Black black))))) (cons "^<10>$" (function (lambda () (chess-ics-send "style 12\nrefresh")))) (cons "^Game \\([0-9]+\\): \\S-+ backs up \\([0-9]+\\).$" (function (lambda () (chess-game-undo (chess-ics-game (string-to-number (match-string 1))) (string-to-number (match-string 2)))))) (cons chess-ics-style12-regexp #'chess-ics-handle-style12) (cons "Removing game \\([0-9]+\\) from observation list.$" (function (lambda () (chess-ics-game-destroy (string-to-number (match-string 1)))))) (cons "You are no longer examining game \\([0-9]+\\).$" (function (lambda () (chess-ics-game-destroy (string-to-number (match-string 1)))))) (cons "^Movelist for game \\([0-9]+\\):$" (function (lambda () (if (or chess-ics-movelist-game-number chess-ics-movelist-game) (message "[movelist] left-over movelist-game[-number]") (setq chess-ics-movelist-game-number (string-to-number (match-string 1))))))) (cons "^Move\\s-+\\*?\\(\\S-+\\)\\s-+\\*?\\(\\S-+\\)\\s-*$" (function (lambda () (if (not chess-ics-movelist-game-number) (progn (goto-char (match-beginning 0)) (insert "(no game# known) ")) (setq chess-ics-movelist-game (chess-ics-game chess-ics-movelist-game-number :White (match-string 1) :Black (match-string 2))) (when chess-ics-movelist-start-position (chess-game-set-start-position chess-ics-movelist-game chess-ics-movelist-start-position))) t))) ;; Movelist item (cons (concat "^\\s-*\\([0-9]+\\)\\.\\s-+\\(" chess-algebraic-regexp "\\)" "\\s-+\\(([0-9][0-9]?:[0-9][0-9])\\)\\s-*" "\\(\\(" chess-algebraic-regexp "\\)\\s-+" "\\(([0-9][0-9]?:[0-9][0-9])\\)\\s-*\\)?$") #'chess-ics-handle-movelist-item) (cons "\\s-+{Still in progress}\\s-+\\*$" (function (lambda () (if (integerp chess-ics-movelist-game-number) (setq chess-ics-movelist-game-number nil chess-ics-movelist-game nil) (message "[movelist] end of movelist seen where no game known about"))))) (cons "\\S-+ would like to take back \\([0-9]+\\) half move(s)." (function (lambda () (funcall chess-engine-response-handler 'undo (string-to-number (match-string 1)))))) (cons "The game has been aborted on move [^.]+\\." (function (lambda () (let ((chess-engine-pending-offer 'abort)) (funcall chess-engine-response-handler 'accept))))) (cons "\\S-+ accepts the takeback request\\." (function (lambda () (funcall chess-engine-response-handler 'accept)))) (cons ;; resign announcement "{Game \\([0-9]+\\) (\\(\\S-+\\) vs\\. \\(\\S-+\\)) \\(\\S-+\\) resigns}" (function (lambda () (let ((chess-engine-handling-event t) (opponent-p (not (string= chess-ics-handle (match-string 4)))) (game (chess-ics-game (string-to-number (match-string 1)) :White (match-string 2) :Black (match-string 3)))) (with-current-buffer (chess-game-data game 'engine) (if opponent-p (funcall chess-engine-response-handler 'resign) (unless (chess-game-status game) (chess-game-end game :resign)))) t)))) (cons "\\(\\S-+\\) forfeits on time}" (function (lambda () (if (string= (match-string 1) chess-engine-opponent-name) (funcall chess-engine-response-handler 'flag-fell) (funcall chess-engine-response-handler 'call-flag t))))) (cons "Illegal move (\\([^)]+\\))\\." (function (lambda () (funcall chess-engine-response-handler 'illegal (match-string 1))))) (cons "Challenge: \\(\\S-+\\) \\S-+ \\S-+ \\S-+ .+" (function (lambda () (let ((opponent (match-string 1))) (if (y-or-n-p (chess-string 'want-to-play opponent)) (chess-ics-send (concat "accept " opponent)) (chess-ics-send "decline match")))))) ;; Buttonize URLs. (cons "\"?\\(\\(https?\\|ftp\\)://[^ \t\n\r\"]+\\)\"?" (function (lambda () (make-button (match-beginning 1) (match-end 1) 'action (lambda (button) (browse-url (button-label button)))))))) "An alist of regular expressions to use to scan ICS server output. The car of each element is the regexp to try, and the cdr is a function to run whenever the regexp matches.") (defvar chess-ics-sessions nil "A list of chess-sessions spawned from an Internet Chess Server connection. See `chess-ics-game'.") (make-variable-buffer-local 'chess-ics-sessions) (defun chess-ics-game (game-number &rest tags) "Either create, or retrieve an existing game object with GAME-NUMBER." (cl-assert (integerp game-number)) (cl-assert (or (zerop (logand (length tags) 1)) (eq (car tags) t))) (or ;; First try to find a game which matches the constraints in TAGS (catch 'ics-game (let ((sessions chess-ics-sessions)) (while sessions (if (not (buffer-live-p (caar sessions))) (message "Found dead engine session in `chess-ics-sessions'") (let ((game (chess-engine-game (caar sessions))) (tag-pairs tags)) (when (= game-number (chess-game-data game 'ics-game-number)) (if (or (null tags) (eq (car tags) t)) (throw 'ics-game game) (while tag-pairs (cl-assert (symbolp (car tag-pairs))) (let ((tag (substring (symbol-name (car tag-pairs)) 1)) (val (cadr tag-pairs))) (cl-assert (stringp val)) (if (string= (chess-game-tag game tag) val) (setq tag-pairs (cddr tag-pairs)) (if (not (string= (chess-game-tag game tag) "?")) (message "Game %d %s %s != %s" game-number tag (chess-game-tag game tag) val)) ;; Update tag and proceed (chess-game-set-tag game tag val) (setq tags (cddr tags))))) (throw 'ics-game game))))) (setq sessions (cdr sessions))))) ;; if we are allowed to, create a new session for this game number (unless (eq (car tags) t) (push (let (chess-engine-handling-event) (chess-session 'chess-ics)) chess-ics-sessions) (cl-assert (caar chess-ics-sessions)) (let ((game (chess-engine-game (caar chess-ics-sessions)))) (chess-game-set-data game 'ics-game-number game-number) (chess-game-set-data game 'ics-buffer (current-buffer)) (chess-game-set-tag game "Site" chess-ics-server) (while tags (cl-assert (keywordp (car tags))) (chess-game-set-tag game (substring (symbol-name (car tags)) 1) (cadr tags)) (setq tags (cddr tags))) game)))) (defun chess-ics-game-destroy (game-number &rest tags) (let ((sessions chess-ics-sessions) last-session) (while sessions (if (not (buffer-live-p (caar sessions))) (message "Found dead engine session in `chess-ics-sessions'") (let ((game (chess-display-game (cl-cadar sessions))) (tag-pairs tags) (found t)) (when (= game-number (chess-game-data game 'ics-game-number)) (if (null tags) (progn (chess-display-destroy (cl-cadar sessions)) (if last-session (setcdr last-session (cdr sessions)) (setq chess-ics-sessions (cdr sessions)))) (while (and tag-pairs found) (cl-assert (symbolp (car tag-pairs))) (let ((tag (substring (symbol-name (car tag-pairs)) 1)) (val (cadr tag-pairs))) (cl-assert (stringp val)) (if (string= (chess-game-tag game tag) val) (setq tag-pairs (cddr tag-pairs)) (setq found nil)))) (if (not found) (error "Game not found") (chess-engine-destroy (cl-cadar sessions)) (if last-session (setcdr last-session (cdr sessions)) (setq chess-ics-sessions (cdr sessions)))))))) (setq last-session sessions sessions (cdr sessions))))) (defun chess-ics-handle-movelist-item () ;; TBD: time taken per ply (let ((chess-engine-handling-event t) (seq (string-to-number (match-string 1))) (wmove (match-string 2)) (bmove (match-string 14)) (game chess-ics-movelist-game)) (when game (if (/= (chess-game-seq game) seq) (progn (goto-char (match-beginning 0)) (insert (format "SeqNr. unmatched (%d): " seq))) (when (chess-pos-side-to-move (chess-game-pos game)) (chess-game-move game (chess-algebraic-to-ply (chess-game-pos game) wmove)) (when bmove (chess-game-move game (chess-algebraic-to-ply (chess-game-pos game) bmove)))))) t)) ;; ICS style12 format (with artificial line breaks): ;; ;; <12> rnbqkbnr pppppppp -------- -------- \ ;; -------- -------- PPPPPPPP RNBQKBNR W -1 1 1 1 1 0 \ ;; 65 jwiegley GuestZYNJ 1 5 0 39 39 300 300 1 P/e2-e4 (0:00) e4 0 0 0 (defun chess-ics-handle-style12 () "Handle an ICS Style12 board string." (let* ((chess-engine-handling-event t) (begin (match-beginning 0)) (end (match-end 0)) (position (let ((pos (chess-pos-create t))) (dotimes (r 8) (let ((rank (match-string (1+ r)))) (dotimes (f 8) (unless (= (aref rank f) ?-) (chess-pos-set-piece pos (chess-rf-to-index r f) (aref rank f)))))) (chess-pos-set-side-to-move pos (string= (match-string 9) "W")) (let ((file (string-to-number (match-string 10)))) (when (>= file 0) (chess-pos-set-en-passant pos (chess-rf-to-index (if (chess-pos-side-to-move pos) 3 4) file)))) (mapc (lambda (info) (if (string= (match-string (cdr info)) "1") (chess-pos-set-can-castle pos (car info) t))) '((?K . 11) (?Q . 12) (?k . 13) (?q . 14))) pos)) (game (save-match-data (chess-ics-game (string-to-number (match-string 16)) :White (match-string 17) :Black (match-string 18)))) (status ;; my relation to this game: ;; -3 isolated position, such as for "ref 3" or the "sposition" ;; command ;; -2 I am observing game being examined ;; 2 I am the examiner of this game ;; -1 I am playing, it is my opponent's move ;; 1 I am playing and it is my move ;; 0 I am observing a game being played (string-to-number (match-string 19)))) (when (or (= status 2) (= status -2) (= status 0)) (chess-game-set-data game 'my-color (chess-pos-side-to-move position))) ;; initial time and increment (in seconds) of the match (chess-game-set-tag game "TimeControl" (format "%s/%s" (match-string 20) (match-string 21))) ;; material values for each side (let ((centipawn (* 100 (- (string-to-number (match-string 22)) (string-to-number (match-string 23)))))) (chess-pos-set-epd position 'ce (if (chess-pos-side-to-move position) centipawn (- centipawn)))) ;; White's and Black's remaining time (chess-game-set-data game 'white-remaining (string-to-number (match-string 24))) (chess-game-set-data game 'black-remaining (string-to-number (match-string 25))) (let ((index (- (* (string-to-number (match-string 26)) 2) (if (eq (chess-game-data game 'black-moved-first) t) (if (chess-pos-side-to-move position) 3 2) (if (chess-pos-side-to-move position) 2 1)))) (move (unless (string= (match-string 29) "none") (cl-case (aref (match-string 29) (1- (length (match-string 29)))) (?+ (chess-pos-set-status position :check)) (?# (chess-pos-set-status position :checkmate) (chess-pos-set-epd position 'ce 32767))) ;; jww (2002-04-30): what about stalemate? do I need to ;; calculate this each time? (when nil (chess-pos-set-status position :stalemate)) (match-string 29))) error) (unwind-protect (if move (if (progn (setq error 'comparing-index) (= (1- index) (chess-game-index game))) (let ((ply (progn (setq error 'converting-ply) (chess-algebraic-to-ply (chess-game-pos game) move t)))) ;; each move gives the _position occurring after the ply_ (if (progn (setq error 'comparing-colors) (eq (chess-pos-side-to-move position) (chess-game-data game 'my-color))) (setq error 'applying-opponent-move) (setq error 'applying-my-move)) ;; save us from generating a position we already have (chess-ply-set-keyword ply :next-pos position) (chess-pos-set-preceding-ply position ply) ;; apply the move (chess-game-move game ply) (setq error nil)) (if (= index (chess-game-index game)) ;; this is a refresh, which we can use to verify that our ;; notion of the game's current position is correct (let ((their-fen (chess-pos-to-fen position)) (our-fen (chess-pos-to-fen (chess-game-pos game)))) (if (string= their-fen our-fen) (setq error nil) ; ignore the refresh (setq error (format "comparing-position (%s != %s)" their-fen our-fen)))) (if (and (> index (1+ (chess-game-index game))) (= 1 (chess-game-seq game))) ;; we lack a complete game, try to get it via the ;; movelist (progn (setq error nil) (chess-ics-send (format "moves %d" (chess-game-data game 'ics-game-number)))) (setq error (format "comparing-index (%d:%d)" index (chess-game-index game)))))) ;; no preceeding ply supplied, so this is a starting position (let ((chess-game-inhibit-events t) (color (chess-pos-side-to-move position))) (when (or (= 1 status) (= -1 status)) (chess-game-set-data game 'my-color (if (= 1 status) color (not color))) (chess-game-set-data game 'active t)) (setq error 'setting-start-position) (chess-game-set-start-position game position) (chess-game-set-data game 'black-moved-first (not color))) (setq error 'orienting-board) (chess-game-run-hooks game 'orient) (setq error nil)) (goto-char begin) (if error (insert (chess-string 'failed-ics-parse error)) (delete-region begin end) (save-excursion (while (and (forward-line -1) (or (looking-at "^[ \t]*$") (looking-at "^[^% \t\n\r]+%\\s-*$"))) (delete-region (match-beginning 0) (1+ (match-end 0))))) ;; we need to counter the forward-line in chess-engine-filter (forward-line -1))) t))) (defvar chess-ics-sought-parent-buffer nil "Contains the buffer from which this seektable originates.") (make-variable-buffer-local 'chess-ics-sought-parent-buffer) (defun chess-ics-sought-accept (button) "Perform the action specified by a BUTTON." (let ((buffer (button-get button 'ics-buffer)) (command (button-get button 'ics-command))) (when (and (buffer-live-p buffer) (stringp command)) (chess-ics-send command buffer) t))) (defcustom chess-ics-popup-sought t "If non-nil, display the sought buffer automatically." :type 'boolean) (defcustom chess-ics-sought-buffer-name "*chess-ics-sought*" "The name of the buffer which accumulates seek ads." :type 'string) (define-derived-mode chess-ics-ads-mode tabulated-list-mode "ICSAds" "Mode for displaying sought games from Internet Chess Servers." (setq tabulated-list-format [("Player" 20 t) ("Rating" 10 t :right-align t) ("Rated" 5 nil :right-align t) ("Time" 4 t :right-align t) ("Inc" 4 t) ("Variant" 40 t)]) (setq tabulated-list-entries nil) (tabulated-list-init-header) (tabulated-list-print)) (defun chess-ics-sought-add (id name rating rated time inc variant ics-buffer cmd) (let ((inhibit-redisplay t)) (with-current-buffer (or (get-buffer chess-ics-sought-buffer-name) (with-current-buffer (get-buffer-create chess-ics-sought-buffer-name) (chess-ics-ads-mode) (and chess-ics-popup-sought (display-buffer (current-buffer))) (current-buffer))) (setq chess-ics-sought-parent-buffer ics-buffer) (add-to-list 'tabulated-list-entries (list id (vector (list name 'ics-buffer ics-buffer 'ics-command cmd 'action #'chess-ics-sought-accept) (number-to-string rating) rated (number-to-string time) (number-to-string inc) variant))) (tabulated-list-revert)))) (defun chess-ics-seeking (string) ;; jww (2008-09-02): we should use rx for this regular expression also (while (string-match (concat "[\n\r]+\\(\\S-+\\) (\\([0-9+ -]+\\)) seeking \\([a-z]\\S-+ \\)?\\([0-9]+\\) \\([0-9]+\\) \\(\\(un\\)?rated\\) \\([^(]*\\)(\"\\([^\"]+\\)\" to respond)\\s-*[\n\r]+" chess-ics-prompt-regexp) string) (let* ((pre (substring string 0 (match-beginning 0))) (post (substring string (match-end 0)))) (chess-ics-sought-add (string-to-number (substring (match-string 9 string) 5)) (match-string 1 string) (string-to-number (match-string 2 string)) (if (string= (match-string 6 string) "rated") "yes" "no") (string-to-number (match-string 4 string)) (string-to-number (match-string 5 string)) (concat (if (match-string 3 string) (concat (match-string 3 string) " ") "") (match-string 8 string)) (current-buffer) (match-string 9 string)) (setq string (concat pre post)))) string) (defun chess-ics-ads-removed (string) "Look for Seek ad removal announcements in the output stream. This function should be put on `comint-preoutput-filter-functions'." (let (ids) (while (string-match (concat "[\n\r]+Ads removed: \\([0-9 ]+\\)\\s-*[\n\r]+" chess-ics-prompt-regexp) string) (setq ids (append (mapcar #'string-to-number (save-match-data (split-string (match-string 1 string) " +"))) ids) string (concat (substring string 0 (match-beginning 0)) (substring string (match-end 0))))) (when ids (let ((buf (get-buffer chess-ics-sought-buffer-name)) (inhibit-redisplay t)) (when (buffer-live-p buf) (with-current-buffer buf (let ((old-length (length tabulated-list-entries))) (setq tabulated-list-entries (cl-remove-if (lambda (entry) (member (car entry) ids)) tabulated-list-entries)) (when (/= (length tabulated-list-entries) old-length) (tabulated-list-revert)))))))) string) ;; FIXME: This is evil! ;;(make-variable-buffer-local 'comint-preoutput-filter-functions) ;;;###autoload (defun chess-ics (server port &optional handle password-or-filename helper &rest helper-args) "Connect to an Internet Chess Server." (interactive (let ((args (if (= (length chess-ics-server-list) 1) (car chess-ics-server-list) (assoc (completing-read (chess-string 'ics-server-prompt) chess-ics-server-list nil t (caar chess-ics-server-list)) chess-ics-server-list)))) (if (and (nth 2 args) (not (nth 3 args))) (append (list (nth 0 args) (nth 1 args) (nth 2 args) (read-passwd "Password: ") (nth 4 args)) (nthcdr 5 args)) args))) (unless handle (setq handle "guest")) (chess-message 'ics-connecting server) (let ((buf (if helper (apply #'make-comint "chess-ics" helper nil helper-args) (make-comint "chess-ics" (cons server port))))) (chess-message 'ics-connected server) (set-buffer buf) (setq chess-ics-server server chess-ics-handle handle chess-ics-password (if (and password-or-filename (file-readable-p password-or-filename)) (with-temp-buffer (insert-file-contents password-or-filename) (buffer-string)) password-or-filename) chess-ics-handling-login t chess-engine-regexp-alist (copy-alist chess-ics-matcher-alist) comint-prompt-regexp "^[^%\n]*% *" comint-scroll-show-maximum-output t) (add-hook 'comint-output-filter-functions #'chess-engine-filter t t) (add-hook 'comint-preoutput-filter-functions #'chess-ics-seeking nil t) (add-hook 'comint-preoutput-filter-functions #'chess-ics-ads-removed nil t) (let ((ntimes 50)) (while (and chess-ics-handling-login (> (setq ntimes (1- ntimes)) 0)) (accept-process-output (get-buffer-process (current-buffer)) 0 100))) (switch-to-buffer buf))) ;;;###autoload (define-key menu-bar-games-menu [chess-ics] '(menu-item "Internet Chess Servers" chess-ics :help "Play Chess on the Internet")) ;;; ICC datagrams ;; See http://www.chessclub.com/resources/formats/formats.txt (defvar chess-icc-unprocessed nil) (defun chess-icc-datagram-handler (string) (if (not (string-match "^\\([0-9]+\\) \\(.*\\)$" string)) (format "\nUnknown datagram format: %s\n" string) (let ((chess-engine-handling-event t) (dg (string-to-number (match-string 1 string))) (args (match-string 2 string))) (cond ((and (or (= dg 22) (= dg 23)) (string-match "\\([0-9]+\\) \\([1-9][0-9]*\\)" args)) (chess-game-undo (chess-ics-game (string-to-number (match-string 1 args))) (string-to-number (match-string 2 args))) "") ((and (or (= dg 101) (= dg 110)) (string-match "\\([0-9]+\\) {\\(.+\\) \\(?:[0-9]+\\) \\(?:[0-9]+\\)} \\([0-9]+\\)" args)) (let ((pos (chess-fen-to-pos (match-string 2 args)))) (chess-game-set-start-position (chess-ics-game (string-to-number (match-string 1 args))) pos)) "") ((and (or (= dg 24) (= dg 111)) (string-match "^\\([0-9]+\\) \\(.+\\)$" args)) (let* ((move (match-string 2 args)) (game (chess-ics-game (string-to-number (match-string 1 args)))) (pos (chess-game-pos game)) (ply (chess-algebraic-to-ply pos move))) (chess-game-move game ply) "")) ((and (= dg 26) (string-match "^\\([0-9]+\\) \\(\\S-+\\) {\\([^}]*\\)} \\([01]\\) {\\(.*\\)}" args)) (let ((game-number (match-string 1 args)) (action (if (string= (match-string 4 args) "1") "kibitzes" "whispers")) (name (match-string 2 args)) (titles (match-string 3 args)) (text (match-string 5 args))) (setq name (concat name (mapconcat (lambda (title) (concat "(" title ")")) (split-string titles " ") ""))) (format "\n%s[%s] %s: %s\n" name game-number action text))) ((and (= dg 56) (string-match "^\\([0-9]+\\) \\([WB]\\) \\([0-9]+\\) \\([01]\\)" args)) (let ((sec (/ (string-to-number (match-string 3 args)) 1000)) (color (if (string= (match-string 2 args) "W") 'white-remaining 'black-remaining)) (game (chess-ics-game (string-to-number (match-string 1 args))))) (chess-game-set-data game color sec)) "") ((and (= dg 50) (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)) (chess-ics-sought-add (string-to-number (match-string 1 args)) (concat (match-string 2 args) (if (not (string= (match-string 3 args) "")) (format "(%s)" (match-string 3 args)) "")) (string-to-number (match-string 4 args)) (if (string= (match-string 10 args) "1") "yes" "no") (string-to-number (match-string 8 args)) (string-to-number (match-string 9 args)) (concat (match-string 7 args) (if (not (string= (match-string 6 args) "0")) (concat " " (match-string 6 args)) "") (if (string= (match-string 14 args) "0") " m" "") (if (string= (match-string 15 args) "1") " f" "")) (current-buffer) (concat "play " (match-string 1 args))) "") ((= dg 51) (let ((id (string-to-number (car (split-string args " +")))) (buf (get-buffer chess-ics-sought-buffer-name))) (when (buffer-live-p buf) (with-current-buffer buf (setq tabulated-list-entries (cl-remove-if (lambda (entry) (equal (car entry) id)) tabulated-list-entries)) (tabulated-list-revert)))) "") (t (format "\nIgnoring unhandled datagram DG%03d: %s\n" dg args)))))) (defun chess-icc-preoutput-filter (string) (if chess-icc-unprocessed (let ((string (concat chess-icc-unprocessed string))) (if (string-match ")" string) (let ((newstr (unwind-protect (chess-icc-datagram-handler (substring string 0 (match-beginning 0))) (setq chess-icc-unprocessed nil)))) (chess-icc-preoutput-filter (concat (or newstr "") (substring string (match-end 0))))) (setq chess-icc-unprocessed string) "")) (if (string-match "(" string) (let ((pre (substring string 0 (match-beginning 0))) (substr (substring string (match-end 0)))) (if (string-match ")" substr) (let ((post (substring substr (match-end 0))) (newstr (chess-icc-datagram-handler (substring substr 0 (match-beginning 0))))) (chess-icc-preoutput-filter (concat pre newstr post))) (setq chess-icc-unprocessed substr) pre)) string))) (defun chess-ics-icc-preoutput-filter (string) (while (string-match "(\\([0-9]+\\) \\(.*?\\))" string) (let ((dg (string-to-number (match-string 1 string))) (args (match-string 2 string)) (pre (substring string 0 (match-beginning 0))) (post (substring string (match-end 0)))) (cond ((and (or (= dg 101) (= dg 110)) (string-match "\\([0-9]+\\) {\\(.+\\) \\(?:[0-9]+\\) \\(?:[0-9]+\\)} \\([0-9]+\\)" args)) (let ((pos (chess-fen-to-pos (match-string 2 args)))) (chess-game-set-start-position (chess-ics-game (string-to-number (match-string 1 args))) pos)) (setq string (concat pre post))) ((and (or (= dg 24) (= dg 111)) (string-match "\\([0-9]+\\) \\(.+\\)$" args)) (let* ((chess-engine-handling-event t) (move (match-string 2 args)) (game (chess-ics-game (string-to-number (match-string 1 args)))) (pos (chess-game-pos game)) (ply (chess-algebraic-to-ply pos move))) (if ply (chess-game-move game ply) (setq pre (format "%s\nunable to apply move %s\n" pre move)))) (setq string (concat pre post))) ((and (= dg 26) (string-match "\\([0-9]+\\) \\(\\S-+\\) {\\([^}]*\\)} \\([01]\\) {\\(.*\\)}" args)) (let ((game-number (match-string 1 args)) (action (if (string= (match-string 4 args) "1") "kibitzes" "whispers")) (name (match-string 2 args)) (titles (match-string 3 args)) (text (match-string 5 args))) (setq name (concat name (mapconcat (lambda (title) (concat "(" title ")")) (split-string titles " ") ""))) (setq string (format "%s\n%s[%s] %s: %s\n%s" pre name game-number action text post)))) ((and (= dg 56) (string-match "\\([0-9]+\\) \\([WB]\\) \\([0-9]+\\) \\([01]\\)" args)) (let ((sec (/ (string-to-number (match-string 3 args)) 1000)) (color (if (string= (match-string 2 args) "W") 'white-remaining 'black-remaining)) (game (chess-ics-game (string-to-number (match-string 1 args))))) (chess-game-set-data game color sec)) (setq string (concat pre post))) ((and (= dg 50) (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)) (chess-ics-sought-add (match-string 1 args) (concat (match-string 2 args) (if (not (string= (match-string 3 args) "")) (format "(%s)" (match-string 3 args)) "")) (string-to-number (match-string 4 args)) (if (string= (match-string 10 args) "1") "yes" "no") (string-to-number (match-string 8 args)) (string-to-number (match-string 9 args)) (concat (match-string 7 args) (if (not (string= (match-string 6 args) "0")) (concat " " (match-string 6 args)) "") (if (string= (match-string 14 args) "0") " m" "") (if (string= (match-string 15 args) "1") " f" "")) (current-buffer) (concat "play " (match-string 1 args))) (setq string (concat pre post))) ((= dg 51) (let ((id (car (split-string args " "))) (buf (get-buffer chess-ics-sought-buffer-name))) (when (buffer-live-p buf) (with-current-buffer buf (let ((here (point))) (goto-char (point-min)) (when (re-search-forward (concat "^" id " ") nil t) (delete-region (line-beginning-position) (1+ (line-end-position)))) (goto-char here))))) (setq string (concat pre post))) (t (message "Ignoring Datagram %03d: %s" dg args) (setq string (concat pre post)))))) string) (defun chess-ics-handler (game event &rest args) (unless chess-engine-handling-event (cond ((eq event 'initialize)) ((eq event 'ready) (chess-game-run-hooks game 'announce-autosave)) ((eq event 'busy)) ; ICS will inform them ((eq event 'match) (setq chess-engine-pending-offer 'match) (chess-engine-send nil (format "match %s\n" (read-string (chess-string 'challenge-whom))))) ;; we need to send long algebraic notation to the ICS server, not short ((eq event 'move) (let ((ply (car args))) (chess-ics-send (if (chess-ply-any-keyword ply :castle :long-castle) (chess-ply-to-algebraic ply) (concat (chess-index-to-coord (chess-ply-source ply)) "-" (chess-index-to-coord (chess-ply-target ply)) (if (characterp (chess-ply-keyword ply :promote)) (format "=%c" (chess-ply-keyword ply :promote)) ""))) (chess-game-data game 'ics-buffer))) (if (chess-game-over-p game) (chess-game-set-data game 'active nil))) ((eq event 'flag-fell) (chess-common-handler game 'flag-fell)) ((eq event 'forward) (chess-ics-send "forward" (chess-game-data game 'ics-buffer))) ((eq event 'undo) (chess-ics-send (format "takeback %d" (car args)) (chess-game-data game 'ics-buffer))) ((eq event 'abort) (chess-ics-send "abort" (chess-game-data game 'ics-buffer))) ((eq event 'call-flag) (chess-ics-send "flag" (chess-game-data game 'ics-buffer))) ((eq event 'draw) (chess-ics-send "draw" (chess-game-data game 'ics-buffer))) ((eq event 'resign) (chess-ics-send "resign" (chess-game-data game 'ics-buffer))) (t (apply #'chess-network-handler game event args))))) (provide 'chess-ics) ;;; chess-ics.el ends here