Switch to recommended form of GPLv3 permissions notice.
[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
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
8101c2f2
JB
42 "*Set this to nil if you want dings on inputs.
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
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
EZ
54(defcustom mpuz-allow-double-multiplicator nil
55 "*Allow 2nd factors like 33 or 77."
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)))
d93196b3 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)))
d93196b3 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)))
d93196b3 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)))
d93196b3 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
RS
89
90(defvar mpuz-mode-map nil
91 "Local keymap to use in Mult Puzzle.")
92
5f7e5584 93(if mpuz-mode-map nil
d93196b3
EZ
94 (setq mpuz-mode-map (make-sparse-keymap))
95 (define-key mpuz-mode-map "a" 'mpuz-try-letter)
96 (define-key mpuz-mode-map "b" 'mpuz-try-letter)
97 (define-key mpuz-mode-map "c" 'mpuz-try-letter)
98 (define-key mpuz-mode-map "d" 'mpuz-try-letter)
99 (define-key mpuz-mode-map "e" 'mpuz-try-letter)
100 (define-key mpuz-mode-map "f" 'mpuz-try-letter)
101 (define-key mpuz-mode-map "g" 'mpuz-try-letter)
102 (define-key mpuz-mode-map "h" 'mpuz-try-letter)
103 (define-key mpuz-mode-map "i" 'mpuz-try-letter)
104 (define-key mpuz-mode-map "j" 'mpuz-try-letter)
105 (define-key mpuz-mode-map "A" 'mpuz-try-letter)
106 (define-key mpuz-mode-map "B" 'mpuz-try-letter)
107 (define-key mpuz-mode-map "C" 'mpuz-try-letter)
108 (define-key mpuz-mode-map "D" 'mpuz-try-letter)
109 (define-key mpuz-mode-map "E" 'mpuz-try-letter)
110 (define-key mpuz-mode-map "F" 'mpuz-try-letter)
111 (define-key mpuz-mode-map "G" 'mpuz-try-letter)
112 (define-key mpuz-mode-map "H" 'mpuz-try-letter)
113 (define-key mpuz-mode-map "I" 'mpuz-try-letter)
114 (define-key mpuz-mode-map "J" 'mpuz-try-letter)
115 (define-key mpuz-mode-map "\C-g" 'mpuz-offer-abort)
116 (define-key mpuz-mode-map "?" 'describe-mode))
5f7e5584 117
5f7e5584 118(defun mpuz-mode ()
58c43274 119 "Multiplication puzzle mode.
5f7e5584 120
a0963566 121You have to guess which letters stand for which digits in the
58c43274 122multiplication displayed inside the `*Mult Puzzle*' buffer.
5f7e5584 123
58c43274 124You may enter a guess for a letter's value by typing first the letter,
d93196b3 125then the digit. Thus, to guess that A=3, type `A 3'.
5f7e5584 126
58c43274
RS
127To leave the game to do other editing work, just switch buffers.
128Then you may resume the game with M-x mpuz.
129You may abort a game by typing \\<mpuz-mode-map>\\[mpuz-offer-abort]."
5f7e5584 130 (interactive)
c83c9654 131 (kill-all-local-variables)
5f7e5584 132 (setq major-mode 'mpuz-mode
d93196b3
EZ
133 mode-name "Mult Puzzle"
134 tab-width 30)
5f7e5584 135 (use-local-map mpuz-mode-map)
c83c9654 136 (run-mode-hooks 'mpuz-mode-hook))
5f7e5584
RS
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.
a87c4c30
DK
263 (let* ((A (if mpuz-allow-double-multiplicator (+ 112 (random 888))
264 (+ 125 (random 875))))
265 (min (1+ (/ 999 A)))
d93196b3
EZ
266 (B1 (+ min (random (- 10 min))))
267 B2 C D E)
268 (while (if (= B1 (setq B2 (+ min (random (- 10 min)))))
269 (not mpuz-allow-double-multiplicator)))
270 (setq C (* A B2)
271 D (* A B1)
272 E (+ C (* D 10)))
a7acbbe4 273 ;; Individual digits are now put on their respective squares.
d93196b3
EZ
274 ;; [NB: A square is a pair (row . column) of the screen.]
275 (mpuz-put-number-on-board A 2 9 7 5)
276 (mpuz-put-number-on-board (+ (* B1 10) B2) 4 9 7)
277 (mpuz-put-number-on-board C 6 9 7 5 3)
278 (mpuz-put-number-on-board D 8 7 5 3 1)
279 (mpuz-put-number-on-board E 10 9 7 5 3 1)))
5f7e5584
RS
280\f
281;; Display
282;;--------
283(defconst mpuz-framework
284 "
285 . . .
d93196b3 286 Number of errors (this game): 0
5f7e5584
RS
287 x . .
288 -------
289 . . . .
d93196b3 290 Number of completed games: 0
5f7e5584 291 . . . .
d93196b3 292 --------- Average number of errors: 0.00
5f7e5584
RS
293 . . . . ."
294 "The general picture of the puzzle screen, as a string.")
295
296(defun mpuz-create-buffer ()
297 "Create (or recreate) the puzzle buffer. Return it."
d93196b3 298 (let ((buf (get-buffer-create "*Mult Puzzle*"))
530b0472 299 (face '(face mpuz-text))
d93196b3 300 buffer-read-only)
5f7e5584 301 (save-excursion
d93196b3
EZ
302 (set-buffer buf)
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)
d93196b3
EZ
323 (goto-line 3)
324 (mpuz-paint-number (prin1-to-string mpuz-nb-errors)))
5f7e5584
RS
325
326(defun mpuz-paint-statistics ()
327 "Paint statistics about previous games on the puzzle screen."
d93196b3
EZ
328 (goto-line 7)
329 (mpuz-paint-number (prin1-to-string mpuz-nb-completed-games))
330 (mpuz-paint-number
331 (format "%.2f"
332 (if (zerop mpuz-nb-completed-games)
333 0
334 (/ (+ 0.0 mpuz-nb-cumulated-errors)
335 mpuz-nb-completed-games)))
336 3 2))
5f7e5584
RS
337
338(defun mpuz-paint-board ()
339 "Paint board situation on the puzzle screen."
340 (mpuz-switch-to-window)
d93196b3 341 (mapc 'mpuz-paint-digit [0 1 2 3 4 5 6 7 8 9])
5f7e5584
RS
342 (goto-char (point-min)))
343
344(defun mpuz-paint-digit (digit)
345 "Paint all occurrences of DIGIT on the puzzle board."
5f7e5584
RS
346 (let ((char (if (mpuz-digit-solved-p digit)
347 (+ digit ?0)
d93196b3
EZ
348 (+ (mpuz-to-letter digit) ?A)))
349 (face `(face
530b0472
MB
350 ,(cond ((aref mpuz-trivial-digits digit) 'mpuz-trivial)
351 ((aref mpuz-found-digits digit) 'mpuz-solved)
352 ('mpuz-unsolved))))
d93196b3
EZ
353 buffer-read-only)
354 (mapc (lambda (square)
355 (goto-line (car square)) ; line before column!
356 (move-to-column (cdr square))
357 (insert char)
358 (set-text-properties (1- (point)) (point) face)
359 (delete-char 1))
360 (aref mpuz-board digit))))
5f7e5584
RS
361
362(defun mpuz-get-buffer ()
363 "Get the puzzle buffer if it exists."
364 (get-buffer "*Mult Puzzle*"))
365
366(defun mpuz-switch-to-window ()
367 "Find or create the Mult-Puzzle buffer, and display it."
d93196b3
EZ
368 (let ((buf (mpuz-get-buffer)))
369 (or buf (setq buf (mpuz-create-buffer)))
370 (switch-to-buffer buf)
d7f5c8f9 371 (setq buffer-read-only t)
5f7e5584
RS
372 (mpuz-mode)))
373
374\f
375;; Game control
376;;-------------
5f7e5584
RS
377(defun mpuz-start-new-game ()
378 "Start a new puzzle."
379 (message "Here we go...")
380 (setq mpuz-nb-errors 0
381 mpuz-in-progress t)
382 (fillarray mpuz-found-digits nil) ; initialize mpuz-found-digits
d93196b3 383 (fillarray mpuz-trivial-digits nil)
5f7e5584
RS
384 (mpuz-random-puzzle)
385 (mpuz-switch-to-window)
386 (mpuz-paint-board)
387 (mpuz-paint-errors)
388 (mpuz-ask-for-try))
389
58c43274
RS
390;;;###autoload
391(defun mpuz ()
5f7e5584
RS
392 "Multiplication puzzle with GNU Emacs."
393 ;; Main entry point
394 (interactive)
395 (mpuz-switch-to-window)
396 (if mpuz-in-progress
397 (mpuz-offer-abort)
d93196b3 398 (mpuz-start-new-game)))
5f7e5584
RS
399
400(defun mpuz-offer-abort ()
401 "Ask if user wants to abort current puzzle."
402 (interactive)
ce5a3ac0 403 (if (y-or-n-p "Abort game? ")
d93196b3
EZ
404 (let ((buf (mpuz-get-buffer)))
405 (message "Mult Puzzle aborted.")
406 (setq mpuz-in-progress nil
407 mpuz-nb-errors 0)
408 (fillarray mpuz-board nil)
409 (if buf (kill-buffer buf)))
410 (mpuz-ask-for-try)))
5f7e5584
RS
411
412(defun mpuz-ask-for-try ()
413 "Ask for user proposal in puzzle."
d93196b3
EZ
414 (message "Your try?"))
415
416(defun mpuz-ding (error)
417 "Dings, unless global variable `mpuz-silent' forbids it."
418 (cond ((eq mpuz-silent t))
419 ((not mpuz-silent) (ding t))
420 (error (ding t))))
5f7e5584
RS
421
422(defun mpuz-try-letter ()
423 "Propose a digit for a letter in puzzle."
424 (interactive)
425 (if mpuz-in-progress
426 (let (letter-char digit digit-char message)
315b2369 427 (setq letter-char (upcase last-command-char)
5f7e5584
RS
428 digit (mpuz-to-digit (- letter-char ?A)))
429 (cond ((mpuz-digit-solved-p digit)
d93196b3
EZ
430 (message "%c already solved." letter-char)
431 (mpuz-ding t))
5f7e5584 432 ((null (aref mpuz-board digit))
d93196b3
EZ
433 (message "%c does not appear." letter-char)
434 (mpuz-ding t))
315b2369 435 ((progn (message "%c = " letter-char)
5f7e5584
RS
436 ;; <char> has been entered.
437 ;; Print "<char> =" and
438 ;; read <num> or = <num>
315b2369
RS
439 (setq digit-char (read-char))
440 (if (eq digit-char ?=)
441 (setq digit-char (read-char)))
5f7e5584 442 (or (> digit-char ?9) (< digit-char ?0))) ; bad input
d93196b3
EZ
443 (message "%c = %c" letter-char digit-char)
444 (mpuz-ding t))
5f7e5584
RS
445 (t
446 (mpuz-try-proposal letter-char digit-char))))
ce5a3ac0 447 (if (y-or-n-p "Start a new game? ")
d93196b3
EZ
448 (mpuz-start-new-game)
449 (message "OK. I won't."))))
5f7e5584
RS
450
451(defun mpuz-try-proposal (letter-char digit-char)
452 "Propose LETTER-CHAR as code for DIGIT-CHAR."
453 (let* ((letter (- letter-char ?A))
454 (digit (- digit-char ?0))
d93196b3
EZ
455 (correct-digit (mpuz-to-digit letter))
456 (game mpuz-nb-completed-games))
5f7e5584 457 (cond ((mpuz-digit-solved-p correct-digit)
7b427043
KH
458 (message "%c has already been found." (+ correct-digit ?0)))
459 ((mpuz-digit-solved-p digit)
460 (message "%c has already been placed." digit-char))
5f7e5584 461 ((= digit correct-digit)
d93196b3
EZ
462 (message "%c = %c correct!" letter-char digit-char)
463 (mpuz-ding nil)
464 (aset mpuz-found-digits digit t) ; Mark digit as solved
465 (and (mpuz-check-all-solved)
466 (mpuz-close-game)))
5f7e5584 467 (t ;;; incorrect guess
d93196b3
EZ
468 (message "%c = %c incorrect!" letter-char digit-char)
469 (mpuz-ding t)
5f7e5584
RS
470 (setq mpuz-nb-errors (1+ mpuz-nb-errors))
471 (mpuz-paint-errors)))))
472
5f7e5584
RS
473(defun mpuz-close-game ()
474 "Housecleaning when puzzle has been solved."
475 (setq mpuz-in-progress nil
476 mpuz-nb-cumulated-errors (+ mpuz-nb-cumulated-errors mpuz-nb-errors)
477 mpuz-nb-completed-games (1+ mpuz-nb-completed-games))
478 (mpuz-paint-statistics)
d93196b3
EZ
479 (let ((message (format "Puzzle solved with %d error%s. That's %s"
480 mpuz-nb-errors
481 (if (= mpuz-nb-errors 1) "" "s")
482 (cond ((= mpuz-nb-errors 0) "perfect!")
483 ((= mpuz-nb-errors 1) "very good!")
484 ((= mpuz-nb-errors 2) "good.")
485 ((= mpuz-nb-errors 3) "not bad.")
486 ((= mpuz-nb-errors 4) "not too bad...")
487 ((< mpuz-nb-errors 10) "bad!")
488 ((< mpuz-nb-errors 15) "awful.")
489 (t "not serious.")))))
274f1353 490 (message "%s" message)
5f7e5584 491 (sit-for 4)
ce5a3ac0 492 (if (y-or-n-p (concat message " Start a new game? "))
5f7e5584 493 (mpuz-start-new-game)
d93196b3
EZ
494 (message "Good Bye!"))))
495
496(defun mpuz-solve (&optional row col)
497 "Find solution for autosolving."
498 (mapc (lambda (digit)
499 (or (mpuz-digit-solved-p digit)
500 (if row
501 (not (if col
502 (member (cons row col) (aref mpuz-board digit))
503 (assq row (aref mpuz-board digit)))))
504 (aset mpuz-trivial-digits digit t)))
505 [0 1 2 3 4 5 6 7 8 9])
506 t)
507
508(defun mpuz-show-solution (row)
5f7e5584 509 "Display solution for debugging purposes."
d93196b3 510 (interactive "P")
5f7e5584 511 (mpuz-switch-to-window)
d93196b3
EZ
512 (mpuz-solve (if row (* 2 (prefix-numeric-value row))))
513 (mpuz-paint-board)
514 (if (mpuz-check-all-solved)
515 (mpuz-close-game)))
5f7e5584 516
896546cd
RS
517(provide 'mpuz)
518
cbee283d 519;; arch-tag: 2781d6ba-89e7-43b5-85c7-5d3a2e73feb1
6594deb0 520;;; mpuz.el ends here