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.
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
.
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
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
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
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
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
= 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:
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
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
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 developmentfor (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
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 upNSBezierPath *path = [NSBezierPath bezierPath];// move to the bottom-left[path moveToPoint: rect.origin];// line to the top-middleNSPoint 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-rightpoint.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 fiddlingfloat 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 rowNSRect 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 pegfor (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
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.