Edgardo Carreras | Blog

Persisting Tic-Tac-Toe State on Server Restarts

October 19, 2021

👋 Hello there!!

So far we’ve been keeping state of the server tic-tac-toe game application in memory but no more, here we will implement saving the state in a file.

Thinking about the design of the state saving I need a way to deserialize our state in order to save it in a text file, and a way to serialize it back in order to load it. My game state is mostly easily serializable using clojure.edn/read-string

defn read-string

“Reads one object from the string s. Returns nil when s is nil or empty. Reads data in the edn format (subset of Clojure data)

My main issue is our :ai-play which is a function and is not serializable so on saving and loading state I need to either, remove the ai-play function before serializing it and adding it when deserializing.

So let’s start with our test:

(def test-sessions
          {:play-mode     nil
           :ai-play       nil
           :first-player  nil
           :online-mode   nil
           :ai-difficulty nil
    :game nil}})

(def test-sessions-2
   {:options default-game-options
    :game    (create-game-factory default-game-options)}})

(def ai-game-options
  (assoc default-game-options :play-mode "ai" :ai-difficulty "easy" :first-player "ai"))

(def test-sessions-3
   {:options ai-game-options
    :game    (create-game-factory ai-game-options)}})

  (before (save-sessions test-sessions))
  (it "should load a new game session from file"
    (should= test-sessions (load-sessions)))
  (it "should save a default game session from file"
        (save-sessions test-sessions-2)
  (it "should save an ai game session from file"
        (save-sessions test-sessions-3)

In our test we mock some sessions and have some function for saving and loading the state.

Lets look at the implementation:

(def sessions-file-path "./sessions.txt")

(defn load-sessions []
  (let [sessions-without-ai-play (clojure.edn/read-string (slurp sessions-file-path))]
        (fn [m k v]
          (let [game (:game v)
                options (:options v)
                ai-play (get-ai-command (:ai-difficulty options))
                game-with-ai-play (if (nil? game) nil (assoc game :ai-play ai-play))]
            (assoc m k (assoc v :game game-with-ai-play))))

(defn save-sessions [sessions]
        (fn [m k v]
          (let [game (:game v)
                game-without-ai-play (if (nil? game) nil (assoc game :ai-play nil))]
            (assoc m k (assoc v :game game-without-ai-play))))

Our hero function that I learned about is reduce-kv:

(reduce-kv f init coll)

Reduces an associative collection. f should be a function of 3 arguments. Returns the result of applying f to init, the first key and the first value in coll, then applying f to that result and the 2nd key and value, etc. If coll contains no entries, returns init and f is not called. Note that reduce-kv is supported on vectors, where the keys will be the ordinals.

It allowed us to easily reduce a map into a new map.

Our save function removes the :ai-play function from the state to be able to easily serialize it into a string that we can later easily load with our read-str function.

Our load function loads from the text file and adds the ai-play into it. Thankfully we had a function in our tic-tac-toe core library (business /use cases rules) to get this ai-play function.

I also found extremely intuitive and hilarious the function name for writing and reading files in clojure (spit/slurp)


Want to hear more from me?

Signup to my newsletter!

CarrerasDev Newsletter

A free email newsletter on how to create high-performing development teams.

Written by Edgardo Carreras.

© 2024, Edgardo Carreras