Dan Torop

Part 7: motion, drunkard's walk, keyboard control

Continued from Emacs Lisp programming pt. 6.

File handling

To start working on code, create an emacs buffer which edits a file. For example, type c-x c-f \~/Desktop/motion.el. The ~/Desktop/ navigates to your Desktop (on OS X), then motion.el is the filename. You can choose any name, but have it end in .el to let Emacs know it is an Emacs Lisp program.

The first time you navigate to the file with c-x c-f Emacs will create the file. You then save you work via c-x c-s. The next time you start Emacs, c-x c-f ~/Desktop/motion.el (or whatever its name was) will load the file.

At any point when specifying a filename or directory, type TAB and Emacs will complete the directory/filename. If it doesn’t find a completion, typing TAB twice will show all possible completions. To edit a file on a flash drive, use a file pathname like /Volumes/drivename/filename (on OS X).

key command meaning
c-x c-f find file
c-x c-s save file
TAB try to complete directory/file name

Test buffers

To show a test buffer in the same Emacs window as your code, type c-x 3 to split the Emacs window. You can now flip the cursor between the left and right panes via c-x o. In the right pane, type c-x b test <return> to create a test buffer. Then c-x o to go back to the left pane and edit code. Widen the window so there’s enough space to see code on the left and the test buffer on the right.

Emacs two pane

Now in the left pane, you can type m-x eval-buffer <return> to load your Lisp code into Emacs. Then type c-x o to move to the right pane (test buffer), and type m-: (defunname) to run a defun you've created. If you need to quit your defun (because it is looping forever), type c-g. Then c-x o will bring you back to you Emacs code in the left pane.

If you accidentally run your Lisp program in the same buffer as you Lisp code, it’ll probably overwrite your code. To get your program back, type c-/ which performs an undo. If the Lisp code has hidden the cursor, typing m-: (setq cursor-type t) <return> will bring back the cursor.

key command meaning
c-x 3 split window horizontally
c-x o move cursor to other pane
c-x b switch to buffer
m-x eval-buffer load the Lisp code in the current buffer
m-: prompt for and evaluate a Lisp expression
c-g abort running Lisp program (or pretty much anything else)
c-/ undo
m-: (setq cursor-type t) bring back cursor

Comments

Lisp ignores any text on a line after a semicolon (;). This lets you comment on your code in non-Lisp-language. It’s customary for full-line comments to start with two semicolons (;;):

;; set some variables
(setq i 23)
(setq j "hello")

Helper routines and global variables

For the code in this page, we’ll set up global variables (setq’d outside of a defun, so all defuns can see them) for drawing grid size and background. We’ll then set up functions to draw a grid, draw a character, and clear a character. This code will need to be eval’d for the rest of the code to work.

;; global variables

(setq width 50)
(setq height 35)
(setq background-char ?\.)

;; helper routines (little elves)

(defun make-grid ()
  (erase-buffer)
  (dotimes (i height)
    (insert-char background-char width)
    (newline)))

(defun draw-char (x y char)
  (goto-char (+ x (* (1- y) (1+ width))))
  (delete-char 1)
  (insert-char char 1))

(defun clear-char (x y)
  (draw-char x y background-char))

1+ returns 1 more than its argument, 1- returns one less. So (1+ width) is short for (+ 1 width) and (1- y) is short for (- y 1).

Note that clear-char is a defun which uses another defun draw-char to do its work, along with a reference to the global variable background-char. (Just as draw-char uses built-in Emacs defuns goto-char, delete-char, and insert-char to do its work.)

Animate a character down and to right

(defun move-char ()
  (make-grid)
  (let ((x 1) (y 1))
    (dotimes (i 30)
      (draw-char x y ?\*)
      (sit-for 0.2)
      (clear-char x y)
      (setq x (+ x (random 3))
            y (+ y (random 3))))))

The let sets up x and y to keep track of the character position. (random 3) returns 0, 1, or 2. Thus, the setq sets x and y each to be randomly either the same or a bit more. Note that setq can set more than one variable.

Animate two characters

(defun move-two-chars ()
  (make-grid)
  (let ((x1 1) (y1 1)
        (x2 1) (y2 1))
    (dotimes (i 30)
      (draw-char x1 y1 ?\*)
      (draw-char x2 y2 ?\o)
      (sit-for 0.2)
      (clear-char x1 y1)
      (clear-char x2 y2)
      (setq x1 (+ x1 (random 3))
            y1 (+ y1 (random 3))
            x2 (+ x2 (random 4))
            y2 (+ y2 (random 2))))))

This is a pretty characteristic animation code. The let sets up the two characters. For each time through the loop, first the code draws the characters, then pauses so the viewer can see them, then clears the characters, then updates their position, then repeats. (In slicker code, the computer might update the character position data during the pause, to save time and eliminate flicker.)

Drunkard’s walk

(defun drunkard ()
  (setq cursor-type nil)
  (make-grid)
  (let ((x (/ width 2))
        (y (/ height 2)))
    (while t
      (draw-char x y ?\*)
      (sit-for 0.2)
      (clear-char x y)
      (setq x (+ x (1- (random 3)))
            y (+ y (1- (random 3)))))))

The character staggers around the screen. The (setq cursor-type nil) hides the cursor. (1- (random 3)) returns -1, 0, or 1. The while t means that the drunkard will wander forever (well, actually not forever, this code will break when the character wanders off the grid).

Two drunkards

(defun two-drunks ()
  (setq cursor-type nil)
  (make-grid)
  (let ((x1 (/ width 2)) (y1 (/ height 2))
        (x2 (/ width 2)) (y2 (/ height 2)))
    (while t
      (draw-char x1 y1 ?\*)
      (draw-char x2 y2 ?\#)
      (sit-for 0.2)
      (make-grid)
      (setq x1 (+ x1 (1- (random 3)))
            y1 (+ y1 (1- (random 3)))
            x2 (+ x2 (1- (random 3)))
            y2 (+ y2 (1- (random 3)))))))

Similar two move-two-chars.

A version with a helper function to determine stagger direction:

(defun delta ()
  (1- (random 3)))

(defun two-drunks2 ()
  (setq cursor-type nil)
  (make-grid)
  (let ((x1 (/ width 2)) (y1 (/ height 2))
        (x2 (/ width 2)) (y2 (/ height 2)))
    (while t
      (draw-char x1 y1 ?\*)
      (draw-char x2 y2 ?\#)
      (sit-for 0.2)
      (make-grid)
      (setq x1 (+ x1 (delta))
            y1 (+ y1 (delta))
            x2 (+ x2 (delta))
            y2 (+ y2 (delta))))))

Note that delta is a defun which returns a value, the result of the last (and only, in this case) expression in the defun. This value will be either -1, 0, or 1, and will vary each time it is called, due to its calling random.

A drunkard who can wander forever

(defun circular-drunkard ()
  (setq cursor-type nil)
  (make-grid)
  (let ((x (/ width 2))
        (y (/ height 2)))
    (while t
      (draw-char x y ?\*)
      (sit-for 0.05)
      (clear-char x y)
      (setq x (+ x (1- (random 3)))
            y (+ y (1- (random 3))))
      ;; like pac-man, edges of screen connect
      (if (< x 1) (setq x width))
      (if (> x width) (setq x 1))
      (if (< y 1) (setq y height))
      (if (> y height) (setq y 1)))))

The if clauses at the end do something about the character walking off the grid.

Keyboard controlled walker

(defun walker ()
  (setq cursor-type nil)
  (make-grid)
  (let ((x 1) (y 1))
    (while t
      (draw-char x y ?\*)
      (let ((key (read-event)))
        (clear-char x y)
        (cond
         ((eq key 'left)
          (setq x (1- x)))
         ((eq key 'right)
          (setq x (1+ x)))
         ((eq key 'up)
          (setq y (1- y)))
         ((eq key 'down)
          (setq y (1+ y)))))
      ;; stop at edges of screen
      (setq x (min x width)
            y (min y height)
            x (max x 1)
            y (max y 1)))))

Like the single-drunkard walk, except that now the keyboard controls where the character walks. (read-event) waits for a keypress, then returns it. The cond (short for conditional) function is a way to do a series of if’s without writing if each time. Storing the result of (read-event) temporarily in a variable key via the let command lets the cond compare the same keypress against all four arrow keys. The apostrophe (') before left, right, up, and down is necessary to “quote” these, otherwise Lisp would think we meant a variable named left, etc.

The min and max function respectively return the smallest and largest of their arguments. This is a succinct way to make sure that x is always less than or equal to width, y is always less than or equal to height, and both x and y are greater than or equal to 1.

The character keeps moving once directed by keypress

(defun runner ()
  (setq cursor-type nil)
  (make-grid)
  (let ((x 1) (y 1)
        (dx 0) (dy 0))
    (while (and (>= x 1) (<= x width))
      (draw-char x y ?\*)
      (let ((key (read-event nil nil 0.1)))
        (cond
         ((eq key 'left)
          (setq dx -1))
         ((eq key 'right)
          (setq dx 1))
         ((eq key 'up)
          (setq dy -1))
         ((eq key 'down)
          (setq dy 1))))
      (clear-char x y)
      (setq x (+ x dx)
            y (+ y dy))
      ;; bounce off edges
      (if (or (< x 1) (> x width))
          (setq dx (- dx)))
      (if (or (< y 1) (> y height))
          (setq dy (- dy)))
      ;; stop at edges of screen
      (setq x (min x width)
            y (min y height)
            x (max x 1)
            y (max y 1)))))

The variables dx and dy stand for delta-x and delta-y, and describe the rate of change of x and y.

(read-event nil nil 0.1) waits a tenth of a second for a keypress, and returns either the keypress (if there was one), or nil if there was no keypress in that time. It’s a good way to check for input, but keep the action going if there is no input. Because it waits for a bit, we no longer need a (sit-for 0.1), the wait in read-event does the same thing.

The (setq dx (- dx)) sets dx to negative dx, reversing the side-to-side motion of the character. Similarly (setq dy (- dy)) reverses the up/down motion of the character. This (within the if tests) makes the character bounce of screen edges if it has gone too far. It’s a bit much to code in the character bouncing off the screen edges and also constrained by them…

The character bounces off of top/bottom of grid, but falls off the sides

(defun runner-with-boundaries ()
  (setq cursor-type nil)
  (make-grid)
  (let ((x 1) (y 1)
        (dx 0) (dy 0))
    (while (and (>= x 1) (<= x width))
      (draw-char x y ?\*)
      (let ((key (read-event nil nil 0.1)))
        (cond
         ((eq key 'left)
          (setq dx -1))
         ((eq key 'right)
          (setq dx 1))
         ((eq key 'up)
          (setq dy -1))
         ((eq key 'down)
          (setq dy 1))))
      (clear-char x y)
      (setq x (+ x dx)
            y (+ y dy))
      ;; bounce off top/bottom edges
      (if (or (< y 1) (> y height))
          (setq dy (- dy)))
      ;; stop at edges of screen
      (setq y (min y height)
            y (max y 1)))
    (insert "Game over")))

The while loop now doesn’t go forever, but only so long as the character stays in the left/right grid bounds. The final “Game over” is a bit crude, but this starts getting us closer to Pong!

Continued in Emacs Lisp programming pt. 8.