autotetris.el 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250
  1. ;;; autotetris-mode.el --- automatically play tetris
  2. ;; This is free and unencumbered software released into the public domain.
  3. ;; Author: Christopher Wellons <wellons@nullprogram.com>
  4. ;; URL: https://github.com/skeeto/autotetris-mode
  5. ;; Package-Requires: ((cl-lib "0.5"))
  6. ;;; Commentary:
  7. ;; This package provides two commands:
  8. ;; * `autotetris' starts `tetris' with autotetris-mode enabled. This
  9. ;; is the command you probably want to run.
  10. ;; * `autotetris-mode' a minor mode for tetris-mode that
  11. ;; automatically plays the game. Can be turned on or off at any
  12. ;; time to allow a human to step in or out of control.
  13. ;; The AI is straightforward. It has a game state evaluator that
  14. ;; computes a single metric for a game state based on the following:
  15. ;; * Number of holes
  16. ;; * Maximum block height
  17. ;; * Mean block height
  18. ;; * Largest block height disparity
  19. ;; * Surface roughness
  20. ;; Lower is better. When a new piece is to be placed it virtually
  21. ;; attempts to place it in every possible position and rotation,
  22. ;; choosing the lowest evaluation score. It's all loosely based on
  23. ;; this algorithm:
  24. ;; http://www.cs.cornell.edu/boom/1999sp/projects/tetris/
  25. ;; Current shortcomings:
  26. ;; The weights could using some tweaking because the priorities are
  27. ;; obviously wrong at times. It does not account for the next piece,
  28. ;; which sometimes has tragic consequences. It does not attempt to
  29. ;; "slide" pieces into place. It does not try to maximize score (the
  30. ;; score is not part of the evaluation algorithm). The evaluation
  31. ;; function is kind of slow, so you should byte-compile this file.
  32. ;;; Code:
  33. (require 'cl-lib)
  34. (require 'tetris)
  35. ;; Set up some hooks:
  36. (defvar autotetris-new-shape-hook ()
  37. "Hooks that run after tetris sets a new shape.")
  38. (defvar autotetris-start-game-hook ()
  39. "Hooks that run immediately after a game starts.")
  40. (defadvice tetris-new-shape (after autotetris-new-shape-hook activate)
  41. (run-hooks 'autotetris-new-shape-hook))
  42. (defadvice tetris-start-game (after autotetris-start-game-hook activate)
  43. (run-hooks 'autotetris-start-game-hook))
  44. ;; Define autotetris minor mode:
  45. (defvar-local autotetris-timer nil
  46. "Stores the local timer value.")
  47. (defvar autotetris-period 0.2
  48. "How often autotetris should make a move.")
  49. (defvar autotetris-mode-map
  50. (let ((keymap (make-sparse-keymap)))
  51. (prog1 keymap
  52. ;; One key for debugging:
  53. (define-key keymap "a" #'autotetris-move)))
  54. "Keymap for autotetris-mode.")
  55. (defun autotetris-kill-timer ()
  56. "Stop running the autotetris timer."
  57. (when autotetris-timer
  58. (cancel-timer autotetris-timer)
  59. (setf autotetris-timer nil)))
  60. ;;;###autoload
  61. (define-minor-mode autotetris-mode
  62. "Automatically play tetris in the current buffer."
  63. :lighter " autotetris"
  64. :keymap autotetris-mode-map
  65. (unless (eq major-mode 'tetris-mode)
  66. (setf autotetris-mode nil)
  67. (error "autotetris-mode can only be used with tetris-mode!"))
  68. (if autotetris-mode
  69. (progn
  70. (add-hook 'kill-buffer-hook #'autotetris-kill-timer nil t)
  71. (unless autotetris-timer
  72. (setf autotetris-timer
  73. (run-at-time t autotetris-period #'autotetris-move))))
  74. (cancel-timer autotetris-timer)))
  75. ;;;###autoload
  76. (defun autotetris ()
  77. "Automatically play a game of tetris."
  78. (interactive)
  79. (tetris)
  80. (unless autotetris-mode
  81. (autotetris-mode)))
  82. ;; The AI:
  83. (defun autotetris-get (x y)
  84. "Get the tetris block at X, Y."
  85. (gamegrid-get-cell (+ tetris-top-left-x x) (+ tetris-top-left-y y)))
  86. (defmacro autotetris-visit (x-y-cell &rest body)
  87. "Visit each cell in the game with BODY, binding X-Y-CELL."
  88. (declare (indent defun))
  89. (cl-destructuring-bind (x y cell) x-y-cell
  90. `(catch 'done
  91. (dotimes (,y tetris-height)
  92. (dotimes (,x tetris-width)
  93. (let ((,cell (autotetris-get x y)))
  94. ,@body))))))
  95. (defun autotetris--holes ()
  96. "Count the number of holes in the gamegrid."
  97. (let ((n 0)
  98. (columns (make-vector tetris-width nil)))
  99. (autotetris-visit (x y cell)
  100. (if (eql cell tetris-blank)
  101. (when (aref columns x) (cl-incf n))
  102. (setf (aref columns x) t)))
  103. n))
  104. (defun autotetris--height (x)
  105. "Return the current block height for column X."
  106. (cl-loop for y below tetris-height
  107. unless (eql tetris-blank (autotetris-get x y))
  108. return (- tetris-height y)
  109. finally (cl-return 0)))
  110. (defun autotetris--min-max-mean-rms ()
  111. "Return the min, max, mean, and rms height."
  112. (cl-flet ((sum (vs) (apply #'+ vs)))
  113. (let* ((heights (cl-loop for x below tetris-width
  114. collect (autotetris--height x)))
  115. (min (apply #'min heights))
  116. (max (apply #'max heights))
  117. (mean (/ (sum heights) 1.0 (length heights)))
  118. (rms (sqrt (sum (mapcar (lambda (v) (expt (- mean v) 2)) heights)))))
  119. (cl-values min max mean rms))))
  120. (defun autotetris-eval ()
  121. "Evaluate the gamegrid in the current buffer; lower is better."
  122. (let ((hole-weight 8.0)
  123. (mean-weight 4.0)
  124. (max-weight 3.0)
  125. (disparity-weight 3.0)
  126. (roughness-weight 2.0))
  127. (cl-multiple-value-bind (min max mean rms) (autotetris--min-max-mean-rms)
  128. (+ (* hole-weight (autotetris--holes))
  129. (* mean-weight mean)
  130. (* max-weight max)
  131. (* disparity-weight (- max min))
  132. (* roughness-weight rms)))))
  133. (defmacro autotetris-save-excursion (&rest body)
  134. "Restore tetris game state after BODY completes."
  135. (declare (indent defun))
  136. `(with-current-buffer tetris-buffer-name
  137. (let ((autotetris-saved (clone-buffer "*Tetris-saved*")))
  138. (unwind-protect
  139. (with-current-buffer autotetris-saved
  140. (kill-local-variable 'kill-buffer-hook)
  141. ,@body)
  142. (kill-buffer autotetris-saved)))))
  143. (defvar autotetris-target nil
  144. "The current block target position and orientation.")
  145. (defun autotetris-game-running-p ()
  146. "Return t if tetris is currently running."
  147. (ignore-errors
  148. (with-current-buffer tetris-buffer-name
  149. (not (eq (current-local-map) tetris-null-map)))))
  150. (defun autotetris--more-middle-p (x1 x2)
  151. "Return t if X1 is closer to the middle than X2."
  152. (cond
  153. ((null x1) nil)
  154. ((null x2) t)
  155. (:else
  156. (let* ((half (/ tetris-width 2.0))
  157. (d1 (abs (- x1 half)))
  158. (d2 (abs (- x2 half))))
  159. (< d1 d2)))))
  160. (defun autotetris-compute-target ()
  161. "Compute the target X position and rotation."
  162. (let ((best-x nil)
  163. (best-r nil)
  164. (best-score 1.0e+INF))
  165. (dotimes (r (tetris-shape-rotations) (list best-x best-r best-score))
  166. (dotimes (xx (+ 2 tetris-width))
  167. (let ((x (1- xx)))
  168. (autotetris-save-excursion
  169. (tetris-erase-shape)
  170. (setf tetris-pos-y 1)
  171. (setf tetris-pos-x x)
  172. (setf tetris-rot r)
  173. (unless (tetris-test-shape)
  174. (tetris-draw-shape)
  175. (tetris-move-bottom)
  176. (tetris-erase-shape)
  177. (let ((score (autotetris-eval)))
  178. (when (or (< score best-score)
  179. (and (= score best-score)
  180. (autotetris--more-middle-p x best-x)))
  181. (setf best-x x
  182. best-r r
  183. best-score score))))))))))
  184. (defun autotetris-clear-target ()
  185. "Clear the current target x-position and rotation."
  186. (setf autotetris-target nil))
  187. (defun autotetris-move ()
  188. "Make exactly one action (move, rotate, drop) in the game."
  189. (interactive)
  190. (when (and autotetris-mode
  191. (not tetris-paused)
  192. (autotetris-game-running-p))
  193. (when (null autotetris-target)
  194. (setf autotetris-target (autotetris-compute-target)))
  195. (cl-destructuring-bind (x r score) autotetris-target
  196. (cond
  197. ((/= tetris-rot r) (tetris-rotate-next))
  198. ((< tetris-pos-x x) (tetris-move-right))
  199. ((> tetris-pos-x x) (tetris-move-left))
  200. (:else (progn
  201. (tetris-move-bottom)
  202. (autotetris-clear-target)))))))
  203. (add-hook 'autotetris-new-shape-hook #'autotetris-clear-target)
  204. (add-hook 'autotetris-start-game-hook #'autotetris-clear-target)
  205. (provide 'autotetris-mode)
  206. ;;; autotetris-mode.el ends here