15
\$\begingroup\$

In order to exercise and learn Clojure, I decided to write a simple Minesweeper game from scratch. I'd consider myself as a Clojure novice and would be thankful if somebody could do a review or give me feedback on the code.

The full repository can be found here but I'd be also happy if somebody could have a look on the core functionalities at least:

board.clj

(ns minesweeper.board
  (:use [clojure.pprint]))


(defn empty-board
  "Create a rectangular empty board of
  the specified with and height"
  [w h]
  (vec (repeat w (vec (repeat h {})))))


(defn to-coords
  "Transform the board cells into coordinates"
  ([board]
   (to-coords board (constantly true)))
  ([board pred]
   (let [w (count board)
         h (count (first board))]
     (for [x (range w) y (range h) :when (pred (get-in board [x y]))]
       [x y]))))


(defn neighbour-cells
  "Locate neighbour cells based on coordinates [x y],
  respecting board width and height"
  [board [x y]]
  (let [w (count board)
        h (count (first board))]
    (for [dx (map (partial + x) [-1 0 1])
          dy (map (partial + y) [-1 0 1])
          :when (and (or (not= x dx) (not= y dy))
                     (> w dx -1)
                     (> h dy -1))]
      [dx dy])))


(defn warnings-freq [board]
  "Count the number of nearby mines"
  (let [mines (to-coords board :mine)
        warnings (mapcat (partial neighbour-cells board) mines)]
    (frequencies
     (remove (set mines) warnings))))


(defn random-mines
  [board start-pos]
  (-> (set (to-coords board))
      (disj start-pos)
      (shuffle)))


(defn place-mines
  "Place n mines randomly on the board"
  [board mine-count start-pos]
  (let [mines (take mine-count
                    (random-mines board start-pos))]
    (reduce
     (fn [m k]
       (assoc-in m k {:mine true}))
     board
     mines)))


(defn place-warnings
  "Place warnings on a mines' neighbour cells"
  [board]
  (let [mine-counts (warnings-freq board)]
    (reduce-kv
     (fn [m k v]
       (assoc-in m k {:warn v}))
     board
     mine-counts)))


(defn explore-field
  "Explore single field on the board"
  [board coords]
  (update-in board coords conj {:explored true}))


(defn handle-flag
  "Handles set and remove of a flag"
  [board coords]
  (update-in board coords
             #(assoc % :flag (not (:flag %)))))


(defn game-started?
  "At least one field explored?"
  [board]
  (pos? (count (to-coords board :explored))))


(defn game-lost?
  "Any mine exploded?"
  [board]
  (letfn [(pred [m] (and (:mine m) (:explored m)))]
    (pos? (count (to-coords board pred)))))


(defn game-won?
  "All fields cleared?"
  [board]
  (letfn [(pred [m] (or (:mine m) (:explored m)))]
    (= (to-coords board pred)
       (to-coords board))))

game.clj

(ns minesweeper.game
  (:require [minesweeper.board :as board]
            [minesweeper.dispatch :as disp]))


(def levels { :beginner     { :rows 8,  :cols 8,  :mines 10 }
              :intermediate { :rows 16, :cols 16, :mines 40 }
              :expert       { :rows 30, :cols 16, :mines 99 }})

(def ^:private level (atom {}))
(def ^:private board (atom []))


(defn- new-game
  [data]
  (let [new-level (:level data)]
    (do
      (reset! level new-level)
      (reset! board (board/empty-board
                     (:rows new-level)
                     (:cols new-level)))
      (disp/fire :game-initialized data))))


(defn- start-game
  [board mine-count start-pos]
  (-> board
      (board/place-mines mine-count start-pos)
      (board/place-warnings)))


(defn- explore
  [board data]
  (let [mine-count (:mines (:level data))
        position   (vector (:row data) (:col data))]
    (if (not (board/game-started? board))
      (-> board
          (start-game mine-count position)
          (board/explore-field position))
      (board/explore-field board position))))


(defn- explore-field
  [data]
  (let [board (swap! board explore data)
        attrs (get-in board (vector (:row data) (:col data)))
        data  (assoc data :attrs attrs)]
    (cond
     (board/game-won? board)   (disp/fire :game-won data)
     (board/game-lost? board)  (disp/fire :game-lost (assoc data :board board))
     :else                     (disp/fire :uncover-field data))))


(defn- handle-flag
  [data]
  (let [position (vector (:row data) (:col data))]
    (do
      (swap! board (partial board/handle-flag) position)
      (disp/fire :uncover-field data))))


(disp/register :explore-field #'explore-field)
(disp/register :handle-flag #'handle-flag)
(disp/register :new-game #'new-game)
\$\endgroup\$
2
  • 3
    \$\begingroup\$ There's no need to use do under let, let has implicit do. \$\endgroup\$
    – user85203
    Commented Sep 23, 2015 at 15:16
  • 1
    \$\begingroup\$ Since it appears a few times, I might define something like (def position (juxt :row :col)) so you can just say (position data) instead of (vector (:row data) (:col data)). \$\endgroup\$
    – BenC
    Commented Dec 18, 2015 at 7:10

1 Answer 1

2
\$\begingroup\$

This is excellent. You have nice small functions, intent is clear, and the docstrings are helpful.

These are very minor suggestions:

(update-in board coords
         #(assoc % :flag (not (:flag %)))

Might be slightly more obvious as

(update-in board (conj coords :flag) not))

or

(update-in board coords update :flag not)

update is 1.7 only, but you could also use update-in [:flag]

I think this pred would be better promoted to a defn, and perhaps called something like boom

(letfn [(pred [m] (and (:mine m) (:explored m)))]

(especially since it is repeated 2x)

(disp/fire) might just be a multimethod?

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.