Sync to HEAD
[bpt/emacs.git] / lisp / play / mpuz.el
CommitLineData
6594deb0
ER
1;;; mpuz.el --- multiplication puzzle for GNU Emacs
2
d93196b3 3;; Copyright (C) 1990, 2002 Free Software Foundation, Inc.
0aba61c5 4
2b38b487 5;; Author: Philippe Schnoebelen <phs@lsv.ens-cachan.fr>
d93196b3 6;; Overhauled: Daniel Pfeiffer <occitan@esperanto.org>
84176303
ER
7;; Keywords: games
8
5f7e5584
RS
9;; This file is part of GNU Emacs.
10
59243403
RS
11;; GNU Emacs is free software; you can redistribute it and/or modify
12;; it under the terms of the GNU General Public License as published by
13;; the Free Software Foundation; either version 2, or (at your option)
14;; any later version.
15
5f7e5584 16;; GNU Emacs is distributed in the hope that it will be useful,
59243403
RS
17;; but WITHOUT ANY WARRANTY; without even the implied warranty of
18;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19;; GNU General Public License for more details.
20
21;; You should have received a copy of the GNU General Public License
b578f267
EN
22;; along with GNU Emacs; see the file COPYING. If not, write to the
23;; Free Software Foundation, Inc., 59 Temple Place - Suite 330,
24;; Boston, MA 02111-1307, USA.
5f7e5584 25
edbd2f74
ER
26;;; Commentary:
27
d93196b3
EZ
28;; `M-x mpuz' generates a random multiplication puzzle. This is a
29;; multiplication example in which each digit has been consistently replaced
30;; with some letter. Your job is to reconstruct the original digits. Type
31;; `?' while the mode is active for detailed help.
edbd2f74 32
84176303
ER
33;;; Code:
34
323f7c49
SE
35(defgroup mpuz nil
36 "Multiplication puzzle."
37 :prefix "mpuz-"
38 :group 'games)
39
5f7e5584
RS
40(random t) ; randomize
41
d93196b3 42(defcustom mpuz-silent 'error
8101c2f2
JB
43 "*Set this to nil if you want dings on inputs.
44t means never ding, and `error' means only ding on wrong input."
d93196b3
EZ
45 :type '(choice (const :tag "No" nil)
46 (const :tag "Yes" t)
47 (const :tag "If correct" error))
48 :group 'mpuz)
49
50(defcustom mpuz-solve-when-trivial t
51 "*Solve any row that can be trivially calculated from what you've found."
323f7c49
SE
52 :type 'boolean
53 :group 'mpuz)
5f7e5584 54
d93196b3
EZ
55(defcustom mpuz-allow-double-multiplicator nil
56 "*Allow 2nd factors like 33 or 77."
57 :type 'boolean
58 :group 'mpuz)
59
a0db0bbf
EZ
60(defface mpuz-unsolved-face
61 '((((class color)) (:foreground "red1" :bold t))
62 (t (:bold t)))
d93196b3 63 "*Face to use for letters to be solved."
d93196b3
EZ
64 :group 'mpuz)
65
a0db0bbf
EZ
66(defface mpuz-solved-face
67 '((((class color)) (:foreground "green1" :bold t))
68 (t (:bold t)))
d93196b3 69 "*Face to use for solved digits."
d93196b3
EZ
70 :group 'mpuz)
71
a0db0bbf
EZ
72(defface mpuz-trivial-face
73 '((((class color)) (:foreground "blue" :bold t))
74 (t (:bold t)))
d93196b3 75 "*Face to use for trivial digits solved for you."
d93196b3
EZ
76 :group 'mpuz)
77
a0db0bbf
EZ
78(defface mpuz-text-face
79 '((t (:inherit variable-pitch)))
d93196b3 80 "*Face to use for text on right."
d93196b3 81 :group 'mpuz)
5f7e5584
RS
82
83\f
84;; Mpuz mode and keymaps
85;;----------------------
323f7c49
SE
86(defcustom mpuz-mode-hook nil
87 "Hook to run upon entry to mpuz."
88 :type 'hook
89 :group 'mpuz)
5f7e5584
RS
90
91(defvar mpuz-mode-map nil
92 "Local keymap to use in Mult Puzzle.")
93
5f7e5584 94(if mpuz-mode-map nil
d93196b3
EZ
95 (setq mpuz-mode-map (make-sparse-keymap))
96 (define-key mpuz-mode-map "a" 'mpuz-try-letter)
97 (define-key mpuz-mode-map "b" 'mpuz-try-letter)
98 (define-key mpuz-mode-map "c" 'mpuz-try-letter)
99 (define-key mpuz-mode-map "d" 'mpuz-try-letter)
100 (define-key mpuz-mode-map "e" 'mpuz-try-letter)
101 (define-key mpuz-mode-map "f" 'mpuz-try-letter)
102 (define-key mpuz-mode-map "g" 'mpuz-try-letter)
103 (define-key mpuz-mode-map "h" 'mpuz-try-letter)
104 (define-key mpuz-mode-map "i" 'mpuz-try-letter)
105 (define-key mpuz-mode-map "j" 'mpuz-try-letter)
106 (define-key mpuz-mode-map "A" 'mpuz-try-letter)
107 (define-key mpuz-mode-map "B" 'mpuz-try-letter)
108 (define-key mpuz-mode-map "C" 'mpuz-try-letter)
109 (define-key mpuz-mode-map "D" 'mpuz-try-letter)
110 (define-key mpuz-mode-map "E" 'mpuz-try-letter)
111 (define-key mpuz-mode-map "F" 'mpuz-try-letter)
112 (define-key mpuz-mode-map "G" 'mpuz-try-letter)
113 (define-key mpuz-mode-map "H" 'mpuz-try-letter)
114 (define-key mpuz-mode-map "I" 'mpuz-try-letter)
115 (define-key mpuz-mode-map "J" 'mpuz-try-letter)
116 (define-key mpuz-mode-map "\C-g" 'mpuz-offer-abort)
117 (define-key mpuz-mode-map "?" 'describe-mode))
5f7e5584 118
5f7e5584 119(defun mpuz-mode ()
58c43274 120 "Multiplication puzzle mode.
5f7e5584 121
a0963566 122You have to guess which letters stand for which digits in the
58c43274 123multiplication displayed inside the `*Mult Puzzle*' buffer.
5f7e5584 124
58c43274 125You may enter a guess for a letter's value by typing first the letter,
d93196b3 126then the digit. Thus, to guess that A=3, type `A 3'.
5f7e5584 127
58c43274
RS
128To leave the game to do other editing work, just switch buffers.
129Then you may resume the game with M-x mpuz.
130You may abort a game by typing \\<mpuz-mode-map>\\[mpuz-offer-abort]."
5f7e5584
RS
131 (interactive)
132 (setq major-mode 'mpuz-mode
d93196b3
EZ
133 mode-name "Mult Puzzle"
134 tab-width 30)
5f7e5584
RS
135 (use-local-map mpuz-mode-map)
136 (run-hooks 'mpuz-mode-hook))
137
138\f
139;; Some variables for statistics
140;;------------------------------
141(defvar mpuz-nb-errors 0
a0963566 142 "Number of errors made in current game.")
5f7e5584
RS
143
144(defvar mpuz-nb-completed-games 0
a0963566 145 "Number of games completed.")
5f7e5584
RS
146
147(defvar mpuz-nb-cumulated-errors 0
148 "Number of errors made in previous games.")
149
150
151;; Some variables for game tracking
152;;---------------------------------
153(defvar mpuz-in-progress nil
154 "True if a game is currently in progress.")
155
d93196b3 156(defvar mpuz-found-digits (make-bool-vector 10 nil)
5f7e5584
RS
157 "A vector recording which digits have been decrypted.")
158
d93196b3
EZ
159(defvar mpuz-trivial-digits (make-bool-vector 10 nil)
160 "A vector recording which digits have been solved for you.")
161
5f7e5584 162(defmacro mpuz-digit-solved-p (digit)
d93196b3
EZ
163 `(or (aref mpuz-found-digits ,digit)
164 (aref mpuz-trivial-digits ,digit)))
5f7e5584
RS
165
166
167;; A puzzle uses a permutation of [0..9] into itself.
168;; We use both the permutation and its inverse.
169;;---------------------------------------------------
170(defvar mpuz-digit-to-letter (make-vector 10 0)
171 "A permutation from [0..9] to [0..9].")
172
173(defvar mpuz-letter-to-digit (make-vector 10 0)
174 "The inverse of mpuz-digit-to-letter.")
175
176(defmacro mpuz-to-digit (letter)
177 (list 'aref 'mpuz-letter-to-digit letter))
178
179(defmacro mpuz-to-letter (digit)
180 (list 'aref 'mpuz-digit-to-letter digit))
181
182(defun mpuz-build-random-perm ()
183 "Initialize puzzle coding with a random permutation."
184 (let ((letters (list 0 1 2 3 4 5 6 7 8 9)) ; new cons cells, because of delq
185 (index 10)
186 elem)
187 (while letters
c9575b25 188 (setq elem (nth (random index) letters)
5f7e5584
RS
189 letters (delq elem letters)
190 index (1- index))
191 (aset mpuz-digit-to-letter index elem)
192 (aset mpuz-letter-to-digit elem index))))
193
194
eb8c3be9 195;; A puzzle also uses a board displaying a multiplication.
5f7e5584
RS
196;; Every digit appears in the board, crypted or not.
197;;------------------------------------------------------
198(defvar mpuz-board (make-vector 10 nil)
f582564f 199 "The board associates to any digit the list of squares where it appears.")
5f7e5584 200
d93196b3
EZ
201(defun mpuz-put-number-on-board (number row &rest l)
202 "Put (last digit of) NUMBER on ROW and COLUMNS of the puzzle board."
203 (let (digit)
204 (while l
205 (setq digit (% number 10)
206 number (/ number 10))
207 (aset mpuz-board digit `((,row . ,(car l)) ,@(aref mpuz-board digit)))
208 (setq l (cdr l)))))
5f7e5584 209
d93196b3 210(defun mpuz-check-all-solved (&optional row col)
5f7e5584 211 "Check whether all digits have been solved. Return t if yes."
d93196b3
EZ
212 (catch 'solved
213 (let (A B1 B2 C D E squares)
214 (and mpuz-solve-when-trivial
215 (not row)
216 (while
217 (cond ((or (and (setq B1 (or B1 (mpuz-check-all-solved 4 7))
218 B2 (or B2 (mpuz-check-all-solved 4 9))
219 E (or E (mpuz-check-all-solved 10))
220 A (or A (mpuz-check-all-solved 2)))
221 B1 B2)
222 (and E (or A (and B1 B2))))
223 (mpuz-solve)
224 (mpuz-paint-board)
225 (throw 'solved t))
226 ((and (setq D (or D (mpuz-check-all-solved 8))
227 C (or C (mpuz-check-all-solved 6)))
228 D (not E))
229 (mpuz-solve 10))
230 ((and E (not (eq C D)))
231 (mpuz-solve (if D 6 8)))
232 ((and A (not (eq B2 C)))
233 (mpuz-solve (if C 4 6) (if C 9)))
234 ((and A (not (eq B1 D)))
235 (mpuz-solve (if D 4 8) (if D 7)))
236 ((and (not A) (or (and B2 C) (and B1 D)))
237 (mpuz-solve 2)))))
238 (mpuz-paint-board)
239 (mapc (lambda (digit)
240 (and (not (mpuz-digit-solved-p digit)) ; unsolved
241 (setq squares (aref mpuz-board digit))
242 (if row
243 (if col
244 (member (cons row col) squares)
245 (assq row squares))
246 squares) ; and appearing in the puzzle!
247 (throw 'solved nil)))
248 [0 1 2 3 4 5 6 7 8 9]))
5f7e5584
RS
249 t))
250
251
252;; To build a puzzle, we take two random numbers and multiply them.
253;; We also take a random permutation for encryption.
254;; The random numbers are only use to see which digit appears in which square
255;; of the board. Everything is stored in individual squares.
256;;---------------------------------------------------------------------------
257(defun mpuz-random-puzzle ()
258 "Draw random values to be multiplied in a puzzle."
259 (mpuz-build-random-perm)
260 (fillarray mpuz-board nil) ; erase the board
d93196b3
EZ
261 ;; A,B,C,D & E, are the five rows of our multiplication.
262 ;; Choose random values, discarding cases with leading zeros in C or D.
263 (let* ((A (+ 112 (random 888)))
264 (min (1+ (/ 1000 A)))
265 (B1 (+ min (random (- 10 min))))
266 B2 C D E)
267 (while (if (= B1 (setq B2 (+ min (random (- 10 min)))))
268 (not mpuz-allow-double-multiplicator)))
269 (setq C (* A B2)
270 D (* A B1)
271 E (+ C (* D 10)))
a7acbbe4 272 ;; Individual digits are now put on their respective squares.
d93196b3
EZ
273 ;; [NB: A square is a pair (row . column) of the screen.]
274 (mpuz-put-number-on-board A 2 9 7 5)
275 (mpuz-put-number-on-board (+ (* B1 10) B2) 4 9 7)
276 (mpuz-put-number-on-board C 6 9 7 5 3)
277 (mpuz-put-number-on-board D 8 7 5 3 1)
278 (mpuz-put-number-on-board E 10 9 7 5 3 1)))
5f7e5584
RS
279\f
280;; Display
281;;--------
282(defconst mpuz-framework
283 "
284 . . .
d93196b3 285 Number of errors (this game): 0
5f7e5584
RS
286 x . .
287 -------
288 . . . .
d93196b3 289 Number of completed games: 0
5f7e5584 290 . . . .
d93196b3 291 --------- Average number of errors: 0.00
5f7e5584
RS
292 . . . . ."
293 "The general picture of the puzzle screen, as a string.")
294
295(defun mpuz-create-buffer ()
296 "Create (or recreate) the puzzle buffer. Return it."
d93196b3 297 (let ((buf (get-buffer-create "*Mult Puzzle*"))
a0db0bbf 298 (face '(face mpuz-text-face))
d93196b3 299 buffer-read-only)
5f7e5584 300 (save-excursion
d93196b3
EZ
301 (set-buffer buf)
302 (erase-buffer)
303 (insert mpuz-framework)
304 (set-text-properties 13 42 face)
305 (set-text-properties 79 105 face)
306 (set-text-properties 128 153 face)
307 (mpuz-paint-board)
308 (mpuz-paint-errors)
309 (mpuz-paint-statistics))
310 buf))
311
312(defun mpuz-paint-number (n &optional eol words)
313 (end-of-line eol)
314 (let (buffer-read-only)
315 (delete-region (point)
316 (progn (backward-word (or words 1)) (point)))
317 (insert n)))
5f7e5584
RS
318
319(defun mpuz-paint-errors ()
320 "Paint error count on the puzzle screen."
321 (mpuz-switch-to-window)
d93196b3
EZ
322 (goto-line 3)
323 (mpuz-paint-number (prin1-to-string mpuz-nb-errors)))
5f7e5584
RS
324
325(defun mpuz-paint-statistics ()
326 "Paint statistics about previous games on the puzzle screen."
d93196b3
EZ
327 (goto-line 7)
328 (mpuz-paint-number (prin1-to-string mpuz-nb-completed-games))
329 (mpuz-paint-number
330 (format "%.2f"
331 (if (zerop mpuz-nb-completed-games)
332 0
333 (/ (+ 0.0 mpuz-nb-cumulated-errors)
334 mpuz-nb-completed-games)))
335 3 2))
5f7e5584
RS
336
337(defun mpuz-paint-board ()
338 "Paint board situation on the puzzle screen."
339 (mpuz-switch-to-window)
d93196b3 340 (mapc 'mpuz-paint-digit [0 1 2 3 4 5 6 7 8 9])
5f7e5584
RS
341 (goto-char (point-min)))
342
343(defun mpuz-paint-digit (digit)
344 "Paint all occurrences of DIGIT on the puzzle board."
5f7e5584
RS
345 (let ((char (if (mpuz-digit-solved-p digit)
346 (+ digit ?0)
d93196b3
EZ
347 (+ (mpuz-to-letter digit) ?A)))
348 (face `(face
a0db0bbf
EZ
349 ,(cond ((aref mpuz-trivial-digits digit) 'mpuz-trivial-face)
350 ((aref mpuz-found-digits digit) 'mpuz-solved-face)
351 ('mpuz-unsolved-face))))
d93196b3
EZ
352 buffer-read-only)
353 (mapc (lambda (square)
354 (goto-line (car square)) ; line before column!
355 (move-to-column (cdr square))
356 (insert char)
357 (set-text-properties (1- (point)) (point) face)
358 (delete-char 1))
359 (aref mpuz-board digit))))
5f7e5584
RS
360
361(defun mpuz-get-buffer ()
362 "Get the puzzle buffer if it exists."
363 (get-buffer "*Mult Puzzle*"))
364
365(defun mpuz-switch-to-window ()
366 "Find or create the Mult-Puzzle buffer, and display it."
d93196b3
EZ
367 (let ((buf (mpuz-get-buffer)))
368 (or buf (setq buf (mpuz-create-buffer)))
369 (switch-to-buffer buf)
d7f5c8f9 370 (setq buffer-read-only t)
5f7e5584
RS
371 (mpuz-mode)))
372
373\f
374;; Game control
375;;-------------
5f7e5584
RS
376(defun mpuz-start-new-game ()
377 "Start a new puzzle."
378 (message "Here we go...")
379 (setq mpuz-nb-errors 0
380 mpuz-in-progress t)
381 (fillarray mpuz-found-digits nil) ; initialize mpuz-found-digits
d93196b3 382 (fillarray mpuz-trivial-digits nil)
5f7e5584
RS
383 (mpuz-random-puzzle)
384 (mpuz-switch-to-window)
385 (mpuz-paint-board)
386 (mpuz-paint-errors)
387 (mpuz-ask-for-try))
388
58c43274
RS
389;;;###autoload
390(defun mpuz ()
5f7e5584
RS
391 "Multiplication puzzle with GNU Emacs."
392 ;; Main entry point
393 (interactive)
394 (mpuz-switch-to-window)
395 (if mpuz-in-progress
396 (mpuz-offer-abort)
d93196b3 397 (mpuz-start-new-game)))
5f7e5584
RS
398
399(defun mpuz-offer-abort ()
400 "Ask if user wants to abort current puzzle."
401 (interactive)
402 (if (y-or-n-p "Abort game ")
d93196b3
EZ
403 (let ((buf (mpuz-get-buffer)))
404 (message "Mult Puzzle aborted.")
405 (setq mpuz-in-progress nil
406 mpuz-nb-errors 0)
407 (fillarray mpuz-board nil)
408 (if buf (kill-buffer buf)))
409 (mpuz-ask-for-try)))
5f7e5584
RS
410
411(defun mpuz-ask-for-try ()
412 "Ask for user proposal in puzzle."
d93196b3
EZ
413 (message "Your try?"))
414
415(defun mpuz-ding (error)
416 "Dings, unless global variable `mpuz-silent' forbids it."
417 (cond ((eq mpuz-silent t))
418 ((not mpuz-silent) (ding t))
419 (error (ding t))))
5f7e5584
RS
420
421(defun mpuz-try-letter ()
422 "Propose a digit for a letter in puzzle."
423 (interactive)
424 (if mpuz-in-progress
425 (let (letter-char digit digit-char message)
315b2369 426 (setq letter-char (upcase last-command-char)
5f7e5584
RS
427 digit (mpuz-to-digit (- letter-char ?A)))
428 (cond ((mpuz-digit-solved-p digit)
d93196b3
EZ
429 (message "%c already solved." letter-char)
430 (mpuz-ding t))
5f7e5584 431 ((null (aref mpuz-board digit))
d93196b3
EZ
432 (message "%c does not appear." letter-char)
433 (mpuz-ding t))
315b2369 434 ((progn (message "%c = " letter-char)
5f7e5584
RS
435 ;; <char> has been entered.
436 ;; Print "<char> =" and
437 ;; read <num> or = <num>
315b2369
RS
438 (setq digit-char (read-char))
439 (if (eq digit-char ?=)
440 (setq digit-char (read-char)))
5f7e5584 441 (or (> digit-char ?9) (< digit-char ?0))) ; bad input
d93196b3
EZ
442 (message "%c = %c" letter-char digit-char)
443 (mpuz-ding t))
5f7e5584
RS
444 (t
445 (mpuz-try-proposal letter-char digit-char))))
d93196b3
EZ
446 (if (y-or-n-p "Start a new game ")
447 (mpuz-start-new-game)
448 (message "OK. I won't."))))
5f7e5584
RS
449
450(defun mpuz-try-proposal (letter-char digit-char)
451 "Propose LETTER-CHAR as code for DIGIT-CHAR."
452 (let* ((letter (- letter-char ?A))
453 (digit (- digit-char ?0))
d93196b3
EZ
454 (correct-digit (mpuz-to-digit letter))
455 (game mpuz-nb-completed-games))
5f7e5584 456 (cond ((mpuz-digit-solved-p correct-digit)
7b427043
KH
457 (message "%c has already been found." (+ correct-digit ?0)))
458 ((mpuz-digit-solved-p digit)
459 (message "%c has already been placed." digit-char))
5f7e5584 460 ((= digit correct-digit)
d93196b3
EZ
461 (message "%c = %c correct!" letter-char digit-char)
462 (mpuz-ding nil)
463 (aset mpuz-found-digits digit t) ; Mark digit as solved
464 (and (mpuz-check-all-solved)
465 (mpuz-close-game)))
5f7e5584 466 (t ;;; incorrect guess
d93196b3
EZ
467 (message "%c = %c incorrect!" letter-char digit-char)
468 (mpuz-ding t)
5f7e5584
RS
469 (setq mpuz-nb-errors (1+ mpuz-nb-errors))
470 (mpuz-paint-errors)))))
471
5f7e5584
RS
472(defun mpuz-close-game ()
473 "Housecleaning when puzzle has been solved."
474 (setq mpuz-in-progress nil
475 mpuz-nb-cumulated-errors (+ mpuz-nb-cumulated-errors mpuz-nb-errors)
476 mpuz-nb-completed-games (1+ mpuz-nb-completed-games))
477 (mpuz-paint-statistics)
d93196b3
EZ
478 (let ((message (format "Puzzle solved with %d error%s. That's %s"
479 mpuz-nb-errors
480 (if (= mpuz-nb-errors 1) "" "s")
481 (cond ((= mpuz-nb-errors 0) "perfect!")
482 ((= mpuz-nb-errors 1) "very good!")
483 ((= mpuz-nb-errors 2) "good.")
484 ((= mpuz-nb-errors 3) "not bad.")
485 ((= mpuz-nb-errors 4) "not too bad...")
486 ((< mpuz-nb-errors 10) "bad!")
487 ((< mpuz-nb-errors 15) "awful.")
488 (t "not serious.")))))
5f7e5584
RS
489 (message message)
490 (sit-for 4)
491 (if (y-or-n-p (concat message " Start a new game "))
492 (mpuz-start-new-game)
d93196b3
EZ
493 (message "Good Bye!"))))
494
495(defun mpuz-solve (&optional row col)
496 "Find solution for autosolving."
497 (mapc (lambda (digit)
498 (or (mpuz-digit-solved-p digit)
499 (if row
500 (not (if col
501 (member (cons row col) (aref mpuz-board digit))
502 (assq row (aref mpuz-board digit)))))
503 (aset mpuz-trivial-digits digit t)))
504 [0 1 2 3 4 5 6 7 8 9])
505 t)
506
507(defun mpuz-show-solution (row)
5f7e5584 508 "Display solution for debugging purposes."
d93196b3 509 (interactive "P")
5f7e5584 510 (mpuz-switch-to-window)
d93196b3
EZ
511 (mpuz-solve (if row (* 2 (prefix-numeric-value row))))
512 (mpuz-paint-board)
513 (if (mpuz-check-all-solved)
514 (mpuz-close-game)))
5f7e5584 515
896546cd
RS
516(provide 'mpuz)
517
6b61353c 518;;; arch-tag: 2781d6ba-89e7-43b5-85c7-5d3a2e73feb1
6594deb0 519;;; mpuz.el ends here