15 Puzzle CS 483 Due 11:59 pm Friday February 3, 2012
1
Introduction
For this project you will create an iOS 5 Fifteen Puzzle app for the iPhone: http://en.wikipedia.org/wiki/Fifteen_puzzle Section 2 describes how to use the Interface Builder (IB) editor to layout your view components. The methods for the internal game logic (which you will need to implement) are specified in Section 3. The details of the Model View Controller (MVC) design and the flow of events is outlined in Section 4. Some ideas of extra features you can add to your app is given in Section 5. How to submit your solution is specified in Section 6.
1.1
Creating a new project
You app will contain a single view that displays the puzzle. To get started, create a new iOS Application project in Xcode 4.2 that uses the iOS “Single View Application” template. Choose “iPhone” for the device family. Uncheck “Use Storyboard” (we will use a NIB file to lay out the user interface) and use “Automatic Reference Counting” (ARC). Name your project FifteenPuzzle and enter edu.username (or something unique) for the “Company Identifier.” Specify FifteenPuzzle for the “Class Prefix;” Xcode will use this to generate class names. Don’t ever begin this string with a digit since Xcode will have to tweak it since identifiers can’t begin with a digit in C. You can let Xcode generate a git repository for you or you can create one by hand. Either way, using git is a good idea. Figure 1 shows the supporting files in your project under the Project Navigator tab.
2
Laying out the View
Select FifteenViewController.xib in the Project Navigator and edit the NIB file as illustrated in Figure 2. The Xcode template will have already created a “main view” for you. First add a UIToolBar and another UIView as illustrated on the figure’s left. Another UIView is then added which we will call the “board view” since it will contain the tiles. Use the Size Inspector to set its size at 300 × 300 and center it. Change its background to black in the Attributes Inspector. 300 is a nice choice since its a multiple of four and fits snuggly within the 320 pixel wide screen and won’t need to be resized if we allow the user to rotate the device. Drop a UIButton onto the board view, resize to 75 × 75, set the title two a two-digit number and resize the font to taste. Replicate (copy and paste) this button 14 times and lay them out as shown on the figure’s right. Set the title and the tags (via the Attribute Inspector) of each button to one through fifteen; we will use the tags to identify each button in our our code. The views and controls in the NIB file form a hierarchy as illustrated in Figure 3. The application’s view controller is created in the App Delegate’s application:didFinishLaunchingWithOptions: method which creates its view when this NIB file is loaded. The “File’s Owner” references the object that loaded the NIB file, thus it is a “placeholder” which references the App Delegate in this case.
1
created by single-view app template
{
} Application Delegate } View Controller } Board Model
NIB
Application Property List main (don't edit)
}
Frameworks our app uses
Figure 1: Content of project.
2.1
Adding a boardView outlet
Since the controller will frequently access the tiles of our board view, create an outlet for it in the controller class: @property(weak,nonatomic) IBOutlet UIView *boardView; Since this view’s superview already “owns” this object and will provide it with the necessary lifetime, we can safely reference it with a weak pointer. We use the nonatomic keyword since we won’t be accessing it concurrently (i.e., with multiple threads). Make sure you provide the corresponding @synthesize directive in the implementation. Use the Connections Inspector in IB to connect the boardView outlet with this instance variable.
2.2
Adding action methods
We need to create the appropriate callback methods that will be invoked when the user selects a tile or wants a new game. Add the following “action methods” to the FiftenPuzzleViewController class: -(IBAction)tileSelected:(UIButton*)sender; -(IBAction)scrambleTiles:(id)sender; I usually add code to log when the methods are invoked for diagnostic purposes: -(IBAction)tileSelected:(UIButton*)sender { const int tag = [sender tag]; NSLog(@"tileSelected: %d", tag); ... } Use the Connection Inspector in IB to connect these actions with the appropriate buttons on “Touch Up” events.
2
300
UIView
75
UIView
416
75
300 320
UIBarButtomItem
UIButton
UIToolBar
Figure 2: Views and control in NIB file. The toolbar and view on the left are subviews of the main view. The 300 × 300 view in the center (we call the “board view”) is a subview of the view on the left which will contain the tiles. Each of the 15 buttons on the left are subview of the board view.
2.3
Additional Setup
Usually there is more work to be done to setup the view after its NIB file is loaded. The UIViewController class provides a method for its subclasses to override when there is additional setup to be done programmatically. We will use FifteenPuzzleViewController’s overloaded version to create a new board (see Section 4), shuffle the tiles on the board, and arrange the view’s tiles (represented as buttons) to reflect the state of the board: - (void)viewDidLoad { [super viewDidLoad]; self.board = [[FifteenBoard alloc] init]; // create/init board [board scramble:NUM_SHUFFLES]; // scramble tiles [self arrangeBoardView]; // sync view with model }
3
The Model
Define a class FifteenPuzzle that will encapsulate the puzzle’s logic and support the methods below. The puzzle tiles are encoded as integers from 1 to 15 and the space is represented with a zero. The puzzle tiles are assumed to lie in a 4 × 4 grid with one empty space. -(id)init; Initialize a new 15-puzzle board with the tiles in the solved configuration (only called once when the puzzle is instantiated). -(void)scramble:(int)n; Choose one of the “slidable” tiles at random and slide it into the empty space; repeat n times. We use this method to start a new game using a large value (e.g., 150) for n. -(int)getTileAtRow:(int)row Column:(int)col; Fetch the tile at the given position (0 is used for the space). -(void)getRow:(int*)row Column:(int*)col ForTile:(int)tile; Find the position of the given tile (0 is used for the space). -(BOOL)isSolved; Determine if puzzle is in solved configuration. -(BOOL)canSlideTileUpAtRow:(int)row Column:(int)col; Determine if the specified tile can be slid up into the empty space. 3
view controller
main view
board view
Figure 3: Hierarchical view of objects in the NIB file. “File’s Owner” is simply a placeholder that references the object that loaded the NIB file which is our view controller in this case. All of our views and controls are subviews of the “main view” which was created by the Xcode template. -(BOOL)canSlideTileDownAtRow:(int)row Column:(int)col; -(BOOL)canSlideTileLeftAtRow:(int)row Column:(int)col; -(BOOL)canSlideTileRightAtRow:(int)row Column:(int)col; -(void)slideTileAtRow:(int)row Column:(int)col; Slide the tile into the empty space. It is important that the positions of the tiles are not simply chosen at random since you may create an unsolvable puzzle. Instead, “scramble” the tiles of a puzzle that we know is solvable when the user wants to start a new game.
3.1
Creating the model
Since the view controller will be the only entity accessing our puzzle board, it will create the puzzle board in its viewDidLoad method as described above. Create an instance variable for it (and make sure to synthesize its setter and getters in the implementation): @property(strong,nonatomic) FifteenBoard *board; Note that we make this a strong reference since the controller is the sole owner. Make sure to import the FifteenBoard.h header file in the implementation; For faster compiles, you only need the terse directive @class FifteenBoard; in the controller’s header file; this merely tells the compiler that the FifteenBoard class exists.
4
Model View Controller
Figure 4 illustrates the main components of the 15-Puzzle App classified according to the MVC pattern. The UIButton’s and UIView’s that are archived and reanimated at run time from the NIB file are shown on the right. The controller references the boardView via an IB outlet so that it can manipulate the tiles on the board. The target/action mechanism is used to inform the controller when the user initiates an event.
4
CONTROLLER FifteenPuzzleViewController
boardView board tileSelected: scrambleTiles:
outlet
g tar
FifteenPuzzleBoard
et/a
char state[4][4]; init getTileAtRow:Column: getRow:Column:ForTile: isSolved canSlideTileUpAtRow:Column: canSlideTileDownAtRow:Column: canSlideTileLeftAtRow:Column: canSlideTileRightAtRow:Column: slideTileAtRow:Column: scramble:
ctio n
MODEL VIEW Figure 4: MVC elements of 15-puzzle app. The View objects are stored in the NIB file. When the NIB is loaded, the boardView outlet of the Controller is bound to the associated UIView and the various UIButton target/actions are created. The Controller creates an instance of FiftenPuzzleBoard (the sole Model object) which encapsulates the game’s logic. The controller owns the sole model object which encodes the state of the puzzle and provides methods for querying and modifying the board. The model is not tied to any specific user interface and could be used in another application with a different user interface.
4.1
Sliding Tiles
Figure 5 shows the sequence of events that are triggered on a “touch-up” event when the user touches tile 12. We wired this event to send a tileSelected: message to the controller. Here is snippet of the controller’s code annotated with steps 2, 3 and 4: -(IBAction)tileSelected:(UIButton*)sender { const int tag = [sender tag]; int row, col; [board getRow:&row Column:&col ForTile:tag]; // (2) CGRect buttonFrame = sender.frame; if ([board canSlideTileUpAtRow:row Column:col]) { [board slideTileAtRow:row Column:col]; // (3) buttonFrame.origin.y = (row-1)*buttonFrame.size.height; sender.frame = buttonFrame; // (4) } else ... The tag field that was assigned in IB identifies which tile was selected. We then query the model to determine which row and column the tile is in. buttonFrame is a rectangle that specifies the size and position of the button within its parent view (the boardView). If the model indicates that we can slide this tile up, the we tell the model to slide the tile (Step 3) and then we move the button up (step 4). Changing the frame property of the button immediately moves the button. It is amazingly simple to show the button sliding by animating the change of the frame property as follows: 5
CONTROLLER FifteenPuzzleViewController
2
boardView board tileSelected: scrambleTiles:
3
1 4
FifteenPuzzleBoard
char state[4][4]; init getTileAtRow:Column: getRow:Column:ForTile: isSolved canSlideTileUpAtRow:Column: canSlideTileDownAtRow:Column: canSlideTileLeftAtRow:Column: canSlideTileRightAtRow:Column: slideTileAtRow:Column: scramble:
MODEL VIEW Figure 5: Communication sequence triggered when the user “touches up” on the 12-tile: (1) the button sends a tileSelected: message to the Controller; (2) The Controller (using the sender’s tag = 12) queries the Model for the column and row of the corresponding button; The Controller then queries the Model to determine which direction the tile can slide (if any); Since it was determined that the tile can slide down, (3) the Controller tells the Model to slide the tile and (4) moves the View’s button by altering it’s frame property.
6
[UIView animateWithDuration:0.5 animations:^{sender.frame = buttonFrame;}]; The strange ^{...} syntax is an Objective-C block which provides a simple mechanism for sending code snippets as arguments in method calls. All the UIKit views are backed by a Core Animation layer and it is really simple to animate their properties.
4.2
Scrambling Tiles
When the user chooses a new game, we simply tell the model to scramble the tiles and send a message to the boardView to arrange its buttons accordingly: -(IBAction)scrambleTiles:(id)sender { [board scramble:NUM_SHUFFLES]; [self arrangeBoardView]; } Repositioning the buttons in the view to match the model is a simple matter of changing their frame properties: -(void)arrangeBoardView { const CGRect boardBounds = boardView.bounds; const CGFloat tileWidth = boardBounds.size.width / 4; const CGFloat tileHeight = boardBounds.size.height / 4; for (int row = 0; row < 4; row++) for (int col = 0; col < 4; col++) { const int tile = [board getTileAtRow:row Column:col]; if (tile > 0) { __weak UIButton *button = (UIButton *)[boardView viewWithTag:tile]; button.frame = CGRectMake(col*tileWidth, row*tileHeight, tileWidth, tileHeight); } } } We assume that the tile buttons are a quarter the size of the boardView. The bounds property is similar to the frame property except that it describes its position and size in its own coordinate system (instead of the parent view’s). The types and functions that are prefixed with CG are part of Core Graphics which is a C API that we will look into with much detail later.
5
Optional Bells and Whistles
You should add 57 × 57 and 114 × 114 PNG images to be used as app icons (the larger one for Apple’s “Retina Display”). Adding a “splash screen” is nice so the user doesn’t see a blank screen while the app loads. You could also add an background images to your buttons as shown on the left of Figure 6. I added the cougar.png to my project (just drag and drop into the Project Navigator) and then used Core Graphics to divvy it up: - (void)viewDidLoad { ... UIImage* image = [UIImage imageNamed:@"cougar.png"]; [self setBackgroundImageOfButtons:image]; // helper method using CG } Perhaps you could let the user choose an image from their photo library as shown in Figure 6 – this requires using a UIImagePickerController and conforming to the UIImagePickerControllerDelegate protocol (as well as some other CG tricks). We’ll shown you how to do this later. 7
Figure 6: Using images for the button backgrounds. We can allow the user to choose an image from their photo library.
6
What to submit
For this project you do not need to install your app on a device (we will be doing that later). Simply make sure it runs as advertised under the iPhone Simulator. You will archive your project as a compressed tarball and submit electronically via the course website (stay tuned for further instructions). Its nice to do a “build clean” before archiving so that all of the build riffraff is not included.
8