├── .gitignore ├── README.md ├── project.clj ├── src └── mastermind │ ├── auto_play.clj │ ├── code_breaker.clj │ └── code_maker.clj └── test ├── code_maker_test.clj └── mastermind ├── auto_play_test.clj └── code_breaker_test.clj /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .lein* 3 | *.iml 4 | target/* 5 | .nrepl-port 6 | 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mastermind 2 | This project is used in Clean Code E54 on cleancoders.com. 3 | 4 | It plays the old logic game called "Master Mind". 5 | 6 | ------ 7 | 8 | The project uses [Midje](https://github.com/marick/Midje/). 9 | 10 | ## How to run the tests 11 | 12 | `lein midje` will run all tests. 13 | 14 | `lein midje namespace.*` will run only tests beginning with "namespace.". 15 | 16 | `lein midje :autotest` will run all the tests indefinitely. It sets up a 17 | watcher on the code files. If they change, only the relevant tests will be 18 | run again. 19 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject mastermind "0.0.1-SNAPSHOT" 2 | :description "Functional Mastermind for Clean Code E54" 3 | :dependencies [[org.clojure/clojure "1.8.0"]] 4 | :profiles {:dev {:dependencies [[midje "1.9.2"]]} 5 | ;; You can add dependencies that apply to `lein midje` below. 6 | ;; An example would be changing the logging destination for test runs. 7 | :midje {}}) 8 | ;; Note that Midje itself is in the `dev` profile to support 9 | ;; running autotest in the repl. 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/mastermind/auto_play.clj: -------------------------------------------------------------------------------- 1 | (ns mastermind.auto-play 2 | (:require 3 | [mastermind.code-breaker :as cb] 4 | [mastermind.code-maker :as cm])) 5 | 6 | (defn random-code [] 7 | (cb/number-to-guess (rand-int (dec (* 6 6 6 6)))) 8 | ) 9 | 10 | (defn auto-play 11 | ([] 12 | (auto-play cb/break-code-seq)) 13 | ([strategy] 14 | (let [code (random-code)] 15 | (loop [n 1 past-scores [] starting-guess nil] 16 | (let [guess (strategy starting-guess past-scores) 17 | score (cm/score code guess)] 18 | (if (= score [4 0]) 19 | n 20 | (recur (inc n) (conj past-scores [guess score]) (cb/inc-guess guess)))))))) 21 | 22 | (def square #(* % %)) 23 | 24 | (defn mean [x] 25 | (/ (reduce + x) (count x))) 26 | 27 | (defn sigma [x] 28 | (let [mn (mean x)] 29 | (Math/sqrt 30 | (/ (reduce #(+ %1 (square (- %2 mn))) 0 x) 31 | (dec (count x)))))) 32 | 33 | (defn analyze-strategy [strategy n] 34 | (let [scores (sort (for [x (repeat n nil)] (auto-play strategy)))] 35 | {:mean (double (mean scores)) 36 | :sigma (sigma scores) 37 | :min (first scores) 38 | :max (last scores) 39 | :median (nth scores (int (/ (count scores) 2))) 40 | :hist (map count (partition-by identity scores))})) 41 | 42 | (defn analyze-strategies [n] 43 | {:seq (analyze-strategy cb/break-code-seq n) 44 | :3x2 (analyze-strategy cb/break-code-3x2 n) 45 | :double-rainbow (analyze-strategy cb/break-code-double-rainbow n)}) -------------------------------------------------------------------------------- /src/mastermind/code_breaker.clj: -------------------------------------------------------------------------------- 1 | (ns mastermind.code-breaker 2 | (:require [mastermind.code-maker :as cm])) 3 | 4 | (defn guess-to-number [guess] 5 | (reduce #(+ (* 6 %1) %2) guess)) 6 | 7 | (defn number-to-guess [n] 8 | [(rem (quot n 216) 6) 9 | (rem (quot n 36) 6) 10 | (rem (quot n 6) 6) 11 | (rem n 6)]) 12 | 13 | (defn inc-guess [guess] 14 | (if (= guess [5 5 5 5]) 15 | :overflow 16 | (->> guess 17 | (guess-to-number) 18 | (inc) 19 | (number-to-guess)))) 20 | 21 | (defn guess-consistent-with-past-scores [guess past-scores] 22 | (every? identity (for [past-score past-scores] 23 | (= (cm/score guess (first past-score)) 24 | (second past-score))))) 25 | 26 | (defn next-guess [starting-guess past-scores] 27 | (loop [guess starting-guess] 28 | (if (= guess :overflow) 29 | :error 30 | (if (guess-consistent-with-past-scores guess past-scores) 31 | guess 32 | (recur (inc-guess guess)))))) 33 | 34 | 35 | (defn break-code-seq 36 | "Sequential Strategy" 37 | [starting-guess past-scores] 38 | (if (nil? starting-guess) 39 | [0 0 0 0] 40 | (next-guess starting-guess past-scores))) 41 | 42 | (defn break-code-3x2 43 | "The 3x2 strategy" 44 | [starting-guess past-scores] 45 | (case (count past-scores) 46 | 0 [0 0 1 1] 47 | 1 [2 2 3 3] 48 | 2 [4 4 5 5] 49 | 3 (next-guess [0 0 0 0] past-scores) 50 | (next-guess starting-guess past-scores))) 51 | 52 | (defn break-code-double-rainbow 53 | "All the way!" 54 | [starting-guess past-scores] 55 | (case (count past-scores) 56 | 0 [0 1 2 3] 57 | 1 [2 3 4 5] 58 | 2 [4 5 0 1] 59 | 3 (next-guess [0 0 0 0] past-scores) 60 | (next-guess starting-guess past-scores))) 61 | -------------------------------------------------------------------------------- /src/mastermind/code_maker.clj: -------------------------------------------------------------------------------- 1 | (ns mastermind.code-maker) 2 | 3 | (defn count-true [bools] 4 | (count (filter identity bools))) 5 | 6 | (defn position-matches [code guess] 7 | (count-true 8 | (map #(= %1 %2) code guess))) 9 | 10 | (defn value-matches [code guess] 11 | (count-true 12 | (map #(contains? (set code) %1) guess))) 13 | 14 | (defn count-of [value values] 15 | (count (filter #(= value %) values))) 16 | 17 | (defn over-count [code guess] 18 | (let [code-values (set code)] 19 | (->> code-values 20 | (map #(- (count-of % guess) (count-of % code))) 21 | (filter pos?) 22 | (reduce +)))) 23 | 24 | (defn score [code guess] 25 | (let [p (position-matches code guess) 26 | v (value-matches code guess) 27 | o (over-count code guess)] 28 | [p (- v p o)]) 29 | ) -------------------------------------------------------------------------------- /test/code_maker_test.clj: -------------------------------------------------------------------------------- 1 | (ns code-maker-test 2 | (:require [midje.sweet :refer :all] 3 | [mastermind.code-maker :refer :all])) 4 | 5 | (facts 6 | "Scoring Position Matches. Matches with the 7 | correct value in the correct position." 8 | (fact 9 | "score guess with no matches" 10 | (score [0 0 0 0] [1 1 1 1]) => [0 0]) 11 | 12 | (fact 13 | "score guess with one position match" 14 | (score [0 0 0 0] [0 1 1 1]) => [1 0]) 15 | 16 | (fact 17 | "guess with two position matches" 18 | (score [0 0 0 0] [0 1 1 0]) => [2 0] 19 | (score [0 0 0 0] [1 0 1 0]) => [2 0] 20 | (score [0 0 0 0] [0 1 0 1]) => [2 0] 21 | ) 22 | 23 | (fact 24 | "guess with many position matches" 25 | (score [1 1 1 1] [0 1 1 1]) => [3 0] 26 | (score [0 0 0 0] [0 0 0 1]) => [3 0] 27 | (score [1 2 3 4] [1 2 3 4]) => [4 0]) 28 | ) 29 | 30 | (facts 31 | "Scoring Value Matches. Matches that have the 32 | right value, but are in the wrong position." 33 | 34 | (fact 35 | "value matches" 36 | (score [1 2 3 4] [2 0 0 0]) => [0 1] 37 | (score [1 2 3 4] [2 3 0 0]) => [0 2] 38 | (score [1 2 3 4] [2 4 1 0]) => [0 3] 39 | (score [1 2 3 4] [4 3 2 1]) => [0 4] 40 | (score [1 2 3 4] [2 3 4 1]) => [0 4] 41 | ) 42 | 43 | ) 44 | 45 | (fact 46 | "Guesses with some position and some value matches" 47 | (score [1 2 3 4] [1 2 4 3]) => [2 2]) 48 | 49 | (fact 50 | "Confound 1. If there are duplicate colours in the guess, 51 | they cannot all be awarded a key peg unless they correspond 52 | to the same number of duplicate colours in the hidden code." 53 | (score [1 2 3 4] [3 3 3 4]) => [2 0] 54 | (score [1 1 2 3] [1 2 1 2]) => [1 2] 55 | (score [3 3 3 3] [3 1 1 1]) => [1 0] 56 | ) 57 | -------------------------------------------------------------------------------- /test/mastermind/auto_play_test.clj: -------------------------------------------------------------------------------- 1 | (ns mastermind.auto-play-test 2 | (:require [midje.sweet :refer :all] 3 | [mastermind.auto-play :refer :all] 4 | [mastermind.code-breaker :as cb] 5 | [mastermind.code-maker :as cm])) 6 | 7 | 8 | (facts 9 | "about auto-play" 10 | (fact 11 | "Spy. If the initial guess is correct, return 1" 12 | (auto-play) => 1 13 | (provided 14 | (random-code) => [0 0 0 0]) 15 | (provided 16 | (cm/score [0 0 0 0] [0 0 0 0]) => [4 0]) 17 | (provided 18 | (cb/break-code-seq nil []) => [0 0 0 0])) 19 | 20 | (fact 21 | "If code is [0 0 0 1] should take two tries." 22 | (auto-play) => 2 23 | (provided (random-code) => [0 0 0 1])) 24 | 25 | (fact 26 | "If code is [0 0 1 0] should take 3 tries." 27 | (auto-play) => 3 28 | (provided (random-code) => [0 0 1 0])) 29 | ) 30 | -------------------------------------------------------------------------------- /test/mastermind/code_breaker_test.clj: -------------------------------------------------------------------------------- 1 | (ns mastermind.code-breaker-test 2 | (:require [midje.sweet :refer :all] 3 | [mastermind.code-breaker :refer :all] 4 | [mastermind.code-maker :as cm])) 5 | 6 | (facts 7 | "Code Breaker" 8 | (fact 9 | "guess-to-number" 10 | (guess-to-number [0 0 0 0]) => 0 11 | (guess-to-number [0 0 0 1]) => 1 12 | (guess-to-number [0 0 1 0]) => 6 13 | (guess-to-number [0 0 1 1]) => 7 14 | (guess-to-number [0 1 1 1]) => 43 15 | (guess-to-number [1 1 1 1]) => 259 16 | (guess-to-number [5 5 5 5]) => (dec (* 6 6 6 6))) 17 | 18 | (fact 19 | "number-to-guess" 20 | (number-to-guess 0) => [0 0 0 0] 21 | (number-to-guess 1) => [0 0 0 1] 22 | (number-to-guess 6) => [0 0 1 0] 23 | (number-to-guess 7) => [0 0 1 1] 24 | (number-to-guess 43) => [0 1 1 1] 25 | (number-to-guess 259) => [1 1 1 1] 26 | (number-to-guess (dec (* 6 6 6 6))) => [5 5 5 5] 27 | ) 28 | 29 | (fact 30 | "increment guess" 31 | (inc-guess [0 0 0 0]) => [0 0 0 1] 32 | (inc-guess [0 0 0 5]) => [0 0 1 0] 33 | (inc-guess [0 0 5 5]) => [0 1 0 0] 34 | (inc-guess [0 5 5 5]) => [1 0 0 0] 35 | (inc-guess [5 5 5 5]) => :overflow 36 | ) 37 | 38 | (fact ;This was an experiment to make sure that past guesses cannot be repeated. 39 | "next-guess will not return a previous guess" 40 | (next-guess [0 0 0 0] 41 | [[[0 0 0 1] [3 1]]]) =not=> [0 0 0 1] 42 | ) 43 | 44 | (fact 45 | "initial guess" 46 | (break-code-seq nil []) => [0 0 0 0]) 47 | 48 | (fact 49 | "first step for code [1 2 3 4]" 50 | (break-code-seq 51 | [0 0 0 0] 52 | [[[0 0 0 0] [0 0]]]) => [1 1 1 1] 53 | ) 54 | 55 | (fact 56 | "first step for code [0 0 0 1]" 57 | (break-code-seq 58 | [0 0 0 0] 59 | [[[0 0 0 0] [3 0]]]) => [0 0 0 1] 60 | ) 61 | 62 | (fact 63 | "second step for code [0 0 1 0]" 64 | (break-code-seq 65 | [0 0 0 1] 66 | [[[0 0 0 1] [2 2]]]) => [0 0 1 0] 67 | ) 68 | 69 | (fact 70 | "Two steps are required for [0 0 1 0]" 71 | (break-code-seq 72 | [0 0 0 0] 73 | [[[0 0 0 0] [3 0]] 74 | [[0 0 0 1] [2 2]]]) => [0 0 1 0] 75 | ) 76 | ) 77 | 78 | (facts 79 | "3x2 strategy" 80 | (fact 81 | "first step" 82 | (break-code-3x2 nil []) => [0 0 1 1]) 83 | (fact 84 | "second step" 85 | (break-code-3x2 [0 0 1 1] 86 | [[[0 0 1 1] [0 0]]]) => [2 2 3 3]) 87 | (fact 88 | "third step" 89 | (break-code-3x2 [2 2 3 3] 90 | [[[0 0 1 1] [0 0]] 91 | [[2 2 3 3] [0 0]]]) => [4 4 5 5]) 92 | (fact 93 | "Fourth step falls back to sequential decoding" 94 | (break-code-3x2 [4 4 5 5] 95 | [[[0 0 1 1] [0 0]] 96 | [[2 2 3 3] [0 0]] 97 | [[4 4 5 5] [0 4]]]) => [5 5 4 4]) 98 | (fact 99 | "fifth step carries on with sequential decoding" 100 | (break-code-3x2 [4 4 5 5] 101 | [[[0 0 1 1] [0 0]] 102 | [[2 2 3 3] [0 0]] 103 | [[4 4 5 5] [2 2]] 104 | [[4 5 4 5] [0 4]]]) => [5 4 5 4]) 105 | ) --------------------------------------------------------------------------------