patternMinor
A port of the 1971 StarTrek game to Clojure
Viewed 0 times
theclojuregamestartrekport1971
Problem
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.
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
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
Solution
This is a really cool project, and I don't think the non-functional aspects are as problematic as they might seem. One of the things that makes Clojure so flexible is that it lets you be imperative, or even object-oriented, when it makes sense. I think most of the messiness in this code comes from insufficient generalization, rather than being imperative. If you do want to make it more purely functional, James Hague has written some really interesting essays about game programming in functional languages (he uses Erlang, which is even more purely functional than Clojure); the most famous is Purely Functional Retrogames, where he takes apart the process of implementing Pac-Man in Erlang in a purely functional manner, without passing around a "game state" variable. He also argues in Functional Programming Doesn't Work (and what to do about it) that even in a purely functional language, there are certain situations where an imperative "pressure relief valve" is extremely useful and that we shouldn't contort ourselves trying to avoid those. We Clojure users have it good here, because Clojure has high-quality imperative pressure relief valves like the reference types.
I've never written a game in Clojure, but I'll try to offer some suggestions based on general principles and my understanding of Hague's advice, starting at a low level and moving up.
In general, I think you could clean up your code quite a bit if you had more small helper functions. For example, I would put the calculation of
Even if this function is only used once, the code inside is ugly enough that the call site will look nicer without it. The same is true for a lot of the calculations in the main
You asked where macros might have been helpful, and I see one obvious place. You have the code
Then you can call
These changes give you a kind of API to shorten and simplify interaction with your game state.
Here's what those helpers would look like as macros. (Hopefully; I'm far from a macro expert.)
In this case, since we're not doing anything special with the order of evaluation, the macro version of
You could also throw a
Then you could do destructive updates to the game state much more succinctly.
On the other hand, having a giant, monolithic
To get out of doing destructive updates, Hague suggests unpacking only the pieces of the game state which are relevant to a given function, passing them in, and returning a value which represents the effects on the game state to the main loop. The main loop can then update state based on what value it gets back. This would get rid of a lot of the calls to
```
(defn transition-quadrant
[factor coord dir-vec]
(swap! game-state merge (leave-quadrant factor coord dir-vec))
(enter-qu
I've never written a game in Clojure, but I'll try to offer some suggestions based on general principles and my understanding of Hague's advice, starting at a low level and moving up.
In general, I think you could clean up your code quite a bit if you had more small helper functions. For example, I would put the calculation of
h in enterprise-attack into another function:(defn- hit-value [pos1 pos2 power k-count]
(-> power
(/ k-count (u/euclidean-distance pos1 pos2))
(* 2 (r/gen-double)))Even if this function is only used once, the code inside is ugly enough that the call site will look nicer without it. The same is true for a lot of the calculations in the main
let of leave-quadrant.You asked where macros might have been helpful, and I see one obvious place. You have the code
(get-in @game-state [:something :something-else]) and (update-in @game-state [:something :something-else] some-fn args) all over the place. You could write a helper function or macro to shorten that up, maybe something like this (as a function):(defn get-state [& keys]
(get-in @game-state keys))
(defn update-state [keys f & args]
(apply (partial update-in @game-state keys f) args))Then you can call
(get-state :enterprise :energy) instead of (get-in @game-state [:enterprise :energy]) and (reset! game-state (update-state [:enterprise :energy] - @power)) instead of (swap! game-state update-in [:enterprise :energy] - @power) You could also put a swap! inside update-state to make things even shorter. These changes give you a kind of API to shorten and simplify interaction with your game state.
Here's what those helpers would look like as macros. (Hopefully; I'm far from a macro expert.)
In this case, since we're not doing anything special with the order of evaluation, the macro version of
get-state looks exactly the same as the function version. (defmacro get-state [& keys]
(get-in @game-state keys))
(defmacro update-state [keys f & args]
`(update-in @game-state ~keys ~f ~@args))You could also throw a
swap! inside the macro version of update-state, like this:(defmacro update-state [keys f & args]
`(swap! game-state update-in ~keys ~f ~@args))Then you could do destructive updates to the game state much more succinctly.
On the other hand, having a giant, monolithic
game-state variable being passed through every function is sort of not functional to begin with. In the following code, I'm going to treat game-state as a global, because if you have a mutable variable that you always, without fail, pass into every function, wherein you make destructive updates to it, you basically have a global. Let's cut the clutter and just treat it as such. James Hague discusses this issue in Purely Functional Retrogames Part 3 and Part 4, and I'll go over what he says below, but for now, we'll have a global game state, because I do prefer that having a global game state that takes up space in the argument list of every function.To get out of doing destructive updates, Hague suggests unpacking only the pieces of the game state which are relevant to a given function, passing them in, and returning a value which represents the effects on the game state to the main loop. The main loop can then update state based on what value it gets back. This would get rid of a lot of the calls to
swap! in your functions; you could bundle up all the necessary changes inside the functions, return those changes, and then have a single swap! at a higher level that makes those changes. With this kind of system, you might move between quadrants something like this:```
(defn transition-quadrant
[factor coord dir-vec]
(swap! game-state merge (leave-quadrant factor coord dir-vec))
(enter-qu
Code Snippets
(defn- hit-value [pos1 pos2 power k-count]
(-> power
(/ k-count (u/euclidean-distance pos1 pos2))
(* 2 (r/gen-double)))(defn get-state [& keys]
(get-in @game-state keys))
(defn update-state [keys f & args]
(apply (partial update-in @game-state keys f) args))(defmacro get-state [& keys]
(get-in @game-state keys))
(defmacro update-state [keys f & args]
`(update-in @game-state ~keys ~f ~@args))(defmacro update-state [keys f & args]
`(swap! game-state update-in ~keys ~f ~@args))(defn transition-quadrant
[factor coord dir-vec]
(swap! game-state merge (leave-quadrant factor coord dir-vec))
(enter-quadrant))
(defn- leave-quadrant
[factor coord dir-vec]
(let [place (warp-travel-distance (get-state :enterprise)
factor
dir-vec)
energy (- (get-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)))))]
{:enterprise {:sector s,
:quadrant q,
:energy energy}
:stardate (update-in @game-state [:stardate :current]
#(if (> factor 1) (inc %) %))
;; Include whatever changes update-lrs-cell does here
:current-klingons []})Context
StackExchange Code Review Q#82627, answer score: 5
Revisions (0)
No revisions yet.