2012-09-06

One big missing piece

I’ve spent many, many hours now on the code for piece management, but we still have a huge hole in our application: the chess board itself.

Let’s start by listing tasks for our program.

Mandatory/highly desirable features

Optional features

Roles

From the above lists of features, we’ll need code that:

Procrastination

Because I can easily trigger moves from the Erlang shell, anything dealing with user interaction will be very low on our priority list.

First, the obvious

We know we’ll need to store piece location and move history, so let’s start there.

Data structures

The obvious choice for a chess board is a matrix, but in Erlang it’s not quite so simple.

Erlang does have an array library, but it seems to be a more recent addition and I’d like to see how this has been done historically. Nested lists might work, but tuples seem to be the preferred choice for data structures.

Given my wild success (cough) with math-related tasks over the last couple of days, I think I’m going to steal some ideas and code line. Here is a short module someone wrote to handle matrix operations.

Walking through the matrix module, a couple of learning points:

mkmatrix is particularly hairy and exactly what I need to understand (I think), so I’ll walk through it here.

Call sequence:

  1. mkmatrix(30, 30)
  2. mkmatrix(30, 30, 1, [])
  3. mkrow(30, [], 1)
  4. mkrow(29, [1], 2)
  5. mkrow(28, [2, 1], 3)
  6. mkrow(27, [3, 2, 1], 4)
  7. Eventually: mkrow(0, [30 ... 1], 31)
  8. Which returns: { { 30 ... 1 }, 31 }
  9. mkmatrix(29, 30, 31, [ { 30 ... 1 } ])
  10. mkrow(30, [], 31)
  11. mkrow(29, [31], 32)
  12. mkrow(28, [32, 31], 33)
  13. …continuing to next recursive call of mkmatrix(28, 30, 61, [ { 31 .. 60 }, { 1 .. 30 } ])
  14. …finally arriving at base case mkmatrix(0, 30, ?, [ { lots }, { of }, { tuples } ])
  15. …which reverses the list of tuples and converts it into a tuple of tuples to return.

In my case, I need a matrix of blank values to populate. In Perl, I’d populate the list with undef or 0; here, I think I’ll use the atom blank.

But wait!

Here’s the great thing about writing code instead of just thinking about it: you discover where your mental model and reality don’t align.

As my first complex functional programming project, it just occurred to me that, of course, I can’t just overwrite cells with new values. So my blank cell can never become a process PID when I need to know that a piece is there.

Wow. Oops. I’m sorry, what is this language good for?

Ok, a big of overreaction. Let’s think this through.

It does appear that the array library supports setting new values.

Having come this far, however, I must admit that the thought of changing a variable is unpleasant. I’ve definitely drunk the koolaid. What are my other options?

We could rebuild the matrix each time we move or promote a piece. This feels like the functional way to do it.

However, what if we populate the board with 64 piece processes, but pass a no-op or exception-throwing function to the blank” cells?

Instead of my pawncapture or queenmove anonymous functions that are used to initialize a process, we send:

fun(Source, _Dest, _team) -> { Source, [] } end.

The first argument to our piece_loop/3 function would have to be a blank_square atom or similar. When we receive the message back in our mailbox, it would look like: { no, blank_square, { X1, Y1 }, { Xf, Yf }, [] }.

That also means we’ll have to modify our piece module to take a new (old) message, replacement values like our old promotion message. piece also will need respond to a what are you” message.

My most recent plan was to have just one process that interpreted pawn moves, and it would be stored at each square where a pawn lived. Now we’ll be managing 64 processes, which is a piece of cake for Erlang.

Revised piece code

Have I mentioned how much I love pattern matches and robust concurrency? Here I’ve just had to make a fairly significant model change, and my code difference in one of my most important objects is adding 4 lines of code.

piece_loop(Piece, Move, Capture) ->
    receive
        { Pid, move, Start, End, Team } ->
            move_response(Pid, Piece, Start, End, Move(Start, End, Team)),
            piece_loop(Piece, Move, Capture);
        { Pid, capture, Start, End, Team } ->
            move_response(Pid, Piece, Start, End, Capture(Start, End, Team)),
            piece_loop(Piece, Move, Capture);
        { swap, NewPiece, NewMove, NewCapture } ->
            piece_loop(NewPiece, NewMove, NewCapture);
        { Pid, what_am_i } ->
            Pid ! Piece;
        done ->
            done;
        Message ->
            throw({unknown_message, Message})
    end.

All I did: add the new swap and what_am_i messages. Note that there’s no reason to communicate back to the board after a swap message is processed.

mkmatrix code

Here’s my version of mkmatrix and mkrow. Structure is basically the same, but instead of passing a counter in to increment each cell of the matrix, I pass the necessary initialization code to spawn a new PID.

I’ll definitely extract the no-op function and store it in movefuns, but wanted to illustrate how this could be done.

mkrow(0, L, _) -> list_to_tuple(lists:reverse(L));
mkrow(N, L, Init) -> mkrow(N-1, [ spawn(piece, piece_loop, Init) | L], Init).

mkmatrix(0, _, _, M) -> list_to_tuple(lists:reverse(M));
mkmatrix(NR, NC, Init, M) ->
    Row = mkrow(NC, [], Init),
    mkmatrix(NR-1, NC, Init, [Row|M]).

mkmatrix(NR, NC) -> mkmatrix(NR, NC,
    [ blank_square, fun(Source, _Dest, _team) -> { Source, [] } end,
      fun(Source, _Dest, _team) -> { Source, [] } end ], []).

And invoking from my new module board:

10> board:mkmatrix(1, 1).

I haven’t given myself a hook to test that process, but that’ll be an easy fix.

Here’s my prototype board loop:

board_loop(Matrix) ->
    receive
        { Pid, move, {X, Y}, End, Team } ->
            element(X, element(Y, Matrix)) ! { self(), move, {X, Y}, End, Team },
            receive
                { Response, Piece, _, _, _ } ->
                    Pid ! { Response, Piece }
            end,
            board_loop(Matrix)
    end.

And here we create and test it:

12> Board = spawn(board, board_loop, [ board:mkmatrix(1, 1) ]).
<0.73.0>
13> Board ! { self(), move, {1, 1}, {1, 1}, purple }.
{<0.62.0>,move,{1,1},{1,1},purple}
14> flush().
Shell got {no,blank_square}
ok

Erlang, I rescind any unpleasant thoughts I may or may not have expressed earlier in this blog post.

Writing the prototype revealed to me that if I want to maintain any sort of parallelism in the board object, I need to send an extra argument in the message to each piece; that argument will be returned to me with the response so I know who to forward the reply to.

In other words, if user X asks the board to move piece Y, I need to send the sending PID for the request from user X to the piece; when the piece feeds that back to me, I’ll have the user PID again and I can send a useful response back to it.

As you’ll notice in the prototype board_loop code above, I had to immediately switch to receive mode after sending a message to the piece, because if I returned to my loop to wait for more messages, I lose track of the sending process ID.

Sending a process ID around to receive it back later is probably a standard Erlang convention. I’m overdue to start digging into open source production code to see what makes it tick.

Just pidding around

Final code update for the evening: here’s our piece and board code handle passing extra data back and forth for state:

Piece

piece_loop(Piece, Move, Capture) ->
    receive
        { Pid, move, Start, End, Team, Xtra } ->
            move_response(Pid, Piece, Start, End, Move(Start, End, Team), Xtra),
            piece_loop(Piece, Move, Capture);
        { Pid, capture, Start, End, Team, Xtra } ->
            move_response(Pid, Piece, Start, End, Capture(Start, End, Team), Xtra),
            piece_loop(Piece, Move, Capture);
        { swap, NewPiece, NewMove, NewCapture } ->
            piece_loop(NewPiece, NewMove, NewCapture);
        { Pid, what_am_i, Xtra } ->
            Pid ! { Piece, Xtra };
        done ->
            done;
        Message ->
            throw({unknown_message, Message})
    end.

move_response(Pid, Piece, Start, End, { Start, Traverse }, Xtra) ->
    Pid ! { no, Piece, Start, End, Traverse, Xtra };
move_response(Pid, Piece, Start, End, { End, Traverse }, Xtra) ->
    Pid ! { yes, Piece, Start, End, Traverse, Xtra }.

Board

board_loop(Matrix) ->
    receive
        { Pid, move, {X, Y}, End, Team } ->
            element(X, element(Y, Matrix)) ! { self(), move, {X, Y}, End, Team, Pid },
            board_loop(Matrix);
        { Response, Piece, _Start, _End, _Traverse, Pid } ->
            Pid ! { Response, Piece },
            board_loop(Matrix)
    end.

The board logic is much cleaner, but I’m starting to see another problem: without atoms in my message patterns, I could easily match the wrong message if the number of arguments align.

I could handle no and yes messages explicitly and separately, which is probably the easiest way to solve this. Thus, the pattern matches in board_loop/1 would look like:

{ Pid, move, {X, Y}, End, Team }
{ no, Piece, _Start, _End, _Traverse, Pid }
{ yes, Piece, _Start, _End, _Traverse, Pid }

I’ll keep that in mind as I start moving the board module out of prototype status.

erlang chessboard
Previous post
Chessboard: Day 7 Go test old man.
Next post
Chessboard: Day 9 Or is it Day 10?