frpong/src/cljs/frpong/core.cljs

356 lines
15 KiB
Plaintext
Raw Normal View History

2013-09-15 03:19:05 +05:30
(ns frpong.core
(:require [frpong.signals :refer (signal keyboard ticks dom-events mult tap)]
[domina :as dom :refer [log]]
[domina.events :as ev])
2013-10-16 21:48:24 +05:30
(:require-macros [frpong.core :refer (go go-loop rd wt)]))
2013-10-16 21:54:34 +05:30
;; * `signal` creates a new signal
;; * `keyboard`, `ticks` and `dom-events` create signals for keydown events, browser animation ticks and
2013-10-16 21:48:24 +05:30
;; JS DOM events repectively
2013-10-16 21:54:34 +05:30
;; * `mult` creates a mult(iple) of a signal which can then be tapped using `tap` to create a copy of the
2013-10-16 21:48:24 +05:30
;; original signal
2013-10-16 21:54:34 +05:30
;; * `rd` reads the current value of a signal
;; * `wt` sets the current value of a signal to the provided value
;; * `go` and `go-loop` start and run a component
2013-10-16 21:48:24 +05:30
2013-10-16 19:24:43 +05:30
;; Signal Diagram
2013-09-25 16:37:14 +05:30
;;
;; +-d-----------+--------------------------+
;; v | |
;; keyboard +-e-> sampler +---k---> paddle-postnr +-d-+ +-> gravitation +--+ |
;; ^ | p a |
;; t | | | |
;; | | | | |
;; +-------+ +-p----------+-|---+ | d
;; | | +-a-------|-|------+---------------+ |
;; | v v | | | v
;; browser +-b--> ticker +-+--t--> ball-postnr +-p-+-|------|------------p-> renderer
;; ^ | ^ | | | ^ ^
;; Signals | | +-----|---------+ | | | |
;; ------- s | | l +--d--+ | s t
;; b: browser ticks | | | +-+-----|-------+ | | |
;; t: game ticks | | p l +-a-|-------|----+ | |
;; e: keyboard events | | v v v v +-l--+ | |
;; k: keydowns (at rate of t) | +---t-> collision-detr | |
;; p: ball position | | ^ +-s--+-----------------------+ |
;; l: ball velocity | | s | |
;; a: ball acceleration +-----|-------------+-------------+ |
;; d: paddle positions | |
;; s: game state +------------------------------------------------------+
2013-10-16 19:24:43 +05:30
;;
;; All signals except the signal e are at the rate of the signal b. The signal e is at the rate
;; at which the keyboard issues events. The signal b is at the rate at which the browser supplies
2013-10-16 21:48:24 +05:30
;; animation frames.
2013-09-15 03:19:05 +05:30
(defn abs [x] (.abs js/Math x))
2013-10-13 19:34:24 +05:30
(defn sqrt [x] (.sqrt js/Math x))
(defn sq [x] (* x x))
(def PI 3.141592653589793)
(defn deg->rad [deg] (* (/ deg 180) PI))
(defn cos [x] (.cos js/Math x))
(defn sin [x] (.sin js/Math x))
2013-10-15 16:57:09 +05:30
;; Global settings
(def width (- (.-scrollWidth (.-body js/document)) 20))
(def height (- (.-scrollHeight (.-body js/document)) 130))
(def center [(/ width 2 ) (/ height 2)])
(def padding 5)
(def paddle-size 100)
(def ball-radius 8)
(def ball-speed 0.6)
(def init-vel-deg-lim [35 55])
(def perturb-factor 0.05)
(def init-mass-radius 0)
(def paddle-width 10)
(def paddle-step 8)
(def max-paddle-y (- height paddle-size))
(def ef-paddle-width (+ paddle-width padding))
(def init-paddle-pos (/ (- height paddle-size) 2))
(def gravity (atom 0.005))
(def gravity-step 0.005)
(defn mass-radius []
(+ init-mass-radius (* (deref gravity) 1000)))
2013-10-15 19:03:31 +05:30
2013-10-16 01:02:16 +05:30
(defn setup-gravity-controls
"Sets up keyboard controls for changing gravity."
[]
(let [keydowns (first (dom-events :keydown))
actions { 37 #(- % gravity-step) 39 #(+ % gravity-step) }
2013-10-16 01:02:16 +05:30
mass-el (dom/by-id "mass")]
(go-loop
(let [k (:keyCode (rd keydowns))]
(when (contains? actions k)
(do (swap! gravity #(max 0 (min 0.1 ((actions k) %))))
2013-10-16 01:02:16 +05:30
(dom/set-attr! mass-el "r" (mass-radius))))))))
(defn layout-game
"Lays out the game screen."
[]
(doto (dom/by-id "canvas")
(dom/set-style! "width" (str width "px"))
(dom/set-style! "height" (str height "px")))
(doto (dom/by-id "ball")
(dom/set-attr! "r" ball-radius)
(dom/set-attr! "cx" (first center))
(dom/set-attr! "cy" (second center)))
(doto (dom/by-id "mass")
(dom/set-attr! "r" (mass-radius))
(dom/set-attr! "cx" (first center))
(dom/set-attr! "cy" (second center)))
(doto (dom/by-id "score")
(dom/set-attr! "x" (first center))
(dom/set-attr! "y" (- height 50)))
(doseq [id ["lpaddle" "rpaddle"]]
(doto (dom/by-id id)
(dom/set-attr! "width" paddle-width)
(dom/set-attr! "height" paddle-size)
(dom/set-attr! "y" (/ (- height paddle-size) 2))))
(dom/set-attr! (dom/by-id "lpaddle") "x" 0)
(dom/set-attr! (dom/by-id "rpaddle") "x" (- width paddle-width)))
2013-10-13 12:19:55 +05:30
2013-10-15 16:57:09 +05:30
(defn initial-velocity
"Calculates a random initial ball velocity, randomly in any four quadrants, between
the limits of degrees specified by init-vel-deg-lim."
2013-10-15 16:57:09 +05:30
[]
(let [[l h] init-vel-deg-lim
2013-10-13 12:19:55 +05:30
sgn #(if (< % 0.5) -1 1)
deg (+ l (* (- h l) (rand)))
rad (deg->rad deg)]
(map #(* ball-speed %)
2013-10-13 12:19:55 +05:30
[(* (sgn (rand)) (sin rad)) (* (sgn (rand)) (cos rad))])))
(defn start-game
"Sets up the game by creating the signals and setting up the components and starts the game."
[]
(let [br-ticks (ticks) ;; ticks signal from the browser
pos (signal) ;; ball position signal
vel (signal) ;; ball velocity signal
acc (signal) ;; ball acceleration signal
pd-pos (signal) ;; paddles position signal
game-state (signal) ;; game state signal, the state of the game and the current score
2013-10-13 12:19:55 +05:30
init-vel (initial-velocity)]
(setup-components br-ticks game-state pos vel acc pd-pos)
;; start the game by setting the initial values of the signals
(go
(wt pos center)
(wt vel init-vel)
(wt pd-pos [init-paddle-pos init-paddle-pos])
(wt game-state [:moving 0]))))
2013-10-13 19:34:24 +05:30
(defn start-on-space []
(ev/listen-once! :keypress #(if (= (:keyCode %) 32) (start-game) (start-on-space))))
(defn setup-components
2013-10-16 01:02:16 +05:30
"Creates mult(iple)s of the signals and sets up the components by connecting them using
2013-10-13 12:19:55 +05:30
the signals tapped from the mults.
2013-10-15 16:57:09 +05:30
The signals and their stop functions are taken as parameters."
[[br-ticks stop-ticks] game-state pos vel acc pd-pos]
(let [ticks (signal) ;; game ticks signal
ticks-m (mult ticks) ;; mult(iple)s for all signals
pos-m (mult pos)
vel-m (mult vel)
acc-m (mult acc)
pd-pos-m (mult pd-pos)
game-state-m (mult game-state)
;; keyboard signal for w, s, up and down keys
[keyboard stop-keyboard] (keyboard (tap ticks-m) {83 :s 87 :w 38 :up 40 :down})
;; calling this will stop the ticks and the keyboard signals and hence stop the game
stop-game #(do (stop-ticks) (stop-keyboard))]
2013-10-13 12:19:55 +05:30
;; set up the components by tapping into mults
(ticker br-ticks stop-game (tap game-state-m) ticks)
2013-10-13 19:34:24 +05:30
(gravitation (tap pos-m) acc)
(ball-positioner (tap ticks-m) (tap pos-m) (tap vel-m) (tap acc-m) pos)
(paddle-positioner keyboard (tap pd-pos-m) pd-pos)
2013-10-13 19:34:24 +05:30
(collision-detector (tap ticks-m) (tap pos-m) (tap vel-m) (tap acc-m)
(tap pd-pos-m) (tap game-state-m) game-state vel)
(renderer (tap ticks-m) (tap game-state-m) (tap pos-m) (tap pd-pos-m))))
2013-10-16 01:02:16 +05:30
(defn ticker
"Ticker component.
Reads ticks generated by the browser from the `br-ticks` signal and outputs them to the
`game-ticks` signal as long as the `game-state` signal is not :gameover.
Once the `game-state` signal is :gameover, stops the game by calling the `stop-game` function.
Each tick is the number of milliseconds since the last tick was generated."
[br-ticks stop-game game-state game-ticks]
(go (loop []
(let [[state _] (rd game-state)]
(do (wt game-ticks (rd br-ticks))
(if-not (= :gameover state)
(recur)
(stop-game)))))))
2013-10-13 12:19:55 +05:30
2013-10-15 16:57:09 +05:30
(defn gravity-acc
2013-10-16 01:02:16 +05:30
"Calculates acceleration due to gravitation for the ball caused by the mass placed at the
2013-10-15 16:57:09 +05:30
center of the board."
[[x y]]
(let [grav (deref gravity)
[cx cy] center
2013-10-13 19:34:24 +05:30
x-dist (- cx x)
y-dist (- cy y)
distance (sqrt (+ (sq x-dist) (sq y-dist)))
bearing [(/ x-dist distance) (/ y-dist distance)]]
(if-not (= distance 0)
2013-10-15 19:03:31 +05:30
(map #(* grav % (/ 1 distance)) bearing)
2013-10-13 19:34:24 +05:30
[0 0])))
(defn gravitation
"Gravitation component.
Calculates acceleration due to gravitation using the pos from the `pos-in` signal and outputs
it to the `acc` signal."
[pos-in acc]
(go-loop
(wt acc (gravity-acc (rd pos-in)))))
2013-10-13 19:34:24 +05:30
(defn next-pos [[x y] [vel-x vel-y] [acc-x acc-y] tick]
[(+ x (* vel-x tick) (* acc-x (sq tick))) (+ y (* vel-y tick) (* acc-y (sq tick)))])
(defn ball-positioner
"Ball Positioner component.
2013-10-13 19:34:24 +05:30
Calculates the next ball position using the current ball position, velocity and acceleration
(from the `pos-in`, `vel` and `acc` signals respectively) and the current tick (from the
`ticks` signal) and outputs it to the `pos-out` signal."
[ticks pos-in vel acc pos-out]
(go-loop
(let [tick (rd ticks)
pos-next (next-pos (rd pos-in) (rd vel) (rd acc) tick)]
(wt pos-out pos-next))))
(defn paddle-positioner
"Paddle Positioner component.
2013-10-15 16:57:09 +05:30
Captures the keys signal for the provides keycodes and calculates the next paddle
positions using the current paddle positions (from the `pos-in` signal) and keys signal
and outputs it to the `pos-out` signal."
[keys pos-in pos-out]
(go-loop
(let [[lpos rpos] (rd pos-in)
ks (rd keys)
move (fn [pos up down]
(cond
(contains? ks up) (max (- pos paddle-step) 0)
(contains? ks down) (min (+ pos paddle-step) max-paddle-y)
:else pos))]
(wt pos-out [(move lpos :w :s) (move rpos :up :down)]))))
(defn in-y-range? [y paddle-y]
(and (> y (- paddle-y padding)) (< y (+ paddle-y paddle-size padding))))
(defn detect-x-collision [x y lpaddle-y rpaddle-y]
(cond
(< x ef-paddle-width)
(if (in-y-range? y lpaddle-y) :collision-left :gameover)
(> x (- width ef-paddle-width))
(if (in-y-range? y rpaddle-y) :collision-right :gameover)
:else :moving))
(defn detect-y-collision [y]
(cond
(< y padding) :collision-left
(> y (- height padding)) :collision-right
:else :moving))
2013-10-13 12:19:55 +05:30
(defn collision? [state]
(or (= state :collision-left) (= state :collision-right)))
2013-10-13 12:19:55 +05:30
(defn adjust-vel [state vel]
(condp = state
2013-10-13 12:19:55 +05:30
:collision-left (abs vel)
:collision-right (- (abs vel))
:moving vel
:gameover 0))
(defn perturb [v] (* v (+ 1 (* (rand) perturb-factor))))
(defn collision-detector [ticks pos vel-in acc pd-pos game-state-in game-state vel-out]
"Collision Detector component.
Detects the collision of the ball with the walls and the paddles and accordingly calculates
2013-10-16 01:02:16 +05:30
and outputs the next ball velocity and next game state to the `vel-out` and `game-state`
signals respectively.
2013-10-16 01:02:16 +05:30
Reads the current tick, ball position, ball velocity, ball acceleration, left and right paddle
positions and game state from the `ticks`, `pos`, `vel-in`, `acc`, `pd-pos` and `game-state`
2013-10-15 16:57:09 +05:30
signals respectively."
(go-loop
(let [;; read all current values
tick (rd ticks)
[vel-x vel-y] (rd vel-in)
[x y] (rd pos)
[gx gy] (rd acc)
[lpaddle-y rpaddle-y] (rd pd-pos)
[_ score] (rd game-state-in)
2013-10-16 01:02:16 +05:30
;; calculate next position and detect collision
[xn yn] (next-pos [x y] [vel-x vel-y] [gx gy] tick)
x-state (detect-x-collision xn yn lpaddle-y rpaddle-y)
y-state (detect-y-collision yn)
x-collision (collision? x-state)
y-collision (collision? y-state)
2013-10-16 01:02:16 +05:30
2013-10-13 12:19:55 +05:30
;; calculate next velocity and game state
vel-xn (min ball-speed (+ (adjust-vel x-state vel-x) (* gx tick)))
vel-yn (min ball-speed (+ (adjust-vel y-state vel-y) (* gy tick)))
state-n (cond
(= x-state :gameover) :gameover
(or x-collision y-collision) :collision
:else :moving)
score-n (if x-collision (inc score) score)
;; add a small random perturbation to the ball velocity on collision with paddles
[vel-xn vel-yn] (if x-collision
(map perturb [vel-xn vel-yn])
[vel-xn vel-yn])]
(wt vel-out [vel-xn vel-yn])
(wt game-state [state-n score-n]))))
(defn renderer
"Renderer component.
Renders the ball and paddle positions on the browser. Also shows the game state and stats.
Reads the current values from the signals supplied as parameters."
[ticks game-state pos pd-pos]
(let [ball-el (dom/by-id "ball")
score-el (dom/by-id "score")
lpaddle-el (dom/by-id "lpaddle")
rpaddle-el (dom/by-id "rpaddle")
2013-10-15 19:38:47 +05:30
fps-el (dom/by-id "fps")
msg-el (dom/by-id "msg")]
2013-10-15 19:03:31 +05:30
(dom/set-style! ball-el "fill" "orange")
(dom/set-text! msg-el "")
2013-10-15 19:03:31 +05:30
(go (loop [fps-p nil score-p nil]
(let [fps (int (/ 1000 (rd ticks)))
[x y] (rd pos)
[lpaddle-y rpaddle-y] (rd pd-pos)
[state score] (rd game-state)]
(doto ball-el
(dom/set-attr! "cx" x)
(dom/set-attr! "cy" y))
(dom/set-attr! lpaddle-el "y" lpaddle-y)
(dom/set-attr! rpaddle-el "y" rpaddle-y)
(when-not (= fps fps-p)
(dom/set-text! fps-el fps))
(when-not (= score score-p)
(dom/set-text! score-el score))
(when (= state :gameover)
2013-10-15 19:03:31 +05:30
(do (dom/set-style! ball-el "fill" "red")
(dom/set-html! msg-el "<span class='imp'>GAME OVER</span><br>press &lt;space&gt; to restart")
(start-on-space)))
2013-10-15 19:03:31 +05:30
(recur fps score))))))
;; Everything is ready now. Layout the game and start it on pressing <space>!
(defn ^:export frpong []
2013-10-16 01:02:16 +05:30
(setup-gravity-controls)
(layout-game)
2013-10-13 19:34:24 +05:30
(start-on-space))