I'm building a tic-tac-toe app. It needs to be able to run in the terminal and in a desktop GUI. So, I'm getting to know the quil graphics library for Clojure (based on Processing, for Java). I tinkered with a few examples and watched a clean coders video to get a feel for how functional graphics programming with quil works. Uncle Bob summarized the quil processing loop like this:
(defn quil [setup draw update]
(loop [state (setup)]
(draw state)
(recur (update state))))
So, the general idea is that you provide quil with 3 functions:
- setup (invoked once, sets up initial 'state')
- draw (called to render each frame, receives the 'state' initialized in setup)
- update (called after draw, gives you a chance to return new, updated 'state')
I had already diagrammed the four different screens I wanted in the game:
- Choose the grid size (3x3 or 4x4)
- Configure Player 1 as human or AI (easy, medium, hard)
- Configure Player 2 (see step 2)
- Game Play
I had several questions at this point:
- How would I receive input from the user? (mouse clicks)
- How would I transition from screen to screen at the appropriate times?
- How would I integrate the UI with the game rules already implemented?
As a way to get familiar with the quil api and to answer question 1 (above) I built a small prototype that rendered a grid and allowed the user to place alternating X
and O
marks in cells indicated by mouse clicks. Here's what it looked like in action:
That prototype helped me get a handle on what it would be like to work with a state
data structure. Now, on to the question of transitioning from one screen/stage of the game to the next. Here's what I think I'll do, starting with the main
function:
(defn -main [& args]
(q/defsketch
tic-tac-toe
:title "Tic-Tac-Toe"
:size [screen-width screen-width]
:setup #'setup-root
:update #'update-root
:draw #'draw-root
:features [:keep-on-top]
:middleware [m/fun-mode]))
Ok, so, there has to be a few 'root' functions. The setup-root
function will establish the state
and, by so doing, declare which screen we are on:
(defn setup-root []
{:current-screen :choose-grid})
Here's a peak at update-root
:
(def updates
{:choose-grid #'choose-grid/update
:player1 #'player1/update
:player2 #'player2/update
:in-play #'in-play/update
:game-over #'game-over/update})
(defn update-root [state]
((updates (:current-screen state)) state))
So, update-root
is in charge of forwarding to more specific 'update' function depending on which screen (stage) of the game is currently being played. I'll bet you can guess what draw-root
will look like...
The update functions will probably set a flag on the state when it's time to transition and the root function will refer to a mapping of transitions to know which screen to go to next. (I'd rather not let any of the screens know which one is next, in case I want to rearrange the order at some point.) Stay tuned for how that shakes out...
Ok, so that [mostly] takes care of question 2, and for now I'm content to defer dealing with question 3 (I don't think it will be too difficult--one of the update functions will initialize the grid and attach it to the state data structure when enough info is present).
But another issue surfaced as I implemented the choose-grid/update
function. It was full of calculations. I felt it was important to anchor all on-screen elements based on ratios calculated from the width of the actual screen at runtime, which I wanted to be somewhat flexible. This meant lots of division and offsets and [x, y] coordinates to draw shapes and lines. It was a mess.
So, I decided to let the setup-root
function assemble and nest a collection of what I'm going to call screen-anchors
inside the state data structure. Here's a sneak peak:
(def screen-width 500)
(defn setup-root []
{:current-screen :choose-grid
:screen-anchors {:choose-grid (choose-grid/calculate-anchors screen-width)
:player1 nil ; TODO
:player2 nil ; TODO
:in-play nil ; TODO
:game-over nil}}) ; TODO
And here's what choose-grid/calculate-anchors
returns:
(defn calculate-anchors [screen-width]
(let [center (/ screen-width 2)
width screen-width
height screen-width]
{:text-size (/ height 24)
:welcome-text {:x (/ width 4) :y (/ height 10)}
:what-size-text {:x (/ width 3) :y (/ height 3)}
:welcome-divider-line {:p1 [0,,,,, center] :p2 [width center]}
:grid-divider-line {:p1 [center center] :p2 [center height]}
:grid3x3 {:width (/ width 2) :p1 [0 center]}
:grid4x4 {:width (/ width 2) :p1 [center center]}}))
Just a data structure with lots of [x,y] stuff all based on the actual screen width of the running program. Now, drawing the "choose-grid" screen looks quite straight-forward:
(defn draw [state]
(let [{:keys [text-size welcome-text what-size-text
welcome-divider-line grid-divider-line
grid3x3 grid4x4]}
(get-in state [:screen-anchors :choose-grid])]
(q/stroke-weight 10)
(q/stroke 50)
(q/fill 0)
(q/text-size text-size)
(q/text "Welcome to Tic-Tac-Toe!" (:x welcome-text) (:y welcome-text))
(q/line (:p1 welcome-divider-line) (:p2 welcome-divider-line))
(q/text "What size grid?" (:x what-size-text) (:y what-size-text))
(q/line (:p1 grid-divider-line) (:p2 grid-divider-line))
(q/stroke-weight 2)
(q/fill 255)
(render-grid 3 (:width grid3x3) (:p1 grid3x3))
(render-grid 4 (:width grid4x4) (:p1 grid4x4))))
And here's what the rendered screen looks like:
It's a work in progress, to be sure, (and there's lots left to do) but for day 1 of GUI work I feel pretty good. Today was filled with what felt like big decisions. Having made most of them, the next few days should go pretty smoothly...