Cracker Barrel
August 19, 2014
We number the holes as shown below, and start with the initial vacancy at hole 1:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
A move is written as a list containing the from hole, jump hole and to hole, so the two possible starting moves are (4 2 1) and (6 3 1). A board is a list of occupied holes followed by a list of moves taken to reach the board (most recent move first). For instance, after starting with (4 2 1), continuing with (6 5 4) and then (1 3 6), the board looks like this: ((4 6 7 8 9 10 11 12 13 14 15) (4 2 1) (6 5 4) (1 3 6)). There are 36 possible moves:
(define possibles '((1 2 4) (1 3 6) (2 4 7) (2 5 9) (3 5 8)
(3 6 10) (4 2 1) (4 5 6) (4 7 11) (4 8 13) (5 8 12) (5 9 14)
(6 3 1) (6 5 4) (6 9 13) (6 10 15) (7 4 2) (7 8 9) (8 5 3)
(8 9 10) (9 5 2) (9 8 7) (10 6 3) (10 9 8) (11 7 4)
(11 12 13) (12 8 5) (12 13 14) (13 8 4) (13 9 6) (13 12 11)
(13 14 15) (14 9 5) (14 13 12) (15 10 6) (15 14 13)))
(define init '((2 3 4 5 6 7 8 9 10 11 12 13 14 15)))
A move is feasible if the from hole is occupied, the jump hole is occupied, and the to hole is vacant:
(define (feasible? board move)
(and (member (car move) (car board))
(member (cadr move) (car board))
(not (member (caddr move) (car board)))))
A jump takes a board and a move and returns a new board, if the move is feasible, or #f
:
(define (jump board move)
(and (feasible? board move)
(cons (insert (caddr move)
(remove (car move)
(remove (cadr move) (car board))))
(cons move (cdr board)))))
Function all-jumps
returns a list of all boards resulting from the possible jumps on a given board:
(define (all-jumps board)
(filter identity
(map (lambda (move) (jump board move))
possibles)))
Now our solution iterates 13 times, collecting after each round all possible boards:
(define (solve init)
(do ((k 13 (- k 1))
(boards (list init) (mappend all-jumps boards)))
((zero? k) boards)))
We find 29760 solutions, of which 6816 end with the peg in the initially-vacant hole:
> (let ((solutions (solve init)))
(display (length solutions)) (newline)
(display (length (filter (lambda (x) (equal? (car x) '(1))) solutions))) (newline)
(display (list-ref solutions 17493)) (newline))
29760
6816
((13) (11 12 13) (14 13 12) (6 9 13) (2 5 9) (7 4 2) (15 10 6) (3 6 10) (13 9 6) (1 2 4) (12 8 5) (10 6 3) (4 5 6) (6 3 1))
You can run the program at http://programmingpraxis.codepad.org/HjimquyE, where you will also see the contributions from the Standard Prelude. There is more to this puzzle than we have discussed here; if you are interested, look at George Bell’s site. We may return to this puzzle in future exercises.
That’s amusing. About a month and a half ago, I came across another site that had the same challenge, so I worked it out then:
Cracker Barrel Peg Game – framework and basic solver
Cracker Barrel Peg Game, Part 2 – which solutions are most solvable
Cracker Barrel Peg Game, Part 3 – interactive command line game
Although I will admit I don’t have anything that will end in a specific win state. It should be only a single line to add though.
(All three in Racket.)
I remember writing a solver for that puzzle long ago. I was still living with my parents, and I was writing in C, so that dates it around 1981-84. Of course, the code is long gone.
I think this is slightly slower – but slight more compact code…
In Python.
Recursive backtracking solution with memoization, using a bitset for the board. I was going to keep track of the moves in a tree structure, but thought it would be more fun to output a .dot file – see http://matthewarcus.files.wordpress.com/2014/08/puzzle.png for the (2Mb) result.
That should have been:
(Last minute “improvement” – means the same of course).
The solution that you posted above seems to timeout on my machine and on codepad. I also have a scheme solution that is timing out at the moment, attempting to debug it.
It is slow because it must form all possible solutions. It times out at both codepad and ideone, but runs to completion on my machine at work:
I didn’t think that you were wrong, was just curious. I was using the mit-scheme interpreter, which apparently has very low memory limits. Switch to Gauche the program ran fine.
My C++ program takes about 1.5ms to generate the .dot file (perf reckons about 2000000 cycles). As well as the obvious left-right symmetry, there’s also a forward and backward symmetry, swapping pegs for holes, which might save some time.
So I was having trouble with my scheme implemenation, it got to the point where I was just swapping in all of the functions that you wrote and I ran them and was still getting the incorrect answer. It turns out it was the way that I was encoding the data. If you run your program with data that marks the first peg as position 0 the results are skewed. Still haven’t figured out why.
Results:
40520
5736
((14) (12 13 14) (10 11 12) (5 8 11) (6 7 8) (11 12 13) (0 2 5) (1 3 6) (9 8 7) (5 4 3) (14 13 12) (6 3 1) (12 7 3) (3 1 0))
(define possible-moves
‘((0 1 3) (0 2 5)
(1 3 6) (1 4 8)
(2 4 7) (2 5 9)
(3 1 0) (3 4 5) (3 6 10) (3 7 12)
(4 7 11) (4 8 13)
(5 2 0) (5 4 3) (5 8 11) (5 9 14)
(6 3 1) (6 7 8)
(7 4 2) (7 8 9)
(8 4 1) (8 7 6)
(9 5 2) (9 8 7)
(10 6 3) (10 11 12)
(11 7 4) (11 12 13)
(12 7 3) (12 8 5) (12 11 10) (12 13 14)
(13 12 11) (13 8 4)
(14 9 5) (14 13 12)))
The initial board is also different due to the zero encoding:
(define init ‘((1 2 3 4 5 6 7 8 9 10 11 12 13 14)))
That (5 8 11) should be (5 8 12)
…………………………………………. That is what I get for drawing a shoddy triangle in my notes. Thank you.
Pleasure. Here’s some general code for the lower to higher moves:
Awesome, here is a solution using your standard prelude but with the data format a little more compressed:
So here is my code. It’s rough but it’s correct.
__author__ = ‘Christophe De Troyer’
# Define the the puzzle with 14 pegs and 1 hole.
# 0 = no peg, 1 = peg
INITIAL_BOARD = [0] + [1] * 14
# Define all possible moves.
moves = [
[0, 1, 3],
[0, 2, 5],
[1, 3, 6],
[1, 4, 8],
[2, 4, 7],
[2, 5, 9],
[3, 4, 5],
[3, 6, 10],
[3, 7, 12],
[4, 7, 11],
[4, 8, 13],
[5, 8, 12],
[5, 9, 14],
[6, 7, 8],
[7, 8, 9],
[10, 11, 12],
[11, 12, 13],
[12, 13, 14],
]
TOTALMOVES = []
global total_solutions
total_solutions = 0
global total_with_zero_peg
total_with_zero_peg = 0
for m in moves:
# Add initial move.
TOTALMOVES.append(m)
# Swap the move.
m2 = list(m)
temp = m2[0]
m2[0] = m2[2]
m2[2] = temp
TOTALMOVES.append(m2)
# A move can be made iff the right index in the list contains a False value in
# the list (puzzle).
def can_make_move(move, board):
if board[move[0]] == 1 and board[move[1]] == 1 and board[move[2]] \
== 0:
return True
return False
# A move is made by marking the index contained in the 2nd position of the move
# as true in the puzzle list and the first position as false.
def make_move(move, board):
new_board = list(board)
# Move the peg.
new_board[move[2]] = 1
new_board[move[0]] = 0
# Remove the peg that just got jumped over.
new_board[move[1]] = 0
return new_board
# A game is finished once the sum of the list is 1.
def is_finished(board):
return sum(board) == 1
# Returns a list of all the moves we can make in the current game state.
def yield_moves(board):
for move in TOTALMOVES:
if can_make_move(move, board):
yield move
def solve(board=INITIAL_BOARD, moves_done=[]):
# If the game is done, print out the moves.
global total_solutions, total_with_zero_peg
if is_finished(board):
total_solutions += 1
if board[0] == 1:
total_with_zero_peg += 1
else:
for move in yield_moves(board):
solve(make_move(move, board), moves_done + [m])
solve(INITIAL_BOARD, [])
print ‘Total solutions found: {0}:’.format(total_solutions)
print ‘Total solutions found with a peg in the zero hole: {0}’.format(total_with_zero_peg)
Another solution using Java Fork/Join framework:
Total solutions: 29760
Total solutions with peg in top hole: 6816
duration: 244
This is similar to the solver I wrote in the early 1980s.
I’d been sitting on this trying to get access to a fast server for some benchmarking. But I didn’t get the server, and it doesn’t matter. Suffice it to say that this solver runs in milliseconds.