diff --git a/practice/ttt/README.md b/practice/ttt/README.md new file mode 100644 index 0000000..08c1596 --- /dev/null +++ b/practice/ttt/README.md @@ -0,0 +1,46 @@ +# Tic Tac Toe +### Cli interface + +To start the game use `lein run` + +Possible positions: + +| 1 | 2 | 3 | +|-----|-----|-----| + | 4 | 5 | 6 | +| 7 | 8 | 9 | + +=> lein run: + +Enter a number between 1-9 to make a move + +Enter h to view position board + +Enter q to quit + +Current board: + +| e | e | e | +|-----|-----|-----| +| e | e | e | +| e | e | e | + +Player is o +=> 1 + +Current board: + +| o | e | e | +|-----|-----|-----| +| e | e | e | +| e | e | e | + +Player is x + +=> q + +Bye bye! + +### Developer notes + +Run tests using `lein test` \ No newline at end of file diff --git a/practice/ttt/board.clj b/practice/ttt/board.clj new file mode 100644 index 0000000..74dd092 --- /dev/null +++ b/practice/ttt/board.clj @@ -0,0 +1,84 @@ +(ns ttt.board + (:refer-clojure :exclude [update]) + (:require + [clojure.set] + [ttt.matrix-operations :as mo])) + + +(def size 3) + + +(def position-to-coordinate + (->> (for [x (range size) + y (range size)] + [x y]) + (map-indexed (fn [i coord] [(inc i) coord])) + (into {}))) + + +(def empty-board + (mapv vec (partition size (repeat (* size size) :e)))) + + +(def valid-positions + (keys position-to-coordinate)) + + +(def positions-string + (mo/matrix->string (partition size (sort valid-positions)))) + + +(def valid-game-player-set #{:o :x :e}) + +(def game-player-set #{:o :x}) + + +(defn to-string + [board] + (mo/matrix->string (mapv #(mapv name %) board))) + + +(defn empty-coordinate? + [board coordinate] + (= :e (get-in board coordinate))) + + +(defn valid? + [board] + (and (mo/square-matrix? board) + (clojure.set/subset? (set (flatten board)) valid-game-player-set))) + + +(defn update + [board coordinate game-player] + (when (valid? board) + (assoc-in board coordinate game-player))) + + +(defn winner-of-collection + [coll] + (let [unique-val (set coll)] + (when (and + (= 1 (count unique-val)) + (not (:e unique-val))) + (first unique-val)))) + + +(defn winners-of-seqs + [matrix] + (->> matrix + (mo/get-all-possible-seqs) + (map winner-of-collection) + (remove nil?))) + + +(defn winning-game-player + [board] + (when (= 1 (count (winners-of-seqs board))) + (first (winners-of-seqs board)))) + + +(defn winner + [board] + (when (valid? board) + (winning-game-player board))) diff --git a/practice/ttt/core.clj b/practice/ttt/core.clj new file mode 100644 index 0000000..7be790c --- /dev/null +++ b/practice/ttt/core.clj @@ -0,0 +1,8 @@ +(ns ttt.core + (:gen-class) + (:require [ttt.game :as game] + [ttt.board :as board])) + +(defn -main + [] + (game/play board/empty-board)) diff --git a/practice/ttt/game.clj b/practice/ttt/game.clj new file mode 100644 index 0000000..d0d5a01 --- /dev/null +++ b/practice/ttt/game.clj @@ -0,0 +1,63 @@ +(ns ttt.game + (:require + [failjure.core :as f] + [ttt.board :as board] + [ttt.user-input :as user-input])) + + +(def player-order + (take 9 (cycle board/game-player-set))) + + +(defn over? + [board player-sequence] + (or (board/winner board) + (empty? player-sequence))) + + +(defn process-parsed-command + [parsed-command board player-sequence] + (case parsed-command + "h" (do + (println "\nPosition board" board/positions-string "\n") + [board player-sequence]) + + "q" [nil player-sequence] + + (let [player (first player-sequence)] + (if (board/empty-coordinate? board parsed-command) + [(board/update board parsed-command player) (rest player-sequence)] + (do (println "Enter an empty position") + [board player-sequence]))))) + + +(defn process-command + [command board player-sequence] + (f/attempt-all [valid-command (user-input/valid-command? command) + parsed-command (user-input/parse-command valid-command)] + (process-parsed-command parsed-command board player-sequence) + (f/when-failed [e] + (println (f/message e)) + [board player-sequence]))) + + +(defn play + [board] + (println "Enter a number between 1-9 to make a move \nEnter h to view position board \nEnter q to quit") + (loop [board board + player-sequence player-order] + (cond + (or (nil? board)) + (println "Bye bye!") + + (over? board player-sequence) + (if-let [winner (board/winner board)] + (println "Winner is" (name winner)) + (println "It's a draw")) + + :else + (do (println "Current board:" (board/to-string board) + "\nPlayer is" (name (first player-sequence))) + (let [command (read-line) + [new-board new-player-sequence] (process-command command board player-sequence)] + (recur new-board new-player-sequence)))))) diff --git a/practice/ttt/matrix_operations.clj b/practice/ttt/matrix_operations.clj index 9033c5e..7699271 100644 --- a/practice/ttt/matrix_operations.clj +++ b/practice/ttt/matrix_operations.clj @@ -47,3 +47,17 @@ (transpose-matrix matrix) (get-diagonals-of-matrix matrix))) + +(defn matrix->string + [matrix] + (apply str (for [row matrix] + (->> row + (interpose " ") + (apply str "\n") + str)))) + + +(def test-matrix + [[:e :x :e] + [:o :o :o] + [:x :e :x]]) \ No newline at end of file diff --git a/practice/ttt/test/board_test.clj b/practice/ttt/test/board_test.clj new file mode 100644 index 0000000..e59953f --- /dev/null +++ b/practice/ttt/test/board_test.clj @@ -0,0 +1,76 @@ +(ns board-test + (:require + [clojure.test :refer :all] + [ttt.board :refer :all])) + + +(def test-empty-board + [[:e :e :e] + [:e :e :e] + [:e :e :e]]) + + +(deftest empty-coordinate-test + (testing "Returns true" + (testing "when position is empty" + (is (= true (empty-coordinate? test-empty-board [1 1]))))) + (testing "Returns false" + (testing "when position is not empty" + (is (= false (empty-coordinate? [[:e :e :e] + [:e :o :e] + [:e :e :e]] [1 1])))))) + + +(deftest valid-test + (testing "Returns true" + (testing "when board is a square matrix and all pieces are valid" + (is (= true (valid? test-empty-board))))) + (testing "Return false" + (testing "when board is not square matrix and all pieces are valid" + (is (= false (valid? [[:e :e :e]])))) + (testing "when board is square matrix and all pieces are not valid" + (is (= false (valid? [[:e :e :e] + [:e :e :e] + [:e :e 1]])))))) + + +(deftest update-test + (testing "Returns correctly updated board" + (testing "when board is valid" + (is (= [[:e :e :e] + [:e :o :e] + [:e :e :e]] (update test-empty-board [1 1] :o)))))) + + +(deftest winner-of-collection-test + (testing "With all :e elements" + (is (= nil (winner-of-collection [:e :e :e])))) + + (testing "With all :x elements " + (is (= :x (winner-of-collection [:x :x :x])))) + + (testing "With 2 :x elements" + (is (= nil (winner-of-collection [:x :x :o]))))) + + +(deftest winning-game-piece-test + + (testing "Returns winning game piece " + (testing "when winner is along a row" + (is (= :o (winning-game-player [[:e :x :e] + [:o :o :o] + [:x :e :x]])))) + (testing "when winner is along column" + (is (= :x (winning-game-player [[:x :e :o] + [:x :e :e] + [:x :e :o]])))) + (testing "when winner is across diagonal" + (is (= :x (winning-game-player [[:x :e :e] + [:o :x :e] + [:o :e :x]]))))) + + (testing "Returns nil " + (testing "when there is a draw" + (is (= nil (winning-game-player [[:x :e :o] + [:x :x :e] + [:o :x :o]])))))) diff --git a/practice/ttt/test/game_test.clj b/practice/ttt/test/game_test.clj new file mode 100644 index 0000000..32dd05c --- /dev/null +++ b/practice/ttt/test/game_test.clj @@ -0,0 +1,30 @@ +(ns game-test + (:require + [clojure.set] + [clojure.test :refer :all] + [ttt.game :refer :all])) + + +(def test-player-sequence (cycle #{:o :x})) + + +(deftest over-test + (testing "Returns winner " + (testing "when there is a winner in board" + (is (= :o (over? [[:e :x :e] + [:o :o :o] + [:x :e :x]] (take 2 test-player-sequence)))))) + (testing "Returns true" + (testing "when there is a draw" + (is (= true (over? [[:x :o :x] + [:x :o :o] + [:o :x :x]] (take 0 test-player-sequence)))))) + (testing "Returns false" + (testing "when there no winner with moves left" + (is (= false (over? [[:x :e :o] + [:e :x :e] + [:o :x :o]] (take 3 test-player-sequence))))) + (testing "when the board is empty" + (is (= false (over? [[:e :e :e] + [:e :e :e] + [:e :e :e]] (take 9 test-player-sequence))))))) diff --git a/practice/ttt/test/matrix_operations_test.clj b/practice/ttt/test/matrix_operations_test.clj index 73b5821..f8670ae 100644 --- a/practice/ttt/test/matrix_operations_test.clj +++ b/practice/ttt/test/matrix_operations_test.clj @@ -20,10 +20,8 @@ [2 5 8] [3 6 9]] (transpose-matrix test-square-matrix)))) - (testing "With 2x1 matrix" - (is (= [[0 2]] (transpose-matrix [[0] [2]])))) - (testing "With empty matrix" - (is (= [[]] (transpose-matrix [[]]))))) + (testing "With 1x2 matrix" + (is (= [[0 2]] (transpose-matrix [[0] [2]]))))) (deftest primary-diag-coordinates-test @@ -58,3 +56,9 @@ (testing "With 1x2 matrix" (is (= false (square-matrix? test-1-by-2-matrix))))) + +(deftest matrix-to-string-test + (testing "With 3x3 matrix" + (is (= "\n1 2 3\n4 5 6\n7 8 9" (matrix->string test-square-matrix)))) + (testing "With 1x2 matrix" + (is (= "\n1 2" (matrix->string test-1-by-2-matrix))))) diff --git a/practice/ttt/test/tic_tac_toe_test.clj b/practice/ttt/test/tic_tac_toe_test.clj deleted file mode 100644 index 71c5f24..0000000 --- a/practice/ttt/test/tic_tac_toe_test.clj +++ /dev/null @@ -1,60 +0,0 @@ -(ns tic-tac-toe-test - (:require - [clojure.set] - [clojure.test :refer :all] - [ttt.tic-tac-toe :refer :all])) - - -(deftest winner-of-collection-test - (testing "With all :e elements" - (is (= nil (winner-of-collection [:e :e :e])))) - - (testing "With all :x elements " - (is (= :x (winner-of-collection [:x :x :x])))) - - (testing "With 2 :x elements" - (is (= nil (winner-of-collection [:x :x :o]))))) - - -(deftest valid-board-test - (testing "Validity of a board" - (testing "with valid game pieces" - (is (= true (valid-board? [[:x :e :o] - [:x :x :e] - [:o :x :o]])))) - (testing "with invalid game pieces" - (is (= false (valid-board? [[:x :e :o] - [:x :x :e] - [:o 1 :o]])))))) - - -(deftest winner-of-board-test - - (testing "Returns winning game piece " - (testing "when winner is along a row" - (is (= :o (winner-of-board [[:e :x :e] - [:o :o :o] - [:x :e :x]])))) - (testing "when winner is along column" - (is (= :x (winner-of-board [[:x :e :o] - [:x :e :e] - [:x :e :o]])))) - (testing "when winner is across diagonal" - (is (= :x (winner-of-board [[:x :e :e] - [:o :x :e] - [:o :e :x]]))))) - - (testing "Returns no winner " - (testing "when there is a draw" - (is (= "No Winner" (winner-of-board [[:x :e :o] - [:x :x :e] - [:o :x :o]]))))) - (testing "Returns invalid board " - (testing "when game pieces aren't valid" - (is (= "Invalid Board" (winner-of-board [[:x :e :o] - [:x :x :e] - [:o 1 :o]])))) - (testing "when board is not a square matrix " - (is (= "Invalid Board" (winner-of-board [[:x :e]])))))) - - diff --git a/practice/ttt/test/user_input_test.clj b/practice/ttt/test/user_input_test.clj new file mode 100644 index 0000000..4675971 --- /dev/null +++ b/practice/ttt/test/user_input_test.clj @@ -0,0 +1,15 @@ +(ns user-input-test + (:require + [clojure.test :refer :all] + [failjure.core :as f] + [ttt.user-input :refer :all])) + + +(deftest valid-command-test + (testing "Returns command" + (testing "if command is valid" + (is (= "1" (valid-command? "1"))))) + (testing "Will not return exception" + (testing "when argument is invalid" + (is (f/failed? (valid-command? "abc")))))) + diff --git a/practice/ttt/tic_tac_toe.clj b/practice/ttt/tic_tac_toe.clj deleted file mode 100644 index d721fb9..0000000 --- a/practice/ttt/tic_tac_toe.clj +++ /dev/null @@ -1,65 +0,0 @@ -(ns ttt.tic-tac-toe - (:gen-class) - (:require - [clojure.set] - [ttt.matrix-operations :as mo])) - - -(def valid-game-pieces-set #{:o :x :e}) - - -(defn winner-of-collection - [coll] - (let [unique-val (set coll)] - (when (and - (= 1 (count unique-val)) - (not (:e unique-val))) - (first unique-val)))) - - -(defn winners-of-seqs - [matrix] - (->> matrix - (mo/get-all-possible-seqs) - (map winner-of-collection) - (remove nil?))) - - -(defn winning-game-piece - [board] - (if (= 1 (count (winners-of-seqs board))) - (first (winners-of-seqs board)) - "No Winner")) - - -(defn valid-board? - [board] - (clojure.set/subset? (set (flatten board)) valid-game-pieces-set)) - - -(defn winner-of-board - [board] - (if (and - (mo/square-matrix? board) - (valid-board? board)) - (winning-game-piece board) - "Invalid Board")) - - -(def test-board - [[:x :e]]) - - -(defn -main - [] - (prn (winner-of-board test-board))) - - -;; board -;; -> board-is-valid -;; -> get-all-possible-seqs -;; -> winners-of-seqs -;; -> remove nil? -;; -> count and all -;; -;; (count and all (remove nil? (winning seqs (get-all-possible-seqs board )))) diff --git a/practice/ttt/user_input.clj b/practice/ttt/user_input.clj new file mode 100644 index 0000000..b8c1aba --- /dev/null +++ b/practice/ttt/user_input.clj @@ -0,0 +1,30 @@ +(ns ttt.user-input + (:require + [failjure.core :as f] + [ttt.board :as board])) + + +(def extra-commands #{"q" "h"}) + + +(def valid-commands + (->> board/valid-positions + (map str) + (concat extra-commands) + set)) + + +(defn valid-command? + [command] + (if (contains? valid-commands command) + command + (f/fail "Please enter a valid input"))) + + +(defn parse-command + [command] + (if (contains? extra-commands command) + command + (get board/position-to-coordinate (Integer/parseInt command)))) + + diff --git a/project.clj b/project.clj index 4bd8ed7..44fa307 100644 --- a/project.clj +++ b/project.clj @@ -1,6 +1,8 @@ (defproject mohita-clj "1.0.0-SNAPSHOT" :description "FIXME: write description" - :dependencies [[org.clojure/clojure "1.10.0"]] + :dependencies [[org.clojure/clojure "1.10.0"] + [cli4clj "1.9.0"] + [failjure "2.2.0"]] :source-paths ["practice/"] :test-paths ["practice/ttt/test"] - :main ttt.tic-tac-toe) + :main ttt.core)