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