Problem
Disclaimer: This is my first clojure function & I'm still busy finishing the last chapters of "Programming Clojure". Thanks! :)
I am writing a function to randomly flood-fill parts of a game's map (i.e. this is not a traditional flood-fill that can recursively fill all available space). The idea is to create a random-shaped island, and inside that island create a random-shaped mountain. (i.e. pass the set of island points as a filter to the next flood fill function, so the mountain will be created only within the island's tiles).
This is what my code currently produces (You'll notice the window / drawing code stolen from the snake game in PragProg's Programming Clojure. ;))
Clojure Style
This is my code:
(def dirs2 '([-1 0] [1 0] [0 -1] [0 1]))
(defn neighbours [pt]
(for [dir dirs2]
(vec (map + pt dir))))
(defn flood-fill2
"start -> point to start filling from
n -> number of tiles to create
filter-fn -> a fn that must return true for new points"
([start n]
(flood-fill2 start n (fn [& _] true)))
([start n filter-fn]
(loop [result #{}
candidates #{start}
i n]
(if (or (zero? i) (empty? candidates))
result
(let [current (rand-nth (seq candidates))]
(if (and (filter-fn current) (not (result current)))
(recur
(conj result current)
(apply conj (disj candidates current) (neighbours current))
(dec i))
(recur result (disj candidates current) i)))))))
Is this idiomatic clojure code? The loop construct in there feels like standard iterative code I learned to write in Java / C++. Do you see a shorter way to write this?
Could I implement this as a lazy-seq somehow?
(take 600 (lazy-fill [25 25] optional-src-set))
looks much cooler than(flood-fill2 [25 25] 600 island)
.I tried to write an anonymous function that returns true for any argument(s) using this notation
#()
but had to eventually give up and wrote it usingfn
:(fn [& _] true)
. Could I have used#()
somehow?
Performance
My intial (naive) implementation looked like this:
(defn flood-fill [filled n]
(if (zero? n)
filled
(let [start-point (rand-nth filled)
next-point (rand-nth (neighbours start-point))]
(if (contains? (set filled) next-point)
(recur filled n)
(recur (cons next-point filled) (dec n))))))
But:
user=> (time (flood-fill [[25 25]] 1250))
(time (flood-fill [[25 25]] 1250))
"Elapsed time: 8430.56 msecs"
([8 18] [43 40] [23 9] [38 40] ...
My flood-fill2
does much better:
user=> (time (flood-fill2 [25 25] 1250))
(time (flood-fill2 [25 25] 1250))
"Elapsed time: 257.361 msecs"
#{[34 33] [35 34] [36 3] [36 35] [38 5] [10 9] ...
But using (time (flood-fill2 [500 500] 500000))
to fill 50% of a 1000 * 1000 tiles map takes forever (@ 100% CPU). I ended up killing that REPL.
Any performance suggestions? I tried to avoid many conversions from Sets to Vectors/Lists, but rand-nth
needs a sequence and I couldn't find a nice "random set element" function.
Thanks for reading all of this.