As penalty for being distracted by other Erlang projects yesterday and not continuing my chess project, I hereby sentence myself to a bacon and egg biscuit for breakfast. Yum.
Before we continue fleshing out the board code, let’s revisit the move evaluation needs from day 8 and see if our current design, such as it is, allows for those to be accomplished.
We need a way to express a path and find any obstacles. This will be useful for several tasks.
The slope/row/column/diagonal logic will come into play here; I suspect that code will move out of piece.erl
into a utility module.
We also need, and have, a way to ask each square whether it contains a piece (recall blank_square
?).
To avoid contortions around which piece is under discussion, let’s simplify this question: given a white king, white bishop, and assorted black pieces.
This involves 3 distinct questions: for each black piece, can that piece attack the white king (assuming no obstacles), can it attack the white bishop (same assumption), and are there no obstacles in the path? Only if the answer to each question is yes is the bishop pinned.
We can already answer the first two questions today by asking each black piece whether it can capture the bishop and king squares; we have enough code in place that I’m confident the 3rd question can be answered.
If any of the 3 squares involved for the king (its current, future, or intermediate square) is under attack, castling is not a legal move.
As with pinning, this is effectively a solved problem.
If a king has moved, it can never castle.
If a rook has moved, the king cannot castle to that side.
Let’s set this question aside for the moment: we have bigger problems.
Once again, I may have led myself down a wrong path.
If you’ll recall, at day 5 I changed tack: instead of having one process per piece with retained state, I decided on a stateless process that would evaluate move functions based on incoming state. So no location, no team would be stored.
At day 8 I discovered that I would need “no-op” pieces at each blank square.
So now I have 64 “pieces” on a board, some of which are blank, and none of which know what team they represent.
How am I supposed to find each black piece to evaluate pinning?
How am I supposed to know whether a rook or a king has previously moved if it has no self-contained state?
To answer that last question, I discussed having a record of moves, so there may be a way to evaluate that list. Still, though, I think it is going to be necessary to reintroduce state to each piece.
I was inspired to remove state from piece.erl
because having the current location as part of a piece’s state struck me as risky. And, in fact, at this point my expectation is that the process at square {4, 7}
will never actually move, rather that process will represent different pieces over time.
In fact, perhaps my piece
module should be renamed to square
, but I’m inclined to think not. All of the code is related to piece movement.
Anyway, if we reintroduce1 state, I don’t want location to be included, but team and movement history are reasonable additions.
I think we also need a new message to a piece process: give your state to another process. So, if a rook moves from {4, 7}
to {2, 7}
the process representing {4, 7}
will be asked to send a swap
message to {2, 7}
.
In pseudo-code, then, moving a piece looks like this:
{4, 7}
whether it can conceptually move to {2, 7}
.{4, 7}
is pinned or whether another piece sits at {3, 7}
or {2, 7}
.{4, 7}
telling it to forward its state to {2, 7}
.{2, 7}
asking it to add the latest move to its state.
{4, 7}
giving it a new blank_square
state.
A capture move would be largely the same, but a piece of the opposing team at the destination square would no longer be an obstacle.
En passant has an extra step: the “captured” square will receive a blank_square
state.
And now we have a simple way to determine whether a king or rook has previously moved and thus can participate in a castle move: ask to see their move history.
Brute force: if any white piece can attack the black king, we have to evaluate whether any black piece can intervene and whether the king can move.
piece_loop/3
becomes piece_loop/5
: add Team
and History
to Piece
, Move
, and Capture
.Team
from the move
and capture
messages.NewTeam
and NewHistory
to the swap
message.Team
and History
to the what_am_i
response.moveto
message.The History
list contains tuples: starting square, ending square, and piece name (as atom) captured (if any). The first tuple in the list is the most recent move.2
Cleanup items:
move
and capture
messages to limit confusion: testmove
and testcapture
.swap
message to replace
, since “swap” can imply two pieces changing places.3blank_square/0
function that the moveto
message can leverage to “blank out” the current process, and the board can also use to initialize all 64 processes.piece_loop/5
(and in related messages) so the piece state comes first, move/capture functions last.Revised code:
blank_square() ->
piece_loop(blank_square, none, [],
fun(Source, _Dest, _team) -> { Source, [] } end,
fun(Source, _Dest, _team) -> { Source, [] } end).
piece_loop(Piece, Team, History, Move, Capture) ->
receive
{ Pid, testmove, Start, End, Xtra } ->
move_response(Pid, Piece, Start, End, Move(Start, End, Team), Xtra),
piece_loop(Piece, Team, History, Move, Capture);
{ Pid, testcapture, Start, End, Xtra } ->
move_response(Pid, Piece, Start, End, Capture(Start, End, Team), Xtra),
piece_loop(Piece, Team, History, Move, Capture);
{ replace, NewPiece, NewTeam, NewHistory, NewMove, NewCapture } ->
piece_loop(NewPiece, NewTeam, NewHistory, NewMove, NewCapture);
{ moveto, Start, End, CapturedPiece, NewSquarePid } ->
NewSquarePid ! { replace, Piece, Team,
[ { Start, End, CapturedPiece } | History ], Move, Capture },
blank_square();
{ Pid, what_am_i, Xtra } ->
Pid ! { Piece, Team, History, Xtra };
done ->
done;
Message ->
throw({unknown_message, Message})
end.
The board prototype also underwent some small changes.
None of the above has been tested beyond compilation, but the Indianapolis Chinese Festival is underway, so I must be headed downtown!
Technically there is already state (the piece type and movement functions), so we’re not reintroducing, just re-expanding.↩
One consequence of going with the current approach of moving piece state between processes instead of rebuilding the board data structure after each move (see day 8) is that I lose the option of retaining each board state as one discrete snapshot to make rollbacks easier. Will have to mull on this.↩
If I were serious about supporting other games with this codebase I’d have to give more thought to how to allow two pieces to exchange places on the board.↩