Peg Solitaire Narrative - Basic Drawing

Peg Solitaire Narrative

The second version of Peg Solitaire adds drawing of the pegboard and pegs. I haven't figured out the storage of the Pegs themselves (there's not a whole lot of state to make a Peg class worthwhile yet), so right now the 15 pegs and their locations are stored as arrays in the CHPegBoard object:


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

#define PEG_COUNT 15

@interface CHPegBoard : NSView
{
    BOOL pegStates[PEG_COUNT];
    NSRect pegRects[PEG_COUNT];
}

@end // CHPegBoard

In the pegStates array, YES means the location has a peg, NO means it is empty. pegRects holds the rectangle for each peg.

Drawing

The pegboard is drawn a triangle in the middle of the view. A square that fits in the view is what controls the size of the triangle. (This square is currently drawn in blue to show where it is and how it reacts to window resizing. It'll go away in a More Real version). The first two methods of CHPegBoard.m initialize the object (setting the peg on and off values set) and some utility code for centering a rectangle inside another once (which will come in handy later when we lay out the pegs)
CHPegBoard.m
#import "CHPegBoard.h"

@implementation CHPegBoard

// designated initializer for our superclass.
// set up the pegStates (on/off) array here, since this is still
// 'scaffolding to get stuff working' mode

- (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;
        }
    }

    return (self);

} // initWIthFrame


// utility method that ideally should be in some common library
// or provided by Apple.
// Given two rectangles, center one inside of the other

- (NSRect) centerRect: (NSRect) smallRect
               inRect: (NSRect) bigRect
{
    NSRect centerRect;
    centerRect.size = smallRect.size;

    centerRect.origin.x = (bigRect.size.width - smallRect.size.width) / 2.0;
    centerRect.origin.y = (bigRect.size.height - smallRect.size.height) / 2.0;

    return (centerRect);

} // centerRect

This next method takes the bounds of the view and constructs a square in the middle.
// create a maximal-sized square that fits inside of our view.

- (NSRect) makeCenterSquareRect
{
    // our bounding rectangle
    NSRect bounds = [self bounds];

    // figure out whether the width or height is smaller.  That's our
    // controlling dimension
    float minDimension;
    minDimension = MIN (bounds.size.width, bounds.size.height);

    // make a square the proper size.  This has an origin of zero, but
    // can be based anywhere since [self centerRect] will change the 
    // x and y
    NSRect centerSquare = NSMakeRect (0.0, 0.0, minDimension, minDimension);

    // and now center it in our bounds
    centerSquare = [self centerRect: centerSquare
                         inRect: bounds];

    // and skootch it in to make it not coincident with the view border.
    // makes it look a little nicer
    centerSquare = NSInsetRect (centerSquare, 5, 5);

    return (centerSquare);
    
} // makeCenterSquareRect

Peg Layout

The laying out of the pegs took the longest time to do. My first attempts ended up looking like trash. I stole Jeff's implementation from the classic version, and had problems adapting it to the flipped coordinate system and the very flexible pegboard location and size. I finally came up with this algorithm.

Setting up horizontal rows of pegs was actually pretty easy. Calculating the vertical locations is easy, and distributing pegs evenly across in a row is really easy. Getting the nice offset-stairstep effect of the peg solitaire board is harder to get to look good.

So, the pegs have their rectangles calculated like a right-triangle:

Then the union of each row was calculated:

And then these rows are centered.


So here is the layout code. There are some hard-coded magic numbers that got tweaked to get something that looked good, but there's a Fundamental Algorithm that's struggling to get out, which hopefully will become clearer once it gets used some more. I think that this layout method will work for the "Line O' Pegs" and "Cross" patterns for other peg solitaire boards.
CHPegBoard.m
- (void) recalcPegRectsForRect: (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 x, y;

    // union rectangle for each row
    NSRect unions[5];

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

    int row;
    for (row = 0; row < 5; row++) {
        y = pad + rowHeight * row;
        x = 0;

        // do the pegs of the row.
        int peg;
        for (peg = 0; peg < 5 - 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 (row = 0; row < 5; row++) {
        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++) {
            pegRects[i].origin.x += rect.origin.x + centerSquare.origin.x;
            pegRects[i].origin.y += centerSquare.origin.y;
            i++;
        }
    }

} // recalcPegRectsForRect

Drawing an individual peg is pretty simple right now. Make a circular Bezier path the size of the given peg (calcualted by the above method), then fill it in white for an empty spot or black for a filled spot. Then stroke it in black to outline the white peg. Eventually we can use images or shaders or whatever to make it look a little more interesting.
// given a peg index, draw it.

- (void) drawPeg: (int) i
{
    assert (i >= 0 && i < PEG_COUNT);

    // right now, just make 'on' pegs black, and 'off' pegs white
    // eventually we can do much snazzier pegs

    NSRect pegRect = pegRects[i];

    if (pegStates[i]) {
        [[NSColor blackColor] set];
    } else {
        [[NSColor whiteColor] set];
    }
    
    NSBezierPath *path;
    path = [NSBezierPath bezierPathWithOvalInRect: pegRect];

    [path fill];

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

} // drawPeg

To draw the board, make a triangular bezier path that fills a given rectangle (which will be our middle-square). This path is filled with brown (cheezy wood effect unitl something better comes along) and framed with black.
- (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];

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

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

} // drawBoardInRect

And finally is the drawRect: method. It keeps the background drawing from the previous version. Then it calculates the center-most rectangle and stroked so we can see what it does. (it'll get taken out eventually). The board is then drawn and the pegs drawn on top of it.
- (void) drawRect: (NSRect) rect
{
    // get the bounds, since the rect might be a smaller area to redraw
    NSRect bounds = [self bounds];

    // make a nice white background to draw on
    [[NSColor whiteColor] set];
    [NSBezierPath fillRect: bounds];

    // this area is where the triangular pegboard will go
    NSRect squareRect = [self makeCenterSquareRect];

    // pretty heavy-handed doing this for each re-draw.
    // should just look for resizes to happen
    [self recalcPegRectsForRect: squareRect];

    // draw it in blue so we can see the rectangle we're using.
    // this will go away in the final version
    [[NSColor blueColor] set];
    [NSBezierPath strokeRect: squareRect];

    // draw the board
    [self drawBoardInRect: squareRect];

    // draw the pegs on top of the board
    int i;
    for (i = 0; i < PEG_COUNT; i++) {
        [self drawPeg: i];
    }

    [[NSColor blackColor] set];
    [NSBezierPath strokeRect: bounds];

} // drawRect


webmonster@cocoaheads.org