I am trying to solve Peg Solitaire with a depth-first search algorithm – it should be possible to solve the game since "modern computers can easily examine all game positions in a reasonable time". Even after 23 hours, the algorithm didn't find any solution. I did a web search and found the paper "Depth-first search solves peg solitaire". I tried the c-program from the paper and the first solution was found immediately after the program started. I compared the algorithms. The main difference between the algorithms is the way they find possible peg-jumps. While my algorithm searches the board for holes from top left to bottom right (each hole is checked if there are possible jumps), the paper-algorithm searches the board for pegs from top left to bottom right (each peg is checked if there are possible jumps). Both algorithms are trying the jumps in the order they are found. To underline the difference:
Question: Why is there such a giant (not solvable / immediately solved) difference on how to search the board for jumps? Why is it better to check the pegs instead of checking the holes for possible jumps?
You can try the algorithm with following C++ program. To keep it compact I have removed the lesser important parts (printing the board, generating the initial bitboard, ...).
#include <iostream>
#include <ctime>
#include <vector>
#include <utility>
#include <ctime>
typedef unsigned __int64 ui64;
typedef std::vector<std::pair<int, int> > vecjumps; // first=from, second=to
ui64 moves = 0; // Number of moves made so far
int solutions = 0; // Number of solutions found so far
clock_t start; // To measure time
// Bitboard
// Board Bits
// ------------------------------
// *** 02 03 04
// *** 10 11 12
// ******* 16 17 18 19 20 21 22
// ***o*** 24 25 26 27 28 29 30
// ******* 32 33 34 35 36 37 38
// *** 42 43 44
// *** 50 51 52
const ui64 bitboard = 0x001c1c7f7f7f1c1c; // 1ULL << 2 | 1ULL << 3 | ...
ui64 board = bitboard & ~(1ULL << 27); // Initial Board: Bit 27 <=> Hole
// To try the hole-version: Swap Commented and Uncommented parts
vecjumps getMoves(ui64 b)
{
// Find the possible jumps through bit-shift operations. mr <=> right jump
// possibilities. The inverted Board represents the Holes. Shifting the
// board by 16 right/left --> moving all pegs up/down. Shifting the board
// by 1 --> moving all pegs left/right
//ui64 mr = (((b >> 1) & b) << 2) & ~b & bitboard;
//ui64 md = (((b >> 8) & b) << 16) & ~b & bitboard;
//ui64 ml = (((b >> 1) & b) >> 1) & ~b & bitboard;
//ui64 mu = (((b >> 8) & b) >> 8) & ~b & bitboard;
ui64 mr = ((((b >> 1) & b) << 2) & ~b & bitboard) >> 2;
ui64 md = ((((b >> 8) & b) << 16) & ~b & bitboard) >> 16;
ui64 ml = ((((b >> 1) & b) >> 1) & ~b & bitboard) << 2;
ui64 mu = ((((b >> 8) & b) >> 8) & ~b & bitboard) << 16;
vecjumps jumps;
jumps.reserve(12);
for (int i = 2; i < 53; i++)
{
//if (mr & (1ULL << i)) jumps.push_back(std::make_pair(i - 2, i));
//if (md & (1ULL << i)) jumps.push_back(std::make_pair(i - 16, i));
//if (ml & (1ULL << i)) jumps.push_back(std::make_pair(i + 2, i));
//if (mu & (1ULL << i)) jumps.push_back(std::make_pair(i + 16, i));
if (mr & (1ULL << i)) jumps.push_back(std::make_pair(i, i + 2));
if (md & (1ULL << i)) jumps.push_back(std::make_pair(i, i + 16));
if (ml & (1ULL << i)) jumps.push_back(std::make_pair(i, i - 2));
if (mu & (1ULL << i)) jumps.push_back(std::make_pair(i, i - 16));
}
return jumps;
}
void makeMove(ui64& b, int from, int to)
{
// Through a xor-instruction, a jump from 11 to 27 includes 19
// 19 = (11 + 27) / 2
b ^= 1ULL << from | 1ULL << (from + to) / 2 | 1ULL << to;
moves++;
if (moves % 10000000 == 0)
printf("(%8d, %14llu moves, %lf)\n",
solutions, moves, (double)(clock() - start) / CLOCKS_PER_SEC);
}
// Depth First Search Algorithm
bool findSolution(int depth)
{
if (!depth) return ((1ULL << 27) & board);
vecjumps jumps = getMoves(board);
for (vecjumps::const_iterator cit = jumps.begin(); cit != jumps.end();
++cit)
{
ui64 copy = board;
makeMove(board, (*cit).first, (*cit).second);
if (findSolution(depth - 1)) solutions++;
board = copy;
}
return false;
}
int main()
{
start = clock();
findSolution(31);
printf("(%8d, %14llu moves, %lf)\n",
solutions, moves, (double)(clock() - start) / CLOCKS_PER_SEC);
return 0;
}
The minimal board is solvable for the initial state of either end being vacant, but if the middle hole is vacant there are no legal moves so the complement cannot be solved. This complement problem is the main goal when playing peg solitaire so its solvability plays a large part in what boards are played on.
The only place it is possible to end up with a solitary peg is the centre, or the middle of one of the edges; on the last jump, there will always be an option of choosing whether to end in the centre or the edge.
If there are no loops in the resulting tree in either method, and the resulting tree is the same, the reason for the hudge difference when aplying the same search algorithm must be the order in which the nodes are expanded (heuristic). I'm still checking your implementation but everything seems right on it, so I can't see any other reason for the speed difference.
Some time ago I had an assigment on this problem, and I found this on my bookmarks: You can read more info, depth search vs breadth search and a couple of heuristics to improve the search here: http://www.durangobill.com/Peg33.html
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With