This week, both tasks are chess-based, so I’m just linking you to the original concept album for the musical Chess, knowing full well most of you will only know the big single, One Night in Bangkok.
So let’s move on to the ultimate test of cerebral fitness: Perl Weekly Challenge 281!
Task 1: Check Color
You are given coordinates, a string that represents the coordinates of a square of the chessboard as shown below:
Write a script to return true
if the square is light
, and false
if the square is dark
.
Example 1
Input: $coordinates = "d3"
Output: true
Example 2
Input: $coordinates = "g5"
Output: false
Example 3
Input: $coordinates = "e6"
Output: true
Approach
Ok, this one feels fairly straightforward: if the letter of the coordinate is a
, c
, e
, or g
and the number is even, or the letter is b
, d
, f
, or h
and the number is odd, then the square is light.
Raku
I’m sure I could get this to be more terse, but I’m going for readability and ease of comprehension, not lowest golf score.
sub isLight($coordinates) {
my ($letter, $num) = $coordinates.comb;
return (
$letter.match(/<[aceg]>/) && ($num %% 2)
||
$letter.match(/<[bdfh]>/) && !($num %% 2)
) ?? 'true' !! 'false';
}
View the entire Raku script for this task on GitHub.
$ raku/ch-1.raku
Example 1:
Input: $coordinates = "d3"
Output: true
Example 2:
Input: $coordinates = "g5"
Output: false
Example 3:
Input: $coordinates = "e6"
Output: true
Perl
sub isLight($coordinates) {
my ($letter, $num) = split //, $coordinates;
return (
( ($letter =~ /[aceg]/) && ($num % 2 == 0) )
||
( ($letter =~ /[bdfh]/) && ($num % 2 == 1) )
) ? 'true' : 'false';
}
View the entire Perl script for this task on GitHub.
Python
In Python, it’s easy to just test to see if a string exists in another string, so we don’t have to resort to a regular expression.
def isLight(coordinates):
letter = coordinates[0:1]
num = int(coordinates[1:])
return (
( (letter in "aceg") and (num % 2 == 0) )
or
( (letter in "bdfh") and (num % 2 == 1) )
)
View the entire Python script for this task on GitHub.
Elixir
Functions I wound up using:
At first, I got the error (UndefinedFunctionError) function Integer.is_odd/1 is undefined or private. However, there is a macro with the same name and arity. Be sure to require Integer if you intend to invoke this macro
. However, once I added line 4, the error went away. Looking into why, I discovered that because Integer.is_even/1
and Integer.is_odd/1
are defined as macros so they can be used as guards, we have to require the module before we can use the functions.
require Integer
def isLight(coordinates) do
letter = String.first(coordinates)
{num, _} = Integer.parse(String.last(coordinates))
cond do
String.contains?("aceg", letter)
and Integer.is_even(num) -> "true"
String.contains?("bdfh", letter)
and Integer.is_odd(num) -> "true"
true -> "false"
end
end
View the entire Elixir script for this task on GitHub.
Task 2: Knight’s Move
A Knight in chess can move from its current position to any square two rows or columns plus one column or row away. So in the diagram below, if it starts a S
, it can move to any of the squares marked E
.
Write a script which takes a starting position and an ending position and calculates the least number of moves required.
Example 1
Input: $start = 'g2', $end = 'a8'
Ouput: 4
g2 -> e3 -> d5 -> c7 -> a8
Example 2
Input: $start = 'g2', $end = 'h2'
Ouput: 3
g2 -> e3 -> f1 -> h2
Approach
There’s graph theory about how knights move around a chessboard, and we can think about the moves to get from a starting point on the board to an ending point on the board as a graph. This lets us do a breadth-first search of all the nodes to find a solution with the fewest number of moves.
First, we need a function to generate all the possible moves from a given starting point. Then we use hashes to keep track of where we’ve been on the board already (so we don’t get into endless loops) and how we got there. We put our starting point into a queue and note that we’ve been to that starting point and it took us 0 moves to get there. Then we pull that point out of the queue and generate all the possible points we can get to from there. We loop over those points, updating the hash to indicate the path we took to get there and the number of moves we needed. If one of these points is our destination, we bail out of the search; otherwise we push the point onto the end of the queue and shift the next point off the queue.
Raku
my @knightMoveList = (
(-2, -1), (-2, +1), (-1, -2), (-1, +2),
(+2, -1), (+2, +1), (+1, -2), (+1, +2),
);
sub knightMoves($coordinates) {
my ($letter, $num) = $coordinates.comb;
my @endpoints;
for @knightMoveList -> @colRow {
my ($col, $row) = @colRow;
my $newcol = ($letter.ord + $col).chr;
next unless $newcol ge "a" && $newcol le "h";
my $newrow = $num + $row;
next unless 1 <= $newrow <= 8;
@endpoints.push($newcol ~ $newrow);
}
return @endpoints;
}
sub leastMoves($start is copy, $end) {
# trivial case: we're already at the end point
return ( 0, $end ) if $start eq $end;
# Ok, we're going to need to search for a solution.
# Keep track of how many moves it takes to get to
# a particular position, starting at $start
my %moves = ( $start => 0 );
# also keep track of the path to get there
my %path_to = ( $start => $start );
# make a queue of starting points
my @queue = ( $start );
while ( @queue ) {
$start = @queue.shift;
# figure out the valid moves that we haven't been to yet
my @endpoints = knightMoves($start).grep({
%path_to{$_}:!exists
});
for @endpoints -> $next {
# build the path to the next endpoint
%path_to{$next} = %path_to{$start} ~ " -> $next";
# increment the number of moves it takes to get there
%moves{$next} = %moves{$start} + 1;
# have we arrived at our destination
return ( %moves{$next}, %path_to{$next} ) if $next eq $end;
# no? then push this space onto our processing queue
@queue.push: $next;
}
}
# we can't get there from here!
# (only possible when the chessboard is an odd size)
return ( -1, "no path found" );
}
View the entire Raku script for this task on GitHub.
$ raku/ch-2.raku | less
Example 1:
Input: $start = 'g2', $end = 'a8'
Output: 4
g2 -> e3 -> c4 -> b6 -> a8
Example 2:
Input: $start = 'g2', $end = 'h2'
Output: 3
g2 -> e1 -> f3 -> h2
Example 3:
Input: $start = 'a1', $end = 'h8'
Output: 6
a1 -> c2 -> a3 -> c4 -> e5 -> g6 -> h8
Perl
my @knightMoveList = (
[-2, -1], [-2, +1], [-1, -2], [-1, +2],
[+2, -1], [+2, +1], [+1, -2], [+1, +2],
);
sub knightMoves($coordinates) {
my ($letter, $num) = split //, $coordinates;
my @endpoints;
foreach my $colRow ( @knightMoveList ) {
my ($col, $row) = @$colRow;
my $newcol = chr(ord($letter) + $col);
next unless $newcol ge "a" && $newcol le "h";
my $newrow = $num + $row;
next unless 1 <= $newrow <= 8;
push @endpoints, ($newcol . $newrow);
}
return @endpoints;
}
sub leastMoves($start, $end) {
# trivial case: we're already at the end point
return ( 0, $end ) if $start eq $end;
# Ok, we're going to need to search for a solution.
# Keep track of how many moves it takes to get to
# a particular position, starting at $start
my %moves = ( $start => 0 );
# also keep track of the path to get there
my %path_to = ( $start => $start );
# make a queue of starting points
my @queue = ( $start );
while ( @queue ) {
$start = shift @queue;
# figure out the valid moves that we haven't been to yet
my @endpoints = grep {
! exists $path_to{$_}
} knightMoves($start);
foreach my $next ( @endpoints ) {
# build the path to the next endpoint
$path_to{$next} = $path_to{$start} . " -> $next";
# increment the number of moves it takes to get there
$moves{$next} = $moves{$start} + 1;
# have we arrived at our destination
return ( $moves{$next}, $path_to{$next} ) if $next eq $end;
# no? then push this space onto our processing queue
push @queue, $next;
}
}
# we can't get there from here!
# (only possible when the chessboard is an odd size)
return ( -1, "no path found" );
}
View the entire Perl script for this task on GitHub.
Python
knightMoveList = [
[-2, -1], [-2, +1], [-1, -2], [-1, +2],
[+2, -1], [+2, +1], [+1, -2], [+1, +2],
]
def knightMoves(coordinates):
letter = coordinates[0:1]
num = int(coordinates[1:])
endpoints = []
for colRow in knightMoveList:
col, row = colRow
newcol = chr(ord(letter) + col)
if "a" <= newcol <= "h":
newrow = num + row
if 1 <= newrow <= 8:
endpoints.append(newcol + str(newrow))
return endpoints
def leastMoves(start, end):
# trivial case: we're already at the end point
if start == end:
return ( 0, end )
# Ok, we're going to need to search for a solution.
# Keep track of how many moves it takes to get to
# a particular position, starting at $start
moves = { start: 0 }
# also keep track of the path to get there
path_to = { start: start }
# make a queue of starting points
queue = [ start ]
while ( queue ):
start = queue.pop(0)
# figure out the valid moves that we haven't been to yet
endpoints = [
m for m in knightMoves(start) if m not in path_to
]
for next in endpoints:
# build the path to the next endpoint
path_to[next] = f'{path_to[start]} -> {next}'
# increment the number of moves it takes to get there
moves[next] = moves[start] + 1
# have we arrived at our destination
if next == end:
return ( moves[next], path_to[next] )
# no? then push this space onto our processing queue
queue.append(next)
# we can't get there from here!
# (only possible when the chessboard is an odd size)
return ( -1, "no path found" )
View the entire Python script for this task on GitHub.
Elixir
Wow, this is twice as big as the Raku, Perl, or Python solutions. I’m going to break it down. First, the knightMoves
function:
defp letterAdd(letter, add) do
<<val::utf8>> = letter
List.to_string([val + add])
end
def onBoard(c,r) do
"a" <= c and c <= "h" and 1 <= r and r <= 8
end
defp knightMoveList(), do: [
{-2, -1}, {-2, +1}, {-1, -2}, {-1, +2},
{+2, -1}, {+2, +1}, {+1, -2}, {+1, +2},
]
def knightMoves([], _, _, endpoints), do: endpoints
def knightMoves([next | rest], letter, num, endpoints) do
{col, row} = next
newcol = letterAdd(letter, col)
newrow = num + row
endpoints = if onBoard(newcol,newrow) do
# add to list being returned
endpoints ++ [ newcol <> to_string(newrow) ]
else
endpoints
end
knightMoves(rest, letter, num, endpoints)
end
def knightMoves(coordinates) do
letter = String.first(coordinates)
{num, _} = Integer.parse(String.last(coordinates))
knightMoves(knightMoveList(), letter, num, [])
end
Adding or subtracting from the ASCII value of a character isn’t so easy in Elixir (“easy” being something like chr(ord(letter) + col)
), so I decided to abstract it out into a letterAdd/2
function. Also, since I like to keep the lines in my PWC solutions under 65 characters (because of how my blog is formatted), I decided to abstract out a onBoard/2
function to determine if a column/row set was on the board. Then I thought about adding knightMovesList
as a module attribute, but reading through the section on using module attributes as compile-time constants, they recommend using private functions as constants instead.
The knightMoves/4
functions themselves use recursion to process knightMovesList
; this is the recommended method of looping in Elixir (that is, when you’re not looping over an enumerable and returning a modified version of that enumerable, like you do in Enum.map/2). I also provided a knightMoves/1
function that parsed the coordinates into its letter and number and then called knightMoves/4
with the list of move offsets, the letter and number, and an empty result list.
def processEndpoints(
next, params = %{
moves_to: moves_to,
path_to: path_to,
startPos: startPos,
endPos: endPos,
queue: queue
}
) do
# update the move count map
moves_to = Map.put(moves_to, next, moves_to[startPos] + 1)
# update the path to current space map
path_to = Map.put(path_to, next,
"#{path_to[startPos]} -> #{next}"
)
if next == endPos do
# we found the shortest path, update the return
# values which will stop the loop
%{ moves: moves_to[next], path: path_to[next] }
else
# update params for next iteration
params |> Map.put(:moves_to, moves_to)
|> Map.put(:path_to, path_to)
|> Map.put(:queue, queue ++ [ next ])
end
end
def processEndpoints(_, params = %{moves: moves})
when not is_nil(moves), do: params
Because there’s a bunch of states I need to keep track of (the queue of spaces to consider, the starting position, the ending position, the map of move counts, and the map of paths to get to each space, I’m bundling them all together in a Map so I can pass them to the next recursive call (and return the values from those calls) in a single labelled data structure. But because I want to be able to easily access those values within the function, I use pattern matching to extract them as individual labels so I can access them easily within the function.
I initially implemented this as a recursive function, but then I realized I could use Enum.reduce/3
to call the function with each endpoint and pass in my params
map as the “accumulator”:
params = Enum.reduce(endpoints, params, &processEndpoints/2)
But that left me with the problem of bailing out of the loop once we identified the shortest path. I was able to use a Guard in one of my processEndpoints/2
definitions (TBH, the definition I had initially written as the stopping case for the recursion) to check whether the :moves
key in my params
map had been defined, and if so, just return the params
map without further processing. We still wind up calling processEndpoints/2
once for each item in endpoints
, but once we find the shortest path, the rest of those calls are no-ops.
# we've exhausted the queue
# (only possible when the chessboard is an odd size)
def processQueue([], _), do: %{
moves: -1, path: "no path found"
}
def processQueue(_, params = %{moves: moves})
when not is_nil(moves), do: params
def processQueue([startPos | queue], params = %{
path_to: path_to
}) do
# put the current starting position and the current queue
# into the params
params = params |> Map.put(:queue, queue)
|> Map.put(:startPos, startPos)
# figure out the valid moves that we haven't been to yet
endpoints = knightMoves(startPos)
|> Enum.filter(fn m -> !Map.has_key?(path_to, m) end)
# call processEndpoints/2 for each of the endpoints
params = Enum.reduce(endpoints, params, &processEndpoints/2)
# recursively call to process the rest of the queue
processQueue(params[:queue], params)
end
I couldn’t do the same Enum.reduce/3
trick for processQueue/2
, though, because we need to be able to add items to the queue from inside the function (well, really from within processEndpoints/2
), and Enum.reduce/3
takes the list being processed as its first parameter. That’s not really a big deal though; this function still winds up being fairly succinct. It stuffs the current queue and starting position into the params map, gets the list of possible moves from this starting point, filters out the places we’ve already visited, and then makes the Enum.reduce/3
call to processEndpoints/2
. I did add a stopping case that tested whether or not the :moves
key in my params
map had been defined, and if so, just return the params
map without further processing.
# trivial case: we're already at the end point
def leastMoves(startPos, endPos) when startPos == endPos, do:
{ 0, endPos }
def leastMoves(startPos, endPos) do
params = processQueue([ startPos ], %{
endPos: endPos,
moves_to: %{ startPos => 0 },
path_to: %{ startPos => startPos },
moves: nil,
path: nil
})
{ params[:moves], params[:path] }
end
Finally, I get to the function I call from my solution
function. Really, besides handling the trivial case, all this does is set up the values being passed around in the params
map, make the initial call to processQueue/2
, and then unpack the :moves
and :path
keys in the returned params
map and pass them back to the caller as a tuple.
View the entire Elixir script for this task on GitHub.
Here’s all my solutions in GItHub: https://github.com/packy/perlweeklychallenge-club/tree/master/challenge-281/packy-anderson