Sunday, April 11, 2010

Sliding Block Puzzle in F# and WPF

I realized that I wanted something more visual with which to test my search algorithms. I have also been wanting to learn how to write WPF applications using F#. To that end, I offer the following F#/WPF application of the famous Fifteen Puzzle, the ancestor of all sliding-block puzzles.

I did some early experiments with XAML, but what I needed was more repetitive than complex. It turns out creating the controls in code was simpler for this application. I’m very much a WPF novice, so I don’t expect my simple user interface to win any awards. It was, however, a fun afternoon and I learned a lot.

This weekend, on the ##fsharp IRC channel on irc.freenode.net, we were joking around about the compulsion to point out tricky or interesting bits of one's code. In that mode, I would like to call attention to the fact that I use one-dimensional arrays rather than the two-dimensional arrays that immediately come to mind for a sliding block puzzle. One dimensional arrays seem to provide a simpler solution. (Technically, the second dimension is smuggled in via the "tileMoves" array.)

In a comment at the top of the code, I have listed the references my project includes. If you use visual studio, you will also need to make sure to set your project type to "Windows Application" on the project properties Application panel.

Coming soon, I will connect the interface to one or more of my search algorithms and will animate the interface to show the generated solution. (Extending the puzzle to formats other than 4x4 is left as an exercise to the reader.)

(Presented “as-is” and without warranty or implied fitness; use at your own risk.)
// I have the following libraries referenced
// in the solution:
//
// Accessibility
// FSharp.Core
// mscorlib
// PresentationCore
// PresentationFramework
// System
// System.Core
// System.Xml
// WindowsBase

open System
open System.Windows
open System.Windows.Controls
open System.Windows.Media


// These literals define the
// cardinal directions as
// array offsets.
[<Literal>]
let up = 0
[<Literal>]
let right = 1
[<Literal>]
let down = 2
[<Literal>]
let left = 3


// Class which handles the tiles.
type Tiles () =

// The slots as a one-dimensional array.
let tileSlot = [| 0..15 |]

// The moves from each tileSlot.
// Format is:
// tileSlot 0 (up,right,down,left)
// tileSlot 1 ...
// -1 = an illegal move.
let tileMoves =
[|
[|-1; 1; 4;-1 |]
[|-1; 2; 5; 0 |]
[|-1; 3; 6; 1 |]
[|-1;-1; 7; 2 |]

[| 0; 5; 8;-1 |]
[| 1; 6; 9; 4 |]
[| 2; 7;10; 5 |]
[| 3;-1;11; 6 |]

[| 4; 9;12;-1 |]
[| 5;10;13; 8 |]
[| 6;11;14; 9 |]
[| 7;-1;15;10 |]

[| 8;13;-1;-1 |]
[| 9;14;-1;12 |]
[|10;15;-1;13 |]
[|11;-1;-1;14 |]
|]

// The blank tile is at the
// center of the action
let mutable tileBlank = 15;

// Return the index of the tile
// in a tileSlot.
member this.GetTile i =
tileSlot.[i]

// Move the blank slot.
// Returns a to*from pair.
// Returns to*to on an illegal move.
member this.Move dir =
let tb = tileBlank
let tn = tileMoves.[tileBlank].[dir]
match tn with
| -1 -> (tb,tb)
| _ ->
tileSlot.[tb] <- tileSlot.[tn]
tileSlot.[tn] <- 15
tileBlank <- tn
(tb,tn)



// Window class.
type MainWindow (app: Application) =
inherit Window()

// Colors for even-numbered tiles.
let tileColorEven = (
Brushes.DarkSlateGray,
Brushes.Bisque,
Brushes.DarkSlateGray )

// Colors for odd-numbered tiles.
let tileColorOdd = (
Brushes.DarkSlateGray,
Brushes.Azure,
Brushes.DarkSlateGray )

// Colors for the blank slot.
let tileColorBlank = (
Brushes.DimGray,
Brushes.Gray,
Brushes.Gray)

// Tile storage.
let tiles = Tiles()

// The window has one element, a grid.
let grid:Grid = System.Windows.Controls.Grid()

// Returns a format tuple for the tileSlot.
let getFormat i =
match tiles.GetTile i with
| 15 -> ("F#",tileColorBlank)
| t ->
match t%2 with
| 0 -> (((t+1).ToString()),tileColorEven)
| _ -> (((t+1).ToString()),tileColorOdd)

// Creates a Label control for the tileSlot.
let createLabel (i:int) =
let l = new Label()
l.FontSize <- 20.0
l.HorizontalContentAlignment <-
HorizontalAlignment.Center
l.VerticalContentAlignment <-
VerticalAlignment.Center
l.BorderThickness <- Thickness(1.0)
l.SetValue(Grid.RowProperty,(i/4))
l.SetValue(Grid.ColumnProperty,(i%4))
l

// Updates the Label format for the tileSlot.
let labelFormat (l:Label) (i:int) =
let (t,(b0,b1,b2)) = getFormat i
l.Content <- t
l.Foreground <- b0
l.Background <- b1
l.BorderBrush <- b2
l

// Move the blank slot.
let moveTile d =
let (t0,t1) = tiles.Move d
if (t0<>t1) then
labelFormat (grid.Children.[t0] :?> Label) t0 |> ignore
labelFormat (grid.Children.[t1] :?> Label) t1 |> ignore

// Move the blank slot.
let rec moveTiles d =
match d with
| [] -> ()
| h::t ->
moveTile h
moveTiles t

// Keyboard handler.
// Note that the blank slot moves in the
// opposite direction from the keypress.
// This feels more natural to me.
let onKeyDown (e:Input.KeyEventArgs) =
match e.Key with
| Input.Key.Up -> moveTile down
| Input.Key.Right -> moveTile left
| Input.Key.Down -> moveTile up
| Input.Key.Left -> moveTile right
| _ -> ()


// Move the blank slot.
member this.MoveTile d =
moveTile d

// Move the blank slot.
member this.MoveTiles d =
moveTiles d

// Initialize the window and all the data.
member this.Initialize () =

this.Title <- "Fifteen Puzzle"
this.Width <- 240.0
this.Height <- this.Width

for i in 0..3 do
grid.ColumnDefinitions.Add(new ColumnDefinition());
grid.RowDefinitions.Add(new RowDefinition());

this.AddChild grid

for i in 0..15 do
grid.Children.Add (labelFormat (createLabel i) i) |> ignore

this.KeyDown.Add onKeyDown

()


// Test application.
type App () =
inherit Application()

static member Go() =

let app = new App()

let win = new MainWindow(app)

win.Initialize()

let r = new Random()

for i=1 to 50 do
win.MoveTile (r.Next 4)

app.Run(win) |> ignore


[<STAThread>]
do
App.Go()

No comments: