Download the raw code.
#!/usr/local/bin/perl6
## ======================================================================== ##
## Hex Puzzle solver
##
## This code uses a variant of A-star search to try to find a reasonable
## and efficient set of moves to solve the puzzle. For speed purposes,
## I rely on a set of precomputed tables to make it easy to determine
## piece collisions and movement possibilities.
## ======================================================================== ##
## ------------------------------------------------------------------------ ##
## Resource Tracking Bitmaps
##
## Represent all piece positions as 38-bit bitmaps. Two pieces coexist
## if the bit-wise and between their bitmaps is 0.
##
## Bit position assignments are in left-to-right order from the original
## hexagon graph, starting with i1 in bit 37 and ending with o5 in bit 0.
## ------------------------------------------------------------------------ ##
my %piece_bmp =
(
# iiiii jjjjjj kkkkk llllll mmmmm nnnnnn ooooo
# 12345 123456 12345 123456 12345 123456 12345
i12 => 0b11000_000000_00000_000000_00000_000000_00000,
i23 => 0b01100_000000_00000_000000_00000_000000_00000,
i34 => 0b00110_000000_00000_000000_00000_000000_00000,
i45 => 0b00011_000000_00000_000000_00000_000000_00000,
i123 => 0b11100_000000_00000_000000_00000_000000_00000,
i234 => 0b01110_000000_00000_000000_00000_000000_00000,
i345 => 0b00111_000000_00000_000000_00000_000000_00000,
j12 => 0b00000_110000_00000_000000_00000_000000_00000,
j23 => 0b00000_011000_00000_000000_00000_000000_00000,
j34 => 0b00000_001100_00000_000000_00000_000000_00000,
j45 => 0b00000_000110_00000_000000_00000_000000_00000,
j56 => 0b00000_000011_00000_000000_00000_000000_00000,
j123 => 0b00000_111000_00000_000000_00000_000000_00000,
j234 => 0b00000_011100_00000_000000_00000_000000_00000,
j345 => 0b00000_001110_00000_000000_00000_000000_00000,
j456 => 0b00000_000111_00000_000000_00000_000000_00000,
k12 => 0b00000_000000_11000_000000_00000_000000_00000,
k23 => 0b00000_000000_01100_000000_00000_000000_00000,
k34 => 0b00000_000000_00110_000000_00000_000000_00000,
k45 => 0b00000_000000_00011_000000_00000_000000_00000,
k123 => 0b00000_000000_11100_000000_00000_000000_00000,
k234 => 0b00000_000000_01110_000000_00000_000000_00000,
k345 => 0b00000_000000_00111_000000_00000_000000_00000,
l12 => 0b00000_000000_00000_110000_00000_000000_00000,
l23 => 0b00000_000000_00000_011000_00000_000000_00000,
l34 => 0b00000_000000_00000_001100_00000_000000_00000,
l45 => 0b00000_000000_00000_000110_00000_000000_00000,
l56 => 0b00000_000000_00000_000011_00000_000000_00000,
l123 => 0b00000_000000_00000_111000_00000_000000_00000,
l234 => 0b00000_000000_00000_011100_00000_000000_00000,
l345 => 0b00000_000000_00000_001110_00000_000000_00000,
l456 => 0b00000_000000_00000_000111_00000_000000_00000,
m12 => 0b00000_000000_00000_000000_11000_000000_00000,
m23 => 0b00000_000000_00000_000000_01100_000000_00000,
m34 => 0b00000_000000_00000_000000_00110_000000_00000,
m45 => 0b00000_000000_00000_000000_00011_000000_00000,
m123 => 0b00000_000000_00000_000000_11100_000000_00000,
m234 => 0b00000_000000_00000_000000_01110_000000_00000,
m345 => 0b00000_000000_00000_000000_00111_000000_00000,
n12 => 0b00000_000000_00000_000000_00000_110000_00000,
n23 => 0b00000_000000_00000_000000_00000_011000_00000,
n34 => 0b00000_000000_00000_000000_00000_001100_00000,
n45 => 0b00000_000000_00000_000000_00000_000110_00000,
n56 => 0b00000_000000_00000_000000_00000_000011_00000,
n123 => 0b00000_000000_00000_000000_00000_111000_00000,
n234 => 0b00000_000000_00000_000000_00000_011100_00000,
n345 => 0b00000_000000_00000_000000_00000_001110_00000,
n456 => 0b00000_000000_00000_000000_00000_000111_00000,
o12 => 0b00000_000000_00000_000000_00000_000000_11000,
o23 => 0b00000_000000_00000_000000_00000_000000_01100,
o34 => 0b00000_000000_00000_000000_00000_000000_00110,
o45 => 0b00000_000000_00000_000000_00000_000000_00011,
o123 => 0b00000_000000_00000_000000_00000_000000_11100,
o234 => 0b00000_000000_00000_000000_00000_000000_01110,
o345 => 0b00000_000000_00000_000000_00000_000000_00111,
# edcba fedcba fedcb gfedcb gfedc hgfedc hgfed
# 11111 122222 23333 134444 24555 135666 24677
a12 => 0b00001_000001_00000_000000_00000_000000_00000,
# 1 2 3 4
b12 => 0b00010_000010_00000_000000_00000_000000_00000,
b23 => 0b00000_000010_00001_000000_00000_000000_00000,
b34 => 0b00000_000000_00001_000001_00000_000000_00000,
b123 => 0b00010_000010_00001_000000_00000_000000_00000,
b234 => 0b00000_000010_00001_000001_00000_000000_00000,
# 1 2 3 4 5 6
c12 => 0b00100_000100_00000_000000_00000_000000_00000,
c23 => 0b00000_000100_00010_000000_00000_000000_00000,
c34 => 0b00000_000000_00010_000010_00000_000000_00000,
c45 => 0b00000_000000_00000_000010_00001_000000_00000,
c56 => 0b00000_000000_00000_000000_00001_000001_00000,
c123 => 0b00100_000100_00010_000000_00000_000000_00000,
c234 => 0b00000_000100_00010_000010_00000_000000_00000,
c345 => 0b00000_000000_00010_000010_00001_000000_00000,
c456 => 0b00000_000000_00000_000010_00001_000001_00000,
# 1 2 3 4 5 6 7
d12 => 0b01000_001000_00000_000000_00000_000000_00000,
d23 => 0b00000_001000_00100_000000_00000_000000_00000,
d34 => 0b00000_000000_00100_000100_00000_000000_00000,
d45 => 0b00000_000000_00000_000100_00010_000000_00000,
d56 => 0b00000_000000_00000_000000_00010_000010_00000,
d67 => 0b00000_000000_00000_000000_00000_000010_00001,
d123 => 0b01000_001000_00100_000000_00000_000000_00000,
d234 => 0b00000_001000_00100_000100_00000_000000_00000,
d345 => 0b00000_000000_00100_000100_00010_000000_00000,
d456 => 0b00000_000000_00000_000100_00010_000010_00000,
d567 => 0b00000_000000_00000_000000_00010_000010_00001,
# 1 2 3 4 5 6 7
e12 => 0b10000_010000_00000_000000_00000_000000_00000,
e23 => 0b00000_010000_01000_000000_00000_000000_00000,
e34 => 0b00000_000000_01000_001000_00000_000000_00000,
e45 => 0b00000_000000_00000_001000_00100_000000_00000,
e56 => 0b00000_000000_00000_000000_00100_000100_00000,
e67 => 0b00000_000000_00000_000000_00000_000100_00010,
e123 => 0b10000_010000_01000_000000_00000_000000_00000,
e234 => 0b00000_010000_01000_001000_00000_000000_00000,
e345 => 0b00000_000000_01000_001000_00100_000000_00000,
e456 => 0b00000_000000_00000_001000_00100_000100_00000,
e567 => 0b00000_000000_00000_000000_00100_000100_00010,
# 1 2 3 4 5 6
f12 => 0b00000_100000_10000_000000_00000_000000_00000,
f23 => 0b00000_000000_10000_010000_00000_000000_00000,
f34 => 0b00000_000000_00000_010000_01000_000000_00000,
f45 => 0b00000_000000_00000_000000_01000_001000_00000,
f56 => 0b00000_000000_00000_000000_00000_001000_00100,
f123 => 0b00000_100000_10000_010000_00000_000000_00000,
f234 => 0b00000_000000_10000_010000_01000_000000_00000,
f345 => 0b00000_000000_00000_010000_01000_001000_00000,
f456 => 0b00000_000000_00000_000000_01000_001000_00100,
# 1 2 3 4
g12 => 0b00000_000000_00000_100000_10000_000000_00000,
g23 => 0b00000_000000_00000_000000_10000_010000_00000,
g34 => 0b00000_000000_00000_000000_00000_010000_01000,
g123 => 0b00000_000000_00000_100000_10000_010000_00000,
g234 => 0b00000_000000_00000_000000_10000_010000_01000,
# 1 2
h34 => 0b00000_000000_00000_000000_00000_100000_10000,
# pqrst pqrstu qrstu qrstuv rstuv rstuvw stuvw
# 24677 135666 24555 134444 23333 122222 11111
p12 => 0b10000_100000_00000_000000_00000_000000_00000,
# 4 3 2 1
q12 => 0b00000_000000_10000_100000_00000_000000_00000,
q23 => 0b00000_010000_10000_000000_00000_000000_00000,
q34 => 0b01000_010000_00000_000000_00000_000000_00000,
q123 => 0b00000_010000_10000_100000_00000_000000_00000,
q234 => 0b01000_010000_10000_000000_00000_000000_00000,
# 6 5 4 3 2 1
r12 => 0b00000_000000_00000_000000_10000_100000_00000,
r23 => 0b00000_000000_00000_010000_10000_000000_00000,
r34 => 0b00000_000000_01000_010000_00000_000000_00000,
r45 => 0b00000_001000_01000_000000_00000_000000_00000,
r56 => 0b00100_001000_00000_000000_00000_000000_00000,
r123 => 0b00000_000000_00000_010000_10000_100000_00000,
r234 => 0b00000_000000_01000_010000_10000_000000_00000,
r345 => 0b00000_001000_01000_010000_00000_000000_00000,
r456 => 0b00100_001000_01000_000000_00000_000000_00000,
# 7 6 5 4 3 2 1
s12 => 0b00000_000000_00000_000000_00000_010000_10000,
s23 => 0b00000_000000_00000_000000_01000_010000_00000,
s34 => 0b00000_000000_00000_001000_01000_000000_00000,
s45 => 0b00000_000000_00100_001000_00000_000000_00000,
s56 => 0b00000_000100_00100_000000_00000_000000_00000,
s67 => 0b00010_000100_00000_000000_00000_000000_00000,
s123 => 0b00000_000000_00000_000000_01000_010000_10000,
s234 => 0b00000_000000_00000_001000_01000_010000_00000,
s345 => 0b00000_000000_00100_001000_01000_000000_00000,
s456 => 0b00000_000100_00100_001000_00000_000000_00000,
s567 => 0b00010_000100_00100_000000_00000_000000_00000,
# 7 6 5 4 3 2 1
t12 => 0b00000_000000_00000_000000_00000_001000_01000,
t23 => 0b00000_000000_00000_000000_00100_001000_00000,
t34 => 0b00000_000000_00000_000100_00100_000000_00000,
t45 => 0b00000_000000_00010_000100_00000_000000_00000,
t56 => 0b00000_000010_00010_000000_00000_000000_00000,
t67 => 0b00001_000010_00000_000000_00000_000000_00000,
t123 => 0b00000_000000_00000_000000_00100_001000_01000,
t234 => 0b00000_000000_00000_000100_00100_001000_00000,
t345 => 0b00000_000000_00010_000100_00100_000000_00000,
t456 => 0b00000_000010_00010_000100_00000_000000_00000,
t567 => 0b00001_000010_00010_000000_00000_000000_00000,
# 6 5 4 3 2 1
u12 => 0b00000_000000_00000_000000_00000_000100_00100,
u23 => 0b00000_000000_00000_000000_00010_000100_00000,
u34 => 0b00000_000000_00000_000010_00010_000000_00000,
u45 => 0b00000_000000_00001_000010_00000_000000_00000,
u56 => 0b00000_000001_00001_000000_00000_000000_00000,
u123 => 0b00000_000000_00000_000000_00010_000100_00100,
u234 => 0b00000_000000_00000_000010_00010_000100_00000,
u345 => 0b00000_000000_00001_000010_00010_000000_00000,
u456 => 0b00000_000001_00001_000010_00000_000000_00000,
# 4 3 2 1
v12 => 0b00000_000000_00000_000000_00000_000010_00010,
v23 => 0b00000_000000_00000_000000_00001_000010_00000,
v34 => 0b00000_000000_00000_000001_00001_000000_00000,
v123 => 0b00000_000000_00000_000000_00001_000010_00010,
v234 => 0b00000_000000_00000_000001_00001_000010_00000,
# 2 1
w12 => 0b00000_000000_00000_000000_00000_000001_00001,
);
## ------------------------------------------------------------------------ ##
## Movement Connectivity
##
## %succ moves a piece toward the next higher index in its groove.
## %pred moves a piece toward the next lower index.
##
## A move in a particular direction is allowed if and only if
## .contains($curr) == True for that direction.
## ------------------------------------------------------------------------ ##
my %succ =
(
i12 => 'i23', i23 => 'i34', i34 => 'i45',
j12 => 'j23', j23 => 'j34', j34 => 'j45', j45 => 'j56',
k12 => 'k23', k23 => 'k34', k34 => 'k45',
l12 => 'l23', l23 => 'l34', l34 => 'l45', l45 => 'l56',
m12 => 'm23', m23 => 'm34', m34 => 'm45',
n12 => 'n23', n23 => 'n34', n34 => 'n45', n45 => 'n56',
o12 => 'o23', o23 => 'o34', o34 => 'o45',
i123 => 'i234', i234 => 'i345',
j123 => 'j234', j234 => 'j345', j345 => 'j456',
k123 => 'k234', k234 => 'k345',
l123 => 'l234', l234 => 'l345', l345 => 'l456',
m123 => 'm234', m234 => 'm345',
n123 => 'n234', n234 => 'n345', n345 => 'n456',
o123 => 'o234', o234 => 'o345',
b12 => 'b23', b23 => 'b34',
c12 => 'c23', c23 => 'c34', c34 => 'c45', c45 => 'c56',
d12 => 'd23', d23 => 'd34', d34 => 'd45', d45 => 'd56', d56 => 'd67',
e12 => 'e23', e23 => 'e34', e34 => 'e45', e45 => 'e56', e56 => 'e67',
f12 => 'f23', f23 => 'f34', f34 => 'f45', f45 => 'f56',
g12 => 'g23', g23 => 'g34',
b123 => 'b234',
c123 => 'c234', c234 => 'c345', c345 => 'c456',
d123 => 'd234', d234 => 'd345', d345 => 'd456', d456 => 'd567',
e123 => 'e234', e234 => 'e345', e345 => 'e456', e456 => 'e567',
f123 => 'f234', f234 => 'f345', f345 => 'f456',
g123 => 'g234',
q12 => 'q23', q23 => 'q34',
r12 => 'r23', r23 => 'r34', r34 => 'r45', r45 => 'r56',
s12 => 's23', s23 => 's34', s34 => 's45', s45 => 's56', r56 => 'r67',
t12 => 't23', t23 => 't34', t34 => 't45', t45 => 't56', s56 => 's67',
u12 => 'u23', u23 => 'u34', u34 => 'u45', u45 => 'u56',
v12 => 'v23', v23 => 'v34',
q123 => 'q234',
r123 => 'r234', r234 => 'r345', r345 => 'r456',
s123 => 's234', s234 => 's345', s345 => 's456', s456 => 's567',
t123 => 't234', t234 => 't345', t345 => 't456', t456 => 't567',
u123 => 'u234', u234 => 'u345', u345 => 'u456',
v123 => 'v234',
);
my %pred = %succ.invert;
## ------------------------------------------------------------------------ ##
## Helper function: Render a from/to as a move in desired output format
## ------------------------------------------------------------------------ ##
sub render_move(Str $from, Str $to)
{
return $from.substr(0, 1)
~ "[" ~ $from.substr(1) ~ " -> " ~ $to.substr(1) ~ "]";
}
## ------------------------------------------------------------------------ ##
## Priority Queue
##
## This is a simple priority queue class. It uses a hash, keying on the
## "metric" value for each entry. It returns the key with the lowest
## metric.
##
## The only requirement prio-queue places on its input is that it must
## support a method named "metric".
## ------------------------------------------------------------------------ ##
class prio-queue {
has %!q;
has $!elems;
method push($item) {
my $key = $item.metric;
$!elems++;
%!q{$key} = my @q if !%!q.contains($key);
%!q{$key}.push( $item );
}
method pop() {
return if @( %!q.keys ).elems == 0;
# "There is a min_key in my rhim." -- Inspector Clousseau
my $min_key = %!q.keys.min( :by({ $^a <=> $^b }) );
my $best = @( %!q.{$min_key} ).shift;
%!q.delete($min_key) if ( ! %!q.{$min_key}.elems );
$!elems--;
return $best;
}
method unshift($item) { return self.push($item); }
method shift() { return self.pop(); }
method elems() {
return $!elems;
}
};
## ------------------------------------------------------------------------ ##
## A-star Search
##
## Note that this implementation goes light on the abstraction. That can
## always be added later when it's time to reuse the code.
##
## The class a-star-node encapsulates a single state node from the
## state tree search. The class a-star encapsulates knowledge of visited
## states and handles iterating the actual a-star search.
## ------------------------------------------------------------------------ ##
class a-star-node
{
has Int $!cost;
has Str $!move;
has Int $!in_use;
has @!pieces;
has Bool $!bullet;
has Str $!parent;
has Int $!est_cost;
method Str() {
return @!pieces.join();
}
method is_goal() {
return @!pieces[0] eq 'l56';
}
method add_piece(Str $piece) {
# This is used only when populating start nodes from the input
say "Invalid piece name." and exit(1)
if !%piece_bmp.contains($piece);
my $new_bmp = %piece_bmp{$piece};
say "Overlapping pieces." and exit(1) if $!in_use +& $new_bmp;
$!in_use +|= $new_bmp;
if ($piece eq 'l12') {
$!bullet = True;
unshift @!pieces, $piece;
} else {
say "No solution." and exit(1) if $piece.substr(0,1) eq 'l';
push @!pieces, $piece;
}
}
method has_bullet {
# This is used only when populating start nodes from the input
return $!bullet;
}
method get_moves {
my (Int $piece, Int $cost, Str $orig, Str $curr, Int $others,
Int $p, $next, @moves);
## Try to move each piece as far backward as possible,
## then try to move each piece as far forward as possible.
##
## Cost of the move is 1 + # of segments.
##
## Insert the options into the result buffer in reverse
## order, so the first move we consider is the one that
## moves the bullet the furthest right.
for $(%pred), $(%succ) -> $next {
for @!pieces.elems-1 ... 0 -> $p {
my @p_copy = @!pieces;
$curr = $orig = @!pieces[$p];
$cost = $!cost + 1;
$others = $!in_use +^ %piece_bmp{$orig};
while ( $next.contains($curr) ) {
$curr = $next{$curr};
$cost++;
if ($others +& %piece_bmp{$curr}) == 0 {
@p_copy[$p] = $curr;
@moves.unshift(
a-star-node.new(
cost => $cost,
move => render_move($orig, $curr),
in_use => $others +| %piece_bmp{$curr},
pieces => @p_copy,
bullet => $!bullet,
parent => self.Str
)
)
} else {
last;
}
}
}
}
return @moves;
}
method get_parent { return $!parent; }
method get_move { return $!move; }
method tot_cost_to_here {
# Actual cost incurred getting to this node.
return $!cost;
}
method est_cost_to_goal {
return $!est_cost if $!est_cost > 0;
# Estimated cost to get from this node to the goal state.
#
# Our heuristic:
# -- Number of slots the bullet needs to slide, plus
# -- Number of other piece segments overlapping l1 .. l6 to
# the right of the bullet, plus
# -- 1, to cover the minimum cost of any move.
#
# Because we start calculating "overlapping piece segments"
# on the right-edge of the "bullet", we don't need to add 1
# explicitly.
my Int $bull_pos = @!pieces[0].substr(1,1).Int;
my Int $in_way = 0;
my Int $bit_num;
for (21 - $bull_pos) ... 16 -> $bit_num {
$in_way += ($!in_use +> $bit_num) +& 1;
}
return $!est_cost = ( 5 - $bull_pos ) + $in_way;
}
method metric {
return self.est_cost_to_goal + $!cost;
}
submethod BUILD
(
:$!cost = 0,
:$!move = '',
:$!in_use = 0,
:@!pieces = ( ),
:$!bullet = False,
:$!parent = 'START',
:$!est_cost = -1
) {
}
};
class a-star
{
has %!visited;
has $!frontier;
has $!best_goal;
method !visit(a-star-node $state) {
my Str $str = $state.Str;
my Bool $was_visited = %!visited.contains($str);
%!visited{$str} = $state
if (!$was_visited ||
$state.tot_cost_to_here < %!visited{$str}.tot_cost_to_here);
$!best_goal = $state
if $state.is_goal &&
(!$!best_goal.defined ||
$state.tot_cost_to_here < $!best_goal.tot_cost_to_here);
return $was_visited;
}
method !best_path(a-star-node $end) {
my @moves;
my $state = $end.Str;
while (%!visited.contains($state)) {
@moves.unshift( %!visited{$state}.get_move );
$state = %!visited{$state}.get_parent;
}
shift @moves; ## remove extra move from last iter
return @moves;
}
method search {
my (a-star-node $node, a-star-node $exit);
my (@exits, $curr);
while ($!frontier.elems > 0) {
$node = $!frontier.pop;
$curr = $node.Str;
if $node.is_goal {
return self!best_path($!best_goal);
}
@exits = $node.get_moves;
for @exits -> $exit {
$!frontier.push($exit) if self!visit($exit) == 0;
}
}
return @( );
}
submethod BUILD(:$start) {
my $!frontier = prio-queue.new;
$!frontier.push($start); ## like an old Ford
self!visit($start);
}
};
## ======================================================================== ##
## MAIN PROGRAM
## ======================================================================== ##
sub MAIN
{
my $start = a-star-node.new;
for lines() {
$start.add_piece( $_ );
}
say "No bullet specified" and exit(1) if !$start.has_bullet;
my $a-star = a-star.new(start => $start);
my @path = $a-star.search;
if @path.elems {
say @path.join("\n");
} else {
say "No solution.";
}
}
This version introduces the $!best_goal
attribute into the A star search, to
make it return not just the path to the latest goal configuration found, but
the path to the best configuration found so far.
As you can see from the diff below, the $!best_goal
attribute minimizes on
total path cost.
509a510
> has $!best_goal;
518a520,524
> $!best_goal = $state
> if $state.is_goal &&
> (!$!best_goal.defined ||
> $state.tot_cost_to_here < $!best_goal.tot_cost_to_here);
>
545c551
< return self!best_path($node);
---
> return self!best_path($!best_goal);