Dodgy Game
Compile this and run it as root on your computer. What could go wrong?

Table of Contents

Introduction

This is a small game I wrote a while back when I wanted to mess around with clojurescript. I am currently working on rewriting it with clara-rules. You can expect this file to be a WIP. You can play it here.

I think that, when I do refactor it 1 I promise it will happen soon™. I just have a lot of other work and stuff I need to do. I probably should consider also refactoring it to an ECS. So you might have both enemies and point squares be moving entities which applies a certain set of functions both on spawn and so on.

Basic Setup

Project Configuration

(defproject dodgy "0.1.0-SNAPSHOT"
  :description "FIXME: write description"
  :url "http://example.com/FIXME"
  :license {:name "Eclipse Public License"
            :url  "http://www.eclipse.org/legal/epl-v10.html"}
  :dependencies [[org.clojure/clojure "1.10.1"]
                 [org.clojure/clojurescript "1.10.520"]
                 [com.cerner/clara-rules "0.21.1"]
                 [cider/piggieback "0.4.2"]
                 [figwheel-sidecar "0.5.19"]
                 [quil "3.1.0"]
                 [reagent-utils "0.3.4"]]
  :plugins [[lein-cljsbuild "1.1.7"]
            [lein-figwheel "0.5.19"]]
  :hooks [leiningen.cljsbuild]
  :repl-options {:nrepl-middleware [cider.piggieback/wrap-cljs-repl]}
  :clean-targets ^{:protect false} ["resources/public/js"]
  :cljsbuild
  {:builds [; development build with figwheel hot swap
            {:id           "development"
             :source-paths ["src"]
             :figwheel     true
             :compiler
             {:main       "dodgy.core"
              :output-to  "resources/public/js/main.js"
              :output-dir "resources/public/js/development"
              :asset-path "js/development"}}

            {:id           "optimized"
             :source-paths ["src"]
             :compiler
             {:main          "dodgy.core"
              :output-to     "resources/public/js/main.js"
              :output-dir    "resources/public/js/optimized"
              :asset-path    "js/optimized"
              :optimizations :advanced}}]})

Index File

This is the file used as the index of the game.

<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
 "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en">
  <head>
    <meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <meta name="description" content="A bullet hell type game where you attempt to dodge oncoming squares with the speed of oncoming squares being a function of your speed." />
    <title>dodgy</title>
  </head>
  <body style="background-color:#a4a4a4;font-family: 'Jura';">
    <div id="dodgy" style="margin: auto;"></div>
    <script src="js/main.js"></script>
    <script>dodgy.core.run_sketch()</script>
  </body>
</html>

Build/Deploy Script

A small script that builds the game. Deployments copy the code to the public directory using the Emacs project interface.

cd dodgy
lein clean
lein cljsbuild once optimized

Core

The core of the game which sets the initial state.

(ns dodgy.core
  (:require [quil.core :as q :include-macros true]
            [quil.middleware :as m]
            [reagent.cookies :as cookies]
            [dodgy.render :as render]
            [dodgy.stages :as stages]))

(defn setup []
  (q/frame-rate 30)
  {:text
   (str
    "DODGY - a game about dodging squares.\n"
    "Max score: " (cookies/get :max-score 0) "\n"
    "\n"
    "Controls:\n"
    "- The arrow keys control acceleration.\n"
    "\n"
    "Rules:\n"
    "- Hitting orange squares ends the game.\n"
    "- Hitting purple squares increments the score counter.\n"
    "- The longer you play the more frequently orange squares spawn.\n"
    "- The squares move at the output of a function of your speed.\n"
    "\n"
    "Good luck!\n"
    "Press any key to continue...")
   :speed         0
   :distance      0
   :player        {:x -20
                   :y 20}
   :enemies       []
   :point-squares []
   :time          0
   :score         0
   :max-score     0
   :stage         "title"})

(defn ^:export run-sketch []
  (let [width  (- (.-innerWidth js/window) 15)
        height (- (.-innerHeight js/window) 20)]
    (q/defsketch dodgy
      :host "dodgy"
      :size [width height]
      :setup setup
      :update stages/update-stage-state
      :draw render/render-state
      :middleware [m/fun-mode])))

Engine

The engine behind the game. It provides utility functions that are used in the state update function. I will be rewriting these as I refactor the code-base of the game.

(ns dodgy.engine
  (:require [quil.core :as q :include-macros true]))


;; these define the two functions that an enemy may have


(defn entity-bounds
  "Returns the rectangle representing the bounds of a size 20 square entity."
  [{:keys [x y]}]
  [(- x 20) x (- y 20) y])

(defn intersect?
  "Whether the two given rectangles intersect."
  [[axmin axmax aymin aymax] [bxmin bxmax bymin bymax]]
  (and (>= aymax bymin) (<= aymin bymax) (>= axmax bxmin) (<= axmin bxmax)))

(defn player-alive?
  "Returns whether the player has stayed clear of the walls and enemies."
  [player min-x max-x min-y max-y enemies]
  (and (intersect? (entity-bounds player) [(- min-x 20 ) (+ max-x 20) (- min-y 20) max-y])
       (not-any? (fn [enemy] (intersect? (entity-bounds player) (entity-bounds enemy))) enemies)))

(defn gc-entities
  "Filters out out of bounds entities."
  [max-y entities]
  (filter (fn [ent]
            (let [ent-y (:y ent)]
              (<= ent-y (+ max-y 30))))
          entities))

(defn update-point-square-pos
  "Updates the position of the point dodgy."
  [player max-y point-squares speed]
  (->> point-squares
       (filter (fn [point] (not (intersect? (entity-bounds player) (entity-bounds point)))))
       (gc-entities max-y)
       (map (fn [{mov-fn :mov-fn :as point}]
              (apply mov-fn [point speed])))))

(defn update-enemy-pos
  "Updates the positions of each enemy."
  [max-y enemies speed]
  (->> enemies
       (gc-entities max-y)
       (map (fn [{mov-fn :mov-fn :as enemy}]
              (apply mov-fn [enemy speed])))))


(defn gen-mov-fn []
  (partial (rand-nth [(fn [mul {y :y :as e} speed] (merge e {:y (+ (* (Math/pow speed 1.2) mul) y)}))
                      (fn [mul {y :y :as e} speed] (merge e {:y (+ (* (Math/pow speed 1.1) mul) y)}))
                      (fn [mul {y :y :as e} speed] (merge e {:y (+ (* (Math/pow speed 1.3) mul) y)}))
                      (fn [mul {y :y :as e} speed] (merge e {:y (+ (* speed mul) y)}))
                      (fn [mul {y :y :as e} speed] (merge e {:y (+ (* speed y mul 0.5) y)}))])
           (q/random 1.5 0.5)))

(defn gen-enemies
  "Generates enemies and updates their positions."
  [min-x max-x min-y max-y time speed enemies]
  (let [spawn-freq (/ 60 (* (inc (/ (+ 10 time) 500))
                            (/ (q/width) 700)))]
    (if (and (= 0 (mod time (max 1 (int spawn-freq))))
             (< 0.4 (max speed (- speed)))
             (< 100 time))
      (concat (update-enemy-pos max-y enemies speed)
              (map (fn []
                     {:x      (q/random min-x max-x)
                      :y      (- min-y 20)
                      :mov-fn (gen-mov-fn)})
                   (repeat (max 1 (int (/ 1 spawn-freq))) 1)))
      (update-enemy-pos max-y enemies speed))))

(defn update-score
  "Updates the score of the player."
  [player score point-squares]
  (reduce (fn [acc point]
            (if (intersect? (entity-bounds player) (entity-bounds point))
              (inc acc)
              (+ acc 0))) score point-squares))

(defn gen-point-squares
  "This calculates when the enemies should be spawned in the game.
  the spawn frequency calculation computes the frequency with which
  enemies will spawn."
  [player min-x max-x min-y max-y point-squares speed time]
  (if (and (= 0 (mod time (int (/ 128 (/ (q/width) 700)))))
           (< 0.4 (max speed (- speed))))
    (conj (update-point-square-pos player max-y point-squares speed)
          {:x      (q/random min-x max-x)
           :y      (- min-y 20)
           :mov-fn (gen-mov-fn)})
    (update-point-square-pos player max-y point-squares speed)))

(defn update-player
  "This updates the player state."
  [player-speed-x player-speed-y player-x player-y]
  {:x       (+ player-x player-speed-x)
   :y       (+ player-y player-speed-y)
   :speed-x player-speed-x
   :speed-y player-speed-y})

Input

A small collection of input wrappers.

(ns dodgy.input)

(def keystates (atom {}))
(def keys-by-code {37 :left 38 :up 39 :right 40 :down})

(defn update-keystate! [state code]
  (when-let [k (get keys-by-code code)]
    (swap! keystates assoc k state)))

(.addEventListener js/window "keydown" (fn [e] (update-keystate! :pressed (. e -keyCode))))
(.addEventListener js/window "keyup" (fn [e] (update-keystate! nil (. e -keyCode))))

(defn get-x-accel []
  (+ (if (= (get @keystates :left) :pressed) -0.5 0)
     (if (= (get @keystates :right) :pressed) 0.5 0)))

(defn get-y-accel []
  (+ (if (= (get @keystates :up) :pressed) -0.5 0)
     (if (= (get @keystates :down) :pressed) 0.5 0)))

Render

The rendering utilities. It renders the game regardless of the stage.

(ns dodgy.render
  (:require [quil.core :as q  :include-macros true]))


(def enemy-color [0xf5 0x7f 0x17])
(def score-color [0x7c 0x1f 0xa3])
(def player-color [0xe5 0x39 0x35])
(def bg [0xff 0xff 0xff])

(defn render-square [[r g b] thickness size {:keys [x y]}]
  (let [inner (- size (* thickness 2))]
    (q/fill r g b)
    (q/rect x y size size)))


(defn render-entities [{:keys [spawned? player enemies point-squares]}]
  (q/text (str spawned?) 100 100)
  (q/with-translation [(/ (q/width) 2)
                       (/ (q/height) 2)]
    (dorun (map (partial render-square score-color 2.5 20) point-squares))
    (dorun (map (partial render-square enemy-color 2.5 20) enemies))
    (render-square player-color 2.5 20 player)))

(defn render-state [state]
  (q/background 0xff 0xff 0xff)
  (q/stroke 0xff 0xff 0xff)
  (render-entities state)
  (q/fill player-color)
  (q/text (str (:text state)
               (if (= (int (mod (q/seconds) 2)) 0)
                 "█"
                 "")) 10 20))

Stages

A series of different functions that serves to manage the state of the game, splitting it into a number of different stages that are synchronized with the rendering function. Each stage manages the state of the code and changes between the different stages. Future games will not use a stage system and instead endeavor for a more simple mono-stage rule-based system.

(ns dodgy.stages
  (:require [dodgy.input :as io]
            [dodgy.engine :as engine]
            [reagent.cookies :as cookies]
            [quil.core :as q :include-macros true]))

(defn title-stage
  "The initial stage of the game where the title is displayed."
  [state]
  (merge
   state
   (when (q/key-pressed?)
     {:stage "game"})))

(defn game-stage
  "The function that executes during the game stage."
  [{:as            state
    :keys          [enemies time score point-squares distance]
    {:keys [x y speed-x speed-y]
     :as   player} :player}]
  (merge
   state
   (let [new-speed-x (+ speed-x (io/get-x-accel))
         new-speed-y (+ speed-y (io/get-y-accel))
         min-x       (- (/ (q/width) 2))
         max-x       (- (/ (q/width) 2) 20)
         min-y       (- (/ (q/height) 2))
         max-y       (- (/ (q/height) 2) 20)
         speed       (q/sqrt (+ (q/pow new-speed-x 2) (q/pow new-speed-y 2)))]
     (if (engine/player-alive? player min-x max-x min-y max-y enemies)
       {:text          (str "Score: " (:score state) "\n"
                            "Frame: " (:time state) "\n"
                            "Speed: " (.toFixed (:speed state) 1))
        :speed         speed
        :distance      (+ distance speed)
        :player        (engine/update-player new-speed-x new-speed-y x y)
        :enemies       (engine/gen-enemies min-x max-x min-y max-y time speed enemies)
        :point-squares (engine/gen-point-squares player min-x max-x min-y max-y point-squares speed time)
        :time          (inc time)
        :score         (engine/update-score player score point-squares)
        :max-score     (:max-score state)}
       {:ignore-keypress true
        :screen-time     0
        :stage           "score"}))))

(defn score-stage
  [{:keys [ignore-keypress score time distance screen-time]
    :as   state}]
  (merge
   state
   (let [restart       (if-not ignore-keypress
                         (q/key-pressed?)
                         false)
         new-max-score (max (cookies/get :max-score 0) score)]
     (cookies/set! :max-score new-max-score {:max-age (Math/pow 2 31)})
     (if-not restart
       {:text            (str "GAME OVER\n"
                              "Frames: " time "\n"
                              "Distance: " (int distance) "\n"
                              "Max score: " new-max-score "\n"
                              "Score: " score
                              (when (< 60 screen-time)
                                "\n\nPress any key to continue..."))
        :screen-time     (inc screen-time)
        :ignore-keypress (if (or (q/key-pressed?) (> 30 screen-time))
                           ignore-keypress
                           false)
        :speed           0}
       {:player        {:x -20
                        :y 20}
        :enemies       []
        :point-squares []
        :time          0
        :distance      0
        :score         0
        :stage         "game"}))))

(defn update-stage-state [state]
  (case (:stage state)
    "title" (title-stage state)
    "game"  (game-stage state)
    "score" (score-stage state)))

Last Modified: 2022-W11-4 01:36

Generated Using: Emacs 27.2 (Org mode 9.4.6)

Except where otherwise noted content on cons.dev is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License.