Programming Mini-Challenge: KnightBot
Another programming mini-challenge for you. It's been a month since the first one and that seemed to be rather successful. (I appreciate that there are other challenges on here but trying to sync with them seems tricky!)
A reminder:
I'm certain that many of you might find these pretty straight forward, but I still think there's merit in sharing different approaches to simple problems, including weird-and-wonderful ones.
KnightBot
Info
You will be writing a small part of a Chess program, specifically focusing on the Knight, on an 8 x 8 board.
Input
The top-left square of the board will have index 0, and the bottom-right square will have index 63.
- The first input is the starting square of the knight.
- The second input is the requested finishing square of the knight.
- The third input is the number of maximum moves allowed.
Output
The expected outcome is either True or False, determined by whether or not the Knight can reach the requested finishing square within the number of allowed moves when stating on the starting square.
e.g. The expected output for the input 16, 21, 4 is True since the Knight can move 16->33->27->21, which is 3 moves.
Extensions
Some additional ideas for extending this challenge...
- Instead of an 8x8, what if the board was nxn?
- Instead of "within x moves", what if it was "with exactly x moves?"
- Instead of a traditional Knight's move (2 long, 1 short), what if it was n long and m short?
- What if the board was infinite?
- What if the board looped back around when crossing the edges? (e.g. the square to the right of 7 is 0)
I like this! Here is a C solution. It might look like magic at first, but stick around and I'll explain it.
Try it online!
The idea here is something called bitboards, which are a very common way of expressing chess positions in computers. If a chess board is 8×8 squares, then there are 64 squares total where a piece could be placed. Hmm, what else is 64 things long?
We'll assign each bit to a square as follows:
Unfortunately, there are three coordinate systems at play: 0 at upper-left, as assigned in the challenge; a1 at lower-left, as standard chess notation; and 0 at far right, as standard bit notation. It's a bit complicated to keep these straight, so get used to tilting your monitor.
By the way, in chess notation, the numbers 1–8 are called ranks and the letters a–h are called files.
Now we'll do a standard searching procedure. At every stage, a board represents all of the reachable positions within that number of steps. For example, at stage 0, only the starting position is reachable. At stage 1, the starting position, as well as all legal moves therefrom, are reachable. A bitboard is perfect for this kind of manipulation: each step expands the number of positions that are available.
Okay, put on your coordinate galoshes — it's time to get dirty.
Let's assume our knight is in one position: there is only one "on" bit in the board. I've chosen that he is on c3. There are eight possible positions he could move to.
Each of these moves is possible with a simple bit shift. To move to, say, a2, we shift him left 6 bits — leftwise shifts move down-rank (down the board) and up-file (right the board). So we can think of this as
… << 8
, thus moving down one rank, followed by… >> 2
, thus moving left two files.But wait! What happens if our knight falls off the right or left edge? Such moves should be illegal, but with bit shifts, he'll just warp to an adjacent rank. In order to prevent this, for each possible move, we mask off the bits that would result in a warp. Let's take a specific example: which squares can move down one and left two?
The dot squares would fall off an edge with that move (c3 → a2); the star squares would not. Each rank of the board can be represented by eight bits, "on" where the square would result in a legal move. (Left squares are the least significant bits.) So the entire board can be represented by 64 bits. (Upper ranks are the least significant bytes.) This mask is
0x00FCFCFCFCFCFCFC
.In summary, to make a single move, we mask off the squares which would fall off the edge, then simply bit-wise shift the resulting board. When we started, we had the set of all possible positions. After the mask, we had the set of all possible positions that would survive shifting. After the shift, we had the set of all possible positions resulting from that specific move.
Here's another way to think about this:
Hopefully that was a useful introduction to applied bit-wise arithmetic! I'll leave the extensions as an exercise for the reader.
That is really interesting solution. So short yet powerful.
How does the
square_to_board
method work?The provided input positions are regular old numbers, 0–63. Their bit patterns are unrelated to the solution — for example,
long end = 21;
has the bit pattern10101
.We don't really care about the bit pattern of that number. Instead, we care about the number as a number: we want to transform it into a bit pattern with just one "on" bit at that position. In this example, we want
10…00
, an "on" followed by 20 "off"s.So we use the common trick
1 << position
. The left side is a number with bit pattern00000001
. (It has more zeros to the left.) The right side is how many bits to shift it by. If you shift it by four,(1 << 4) == 16
is00010000
, a number with bit 4 "on" and all others "off". If you shift it by zero,(1 << 0) == 1
is00000001
, a number with bit 0 "on" and all others "off".The expression I wrote is
((board) 1) << square
, which first converts1
to aboard
(a 64-bit integer) before the shift. Without the integer cast,1
would be interpreted as asigned int
, which, depending on your computer, might have only 16 bits. You could also get the right behavior with1ull
(meaningunsigned long long
), but I thought it was clearer to declare that the shift is an operation performed on aboard
.I'm doing this using traditional exploration methods. Nothing domain specific (I suppose there's a fancy efficient solution out there).
Here's my random walk implementation for now (later I'll post an updated BFS implementation).
It can eventually get a good solution :P
I have pretty nice solution. There is no bruteforcing, everything is calculated. It doesn't take into account infinite boards, repeating boards and sizes of boards, but I'll fix it later.
First of all, let's focus on the
y
coordinate. We can go to targety
by rising oury
position by either 1 or 2. With this knowledge, we can get list of numbers it can take us to get toy
(I assume we won't go up and down again). This list is about<delta.y / 2; delta.y>
(sometimes plus 1). This is the amount of turns we have to use when moving byx
coordinate. Let's call this listn
.We have our coordinate
A.x
and target coordinateB.x
. We have to go throughdelta.x
tiles withinq, q ∊ n
turns. We have two moves available: one that moves us 1 tile left/right and costs two turns (two tiles up), or one that moves us two tiles left/right and costs one turn (one tile up). To get to target position, we have to usedelta.x / 2
turns (sometimes plus 2). Now, we just have to move around the position until we spend allq
turns. We can move around and return to the same direction with costs2
,5
, and, ifdelta.x %2 == 1
, we can once use1
.This gives us nice amount of moves. Without the
1
move, we can use any amount of turns - except1
and3
. With the extra1
move, we can use any amount of moves.Checkmate, bruteforce.
Edit: highlighted syntax
It's a bit late and I haven't run it through any rigorous test cases, but here's my quick and dirty brute-force PHP solution: