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