2
\$\begingroup\$

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:

  1. How "idiomatic" my code is
  2. How I can make the error-checking code in -main less deeply nested (sort of like a "guard clause" in an imperative language)
  3. Cool clojure "tricks" that might make my easier to follow
  4. 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))))))))))))
\$\endgroup\$

0