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)
Jamal
35.2k13 gold badges134 silver badges238 bronze badges
asked Sep 23, 2015 at 7:39
\$\endgroup\$
2
  • 3
    \$\begingroup\$ There's no need to use do under let, let has implicit do. \$\endgroup\$ 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\$ 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?

answered Nov 3, 2015 at 23:09
\$\endgroup\$

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

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

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.