Mine Sweeper for Mac OS X (DRAFT)
Transcription
Mine Sweeper for Mac OS X (DRAFT)
Mine Sweeper for Mac OS X (DRAFT) CS 224 Due 11:59pm May 1, 2015 1 Introduction remaining unexposed cells without mines mine field detonated, game over unexposed cell exposed cell marked as containing mine number of neighboring mines revealed mine player clicked bomb! Figure 1: Mine Sweeper screenshots. Game in progress (left), and game over (right). For this project you will implement the popular Mine Sweeper game via OS X’s Cocoa API using Xcode. Some screenshots of the game in action are given in Figure 1. We employ the Model-View-Controller (MVC) design pattern to classify the various objects in the program code. Since we are focusing on user-interface design, I will provide you with the source code for the model classes. The various objects that control game play are described in Section 2. A description of how to lay out the user-interface elements and connect them with the controller’s outlets and target action methods is given is Section 3. Section 4 outlines the game logic and other implementation details. Submission instructions, final requirements, and suggestions for further refinements are give in Section 5. 2 Model View Controller In order to separate internal game logic and state information from the user-interface elements we use the MVC design pattern. A “model” object (the mineField) records where mines are located, which cells are marked and exposed, and how many mines surround each cell. The “view” objects consist of a matrix 1 if minefield cell not marked, the controller sends a exposeCellAtRow:Col: message to minefield model object 2 Controller 1 4 3 update view minefield responds with... -1 ➜ "boom", mine hit! mineField 0 ➜ cell safely exposed along with neighbors. 1..8 ➜ cell safely exposed. Model user presses button in minefield view (buttons for exposed cells are disabled) View Figure 2: Usual event sequence and communication pattern between MVC objects. of buttons and other user-interface elements which the user directly interacts with. The “control” object mediates communication between the model and the view. Figure 2 illustrates a typical sequence of messages passed between the various objects that is initiated by the user clicking on one of the view’s buttons representing a cell in the minefield: 1. The user clicks a button (a view object) which triggers a “button clicked” message to be sent to the controller object. Note that buttons representing exposed cells have been disabled (i.e., no action is generated). 2. The controller fetches the corresponding cell (a model object) contained in the mineField (the primary model object) to see if it has been “marked.” If not, the controller sends an exposeCellAtRow:Col: message to the mineField. 3. The mineField exposes the cell and replies to this message by returning an integer code n : (a) If n = −1 then the user has detonated a mine. (b) If 1 ≤ n ≤ 8 then exactly one cell has been safely exposed and n represents the number of mines in the adjoining cells. (c) n = 0 indicates that there are no mines in the vicinity of the cell and a whole set of neighboring cell have been safely exposed. 4. The controller then completes the cycle by updating the view: (a) If a mine has been detonated, then the controller scans all the cells in the mineField to expose their content, and updates each of the corresponding buttons in the view. The entire matrix of buttons is disabled so no more minefield buttons respond to the user until a new game is generated. The score is updated to display the message “BOOM” (see the screen on the right of Figure 1). (b) If exactly one cell has been exposed, then that button’s title is set to n and its disabled. (c) If more than one mine has been exposed, then the controller scans the minefield to find the exposed cells and updates the view’s corresponding buttons by disabling them and setting their titles. In the last two cases, the controller also queries the mineField for the updated score and update’s the score text-field. If the score is zero (all cells without mines have been found), then all the mineField cells are scanned and their corresponding view buttons are updated, the matrix of cells is disabled, and the score’s text is set to “WIN!.” This form of interplay between the various objects is typical in MVC. Note that the model is “view agnostic” and could be reused in another program with a different UI. The MVC pattern is heavily used in Cocoa. A rich set of view components and a large stock of specialized controllers are accessible via the API and Object palette. 2 3 Building the User Interface NSPopUpButton NSButton NSTextField @interface ViewController : NSViewController @property (weak) IBOutlet NSTextField *scoreTextField; @property (weak) IBOutlet NSMatrix *minefieldMatrix; - (IBAction)newGame:(id)sender; - (IBAction)selectLevel:(NSPopUpButton *)sender; - (IBAction)minefieldClicked:(NSMatrix *)sender; … @end NSMatrix NSButtonCell Figure 3: The primary view components and their connections with the various outlets and actions in the controller object. 3.1 Laying out the beginner view Create a new Cocoa Application project named “MineSweeper” in Xcode (Select Language ”Objective-C” and Check “Use Storyboards” ). Select the storyboard file (named Main.storyboard) to edit the user interface components. Drag a square button from the Library of stock view components onto the view controllers’s main view. Make sure the Inspector window is open (look under the Tool Menu) and “inspect” your button. This button will be the prototype for all of the buttons representing the minefield, so size and groom it as you deem best. For example, you will probably want its mode be “Push On Push Off.” Once you are happy with your prototype button, create a 10 × 10 matrix of buttons by choosing the “Embed Objects In Matrix” options from the Layout menu. This will replace your button with an NSMatrix object containing NSButtonCell objects matching your prototype. Using the Inspector, set the matrix to have 10 rows and 10 columns. Change the mode of the matrix to “Highlight” so multiple buttons can be “selected” (other modes constrain the matrix so that exactly one button is selected – typical of radio button behavior). Add the other view components you see in Figure 3. Groom these objects as desired and follow the blue guidelines provided by Xcode. Rename the items under the pop-up button to “beginner,” “intermediate,” and “advanced.” You can add an “expert” item via Duplicate from the Edit menu. Make sure “beginner” is the first item (item 0) and is the currently selected item. You can occasionally build and run the app to how the controls behave. 3.2 Connecting outlets and action targets Define the score and matrix instance variables in the MyController interface as shown on the right of Figure 3. Add the signatures for the clicked:, newGame:, and the levelSelect: methods too. Controldrag to/from the MyController object in the NIB window from/to the appropriate controls to connect the corresponding outlets and action targets. In the spirit of incremental development, I would go ahead and define stub implementations for my controller methods that simply log events to the console: -(IBAction)minefieldClicked:(NSMatrix*)sender { const NSInteger r = [sender selectedRow]; 3 const NSInteger c = [sender selectedColumn]; NSButtonCell *bcell = [sender selectedCell]; NSLog(@"minefieldClicked: selected cell=%@ at row=%d, col=%d", bcell, (int) r, (int) c); } -(IBAction)selectLevel:(id)sender { const NSInteger index = [sender indexOfSelectedItem]; NSLog(@"levelSelect:sender=%@, index=%d", (int) sender, (int) index); } -(IBAction)newGame:(id)sender { NSLog(@"newGame"); } You should be able to “Build and Go” in Xcode now. Open the console (under the Run menu in Xcode) to see the log messages occur as you interact with your UI facade. 3.3 NSMatrix and NSButton vs NSButtonCell By examining the output to the console, you will note that the sender object invoking the minefieldClicked: method is an NSMatrix (not a button). The prototype NSbutton was converted into its lightweight counterpart of type NSButtonCell which is the cell type contained in the NSMatrix. In the clicked: method above we fetch the most recently selected cell (the one the user clicked on) and its matrix coordinates via the appropriate NSMatrix methods. For the most part, you can treat an NSButtonCell like an NSButton, you just need to go through the matrix to get at them. NSMatrix objects consist of selected (and unselected) cells. Hopefully you configured your matrix to allow for multiple selected cells in IB. Selected cells will have a slightly different appearance (they are the darker cells in Figure 3). When the user marks a mine by clicking a button while pressing the shift key (the cells labeled with a P), I deselect the cell by sending the matrix a deselectSelectedCell message. 3.4 Autolayout Constraints XXXX Explain AutoLayout here. 4 4.1 Game Logic The minefield model I am providing you with the source code for the minefield model. The Cell class represents an individual cell in the minefield which has a one-to-one correspondence with button cells in the view. Each cell has four properties: BOOL BOOL BOOL char hasMine; // Cell contain a mine? exposed; // Has the cell has been exposed (may or may not have mine)? marked; // Cell marked as having a mine (perhaps incorrectly)? numSurroundingMines; // Number of mines in 8 adjacent cells The only property the controller should write to is marked – the remaining values are managed by the mineField object and should be considered immutable. The MineField class encapsulates a 2D array of Cell’s which directly correspond to the NSButtonCell’s in the view’s NSMatrix. The designated initializer is used to create a new minefield with a specified number of randomly placed mines: 4 align leading align center (priority = 750) Beginner 8 112x21 ≥ 8 96x21 New Game ≥ 8 align trailing 76x21 999 align on Center Y 8 align with superview Center X Minefield Matrix 300 x 300 20 Figure 4: Layout of UI components. -(instancetype)initWithWidth:(int)w Height:(int)h Mines:(int)m; The minefield is not resizable. Therefore, if the controller wishes to change the size of the minefield, then a new minefield should be created. The controller sends the minefied a reset message to reinitialize the minefield and shuffle the placement of the mines. The cellAtRow:Col: method is used fetch a cell. As described in Section 2, the controller sends the minefield an exposeCellAtRow:Col: message when the user exposes a cell. The number of remaining unexposed cells (used to “score” the game) are retrieved via the unexposedCells method. 4.1.1 Creating and initializing the minefield model The view controller should contain a mineField instance variable which is first created in its viewDidLoad method: - (void)viewDidLoad { [super viewDidLoad]; // caveat lector : 10x10 size must agree with NSMatrix size! self.mineField = [[MineField alloc] initWithWidth:10 Height:10 Mines:10]; ... } This method is invoked when the view controller’s main view is loaded and is a good place to perform initialization. The outlet and action connections are already initialized since they are part of the serialized state of NIB objects stored in the storyboard. Whenever the user selects a new level, the controller should create a new mineField object. Normally we would release the memory for the old minefield first, but since we are using Automatic Reference Counting (ARC) this is handled automagically for us. 4.2 Marking Mines Unfortunately our buttons do not respond to right mouse clicks since this is not “normal” Mac behavior, but this is the usual convention for marking mines in Mine Sweeper. To receive right mouse clicks requires 5 subclassing NSMatrix as described in Section 5.4. Just use the shift-key to mark mines. We can detect the state of the shift key and toggle minefield cell as follows: BOOL shiftKeyDown = ([[NSApp currentEvent] modifierFlags] & (NSShiftKeyMask | NSAlphaShiftKeyMask)) !=0; if (shiftKeyDown) { cell.marked = !cell.marked; // toggle marked cell [bcell setTitle: cell.marked ? @"P" : @""]; [matrix deselectSelectedCell]; // unselect cell, leave enabled ... The clicked button was automatically selected which is undone by sending a deselectSelectedCell message to the button matrix (the matrix controls which cells are selected). 4.3 Selecting a new level When the user selects a new level from the NSPopUpButton we need to 1. Determine the size and density of the new minefield based on the difficulty level. 2. Resize the minefieldMatrix and the enclosing window to accommodate the new matrix. 3. Create a new minefield model object that corresponds to the new size and difficulty. Figure 5 lists my implementation of the selectLevel: action method. I record the current levelIndex as a property in my view controller: @property (assign, nonatomic) NSInteger levelIndex; // 0 => beginner, ..., 3 => expert to avoid all this work if the level has not changed. Once I determine the size and density of the new minefield I adjust the size of the minefieldMatrix programmatically by sending it a renewRows:columns: message followed by a sizeToCells message. I record the change is size since this will tell me how much to adjust the window size by in the next step. The AutoLayout constraints (described in Section 3.4) that dictate the size of the minefield are updated; If we have set our constraints correctly then the remaining UI elements should be arranged appropriately by the AutoLayout engine. After calculating the new frame for adjust window I update the frame programmatically by setting is from property (without animation since animating the change did not appear very smooth). Finally we need to create a new minefield object, update the matrix cells, and the game score. If you are short on time, you only need to have a single level in your application. 5 5.1 Finishing Up Polishing your application One nice touch is to use images for marked cells and bombs as shown in Figure 6. I created some transparent PNG images to represent a flag and a bomb. I added two new images to the Images.xcassets file and name them “bomb” and “flag.” Note that there are two sizes, 1x and 2x, where the larger size is for “retina display” where 1 pt is actually mapped to twice as many pixels. I then added bombImage and flagImage properties (of type NSImage*) to the controller an initialized them in viewDidLoad - (void)viewDidLoad { ... self.bombImage = [NSImage imageNamed:@"bomb"]; self.flagImage = [NSImage imageNamed:@"flag"]; ... } 6 - (IBAction)selectLevel:(NSPopUpButton *)sender { const NSInteger level = sender.indexOfSelectedItem; if (level == self.levelIndex) return; // no change self.levelIndex = level; // else record change static struct { int width, height, numMines; } levels[4] = { {10, 10, 10}, // 0 : beginner {20, 15, 50}, // 1 : intermediate {25, 18, 90}, // 2 : advanced {30, 20, 150} // 3 : expert }; const int w = levels[level].width; // determine new minefield configuration const int h = levels[level].height; const int m = levels[level].numMines; // // Update minefield matrix and record change in size. // Update AutoLayout size constraints on minefield matrix. // const CGSize matrixSize = self.minefieldMatrix.frame.size; [self.minefieldMatrix renewRows: h columns: w]; [self.minefieldMatrix sizeToCells]; const CGSize newMatrixSize = self.minefieldMatrix.frame.size; const CGSize deltaSize = CGSizeMake(newMatrixSize.width - matrixSize.width, newMatrixSize.height - matrixSize.height); self.matrixWidthConstraint.constant = newMatrixSize.width; self.matrixHeightContraint.constant = newMatrixSize.height; // // Resize enclosing window according to size // of the minefield matrix. // NSRect windowFrame = self.view.window.frame; NSRect newWindowFrame = CGRectMake(windowFrame.origin.x, windowFrame.origin.y - deltaSize.height, windowFrame.size.width + deltaSize.width, windowFrame.size.height + deltaSize.height); [self.view.window setFrame:newWindowFrame display:YES animate:NO]; // // Allocate a new minfield model and update the minefield // matrix cells. // self.mineField = [[MineField alloc] initWithWidth:w Height:h Mines:m]; [self updateCells]; [self updateScore]; } Figure 5: Resizing the minefield and window when the user chooses a different level. 7 Figure 6: Using images to represent marked cells (flags) and bombs. Here is an example of how I alternated between text and images for a button cell: if (cell.marked) [bcell setImage: flagImage]; else { [bcell setImage: nil]; [bcell setType: NSTextCellType]; [bcell setTitle: @""]; } 5.2 Creating an Application Icon In the Images.xcassets you will notice there is a slot for the AppIcon. Here is provide transparent PNG images for my application icon ranging from 16 × 16 all the way up to 256 × 256. 5.3 Altering the Menu In IB you will want to rename or delete various menus and menu items as appropriate for your application. Many of the menu items already provide the desired functionality (e.g., about, hide, quit, print). Others do not make sense (at least for now) with your program (e.g., new, open, close, save, cut, copy) – just whack these. Undo and Redo would be cool, but that is beyond the scope of this project. 5.4 Marking mines with a right mouse click This is not required (and maybe violates Apple’s Human Interface Guidlines (HIG)), but the usual convention for marking mines is via a right mouse click. Since it is the NSMatrix object that receives the mouse events we need to create a subclass of NSMatrix and override its rightMouseDown: method. In Xcode, add new Objective-C class files MyMatrix.[mh] to your project. Edit MyMatrix.h so that MyMatrix is a subclass of NSMatrix and add the method signature for rightMouseDown: @interface MyMatrix : NSMatrix { } - (void)rightMouseDown:(NSEvent *)theEvent; @end 8 We override rightMouseDown in MyMatrix.m to convert the mouse window coordinates to local matrix coordinates, determine the corresponding matrix row and column, fetch the associated cell, and (if it is enabled) select it and send a rightClicked: message to the target (which is our controller): #import "MyMatrix.h" #import "MyController.h" @implementation MyMatrix - (void)rightMouseDown:(NSEvent *)theEvent { NSInteger r, c; NSPoint pt = [self convertPoint:[theEvent locationInWindow] fromView:nil]; [self getRow:&r column:&c forPoint: pt]; NSButtonCell *bcell = [self cellAtRow:r column:c]; if ([bcell isEnabled]) { [self selectCellAtRow:r column:c]; [[self target] rightClicked:self]; } } @end Now we need to add a rightClicked: method to MyController.m (also add the method signature to interface MyController.h) that handles marking a mine using the same logic described in Section 4.2: -(void)rightClicked:(id)sender { int r = [sender selectedRow]; int c = [sender selectedColumn]; // ... mark or unmark mine } Finally, we need to tell Xcode to use an instance of MyMatrix (not NSMatrix) for the button matrix. Click the matrix in the NIB window, and select the identity tab in the Inspector. Change the class name to MyMatrix. Save, build, and run. Viola! We have the traditional behavior. 6 Submitting your solution You will archive your entire project and submit via the course electronic submission form by midnight on the due date. It would be a good idea to add a text file with your name and email address to your project so I can track you down if I have problems building or running your application. Make sure you have created a git repository and have committed your final version. Create a compressed tarball rooted in the directory above your project tar czvf ms.tar.gz MineSweeper You should see the .xcodeproj file and the .git directory (among many other things) there. 9