Edgardo Carreras | Blog

Tic-Tac-Toe Web - Game Factory

September 10, 2021


👋!! Today I want to remove duplication of the code I have in both our web and cli versions where possible.

It seems both of them have a say on how to deal with turns, the UI is also playing for the AI, and setting up the ai-difficulty to the play options. If we do this right, we then can make sure the cli and web support the same features for play settings, ai playing, and it will allow us to do better validation on who is playing.

So I will use the Factory Pattern to create the initial game map in our core library so that the UI doesn’t it will consider all game options so that I can share the code and remove that responsibility away from the UI.

(describe "create game"
          (it "should return a game"
            (should-not (nil? (create-game nil))))
          (it "should of played game when ai goes first"
            (should= 8
                     (-> (create-game
                           {:ai-difficulty :easy :first-player :ai})
                         :board
                         get-empty-indexes
                         count)))
          (it "should of not played game when player goes first"
            (should= 9
                     (-> (create-game
                           {:ai-difficulty :easy :first-player :player})
                         :board
                         get-empty-indexes
                         count))))

Here is the production code to make this test pass!

(defn create-game [options]
  (let [{:keys [ai-difficulty first-player]} options
        game (if (not (= :ai first-player))
               new-game
               (play new-game (ai-play new-game)))]
    game))

Here we consider the game options to see who goes first and if the ai goes first the game will be created with the ai move already played

Next I want my game to take the turns for the ai after our player plays. Here the test:

   (it "ai should play right after player plays"
            (should= 7
                     (-> (create-game
                           {:ai-difficulty :easy :first-player :player})
                         (play [0 0])
                         :board
                         get-empty-indexes
                         count)))

Here is the production code where we use the Strategy Design Pattern.

(defn get-ai-move [ai-difficulty]
  (cond
    (nil? ai-difficulty)
    nil
    (= ai-difficulty :easy)
    get-random-move
    :else
    get-best-move))


(defn create-game [options]
  (let [{:keys [ai-difficulty first-player]} options
        ai-play (get-ai-move ai-difficulty)
        game (if (not (= :ai first-player))
               new-game
               (play new-game (ai-play new-game)))]
    (assoc game :ai-play ai-play)))

(defn play [game index]
  (let [{:keys [board active-player ai-play]} game
        new-board (assoc board index active-player)
        new-game (assoc game :board new-board)
        opponent (get-opponent active-player)]
    (cond
      (invalid-move? game index)
      game
      (game-has-wining-play? new-board active-player)
      (assoc new-game :winner active-player :over? true)
      (board-full? new-board)
      (assoc new-game :over? true)
      (nil? ai-play)
      (assoc new-game :active-player opponent)
      :else
      (let [ai-move (ai-play (assoc new-game :active-player opponent))]
        (assoc new-game :board (assoc new-board ai-move opponent))))))

Awesome! Next I’ll add our peer to peer library to work on our Player vs Player feature!

<3!


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.

© 2023, Edgardo Carreras