I wasn’t really happy with how I deployed the new Context
concept on day 10, and digging a bit has led me to proplists. Much better.
Property lists are a list of tuples or atoms with a simple interface.1
For example, if I define a Context
value as a propery list, like Context = [ { team, white }, { is_castle, true } ]
, I can use simple functions to evaluate it:
true = proplists:get_bool(is_castle, Context).
Team = proplists:get_value(team, Context).
Unfortunately, I can’t use property list accessor functions in pattern matching or as guards.
Here’s the current first pattern match for the pawnmove
anonymous function:
({ X, OldY }, { X, NewY }, white) when NewY - OldY =:= 1 -> { { X, NewY }, [] };
If I passed a property list as the 3rd argument (the value that’s currently white
) I’d have to make the code much longer, because I’d have to build a proper function body to evaluate its contents.
On the gripping hand, I could perform the proplist parsing before calling the move functions.
Here’s the testmove
message processing currently:
{ Pid, testmove, Start, End, Context, Xtra } ->
move_response(Pid, Piece, Start, End, Move(Start, End, Context), Xtra),
piece_loop(Piece, Team, History, Move, Capture);
I could do this, instead (code broken apart a bit for clarity):
{ Pid, testmove, Start, End, Context, Xtra } ->
move_response(Pid, Piece, Start, End,
Move(Start, End,
proplists:get_value(team, Context),
proplists:get_bool(is_castle)
),
Xtra),
piece_loop(Piece, Team, History, Move, Capture);
The new parameter list for each move function becomes Start, End, Team, IsCastle, Xtra
.
It’s annoying that the castling special case has to tag along for every move function parameter list, but I don’t see any clean way around that.
I’m going to leave the above section in place because this is a (mostly) complete record of my mental path through the quagmire, and there are definitely some twists and turns. Besides, I think property lists will likely be useful elsewhere.
However, I think I’m overthinking this whole castling problem.
I’m going to add castle
as a completely separate message. That’ll rip out the whole Context
parameter, and it’ll revert to just being a team value.
If I wanted to make this code more general, perhaps to allow checkers or go, I’d probably find a way to handle this via callbacks.
piece_loop/5
beforepiece_loop(Piece, Team, History, Move, Capture) ->
receive
{ Pid, testmove, Start, End, Context, Xtra } ->
move_response(Pid, Piece, Start, End, Move(Start, End, Context), Xtra),
piece_loop(Piece, Team, History, Move, Capture);
...
piece_loop/5
afterpiece_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, testcastle, Start, End, Xtra } ->
move_response(Pid, Piece, Start, End, Move(Start, End, {Team, castle}), Xtra),
piece_loop(Piece, Team, History, Move, Capture);
...
Now the current move functions as I have redefined them to support the more complex Context
concept still apply; pawns look for an atom there (the team), kings look for tuples, nobody else cares.
And castleto
is a much simpler version of moveto
:
{ castleto, Start, End, NewSquarePid } ->
%% Ask the target square for more information
case Move(Start, End, { Team, castle }) of
{ Start, _ } ->
throw({cannot_moveto, Piece, Start, End});
{ End, _ } ->
NewSquarePid ! { replace, Piece, Team,
[ { Start, End, none } | History ], Move, Capture }
end;
It’s clear from thinking through the harder problems that developing the (perhaps misnamed) piece
module in a void is misleading. I really need the broader context of a chessboard and chess game to think through the implications of some of these design decisions.
We have a board prototype; let’s go ahead and create a fully-populated chessboard.
I have the code to create an 8x8 matrix of squares. I know how to “convert” a blank square to a chess piece.
I have no idea how to map the standard initial layout onto my matrix.
Let’s start with the obvious: my board
module hasn’t been updated to reflect recent changes.
Specifically, this code no longer does anything:
{ Pid, move, {X, Y}, End } ->
element(X, element(Y, Matrix)) ! { self(), move, {X, Y}, End, Pid },
We now have five (sigh) distinct move messages: testmove
, testcapture
, testcastle
, moveto
, and castleto
. This flow originally mapped to what is now called testmove
, so I’ll just rename both atoms to testmove
and move (hah) on.
We need to map 16 pieces onto a matrix of 64 squares. I can envision an external configuration file, but to keep things in code for now I’ll use an internal structure.
I’ll could use something like this (a property list!):
[ { rook, [ { 1, 1 }, { 8, 1 }, { 1, 8 }, { 8, 8 } ] },
{ knight, [ { 2, 1 }, { 7, 1 }, { 2, 8 }, { 7, 8 } ] },
{ bishop, [ { 3, 1 }, { 6, 1 }, { 3, 8 }, { 6, 8 } ] },
...
I’ll have to “know” that anything on ranks 1 and 2 are white, and 7/8 are black. Awful but fine for prototyping.
On the other hand, it’d be easy to place a piece on the wrong square, and computers are better at keeping numbers straight than I am (by far), so we’ll try this property list instead:
[ { pieces, [ { rook, rookmove, rookmove },
{ knight, knightmove, knightmove },
{ bishop, bishopmove, bishopmove },
{ queen, queenmove, queenmove },
{ king, kingmove, kingmove },
{ bishop, bishopmove, bishopmove },
{ knight, knightmove, knightmove },
{ rook, rookmove, rookmove } ] },
{ pawns, [ { pawn, pawnmove, pawncap },
{ pawn, pawnmove, pawncap },
{ pawn, pawnmove, pawncap },
{ pawn, pawnmove, pawncap },
{ pawn, pawnmove, pawncap },
{ pawn, pawnmove, pawncap },
{ pawn, pawnmove, pawncap },
{ pawn, pawnmove, pawncap } ] }
]
Not my finest idea ever, but it’ll work.
We’ll need to have a function to replace the existing square state with new state:
replace(Matrix, {X, Y}, Piece, Team, Move, Capture) ->
element_of(Matrix, { X, Y }) ! { replace, Piece, Team, [], Move, Capture }.
element_of/2
is another new function to simplify the matrix access:
element_of(Matrix, { X, Y }) ->
element(X, element(Y, Matrix)).
The initialize
function for the matrix is surprisingly straightforward:
initialize(Matrix, Proplist) ->
Pieces = proplists:get_value(pieces, Proplist),
Pawns = proplists:get_value(pawns, Proplist),
initialize(Matrix, Pieces, 1, 1, white),
initialize(Matrix, Pawns, 1, 2, white),
initialize(Matrix, Pawns, 1, 7, black),
initialize(Matrix, Pieces, 1, 8, black).
initialize(_, [], _, _, _) ->
done;
initialize(Matrix, [{Piece, M, C}|T], X, Y, Team) ->
Move = proplists:get_value(M, piece:movefuns()),
Capture = proplists:get_value(C, piece:movefuns()),
replace(Matrix, {X, Y}, Piece, Team, Move, Capture),
initialize(Matrix, T, X + 1, Y, Team).
And finally, an init
function (that I’m probably misusing per Erlang convention):
init() ->
Matrix = mkmatrix(8, 8),
Pieces = [ { pieces, [ { rook, rookmove, rookmove },
{ knight, knightmove, knightmove },
{ bishop, bishopmove, bishopmove },
{ queen, queenmove, queenmove },
{ king, kingmove, kingmove },
{ bishop, bishopmove, bishopmove },
{ knight, knightmove, knightmove },
{ rook, rookmove, rookmove }
]
},
{ pawns, [ { pawn, pawnmove, pawncap },
{ pawn, pawnmove, pawncap },
{ pawn, pawnmove, pawncap },
{ pawn, pawnmove, pawncap },
{ pawn, pawnmove, pawncap },
{ pawn, pawnmove, pawncap },
{ pawn, pawnmove, pawncap },
{ pawn, pawnmove, pawncap }
]
}
],
initialize(Matrix, Pieces),
spawn(?MODULE, board_loop, [ Matrix ]).
Compile, hold our breath, and…
12> Board = board:init().
<0.141.0>
13> Board ! { self(), testmove, {2, 1}, {3, 3} }.
{<0.60.0>,testmove,{2,1},{3,3}}
14> flush().
Shell got {yes,knight}
ok
I feel like I should quit while I’m ahead. As always, code on github if you’re really, really bored.
This data structure is so intuitive that I’ve been using it for my anonymous move functions data structure all along. I can replace the hairy list comprehension [ Move ] = [ X || { knightmove, X } <- piece:movefuns() ] with proplists:get_value
↩