I ported the Star Trek 1971 game to Clojure to help me learn more about the language and functional programming in general. You can see the entire game on Github. The game plays (you can start it with lein run). Instructions are i game or you can see the first link to get more details on the history or gameplay.
While porting this game, I picked up on some thing very quickly. For example, I am quite pleased with the main game loop in core.clj.
(defn play-game
"The main game loop. When you quit out, the game shuts down. Otherwise a new game starts
immediately after the old one was finished."
[]
(w/new-game-state game-state)
(n/enter-quadrant game-state)
(let [stop-condition (ref false)]
(while (not (deref stop-condition))
; check to see if the Enterprise is destroyed
(if (game-over? game-state)
(do
(w/new-game-state game-state)
(n/enter-quadrant game-state))
(do
(w/short-range-scan-command game-state)
(println "COMMAND")
(let [choice (read-line)]
(condp = choice
"0" (n/set-course-command game-state)
"1" (w/short-range-scan-command game-state)
"2" (w/long-range-scan-command game-state)
"3" (e/fire-phasers-command game-state)
"4" (e/fire-torpedoes-command game-state)
"5" (e/shield-control-command game-state)
"6" (e/damage-control-report-command game-state)
"7" (c/computer-command game-state)
"q" (dosync (alter stop-condition (fn [_] true)))
(command-help)
Simple functional expressions to control where user input goes for each option. I like this, very simple, clear and easy to read.
Where I ran into issues, is that I could not achieve the same level of clarity everywhere.
In the nav.clj file, I had many issues attempting to simplify the movement commands. The function below is invoked every time the player enters a new quadrant.
(defn- leave-quadrant
[game-state factor coord dir-vec]
(let [place (warp-travel-distance (get-in @game-state [:enterprise])
factor
dir-vec)
energy (- (get-in @game-state [:enterprise :energy])
(+ -5 (* 8 (int factor))))
q (vec (->> (map #(int (/ % 8)) place)
(map #(max % 1))
(map #(min % 8))))
s (vec (map #(math/round %) (map - place (vec (map #(* 8 %) q)))))]
(swap! game-state update-in [:enterprise] merge {:sector s :quadrant q :energy energy})
(when (> factor 1)
(swap! game-state update-in [:stardate :current] inc))
(w/update-lrs-cell game-state (get-in @game-state [:quads (u/coord-to-index q)])))
(swap! game-state assoc-in [:current-klingons] [])
(enter-quadrant game-state))
In this method I do several things that seem wrong. I use a chain of lets to create local variables, I update the global state (the game-state atom) several times. This functions reads as a very imperative style and I would like guidance on how to clean this method up.
I also did not find any opportunities to use macros when writing this game. Suggestions for good uses of macros are very welcome.
These two functions in enterprise.clj drive me crazy. Finally understanding the thrush operator helped immensely, but I find these methods as needlessly complex. I think there must be an elegant way to clean this up, but I simply do not see how.
(defn- enterprise-attack [game-state power k-count klingon]
(let [p [(:x klingon) (:y klingon)]
h (-> @power
(/ k-count
(u/euclidean-distance
(get-in @game-state [:enterprise :sector])
p))
(* 2 (r/gen-double)))
z (max 0.0 (- (:energy klingon) h))]
(u/message (format "%f UNIT HIT ON KLINGON AT SECTOR %s\n (%f LEFT)"
h
(u/point-2-str p)
z))
(assoc klingon :energy z)))
(defn- fire-phasers [game-state]
(when (pos? (get-in @game-state [:enterprise :damage :computer_display]))
(u/message " COMPUTER FAILURE HAMPERS ACCURACY"))
(let [power (atom (select-phaser-power (get-in @game-state [:enterprise :energy])))
k-count (count (get-in @game-state [:current-klingons]))]
(swap! game-state update-in [:enterprise :energy] - @power)
(swap! game-state assoc-in [:enterprise] (k/klingon-turn
(get-in @game-state [:enterprise])
(get-in @game-state [:current-klingons])))
(when-not (neg? (get-in @game-state [:enterprise :shields]))
(when (neg? (get-in @game-state [:enterprise :damage :computer_display]))
(swap! power #(* % (r/gen-double))))
(swap! game-state assoc-in [:current-klingons]
(->> (get-in @game-state [:current-klingons])
(map #(enterprise-attack game-state power k-count %))
(map #(if (pos? (:energy %)) % (phasers-hit-klingon game-state %)))
(remove nil?)
(vec))))))
When reading these two methods I feel overwhelmed by the large number of swaps. I also hate that I needed to make power an atom so I could change the efficiency of the phasers (by reducing power) when certain systems are damaged.
Thanks for the time looking at my first real stab at writing something non-trivial in Clojure. Any feedback welcome. I would really love for anyone to checkout the overall architecture and flow of the game and offer suggestions for improvement.