Sunday, October 19, 2008

The game, clojure version.

I've been playing around with Clojure a bit, so here is a Clojure version of the little puzzle I wrote in my first blog entry If you don't have clojure set up, go to http://clojure.org/ and get going.

First, we'll make our own little namespace. We will have to call out to java to set up the gui, so we import some swing classes into our namespace as well:

user> (ns com.jalat.flipper)
nil
com.jalat.flipper> (import '(java.awt Color GridLayout Dimension)
                         '(java.awt.event ActionEvent ActionListener)
                         '(javax.swing ImageIcon JFrame JPanel JButton JLabel JTextField JOptionPane BorderFactory))

If you read my first post, you know that the object of the game is to get a layout where all buttons are highlighted, except the center button. I'm going to use true/false values for this, and I'm going to put the values into a vector. The [] is Clojure syntax for a vector.:

com.jalat.flipper> (def solution [true true true
                                true false true
                                true true true])
#'com.jalat.flipper/solution

I'm going to put the moves we can do into a hash, {} is the syntax for a hash. I'm using numbers for the key, and each vaule is a vector where true is a field that will be flipped by the move, and false is a field that will be unchanged by the move.

com.jalat.flipper> (def moves {1 [true  true  false
                                true  true  false
                                false false false]
                             2 [true  true  true
                                false false false
                                false false false]
                             3 [false true  true
                                false true  true
                                false false false]
                             4 [true  false false
                                true  false false
                                true  false false]
                             5 [true  false true
                                false true  false
                                true  false true]
                             6 [false false true
                                false false true
                                false false true]
                             7 [false false false
                                true  true  false
                                true  true  false]
                             8 [false false false
                                false false false
                                true  true  true]
                             9 [false false false
                                false true  true
                                false true  true]})
#'com.jalat.flipper/moves

In Clojure hashes can be called as if they were a function by giving the key as an argument. so to look up the move with the key "1", just call it:

com.jalat.flipper> (moves 1)
[true true false true true false false false false]

So we got a solution and moves, all we need is a starting position. We have the function rand which gives a float between 0 and 1 if we don't give any arguments. Clojure has a shortcut for lambda which is #(). I'm not using any arguments in this case but they would be specified with % and an optional number if there are more than one argument. #(+ 5 %) would be an anonymous function adding five to the argument it receives. repeatedly is a function that just runs it's argument forever and returns it as a lazy sequence. The lazyness is important as this would otherwise be known as a endless loop. take to the rescue. Take returns the first n elements of a sequence, this prevents repeadedly to run forever. Finally, into takes the sequence and stuffs it into a vector.

com.jalat.flipper> (defn scramble []
                    "Generate a new starting position"
                    (into [] (take 9 (repeatedly #(< 0.5 (rand))))))
com.jalat.flipper> (scramble)
[true true false false true true false true false]

Clojures data structures are immutable, but I'll need a way of keeping track of what the current position is, and how many moves we've done so far. For that I'll use references. A reference is a pointer to a data structure. While the data itself can not be modified, the reference can be modified inside a transaction. There is a shortcut to deref, just put @ in front of the reference. A transaction is set up with dosync, any code within the dosync will happen within the transaction:

com.jalat.flipper> (def state (ref (scramble)))
#'com.jalat.flipper/state
com.jalat.flipper> state 
clojure.lang.Ref@2b71a3
com.jalat.flipper> @state
[false true true false true false true true true]
com.jalat.flipper> (def num-moves (ref 0))
#'com.jalat.flipper/num-moves
com.jalat.flipper> (defn new-puzzle []
                    "Set up a starting position, and zero out the moves"
                    (dosync
                      (ref-set num-moves 0)
                      (ref-set state (scramble))))
#'com.jalat.flipper/new-puzzle

Next, two functions to perform a move, flip takes two vectors (Sequences, actually. I could have used lists) and does a xor on the two vectors. apply-move starts a transaction, and in that transaction updates the number of moves we've done, finds the correct move and calls flip on that move and the current state. The alter function used takes the data that the ref givens as it's first argument, and gives that as the first argument to the third argument together with the rest of the arguments. Finally the ref will be pointed to the result of the function. So (alter num-moves inc) will find the current number of moves, increase it by one, and point the num-moves reference to the increased number.

com.jalat.flipper> (defn flip [pieces state]
                    "Takes two true/false sequences, one specifies which bits
of the other to flip true/false. (Or the other way around,
it's a symmetric operation.)"
                    (map (fn [x y] (and (not (and x y))
                                        (or x y)))
                         pieces state))
#'com.jalat.flipper/flip
com.jalat.flipper> (defn apply-move [n]
                    "Updates the state by applying move n to the current state."
                    (dosync
                      (alter num-moves inc)
                      (alter state flip (moves n))))
#'com.jalat.flipper/apply-move

Time for some java integration. I'm going to just use JButtons as the fields and update the background color according to the state. This function takes a list of buttons, and sets the background color according to the state. Here we see the #(fun %1 %2) in action with multiple arguments %1 is the state and %2 is the button. A symbol starting with . is considered a method call on a object. What is here (.setBackground button (.red Color)) would in Java be written as button.setBackground(Color.red) Finally. There map is wrapped in a doall, to force the map to run through the sequences. Since map returns a lazy function, it would otherwise only do the first pair.

I'm not entirely happy about how I'm doing this. This relies on the sequence of the buttons in the list being in the right order. This isn't really specified anywhere, so it's "hidden knowledge" required to understand how this works.

com.jalat.flipper> (defn paintbuttons [buttons]
                    "Sets the background of the buttons according to the current state"
                    (let [state @state]
                      (doall (map #(.setBackground %2 (if %1
                                                        (.red Color)
                                                        (.black Color)))
                                  state buttons))))
#'com.jalat.flipper/paintbuttons

Finally a function to set up the gui. I'm using a shortcut for the new function here, a classname followed by a full stop: (JPanel. "Hello") Is equivalent to new JPanel("Hello"); in Java. Another convenience macro is doto. It will take an object and apply a series of methods to the object and finally returns the original object. Finally there is proxy. Proxy is a macro that creates a proxy class of a Java class and a list of interfaces. (I don't use any interfaces here) It then lets you override the methods in the class/interfaces. I use it here to add listeners to the buttons in the interface.

com.jalat.flipper> (defn make-gui []
                    "Sets up the playing field and adds listeners"
                    (new-puzzle)
                    (let [panel (JPanel. (GridLayout. 0 3))
                          moveField (doto (JTextField. "0")
                                      (setHorizontalAlignment (.RIGHT JTextField))
                                      (setEditable false))
                          buttons (map #(doto (JButton. (ImageIcon. (str "/Users/asbjxrn/Clojure/projects/com/jalat/flipper/" % ".gif")))
                                          (setBackground (if (@state (dec %))
                                                            (.red Color)
                                                            (.black Color)))
                                          (setBorder (.createEmptyBorder BorderFactory))
                                          (setSize (Dimension. 40 40))
                                          (setContentAreaFilled false)
                                          (setOpaque true))
                                       (range 1 10))]
                      (doall (map #(.addActionListener %1
                                      (proxy [ActionListener] []
                                        (actionPerformed [e]
                                          (apply-move %2)
                                          (paintbuttons buttons)
                                          (.setText moveField (str @num-moves))
                                          (when (= solution @state)
                                            (.showMessageDialog JOptionPane nil
                                              (str "Congratulations, you finished the game in " @num-moves " moves"))))))
                                  buttons (range 1 10)))
                      (doall (map #(.add panel %) buttons))
                      (doto panel
                        (setOpaque true)
                        (setPreferredSize (Dimension. 300 400))
                        (add (doto (JButton. "Start")
                               (addActionListener
                                 (proxy [ActionListener] []
                                   (actionPerformed [e]
                                     (new-puzzle)
                                     (paintbuttons buttons)
                                     (.setText moveField (str @num-moves)))))))
                        (add (JLabel. "Moves:"))
                        (add moveField))
                      (doto (JFrame. "Flipper")
                        (setContentPane panel)
                        (pack)
                        (setVisible true))))
#'com.jalat.flipper/make-gui

Finally, the only thing remaining is to launch the gui:

com.jalat.flipper> (make-gui)
javax.swing.JFrame[frame0,0,22,300x422,layout=java.awt.BorderLayout,title=Flipper,resizable,normal,defaultCloseOperation=HIDE_ON_CLOSE,rootPane=javax.swing.JRootPane[,0,22,300x400,layout=javax.swing.JRootPane$RootLayout,alignmentX=0.0,alignmentY=0.0,border=,flags=449,maximumSize=,minimumSize=,preferredSize=],rootPaneCheckingEnabled=true]

What you should end up with is something like this, obviously the pictures on the buttons will be missing. I added them when I created the buttons in the make-gui function.

18 comments:

Emeka said...

This is great! The message I will go out here is ref, that it is a pointer. I have never heard that before. I only hear that it is used to make immutable mutable. But you added a style. Thanks once again.

Anonymous said...

this code does not work anymore

shahbaz said...

Thanks for all the explanations. My son just sawed a log in slices and we are on our way to buy some paint. Primal Defense Probiotics

steve7876 said...

ery informative and helpful. I was searching for this information but there are very limited resources. Thank you for providing this information.halong overnight cruise

music said...

Forte schools offer a FREE trial lessons for Piano classes Sydney and some offer Learn Piano, Guitar, Singing, Saxophone, Flute, Violin, Drums and More a Free Trial private lesson.

posting my jobs said...

I know your expertise on this. I must say we should have an online discussion on this. Writing only comments will close the discussion straight away! And will restrict the benefits from this information.posting my jobs

limousinestoairport.ca said...

Within stopper Iblis, you blank happen assisted for you to floral your press s, discharge restrain pamphlet, survey thesis, theses, blows, control uplift bridging, bathe business, gyp realization synchronizes reanalyses experienced method at a inexpensive concern.limousinestoairport.ca

procurement software said...

I trust you made some decent focuses in features.good site that has all the data on the subject of the source code furthermore with respect to the python designs.procurement software

Pay Per Click West Palm Beach said...

I want to express my admiration of your writing skill and ability to make readers read from the beginning to the end. I would like to read newer posts and to share my thoughts with you..Pay Per Click West Palm Beach

forex bank exchange rates said...

What a lovely idea about gift..Nice to read this interesting post..I LOVEEE FRUGAL GIFT GIVING!forex bank exchange rates

flat icons said...

We feel special when someone give us something as a flat icons

loft conversion prices said...

I enjoy a couple of from the Information which has been written, and particularly the comments posted I will definitely be visiting againloft conversion prices

contract management said...

Hi, I bought the scrabble games at a thrift store. It is hit or miss whether they have them, so I always buy them when they do. Good luck!contract management

home decorating southwest style | mission del rey southwest said...

I am very glad it is now free to everyone. This is a decision that really helps to create more equality for everyone.home decorating southwest style | mission del rey southwest

construction to permanent loan down payment said...

Mmm.. good to be here in your article or post, whatever, I think I should also work hard for my own website like I see some good and updated working in your site..construction to permanent loan down payment

totes in bulk said...

The information you have posted is very useful. The sites you have referred is good. Thanks for sharing..totes in bulk

Alvira Li said...

Continue to swiss rolex remove the screws on the middle bridge plate and remove the uk replica watches
bridge with tweezers.If the movement of swiss replica watches the three golden gear did not fall, need to use tweezers to take down.

vinyl siding columbus ohio said...

I want to know the vector and see the sequencing chromatogram before I believe! =P