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