Source

minesweeper.el / minesweeper.el

Full commit
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
;;; minesweeper.el --- play minesweeper in Emacs

;; Copyright 2010-2012 Zachary Kanfer

;; Author: Zachary Kanfer <zkanfer@gmail.com>
;; Version: 2012.01.29
;; URL: https://bitbucket.org/zck/minesweeper.el

;; This file is not part of GNU Emacs

;; This program is free software: you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.

;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
;; GNU General Public License for more details.

;; You should have received a copy of the GNU General Public License
;; along with this program.  If not, see <http://www.gnu.org/licenses/>.


;; Keywords: game fun minesweeper inane diversion

;;; Code:

(require 'cl)

(defvar minesweeper-mode-map
  (let ((map (make-sparse-keymap)))
    (define-key map (kbd "SPC") 'minesweeper-choose)
    (define-key map (kbd "x") 'minesweeper-choose)
    (define-key map (kbd "RET") 'minesweeper-choose)
    (define-key map [mouse-1] 'minesweeper-choose)
    (define-key map (kbd "m") 'minesweeper-toggle-mark)
    (define-key map [mouse-3] 'minesweeper-toggle-mark-mouse)
    (define-key map (kbd "b") 'backward-char)
    (define-key map (kbd "f") 'forward-char)
    (define-key map (kbd "C-n") 'next-line)
    (define-key map (kbd "n") 'next-line)
    (define-key map (kbd "p") 'previous-line)
    (define-key map (kbd "C-p") 'previous-line)
    (define-key map (kbd "a") 'move-beginning-of-line)
    (define-key map (kbd "e") 'minesweeper-move-end-of-field)
    (define-key map (kbd "c") 'minesweeper-choose-around)
    (define-key map [mouse-2] 'minesweeper-choose-around-mouse)
    (define-key map (kbd "s") 'minesweeper-toggle-show-neighbors)
    map))

(defun minesweeper () "Major mode for playing Minesweeper in Emacs.

There's a field of squares; each square may hold a mine. Your goal is to uncover all the squares that don't have mines. If a revealed square doesn't have a mine, you'll see how many mines are in the eight neighboring squares. You may mark squares, which protects them from accidentally being revealed.

\\{minesweeper-mode-map}"
  (interactive)
  (switch-to-buffer "minesweeper")
  (kill-all-local-variables)
  (use-local-map minesweeper-mode-map)
  (setq major-mode 'minesweeper-mode)
  (setq mode-name "Minesweeper")
  (toggle-read-only t)
  (when *minesweeper-idle-timer*
    (cancel-timer *minesweeper-idle-timer*))
  (setq *minesweeper-idle-timer* (run-with-idle-timer *minesweeper-idle-delay*
						      t
						      'minesweeper-show-neighbors))
  (minesweeper-begin-game))

(defun minesweeper-mode () "Major mode for playing Minesweeper.

To learn how to play minesweeper, see the documentation for 'minesweeper'." nil)

(defface minesweeper-blank
  '((t (:foreground "black"))) "face for blank spaces")

(defface minesweeper-marked
  '((t (:foreground "black"))) "face for marked spaces")

(defface minesweeper-0
  '((t (:foreground "Grey"))) "face for zero spaces")

(defface minesweeper-1
  '((t (:foreground "#2020FF"))) "face for 1 spaces")

(defface minesweeper-2
  '((t (:foreground "#00C000"))) "face for 2 spaces")

(defface minesweeper-3
  '((t (:foreground "#6000A0"))) "face for 3 spaces")

(defface minesweeper-4
  '((t (:foreground "#C00000"))) "face for 4 spaces")

(defface minesweeper-5
  '((t (:foreground "#008080"))) "face for 5 spaces")

(defface minesweeper-6
  '((t (:foreground "#FF8000"))) "face for 6 spaces")

(defface minesweeper-7
  '((t (:foreground "#A06000"))) "face for 7 spaces")

(defface minesweeper-8
  '((t (:foreground "#FF0000"))) "face for 8 spaces")

(defface minesweeper-neighbor
  '((t (:background "#C0FFFF"))) "face for the neighbors of point")

(defface minesweeper-explode
  '((t (:background "#FF0000"))) "face for a clicked-on mine")

(defvar *minesweeper-board-width* nil
  "The number of columns on the Minesweeper field.")

(defvar *minesweeper-default-width* 10
  "The default board width")

(defvar *minesweeper-board-height* nil
  "The number of rows on the Minesweeper field.")

(defvar *minesweeper-default-height* 10
  "The default board height.")

(defvar *minesweeper-mines* nil
  "The number of mines on the Minesweeper field.")

(defvar *minesweeper-default-mines* 10
  "The default number of mines")

(defvar *minesweeper-field* nil
  "The minefield itself. If a mine is in the square, ?X is stored. Otherwise, the number of mines in neighboring squares is stored. This is a hashtable where the key is a list. The first element of the list is the row, and the second is the column.")

(defvar *minesweeper-reveals* nil
  "Holds 't in (row, col) if (row, col) has been revealed")

(defvar *minesweeper-marks* nil
  "Holds 't in (row, col) iff (row, col) has been marked. A marked square cannot be chosen.")

(defvar *minesweeper-blanks-left* 0
  "Holds the number of empty squares left. After 'minesweeper-init has been called, the user will win the game when this becomes zero again.")

(defvar *minesweeper-debug* nil
  "when 't, print debugging information.")

(defvar *minesweeper-first-move* 't
  "If 't, the next move is the first move. So if a mine is selected, move that mine elsewhere")

(defvar *minesweeper-wins* 0
  "The number of times the player has won the game this session")

(defvar *minesweeper-losses* 0
  "The number of times the player has lost the game this session")

(defvar *minesweeper-game-epoch* nil
  "The time the current game started.")

(defvar *minesweeper-min-free-squares* 1
  "The minimum number of squares which must be free.")

(defvar *minesweeper-top-overlay*
  (let ((overlay (make-overlay 0 0)))
    (overlay-put overlay 'face 'minesweeper-neighbor)
    overlay)
  "The overlay to go above point")

(defvar *minesweeper-left-overlay*
  (let ((overlay (make-overlay 0 0)))
    (overlay-put overlay 'face 'minesweeper-neighbor)
    overlay)
  "The overlay to go left of point")

(defvar *minesweeper-right-overlay*
  (let ((overlay (make-overlay 0 0)))
    (overlay-put overlay 'face 'minesweeper-neighbor)
    overlay)
  "The overlay to go right of point")

(defvar *minesweeper-bottom-overlay*
  (let ((overlay (make-overlay 0 0)))
    (overlay-put overlay 'face 'minesweeper-neighbor)
    overlay)
  "The overlay to go below point")

(defvar *minesweeper-explode-overlay*
  (let ((overlay (make-overlay 0 0)))
    (overlay-put overlay 'face 'minesweeper-explode)
    overlay)
  "The overlay that marks the chosen square iff it was a mine.")

(defvar *minesweeper-mark-count*
  0
  "The number of mines the user has marked.")

(defvar *minesweeper-faces*
  (let ((table (make-hash-table :test 'equal)))
    (puthash ?0 'minesweeper-0 table)
    (puthash ?1 'minesweeper-1 table)
    (puthash ?2 'minesweeper-2 table)
    (puthash ?3 'minesweeper-3 table)
    (puthash ?4 'minesweeper-4 table)
    (puthash ?5 'minesweeper-5 table)
    (puthash ?6 'minesweeper-6 table)
    (puthash ?7 'minesweeper-7 table)
    (puthash ?8 'minesweeper-8 table)
    (puthash ?- 'minesweeper-blank table)
    (puthash ?* 'minesweeper-marked table)
    table)
  "The hashtable mapping a character to the face it should have.")

(defvar *minesweeper-idle-timer* nil
  "The timer used to highlight neighbors")

(defvar *minesweeper-idle-delay* 0.0625
  "The time Emacs must be idle before highlighting the neigbors of point.")

(defvar *minesweeper-game-over* nil
  "t if the user has selected a mine or selected all the empty squares, nil otherwise.")

(defun minesweeper-move-end-of-field ()
  "Move to the last cell in this row of the minefield."
  (interactive)
  (move-end-of-line nil)
  (backward-char))

(defun minesweeper-begin-game (&optional width height mines)
  "Prompt the user for the minefield size and number of mines, then initialize the game."
  (minesweeper-debug "beginning the game")
  (if (y-or-n-p (concat (number-to-string (or width *minesweeper-board-width* *minesweeper-default-width*))
			" by "
			(number-to-string (or height *minesweeper-board-height* *minesweeper-default-height*))
			" with "
			(number-to-string (or mines *minesweeper-mines* *minesweeper-default-mines*))
			" mines ok? "))
      (minesweeper-init (or width *minesweeper-board-width* *minesweeper-default-width*)
			(or height *minesweeper-board-height* *minesweeper-default-height*)
			(or mines *minesweeper-mines* *minesweeper-default-mines*))
    (let ((width (minesweeper-get-integer "Minefield width? " (number-to-string (or width *minesweeper-board-width* *minesweeper-default-width*))))
	  (height (minesweeper-get-integer "Minefield height? " (number-to-string (or height *minesweeper-board-height* *minesweeper-default-height*))))
	  (mines (minesweeper-get-integer "Number of mines? " (number-to-string (or mines *minesweeper-mines* *minesweeper-default-mines*)))))
      (minesweeper-init width height mines)))
  (minesweeper-print-field)
  (goto-char (+ (* (truncate (1- *minesweeper-board-height*)
                             2)
		   (1+ *minesweeper-board-width*))
		(ceiling (/ (float *minesweeper-board-width*) 2))))
  (message "Good luck!"))

(defun minesweeper-init (&optional width height mines)
  "Begin a game of Minesweeper with a board that's 'width by 'height size containing 'mines mines."
  (minesweeper-debug "initializing the game")
  (setq *minesweeper-board-width* (or width *minesweeper-default-width*)
	*minesweeper-board-height* (or height *minesweeper-default-height*)
	*minesweeper-mines* (or mines *minesweeper-default-mines*)
	*minesweeper-field* (make-hash-table :test 'equal :size (* *minesweeper-board-width*
								   *minesweeper-board-height*))
	*minesweeper-reveals* (make-hash-table :test 'equal :size (* *minesweeper-board-width*
								     *minesweeper-board-height*))
	*minesweeper-marks* (make-hash-table :test 'equal :size (* *minesweeper-board-width*
								   *minesweeper-board-height*))
	*minesweeper-blanks-left* (- (* *minesweeper-board-width*
					*minesweeper-board-height*)
				     *minesweeper-mines*)
	*minesweeper-first-move* 't
	*minesweeper-game-epoch* nil
	*minesweeper-mark-count* 0
	*minesweeper-game-over* nil)
  (minesweeper-debug "most global vars set -- checking for overpopulation of mines.")
  (while (< *minesweeper-blanks-left* *minesweeper-min-free-squares*)
    (setq *minesweeper-mines* (minesweeper-get-integer (format "Too many mines. You can have at most %d mines. Number of mines?" (- (* *minesweeper-board-width*
																       *minesweeper-board-height*)
																    *minesweeper-min-free-squares*))
						       *minesweeper-default-mines*)
	  *minesweeper-blanks-left* (- (* *minesweeper-board-width*
					  *minesweeper-board-height*)
				       *minesweeper-mines*))))


(defun minesweeper-fill-field (protect-row protect-col)
  "Fills '*minesweeper-field* with '*minesweeper-mines* mines, and builds the neighbor count. It will not place any mines in the square (protect-row, protect-col)."
  (minesweeper-debug "filling the field")
  (minesweeper-for col 0 (1- *minesweeper-board-width*)
		   (minesweeper-debug "inside outer loop -- col is " (number-to-string col))
		   (minesweeper-for row 0 (1- *minesweeper-board-height*)
				    (minesweeper-debug "inside inner loop -- setting up mine " (number-to-string col) " " (number-to-string row))
				    (minesweeper-set-mine row col ?0)
				    (minesweeper-hide row col)
				    (minesweeper-unmark row col)))
  (minesweeper-debug "done setting zeros; now inserting mines")
  (minesweeper-insert-mines *minesweeper-mines* protect-col protect-row))

(defun minesweeper-insert-mines (count protect-col protect-row)
  "insert 'count mines into the minefield, and build up the neighbor count. There can't be a mine at the square (protect-col, protect-row)"
  (minesweeper-debug "inserting " (number-to-string count) " mines")
  (let* ((square-count (1- (* *minesweeper-board-width* *minesweeper-board-height*)))
	 (mines (make-vector square-count (list 0 0)))
	 (pos 0))
    (minesweeper-for col 0 (1- *minesweeper-board-width*)
		     (minesweeper-for row 0 (1- *minesweeper-board-height*)
				      (unless (and (eq row protect-row)
						   (eq col protect-col))
					(minesweeper-debug "setting " (number-to-string col) "\t" (number-to-string row))
					(aset mines pos (list row col))
					(setq pos (1+ pos)))))
    (dotimes (i count)
      (let* ((rand (random (- square-count i)))
	     (ele (aref mines rand)))
	(minesweeper-debug "picked a random mine at position " (number-to-string rand) ". The mine is row:f" (number-to-string (car ele)) "\tcol: " (number-to-string (cadr ele)) ". We've picked " (number-to-string i)" mines so far.")
	(aset mines rand (aref mines (- square-count i 1)))
	(minesweeper-set-mine (car ele) (cadr ele) ?X)
	(minesweeper-inform-around (car ele) (cadr ele))))))

(defun minesweeper-position ()
    "Return the current position of point as a minesweeper position construct. This construct is a list where the first element is the row value, the second is the col value, and the third is whether the position is in bounds."
    (let ((row (1- (line-number-at-pos)))
          (col (current-column)))
      (list row col (minesweeper-in-bounds row col))))

(defun minesweeper-view-mine (row col &optional reveal)
  "If reveal is true, or if the selected mine has been revealed, returns the value at position (col, row). Otherwise, it returns the character * if the square is marked, the character - if it is not."
  (minesweeper-debug "called view-mine " (number-to-string col) " " (number-to-string row) " " (if reveal "reveal!" "hide"))
  (cond ((or reveal
	     (minesweeper-is-revealed row col))
	 (gethash (list row col)
		  *minesweeper-field*))
	((minesweeper-marked row col)
	 ?*)
	('t
	 ?-)))

(defun minesweeper-set-mine (row col val)
  "Inserts val into the mine at (col, row)"
  (puthash (list row col)
	   val
	   *minesweeper-field*))

(defun minesweeper-reveal (row col)
  "Reveals (col, row)."
  (puthash (list row col)
	   't
	   *minesweeper-reveals*))

(defun minesweeper-hide (row col)
  "Hides (col, row)."
  (puthash (list row col)
	   nil
	   *minesweeper-reveals*))

(defun minesweeper-is-revealed (row col)
  "Returns 't if (row, col) is revealed, nil otherwise"
  (gethash (list row col)
	   *minesweeper-reveals*))

(defun minesweeper-mark (row col)
  "Marks the square (row, col) as having a mine. It can't be selected until it is unmarked"
  (minesweeper-debug "marking square " (number-to-string row) "\t" (number-to-string col))
  (unless (minesweeper-marked row col)
    (puthash (list row col)
	     't
	     *minesweeper-marks*)
    (setq *minesweeper-mark-count* (1+ *minesweeper-mark-count*))))

(defun minesweeper-unmark (row col)
  "Removes the mark from (row, col). It can now be selected."
  (when (minesweeper-marked row col)
    (puthash (list row col)
	     nil
	     *minesweeper-marks*)
    (setq *minesweeper-mark-count* (1- *minesweeper-mark-count*))))

(defun minesweeper-invert-mark (row col)
  "If (row, col) is marked, unmark it. Otherwise, mark it."
  (when (and (minesweeper-in-bounds row col)
	     (not (minesweeper-is-revealed row col)))
    (if (minesweeper-marked row col)
	(minesweeper-unmark row col)
      (minesweeper-mark row col))))

(defun minesweeper-marked (row col)
  "Returns 't if (row, col) is marked as having a mine, nil otherwise"
  (gethash (list row col)
	   *minesweeper-marks*))

(defun minesweeper-inform-around (row col &optional amount)
  "takes in a square, and increases the values of all its empty neighbors by 'amount"
  (mapc (lambda (position)
	  (minesweeper-++ (car position) (cdr position) amount))
	(minesweeper-neighbors row col)))

(defun minesweeper-++ (row col &optional amount)
  "Increments the value at square (row, col), unless the square is a bomb"
  (let ((val (minesweeper-view-mine row col 't)))
    (when (and (<= ?0 val)
	       (<= val ?8))
      (minesweeper-set-mine row
			    col
			    (+ val
			       (or amount 1))))))

(defun minesweeper-neighbors (row col)
  "Returns a list of the neighbors of (row, col)."
  (let ((neighbors nil))
    (minesweeper-for newcol
		     (max (1- col) 0)
		     (min (1+ col) (1- *minesweeper-board-width*))
		     (minesweeper-for newrow
				      (max (1- row) 0)
				      (min (1+ row) (1- *minesweeper-board-height*))
				      (when (not (and (eq newcol col)
						      (eq newrow row)))
					(push (cons newrow newcol)
					      neighbors))))
    neighbors))

(defun minesweeper-print-field (&optional reveal)
  "Print out the minefield, then put point back where it was when the function was called. The origin is displayed as the upper left corner."
  (minesweeper-debug "Printing out the field")
  (let ((inhibit-read-only t)
        (pt (point)))
    (erase-buffer)
    (minesweeper-for row 0 (1- *minesweeper-board-height*)
		     (minesweeper-for col 0 (1- *minesweeper-board-width*)
				      (minesweeper-insert-value (minesweeper-view-mine row col reveal)))
		     (newline))
    (unless reveal
      (insert-char ?\s *minesweeper-board-width*) ;;insert a row below the field for choosing neighbors.
      (newline)
      (insert (number-to-string *minesweeper-mark-count*)
	      " of "
	      (number-to-string *minesweeper-mines*)
	      " marked."))
    (minesweeper-debug "Field is printed out")
    (goto-char pt)))

(defun minesweeper-refresh-square (row col)
  "Refreshes the printed value of (row, col)"
  (minesweeper-debug "starting refresh-square. (row, col) is (" (number-to-string row) ",\t" (number-to-string col) ")")
  (when (minesweeper-in-bounds row col)
    (let ((val (minesweeper-view-mine row col)))
      (goto-char (point-min))
      (forward-line row)
      (forward-char col)
      (let ((inhibit-read-only t))
        (delete-char 1)
        (minesweeper-insert-value (minesweeper-view-mine row col)))
      (forward-char -1))))

(defun minesweeper-insert-value (val)
  "Outputs val, properly colored, at point."
  (insert-char val 1)
  (add-text-properties (point)
		       (1- (point))
		       (list 'face
			     (minesweeper-get-face val))))

(defun minesweeper-pick (row col)
  "Reveals the square at position (row, col). If the square is zero,  pick all the neighbors around (col, row)."
  (minesweeper-debug "starting pick with args:" (number-to-string row) " " (number-to-string col))
  (unless (or (not (minesweeper-in-bounds row col))
	      (minesweeper-is-revealed row col)
	      (minesweeper-marked row col))
    (minesweeper-debug "in pick, valid position chosen")
    (when *minesweeper-first-move*
      (minesweeper-debug "in pick, first-move is on. Calling view-mine.")
      (minesweeper-fill-field row col)
      (setq *minesweeper-first-move* nil))
    (minesweeper-debug "in pick, done with first-move check. Getting the value of the square.")
    (let ((val (minesweeper-view-mine row col 't)))
      (minesweeper-debug "view-mine called. The value at " (number-to-string col) ", " (number-to-string row) " is " (make-string 1 val))
      (if (eq val ?X)
	  (progn (minesweeper-lose-game row col)
		 (throw 'game-end nil))
	(let ((to-reveal (list (cons row col))))
	  (minesweeper-debug "The user didn't pick an X")
	  (while to-reveal
	    (let* ((cur (pop to-reveal))
		   (cur-row (car cur))
		   (cur-col (cdr cur)))
	      (minesweeper-debug "View-mine says " (number-to-string cur-col) ", " (number-to-string cur-row) " mine = " (make-string 1 (minesweeper-view-mine cur-row cur-col 't)))
	      (unless (or (minesweeper-is-revealed cur-row cur-col)
			  (minesweeper-marked cur-row cur-col))
		(minesweeper-debug "it's not revealed, so reveal it")
		(minesweeper-reveal cur-row cur-col)
		(if (eq (setq *minesweeper-blanks-left* (1- *minesweeper-blanks-left*))
			0)
		    (progn (minesweeper-win-game)
			   (throw 'game-end nil))
		  (when (eq (minesweeper-view-mine cur-row cur-col 't)
			    ?0)
		    (minesweeper-debug "pushing neighbors onto the stack")
		    (mapc '(lambda (position)
			     (push position
				   to-reveal))
			  (minesweeper-neighbors cur-row cur-col))))))))))))


(defun minesweeper-toggle-mark ()
  "Set the marked status of the current square to the opposite of what it currently is"
  (interactive)
  (unless *minesweeper-game-over*
    (multiple-value-bind (row col in-bounds) (minesweeper-position)
      (when in-bounds
        (minesweeper-invert-mark row col)
        (minesweeper-print-field)))))

(defun minesweeper-toggle-mark-mouse (click)
  "Set the marked status of the clicked-on square to the opposite of what it currently is."
  (interactive "e")
  (unless *minesweeper-game-over*
    (let* ((window (elt (cadr click) 0))
	   (pos (elt (cadr click) 6))
	   (col (car pos))
	   (row (cdr pos)))
      (when (minesweeper-in-bounds row col)
        (minesweeper-invert-mark row col)
        (select-window window)
        (minesweeper-print-field))
      (when (minesweeper-neighbors-bounds row col)
        (goto-char 0)
        (forward-line row)
        (forward-char col)))))


(defun minesweeper-choose ()
  "This is the function called when the user picks a mine."
  (interactive)
  (minesweeper-debug "starting choose")
  (unless *minesweeper-game-epoch*
    (setq *minesweeper-game-epoch* (current-time)))
  (unless *minesweeper-game-over*
    (multiple-value-bind (row col in-bounds) (minesweeper-position)
      (when in-bounds
        (catch 'game-end (minesweeper-pick row col)
               (if (eq (minesweeper-view-mine row col) ?0)
                   (minesweeper-print-field)
                 (minesweeper-refresh-square row col))))
      (minesweeper-debug "finishing choose"))))

(defun minesweeper-choose-around ()
  "Choose all non-marked cells around point. It does not include the cell at point. This is a user-facing function."
  (interactive)
  (minesweeper-debug "starting choose-around")
  (unless *minesweeper-game-over*
    (multiple-value-bind (row col) (minesweeper-position)
      (when (minesweeper-neighbors-bounds row col)
        (catch 'game-end (minesweeper-pick-around row col)
               (minesweeper-print-field)))
      (minesweeper-debug "finishing choose-round"))))

(defun minesweeper-choose-around-mouse (click)
  "Choose all the non-marked cells around the one clicked on, not including the one clicked on."
  (interactive "e")
  (minesweeper-debug "beginning choose-around-mouse")
  (unless *minesweeper-game-over*
    (let* ((window (elt (cadr click) 0))
           (pos (elt (cadr click) 6))
           (row (cdr pos))
           (col (car pos)))
      (select-window window)
      (when (minesweeper-neighbors-bounds row col)
        (goto-char 0)
        (forward-line row)
        (forward-char col))
      (catch 'game-end (minesweeper-pick-around row col)
	     (minesweeper-print-field))))
  (minesweeper-debug "ending choose-around-mouse"))

(defun minesweeper-pick-around (row col)
  "Pick all the squares around (col, row) excluding (col, row). This is an internal function."
  (minesweeper-debug "called pick-around " (number-to-string row) " " (number-to-string col))
  (when (minesweeper-neighbors-bounds row col)
    (mapc '(lambda (position)
	     (minesweeper-debug "called pick-around-helper " (number-to-string col) " " (number-to-string row))
	     (minesweeper-pick (car position) (cdr position)))
	  (minesweeper-neighbors row col))))

(defun minesweeper-lose-game (row col)
  "Print the lose-game message and prompt for a new one."
  (setq *minesweeper-losses* (1+ *minesweeper-losses*))
  (minesweeper-end-game nil))

(defun minesweeper-win-game ()
  "Print the win-game message and prompt for a new one."
  (setq *minesweeper-wins* (1+ *minesweeper-wins*))
  (minesweeper-end-game 't))

(defun minesweeper-end-game (won)
  "End the game: print the revealed minefield, and prompt for a new game. This should be called immediately after selecting the winning or losing square, so point is still on that square."
  (setq *minesweeper-game-over* 't)
  (minesweeper-print-field 't)
  (multiple-value-bind (row col) (minesweeper-position)
      (unless won
        (let ((point (+ (* row
                           (1+ *minesweeper-board-width*)) ;;the new line at the end of the board counts as a character
                        col
                        1)));; (point) is 1-based
          (move-overlay *minesweeper-explode-overlay*
                        point
                        (1+ point)
                        (get-buffer "minesweeper"))))
    (when (y-or-n-p (if won
                        (concat "Congrats! You've won in "
				(minesweeper-game-duration-message)
                                ". "
				(minesweeper-record-message)
				"Another game? ")
                      (concat "Sorry, you lost. You chose a bomb. This game took "
                                (minesweeper-game-duration-message)
                                ". "
                                (minesweeper-record-message)
                                "Another game? ")))
      (minesweeper-begin-game *minesweeper-board-width* *minesweeper-board-height* *minesweeper-mines*))))


(defun minesweeper-game-duration-message ()
  "Returns the duration the current game has taken as a human-readable string."
  (let ((game-duration (time-subtract (current-time) *minesweeper-game-epoch*)))
    (format-seconds "%H, %M, %z%S" (+ (* (car game-duration)
					 (expt 2 16))
				      (cadr game-duration)))))

(defun minesweeper-record-message ()
  "Returns the number of wins and losses formatted as a human-readable string."
  (concat "You've won "
	  (number-to-string *minesweeper-wins*)
	  " game"
          (unless (= *minesweeper-wins*
                     1)
            "s")
          " and lost "
	  (number-to-string *minesweeper-losses*)
	  ". "))

(defmacro minesweeper-for (var init end &rest body)
  "Helper function. executes 'body repeatedly, with 'var assigned values starting at 'init, and ending at 'end, increasing by one each iteration."
  `(let ((,var ,init)
	 (end-val ,end))
     (while (<= ,var end-val)
       ,@body
       (setq ,var (1+ ,var)))))

(defmacro minesweeper-debug (&rest body)
  "If *minesweeper-debug* is 't, log ,@body as a string to the buffer named 'debug'"
  `(when *minesweeper-debug*
     (print (concat ,@body)
	    (get-buffer-create "debug"))))

(defun minesweeper-get-integer (&optional message default)
  "Reads one nonzero integer from the minibuffer."
  (setq default (cond ((not default)
		       "0")
		      ((integerp default)
		       (number-to-string default))
		      ((stringp default)
		       default)
		      (t "0")))
  (let ((val (string-to-number (read-string (concat (or message "Input an integer")
						    " (default "
						    default
						    "):")
					    nil nil default))))
    (while (eq val 0)
      (setq val (string-to-number (read-string (concat (or message "Input an integer")
						       ". Please, a nonzero integer. Try again. (default "
						       default
						       "):")
					       nil nil default))))
    val))

(defun minesweeper-show-neighbors ()
  "If point is within the minefield, highlight as many of the eight squares around point that are in the minefield."
  (minesweeper-reset-neighbor-overlays)
  (when (equal "minesweeper"
	       (buffer-name (current-buffer)))
    (multiple-value-bind (row col) (minesweeper-position)
      (let ((point (point)))
        (when (minesweeper-neighbors-bounds row col)
          (when (> row 0) ;; "top" overlay
            (let ((center (- point *minesweeper-board-width* 1)))
              (move-overlay *minesweeper-top-overlay*
                            (- center (min col 1))
                            (+ center (cond ((< col (1- *minesweeper-board-width*)) 2)
                                            ((= col (1- *minesweeper-board-width*)) 1)
                                            ((> col (1- *minesweeper-board-width*)) 0)))
                            (get-buffer "minesweeper"))))
          (when (> col 0) ;; "left" overlay
            (move-overlay *minesweeper-left-overlay* (1- point) point (get-buffer "minesweeper")))
          (when (< col (1- *minesweeper-board-width*)) ;; "right" overlay
            (move-overlay *minesweeper-right-overlay* (1+ point) (+ point 2) (get-buffer "minesweeper")))
          (when (< row (1- *minesweeper-board-height*)) ;; "bottom" overlay
            (let ((center (+ point *minesweeper-board-width* 1)))
              (move-overlay *minesweeper-bottom-overlay*
                            (- center (if (eq col 0) 0 1))
                            (+ center (cond ((< col (1- *minesweeper-board-width*)) 2)
                                            ((= col (1- *minesweeper-board-width*)) 1)
                                            ((> col (1- *minesweeper-board-width*)) 0)))
                            (get-buffer "minesweeper")))))))))

(defun minesweeper-get-face (val)
  "Gets the face for the character value of val. Proper inputs are ?0 through ?8, ?- and ?*"
  (gethash val *minesweeper-faces*))

(defun minesweeper-toggle-show-neighbors ()
  "Toggles whether neighbors are shown."
  (interactive)
  (if *minesweeper-idle-timer*
      (progn (cancel-timer *minesweeper-idle-timer*)
	     (minesweeper-reset-neighbor-overlays)
	     (setq *minesweeper-idle-timer* nil))
    (setq *minesweeper-idle-timer* (run-with-idle-timer *minesweeper-idle-delay*
							t
							'minesweeper-show-neighbors))))

(defun minesweeper-reset-neighbor-overlays ()
  "Move all the neighbor overlays to the beginning of the buffer. They won't be seen."
  (move-overlay *minesweeper-top-overlay* 0 0 (get-buffer "minesweeper"))
  (move-overlay *minesweeper-left-overlay* 0 0 (get-buffer "minesweeper"))
  (move-overlay *minesweeper-right-overlay* 0 0 (get-buffer "minesweeper"))
  (move-overlay *minesweeper-bottom-overlay* 0 0 (get-buffer "minesweeper")))

(defun minesweeper-in-bounds (row col)
  (minesweeper-debug "Called in-bounds with arguments " (number-to-string col) "\t" (number-to-string row) "\treturning " (if (and (< -1 col)
       (< col *minesweeper-board-width*)
       (< -1 row)
       (< row *minesweeper-board-height*)) "t" "nil"))
  (and (< -1 col)
       (< col *minesweeper-board-width*)
       (< -1 row)
       (< row *minesweeper-board-height*)))

(defun minesweeper-neighbors-bounds (row col)
  "Returns 't iff (row, col) has at least one neighbor in the minefield. I.e., (row, col) is in the minefield, or it neighbors the minefield."
    (and (<= -1 col) ;;Right now, you can't get negative rows or columns. Maybe in the future?
       (<= col *minesweeper-board-width*)
       (<= -1 row)
       (<= row *minesweeper-board-height*)))

(provide 'minesweeper)
;;; minesweeper.el ends here