I would say the biggest change is that the core.async version doesn't require any quasi-global variables for tracking game state. In the normal version I had 3 variables for storing state:
(def player (atom ttt/X)) (def board-data (atom nil)) (def board-dom (atom nil))
While in the core.async version, the only quasi-global variable is the channel that passes click events. The rest of the state stays within the game loop. The real version of the loop can be seen on github, but this is a pseudo-code version.
(defn game-loop [first-player] (go (loop [board empty-board player first-player] (let [event (<! click-channel)] new-board (view-to-data dom) next-player (if (= player X) O X)] (if (win? board) (recur (create) first-player)) (if (full? board) (recur (create) first-player)) (recur new-board next-player)))))