Solver of Squares
Playing Games with Clojure
@tessellator
Slide 2
Slide 2 text
@tessellator
@tessellator
http://gameaboutsquares.com
Game About Squares
Slide 3
Slide 3 text
@tessellator
A* Search
Slide 4
Slide 4 text
@tessellator
A* Search
• A tree-search algorithm
• Uses a heuristic to guide search
• Can guarantee shortest-path if the
heuristic meets certain requirements
Slide 5
Slide 5 text
@tessellator
Cost is tree-depth of current state
cost = 0
cost = 1
cost = 2
Slide 6
Slide 6 text
@tessellator
Estimate is depth delta from current state to goal
current
goal
estimate = 2
Slide 7
Slide 7 text
@tessellator
Next node: minimize (cost + estimate)
Slide 8
Slide 8 text
@tessellator
;; (in-ns ‘solver-of-squares.search)
(defn a*
([initial-state succ goal? cost? estimate? max-cost]
(letfn [(wrap-costs [state]
(let [c (cost? state), e (estimate? state)]
(vary-meta state assoc :cost c :estimate e :total (+ c e))))
(apply-conj! [A xs]
(if (empty? xs)
A
(recur (h/insert! (comp :total meta) A (first xs))
(rest xs))))]
(let [cost (comp :cost meta)
total (comp :total meta)]
((fn [queue seen]
(if-let [state (nth queue 0)]
(cond
(goal? state) state
(> (cost state) max-cost) nil
(contains? seen state) (recur (h/pop! total queue) seen)
:else (recur (->> (succ state)
(map wrap-costs)
(apply-conj! (h/pop! total queue)))
(conj seen state)))))
(transient [(wrap-costs initial-state)])
#{})))))
Slide 9
Slide 9 text
@tessellator
;; (in-ns ‘solver-of-squares.search)
(defn a*
([initial-state succ goal? cost? estimate? max-cost]
(letfn [(wrap-costs [state]
(let [c (cost? state), e (estimate? state)]
(vary-meta state assoc :cost c :estimate e :total (+ c e))))
(apply-conj! [A xs]
(if (empty? xs)
A
(recur (h/insert! (comp :total meta) A (first xs))
(rest xs))))]
(let [cost (comp :cost meta)
total (comp :total meta)]
((fn [queue seen]
(if-let [state (nth queue 0)]
(cond
(goal? state) state
(> (cost state) max-cost) nil
(contains? seen state) (recur (h/pop! total queue) seen)
:else (recur (->> (succ state)
(map wrap-costs)
(apply-conj! (h/pop! total queue)))
(conj seen state)))))
(transient [(wrap-costs initial-state)])
#{})))))
Slide 10
Slide 10 text
@tessellator
(fn [queue seen]
(if-let [state (nth queue 0)]
(cond
(goal? state) state
(> (cost state) max-cost) nil
(contains? seen state) (recur (h/pop! total queue) seen)
:else (recur (->> (succ state)
(map wrap-costs)
(apply-conj! (h/pop! total queue)))
(conj seen state)))))
@tessellator
A good start, but not admissible
because it sometimes over-estimates
Slide 17
Slide 17 text
@tessellator
A good start, but not admissible
because it sometimes over-estimates
…so we are not guaranteed an optimal
(shortest-path) solution
Slide 18
Slide 18 text
@tessellator
Slide 19
Slide 19 text
@tessellator
Manhattan Distance: 6
Slide 20
Slide 20 text
@tessellator
Manhattan Distance: 6
Actual Shortest Distance: 5
Slide 21
Slide 21 text
@tessellator
Ideas for a better heuristic?
Let’s talk!
Slide 22
Slide 22 text
@tessellator
But…what about performance?
Slide 23
Slide 23 text
@tessellator
Or…How is a priority queue efficient
with persistent data structures?
Slide 24
Slide 24 text
@tessellator
“If a tree falls in the woods, does it make a sound?
If a pure function mutates some local data in order
to produce an immutable return value, is that ok?”
- Rich Hickey
clojure.org/transients
Slide 25
Slide 25 text
@tessellator
Transients allow to manipulate a data structure in-place
Slide 26
Slide 26 text
@tessellator
Transients allow to manipulate a data structure in-place
Okay under two conditions:
1. You control the “variable” absolutely while transient
2. It is made persistent before made generally available
Slide 27
Slide 27 text
@tessellator
Simple Transient Demo
Slide 28
Slide 28 text
@tessellator
;; (in-ns ‘solver-of-squares.heap)
(defn- sift-up! [f A i]
(let [idx (dec i) pidx (dec (int (/ i 2)))]
(if (zero? idx)
A
(if (< (f (nth A idx)) (f (nth A pidx)))
(recur f (swap! A idx pidx) (inc pidx))
A))))
(defn insert! [f A v]
(let [c (count A)]
(sift-up! f (conj! A v) (inc c))))