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

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.