1 ;;; chess-pgn.el --- Convert a chess game to/from Portable Game Notation (PGN)
3 ;; Copyright (C) 2002, 2004, 2008, 2014 Free Software Foundation, Inc.
5 ;; Author: John Wiegley <johnw@gnu.org>
6 ;; Maintainer: Mario Lang <mlang@delysid.org>
7 ;; Keywords: files, games
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 ;; Portable Game Notation (PGN) is a plain text computer-processible format for
25 ;; recording chess games (both the moves and related data).
27 ;; Here is a sample game in PGN format:
29 ;; [Event "F/S Return Match"]
30 ;; [Site "Belgrade, Serbia Yugoslavia|JUG"]
31 ;; [Date "1992.11.04"]
33 ;; [White "Fischer, Robert J."]
34 ;; [Black "Spassky, Boris V."]
37 ;; 1. e4 e5 2. Nf3 Nc6 3. Bb5 a6 {This opening is called the Ruy Lopez.}
38 ;; 4. Ba4 Nf6 5. O-O Be7 6. Re1 b5 7. Bb3 d6 8. c3 O-O 9. h3 Nb8 10. d4 Nbd7
39 ;; 11. c4 c6 12. cxb5 axb5 13. Nc3 Bb7 14. Bg5 b4 15. Nb1 h6 16. Bh4 c5 17. dxe5
40 ;; Nxe4 18. Bxe7 Qxe7 19. exd6 Qf6 20. Nbd2 Nxd6 21. Nc4 Nxc4 22. Bxc4 Nb6
41 ;; 23. Ne5 Rae8 24. Bxf7+ Rxf7 25. Nxf7 Rxe1+ 26. Qxe1 Kxf7 27. Qe3 Qg5 28. Qxg5
42 ;; hxg5 29. b3 Ke6 30. a3 Kd6 31. axb4 cxb4 32. Ra5 Nd5 33. f3 Bc8 34. Kf2 Bf5
43 ;; 35. Ra7 g6 36. Ra6+ Kc5 37. Ke1 Nf4 38. g3 Nxh3 39. Kd2 Kb5 40. Rd6 Kc5 41. Ra6
44 ;; Nf2 42. g4 Bd3 43. Re6 1/2-1/2
46 ;; This module provides functions for converting to/from PGN format:
51 ;; and a mode for viewing/editing PGN files:
58 (require 'chess-algebraic)
59 (require 'chess-display)
63 (require 'chess-message)
68 (defvar chess-pgn-fill-column 60)
70 (chess-message-catalog 'english
71 '((pgn-read-error . "Error reading move: %s")
72 (pgn-parse-error . "Error parsing PGN syntax")))
74 (defun chess-pgn-read-plies (game position &optional top-level)
75 (let ((plies (list t)) (begin (point)) move-beg prevpos)
79 ((looking-at "[1-9][0-9]*\\.[. ]*")
80 (goto-char (match-end 0)))
82 ((looking-at chess-algebraic-regexp-ws)
83 (setq move-beg (point))
84 (goto-char (match-end 0))
85 (skip-syntax-backward " ")
86 (setq prevpos position)
87 (let* ((move (buffer-substring-no-properties move-beg (point)))
88 (ply (condition-case err
89 (chess-algebraic-to-ply position move)
91 (message "PGN: %s" (buffer-substring begin (point-max)))
92 (error (error-message-string err))))))
94 (chess-error 'pgn-read-error move))
95 (setq position (chess-ply-next-pos ply))
96 (nconc plies (list ply))))
99 (looking-at "\\(\\*\\|1-0\\|0-1\\|1/2-1/2\\)"))
100 (goto-char (match-end 0))
101 (chess-game-set-tag game "Result" (match-string-no-properties 0))
102 (unless (eq t (car (last plies)))
104 ((string= "1/2-1/2" (match-string 1))
105 (nconc plies (list (chess-ply-create
106 (chess-ply-next-pos (car (last plies)))
109 (nconc plies (list (chess-ply-create*
110 (chess-ply-next-pos (car (last plies)))))))))
115 (let ((begin (point)))
118 (chess-pos-add-annotation position (buffer-substring-no-properties
119 begin (- (point) 2)))))
122 (skip-chars-forward " \t\n")
123 (chess-pos-add-annotation prevpos
124 (chess-pgn-read-plies game prevpos)))
126 ((and (not top-level)
132 (if (eq t (car (last plies)))
133 (error "PGN parser: Expected a ply here: '%s'"
134 (buffer-substring (point) (point-max))))
135 (nconc plies (list (chess-ply-create*
136 (chess-ply-next-pos (car (last plies))))))
138 (skip-chars-forward " \t\n\r")))
141 (defun chess-pgn-to-game (&optional string)
142 "Convert PGN notation at point into a chess game.
143 Optionally use the supplied STRING instead of the current buffer."
147 (goto-char (point-min))
151 (defun chess-pgn-parse ()
152 (if (or (looking-at "\\[")
153 (and (search-forward "[" nil t)
154 (goto-char (match-beginning 0))))
155 (let ((game (chess-game-create)))
156 (chess-game-set-tags game nil)
157 (while (looking-at (rx
158 ?[ (group (one-or-more (not (syntax whitespace))))
159 (one-or-more (syntax whitespace))
160 (syntax string-quote)
161 (group (*? not-newline))
162 (syntax string-quote)
164 (one-or-more (char ? ?\n ?\r ?\t))))
165 (chess-game-set-tag game (match-string-no-properties 1)
166 (match-string-no-properties 2))
167 (goto-char (match-end 0)))
168 (let ((fen (chess-game-tag game "FEN")))
170 (chess-game-set-start-position game (chess-fen-to-pos fen))))
171 (chess-game-set-plies game (chess-pgn-read-plies game (chess-game-pos game) t))
173 (error "Data not in legal PGN format: '%s'"
174 (buffer-substring (point) (point-max)))))
176 (defun chess-pgn-insert-annotations (game index ply)
177 (dolist (ann (chess-pos-annotations (chess-ply-pos ply)))
179 (insert "\n{" ann "}")
180 (cl-assert (listp ann))
181 (chess-pgn-insert-plies game index ann))))
183 (defun chess-pgn-insert-plies (game index plies &optional
184 for-black indented no-annotations)
185 "NYI: Still have to implement INDENTED argument."
188 (when (chess-ply-changes (car plies))
189 (if (> (current-column) chess-pgn-fill-column)
191 (insert (format "%d. %s" index (chess-ply-to-algebraic (car plies))))
192 (unless no-annotations
193 (chess-pgn-insert-annotations game index (car plies))))
194 (setq plies (cdr plies) index (1+ index)))
196 (when (chess-ply-changes (car plies))
198 (if (> (current-column) chess-pgn-fill-column)
200 (insert (format "%d. ..." index))
201 (setq for-black nil))
202 (insert (format " %s" (chess-ply-to-algebraic (car plies))))
203 (unless no-annotations
204 (chess-pgn-insert-annotations game index (car plies))))
205 (setq plies (cdr plies)))
209 (defvar chess-pgn-tag-order
210 '("Event" "Site" "Date" "Round"
211 "White" "WhiteElo" "Black" "BlackElo"
212 "Result" "TimeControl"))
214 (defun chess-game-to-pgn (game &optional indented to-string)
215 "Convert a chess GAME to PGN notation.
216 If INDENTED is non-nil, indent the move texts.
217 If TO-STRING is non-nil, return a string instead of inserting the resulting
221 (chess-insert-pgn game indented)
223 (chess-insert-pgn game indented)))
225 (defun chess-member-index (tag)
227 (tags chess-pgn-tag-order))
229 (if (equal tag (car tags))
231 (setq index (1+ index)
235 (defun chess-insert-pgn (game &optional indented)
236 (let ((fen (chess-game-tag game "FEN"))
237 (first-pos (chess-game-pos game 0)))
238 (when (and fen (not (string= fen (chess-pos-to-fen first-pos))))
239 (chess-game-del-tag game "FEN")
242 (not (eq chess-starting-position first-pos)))
243 (chess-game-set-tag game "FEN" (chess-pos-to-fen first-pos))))
244 (dolist (tag (sort (copy-alist (chess-game-tags game))
247 (setq left (car left) right (car right))
248 (let ((l-idx (chess-member-index left))
249 (r-idx (chess-member-index right)))
251 ((and l-idx (not r-idx)) t)
252 ((and (not l-idx) r-idx) nil)
253 ((and l-idx r-idx) (< l-idx r-idx))
254 (t (string-lessp left right))))))))
255 (insert (format "[%s \"%s\"]\n" (car tag) (cdr tag))))
257 (chess-pgn-insert-plies game 1 (chess-game-plies game))
258 (insert (or (chess-game-tag game "Result") "*") ?\n))
260 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
262 ;; chess-pgn-mode for editing and browsing PGN files.
265 (require 'chess-database)
267 (defvar chess-pgn-database nil
268 "Chess database object.")
269 (make-variable-buffer-local 'chess-pgn-database)
271 (defvar chess-pgn-display nil
272 "If non-nil, the chess display object used for this buffer.")
273 (make-variable-buffer-local 'chess-pgn-display)
275 (defvar chess-pgn-current-game)
276 (defvar chess-pgn-current-index)
278 (make-variable-buffer-local 'chess-pgn-current-game)
279 (make-variable-buffer-local 'chess-pgn-current-index)
281 (chess-message-catalog 'english
282 '((could-not-read-pgn . "Could not read or find a PGN game")))
285 (defun chess-pgn-read (&optional file)
286 "Read and display a PGN game after point."
288 (if (or file (not (search-forward "[Event " nil t)))
289 (setq file (read-file-name "Read a PGN game from file: ")))
292 (let ((game (chess-pgn-to-game)))
294 (chess-display-set-game
295 (setq chess-pgn-display (chess-create-display t))
297 (chess-error 'could-not-read-pgn))))
299 (defvar chess-pgn-mode-map
300 (let ((map (make-sparse-keymap)))
301 (set-keymap-parent map text-mode-map)
302 (define-key map [(control ?c) (control ?c)] 'chess-pgn-show-position)
303 (define-key map [mouse-2] 'chess-pgn-mouse-show-position)
305 ;;(define-key map [(control ?m)] 'chess-pgn-move)
306 ;;(define-key map [space] 'chess-pgn-move)
307 (define-key map [? ] 'chess-pgn-insert-and-show-position)
308 (define-key map [tab] 'chess-pgn-complete-move)
312 (define-derived-mode chess-pgn-mode text-mode "PGN"
313 "A mode for editing chess PGN files."
314 (setq comment-start "{"
317 (modify-syntax-entry ?\{ "<")
318 (modify-syntax-entry ?\} ">")
319 (modify-syntax-entry ?\" "\"")
321 (if (fboundp 'font-lock-mode)
323 (set (make-local-variable 'pcomplete-default-completion-function)
324 'chess-pgn-completions)
325 (set (make-local-variable 'pcomplete-command-completion-function)
326 'chess-pgn-completions)
327 (set (make-local-variable 'pcomplete-parse-arguments-function)
328 'chess-pgn-current-word))
331 (defalias 'pgn-mode 'chess-pgn-mode)
333 (defvar chess-pgn-bold-face 'bold)
335 (defconst chess-pgn-move-regexp
336 (concat "[^0-9]\\(\\([1-9][0-9]*\\)\\.\\s-+"
337 "\\(\\.\\.\\.\\|" chess-algebraic-regexp "\\)"
338 "\\(\\s-+\\(" chess-algebraic-regexp "\\)\\)?\\)"))
340 (if (fboundp 'font-lock-add-keywords)
341 (font-lock-add-keywords
343 (list (list "\\[\\(\\S-+\\)\\s-+\".*\"\\]" 1 'font-lock-keyword-face)
344 (cons "\\(1-0\\|0-1\\|1/2-1/2\\|\\*\\)$" 'chess-pgn-bold-face))))
347 (add-to-list 'auto-mode-alist '("\\.pgn\\'" . chess-pgn-mode))
349 (eval-after-load "mm-decode"
350 '(unless (fboundp 'mm-display-pgn-inline)
351 (defun mm-display-pgn-inline (handle)
352 (mm-display-inline-fontify handle 'chess-pgn-mode))
353 (push '("application/x-chess-pgn" mm-display-pgn-inline identity)
354 mm-inline-media-tests)
355 (push "application/x-chess-pgn" mm-inlined-types)
356 (push "application/x-chess-pgn" mm-automatic-display)))
358 (defun chess-pgn-completions ()
359 "Return a list of possible completions for the current move."
360 (let ((position (chess-game-pos chess-pgn-current-game
361 chess-pgn-current-index)))
362 (while (pcomplete-here
363 (mapcar 'chess-ply-to-algebraic
364 (chess-legal-plies position :color
365 (chess-pos-side-to-move position)))))))
367 (defun chess-pgn-current-word ()
368 (let ((here (point)))
369 (if (setq chess-pgn-current-index (chess-pgn-index))
371 (narrow-to-region (match-beginning 3) here)
372 (pcomplete-parse-buffer-arguments)))))
374 (defun chess-pgn-complete-move ()
377 (narrow-to-region (point-min) (point))
378 (chess-pgn-read-game))
379 (if (eq last-command 'chess-pgn-complete-move)
380 (setq last-command 'pcomplete))
381 (call-interactively 'pcomplete))
383 (defun chess-pgn-index (&optional location)
384 "Return the move index associated with point."
386 (when location (goto-char location))
387 (if (re-search-backward chess-pgn-move-regexp nil t)
388 (let* ((index (string-to-number (match-string 2)))
389 ;; (first-move (match-string 3))
390 (second-move (match-string 14))
391 (ply (1+ (* 2 (1- index)))))
396 (defvar chess-file-locations nil)
398 (defun chess-pgn-read-game ()
399 "Load a database to represent this file if not already up."
400 (unless chess-pgn-database
401 (setq chess-pgn-database
402 (chess-database-open buffer-file-name 'chess-file)))
404 ;; a hack for speed's sake to read the current game text
406 (let ((locations chess-file-locations)
410 (if (> (car locations) here)
412 (setq last-location locations
413 locations (cdr locations))))
414 (setq index (if last-location
415 (- (length chess-file-locations) (length last-location))
417 (when (or (null chess-pgn-current-game)
418 (/= index (chess-game-data chess-pgn-current-game
420 (setq chess-pgn-current-game
421 (chess-database-read chess-pgn-database index))))))
423 (defvar chess-game-inhibit-events)
425 (defun chess-pgn-create-display ()
426 "Return the move index associated with point."
427 ;; now find what position we're at in the game
429 (when chess-pgn-current-game
430 (let ((index (chess-pgn-index)))
431 (if (or (and (or (null chess-pgn-display)
432 (not (buffer-live-p chess-pgn-display)))
433 (let ((chess-game-inhibit-events t))
434 (setq chess-pgn-display (chess-create-display t))))
435 (/= (chess-game-data chess-pgn-current-game 'database-index)
436 (or (chess-game-data (chess-display-game chess-pgn-display)
437 'database-index) -1)))
439 (chess-display-disable-popup chess-pgn-display)
440 (chess-display-set-game chess-pgn-display
441 chess-pgn-current-game index)
442 (chess-game-set-tag (chess-display-game chess-pgn-display)
444 (chess-game-data chess-pgn-current-game
446 (chess-display-set-index chess-pgn-display index))
447 (chess-display-popup chess-pgn-display)))))
449 (defun chess-pgn-visualize ()
450 "Visualize the move for the PGN game under point.
451 This does not require that the buffer be in PGN mode."
454 (if (search-backward "[Event " nil t)
455 (setq game (chess-pgn-to-game))))
457 (let ((chess-pgn-current-game game))
458 (chess-pgn-show-position))
459 (chess-error 'could-not-read-pgn))))
461 (defun chess-pgn-show-position ()
463 (if (not (eq major-mode 'chess-pgn-mode))
464 (chess-pgn-visualize)
465 (chess-pgn-read-game)
466 (chess-pgn-create-display)))
468 (defun chess-pgn-mouse-show-position (event)
470 (if (fboundp 'event-window) ; XEmacs
472 (set-buffer (window-buffer (event-window event)))
473 (and (event-point event) (goto-char (event-point event))))
474 (set-buffer (window-buffer (posn-window (event-start event))))
475 (goto-char (posn-point (event-start event))))
476 (chess-pgn-show-position))
478 (defun chess-pgn-insert-and-show-position ()
480 (self-insert-command 1)
481 (chess-pgn-show-position))
485 ;;; chess-pgn.el ends here