Sign in Join
Wiki copytemporary

Peg Solitaire tutorial: what changed

Revision 1 to 2. Removed lines have a minus sign; added lines have a plus sign.

OldNewLine
11This 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](http://en.wikipedia.org/wiki/Peg_solitaire)). If all goes well in the end it will look something like this:
22
3-<figure>
4-<img src="tutpeg_solitaire.png" title="tutpeg_solitaire.png" />
5-<figcaption>tutpeg_solitaire.png</figcaption>
6-</figure>
3+![](/wiki-images/5/5f/Tutpeg_solitaire.png)
74
85## Start the project
96
107As mentioned in the previous tutorial it's best to start with a clean, separate directory for each project. A quick recap:
118
129- Create a new directory for this game.
1310- Start a new Application (Project/New Project... and select Application).
1411- Save the project as PegSolitaire.
1512- Save the main [[TForm|form]] as ufrmMain.
1613- In the object inspector change the form's name to *frmMain*.
1714- Change the caption to *Lazarus Peg Solitaire*.
1815- In the project options insert *bin\* in front of the target filename (or *bin/* for Linuxes).
1916
2017And extra for this project:
2118
22-- Open the project options dialog ( <span class="kbd">Shift</span>+<span class="kbd">Ctrl</span>+ <span class="kbd">F11</span>).
19+- Open the project options dialog ( `Shift`+`Ctrl`+ `F11`).
2320- Select Compiler *Options/Code Generation*.
2421- Enable Range checking and Overflow error checking (see image belows).
2522
26-<figure>
27-<img src="tutpeg_compiler_options.png" title="tutpeg_compiler_options.png" />
28-<figcaption>tutpeg_compiler_options.png</figcaption>
29-</figure>
23+![](/wiki-images/1/10/Tutpeg_compiler_options.png)
3024
3125## First steps
3226
3327It'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|unit]] for our Solitaire data structures.
3428
3529- From the menu choose *File/New Unit*.
3630- Save the unit as *PegDatastructures.pas* (and press the lowercase button that pops up).
3731
3832The 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|class]] that handles all the data manipulation.
3933
4034- Add the following code to the PegDatastructures unit (after the classes in the *[[Uses|uses]]* section and before the *[[Implementation|implementation]]* section):
4135
4236``` pascal
4337const
4438 C_MAX = 7; // Max board size: 7x7
4539
4640type
4741 TCellNums = 1..C_MAX;
4842 TCellType = (ctNoAccess, ctEmpty, ctPeg);
4943 TPegCells = array[TCellNums, TCellNums] of TCellType;
5044
5145 TPegSolitaire = class
5246 private
5347 Size: TCellNums;
5448 PegCells: TPegCells;
5549
5650 public
5751 constructor Create(const pSize: TCellNums);
5852 end;
5953```
6054
6155It'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 [[Function|functions]] to access the cells or define a so called [[Array|array]] property. Let's go for the latter approach and add the following line to the public section of the TPegSolitaire class:
6256
6357``` pascal
6458property Cell[const pRow, pCol: TCellNums]: TCellType;
6559```
6660
6761- Position the text cursor on the constructor line.
68-- Press <span class="kbd">Ctrl</span>+<span class="kbd">Shift</span>+<span class="kbd">C</span>: the IDE generates the constructor body (as we expected) but it also generates the empty bodies for the 2 [[Method|methods]] that give us access to *PegCells* via the *Cells* property.
62+- Press `Ctrl`+`Shift`+`C`: the IDE generates the constructor body (as we expected) but it also generates the empty bodies for the 2 [[Method|methods]] that give us access to *PegCells* via the *Cells* property.
6963- GetCell retrieves data from the private [[Variable|variable]] PegCells. Add the following code to the [[Function|function]]:
7064
7165``` pascal
7266result := PegCells[pRow,pCol]
7367```
7468
7569- SetCell populates the PegCells array with data. Add the following code to the [[Procedure|procedure]]:
7670
7771``` pascal
7872PegCells[pRow,pCol] := pValue
7973```
8074
8175- And now finalize the Create [[Constructor|constructor]]. Add this code to it's body:
8276
8377``` pascal
8478var iRow,iCol: integer;
8579begin
8680 // Store the size of the board locally
8781 Size := pSize;
8882
8983 // Initialize all cells to 'not accessible'
9084 for iRow := 1 to C_MAX do
9185 for iCol := 1 to C_MAX do
9286 Cell[iRow,iCol] := ctNoAccess;
9387```
9488
9589Now 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.
9690
9791Our main form is going to use the data structure we defined in the PegDatastructure file.
9892
9993- Open the main form's sourcefile.
10094- Add PegDatastructures it to the uses list at the top of the file:
10195
10296``` pascal
10397uses
10498 PegDatastructures,
10599 Classes, SysUtils, FileUtil, Forms, Controls, Graphics, Dialogs;
106100```
107101
108-- Press <span class="kbd">F12</span> (this will give us the form editor).
102+- Press `F12` (this will give us the form editor).
109103- From the *[[Standard tab|Standard]]* palette choose a [[TButton]] and drop it on the form (in the upper left corner).
110104- Change the caption to *Test paint*.
111105- On the *[[Additional tab]]* select [[TPaintBox]] and drop it on the form.
112106- Change Align to *alRight*.
113107- Change BordSpacing.Around to *4*.
114108- Change Anchors.akLeft to *true*.
115109- Change Name to *pbPeg*.
116110- Resize the form so it looks something like this:
117111
118-<figure>
119-<img src="tutpeg_empty.png" title="tutpeg_empty.png" />
120-<figcaption>tutpeg_empty.png</figcaption>
121-</figure>
112+![](/wiki-images/0/0d/Tutpeg_empty.png)
122113
123114Next 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.
124115
125116- Double click the test paint button (this creates the event handler).
126117- Add 2 variables:
127118
128119``` pascal
129120var
130121 CellWidth : integer;
131122 CellHeight: integer;
132123```
133124
134125We'll need some extra variables to hold intermediate results:
135126
136127- Add 3 local variables:
137128
138129``` pascal
139130 iRow, iCol: TCellNums;
140131 CellArea : TRect;
141132```
142133
143134CellArea is used to limit the rectangular area on screen where a cell will be drawn.
144135
145136To process all rows and cols two straightforward [[For|for loops]] will do.
146137
147138- Add the following code to the event handler:
148139
149140``` pascal
150141 // Calculate the width/height of each cell to accomodate for all cells in the paintbox
151142 CellWidth := pbPeg.Width div 7;
152143 CellHeight := pbPeg.Height div 7;
153144
154145 // Draw boxes for all cells
155146 for iRow := 1 to 7 do
156147 for iCol := 1 to 7 do
157148 begin
158149 // Calculate the position of the cell in the paintbox
159150 CellArea.Top := (iRow-1) * CellHeight;
160151 CellArea.Left := (iCol-1) * CellWidth;
161152 CellArea.Right := CellArea.Left + CellWidth;
162153 CellArea.Bottom := CellArea.Top + CellHeight;
163154 // And now draw the cell
164155 pbPeg.Canvas.Rectangle(CellArea);
165156 end;
166157```
167158
168159A 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.
169160
170-- Compile and run the program (press <span class="kbd">F9</span>).
161+- Compile and run the program (press `F9`).
171162- Press the *Test paint* button.
172163- Maximize the form (the cells disappear; don't worry we'll fix that).
173164- Press the *Test paint* button again.
174165
175166This 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.
176167
177168- Create a new unit (File/New Unit).
178169- Save it as PegSolPainter.pas (File/Save and then and choose *Rename to lowercase*).
179170- Add a new class to the new unit file that will do all of the drawing (after the uses section, before the implementation section).
180171
181172``` pascal
182173type
183174 TPegSolPainter = class
184175 private
185176 PegSol : TPegSolitaire;
186177 Canvas : TCanvas;
187178
188179 public
189180 constructor Create(pPegSol: TPegSolitaire; pCanvas: TCanvas);
190181 end;
191182```
192183
193184Note that a TPegSolitaire variable is also added, because obviously we are going to use that class to retrieve a cells' state.
194185
195-- Position the text cursor in the constructor line and press <span class="kbd">Ctrl</span>+<span class="kbd">Shift</span>+<span class="kbd">C</span>.
186+- Position the text cursor in the constructor line and press `Ctrl`+`Shift`+`C`.
196187- The constructor must store the 2 parms locally:
197188
198189``` pascal
199190constructor TPegSolPainter.Create(pPegSol: TPegSolitaire; pCanvas: TCanvas);
200191begin
201192 PegSol := pPegSol;
202193 Canvas := pCanvas;
203194end;
204195```
205196
206197Trying 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.
207198
208199- Add *PegDatastructures* and *Graphics* to the *uses* list.
209200
210201``` pascal
211202uses
212203 PegDatastructures,
213204 Graphics,
214205 Classes, SysUtils;
215206```
216207
217208The 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.
218209
219210Now we know this, we can add a paint method to our class.
220211
221212- Add procedure Repaint to the class.
222213
223214``` pascal
224215 TPegSolPainter = class
225216 private
226217 PegSol : TPegSolitaire;
227218 Canvas : TCanvas;
228219
229220 public
230221 constructor Create(pPegSol: TPegSolitaire; pCanvas: TCanvas);
231222 procedure Repaint(const pCanvasWidth, pCanvasHeight: integer);
232223 end;
233224```
234225
235-- Generate the body of the procedure (<span class="kbd">Ctrl</span>+<span class="kbd">Shift</span>+<span class="kbd">C</span>).
226+- Generate the body of the procedure (`Ctrl`+`Shift`+`C`).
236227- Copy the code from *TfrmMain.Button1Click(Sender: TObject)* to this newly created method:
237228
238229``` pascal
239230procedure TPegSolPainter.Repaint(const pCanvasWidth, pCanvasHeight: integer);
240231var
241232 CellWidth : integer;
242233 CellHeight : integer;
243234 iRow, iCol : TCellNums;
244235 CellArea : TRect;
245236begin
246237 // Calculate the width of each cell to accomodate for all cells
247238 CellWidth := pbPeg.Width div 7;
248239 CellHeight := pbPeg.Height div 7;
249240
250241 // Draw boxes for all cells
251242 for iRow := 1 to 7 do
252243 for iCol := 1 to 7 do
253244 begin
254245 // Calculate the position of the cell in the paintbox
255246 CellArea.Top := (iRow-1) * CellHeight;
256247 CellArea.Left := (iCol-1) * CellWidth;
257248 CellArea.Right := CellArea.Left + CellWidth;
258249 CellArea.Bottom := CellArea.Top + CellHeight;
259250 // And now draw the cell
260251 pbPeg.Canvas.Rectangle(CellArea);
261252 end;
262253end;
263254```
264255
265256Because we no longer need the paintbox pbPeg we must remove the references to it. Three changes are needed:
266257
267258- Change the calculation of the CellWidth and CellHeight to:
268259
269260``` pascal
270261 // Calculate the width of each cell to accomodate for all cells
271262 CellWidth := pCanvasWidth div 7;
272263 CellHeight := pCanvasHeight div 7;
273264```
274265
275266- Change the drawing of the rectangle to:
276267
277268``` pascal
278269 // And now draw the cell
279270 Canvas.Rectangle(CellArea);
280271```
281272
282273Now 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.
283274
284275- In the main form source, locate the Button1Click method.
285276- Remove all statements and variables.
286277- Add 2 new variables: one for the game class and one for the painter class:
287278
288279``` pascal
289280procedure TfrmMain.Button1Click(Sender: TObject);
290281var
291282 pegsol : TPegSolitaire; // The game data
292283 pegpaint: TPegSolPainter; // The paint class for the game
293284begin
294285end;
295286```
296287
297288- Add *PegSolPainter* to the uses list.
298289
299290To use the paint class is fairley straightforward: create a new instance and call the repaint method like so:
300291
301292- Add the following code to the Button1Click method:
302293
303294``` pascal
304295 // Create a new game object
305296 pegsol := TPegSolitaire.Create(7);
306297
307298 // Create a new painter object to paint this game
308299 pegpaint := TPegSolPainter.Create(pegsol, pbPeg.Canvas);
309300
310301 // And paint the board
311302 pegpaint.Repaint(pbPeg.Width, pbPeg.Height);
312303
313304 // Clean up
314305 pegpaint.Free;
315306 pegsol.Free
316307```
317308
318309Run and test the program to see that it works exactly as before. The result will look something like this: ![](/wiki-images/2/2b/Tutpeg_empty_cells.png)
319310
320311## It's all about events
321312
322313What 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!
323314
324315As 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.
325316
326-- Open the form editor (select ufrmMain in the editor and press <span class="kbd">F12</span>).
317+- Open the form editor (select ufrmMain in the editor and press `F12`).
327318- Select the paintbox pbPeg.
328319- Open the Events tab in the Object Inspector.
329320- One of the events in the list is OnPaint.
330321
331-<figure>
332-<img src="tutpeg_onpaint.png" title="tutpeg_onpaint.png" />
333-<figcaption>tutpeg_onpaint.png</figcaption>
334-</figure>
322+![](/wiki-images/3/3a/Tutpeg_onpaint.png)
335323
336324This event is 'fired' everytime the paintbox needs to redraw it's surface. That is the place were we are going to do our drawing.
337325
338326- Select the OnPaint event in the Object Inspector and click on the small button with three dots. This will generate the event handler body.
339327- Copy/Paste the exact code from the *Button1Click* event handler to this new method.
340328
341329``` pascal
342330procedure TfrmMain.pbPegPaint(Sender: TObject);
343331var
344332 pegsol : TPegSolitaire; // The game data
345333 pegpaint: TPegSolPainter; // The paint class for the game
346334begin
347335 // Create a new game object
348336 pegsol := TPegSolitaire.Create(7);
349337
350338 // Create a new painter object to paint this game
351339 pegpaint := TPegSolPainter.Create(pegsol, pbPeg.Canvas);
352340
353341 // And paint the board
354342 pegpaint.Repaint(pbPeg.Width, pbPeg.Height);
355343
356344 // Clean up
357345 pegpaint.Free;
358346 pegsol.Free
359347end;
360348```
361349
362350- Remove all code and variables from method *Button1Click*. Remember: the IDE will automatically remove this now empty method for us.
363351- Remove the *Test paint* button from the form.
364352- Run the program to see what happens (ignore the fact that the background color is now different).
365353
366354Now 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.
367355
368356- Find the forms's declaration.
369357- Add the 2 variables we created in the OnPaint event:
370358
371359``` pascal
372360 TfrmMain = class(TForm)
373361 pbPeg: TPaintBox;
374362 procedure pbPegPaint(Sender: TObject);
375363 private
376364 { private declarations }
377365 pegsol : TPegSolitaire; // The game data
378366 pegpaint: TPegSolPainter; // The paint class for the game
379367 public
380368 { public declarations }
381369 end;
382370```
383371
384372These 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.
385373
386374- Add procedure StartNewGame to the form.
387375
388376``` pascal
389377 private
390378 { private declarations }
391379 pegsol : TPegSolitaire; // The game data
392380 pegpaint: TPegSolPainter; // The paint class for the game
393381
394382 procedure StartNewGame;
395383```
396384
397385- Generate the body.
398386- Add the initialzation code:
399387
400388``` pascal
401389procedure TfrmMain.StartNewGame;
402390begin
403391 // Clean up the previous game
404392 pegpaint.Free;
405393 pegsol.Free;
406394
407395 // Start with a new game
408396 pegsol := TPegSolitaire.Create(7);
409397 pegpaint := TPegSolPainter.Create(pegsol, pbPeg.Canvas);
410398end;
411399```
412400
413401Now 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.
414402
415-- Open the form editor (press <span class="kbd">F12</span>).
403+- Open the form editor (press `F12`).
416404- Double click somewhere in a free area on the form; do nót click on the paintbox.
417405
418-<figure>
419-<img src="tutpeg_form_create.png" title="tutpeg_form_create.png" />
420-<figcaption>tutpeg_form_create.png</figcaption>
421-</figure>
406+![](/wiki-images/4/4a/Tutpeg_form_create.png)
422407
423408- Add the call to the StartNewGame procedure:
424409
425410``` pascal
426411procedure TfrmMain.FormCreate(Sender: TObject);
427412begin
428413 StartNewGame
429414end;
430415```
431416
432417Now that the game and paint objects are created at program start, they are no longer required in the OnPaint method.
433418
434419- Locate procedure *procedure TfrmMain.pbPegPaint(Sender: TObject);*
435420- Remove the local variables and all code except for the line that actualle does the painting.
436421
437422``` pascal
438423procedure TfrmMain.pbPegPaint(Sender: TObject);
439424begin
440425 // Paint the board
441426 pegpaint.Repaint(pbPeg.Width, pbPeg.Height);
442427end;
443428```
444429
445430- Run the program and see what happens.
446431
447432## Intermezzo
448433
449434So 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.
450435
451436- In the main form locate the StartNewGame method.
452437- Change the line *pegsol := TPegSolitaire.Create(7);* to *pegsol := TPegSolitaire.Create(5);*. So a smaller board with 5x5 squares is created.
453438- Run the program and see what happens.
454439
455440As 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>).
456441
457442Using magic numbers equals disaster: it's a matter of *when*, not *if* a program will fail.
458443
459444- Open PegSolPainter.
460445- Locate the procedure *TPegSolPainter.Repaint(const pCanvasWidth, pCanvasHeight: integer);*.
461446
462447In 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:
463448
464449``` pascal
465450 // Calculate the width of each cell to accomodate for all cells
466451 CellWidth := pCanvasWidth div pegsol.Size;
467452 CellHeight := pCanvasHeight div pegsol.Size;
468453
469454 // Draw boxes for all cells
470455 for iRow := 1 to pegsol.Size do
471456 for iCol := 1 to pegsol.Size do
472457 begin
473458 // Calculate the position of the cell in the paintbox
474459 CellArea.Top := (iRow-1) * CellHeight;
475460 CellArea.Left := (iCol-1) * CellWidth;
476461 CellArea.Right := CellArea.Left + CellWidth;
477462 CellArea.Bottom := CellArea.Top + CellHeight;
478463 // And now draw the cell
479464 Canvas.Rectangle(CellArea);
480465 end;
481466```
482467
483468There'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.
484469
485470- Open PegDatastructures.
486471- Rename the private variable in TPegSolitaire to *FSize*.
487472- Add a public read only property to the class: property Size: TCellNums read FSize;
488473
489474The class will now look like this:
490475
491476``` pascal
492477 TPegSolitaire = class
493478 private
494479 FSize: TCellNums;
495480 PegCells: TPegCells;
496481 function GetCell(const pRow, pCol: TCellNums): TCellType;
497482 procedure SetCell(const pRow, pCol: TCellNums; const pValue: TCellType);
498483
499484 public
500485 constructor Create(const pSize: TCellNums);
501486 property Cell[const pRow, pCol: TCellNums]: TCellType read GetCell write SetCell;
502487 property Size: TCellNums read FSize;
503488 end;
504489```
505490
506491It'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.
507492
508493- Locate the constructor.
509494- Change *Size := pSize;* to *FSize := pSize;* (if you don't you'll get a compilation error).
510495- And while we're at it, the initialization needs a minor fix as well. There is no need to initialize cells we are not going to use. So the constructor should look like this (*C_MAX* is replaced with *Size*):
511496
512497``` pascal
513498constructor TPegSolitaire.Create(const pSize: TCellNums);
514499var iRow,iCol: integer;
515500begin
516501 FSize := pSize;
517502 for iRow := 1 to Size do
518503 for iCol := 1 to Size do
519504 Cell[iRow,iCol] := ctNoAccess;
520505end;
521506```
522507
523508We are now ready to test the program and see if it now draws a nice 5x5 matrix.
524509
525510- Run the program and see what happens (a 5x5 matrix is drawn).
526511
527512Now that the basics of the game are in place, it's time to flesh out the gui.
528513
529514## Let's get artistic
530515
531516So 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.
532517
533518- In the editor open PegSolPainter (the unit with the paint class for our board).
534519- Locate the Repaint method.
535520- Locate the line where the cell is drawn: *Canvas.Rectangle(CellArea);*.
536521
537522Obviously 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.
538523
539524- Change the drawing of cells like so:
540525
541526``` pascal
542527 // Draw boxes for all cells
543528 for iRow := 1 to pegsol.Size do
544529 for iCol := 1 to pegsol.Size do
545530 begin
546531 // Calculate the position of the cell in the paintbox
547532 CellArea.Top := (iRow-1) * CellHeight;
548533 CellArea.Left := (iCol-1) * CellWidth;
549534 CellArea.Right := CellArea.Left + CellWidth;
550535 CellArea.Bottom := CellArea.Top + CellHeight;
551536
552537 // And now draw the cell based on the cell's contents
553538 case pegsol.Cell[iRow,iCol] of
554539
555540 ctNoAccess: // Draw cells that are not accessible
556541 begin
557542 Canvas.Brush.Color := clGray;
558543 Canvas.Rectangle(CellArea);
559544 end;
560545
561546 ctEmpty: // Draw cells that are currently empty
562547 begin
563548 Canvas.Brush.Color := clBlue;
564549 Canvas.Rectangle(CellArea);
565550 end;
566551
567552 ctPeg: // Draw cells that are occupied
568553 begin
569554 Canvas.Brush.Color := clBlue;
570555 Canvas.Rectangle(CellArea); // Erase the background first
571556 Canvas.Brush.Color := clGreen;
572557 Canvas.Ellipse(CellArea); // Draw the pegs as green circles
573558 end;
574559 end;
575560 end;
576561```
577562
578563We 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.
579564
580565- Open the main form source.
581566- Locate the StartNewGame method.
582567- Create a 7x7 game instead of 5x5.
583568- Initialize a couple of cells. For example:
584569
585570``` pascal
586571procedure TfrmMain.StartNewGame;
587572begin
588573 // Clean up the previous game
589574 pegpaint.Free;
590575 pegsol.Free;
591576
592577 // Start with a new 7x7 game
593578 pegsol := TPegSolitaire.Create(7);
594579 pegpaint := TPegSolPainter.Create(pegsol, pbPeg.Canvas);
595580
596581 // Initialize some cells
597582 pegsol.Cell[3,4] := ctEmpty;
598583 pegsol.Cell[4,2] := ctPeg;
599584 pegsol.Cell[4,3] := ctPeg;
600585 pegsol.Cell[4,4] := ctEmpty;
601586 pegsol.Cell[4,5] := ctPeg;
602587 pegsol.Cell[4,6] := ctPeg;
603588 pegsol.Cell[5,4] := ctEmpty;
604589end;
605590```
606591
607592- Run the program (it'll look something like the image below).
608593
609-<figure>
610-<img src="tutpeg_first_pegs.png" title="tutpeg_first_pegs.png" />
611-<figcaption>tutpeg_first_pegs.png</figcaption>
612-</figure>
594+![](/wiki-images/0/0b/Tutpeg_first_pegs.png)
613595
614596## Populate the board
615597
616598As we know a classic solitaire board should look something like this:
617599
618-<figure>
619-<img src="tutpeg_classic.png" title="tutpeg_classic.png" />
620-<figcaption>tutpeg_classic.png</figcaption>
621-</figure>
600+![](/wiki-images/6/6c/Tutpeg_classic.png)
622601
623602And 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:
624603
625604``` pascal
626605 // Initialize the cells to the classic game
627606 pegsol.InitializeBoard( ' ooo ' + LineEnding +
628607 ' ooo ' + LineEnding +
629608 'ooooooo' + LineEnding +
630609 'ooo.ooo' + LineEnding +
631610 'ooooooo' + LineEnding +
632611 ' ooo ' + LineEnding +
633612 ' ooo ' );
634613```
635614
6366151. **o** is an occupied cell.
6376162. **.** is an empty but playable cell.
6386173. The spaces indicate cells that are not accessible.
639618
640619Let's assume this is going to work and create a method in the TPegSelitaire class that can handle this.
641620
642621- Let's be optimistic (also called 'Top down design') and add the above code to TfrmMain.StartNewGame:
643622
644623``` pascal
645624procedure TfrmMain.StartNewGame;
646625begin
647626 // Clean up the previous game
648627 pegpaint.Free;
649628 pegsol.Free;
650629
651630 // Start with a new 7x7 game
652631 pegsol := TPegSolitaire.Create(7);
653632 pegpaint := TPegSolPainter.Create(pegsol, pbPeg.Canvas);
654633
655634 // Initialize the cells to the classic game
656635 pegsol.InitializeBoard( ' ooo ' + LineEnding +
657636 ' ooo ' + LineEnding +
658637 'ooooooo' + LineEnding +
659638 'ooo.ooo' + LineEnding +
660639 'ooooooo' + LineEnding +
661640 ' ooo ' + LineEnding +
662641 ' ooo ' );
663642end;
664643```
665644
666645- Open the PegDatastructures sourcefile.
667646- Add this procedure to the public section of the class: *InitializeBoard(const pBoard: ansistring);*
668647
669648``` pascal
670649 public
671650 constructor Create(const pSize: TCellNums);
672651 procedure InitializeBoard(const pBoard: ansistring);
673652
674653 property Cell[const pRow, pCol: TCellNums]: TCellType read GetCell write SetCell;
675654 property Size: TCellNums read FSize;
676655```
677656
678-- Generate the body of InitializeBoard (<span class="kbd">Ctrl</span>+<span class="kbd">Shift</span>+<span class="kbd">C</span>...).
657+- Generate the body of InitializeBoard (`Ctrl`+`Shift`+`C`...).
679658
680659Now 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.
681660
682661- Code *InitalizeBoard* like so:
683662
684663``` pascal
685664procedure TPegSolitaire.InitializeBoard(const pBoard: ansistring);
686665var lst : TStringList;
687666 iRow,iCol: integer;
688667 s : string;
689668begin
690669 // Create a list with the board text in it. This will split all lines
691670 // into individual lines, because of the LineEnding 'splitter'.
692671 lst := TStringList.Create;
693672 lst.Text := pBoard;
694673
695674 // Process all lines one at a time
696675 for iRow := 0 to lst.Count-1 do
697676 if iRow < Size then // Make sure there is no overflow in the rows
698677 begin
699678 // Process a single line of text
700679 s := lst[iRow];
701680 for iCol := 1 to length(s) do
702681 if iCol <= Size then // Make sure there is no overflow in the columns
703682 case s[iCol] of
704683 ' ': Cell[iRow+1,iCol] := ctNoAccess;
705684 '.': Cell[iRow+1,iCol] := ctEmpty;
706685 'o': Cell[iRow+1,iCol] := ctPeg;
707686 end;
708687 end;
709688
710689 // Clean up the list
711690 lst.Free;
712691end;
713692```
714693
7156941. a TStringList is used as a buffer. This works because we used LineEnding as a separator between all lines.
7166952. 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.
717696
718697The 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:
719698
720699``` pascal
721700procedure TPegSolitaire.InitializeBoard(const pBoard: ansistring);
722701var iRow,iCol: integer;
723702begin
724703 with TStringList.Create do
725704 begin
726705 Text := pBoard;
727706 for iRow := 0 to Min(Count-1, Size-1) do
728707 for iCol := 1 to Min(length(Strings[iRow]),Size) do
729708 case Strings[iRow][iCol] of
730709 ' ': Cell[iRow+1,iCol] := ctNoAccess;
731710 '.': Cell[iRow+1,iCol] := ctEmpty;
732711 'o': Cell[iRow+1,iCol] := ctPeg;
733712 end;
734713 Free;
735714 end;
736715end;
737716```
738717
739718This 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.
740719
741720- Run the program. All cells are now populated like in the classic Peg Solitaire game.
742721
743722## Events revisited
744723
745724It 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.
746725
747726- Drop a [[TMemo]] on the form, to the left of the paint box pbPeg (it's on the *Standard* tab of the component palette).
748727- In the Object Inspector change Align to *alClient*. It will occupy all remaining space that is left of the paintbox.
749728- Change BorderSpacing.Around to *4*.
750729- Clear the Lines property (click on the 3-dotted button and erase the text). The result should look like this:
751730
752-<figure>
753-<img src="tutpeg_with_memo.png" title="tutpeg_with_memo.png" />
754-<figcaption>tutpeg_with_memo.png</figcaption>
755-</figure>
731+![](/wiki-images/1/11/Tutpeg_with_memo.png)
756732
757733- Select the paintbox pbPeg.
758734- In the Object Inspector select the Events tab.
759735
760736There are 2 events that are interesting to us: OnMouseDown and OnMouseUp. Let's see what happens whith the OnMouseDown event.
761737
762738- In the Object Inspector select the OnMouseDown event.
763739- Click on the 3-dotted button (this generates the body of the event procedure).
764740
765741The 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.
766742
767743- Add the following statement to the procedure; it adds a formatted line to the memo displaying the position where the mouse button was pressed:
768744
769745``` pascal
770746Memo1.Append( format('Mouse down @ %dx%d',[X,Y]) )
771747```
772748
773-- Run the program (press <span class="kbd">F9</span>).
749+- Run the program (press `F9`).
774750- Click on the game board at several different locations. The memo box will display where the mouse button was pressed.
775751- End the program.
776752
777753Obviously 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.
778754
779755- Open the PegDatastructures file.
780756- Add the *TCellPosition* record type to store cell coordinates:
781757
782758``` pascal
783759type
784760 TCellNums = 1..C_MAX;
785761 TCellType = (ctNoAccess, ctEmpty, ctPeg);
786762 TPegCells = array[TCellNums, TCellNums] of TCellType;
787763
788764 TCellPosition = record
789765 Row: TCellNums;
790766 Col: TCellNums;
791767 end;
792768```
793769
794770- Open the PegSolPainter class file.
795771- Add the public function CanvasXYtoCell(...) to the class:
796772
797773``` pascal
798774 public
799775 constructor Create(pPegSol: TPegSolitaire; pCanvas: TCanvas);
800776
801777 procedure Repaint(const pCanvasWidth, pCanvasHeight: integer);
802778 function CanvasXYtoCell(const pX, pY: integer): TCellPosition;
803779```
804780
805-- Generate the body (<span class="kbd">Ctrl</span>+<span class="kbd">Shift</span>+<span class="kbd">C</span>).
781+- Generate the body (`Ctrl`+`Shift`+`C`).
806782- Add the following code:
807783
808784``` pascal
809785function TPegSolPainter.CanvasXYtoCell(const pX, pY: integer): TCellPosition;
810786begin
811787 result.Col := (pX div CellWidth) + 1;
812788 result.Row := (pY div CellHeight) + 1;
813789end;
814790```
815791
816792The 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.
817793
818794- Add *CellWidth* and *CellHeight* as private variables to the *TPegSolPainter* class:
819795
820796``` pascal
821797 TPegSolPainter = class
822798 private
823799 PegSol : TPegSolitaire;
824800 Canvas : TCanvas;
825801 CellWidth : integer;
826802 CellHeight : integer;
827803```
828804
829805- Remove the variables *CellWidth* and *CellHeight* from the variable list in the Repaint procedure:
830806
831807``` pascal
832808procedure TPegSolPainter.Repaint(const pCanvasWidth, pCanvasHeight: integer);
833809var
834810 iRow, iCol : TCellNums;
835811 CellArea : TRect;
836812begin
837813```
838814
839815Now 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.
840816
841817- Open the main form sourcefile.
842818- Locate the *pbPegMouseDown* procedure.
843819- Change the code to:
844820
845821``` pascal
846822procedure TfrmMain.pbPegMouseDown(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
847823var CellRC: TCellPosition;
848824begin
849825 CellRC := pegpaint.CanvasXYtoCell(X,Y);
850826 Memo1.Append( format('Mouse down @ %dx%d',[CellRC.Row, CellRC.Col]) )
851827end;
852828```
853829
854830Let's do the same for the OnMouseUp event:
855831
856832- Generate the procedure body for the OnMouseUp event.
857833- Add this code:
858834
859835``` pascal
860836procedure TfrmMain.pbPegMouseUp(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
861837begin
862838 with pegpaint.CanvasXYtoCell(X,Y) do
863839 Memo1.Append( format('Mouse up @ %dx%d',[Row, Col]) )
864840end;
865841```
866842
867843Note 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...).
868844
869845Let's test this:
870846
871847- Run the program.
872848- Click on the upper left most cell and hold the mouse down.
873849- Move the mouse cursor to another cell.
874850- Release the mouse button.
875851
876852Every 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.
877853
878854## Gluing things together
879855
880856This program works a bit like the calculator program:
881857
8828581. The user selects a peg to move by pressing the mouse button.
8838592. The user moves the peg over another peg to an empty cell (leaps).
8848603. The user release the mouse button to execute the leap.
885861
886862Here 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.
887863
888864- Open the main form's sourcefile.
889865- Add a variable *FromCell* the the form's private variables.
890866
891867``` pascal
892868 private
893869 { private declarations }
894870 pegsol : TPegSolitaire; // The game data
895871 pegpaint: TPegSolPainter; // The paint class for the game
896872 FromCell: TCellPosition; // The peg that is going to leap
897873```
898874
899875- Locate the OnMouseDown event handler.
900876- Add the following line to that procedure:
901877
902878``` pascal
903879FromCell := pegpaint.CanvasXYtoCell(X,Y);
904880```
905881
906882Now 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\>).
907883
908884- Locate the OnMouseUp event handler and add the following line:
909885
910886``` pascal
911887pegsol.Leap(FromCell, pegpaint.CanvasXYtoCell(X,Y));
912888```
913889
914890*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.
915891
916892- Add the following line to the procedure as well:
917893
918894``` pascal
919895pbPeg.Repaint;
920896```
921897
922898This will force the paintbox to repaint itself, so all updated cells show the correct contents.
923899
924900The complete OnMouseUp method now looks like this:
925901
926902``` pascal
927903procedure TfrmMain.pbPegMouseUp(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
928904begin
929905 with pegpaint.CanvasXYtoCell(X,Y) do
930906 Memo1.Append( format('Mouse up @ %dx%d',[Row, Col]) );
931907 pegsol.Leap(FromCell, pegpaint.CanvasXYtoCell(X,Y));
932908 pbPeg.Repaint;
933909end;
934910```
935911
936912Before 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:
937913
938914``` pascal
939915state := pegsol.Cell[ FromCell.Row, FromCell.Col ]
940916```
941917
942918It would be nice if we could write something like:
943919
944920``` pascal
945921state := pegsol.GetCell(FromCell)
946922```
947923
948924Let's add this to our game class.
949925
950926- Open sourcefile PegDatastructures.
951927- Add the private function: *GetCell(const pPosition: TCellPosition): TCellType;*
952928
953929``` pascal
954930 TPegSolitaire = class
955931 private
956932 FSize: TCellNums;
957933 PegCells: TPegCells;
958934
959935 function GetCell(const pRow, pCol: TCellNums): TCellType;
960936 procedure SetCell(const pRow, pCol: TCellNums; const pValue: TCellType);
961937 function GetCell(const pPosition: TCellPosition): TCellType;
962938```
963939
964940There now are two functions with the name GetCell, but the parameter list is different. So Lazarus knows exactly when to call which function.
965941
966-- Generate the body of the function (<span class="kbd">Ctrl</span>+<span class="kbd">Shift</span>+<span class="kbd">C</span>).
942+- Generate the body of the function (`Ctrl`+`Shift`+`C`).
967943- Insert the following code:
968944
969945``` pascal
970946result := Cell[pPosition.Row, pPosition.Col]
971947```
972948
973949Back 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.
974950
975951- Open sourcefile PegDatastructures.
976952- Add procedure Leap to the public section of the TPegSolitaire class.
977953
978954``` pascal
979955 public
980956 constructor Create(const pSize: TCellNums);
981957 procedure InitializeBoard(const pBoard: ansistring);
982958 procedure Leap(const pFromCell, pToCell: TCellPosition);
983959```
984960
985-- Generate the procedure body (<span class="kbd">Ctrl</span>+<span class="kbd">Shift</span>+<span class="kbd">C</span>).
961+- Generate the procedure body (`Ctrl`+`Shift`+`C`).
986962
987963Now 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.
988964
989965``` pascal
990966procedure TPegSolitaire.Leap(const pFromCell, pToCell: TCellPosition);
991967var dx, dy: integer;
992968 JumpedCell: TCellPosition;
993969begin
994970 // Verify that the start cell is occupied and the target cell is empty
995971 // If not, leave the procedure via the EXIT.
996972 if (GetCell(pFromCell) <> ctPeg) or
997973 (GetCell(pToCell) <> ctEmpty) then
998974 EXIT;
999975
1000976 // Calculate the horizontal and vertical distance between the cells
1001977 dx := abs(pFromCell.Col - pToCell.Col);
1002978 dy := abs(pFromCell.Row - pToCell.Row);
1003979
1004980 // A valid move has one direction equal to zero and the other equal to 2
1005981 if ((dx = 2) and (dy = 0))
1006982 or ((dx = 0) and (dy = 2)) then
1007983 begin
1008984 // Determine the position of the jumped cell; it's in the middle
1009985 JumpedCell.Col := (pFromCell.Col + pToCell.Col) div 2;
1010986 JumpedCell.Row := (pFromCell.Row + pToCell.Row) div 2;
1011987
1012988 // Final check: is there a peg in the jumped cell?
1013989 if GetCell(JumpedCell) = ctPeg then
1014990 begin
1015991 // Jump: clear the FromCell, empty the jumped cell and populate the ToCell
1016992 Cell[pFromCell.Row, pFromCell.Col] := ctEmpty;
1017993 Cell[JumpedCell.Row, JumpedCell.Col] := ctEmpty;
1018994 Cell[pToCell.Row, pToCell.Col] := ctPeg;
1019995 end;
1020996 end;
1021997end;
1022998```
1023999
10241000Believe it or not but we now have a working Peg Solitaire game program!
10251001
10261002- Run the program.
10271003- Press the mouse button on one peg cell.
10281004- Leap over another peg cell to an empty cell.
10291005- Release the mouse button.
10301006- Repeat...
10311007
10321008## Cleaning up
10331009
10341010On the form we still have the memo component that displays mouse clicks. We don't really need it anymore.
10351011
10361012- Delete the memo from the main form.
10371013- Delete the memo updates from the MouseDown and MouseUp events.
10381014
10391015``` pascal
10401016procedure TfrmMain.pbPegMouseDown(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
10411017begin
10421018 FromCell := pegpaint.CanvasXYtoCell(X,Y);
10431019end;
10441020
10451021procedure TfrmMain.pbPegMouseUp(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
10461022begin
10471023 pegsol.Leap(FromCell, pegpaint.CanvasXYtoCell(X,Y) );
10481024 pbPeg.Repaint;
10491025end;
10501026```
10511027
1052-- Open the form editor (<span class="kbd">F12</span>).
1028+- Open the form editor (`F12`).
10531029- Select the paintbox pbPeg
10541030- In the Object Inspector change property Align to *alClient* (look on the Properties tab).
10551031- Remove the unit *stdctrls* from the uses list. The compiler would othwerwise give a hint in the Messages window that it isn't used (it was automatically added when we dropped the memo component on the form).
10561032
10571033### What's next?
10581034
10591035To 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.
10601036
10611037## Creating more board layouts
10621038
10631039It 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.
10641040
10651041- From the menu choose *File/New...*; this opens a pop up dialog.
10661042- Choose the option Module/Text and click OK.
10671043- Type the following text (it should look familiar :-) ):
10681044
10691045``` pascal
10701046 ooo
10711047 ooo
10721048ooooooo
10731049ooo.ooo
10741050ooooooo
10751051 ooo
10761052 ooo
10771053```
10781054
10791055- From the menu choose File/Save.
10801056- Save the file in the projects' bin folder as *classic.txt*.
10811057
10821058Add another file:
10831059
10841060- From the menu choose *File/New...*.
10851061- Choose the option Module/Text and click OK.
10861062- Type the following text:
10871063
10881064``` pascal
10891065 ...
10901066 .o.
10911067..ooo..
10921068.ooooo.
10931069ooooooo
10941070 ...
10951071 ...
10961072```
10971073
10981074- From the menu choose File/Save.
10991075- Save the file in the bin folder as *triangle.txt*.
11001076
11011077There 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).
11021078
11031079- Open the main form's editor.
11041080- Select the [[TMainMenu]] control from the Standard components palette.
11051081- Click on the form (it doesn't matter if you 'hit' the paintbox). An icon is displayed showing that the menu is now availabe.
11061082- Double click on the icon with label MainMenu1. This opens the menu editor with one item aldready available, labeled *New Item1*.
11071083- In the Object Inspector change Caption to *&File* (include the ampersand; it's used to draw the underscore symbol when you press Alt to access the main menu).
11081084- Right click on the File item in the Menu Editor and select *Create Submenu* from the pop up window.
11091085- Change the Caption to *E&xit* (this adds a familiar application exit button).
11101086- Right click on the File item.
11111087- Select *Insert New Item (after)* from the pop up menu.
11121088- Change the caption of that new menu to *&Game*.
11131089- Right click on the Game menu and select *Create Submenu* from the pop up..
11141090- Change the caption to *Load from file...*.
11151091
11161092The menu will now look something like this:
11171093
1118-<figure>
1119-<img src="tutpeg_mainmenu.png" title="tutpeg_mainmenu.png" />
1120-<figcaption>tutpeg_mainmenu.png</figcaption>
1121-</figure>
1094+![](/wiki-images/4/4e/Tutpeg_mainmenu.png)
11221095
11231096The menu is also visible at the top of the main form.
11241097
11251098- Click on the *File* menu item; this opens the submenu and *Exit* is visible.
11261099- Double click on *Exit*. As you might have guessed this creates an event handler. In other words the procedure that gets executed as soon as the user clicks on this menu item.
11271100- Add the following not too complex code to the generated procedure:
11281101
11291102``` pascal
11301103Close
11311104```
11321105
11331106This will close the main form. And because it is the only form of our application it will end the application.
11341107
11351108Okay 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.
11361109
11371110- Open the main form's editor.
11381111- In the component palette select the *[[Dialogs tab]]*.
11391112- Select the [[TOpenDialog]] component (click on it).
11401113- Click on the main form (anywhere). A OpenDialog1 component is now visible. This component provides us with a kind of explorer pop up with which we can select files. The main form now looks like this:
11411114
1142-<figure>
1143-<img src="tutpeg_mainform.png" title="tutpeg_mainform.png" />
1144-<figcaption>tutpeg_mainform.png</figcaption>
1145-</figure>
1115+![](/wiki-images/5/51/Tutpeg_mainform.png)
11461116
11471117- In the Object Inspector click on the Filter property and then on the 3-dotted button. This pops up the filter dialog. This is the place where we tell the file dialog what files we allow the user to select.
11481118- We only want to see text files so add this filter line: *Peg Solitaire textfiles (*.txt) \| *.txt* (see image below).
11491119- Press OK.
11501120
1151-<figure>
1152-<img src="tutpeg_filterdialog.png" title="tutpeg_filterdialog.png" />
1153-<figcaption>tutpeg_filterdialog.png</figcaption>
1154-</figure>
1121+![](/wiki-images/1/13/Tutpeg_filterdialog.png)
11551122
11561123- In the Object Inspector change Options.ofFileMustExist to *true*. This forces the dialog to only allow existing files to be selected.
11571124
11581125So far so good. Now to actually open a file when the menu item is selected:
11591126
11601127- On the main form click on the Game menu item. This opens the drop down menu.
11611128- Click on *Load from file...*. As expected this generates an.... event handler.
11621129- Add the following code to the Event handler:
11631130
11641131``` pascal
11651132 if OpenDialog1.Execute then
11661133 ShowMessage(OpenDialog1.FileName);
11671134```
11681135
11691136Remember: 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.
11701137
11711138- Run the application.
11721139- From the menu choose *Game/Load from file...*.
11731140- Select *triangle.txt* and press Open.
11741141- A window pops up that shows us the selected filename.
11751142- End the program.
11761143
11771144Now let's finish the board loading code. The code is pretty self-explanatory and reuses some of the procedures we have created so far.
11781145
11791146- Open the main form's editor.
11801147- From the menu choose *Game/Load from file...*. This will open the event handler.
11811148- Replace the code with this:
11821149
11831150``` pascal
11841151procedure TfrmMain.MenuItem4Click(Sender: TObject);
11851152begin
11861153 // Open the pop up dialog
11871154 if OpenDialog1.Execute then
11881155 begin
11891156 // Start with a new empty board
11901157 StartNewGame;
11911158
11921159 // Dynamically create a stringlist to load the board layout
11931160 with TStringList.Create do
11941161 begin
11951162 // Load the board layout from the textfile
11961163 LoadFromFile(OpenDialog1.FileName);
11971164
11981165 // Initialize the board with the file's contents
11991166 pegsol.InitializeBoard(Text);
12001167
12011168 // Clean up the stringlist
12021169 Free
12031170 end;
12041171
12051172 // After loading the new board update the paintbox
12061173 pbPeg.Repaint;
12071174 end;
12081175end;
12091176```
12101177
12111178And that's all there is to it!
12121179
12131180Run the program, open a board set up file and play the game!
12141181
12151182## Eye candy
12161183
12171184We 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:
12181185
12191186- Open the PegSolPainter sourcefile.
12201187- Add the 3 images to the private variables section:
12211188
12221189``` pascal
12231190 TPegSolPainter = class
12241191 private
12251192 PegSol : TPegSolitaire;
12261193 Canvas : TCanvas;
12271194 CellWidth : integer;
12281195 CellHeight : integer;
12291196
12301197 ImgNoAccess: TPicture;
12311198 ImgEmpty : TPicture;
12321199 ImgPeg : TPicture;
12331200```
12341201
12351202- Add a procedure to the public section for loading the images.
12361203
12371204``` pascal
12381205 public
12391206 constructor Create(pPegSol: TPegSolitaire; pCanvas: TCanvas);
12401207 procedure Repaint(const pCanvasWidth, pCanvasHeight: integer);
12411208 function CanvasXYtoCell(const pX, pY: integer): TCellPosition;
12421209 procedure LoadImage(const pCellType: TCellType; const pFilename: string);
12431210```
12441211
12451212- Generate the body for the procedure.
12461213- Add the code:
12471214
12481215``` pascal
12491216procedure TPegSolPainter.LoadImage(const pCellType: TCellType; const pFilename: string);
12501217
12511218 procedure UpdateImage(var pImg: TPicture; const pNewImage: TPicture);
12521219 begin
12531220 pImg.Free;
12541221 pImg := pNewImage
12551222 end;
12561223
12571224var pic: TPicture;
12581225begin
12591226 // Make sure the file exists
12601227 if FileExists(pFilename) then
12611228 begin
12621229 // First load the picture in a local variable
12631230 pic := TPicture.Create;
12641231 pic.LoadFromFile(pFilename);
12651232
12661233 // Now update the required image based on the pCelltype parm
12671234 case pCelltype of
12681235 ctNoAccess: UpdateImage(ImgNoAccess, pic);
12691236 ctEmpty : UpdateImage(ImgEmpty, pic);
12701237 ctPeg : UpdateImage(ImgPeg, pic);
12711238 end;
12721239 end;
12731240end;
12741241```
12751242
12761243The 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.
12771244
12781245We'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.
12791246
12801247- Locate the Repaint method.
12811248- In the case statement, change the ctNoAccess, ctEmpty and ctPeg sections to:
12821249
12831250``` pascal
12841251 // And now draw the cell based on the cell's contents
12851252 case pegsol.Cell[iRow,iCol] of
12861253
12871254 ctNoAccess: // Draw cells that are not accessible
12881255 if not assigned(ImgNoAccess) then
12891256 begin
12901257 Canvas.Brush.Color := clGray;
12911258 Canvas.Rectangle(CellArea);
12921259 end
12931260 else with ImgNoAccess do
12941261 Canvas.CopyRect(CellArea, Bitmap.Canvas, Rect(0,0,Width,Height));
12951262
12961263 ctEmpty: // Draw cells that are currently empty
12971264 if not assigned(ImgEmpty) then
12981265 begin
12991266 Canvas.Brush.Color := clBlue;
13001267 Canvas.Rectangle(CellArea);
13011268 end
13021269 else with ImgEmpty do
13031270 Canvas.CopyRect(CellArea, Bitmap.Canvas, Rect(0,0,Width,Height));
13041271
13051272 ctPeg: // Draw cells that are occupied
13061273 if not assigned(ImgPeg) then
13071274 begin
13081275 Canvas.Brush.Color := clBlue;
13091276 Canvas.Rectangle(CellArea); // Erase the background first
13101277 Canvas.Brush.Color := clGreen;
13111278 Canvas.Ellipse(CellArea);
13121279 end
13131280 else with ImgPeg do
13141281 Canvas.CopyRect(CellArea, Bitmap.Canvas, Rect(0,0,Width,Height));
13151282
13161283 end;
13171284```
13181285
13191286In the main form locate the StartNewGame method and add the image loading code to the painter class.
13201287
13211288- Open the main form editor.
13221289- Locate method *StartNewGame*.
13231290- Add LoadImage statements after the creation of the *pegpaint* object:
13241291
13251292``` pascal
13261293 // Start with a new 7x7 game
13271294 pegsol := TPegSolitaire.Create(7);
13281295 pegpaint := TPegSolPainter.Create(pegsol, pbPeg.Canvas);
13291296
13301297 pegpaint.LoadImage(ctNoAccess, 'tutpeg_cellforbidden.jpg');
13311298 pegpaint.LoadImage(ctEmpty, 'tutpeg_cellempty.jpg');
13321299 pegpaint.LoadImage(ctPeg, 'tutpeg_celloccupied.jpg');
13331300```
13341301
13351302Make 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.
13361303
13371304![](/wiki-images/1/1c/Tutpeg_cellforbidden.jpg) ![](/wiki-images/5/51/Tutpeg_cellempty.jpg) ![](/wiki-images/9/9c/Tutpeg_celloccupied.jpg)
13381305
13391306- Now test the solitaire game and hopefully it looks something like the image at the beginning of this tutorial...
13401307
13411308## Final tweaks
13421309
13431310Most 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.
13441311
13451312- From the menu choose *Project/Project Options...*.
13461313- Click on *Project Options* in the pop up dialog (right at the top). This will display the window with the application icon.
13471314- Click on *Load Icon* and load your icon of choice; or download the marble that comes with this tutorial:
13481315
1349-<figure>
1350-<img src="tutpeg_marble.png" title="tutpeg_marble.png" />
1351-<figcaption>tutpeg_marble.png</figcaption>
1352-</figure>
1316+![](/wiki-images/e/eb/Tutpeg_marble.png)
13531317
13541318- Press OK.
13551319
13561320The application will now have a nice marble icon in the form's title bar and in explorer windows.
13571321
13581322---
13591323*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](/wiki-contact).*

Back to history