Last time, I presented a solution for Always Turn Left, a Google Code Jam problem. Given that their large dataset was quite big (up to 10k moves), I thought: “It would be interesting to see what mazes those moves produce”. So I set to write (in Clojure, of course) a maze-display app (using Seesaw, of course). Here’s what came out of that.
(ns com.icyrock.clojure.codejam.maze-display (:use clojure.java.io flatland.ordered.map seesaw.border seesaw.chooser seesaw.color seesaw.core seesaw.graphics seesaw.mig) (:require [seesaw.bind :as ssb]))
First, declare a lot of things I’m to use later. Most Seesaw and one thing from here, which is a Clojure implementation of ordered sets / maps which I wanted to try out.
(def state {:frame (atom nil) :file (atom nil) :cases (atom nil) :curr-case (atom nil) :maze (atom nil)})
Main state – contains:
- Main frame
- Currently selected case-file
- Loaded cases themselves
- Currently selected case
- Maze bound to the currently selected case
(def room-width 16) (def room-height 16)
Default room size when drawn, in pixels.
(def default-style (style :foreground "#000000" :stroke (stroke :width 3 :cap :round)))
Default style to use when drawing walls. It’s a black, 3-pixel wide line, with rounded edges.
(defn draw-wall [g w h wall] (case wall :n (draw g (line 0 0 w 0) default-style) :s (draw g (line 0 h w h) default-style) :w (draw g (line 0 0 0 h) default-style) :e (draw g (line w 0 w h) default-style)))
This draws a wall. Given that translation is used below, the north-west corner of the room is always at (0, 0)
, so the above is easy to understand given the case keys (:n
for north, :s
for south, :w
for west and :e
for east).
(defn draw-room [g w h walls-desc] (let [walls (case walls-desc \1 #{ :s :w :e} \2 #{:n :w :e} \3 #{ :w :e} \4 #{:n :s :e} \5 #{ :s :e} \6 #{:n :e} \7 #{ :e} \8 #{:n :s :w } \9 #{ :s :w } \a #{:n :w } \b #{ :w } \c #{:n :s } \d #{ :s } \e #{:n } \f #{ } )] (doseq [wall walls] (push g (draw-wall g w h wall)))))
The room is a set of cases to decipher the letter as set of walls for that room, as given in the problem description and then draw each of these walls.
(defn paint-maze (try (let [w room-width h room-height maze @(state :maze)] (when maze (anti-alias g) (translate g w h) (doseq [row maze] (push g (doseq [room row] (draw-room g w h room) (translate g w 0))) (translate g 0 h)))) (catch Exception e (invoke-later (alert e)) (println e))))
Main paint function:
- Check if maze is valid (i.e. user has selected a case)
- Turn on anit-aliasing
- Go through the rows of the maze
- Translate to the position of the current room
- Draw it
(defn content-panel [] (mig-panel :constraints ["fill" "[|grow]"] :items [[(button :id :load :text "Load file...") ""] [(text :id :file-name) "growx, wrap"] [(scrollable (listbox :id :cases) :border (line-border)) "grow"] [(let [s (scrollable (canvas :id :maze-pict :background "#ffffff" :paint paint-maze) :border (line-border))] (-> s (.getHorizontalScrollBar) (.setUnitIncrement (* 3 room-width))) (-> s (.getVerticalScrollBar) (.setUnitIncrement (* 3 room-height))) s) "grow, push"]]))
Main window contents:
- “Load” button
- Current file name
- List box for cases
- Canvas for the maze
Uses MigLayout, of course.
(defn split-cases [acc line] (let [case (re-find #"^Case #\d+:$" line)] (if case ;; Found case start line (assoc acc :curr-case case :cases (assoc (acc :cases) case [])) ;; Continuation of the current case (maze definition) (let [cases (acc :cases) curr-case (acc :curr-case) curr-maze (cases curr-case) new-maze (conj curr-maze line) new-cases (assoc cases curr-case new-maze)] (assoc acc :cases new-cases)))))
When loading, split the cases one by one, taking into account maze description has two kinds of lines:
- Case start
- Maze lines for the current case
(defn load-cases [file] (with-open [r (reader file)] (let [lines (reduce conj [] (line-seq r)) {:keys [cases]} (reduce split-cases {:cases (ordered-map)} lines)] (reset! (state :cases) cases))))
Case loader function:
- Use reader to read from the file
- Get the lines
- Reduce using previous
split-cases
function
(defn load [e] (let [frame (to-frame e)] (choose-file frame :type :open :success-fn (fn [fc file] (reset! (state :file) file)))))
Just shows the standard Java file chooser to pick the file.
(defn set-listeners [frame] (listen (select frame [:#load]) :action load)) (defn set-bindings [frame] ;; File binding (ssb/bind (state :file) (ssb/tee (ssb/b-do* load-cases) (ssb/bind (ssb/transform #(.getPath %)) (select frame [:#file-name])))) ;; Cases binding (ssb/bind (state :cases) (ssb/transform #(keys %)) (ssb/tee (ssb/property (select frame [:#cases]) :model) (ssb/b-do* (fn [v] (selection! (select frame [:#cases]) (first v)))))) ;; Case selection binding (ssb/bind (ssb/selection (select frame [:#cases])) (ssb/b-do* #(reset! (state :curr-case) %))) ;; Selected case binding (ssb/bind (state :curr-case) (ssb/transform #(@(state :cases) %)) (ssb/b-do* #(reset! (state :maze) %))) ;; Maze binding (ssb/bind (state :maze) (ssb/b-do* (fn [maze] (let [canvas (select frame [:#maze-pict]) cw (* room-width (+ 2 (count (first maze)))) ch (* room-height (+ 2 (count maze)))] (config! canvas :preferred-size [cw :by ch]) (.revalidate canvas) (repaint! canvas))))))
These two set up the listeners (only one in this case – button click) and bindings which nicely describe the state machine for this simple app:
- When file is selected, load the cases and display the file name
- When cases were loaded, populate the list box with the case map description
- When a case is selected, update the current case
- When the current case changes, update the maze
- When the maze is updated, draw it
(defn maze-display [] (native!) (let [f (frame :title *ns* :width 1200 :height 700 :on-close :dispose :visible? true :content (content-panel))] (.setLocation f (java.awt.Point. 100 100)) (reset! (state :frame) f) (set-listeners f) (set-bindings f)))
Main function:
- Make the frame
- Set its location
- Set the listeners and bindings
The final result looks like this: