My First FP attempt, version 1
[Note: the function overview of the code (function signatures and comments only) is at the bottom of this post. The full code .clj is here.]
It's a little FP-ish in this first version. It is also quite likely the worst Clojure and FP code ever written in the history of computing. I have not yet tried to clean it up, use the Clojure syntax consistently, and I still fell back into my old camelCase naming convention many times.
I also made everything as explicit as possible — for my own sake — as reading this code is not yet automatic for me. That means I wrote a lot of... imperative? code including defining a local var on one line before using it in a function on the next. I'm not intimidated by cleaning all that up, but for now I want to keep everything on as many separate lines as I can until I'm confident reading Clojure and thinking Clojure. In other words, I want to see every step as a separate step, with its own line of code.
My goal with putting this code up NOW, in such an awful state, is because I want to eventually make it as FP and Clojuresque as possible, given this specific program, and I'm hoping somebody will volunteer to help me :)
If you are just starting to learn Clojure and/or FP, you should NOT be looking at my code, design, thinking. Save yourself, and wait until someone far more skilled helps "fix" it.
What the program/game does:
It's basically a super-simplified version of the Simon music game: the game plays a note, then you play the note, and each time you finish your turn, the game adds a new note to the end of the previous sequence and plays the sequence from the beginning. When it's your turn, you must match the full sequence, from the beginning. The end goal is to see how long / for how many notes you can keep replaying the sequence perfectly... 8 notes, 12 notes, etc.
There are three buttons, each one represents a specific note and color. When the game plays a sequence, it briefly colors the button and plays the note, but then the button returns to gray (this is different from the Simon game, but hey... I had to add just a tiny bit more challenge).
What the game does (and does not do) now:
In version 1, it does NOT give you a choice about starting over when you "lose" a sequence by playing a wrong note during your turn. The game just... starts over.
If you want to Quit the game, you must quit the Java application, using the GUI menu or command-Q.
HUGE, CANYON-SIZED FLAW: I was too tired to finish for this version... when you hit a wrong note and the game starts over with a new sequence (which is hard to even recognize when it happens), the GUI does not prevent you from clicking buttons. If you don't realize you "failed", you might be clicking a button while the game itself is already starting its new sequence.
I decided to do this game as a way to force myself to learn Clojure and begin to learn to think functional, after two decades of thinking in objects.
Making something that needed a Java Swing GUI and used the Java Sound API was not the smartest move... but I kept those parts almost completely isolated from the rest of the code.
This is not meant to ever be a "real" game. I don't care about making it robust or extensible or fun or not-buggy, except where changes would serve a learning role for me. I suspect some of those would.
I DO want to take what's here now and slowly make it more and more FPish as I learn about FP.
I tried to do a few things The FP Way, though I have no idea if I even succeeded in that:
1. I managed to corral the persistent state that changes into one atom variable that holds the events coming in from the GUI event thread.
2. Everything else that's needed is threaded through the game functions as args.
3. There are two recursive methods (also a first!) that make up the logic of the game:
This function plays the music sequence (adds 1 new note to the sequence first) then calls check_guess. When check_guess returns, the main_game_loop function looks at the return to see whether it should empty the sequence (user failed / needs to start a new music sequence) OR pass the current sequence back to the recursive call to main_game_loop. The recursive call to main_game_loop could go on forever, so I used the "recur" function in Clojure to make it 'tail recursive', which stops it (behind the scenes) from adding each recursive call to the stack. Otherwise, stack overflow error in 3, 2, 1... BOOM.
This function (called from the main_game_loop) just keeps checking for new user events (put into the one persistent queue by the GUI thread / event handler). It keeps comparing the events in the queue to the current music sequence.
If the user hits a wrong note, returns to main_game_loop returning nil (which main_game_loop checks for to determine if it should start a NEW game/sequence or continue with the current one).
If the user event queue IS a match for the current game sequence BUT it's still incomplete, then we (recursively) call check_guess again.
If the user event queue IS a match AND it is the same length as the current game sequence, then the user has successfully completed his "turn", and we return 'true' to the main_game_loop, which it uses to call itself (the main_game_loop) again without first emptying the game sequence.
Everything else in the code
The vast majority of the code is Java GUI stuff (using Clojure syntax for the Java API) almost all of which is isolated in a few functions. Most of the construction/set-up is in the main function, then there is a function that paints the buttons each time the new game sequence is played OR when the user clicks one of the buttons.
The non-GUI Java code is mostly from the Java Sound API, and all it does it play the sounds associated with each of the buttons (and the "bad" sound for "WRONG").
Issues with the NON-Java parts (Clojure stuff)
The idea of programming by threading what's needed through the functions as args is a brain-bender for me.
Having no global variables (well, I had one) or object state, especially with a game (albeit a really tiny one) was a challenge to my way of thinking about it.
I'm certain I did the whole send-things-to-functions-as-args poorly. I could have made it cleaner by stuffing all the args into a list and just passing the list, but that felt like a horrible move as it would depend on my arbitrary ordering, etc. I'm sure that's not how it's supposed to be.
It did "work", though.
I was able to (mostly) isolate the persistent "state" that would be changing to just a single variable: the vector holding the events, updated by the GUI event handler. The function that updates this state is also isolated, but not perfectly: it happens in both the GUI thread and in the main game code.
Obviously my "muscle memory" for Java is still strong despite many years away from programming of any kind. To think in verb-noun vs. noun-verb is just one of the many things I must get used to. Comments almost killed me, as I type that "//" instead of ";" without even realizing it.
Still makes me a little queasy to not be working in a strongly-typed language. We'll find out how sloppy I am. And I was calling Java code that declares exceptions and nothing forced me to handle-or-declare. I assume it will blow-up at runtime, and I'm guessing I should be handling/declaring but I haven't got to that page in the Clojure book yet :)
There are some parts of the syntax I used in this code that I really don't fully understand in terms of intention -- like when to use "let", etc.
Clojure is more fun and simpler than Java!
Issues with the Java parts:
GUI (nothing to do with Clojure)
While I was surprised by how simple and workable it is to use the Java GUI stuff (Swing and AWT) in Clojure code, I had forgotten that when you 'roll your own' UI elements, the repainting doesn't happen the way you want (or rather WHEN you want). Took me nearly a half-day to figure out, as repaint wasn't enough.
The problems were because I was using a simple JPanel for my buttons rather than an actual JButton. I wanted the buttons to change color, and the JButton doesn't really let you do that. I thought I might have to create my own JPanel and override the paint method, but it turned out the answer was to call repaintImmediately, passing in the exact size of the button being painted.
Using GUI event handlers does not map well to FP, because they run in their own thread and there is no way to pass arguments along. To help keep things more FPish, I did not try to have the event listener do anything "smart" with respect to the game, but just had it add the events to the one persistent (atom) queue.
I had some SERIOUS timing issues around syncing up the blink of the button and the corresponding sound, without one or the other happening too quickly. I found found myself sticking in Thread/sleep calls. Yes, I know, MY EYES IT BURNS US. But this is a learning game, just for me, so I didn't want to spend my time on Java-specific weirdness. Were this a real project, oh I would have to figure out what the hell I was actually doing there.
MIDI (nothing to do with Clojure)
Again, I was surprised how natural and easy it was to use the Java Sound API. Everything worked as expected. I did have some problems when I started making it efficient by reusing the Sequencer and Track for everything. A Sequencer uses system resources, and while it worked to just keep making a new one every time I played a note, that was... um... not sustainable. When I decided to pass the Sequencer and Track into the game functions to reuse them, I first started "rewinding" the Sequencer each time, and then realized it was just adding NEW notes/instruments to the same track, so each time it played a note it was playing all previous notes for that tick. Rather than try to remove the events already on the track (could find NO methods for emptying/clearing/erasing a track, though you could remove individual events), I changed the note-playing code to add the new MIDI info to the end of the current (one and only) track, since the Sequencer's default behavior is to just keep playing from wherever it last was (as if you hit pause on a song and then hit play again).
Function overview for the music game
[the full code .clj is here.]
; STATE THAT CHANGES IS IN THIS SECTION ONLY:
(def button_queue (atom ))
; holds the button click events (as integers 1-3)
; is updated by the GUI event handler (from the GUI thread)
; user/player goal is for this queue to always match the current sequence being
; played by the game itself
; included the actual code here, since it's simple
(defn update_queue [numToAdd] (if (> numToAdd 0) (swap! button_queue conj numToAdd) (reset! button_queue ) ) ) ;---------------------------------------------------
; ------------------CHECK USERS GUESS (clicks) ------
; as soon as a sequence plays, this starts looping (recursive)
; looking at the button click events, comparing to current actual game sequence
(defn check_guess [currSeq]
; keeps checking the atom queue where the events are being stored by the GUI event handler
; compares the current game music sequence to the sequence of user events
; if user fails (presses a button out of sequence), start a new game
; if user succeeds -- a perfect match -- return to main game loop to add a note and
; play the next (now longer) sequence
; if user is correct *so far* but has not completed the full sequence, recursively
; call check_guess again
) ; end check_guess function
; this function take a vector w/ MIDI note & instrument integers, the MIDI sequencer, and track
; it sets the instrument and plays the note
; the MIDI sequencer and track were created in main method
(defn play_sound [note_and_inst_vec player track]
; this is 100% Java MIDI code, takes a 2-element vector that holds
; note and instrument properties, and the MIDI Sequencer and Track
; (constructed in the main function)
) ; end play_Sound function
; ------LIGHT / COLOR THE BUTTONS ---
(defn light_buttons [button color]
; like play_sound, 100% Java
; simple GUI code to briefly change the color of the button
; from gray to *that button's color* and back to gray
; for now, the default gray is hard-coded, & the button colors
; are kept in a property list mapping each button to pre-determined color
) ; close light_buttons function
; -----------MAIN GAME LOOP-------------
(defn main_game_loop [buttonlist msg_display currentSequence player track]
; most of the "game" lives here (and in check_guess)
; -- play a sequence of notes
; -- then call check_guess which keeps checking the user's event queue
; -- based on what check_guess returns, either add 1 note to sequence OR start over
) ; close main_game_loop function
(defn startgame [buttonlist msg_display player track]
; for now this is called only the very first time
; all it does is display the game starting message then calls the main_game_loop function
; sending the main_game_loop 1 additional arg, an empty sequence that represents
; the sequence of notes the game creates/plays that the user must match
) ; close startgame function
;---MAIN (constructs GUI and MIDI) ----
; constructs the entire GUI (mostly Java)
; creates a *single* anonymous inner class/obj to handle the mousePressed event
; the mouse event comes in on the GUI thread, not my main game thread, so it cannot
; have any args passed to it from the game.
; SO, the GUI event handler does only TWO things:
; 1. plays the tone and changes the color for this button
; 2. adds this event to the atom event queue that the check_guess function looks at
; this function also constructs the MIDI Sequencer and Track
; Calls start_game, and passes to it the parts of the GUI and MIDI that it needs:
; - text display area to show messages to the user
; - the list of buttons
; - the MIDI Sequencer and Track
) ; close main