Monday, November 30, 2009

Clojure Snake with Predator

Today I spent the morning trying out the Clojure snake from Stuart Halloway's Programming Clojure. I spent the afternoon making up / doing from memory a predator snake that follows the player's snake around.  Here is the full Code.
;; Setup
;;This is the test bed for me to test new things
(ns ai.snake
  (:import (java.awt Color Dimension)
           (javax.swing JPanel JFrame Timer JOptionPane)
           (java.awt.event ActionListener KeyListener))
  (:use clojure.contrib.import-static
    [clojure.contrib.seq-utils :only (includes?)] clojure.contrib.math))
(import-static java.awt.event.KeyEvent VK_LEFT VK_RIGHT VK_UP VK_DOWN)


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

;; The Functional Part            
(defn add-points [& pts]
  "Adds points together"
  (vec (apply map + pts)))
  
(defn point-to-screen-rect [pt]
  "Converts a point to a rectangle"
  (map #(* point-size %)
    [(pt 0) (pt 1) 1 1]))
    
(defn create-apple []
  {:location [(rand-int width) (rand-int height)]
    :color (Color. 210 50 90)
    :type :apple})

(defn create-snake [colour]
  {:body (list [(rand-int width) (rand-int height)])
    :dir [1 0]
    :type :snake
    :color colour})
    
(defn move [{:keys [body dir] :as snake} & grow]
  (assoc snake :body (cons (add-points (first body) dir)
    (if grow body (butlast body))))) 
    
(defn win? [{body :body}]
  (>= (count body) win-length))
  
(defn head-overlaps-body? [{[head & body] :body}]
  "Pretty neat, does the head an body include the same point?"
  (includes? body head))

(def lose? head-overlaps-body?)

(defn eats? [{[snake-head] :body} {apple :location}]
  "Very neat, All the work is done in the bindings in the function args"
  (= snake-head apple))
  
(defn turn  [snake newdir]
  (assoc snake :dir newdir))
  
;; This is the mutable part  
(defn reset-game [snake ai-snake apple]
  (dosync (ref-set apple (create-apple))
    (ref-set snake (create-snake (Color. 15 160 70))))
    (ref-set ai-snake (create-snake (Color. 15 255 70)))
    nil)
  
(defn update-direction [snake newdir]
  (when newdir (dosync (alter snake turn newdir))))
  
 (defn update-positions [snake ai-snake apple]
  (dosync
    (if (eats? @snake @apple)
      (do (ref-set apple (create-apple))
          (alter snake move :grow))
      (
      (alter snake move)
      (alter ai-snake move))))
     nil)

;;My AI part;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;(defn eats? [{[snake-head] :body} {apple :location}]
;;  "Very neat, All the work is done in the bindings in the function args"
;;  (= snake-head apple))
  
;;The functional part
 (defn point-dif [& pts]
 "The difference between two points"
    (vec (apply map - pts)))

(defn int-to-dir [pt] ;;Exception handling from Programming Clojure P83
    (try
        (/ pt (abs pt))
        (catch ArithmeticException _ 0)))

(defn point-to-dir [& pts]
    (vec (apply map int-to-dir pts)))
    
;;New direction Note that params needs to be derefed using @
(defn intercept-direction [{[target-head] :body} {[ai-head] :body}]
    (point-to-dir (point-dif target-head ai-head)))

;;End of my AI part;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;


 ;; The GUI part
 (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)))
        
;;Multimethod
;;Paint takes 2 args, g is the Java.awt graphics object, object is the object to be painted
(defmulti paint (fn [g object & _] (:type object)))

(defmethod paint :apple [g {:keys [location color]}]
    (fill-point g location color))
    
(defmethod paint :snake [g {:keys [body color]}]
    (doseq [point body]
        (fill-point g point color)))
        
;;The meaty part
(defn game-panel [frame snake apple ai-snake]
    (proxy [JPanel ActionListener KeyListener] []
        (paintComponent [g]
            (proxy-super paintComponent g) ;;Draw the panel
            (paint g @snake)
            (paint g @ai-snake)
            (paint g @apple))
        (actionPerformed [e]
            (update-positions snake ai-snake apple) ;;Update positions
            ;;(update-direction ai-snake (intercept-direction @snake @ai-snake)) ;;Putting it here will make the pred track you all the time
                (when (lose? @snake)
                    (reset-game snake ai-snake apple)
                    (JOptionPane/showMessageDialog frame "You Lose!"))
                (when (win? @snake)
                    (reset-game snake ai-snake apple)
                    (JOptionPane/showMessageDialog frame "You Win!"))
                (.repaint this))
        (keyPressed [e]
            (update-direction snake (dirs (.getKeyCode e)))
            (update-direction ai-snake (intercept-direction @snake @ai-snake))) ;;Putting it here will make the pred track you on change
            ;;Update the direction of the ai-snake
        (getPreferredSize []
            (Dimension. (* (inc width) point-size)
                        (* (inc height) point-size)))
        (keyReleased [e])   ;;Ignore
        (keyTyped [e])))    ;;Ignore

(defn game []
    (let [snake (ref (create-snake (Color. 15 160 70)))
        apple (ref (create-apple))
        ai-snake (ref (create-snake (Color. 15 255 70)))
        frame (JFrame. "Snake")
        panel (game-panel frame snake apple ai-snake)
        timer (Timer. turn-millis panel)]
    (doto panel
        (.setFocusable true)
        (.addKeyListener panel))
    (doto frame
        (.add panel)
        (.pack)
        (.setVisible true))
    (.start timer)
    [snake, apple, timer]))
               
 (def test-snake (ref nil))
 (def test-ai (ref nil))
 (def test-apple (ref nil)) 

No comments:

Post a Comment