If you’ve been following along, you know that I paused for a while to do more reading and consider whether my approach (using processes like objects) was reasonable.
And?
Yeah, ’twas a bad idea. All of it. Every last decision…ok, it wasn’t that bad.
Still, it was bad.
My new approach: use records to make state much more explicit, and a dictionary to track the board. I’ll still have a board and a piece module, but I’m no longer creating separate processes for each piece.
Instead, the dictionary will have a square in the usual tuple form {X, Y}
as a key, with a piece record as value. Blank squares will have a piece type of none
.1
Most of the code I wrote can be reused, fortunately, albeit twisted around quite a bit. Here’s what I’ve done so far.
If you haven’t seen Erlang record syntax, good luck. The only record syntax I can understand is what I’ve written myself. Trying to follow someone else’s code with records is awful.
Better yet, I have nested records. Egads, the pain.
Anyway, here are my basic records:
-record(move, { piece,
start,
target,
movetype }).
-record(piece, { type=pawn,
team=white,
history=[],
movefun=pawnmove,
capturefun=pawncap }).
-record(historyitem, { endsquare,
traversed=[] }).
-record(boardstate, { pieces,
whiteking={5, 1},
blackking={5, 8} }).
I have these in a new records.hrl
file to be included into both my new board and piece modules.
Actually, nothing to show here yet. I’ve commented out virtually everything, and changed nothing.
Abandon all hope, ye who enter here.
initialize(Board, Proplist) ->
Pieces = proplists:get_value(pieces, Proplist),
Pawns = proplists:get_value(pawns, Proplist),
%% The Board dictionary gets revised with each call to initialize_row
Board1 = initialize_row(Board, Pieces, {1, 1}, white),
Board2 = initialize_row(Board1, Pawns, {1, 2}, white),
Board3 = initialize_blanks(Board2, 1, 3),
Board4 = initialize_row(Board3, Pawns, {1, 7}, black),
initialize_row(Board4, Pieces, {1, 8}, black).
initialize_blanks(Board, _, 7) ->
Board;
initialize_blanks(Board, 9, Y) ->
initialize_blanks(Board, 1, Y + 1);
initialize_blanks(Board, X, Y) ->
initialize_blanks(store({X, Y}, #piece{type=none}, Board), X + 1, Y).
initialize_row(Board, [], _, _) ->
Board;
initialize_row(Board, [{Piece, M, C}|T], {X, Y}, Team) ->
Move = proplists:get_value(M, newpiece:movefuns()),
Capture = proplists:get_value(C, newpiece:movefuns()),
initialize_row(store({X, Y}, #piece{type=Piece,
team=Team,
movefun=Move,
capturefun=Capture},
Board),
T, {X + 1, Y}, Team).
This is the only logic I’ve written in thus far, other than the hairy initialization code. Without this it’s tough to see whether anything works at all.
matrix_to_text(Matrix) ->
row_to_text(8, 1, Matrix, []).
%% X and Y are swapped here because we're focusing on rows. Made more
%% sense when I had a tuple of rows to navigate as my data structure.
row_to_text(0, _, _, Accum) ->
Accum ++ "\n";
row_to_text(Y, 9, Matrix, Accum) ->
row_to_text(Y - 1, 1, Matrix, ["\n\n|" | Accum]);
row_to_text(Y, X, Matrix, Accum) ->
row_to_text(Y, X + 1, Matrix, [ cell_to_text({X, Y}, Matrix) ++ "|" | Accum ]).
cell_to_text(Square, Matrix) ->
{ok, #piece{type=Type}} = find(Square, Matrix),
piece_to_shorthand(Type).
piece_to_shorthand(Piece) ->
case Piece of
none ->
" ";
pawn ->
" p ";
rook ->
" R ";
knight ->
" N ";
bishop ->
" B ";
queen ->
" Q ";
king ->
" K "
end.
And what does this look like?
38> Board = newboard:init().
[,
...
{...}|...]
39> io:format(newboard:matrix_to_text(Board)).
| R | N | B | K | Q | B | N | R |
| p | p | p | p | p | p | p | p |
| | | | | | | | |
| | | | | | | | |
| | | | | | | | |
| | | | | | | | |
| p | p | p | p | p | p | p | p |
| R | N | B | K | Q | B | N | R |
ok
I’ve taken several big steps backward, but I should be on a much sounder footing now.
Here’s an example of the conundrum I faced as I worked through the old, process-based model.
Let’s say that I want to take advantage of concurrency and ask all pieces: can you attack the white king?
As far as I can see, there are 3 ways I can tackle that:
From the reading I’ve been doing, it seems clear: processes should be defined and used deliberately to manage an independent task. In this case, there’s no independent task.
Per my original plan, the benefit of having each piece run its own state loop (i.e., be its own process) was for state management, but the cost doesn’t seem worth it, or at least it doesn’t seem to be a typical Erlang pattern to handle it this way.
We could just not store blank squares in the dictionary, but the syntax is generally cleaner if we populate all entries.↩