I wrote a command line tic tac toe game in Clojure. I've broken this post into three sections: Feedback Requests, Gameplay, and Code
Feedback Requests
I'm looking for feedback on:
- How "idiomatic" my code is
- How I can make the error-checking code in
-main
less deeply nested (sort of like a "guard clause" in an imperative language) - Cool clojure "tricks" that might make my easier to follow
- Other ways I can make the code easier to read
Gameplay
The symbols get populated in the grid as they are entered.
- - -
- - -
- - -
x's turn. Enter coordinates: 1 1
- - -
- - -
x - -
o's turn. Enter coordinates: 2 2
- - -
- o -
x - -
x's turn. Enter coordinates:
Code
(ns tic-tac-toe.core
(:require [clojure.string :as string]))
(defn board-size [] 3)
(defn make-game []
{::current-player ::x
::board [[nil nil nil]
[nil nil nil]
[nil nil nil]]})
(defn count-non-nil [coll]
(->> coll
(flatten)
(filter some?)
(count)))
(defn play-turn [game i j]
(-> game
(assoc-in [::board i j] (::current-player game))
(assoc ::current-player
(if (= ::x (::current-player game))
::o
::x))))
(defn all-x-or-all-o [row]
(or (every? #{::x} row)
(every? #{::o} row)))
(defn transpose [board]
(->> (range (board-size))
(mapv (fn [i]
(->> board
(mapv (fn [row] (get row i))))))))
(defn diagonals [board]
[[(get-in board [0 0]) (get-in board [1 1]) (get-in board [2 2])]
[(get-in board [0 2]) (get-in board [1 1]) (get-in board [2 0])]])
(defn get-winner [board]
(let [winning-row (or (->> board (filter all-x-or-all-o) first)
(->> board transpose (filter all-x-or-all-o) first)
(->> board diagonals (filter all-x-or-all-o) first))]
(first winning-row)))
(defn parse-input [str]
(->> (string/split (string/trim str) #" ")
(map parse-long)))
(defn mark->char [mark]
(cond (= mark ::x) \x
(= mark ::o) \o
:else \-))
(defn board->string [board]
(str (mark->char (get-in board [0 0])) " "
(mark->char (get-in board [0 1])) " "
(mark->char (get-in board [0 2])) "\n"
(mark->char (get-in board [1 0])) " "
(mark->char (get-in board [1 1])) " "
(mark->char (get-in board [1 2])) "\n"
(mark->char (get-in board [2 0])) " "
(mark->char (get-in board [2 1])) " "
(mark->char (get-in board [2 2]))))
(defn -main [& args]
(loop [game (make-game)]
(println (str "\n" (-> game ::board (board->string))))
(if (= (count-non-nil (::board game)) 9)
(println "Draw")
(let [winner (-> game ::board get-winner)]
(if winner
(println (str (mark->char winner) " wins!"))
(do
(print (str (mark->char (::current-player game))
"'s turn. Enter coordinates: "))
(flush)
(let [input (string/trim (read-line))]
(if (not (re-matches #"[123] [123]" input))
(do (println "Invalid input")
(recur game))
(let [[x y] (parse-input input)
i (- (dec (board-size)) (dec y))
j (dec x)]
(if (not (nil? (get-in (::board game) [i j])))
(do (println "That space is already taken")
(recur game))
(recur (play-turn game i j))))))))))))