Peg Solitaire Narrative - Board Refactoring

Peg Solitaire Narrative

The fifth version of PegSolitaire cleans up the design a bit, and adds a new board type, and a button for the user to toggle between them.

A lot of responsibility was living in PegBoard.m. The shape of the board, the number of pegs, that kind of stuff. It's better to split up the responsibility into several classes. One class to contain information, and the other to draw it. This version of PegSolitaire takes a step towards that structure.


At this point I discovered I made a mistake in naming files. PegBoard is a great name for the class I want to use to hold the general pegboard information. Unfortunately, PegBoard has already been taken by the class that draws the board. That's fine. The code that was in PegBoard was moved into the PegBoardView class.

Part of tracking this change was easy. I copied the code to new files (PegBoardView.h, PegBoardView.m), added them to the XCode proejct, dragged PegBoardView.h into Interface Builder, and changed the class of view object:

If I were running my own CVS repository, I'd just do a little repository surgery to move the revision history from PegBoard to PegBoardView. Since this is hosted on SourceForge's CVS server, I can't do that (without going through their support process, and even then it would have broken the projects from older revisions. More work than I think this part of the project is worth), so I just added new files for the View class and put in a comment about what revision of PegBoard these came from. Apologies for the rather bizarre file history for PegBoard.


So what is this new class that's living in CHPegBoard? It's a semi-abstract base class. It has some methods that subclasses override to describe the board, specifically how many rows of pegs there are, and how many pegs are in each row. Subclasses also provide an NSBezierPath that describes the bounds of the board (since I'm too lazy to try to infer the geometry of the board given an arbitrary grouping of pegs). CHPegBoard also has methods for the total number of pegs and the maximum row size implemented in terms of the other methods. These values are useful when laying out the pegs. Here is some code
CHPegBoard.h
#import <Cocoa/Cocoa.h>

@interface CHPegBoard : NSObject
{
}

- (int) spaceCount;
- (int) maxRowSize;

// subclasses override these
- (int) rowCount;
- (int) pegsForRow: (int) row;

- (NSBezierPath *) pathForBoard;

@end // CHPegBoard

The code specific for the triangular board got moved into CHTriangularBoard, and a new board type, CHCrossBoard was added that has a cross-shaped arrangement of pegs. Here is a cheesy class diagram to show the relationships:

The interfaces for these files are pretty unspectacular:


CHTriangularBoard.h
#import <Cocoa/Cocoa.h>

@interface CHTriangleBoard : CHPegBoard
{
}

@end // CHTriangleBoard

CHCrossBoard.h
#import <Cocoa/Cocoa.h>
#import "CHPegBoard.h"

@interface CHCrossBoard : CHPegBoard
{
}

@end // CHCrossBoard

I don't redeclare methods that subclasses override. Some folks do, that's their prerogative. My opinion is that the header files are for communicating what features the class exports. Implementation details (such as what methods are overridden, and any private methods) don't belong there.

Here is the implementation of the cross board. The first part is straightforward. We have 9 rows of pegs. The top 3 and bottom 3 rows have three pegs in them. The middle 3 rows have 9 pegs in them.


CHCrossBoard.m
#import "CHCrossBoard.h"

@implementation CHCrossBoard


- (int) rowCount
{
    return (9);
} // rowCount


- (int) pegsForRow: (int) row
{
    int count = 3;

    if (row == 3 || row == 4 || row == 5) {
        count = 9;
    }

    return (count);

} // pegsForRow


And finally providing the path for the border of the board. This is a path inscribed in a square 1.0 x 1.0. The PegBoardView will scale this path to fit into whatever area it wants to draw the board. Here a cross-like path is made that is centered in the unit square.
- (NSBezierPath *) pathForBoard
{
    NSBezierPath *path = [NSBezierPath bezierPath];

#define THIRD (1.0 / 3.0)
#define TWO_THIRD (THIRD * 2)

    [path moveToPoint: NSMakePoint (0.0, THIRD)];
    [path lineToPoint: NSMakePoint (0.0, TWO_THIRD)];
    [path lineToPoint: NSMakePoint (THIRD, TWO_THIRD)];
    [path lineToPoint: NSMakePoint (THIRD, 1.0)];
    [path lineToPoint: NSMakePoint (TWO_THIRD, 1.0)];
    [path lineToPoint: NSMakePoint (TWO_THIRD, TWO_THIRD)];
    [path lineToPoint: NSMakePoint (1.0, TWO_THIRD)];
    [path lineToPoint: NSMakePoint (1.0, THIRD)];
    [path lineToPoint: NSMakePoint (TWO_THIRD, THIRD)];
    [path lineToPoint: NSMakePoint (TWO_THIRD, 0.0)];
    [path lineToPoint: NSMakePoint (THIRD, 0.0)];
    [path lineToPoint: NSMakePoint (THIRD, THIRD)];

    [path closePath];

    return (path);

} // pathForBoard

@end // CHCrossBoard

CHTriangularBoard is implemented pretty much the same way, specifying the row count, the number of pegs in a row (rows count up from the bottom), and a triangular path. The bottom of the path is skootched up a bit to make it look a little better given the peg layout in CHPegBoardView. This aspect of things still needs some work.
CHTriangualrBoard.m
#import "CHTriangleBoard.h"

@implementation CHTriangleBoard

- (int) rowCount
{
    return (5);
} // rowCount


- (int) pegsForRow: (int) row
{
    return (5 - row);
} // pegsForRow


- (NSBezierPath *) pathForBoard
{
    NSBezierPath *path = [NSBezierPath bezierPath];

// tweaked until it looked good
#define BOTTOM 0.09

    [path moveToPoint: NSMakePoint (0.0, BOTTOM)];
    [path lineToPoint: NSMakePoint (0.5, 1.0)];
    [path lineToPoint: NSMakePoint (1.0, BOTTOM)];
    [path closePath];

    return (path);

} // pathForBoard

@end // CHTriangleBoard

And now the CHPegBoard implementation. spaceCount (the number of spaces for pegs in the row. I don't like that name and might probably change it to pegCount) sums the number of pegs by asking itself for the pegs for a row. Since subclasses override this method, the code will work for any subclass. Similarly for maxRowSize.
CHTriangualrBoard.m
#import "CHPegBoard.h"

@implementation CHPegBoard

- (int) spaceCount
{
    int count = 0;
    int row;

    for (row = 0; row < [self rowCount]; row++) {
        count += [self pegsForRow: row];
    }

    return (count);

} // spaceCount


- (int) maxRowSize
{
    int max = 0;

    int row;
    for (row = 0; row < [self rowCount]; row++) {
        int count = [self pegsForRow: row];
        if (count > max) {
            count = max;
        }
    }

    return (row);

} // maxRowSize

Cocoa doesn't support pure virtual methods (like declaring a method = 0 in C++. So the methods that are intended for subclasses to override have to have some implementation. For the implemenations, you can have them be a no-op (return zero or nil, for instance), or consider them not being implemented to be a Serious Programming Error and abort the application. I'm feeling draconian right now, so the app will terminate. I use assert() here, using a sick C trick. If you do assert(!"some string");, the "some string" evaluates to an address which is non-zero (otherwise it'd be NULL). Doing !"some string" turns the value into zero. assert (0) causes the application abort. And due to the way assert constructs its application termination message, you get the string used. Here are the stubbages:
- (NSBezierPath *) pathForBoard
{
    assert (!"CHPegBoard subclasses must provide their own pathForBoard method");
    return (nil);
} // pathForBoard


- (int) rowCount
{
    assert (!"CHPegBoard subclasses must provide their own rowCount method");
    return (0);
} // rowCount


- (int) pegsForRow: (int) row
{
    assert (!"CHPegBoard subclasses must provide their own pegsForRow method");
    return (0);
} // pegsForRow

CHAppController has finally gotten something to do. So far it has been an empty shell of a class. A place is needed to create one of these CHPegBoard subclasses and to give it to the view. It'd also be cool to be able to swap between the triangular and cross boards. The application controller is the perfect place for this:
CHAppController.h
#import <Cocoa/Cocoa.h>

@class CHPegBoard;
@class CHPegBoardView;

@interface CHAppController : NSObject
{
    CHPegBoard *pegBoard;
    IBOutlet CHPegBoardView *pegBoardView;- (IBAction) switchBoards: (id) sender;

@end // CHAppController

IBOutlet and IBAction are special tags that Interface Builder uses to figure out what connections it needs to support. The pegBoardView outlet gets pointed to the view in IB. Control-click-drag makes the connection, so set up the outlet to point to the view: And the button's action to point to the controller:
Now for CHAppController.m. awakeFromNib is where the initial PegBoard creation happens. SwitchBoards looks at the class of the pegBoard instance variable to decide what object to replace it with. This use of reflection in Cocoa lets us avoid having to keep around an extra piece of state so we know what to change the peg view to.
CHAppController.m
#import "CHAppController.h"
#import "CHTriangleBoard.h"
#import "CHCrossBoard.h"
#import "CHPegBoardView.h"

@implementation CHAppController

- (void) awakeFromNib
{
    pegBoard = [[CHTriangleBoard alloc] init];
    [pegBoardView setPegBoard: pegBoard];

} // awakeFromNib


- (void) dealloc
{
    [pegBoard release];

} // dealloc


- (IBAction) switchBoards: (id) sender
{
    if ([pegBoard isMemberOfClass: [CHTriangleBoard class]]) {
        [pegBoard release];
        pegBoard = [[CHCrossBoard alloc] init];
    } else {
        [pegBoard release];
        pegBoard = [[CHTriangleBoard alloc] init];
    }

    [pegBoardView setPegBoard: pegBoard];

} // switchBoards

@end // CHAppController

Finally! We've come to the changes to the view code. It's not a fully generalized solution yet (there's still hardcoded values here and there). I'm hoping that later on it will become more clear where different bits and pieces will live. First are changes to CHPegBoard[View].h:
CHPegBoardView.h
#import >Cocoa/Cocoa.h<

#define PEG_COUNT 15
#define PEG_COUNT 300

enum {
    CHPegBoard_NO_PEG = -1
};

@class PegBoard;

@interface CHPegBoardView : NSView
{
    BOOL pegStates[PEG_COUNT];	// YES - occupied, NO - empty
    NSBezierPath *pegPaths[PEG_COUNT];	// where the peg lives

    BOOL recalcPegPaths;
    int selectedPeg;

    NSImage *dragImage; // non-nil means we're in a drag
    int dragPeg;        // index of where the drag came from
                        // selectedPeg is what the mouse is currently over
    NSPoint dragPoint;  // where to draw the drag image
    NSPoint lastMouse;  // previous mouse coordinates

    CHPegBoard *pegBoard;
}

- (void) setPegBoard: (CHPegBoard *) pegBoard;

@end // CHPegBoardView

Yeah, using 300 for the PEG_COUNT is majorly cheesy. Eventually the array of states and paths will be dynamically allocated based on the number of slots in the pegboard.

The instance variable and method to set the variable should be Obvious. To be Perfectly Pure there should be a Getter method to get the pegboard. I generally wait to add those until I know I need them, or during the final clean-up of a chunk of code that will be published to others. Since this still "development stage" code as far as I'm concerned, an asymmetrical API is OK.

The initWithFrame: method got gutted, and the setup code got moved to the new setPegBoard:


CHPegBoardView.m

- (id) initWithFrame: (NSRect) frame
{
    if (self = [super initWithFrame: frame]) {
         int i;
         // alternate them on and off, for initial development
         for (i = 0; i < PEG_COUNT; i++) {
             pegStates[i] = ((i % 2) == 0) ? YES : NO;
         }
 
         recalcPegPaths = YES;
         selectedPeg = CHPegBoard_NO_PEG;
         dragPeg = CHPegBoard_NO_PEG;
    }

    return (self);

} // initWIthFrame

...

- (void) setPegBoard: (CHPegBoard *) newPegBoard
{
    [newPegBoard retain];
    [pegBoard release];

    pegBoard = newPegBoard;

    int i;
    // alternate them on and off, for initial development
    for (i = 0; i < [pegBoard spaceCount]; i++) {
        pegStates[i] = ((i % 2) == 0) ? YES : NO;
    }
    
    recalcPegPaths = YES;
    selectedPeg = CHPegBoard_NO_PEG;
    dragPeg = CHPegBoard_NO_PEG;

    [self setNeedsDisplay: YES];

} // setPegBoard

The background drawing now uses the path from the pegBoard. An affine transform is made to shift the path to the origin of the destination rectangle, and to scale it by the rectangle's size. Since the path provided by the CHPegBoad is situated in a unit square, this works out rather nicely. A bezier path is just a collection of points, this transform is a fast operation.
- (void) drawBoardInRect: (NSRect) rect
{
    // draw the base board, using a path of a triangle
    // since not an alloc or a copy, this can be considered to be
    // autoreleased and we don't have to worry about it being cleaned up

    NSBezierPath *path = [NSBezierPath bezierPath];

    // move to the bottom-left
    [path moveToPoint: rect.origin];

    // line to the top-middle
    NSPoint point;
    point.x = rect.origin.x + rect.size.width / 2.0;
    point.y = rect.origin.y + rect.size.height;
    [path lineToPoint: point];

    // line to the bottom-right
    point.x = rect.origin.x + rect.size.width;
    point.y = rect.origin.y;
    [path lineToPoint: point];

    // and close the path by adding a line back to the bottom left
    [path closePath];

    NSBezierPath *path = [pegBoard pathForBoard];

    NSAffineTransform *transform = [NSAffineTransform transform];
    [transform translateXBy: rect.origin.x  yBy: rect.origin.y];
    [transform scaleXBy: rect.size.width  yBy: rect.size.height];
    
    [path transformUsingAffineTransform: transform];

    [[NSColor brownColor] set];
    [path fill];

    [[NSColor blackColor] set];
    [path stroke];

} // drawBoardInRect

recalcPegPathsForRect: underwent some surgery, replacing the PEG_COUNT constant and parameterizing some of the calculations.
- (void) recalcPegPathsForRect: (NSRect) squareRect
{
    // first make all the rects flush-left

    // these magic constants were determined via tweaking and fiddling

    float pegRadius = squareRect.size.width / 8;
    float pad = (squareRect.size.width - (pegRadius * 5)) / 7.0;
    float rowHeight = squareRect.size.height / 6.0;
    float pegRadius = squareRect.size.width / ([pegBoard rowCount] + 3) ;
    float pad = (squareRect.size.width - (pegRadius * [pegBoard maxRowSize])) / ([pegBoard rowCount] + 3);
    float rowHeight = squareRect.size.height / ([pegBoard rowCount] + 1);
    float x, y;

    // union rectangle for each row
    NSRect pegRects[PEG_COUNT];
    NSRect unions[5];
    NSRect pegRects[[pegBoard spaceCount]];
    NSRect unions[50];

    int i = 0; // which peg we're looking at

    float baseY;
    baseY = ((squareRect.size.height) - ([pegBoard rowCount] * pegRadius + ([pegBoard rowCount] - 1) * pad)) / 2;

    int row;
    for (row = 0; row < 5; row++) {
    for (row = 0; row < [pegBoard rowCount]; row++) {
        y = pad + rowHeight * row;
        y = baseY + pad + rowHeight * row;
        x = 0;

        // do the pegs of the row.
        int peg;
        for (peg = 0; peg < 5 - row; peg++) {
        for (peg = 0; peg < [pegBoard pegsForRow: row]; peg++) {

            pegRects[i] = NSMakeRect (x, y, pegRadius, pegRadius);
            x += pegRadius + pad;
            
            if (peg == 0) {
                unions[row] = pegRects[i];
            } else {
                unions[row] = NSUnionRect (unions[row], pegRects[i]);
            }
            
            i++;
        }
    }
    
    // now center each rect in our useful area
    
    NSRect centerSquare = [self makeCenterSquareRect];
    i = 0;
    
    for (peg = 0; peg < 5 - row; peg++) {
    for (peg = 0; peg < [pegBoard pegsForRow: row]; peg++) {

        NSRect rect = [self centerRect: unions[row]  inRect: centerSquare];
        
        // now update the x values for the row, and update the y
        // for the start of the center rect
        int peg;
        for (peg = 0; peg < 5 - row; peg++) {
        for (peg = 0; peg < [pegBoard pegsForRow: row]; peg++) {
            pegRects[i].origin.x += rect.origin.x + centerSquare.origin.x;
            pegRects[i].origin.y += centerSquare.origin.y;
            i++;
        }
    }

    // make the bezier path for each peg
    for (i = 0; i < PEG_COUNT; i++) {
    for (i = 0; i < [pegBoard spaceCount]; i++) {
        [pegPaths[i] release];
        pegPaths[i] = [NSBezierPath bezierPathWithOvalInRect: pegRects[i]];
        [pegPaths[i] retain];
    }

} // recalcPegPathsForRect

And a general search-and-replace was done, changing PEG_COUNT to [pegBoard spaceCount]. I'll spare you the tedious details.

And that's it for this batch of changes. The general architecture is evolving to something better and more general. The quality of the drawing has regressed a little. The layout in the triangle is not as good as it was before, and the spacing between rows in the cross board isn't quite right. And the hard-coded values are pretty hackish. I still haven't figure out yet where I want to put the layout of the pegs. Putting it into CHPegBoard probably makes sense, but I'm concerned over possible duplication or diffusion of the layout code. But that's something for my subconcious to chew on while moving on to other stuff.


webmonster@cocoaheads.org