WARNING: this entry is technical. I suspect several of the next few in the weeks to come will be. I'll label them with Cartagena (with parts).
Impetus
Several weeks ago, Tanya and I attended clojure/conj, an industry conference for functional developers using the clojure language. It was a great experience on several levels. First of all, it was my first trip to Philadelphia, where the weather was fantastic and the cheesesteaks were... actually not as good as I'd hoped. We saw several former coworkers as well as a lot of really interesting discussions about clojure and how it's being used to make the planet a better place. Or, at least, how it's being used. :-)
One of the presentations we saw was a juxtaposition of object oriented design and functional design. If you're really interested, you can have a look at
API First vs Data First design. I'll skip over the first part of the presentation, as he engages in a bit of reductio ad absurdum. The second part, however, he talks about data first design, and he uses an implementation of the game Cartagena as his example. It prompted me to at least consider doing my own implementation, except as a TDD exercise.
Full disclosure: my brilliant coworker
Timothy Pratley is also working on an
implementation. His is going to be prettier, I have no doubt. It'll be full of figwheel and buttons and menus and all sorts of nice things. Mine's going to be a plain ol' TDD, starting with the engine and *maybe* working up to a nicer UI at some point... :-)
Starting
First things first: I've never played the game, so I needed to find a set of rules and pictures for references. Luckily, the game is popular enough to have the instructions/rules listed on a
handy-dandy webpage. I read through them briefly to make sure they were complete.
Next, I created a new clojure project. Luckily, accomplishing this in clojure is ridiculously busy.
Having an application locally doesn't do anyone any good if my machine gets abducted by aliens. Why aliens would abduct it is beyond me. I am, after all, only a human. To avoid the consequences of that potential catastrophe, I need to get the project under source control. I do that by using git and github. The commands are really simple.
$ cd cartagena
$ git init
I am also using a tool called scm_breeze locally to help manage git repositories. As such, adding the files to the local code repository I just initialized is a *breeze*. I hope you see what I did there...
$ gca
After this, a copy of my project is in a local source code/version control repository. I still need to get it onto the Internet so that I can destroy my computer if need be. To do this, I need to create a repository on the Internet and then synchronize it with my local repository. I use github to accomplish this. I won't go through the details here, aside from saying that you have to have a github account, and you have to press the "New Repository" button. I name my remote repository (the one on the Internet, in github) the same as my local to avoid confusion.
All that's left to do is tell my local repository about my remote repository. Just a couple more commands to enter.
$ git remote add origin git@github.com:rusty-software/cartagena.git
$ git push -u origin master
Github actually tells you about these commands, so you don't even need to figure them out yourself. I love it when tools help me look smart!
First Content and Tests
Now that I've got a remote repository, and no longer have to live with the constant fear of catastrophic failure of my machine, I can settle down enough to get to implementation work. The first real changes I made to the code base have to do with the README file. I want to document at least the basic instructions and rules locally just in case the webpage I'm using as a reference is unexpectedly removed.
I spend a few minutes reviewing the source webpage and realize that it refers to the board configuration, but it feels like there might be specific board constructs that aren't covered. I spend a few more minutes looking for pictures of the playing pieces and find a few. From those pictures, I figure out that the board is made up of six pieces with specific configurations of the iconography. I make note of this in the README and move on to documenting the rest of the basic rules. I read over the full set, then commit the changed content to the local repository and push those changes to the remote repository.
I'm finally ready to start coding. The first thing I want to do is initialize a board. I implement a test to ascertain when I've done this successfully.
(deftest initialize-board-test
(let [board (initialize-board)]
(is (= 36 (count board)))
(doseq [icon [:bottle :gun :hat :key :knife :skull]]
(is (= 6 (count (filter #(= icon %) board)))))))
Running this test fails. In fact, since I haven't defined the calling function, it fails pretty miserably. I stub out the function and have a legitimate failure on my hands. It doesn't do at all what I want it to do, and that's perfect. I have a RED test! Let's make it green!
Notice that I have two basic assertions here. The first is that I think the board should have 36 spaces. The second is that each icon should be represented 6 times. To that end, I begin to codify what I know the board segments are.
(def card1 [:bottle :gun :hat :skull :knife :key])
(def card2 [:knife :bottle :key :gun :hat :skull])
(def card3 [:hat :key :gun :bottle :skull :knife])
(def card4 [:key :bottle :skull :knife :hat :gun])
(def card5 [:gun :key :knife :hat :skull :bottle])
(def card6 [:hat :knife :key :bottle :gun :skull])
(def card1r (vec (reverse card1)))
(def card2r (vec (reverse card2)))
(def card3r (vec (reverse card3)))
(def card4r (vec (reverse card4)))
(def card5r (vec (reverse card5)))
(def card6r (vec (reverse card6)))
(def all-cards [card1 card2 card3 card4 card5 card6
card1r card2r card3r card4r card5r card6r])
I've named the vars "cards for now. This is probably a bad idea, as cards are other things in the game. However, it's good enough for this first test to pass, and I just want to get something accomplished.
The next thing to do is implement the initialize board function. Given that I have a collection of all of the possible cards, getting a random sample of them is simplicity in itself.
(defn initialize-board
"Returns a vector populated with icons from the 6 of the board pieces concatenated."
[]
(->> all-cards
shuffle
(take 6)
flatten
vec))
I'm using the thread-last operator here. My initial implementation consisted of the function calls embedded within each other. This seems slightly clearer to me.
Given this implementation, I re-run the test, and now they pass! Yay! This is a good commit point, so I commit my changes and push them to the remote repo as well.
I realize that there is another test to write. I want to ensure that boards are varying as they are instantiated. One more test should do the trick.
(deftest initialize-board-test
(testing "Returns the right number of spaces as well as icons"
(let [board (initialize-board)]
(is (= 36 (count board)))
(doseq [icon [:bottle :gun :hat :key :knife :skull]]
(is (= 6 (count (filter #(= icon %) board)))))))
(testing "Boards are not exactly alike"
(is (not (= (initialize-board) (initialize-board))))))
As you can see, I extended my original test, adding some testing description. Now I've got a test to make sure the boards are different. Running the test, everything passes as expected.
There are two important points here. The first is that I make small, incremental steps toward a larger solution. The second is that I have now ensured that this rules are enforced on a going-forward basis. These are good practices, as if something breaks I can be fairly certain the scope of the breakage is small and if someone else does something to break my rules, they know about it.
I stopped here for the day, realizing that there are two immediate changes I'll be making next time -- the aforementioned card var name, and the board itself is too simplistic. We'll need to store state about which pieces are on which spots. However, that didn't happen in THIS set of rules, so we delayed implementing that complexity until the simpler things were done.
More on this as it develops. In the meantime, I hope you'll let me know if you have questions or comments. I like feedback! :-D