Slide 1

Slide 1 text

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)))))

Slide 11

Slide 11 text

@tessellator So now we need a heuristic…

Slide 12

Slide 12 text

@tessellator Manhattan Distance

Slide 13

Slide 13 text

@tessellator 1 2 3 4 Manhattan Distance

Slide 14

Slide 14 text

@tessellator 1 2 3 4 Manhattan Distance distance = 4

Slide 15

Slide 15 text

@tessellator ;; (in-ns ‘solver-of-squares.search) (defn manhattan-distance [current target] (->> (map (comp abs -) target current) (reduce +))) ;; (in-ns ‘solver-of-squares.core) (defn estimate-remaining-cost [goal state] (reduce + (map #(manhattan-distance (goal %) (-> state % :position)) (keys goal))))

Slide 16

Slide 16 text

@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))))

Slide 29

Slide 29 text

@tessellator ;; (in-ns ‘solver-of-squares.levels) (def level11 {:goal {:red [1 2] :blue [2 2] :navy [3 2]} :blocks {:red {:position [0 4] :direction :right} :navy {:position [2 0] :direction :up} :blue {:position [4 4] :direction :left}} :arrows {[2 4] :down}}) Defining a level:

Slide 30

Slide 30 text

@tessellator Enough talk…let’s see it work!

Slide 31

Slide 31 text

@tessellator Questions? https://github.com/tessellator/solver-of-squares