Sign in Join
Wiki copytemporary

Peg Solitaire tutorial

English · Suomi

This tutorial is the second Lazarus tutorial that aims at introducing the basics of Lazarus application development. It's best to start this tutorial after having finished the first one (Howdy World (Hello World on steroids)). This tutorial exlpains a bit about how to work with graphics and how to make a program modular. The final product of this tutorial is a basic but working version of the Peg Solitaire game (1). If all goes well in the end it will look something like this:

Start the project

As mentioned in the previous tutorial it's best to start with a clean, separate directory for each project. A quick recap:

And extra for this project:

First steps

It's always a good idea to seperate gui related code from data structure definitions. So the first step will be the creation of a separate unit for our Solitaire data structures.

The basic elements of a Peg Solitaire board are the marbles, the board structure and the empy places. We'll simulate this by a simple matrix that has cells of a certain type (empty, occupied and not accessible). And we'll encapsulate all this in a class that handles all the data manipulation.

pascal
const
  C_MAX = 7;  // Max board size: 7x7

type
  TCellNums = 1..C_MAX;
  TCellType = (ctNoAccess, ctEmpty, ctPeg);
  TPegCells = array[TCellNums, TCellNums] of TCellType;

  TPegSolitaire = class
  private
    Size: TCellNums;
    PegCells: TPegCells;
    
  public
    constructor Create(const pSize: TCellNums);
  end;

It's fair to assume that other code that is going to use this class needs access to the cells contents (i.e. PegCells). The way to handle this is either by defining a set of functions to access the cells or define a so called array property. Let's go for the latter approach and add the following line to the public section of the TPegSolitaire class:

pascal
property Cell[const pRow, pCol: TCellNums]: TCellType;
pascal
result := PegCells[pRow,pCol]
pascal
PegCells[pRow,pCol] := pValue
pascal
var iRow,iCol: integer;
begin
  // Store the size of the board locally
  Size := pSize;

  // Initialize all cells to 'not accessible'
  for iRow := 1 to C_MAX do
    for iCol := 1 to C_MAX do
      Cell[iRow,iCol] := ctNoAccess;

Now that the basic data structure is in place, let's have a look at the graphics we are going to need. There are many ways to display a solitaire board. We are going to use a paintbox. That will give us full control over the graphic features that Lazarus gives us out of the box.

Our main form is going to use the data structure we defined in the PegDatastructure file.

pascal
uses
  PegDatastructures,
  Classes, SysUtils, FileUtil, Forms, Controls, Graphics, Dialogs;

Next step is to draw the cells matrix on this paintbox by splitting it in rows and columns that will contain each cell of the board. To make it scaleable we'll calculate the width and height independantly. We'll need a couple of variables to hold the results. First the width and height of the cells, so all cells (7) fit exactly on the form. We'll develop the painting code interactively and we are going to use the button that was dropped on the form for that.

pascal
var
  CellWidth : integer;
  CellHeight: integer;

We'll need some extra variables to hold intermediate results:

pascal
  iRow, iCol: TCellNums;
  CellArea  : TRect;

CellArea is used to limit the rectangular area on screen where a cell will be drawn.

To process all rows and cols two straightforward for loops will do.

pascal
  // Calculate the width/height of each cell to accomodate for all cells in the paintbox
  CellWidth := pbPeg.Width div 7;
  CellHeight := pbPeg.Height div 7;

  // Draw boxes for all cells
  for iRow := 1 to 7 do
    for iCol := 1 to 7 do
    begin
      // Calculate the position of the cell in the paintbox
      CellArea.Top    := (iRow-1) * CellHeight;
      CellArea.Left   := (iCol-1) * CellWidth;
      CellArea.Right  := CellArea.Left + CellWidth;
      CellArea.Bottom := CellArea.Top  + CellHeight;
      // And now draw the cell
      pbPeg.Canvas.Rectangle(CellArea);
    end;

A Canvas, as it's name suggests, is a control that helps us drawing things like lines, rectangles, circles etc. A paintbox control has an embedded canvas. Therefore the line pbPeg.Canvas.Rectangle(CellArea) will draw a rectangle on the paintbox, limited to the area defined in CellArea. And because the paintbox is placed on the form, we can see the result there.

This proves that our calculations were spot on and that we now have the means to draw whatever is necessary in the right spot. One thing needs to be done though before adding more drawing functionality. Drawing of the cells has no relation with the main form whatsoever (or any form for that matter). The only thing we need for drawing things is a Canvas and some measurements for the cells. So we are going to create a supporting class to clean up the main form.

pascal
type
  TPegSolPainter = class
  private
    PegSol      : TPegSolitaire;
    Canvas      : TCanvas;

  public
    constructor Create(pPegSol: TPegSolitaire; pCanvas: TCanvas);
  end;

Note that a TPegSolitaire variable is also added, because obviously we are going to use that class to retrieve a cells' state.

pascal
constructor TPegSolPainter.Create(pPegSol: TPegSolitaire; pCanvas: TCanvas);
begin
  PegSol := pPegSol;
  Canvas := pCanvas;
end;

Trying to compile this code will failse because we haven't added the PegDatastrucures unit to the uses section. And because we use a TCanvas we'll have to add the Graphics unit as well.

pascal
uses
  PegDatastructures,
  Graphics,
  Classes, SysUtils;

The reason we built this class was to remove all drawing and painting code from the form. So we need to create a method that will do the drawing. Before adding that method there is something that needs to be addressed: to calculate the width of a cell, we divide the paintbox width by the number of cells. In theory we could use the property Canvas.Width for this. However that property does not always give us the right width at the right time. So to be able to draw cells, we must provide our draw method with the correct values for the width and height of the canvas.

Now we know this, we can add a paint method to our class.

pascal
  TPegSolPainter = class
  private
    PegSol      : TPegSolitaire;
    Canvas      : TCanvas;

  public
    constructor Create(pPegSol: TPegSolitaire; pCanvas: TCanvas);
    procedure Repaint(const pCanvasWidth, pCanvasHeight: integer);
  end;
pascal
procedure TPegSolPainter.Repaint(const pCanvasWidth, pCanvasHeight: integer);
var
  CellWidth    : integer;
  CellHeight   : integer;
  iRow, iCol   : TCellNums;
  CellArea     : TRect;
begin
  // Calculate the width of each cell to accomodate for all cells
  CellWidth := pbPeg.Width div 7;
  CellHeight := pbPeg.Height div 7;

  // Draw boxes for all cells
  for iRow := 1 to 7 do
    for iCol := 1 to 7 do
    begin
      // Calculate the position of the cell in the paintbox
      CellArea.Top    := (iRow-1) * CellHeight;
      CellArea.Left   := (iCol-1) * CellWidth;
      CellArea.Right  := CellArea.Left + CellWidth;
      CellArea.Bottom := CellArea.Top  + CellHeight;
      // And now draw the cell
      pbPeg.Canvas.Rectangle(CellArea);
    end;
end;

Because we no longer need the paintbox pbPeg we must remove the references to it. Three changes are needed:

pascal
  // Calculate the width of each cell to accomodate for all cells
  CellWidth := pCanvasWidth div 7;
  CellHeight := pCanvasHeight div 7;
pascal
      // And now draw the cell
      Canvas.Rectangle(CellArea);

Now that we have a class that can do the fancy painting for us, it's time to make use of it. We are going to use this paint class together with the PegSolitare class to draw the cells.

pascal
procedure TfrmMain.Button1Click(Sender: TObject);
var
  pegsol  : TPegSolitaire;  // The game data
  pegpaint: TPegSolPainter; // The paint class for the game
begin
end;

To use the paint class is fairley straightforward: create a new instance and call the repaint method like so:

pascal
  // Create a new game object
  pegsol := TPegSolitaire.Create(7);

  // Create a new painter object to paint this game
  pegpaint := TPegSolPainter.Create(pegsol, pbPeg.Canvas);

  // And paint the board
  pegpaint.Repaint(pbPeg.Width, pbPeg.Height);

  // Clean up
  pegpaint.Free;
  pegsol.Free

Run and test the program to see that it works exactly as before. The result will look something like this:

It's all about events

What have we accomplished so far? There is a datastructure that holds all the data for a Peg Solitaire game, there is a class that can paint on a Canvas and we have a fairly simple form with a test button. So what's next? Events!

As we have seen in the previous section, the cell matrix we painstakingly drew wasn't to last. This happens because the form doesn't know anything about our little game. As soon as the form thinks it's time to redraw itself, it does so and ignores our cell grid. What it dóes do is send a message to it's child controls that a refresh is necessary. This message is available to us as an event: a paint event.

This event is 'fired' everytime the paintbox needs to redraw it's surface. That is the place were we are going to do our drawing.

pascal
procedure TfrmMain.pbPegPaint(Sender: TObject);
var
  pegsol  : TPegSolitaire;  // The game data
  pegpaint: TPegSolPainter; // The paint class for the game
begin
  // Create a new game object
  pegsol := TPegSolitaire.Create(7);

  // Create a new painter object to paint this game
  pegpaint := TPegSolPainter.Create(pegsol, pbPeg.Canvas);

  // And paint the board
  pegpaint.Repaint(pbPeg.Width, pbPeg.Height);

  // Clean up
  pegpaint.Free;
  pegsol.Free
end;

Now what is clear is that we create a game object in the OnPaint method, paint the empty cells and then destroy it. But we need to store the game object until a game has finished. The same goes for the paint object. So the OnPaint event is not the most logical place to create these objects. The form's class declaration is a better place to store them.

pascal
  TfrmMain = class(TForm)
    pbPeg: TPaintBox;
    procedure pbPegPaint(Sender: TObject);
  private
    { private declarations }
    pegsol  : TPegSolitaire;  // The game data
    pegpaint: TPegSolPainter; // The paint class for the game
  public
    { public declarations }
  end;

These variables must be initialized as soon as the form opens (or anytime we want to start a new game). So let's create a procedure that does that for us and add it to the private section of the form.

pascal
  private
    { private declarations }
    pegsol  : TPegSolitaire;  // The game data
    pegpaint: TPegSolPainter; // The paint class for the game

    procedure StartNewGame;
pascal
procedure TfrmMain.StartNewGame;
begin
  // Clean up the previous game
  pegpaint.Free;
  pegsol.Free;

  // Start with a new game
  pegsol := TPegSolitaire.Create(7);
  pegpaint := TPegSolPainter.Create(pegsol, pbPeg.Canvas);
end;

Now that the initialization code is created, it must be executed. A logical time to do this is as soon as the form is created (i.e. the application is started). For this another event is available to us: the FormCreate event. We can create it in two ways: find the OnFormCreate event in the Object Inspector and click on the '...' button. But another way to generate it is double clicking the form itself.

pascal
procedure TfrmMain.FormCreate(Sender: TObject);
begin
  StartNewGame
end;

Now that the game and paint objects are created at program start, they are no longer required in the OnPaint method.

pascal
procedure TfrmMain.pbPegPaint(Sender: TObject);
begin
  // Paint the board
  pegpaint.Repaint(pbPeg.Width, pbPeg.Height);
end;

Intermezzo

So far we have focused on building a program to play the classic solitaire game in the 7x7 grid. Is it now possible to create smaller or bigger grids for other type games? Let's test this.

As we can see on screen we still see a 7x7 matrix! This is the result of using magic numbers, which we should have avoided (see http://en.wikipedia.org/wiki/Magic_number_%28programming%29#Unnamed_numerical_constants).

Using magic numbers equals disaster: it's a matter of when, not if a program will fail.

In there we see the number of 7 a couple of times. This the magic number that plays tricks on us. In the calculation of the cell width and height, we need the size of the board as stored in the pegsol game variable. And the same goes for the iRow and iCol loops. So let's fix this once and for all:

pascal
  // Calculate the width of each cell to accomodate for all cells
  CellWidth := pCanvasWidth div pegsol.Size;
  CellHeight := pCanvasHeight div pegsol.Size;

  // Draw boxes for all cells
  for iRow := 1 to pegsol.Size do
    for iCol := 1 to pegsol.Size do
    begin
      // Calculate the position of the cell in the paintbox
      CellArea.Top    := (iRow-1) * CellHeight;
      CellArea.Left   := (iCol-1) * CellWidth;
      CellArea.Right  := CellArea.Left + CellWidth;
      CellArea.Bottom := CellArea.Top  + CellHeight;
      // And now draw the cell
      Canvas.Rectangle(CellArea);
    end;

There's a caveat here: pegsol doesn't have a publicly accessible Size variable. And this is how it should be: all variables in a class should be private. The way to access those private values is via functions or properties (the interface of the class). For this simple value we will use a property.

The class will now look like this:

pascal
  TPegSolitaire = class
  private
    FSize: TCellNums;
    PegCells: TPegCells;
    function GetCell(const pRow, pCol: TCellNums): TCellType;
    procedure SetCell(const pRow, pCol: TCellNums; const pValue: TCellType);

  public
    constructor Create(const pSize: TCellNums);
    property Cell[const pRow, pCol: TCellNums]: TCellType read GetCell write SetCell;
    property Size: TCellNums read FSize;
  end;

It's common practice to prefix variables that are accessed via properties with a letter F. We've made the public access to the size property read-only, because it should never be changed, once the game is started. The only place where the private variable should get it's final value is in the Create constructor.

pascal
constructor TPegSolitaire.Create(const pSize: TCellNums);
var iRow,iCol: integer;
begin
  FSize := pSize;
  for iRow := 1 to Size do
    for iCol := 1 to Size do
      Cell[iRow,iCol] := ctNoAccess;
end;

We are now ready to test the program and see if it now draws a nice 5x5 matrix.

Now that the basics of the game are in place, it's time to flesh out the gui.

Let's get artistic

So far so good. We now have a game class, a supporting paint class and an unimpressive checker board on our form (well, sort of). In our solitaire game a cell can have 3 states: not accessible, empty or occupied. For each cell type we want a different graphic representation. Let's get to it.

Obviously we need to make a change here. It depends on the cell state what needs to be drawn (not accessible cell, empty cell or peg cell). The case... operation comes to the rescue.

pascal
  // Draw boxes for all cells
  for iRow := 1 to pegsol.Size do
    for iCol := 1 to pegsol.Size do
    begin
      // Calculate the position of the cell in the paintbox
      CellArea.Top    := (iRow-1) * CellHeight;
      CellArea.Left   := (iCol-1) * CellWidth;
      CellArea.Right  := CellArea.Left + CellWidth;
      CellArea.Bottom := CellArea.Top  + CellHeight;

      // And now draw the cell based on the cell's contents
      case pegsol.Cell[iRow,iCol] of

        ctNoAccess: // Draw cells that are not accessible
          begin
            Canvas.Brush.Color := clGray;
            Canvas.Rectangle(CellArea);
          end;

        ctEmpty:    // Draw cells that are currently empty
          begin
            Canvas.Brush.Color := clBlue;
            Canvas.Rectangle(CellArea);
          end;

        ctPeg:      // Draw cells that are occupied
          begin
            Canvas.Brush.Color := clBlue;
            Canvas.Rectangle(CellArea);    // Erase the background first
            Canvas.Brush.Color := clGreen; 
            Canvas.Ellipse(CellArea);      // Draw the pegs as green circles
          end;
      end;
    end;

We could run the program at this stage (just try it), but it will only display a boring 5x5 grayish grid. This is because we didn't define any game setup yet. Let's fix this first.

pascal
procedure TfrmMain.StartNewGame;
begin
  // Clean up the previous game
  pegpaint.Free;
  pegsol.Free;

  // Start with a new 7x7 game
  pegsol := TPegSolitaire.Create(7);
  pegpaint := TPegSolPainter.Create(pegsol, pbPeg.Canvas);

  // Initialize some cells
  pegsol.Cell[3,4] := ctEmpty;
  pegsol.Cell[4,2] := ctPeg;
  pegsol.Cell[4,3] := ctPeg;
  pegsol.Cell[4,4] := ctEmpty;
  pegsol.Cell[4,5] := ctPeg;
  pegsol.Cell[4,6] := ctPeg;
  pegsol.Cell[5,4] := ctEmpty;
end;

Populate the board

As we know a classic solitaire board should look something like this:

And if we were to populate all cells individually, that would result in a lot of code. What if we could intialize the game by just passing it some text that would symbolically describe the board? Something like this:

pascal
  // Initialize the cells to the classic game
  pegsol.InitializeBoard( '  ooo  ' + LineEnding +
                          '  ooo  ' + LineEnding +
                          'ooooooo' + LineEnding +
                          'ooo.ooo' + LineEnding +
                          'ooooooo' + LineEnding +
                          '  ooo  ' + LineEnding +
                          '  ooo  ' );
  1. o is an occupied cell.
  2. . is an empty but playable cell.
  3. The spaces indicate cells that are not accessible.

Let's assume this is going to work and create a method in the TPegSelitaire class that can handle this.

pascal
procedure TfrmMain.StartNewGame;
begin
  // Clean up the previous game
  pegpaint.Free;
  pegsol.Free;

  // Start with a new 7x7 game
  pegsol := TPegSolitaire.Create(7);
  pegpaint := TPegSolPainter.Create(pegsol, pbPeg.Canvas);

  // Initialize the cells to the classic game
  pegsol.InitializeBoard( '  ooo  ' + LineEnding +
                          '  ooo  ' + LineEnding +
                          'ooooooo' + LineEnding +
                          'ooo.ooo' + LineEnding +
                          'ooooooo' + LineEnding +
                          '  ooo  ' + LineEnding +
                          '  ooo  ' );
end;
pascal
  public
    constructor Create(const pSize: TCellNums);
    procedure InitializeBoard(const pBoard: ansistring);

    property Cell[const pRow, pCol: TCellNums]: TCellType read GetCell write SetCell;
    property Size: TCellNums read FSize;

Now what does this procedure actually need to do? It must split the textstring into seperate lines and then process those lines, Because we used LineEnding tokens to separate the lines, we can use a TStringList class to split them.

pascal
procedure TPegSolitaire.InitializeBoard(const pBoard: ansistring);
var lst      : TStringList;
    iRow,iCol: integer;
    s        : string;
begin
  // Create a list with the board text in it. This will split all lines
  // into individual lines, because of the LineEnding 'splitter'.
  lst := TStringList.Create;
  lst.Text := pBoard;

  // Process all lines one at a time
  for iRow := 0 to lst.Count-1 do
    if iRow < Size then // Make sure there is no overflow in the rows
    begin
      // Process a single line of text
      s := lst[iRow];
      for iCol := 1 to length(s) do
        if iCol <= Size then  // Make sure there is no overflow in the columns
          case s[iCol] of
            ' ': Cell[iRow+1,iCol] := ctNoAccess;
            '.': Cell[iRow+1,iCol] := ctEmpty;
            'o': Cell[iRow+1,iCol] := ctPeg;
          end;
    end;

  // Clean up the list
  lst.Free;
end;
  1. a TStringList is used as a buffer. This works because we used LineEnding as a separator between all lines.
  2. There are Count number of lines, but they are numbered 0..Count-1. Our cells are numbered starting with 1. That's why you see iRow+1 in the cell assignments.

The above procedure contains a lot of extra variables and shouldn't be that difficult to understand. It's possible to reduce the procedure to the bare minimum like so:

pascal
procedure TPegSolitaire.InitializeBoard(const pBoard: ansistring);
var iRow,iCol: integer;
begin
  with TStringList.Create do
  begin
    Text := pBoard;
    for iRow := 0 to Min(Count-1, Size-1) do
      for iCol := 1 to Min(length(Strings[iRow]),Size) do
        case Strings[iRow][iCol] of
          ' ': Cell[iRow+1,iCol] := ctNoAccess;
          '.': Cell[iRow+1,iCol] := ctEmpty;
          'o': Cell[iRow+1,iCol] := ctPeg;
        end;
    Free;
  end;
end;

This procedure does exactly the same but it uses the handy feature that you can use the With statement together with dynamically created objects. For this procedure to work add the Math unit to the uses section.

Events revisited

It doesn't look too impressive yet, but we have done quite a lot. The main thing missing at the moment is being able to leap cells to remove them from the board. And for that we are going to use the mouse. But how? Let's investigate the possibilities.

There are 2 events that are interesting to us: OnMouseDown and OnMouseUp. Let's see what happens whith the OnMouseDown event.

The header of the generated procedure contains a number of variables. Two of them are X and Y. They contain the position on the paintbox where the mouse button was pressed. Let's make them visible by adding some information to the memo component we've placed on the form.

pascal
Memo1.Append( format('Mouse down @ %dx%d',[X,Y]) )

Obviously we cannot use the 'raw' X and Y coordinates; they are too high for our 7x7 game board. They must be translated to the cell's row and column coordinates. For that we need a function that translates a X/Y coordinate to a cell coordinate. For that we need the width and height of the paintbox, the number of cells in the grid and the width/height of an individual cell. The one place where all this comes together is... the paint class. So that is the obvious place to do this calculation.

pascal
type
  TCellNums = 1..C_MAX;
  TCellType = (ctNoAccess, ctEmpty, ctPeg);
  TPegCells = array[TCellNums, TCellNums] of TCellType;

  TCellPosition = record
    Row: TCellNums;
    Col: TCellNums;
  end;
pascal
  public
    constructor Create(pPegSol: TPegSolitaire; pCanvas: TCanvas);

    procedure Repaint(const pCanvasWidth, pCanvasHeight: integer);
    function CanvasXYtoCell(const pX, pY: integer): TCellPosition;
pascal
function TPegSolPainter.CanvasXYtoCell(const pX, pY: integer): TCellPosition;
begin
  result.Col := (pX div CellWidth)  + 1;
  result.Row := (pY div CellHeight) + 1;
end;

The above code maps the large X/Y coordinate on the actual cell's row/col numbers. There is one problem though: the CellWidth and CellHeight are not available. They are intermediate results in the Repaint procedure and not stored. Let's fix that as well.

pascal
  TPegSolPainter = class
  private
    PegSol      : TPegSolitaire;
    Canvas      : TCanvas;
    CellWidth   : integer;
    CellHeight  : integer;
pascal
procedure TPegSolPainter.Repaint(const pCanvasWidth, pCanvasHeight: integer);
var
  iRow, iCol   : TCellNums;
  CellArea     : TRect;
begin

Now we can modify the procedure where the mouse down coordinate was processed. With the new function the X/Y position can now be translated to an actuel cell coordinate.

pascal
procedure TfrmMain.pbPegMouseDown(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
var CellRC: TCellPosition;
begin
  CellRC := pegpaint.CanvasXYtoCell(X,Y);
  Memo1.Append( format('Mouse down @ %dx%d',[CellRC.Row, CellRC.Col]) )
end;

Let's do the same for the OnMouseUp event:

pascal
procedure TfrmMain.pbPegMouseUp(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
begin
  with pegpaint.CanvasXYtoCell(X,Y) do
    Memo1.Append( format('Mouse up @ %dx%d',[Row, Col]) )
end;

Note that this method does exactly the same as the OnMouseDown, but using the with construct saves us the trouble of declaring a local variable (less is more...).

Let's test this:

Every time you press the mouse button and release it, the exact cell where the mouse button was pressed or released is displayed in the memo box.

Gluing things together

This program works a bit like the calculator program:

  1. The user selects a peg to move by pressing the mouse button.
  2. The user moves the peg over another peg to an empty cell (leaps).
  3. The user release the mouse button to execute the leap.

Here we do not remember a number but a cell (-position). So as soon as the user presses the mouse button on a cell, we must store that position somewhere.

pascal
  private
    { private declarations }
    pegsol  : TPegSolitaire;  // The game data
    pegpaint: TPegSolPainter; // The paint class for the game
    FromCell: TCellPosition;  // The peg that is going to leap
pascal
FromCell := pegpaint.CanvasXYtoCell(X,Y);

Now as soon as the user releases the mouse button, we must execute the leap (verify if it's ok and update the board). Let's be optimistic (remember the Top Down remark?) and assume there is a procedure that does that for us: Leap(<FromCell>, <ToCell>).

pascal
pegsol.Leap(FromCell, pegpaint.CanvasXYtoCell(X,Y));

FromCell we already populated when the mouse button was pressed. The target cell is simply calculated by calling pegpaint.CanvasXYToCell(...). We could introduce a local variable for that, but it's not really necessary.

pascal
pbPeg.Repaint;

This will force the paintbox to repaint itself, so all updated cells show the correct contents.

The complete OnMouseUp method now looks like this:

pascal
procedure TfrmMain.pbPegMouseUp(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
begin
  with pegpaint.CanvasXYtoCell(X,Y) do
    Memo1.Append( format('Mouse up @ %dx%d',[Row, Col]) );
  pegsol.Leap(FromCell, pegpaint.CanvasXYtoCell(X,Y));
  pbPeg.Repaint;
end;

Before going into the Leap procedure, a bit of extra functionality is introduced. Remember that we can retrieve the state of a cell (empty, peg or no access) via the Cell property of TPegSolitaire? For example pegsol.Cell[2,2] would give us the state of the cell in row 2 and column 2. But in the mean time we have introduced a new datastructure: TCellPosition. So for example if we want to get the state of the FromCell we would need the following awkward statement:

pascal
state := pegsol.Cell[ FromCell.Row, FromCell.Col ]

It would be nice if we could write something like:

pascal
state := pegsol.GetCell(FromCell)

Let's add this to our game class.

pascal
  TPegSolitaire = class
  private
    FSize: TCellNums;
    PegCells: TPegCells;

    function GetCell(const pRow, pCol: TCellNums): TCellType;
    procedure SetCell(const pRow, pCol: TCellNums; const pValue: TCellType);
    function GetCell(const pPosition: TCellPosition): TCellType;

There now are two functions with the name GetCell, but the parameter list is different. So Lazarus knows exactly when to call which function.

pascal
result := Cell[pPosition.Row, pPosition.Col]

Back to the main storyline. We have introduced a procedure Leap(...), but it doesn't exist yet. It is a method that changes the game board, so the place to put it is the TPegSolitaire class.

pascal
  public
    constructor Create(const pSize: TCellNums);
    procedure InitializeBoard(const pBoard: ansistring);
    procedure Leap(const pFromCell, pToCell: TCellPosition);

Now this procedure does all the hard work: check if the cells are valid and if the leap is at all possible. This is not difficult, but the number of checks is quite extensive for something that looks simple. The code is fairly self-explanatory and if not, hopefully the comments explain what is going on. Note that the procedure uses the GetCell(<cellposition>) function we created earlier.

pascal
procedure TPegSolitaire.Leap(const pFromCell, pToCell: TCellPosition);
var dx, dy: integer;
    JumpedCell: TCellPosition;
begin
  // Verify that the start cell is occupied and the target cell is empty
  // If not, leave the procedure via the EXIT.
  if (GetCell(pFromCell) <> ctPeg) or
     (GetCell(pToCell) <> ctEmpty) then
    EXIT;

  // Calculate the horizontal and vertical distance between the cells
  dx := abs(pFromCell.Col - pToCell.Col);
  dy := abs(pFromCell.Row - pToCell.Row);

  // A valid move has one direction equal to zero and the other equal to 2
  if    ((dx = 2) and (dy = 0))
     or ((dx = 0) and (dy = 2)) then
  begin
    // Determine the position of the jumped cell; it's in the middle
    JumpedCell.Col := (pFromCell.Col + pToCell.Col) div 2;
    JumpedCell.Row := (pFromCell.Row + pToCell.Row) div 2;

    // Final check: is there a peg in the jumped cell?
    if GetCell(JumpedCell) = ctPeg then
    begin
      // Jump: clear the FromCell, empty the jumped cell and populate the ToCell
      Cell[pFromCell.Row, pFromCell.Col]   := ctEmpty;
      Cell[JumpedCell.Row, JumpedCell.Col] := ctEmpty;
      Cell[pToCell.Row, pToCell.Col]       := ctPeg;
    end;
  end;
end;

Believe it or not but we now have a working Peg Solitaire game program!

Cleaning up

On the form we still have the memo component that displays mouse clicks. We don't really need it anymore.

pascal
procedure TfrmMain.pbPegMouseDown(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
begin
  FromCell := pegpaint.CanvasXYtoCell(X,Y);
end;

procedure TfrmMain.pbPegMouseUp(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
begin
  pegsol.Leap(FromCell, pegpaint.CanvasXYtoCell(X,Y) );
  pbPeg.Repaint;
end;

What's next?

To make it a fully functional game we need a way to change the board without recompiling the application. And the graphics could be improved as well. Let's address the first issue first.

Creating more board layouts

It would be nice if we could add more boards to our game without recompiling the application. The obvious place would be to store the board layouts in external textfiles. The user can then start a new game by opening one of the textfiles. The fun thing is, it's easy to add because we've already done most of the hard work. First let's make two board layouts.

pascal
  ooo
  ooo
ooooooo
ooo.ooo
ooooooo
  ooo
  ooo

Add another file:

pascal
  ...
  .o.
..ooo..
.ooooo.
ooooooo
  ...
  ...

There are many ways to create and open new boards. In this tutorial we'll use the menu driven approach (a thorough description of the menu is something for another tutorial).

The menu will now look something like this:

The menu is also visible at the top of the main form.

pascal
Close

This will close the main form. And because it is the only form of our application it will end the application.

Okay now it's time to look at how to open the board files. There are standard components and menu's available to do this, but let's do it the manual way.

So far so good. Now to actually open a file when the menu item is selected:

pascal
  if OpenDialog1.Execute then
    ShowMessage(OpenDialog1.FileName);

Remember: small steps. We have now added a menu to the form to exit the application and to load a game board set up from disk. We haven't actually coded the loading part, but let's first try the code we have so far.

Now let's finish the board loading code. The code is pretty self-explanatory and reuses some of the procedures we have created so far.

pascal
procedure TfrmMain.MenuItem4Click(Sender: TObject);
begin
  // Open the pop up dialog
  if OpenDialog1.Execute then
  begin
    // Start with a new empty board
    StartNewGame;

    // Dynamically create a stringlist to load the board layout
    with TStringList.Create do
    begin
      // Load the board layout from the textfile
      LoadFromFile(OpenDialog1.FileName);

      // Initialize the board with the file's contents
      pegsol.InitializeBoard(Text);

      // Clean up the stringlist
      Free
    end;

    // After loading the new board update the paintbox
    pbPeg.Repaint;
  end;
end;

And that's all there is to it!

Run the program, open a board set up file and play the game!

Eye candy

We now have a working Peg Solitaire application. There's still plenty of room for improvent, but the game itself is working nicely. The last part of this tutorial is about improving the looks of the game. Sure the game works, but a bit more detail in the graphics would be nice. And that's pretty easy to add. All we need are three images for the cell types, update the painter class and in the main form, tell the painter to use the images. So let's add the three images to the painter class:

pascal
  TPegSolPainter = class
  private
    PegSol     : TPegSolitaire;
    Canvas     : TCanvas;
    CellWidth  : integer;
    CellHeight : integer;

    ImgNoAccess: TPicture;
    ImgEmpty   : TPicture;
    ImgPeg     : TPicture;
pascal
  public
    constructor Create(pPegSol: TPegSolitaire; pCanvas: TCanvas);
    procedure Repaint(const pCanvasWidth, pCanvasHeight: integer);
    function CanvasXYtoCell(const pX, pY: integer): TCellPosition;
    procedure LoadImage(const pCellType: TCellType; const pFilename: string);
pascal
procedure TPegSolPainter.LoadImage(const pCellType: TCellType; const pFilename: string);

  procedure UpdateImage(var pImg: TPicture; const pNewImage: TPicture);
  begin
    pImg.Free;
    pImg := pNewImage
  end;

var pic: TPicture;
begin
  // Make sure the file exists
  if FileExists(pFilename) then
  begin
    // First load the picture in a local variable
    pic := TPicture.Create;
    pic.LoadFromFile(pFilename);

    // Now update the required image based on the pCelltype parm
    case pCelltype of
      ctNoAccess: UpdateImage(ImgNoAccess, pic);
      ctEmpty   : UpdateImage(ImgEmpty, pic);
      ctPeg     : UpdateImage(ImgPeg, pic);
    end;
  end;
end;

The code again is self-explanatory. Note that we use a short local procedure to update an image. If we wouldn't do that then the case statement (case pCellType of) would be more complex (i.e. contain more redundant code). Now each cell type only needs one line to update the right image.

We've added the pictures to the class. Now we must use them and that of course happens in the Repaint procedure. For each celltype we check whether an image is available. And if so, use that image instead of the plain drawing.

pascal
      // And now draw the cell based on the cell's contents
      case pegsol.Cell[iRow,iCol] of

        ctNoAccess: // Draw cells that are not accessible
          if not assigned(ImgNoAccess) then
            begin
              Canvas.Brush.Color := clGray;
              Canvas.Rectangle(CellArea);
            end
          else with ImgNoAccess do
            Canvas.CopyRect(CellArea, Bitmap.Canvas, Rect(0,0,Width,Height));

        ctEmpty:    // Draw cells that are currently empty
          if not assigned(ImgEmpty) then
            begin
              Canvas.Brush.Color := clBlue;
              Canvas.Rectangle(CellArea);
            end
          else with ImgEmpty do
            Canvas.CopyRect(CellArea, Bitmap.Canvas, Rect(0,0,Width,Height));

        ctPeg:      // Draw cells that are occupied
          if not assigned(ImgPeg) then
            begin
              Canvas.Brush.Color := clBlue;
              Canvas.Rectangle(CellArea);  // Erase the background first
              Canvas.Brush.Color := clGreen;
              Canvas.Ellipse(CellArea);
            end
          else with ImgPeg do
            Canvas.CopyRect(CellArea, Bitmap.Canvas, Rect(0,0,Width,Height));

      end;

In the main form locate the StartNewGame method and add the image loading code to the painter class.

pascal
  // Start with a new 7x7 game
  pegsol := TPegSolitaire.Create(7);
  pegpaint := TPegSolPainter.Create(pegsol, pbPeg.Canvas);

  pegpaint.LoadImage(ctNoAccess, 'tutpeg_cellforbidden.jpg');
  pegpaint.LoadImage(ctEmpty,    'tutpeg_cellempty.jpg');
  pegpaint.LoadImage(ctPeg,      'tutpeg_celloccupied.jpg');

Make sure that the above images are downloaded from below and put in the program's bin folder, or better yet, create your own images. Note: to download these images, click on them first thén download them.

Final tweaks

Most programs have their own icons that are displayed in menu's and in explorer windows. It's easy to add your own custom icon to an application.

The application will now have a nice marble icon in the form's title bar and in explorer windows.


From the Free Pascal wiki, page "Peg Solitaire tutorial", as it stood on 2023-12-04 after a change by Amerou, out of the wiki's dump of 2024-05-26. Copyright stays with the author who wrote it, according to the old wiki's Licensing page. If you have concerns about this material, message the admin.

Revision 2, last changed by WikiArchive .

Talk about this page

Nobody has opened a bench for this page yet. Sign in to open one.