A Clojure Snake

The Snake game features a player-controlled snake that moves around a game grid hunting for an apple. When your snake eats an apple, it grows longer by a segment, and a new apple appears. If your snake reaches a certain length, you win. But if your snake crosses over its own body, you lose.

Before you start building your own snake, take a minute to try the completed version. Follow the instructions in the README file at the root of the sample code for the book to start a REPL, then enter the following:

 (require '[examples.snake :refer :all])
 
 (game)

Select the Snake window and use the arrow keys to control your snake.

Our design for the snake takes advantage of Clojure’s functional nature and its support for explicit mutable state by dividing the game into three layers:

  • The functional model will use pure functions to model as much of the game as possible.

  • The mutable model will handle the mutable state of the game. The mutable model will use one or more of the reference models discussed in this chapter. Mutable state is much harder to test, so we’ll keep this part small.

  • The GUI will use Swing to draw the game and to accept input from the user.

These layers will make the Snake easy to build, test, and maintain.

As you work through this example, add your code to the file reader/snake.clj in the sample code. When you open the file, you’ll see that it already imports/uses the Swing classes and Clojure libraries that you’ll need:

 (ns reader.snake
  (:import (java.awt Color Dimension)
  (javax.swing JPanel JFrame Timer JOptionPane)
  (java.awt.event ActionListener KeyListener))
  (:refer examples.import-static :refer :all))
 (import-static java.awt.event.KeyEvent VK_LEFT VK_RIGHT VK_UP VK_DOWN)

Now you’re ready to build the functional model.

The Functional Model

First, create a set of constants to describe time, space, and motion:

 (​def​ width 75)
 (​def​ height 50)
 (​def​ point-size 10)
 (​def​ turn-millis 75)
 (​def​ win-length 5)
 (​def​ dirs { VK_LEFT [-1 0]
  VK_RIGHT [ 1 0]
  VK_UP [ 0 -1]
  VK_DOWN [ 0 1]})

width and height set the size of the game board, and point-size is used to convert a game point into screen pixels. turn-millis is the heartbeat of the game, controlling how many milliseconds pass before each update of the game board. win-length is how many segments your snake needs before you win the game. (Five is a small number suitable for testing.) The dirs maps symbolic constants for the four directions to their vector equivalents. Since Swing already defines the VK_ constants for different directions, we’ll reuse them here rather than define our own.

Next, create some basic math functions for the game:

 (​defn​ add-points [& pts]
  (vec (apply map + pts)))
 
 (​defn​ point-to-screen-rect [pt]
  (map #(* point-size %)
  [(pt 0) (pt 1) 1 1]))

The add-points function adds points together. You can use add-points to calculate the new position of a moving game object. For example, you can move an object at [10, 10] left by one:

 (add-points [10 10] [-1 0])
 -> [9 10]

point-to-screen-rect converts a point in game space to a rectangle on the screen:

 (point-to-screen-rect [5 10])
 -> (50 100 10 10)

Next, let’s write a function to create a new apple:

 (​defn​ create-apple []
  {:location [(rand-int width) (rand-int height)]
  :color (Color. 210 50 90)
  :type :apple})

Apples occupy a single point, the :location, which is guaranteed to be on the game board. Snakes are a bit more complicated:

 (​defn​ create-snake []
  {:body (list [1 1])
  :dir [1 0]
  :type :snake
  :color (Color. 15 160 70)})

Because a snake can occupy multiple points on the board, it has a :body, which is a list of points. Also, snakes are always in motion in some direction expressed by :dir.

Next, create a function to move a snake. This should be a pure function, returning a new snake. Also, it should take a grow option, allowing the snake to grow after eating an apple.

 (​defn​ move [{:keys [body dir] :as snake} & grow]
  (assoc snake :body (cons (add-points (first body) dir)
  (​if​ grow body (butlast body)))))

move uses a fairly complex binding expression. The {:keys [body dir]} part makes the snake’s body and dir available as their own bindings, and the :as snake part binds snake to the entire snake. The function proceeds as follows:

  1. add-points creates a new point, which is the head of the original snake offset by the snake’s direction of motion.

  2. cons adds the new point to the front of the snake. If the snake is growing, the entire original snake is kept. Otherwise, it keeps all the original snake except the last segment (butlast).

  3. assoc returns a new snake, which is a copy of the old snake but with an updated :body.

Test move by moving and growing a snake:

 (move (create-snake))
 -> {:body ([2 1]), ​; etc.
 
 (move (create-snake) :grow)
 -> {:body ([2 1] [1 1]), ​; etc.

Write a win? function to test whether a snake has won the game:

 (​defn​ win? [{body :body}]
  (>= (count body) win-length))

Test win? against different body sizes. Note that win? binds only the :body, so you don’t need a “real” snake, just anything with a body:

 (win? {:body [[1 1]]})
 -> false
 
 (win? {:body [[1 1] [1 2] [1 3] [1 4] [1 5]]})
 -> true

A snake loses if its head ever comes into contact with the rest of its body. Write a head-overlaps-body? function to test for this, and use it to define lose?:

 (​defn​ head-overlaps-body? [{[head & body] :body}]
  (contains? (set body) head))
 
 (​def​ lose? head-overlaps-body?)

Test lose? against overlapping and nonoverlapping snake bodies:

 (lose? {:body [[1 1] [1 2] [1 3]]})
 -> false
 
 (lose? {:body [[1 1] [1 2] [1 1]]})
 -> true

A snake eats an apple if its head occupies the apple’s location. Define an eats? function to test this:

 (​defn​ eats? [{[snake-head] :body} {apple :location}]
  (= snake-head apple))

Notice how clean the body of the eats? function is. All the work is done in the bindings: {[snake-head] :body} binds snake-head to the first element of the snake’s :body, and {apple :location} binds apple to the apple’s :location. Test eats? from the REPL:

 (eats? {:body [[1 1] [1 2]]} {:location [2 2]})
 -> false
 
 (eats? {:body [[2 2] [1 2]]} {:location [2 2]})
 -> true

Finally, you need some way to turn the snake, updating its :dir:

 (​defn​ turn [snake newdir]
  (assoc snake :dir newdir))

turn returns a new snake, with an updated direction:

 (turn (create-snake) [0 -1])
 -> {:body ([1 1]), :dir [0 -1], ​; etc.

All of the code you’ve written so far is part of the functional model of the Snake game. It’s easy to understand in part because it has no local variables and no mutable state. As you’ll see in the next section, the amount of mutable state in the game is quite small. (It’s even possible to implement the Snake with no mutable state, but that’s not the purpose of this demo.)

Building a Mutable Model with STM

The mutable state of the Snake game can change in only three ways:

  • A game can be reset to its initial state.

  • Every turn, the snake updates its position. If it eats an apple, a new apple is placed.

  • A snake can turn.

We’ll implement each of these changes as functions that modify Clojure refs inside a transaction. That way, changes to the position of the snake and the apple will be synchronous and coordinated.

reset-game is trivial:

 (​defn​ reset-game [snake apple]
  (dosync (ref-set apple (create-apple))
  (ref-set snake (create-snake)))
  nil)

You can test reset-game by passing in some refs and then checking that they dereference to a snake and an apple:

 (​def​ test-snake (ref nil))
 (​def​ test-apple (ref nil))
 (reset-game test-snake test-apple)
 -> nil
 
 @test-snake
 -> {:body ([1 1]), :dir [1 0], ​; etc.
 
 @test-apple
 -> {:location [52 8], ​; etc.

update-direction is even more simpler than that; it’s just a trivial wrapper around the functional turn:

 (​defn​ update-direction [snake newdir]
  (when newdir (dosync (alter snake turn newdir))))

Try turning your test-snake to move in the “up” direction:

 (update-direction test-snake [0 -1])
 -> {:body ([1 1]), :dir [0 -1], ​; etc.

The most complicated mutating function is update-positions. If the snake eats the apple, a new apple is created, and the snake grows. Otherwise, the snake simply moves:

 (​defn​ update-positions [snake apple]
  (dosync
  (​if​ (eats? @snake @apple)
  (do (ref-set apple (create-apple))
  (alter snake move :grow))
  (alter snake move)))
  nil)

To test update-positions, reset the game:

 (reset-game test-snake test-apple)
 -> nil

Then, move the apple into harm’s way, under the snake:

 (dosync (alter test-apple assoc :location [1 1]))
 -> {:location [1 1], ​; etc.

Now, after you update-positions, you should have a bigger, two-segment snake:

 (update-positions test-snake test-apple)
 -> nil
 
 (:body @test-snake)
 -> ([2 1] [1 1])

And that is all the mutable state of the Snake world: three functions, about a dozen lines of code.

The Snake GUI

The Snake GUI consists of functions that paint screen objects, respond to user input, and set up the various Swing components. Since snakes and apples are drawn from simple points, the painting functions are simple. The fill-point function fills in a single point:

 (​defn​ fill-point [g pt color]
  (​let​ [[x y width height] (point-to-screen-rect pt)]
  (.setColor g color)
  (.fillRect g x y width height)))

The paint multimethod knows how to paint snakes and apples:

1: (​defmulti​ paint (​fn​ [g object & _] (:type object)))
2: 
3: (​defmethod​ paint :apple [g {:keys [location color]}]
4:  (fill-point g location color))
5: 
6: (​defmethod​ paint :snake [g {:keys [body color]}]
7:  (doseq [point body]
8:  (fill-point g point color)))

paint takes two required arguments: g is a java.awt.Graphics instance, and object is the object to be painted. The defmulti includes an optional rest argument so that future implementations of paint have the option of taking more arguments. (See Defining Multimethods for an in-depth description of defmulti.) On line 3, the :apple method of paint binds the location and color of the apple and uses them to paint a single point on the screen. On line 6, the :snake method binds the snake’s body and color and then uses doseq to paint each point in the body.

The meat of the UI is the game-panel function, which creates a Swing JPanel with handlers for painting the game, updating on each timer tick, and responding to user input:

1: (​defn​ game-panel [frame snake apple]
(proxy [JPanel ActionListener KeyListener] []
(paintComponent [g]
(proxy-super paintComponent g)
5:  (paint g @snake)
(paint g @apple))
(actionPerformed [e]
(update-positions snake apple)
(when (lose? @snake)
10:  (reset-game snake apple)
(JOptionPane/showMessageDialog frame ​"You lose!"​))
(when (win? @snake)
(reset-game snake apple)
(JOptionPane/showMessageDialog frame ​"You win!"​))
15:  (.repaint this))
(keyPressed [e]
(update-direction snake (dirs (.getKeyCode e))))
(getPreferredSize []
(Dimension. (* (inc width) point-size)
20:  (* (inc height) point-size)))
(keyReleased [e])
(keyTyped [e])))

game-panel is long but simple. It uses proxy to create a panel with a set of Swing callback methods.

  • Swing calls paintComponent (line 3) to draw the panel. paintComponent calls proxy-super to invoke the normal JPanel behavior, and then it paints the snake and the apple.

  • Swing will call actionPerformed (line 7) on every timer tick. actionPerformed updates the positions of the snake and the apple. If the game is over, the program displays a dialog and resets the game. Finally, it triggers a repaint with (.repaint this).

  • Swing calls keyPressed (line 16) in response to keyboard input. keyPressed calls update-direction to change the snake’s direction. (If the keyboard input is not an arrow key, the dirs function returns nil and update-direction does nothing.)

  • The game panel ignores keyReleased and keyTyped.

The game function creates a new game:

1: (​defn​ game []
(​let​ [snake (ref (create-snake))
apple (ref (create-apple))
frame (JFrame. ​"Snake"​)
5:  panel (game-panel frame snake apple)
timer (Timer. turn-millis panel)]
(doto panel
(.setFocusable true)
(.addKeyListener panel))
10:  (doto frame
(.add panel)
(.pack)
(.setVisible true))
(.start timer)
15:  [snake, apple, timer]))

On line 2, game creates all the necessary game objects: the mutable model objects snake and apple and the UI components frame, panel, and timer. Lines 7 and 10 perform boilerplate initialization of the panel and frame. Line 14 starts the game by kicking off the timer.

Line 15 returns a vector with the snake, apple, and time. This is for convenience when testing at the REPL; you can use these objects to move the snake and apple or to start and stop the game.

To start the game, use the snake library at the REPL and run game. If you entered the code yourself, you can use the library name you picked (examples.reader in the instructions); otherwise, you can use the completed sample at examples.snake:

 (require '[examples.snake :refer :all])
 (game)

The game window may appear behind your REPL window. If this happens, use your local operating system fu to locate the game window.

There are many possible improvements to the Snake game. If the snake reaches the edge of the screen, perhaps it should turn to avoid disappearing from view. Or maybe you just lose the game. Sorry! Make the Snake game your own by improving it to suit your personal style.

Snakes Without Refs

We chose to implement the Snake game’s mutable model using refs so we could coordinate the updates to the snake and the apple. Other approaches are also valid. For example, you could combine the snake and apple state into a single game object. With only one object, coordination is no longer required, and you can use an atom instead.

The file examples/atom-snake.clj demonstrates this approach. Functions like update-positions become part of the functional model and return a new game object with updated state:

 (​defn​ update-positions [{snake :snake, apple :apple, :as game}]
  (​if​ (eats? snake apple)
  (merge game {:apple (create-apple) :snake (move snake :grow)})
  (merge game {:snake (move snake)})))

Notice how destructuring makes it easy to get at the internals of the game: both snake and apple are bound by the argument list.

The actual mutable updates are now all atom swap!s. We found these to be simple enough to leave them in the UI function game-panel, as this excerpt shows:

 (actionPerformed [e]
  (swap! game update-positions)
  (when (lose? (@game :snake))
  (swap! game reset-game)
  (JOptionPane/showMessageDialog frame ​"You lose!"​))

There are other possibilities as well. Chris Houser’s fork of the book’s sample code[33] demonstrates using an agent that Thread/sleeps instead of a Swing timer, as well as using a new agent per game turn to update the game’s state.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.12.123.189