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