![]() |
![]() |
|||||||||||||||||||||||||||||||
Introduction Peg Solitaire is a single player puzzle.The board consists of holes (33) and pegs (initially 32). The center position only is open. The final, solved, game has one peg in the center position after 31 moves have striked the other pegs. A move takes a peg over it's neigbour (horizontally or vertically) to an empty hole. The peg that was jumped over is removed from the game. See picture below for a reduced image of an initial- and a solved game.
My version of Peg Solitaire has the following options: Options
The search algorithm My efforts to write a program that solves peg-solitaire go back to 1993.At that time I worked, as a hardware engineer, for the Control Data Corporation (CDC). I was granted permission to run my Fortran program on the Cyber205, the national supercomputer installed at the Academic Computer Center (SARA) in Amsterdam. I remember that it took this giant machine about 6 minutes to find the first solution. Years later I programmed peg-solitair in Pascal. These programs are lost and recently I wrote another program, now in Delphi. Also, while camping in France (without PC) some ideas struck me how to speedup the search process. Peg-Solitaire version 2 is ready now and has these tricks installed. I describe them below. I know of the existence of search techniques, but never studied them. My approach is intuitive. Most likely, I have reinvented the wheel. However, solitaire2.exe solves the solitaire puzzle in 12 milliseconds and finds more solutions in additional fractions of a millisecond. It is the best result I ever obtained. Note: the CYBER205 was a 50MHz machine. A modern PC with 2,4GHz clock needs about 7.5 secs to solve the puzzle if no addtional tricks are programmed to speedup the search. Data Structures Computers can only handle numbers.How is this game coded? Have a look at the board and the numbering of the peg positions: ![]() The peg placement is recorded in array board: var board : array[1..49] of byte; board[n] = 1 if the position n holds a peg, board[n] = 0 if position n is empty. At this time the reader may wonder why a one-dimensional array is used instead of [1..7,1..7] a two dimensional array. The reason is speed. During the search, millions of moves have to be examined. In the computer memory, bytes are ordered as in a one-dimensional array, so if a two-dimensional array is choosen, each time a small calculation is necessary to acces the byte. This time is now saved. From some positions, moves are restricted. (position 3 can only move right or down) The move directions per position are coded as bits 0..3 in a byte. A "1" bit indicates that the move is allowed. See picture below for the coding of move directions: ![]() Array movedirs holds the move direction code for each board position. For positions 1,2,6,7,8,9,13,14,36,37,41,42,43,44,48,49......movedirs[ ] = 0, so no move from these positions is possible. See code below
const movedirs : array[1..49] of byte = //1: right
(0, 0, 9, 8,12, 0, 0, //2: up
0, 0, 9, 8,12, 0, 0, //4: left
9, 9,15,15,15,12,12, //8: down
1, 1,15,15,15, 4, 4,
3, 3,15,15,15, 6, 6,
0, 0, 3, 2, 6, 0, 0,
0, 0, 3, 2, 6, 0, 0);
A move is coded as
type Tmove = record
p1, p2, p3 : byte;
end;
p1, p2, p3 are board positions (1..49) , meaning: p1 moves over p2 to
p3, p2 is removed.Each move has a number, 1..76 as there are 76 different moves possible. The pinlist array stores the pins (or pegs) involved in that move. var pinlist : array[1..76] of Tmove;At create time the pinlist[1..76] table is generated from the movedirs[1..49] table.
procedure makeMovePinList;
//moves 1..76 translated to pin nrs in PinList[ ]
const u : array[0..3] of shortint = (1,-7,-1,7); //pin direction increments
var m,n,d : byte; //m:movepin1 d:direction
begin
n := 0;
for m := 3 to 47 do
for d := 0 to 3 do
if movedirs[m] and (1 shl d) <> 0 then
begin
inc(n); //next entry in table
with PinList[n] do
begin
p1 := m;
p2 := p1 + u[d];
p3 := p2 + u[d];
end;
end;
end;
To avoid confusion, keep in mind the difference between the sequence
number of a move and of it's code.The sequence of moves is, of course, 1,2,3......This is called the move number which has the range 1..31. The move code has range 1..76 and is used as an index to the pinlist[ ] table to find the pegs involved. Moves done are stored in table moves[ ] var moves : array[1..31] of byte;//holds move codes of move 1,2,3...
moveNr : byte; //index to the moves table
moveNr always points to the next move in the moves table. The moves[ ] table shows moves in the format move number.........................moving pin nr (pin1).....direction arrow The basic Search Algorithm A solution is simply a sequence of moves in the moves table.The complete moves table may be regarded as a 31 digit counter, using the 77th number system. Then the first number is 0 0 0................................0 The last number is 76 76 76 ..............................76
Regard "76" as a digit and moves[1..31] as a number of 31
digits. The moveOK question box Here the tests are made to see if Xmove is possible.testmove : //test if move is possible p1 := pinlist[xmove].p1; if board[p1] = 0 then goto nextmove; //no pin p2 := pinlist[xmove].p2; if board[p2] = 0 then goto nextmove; //no pin p3 := pinlist[xmove].p3; if board[p3] = 1 then goto nextmove; //no hole The move box Here, the move is doneboard[p1] := 0; board[p2] := 0; board[p3] := 1; moves[moveNr] := Xmove; The solved? question box //--- test for solution ---- inc(moveNr); if (moveNr = movelimit) and (board[25] = 1) then begin result := ssSolved; topmoveNr := moveNr-1; exit; end else goto start;Variable movelimit is 32 for the solitair puzzle. However, some puzzles start with less then 32 pins (or pegs), so the move number where to test for a solution is puzzle dependent. (But equal to the number of pins the game starts with). Notice the statement: result := ssSolved. Search is started by a call to function solve : function solve : TsearchState;where: type TSearchState = (ssTimeout,ssSolved,ssEnd);ssTimeout means, that the search paused to be continued later. The timeout code will be introduced later. ssEnd means that no solution was found. next move, move back... nextmove :
inc(xmove);
if xmove < 77 then goto testmove; //move range [1..76]<
moveback :
if moveNr <= BottomMoveNr then<
begin
result := ssEnd;
topmoveNr := movenr - 1;
exit;
end;
dec(moveNr); //point to previous move
m := moves[moveNr];
p1 := pinlist[m].p1;
p2 := pinlist[m].p2;
p3 := pinlist[m].p3;
board[p1] := 1;
board[p2] := 1;
board[p3] := 0;
xmove := moves[moveNr];
end;
goto nextmove;
Normally, the bottomMoveNr equals 1 and it is the lowest move
(number) that can be taken back by the search process.After manually moving pins to solve a puzzle, the bottommoveNr is incremented to "protect" our own moves. if at some point the computer is instructed to finish this game, move (numbers) below bottomMoveNr will not be taken back by the search process. In this way we can configure the board and find out if a solution is possible. In the moves[ ] table, the move (numbers) below bottomMoveNr are displayed in the color green. TimeOut Most games are solved in (much) less then a second. However, before I added some tricks, the search time was in the order of minutes. In that case it is convenient to interrupt the search and resume or terminate it. In the search process, this code was added at the beginning: begin
timeout := 100000; //interrupt timer count
stopflag := false;
start : //(re)start with 1st move
dec(timeout);
if timeout = 0 then
begin
application.ProcessMessages;
if stopflag then //test stop btn was pressed
begin>
result := ssTimeOut;
topmoveNr := moveNr - 1;
exit;
end
else timeout := 100000;
end;
xmove := 1; //1st trial move
.....................
Note: topMoveNr is the number of moves stored in the moves[ ]
table.During replay mode, moveNr points to the next move to be performed. The stopflag is set by pressing the pause button during the search. By now, the basic idea is explained and it is time to present some tricks to speedup the search. This all amounts to the avoidance of needless work, by taking advantage of the specific game characteristics. Pin Differentiation Look at position 25 on the original puzzle.A peg (pin) in this position may jump to positions 11, 23, 27, 39 and no place else. So if, at some point in the search, these positions hold no pin, a solution is not possible. Also note the corner positions 3, 5, 15, 29, 21, 35, 45, 47. These pins have to be removed from the corners, which requires a number of specific other pins to accomplish this task. With this in mind, each position on the board has been assigned a pin type. See definition below: pintype : array[1..49] of byte =
(0, 0, 3, 2, 3, 0, 0, //1: win
0, 0, 2, 1, 2, 0, 0, //2: win enable
3, 2, 3, 2, 3, 2, 3, //3: corner
2, 1, 2, 1, 2, 1, 2,
3, 2, 3, 2, 3, 2, 3,
0, 0, 2, 1, 2, 0, 0,
0, 0, 3, 2, 3, 0, 0);
Note, that a pintype of 2 is required to accomplish the last
move.Also a pintype of 2 is sacrificed to eliminate a pintype of 3 from a corner of the board. More research may shorten the search, but this is the extra code I added to the moveOK question box: //------- pin differentiation --- xp1count := p1count; xp2count := p2count; xp3count := p3count; case pintype[p2] of 1 : dec(xp1count); 2 : dec(xp2count); 3 : dec(xp3count); end;//case if (xp1count = 0) or (xp2count < xp3count) then goto nextmove;Note: variables p1count, p2count, p3count are preset at the start of the game and have to be incremented/decremented during the search process as moves are added or taken back. xp1count, xp2count, xp3count is used, because the (trial) move may still be rejected for reasons to come. p1count..p3count is only altered if the move is done. The P - Filter Note: P is an abbreviation of "permutation".The solitair puzzle has hundreds of thousend of solutions. However, many solutions are almost identical: the same moves but in a slightly different sequence. Example: ............a.b.......................versus ............b.a......................where a and b are moves. The P-filter avoids these similar solutions. Not afterward when the solution is found (as did Solitaire version 1) but already during the search process. The accelleration is enormous, especially for the more difficult puzzles. Remember, that moves[ ] is regarded as a 31 digit counter, where a digit is a move(code) ranging from 1 to 76. Let's assume that somewhere in the search, moves[ ] looks like: {...no idea if this is realistic} ( ....20, 35, 28, 0 .................) By examining the pin positions of move28 and move35, we may find that some positions coincide, so move35 was needed to obtain the board state necessary for move28. However, if the are no coinciding pin positions, then there is no difference between (...20, 35, 28,.....) and (...20, 28, 35,...) This combination must have occurred earlier in the search, move28 brings nothing new except the same solution in a little different sequence. Therefore, if moves28 and move35 share no pin positions, move 28 can be skipped and the next move to investigate is move29. The above method may be extended to moves that are further apart. Example: look at this move sequence: (...40, 50, 55, 70, 16, 0,...............) then move16 may be skipped because there is no difference with the sequence (...40, 16, 50, 55, 70,...) How to program this tric? First we need to record which move numbers modify a certain board position. And because moves are added and taken back during the search, per board position a stack is required. The top of the stack of a board position indicates the number of the last move that modified this position. Look at this data structure:
var PMStacks : array[1..49,0..30] of byte; //49 stacks of move numbers
PMHeight : array[1..49] of byte; //entries per stack
Procedure PMpush adds data to a stack:
procedure PMpush(m,nr : byte);
//push nr to stack of board position pins of move code m
var pin,h,i : byte;
begin
for i := 1 to 3 do
begin
case i of
1 : pin := pinlist[m].p1; //get pin1 of move m
2 : pin := pinlist[m].p2; // 2
3 : pin := pinlist[m].p3; // 3
end;
h := PMheight[pin] + 1;
PMstacks[pin,h] := nr;
PMheight[pin] := h;
end;
end;
Procedure PMpop extracts data from a stack:
procedure PMpop(m : byte);
//remove last entry from stack for pins of move m
var pin,x,i : byte;
begin
for i := 1 to 3 do
begin
case i of
1 : pin := pinlist[m].p1; //get pin1 of move m
2 : pin := pinlist[m].p2; // 2
3 : pin := pinlist[m].p3; // 3
end;
dec(PMheight[pin]);
end;
end;
There is no need to know the data extracted.The PMpop procedure is called only when a move has been taken back. Now, if trial move Xmove is investigated and has passed the board[ ] position- and Pin Differentiation tests, it is interesting to know at which latest move number (1..31) the pin positions of Xmove where modified. This information is supplied by function lastPmove(..)
function lastPmove(m : byte) : byte;
//find latest moveNr that changed pin positions of move m
var pin,x,i : byte;
begin
result := 0;
for i := 1 to 3 do
begin
case i of
1 : pin := pinlist[m].p1;
2 : pin := pinlist[m].p2;
3 : pin := pinlist[m].p3;
end;
x := PMstacks[pin,PMheight[pin]]; //x=latest moveNr modifying p1,p2,p3
if x <> 0 then
if x > result then result := x;
end;
end;
The test (to skip or allow a move) now becomes
//------- P filter -------------
if form1.Pfilter1.Checked then
begin
pm := lastPmove(Xmove) + 1;// +1 is the first independent move
while pm < moveNr do
begin
if moves[pm] > Xmove then goto nextmove;//Xmove(code) is smaller,occurred earlier
inc(pm);
end;
end;
This concludes the description of the Peg Solitaire search
algorithm.
const keys : array[1..49] of byte = //xor of all pins must be 2 =
(0, 0, 1, 2, 3, 0, 0, //middle value
0, 0, 2, 3, 1, 0, 0,
1, 2, 3, 1, 2, 3, 1,
2, 3, 1, 2, 3, 1, 2,
3, 1, 2, 3, 1, 2, 3,
0, 0, 3, 1, 2, 0, 0,
0, 0, 1, 2, 3, 0, 0);
A move always changes 3 pin positions, which have key values
1,2,3.......(not necessarily in this order).
procedure initsearchmode;
//called when search mode is selected
//test pin keys for "no solution"
//set movelimit (= last possible move)
var i : byte;
xrsum : byte;
begin
moveNr := topmoveNr+1;
drawmovepointer(moveNr);//places arrow in moves display
movelimit := GetPincount + topMoveNr;
clearprintlist;
xrsum := 2;
for i := 1 to 49 do
if board[i] = 1 then xrsum := xrsum xor keys[i];
if xrsum <> 0 then form1.msgbox.Caption := 'warning: no solution';
end;
Click [here] to go to the Solitaire game page. Click [here] to load the complete Delphi-7 project. David Dirkse
|
||||||||||||||||||||||||||||||||
![]() |
![]() |