2012-08-30

What’s the plan, Stan?

20-some years ago when I first learned object-oriented programming,1 I attempted a multi-user chess game. It was multi-user because I had no knowledge/interest in creating an AI.

Even without that, I found the problem to be quite challenging for my nascent skills, and I don’t believe I ever worked out all the bugs.

It struck me that this would be a good way to stretch my legs with Erlang as well.

Planned features

The most notable features I’m currently not planning on including: AI or a graphical user interface.2

I do want to include:

So what UI?

Since I don’t plan on a GUI, I’m going to interact with this via the Erlang shell for now. Later I’ll create a CLI tool to trigger moves and display the board in some simple format.

What’s first?

Eventually, I’ll have to look at behaviors and see if gen_server or one of the other OTP components makes sense for a framework. For now, I’m just going to create snippets of code to test out.

Stream of consciousness

Piece process

Each piece will be its own process, but this isn’t C++ or Java. I don’t want a Knight class, a Queen class, etc.

Instead, I’ll take a function as an argument that will know the basic legal moves for the given piece.

As is typical for an Erlang process, at least for one that doesn’t implement a behavior, the core of the process will be a receive loop, or rather a recursive function that uses receive to wait for messages.

The essential state for a piece? The behavioral function, the current square (X/Y, with X reflecting the columns, Y the rows, and we’ll start at 1 because that seems to be typical for Erlang), and which team.

Later we may need more PIDs to be passed in as helpers, to decide whether the piece is pinned, or whether there are other pieces blocking the move.

Roadblock 1: if statement?

Because I haven’t written any Erlang in a few days, I’m quickly stymied. My code so far:

    piece_loop(Maneuver, Location, Team) ->
        receive
            { X, Y } ->

Impressive how far I’ve gotten, right?

My move” message will be a tuple with the destination X and Y coordinates.

If this were C or similar, I’d say:

if (Maneuver(X, Y) == true) {
    NewLocation = { X, Y }
}

I know there’s an if construct in Erlang, along with case, and I believe I recall that case is preferable, but I’d rather not use either if I can help it. Shouldn’t be necessary.

Instead, what if Maneuver returns the new tuple?

    piece_loop(Maneuver, Location, Team) ->
        receive
            { X, Y } ->
                piece_loop(Maneuver, Maneuver(X, Y), Team);

This seems reasonable and Erlang-friendly, but I also have to take into account future needs, such as sending a message back to my caller to indicate my new position (or an error message if I can’t move), or sending my desired move to another PID to help me decide whether I can safely move.

As I’ve conceived the Maneuver function, it doesn’t try to take pinning or obstacles into consideration.

Nonetheless, I think I’ll go with it for now. This is purely prototyping. Easy to fix later.

What other messages might I receive?

Thus, I’ve defined my first interface: take a tuple with my new square as its contents.

I also need to be able to answer the question: where am I?”. Who am I?” might also be necessary, but I’ll worry about that when I need it.

Also, I’ll need a termination message to exit the loop.

    piece_loop(Maneuver, Location, Team) ->
        receive
            { X, Y } ->
                piece_loop(Maneuver, Maneuver(X, Y), Team);
            { From, location } ->
                From ! Location,
                piece_loop(Maneuver, Location, Team);
            done ->
                done
        end.

Guards

This seems reasonable, but let’s add a bit of simple guarding against stupid messages. X and Y have to be between 1 and 8, right?

I could set my guards as X >= 1, X =< 8, Y >= 1, Y =< 8,3 but let’s make it less wordy.

    piece_loop(Maneuver, Location, Team) ->
        receive
            { X, Y } when X * Y > 0, X * Y < 9 ->
                piece_loop(Maneuver, Maneuver(X, Y), Team);
            { From, location } ->
                From ! Location,
                piece_loop(Maneuver, Location, Team);
            done ->
                done
        end.

I’m using Emacs as my IDE, so C-c C-k compiles, and voila:

Eshell V5.9.1  (abort with ^G)
1> c("/Users/jdaily/Documents/Projects/erlang-chess/piece", [{outdir, "/Users/jdaily/Documents/Projects/erlang-chess/"}]).
c("/Users/jdaily/Documents/Projects/erlang-chess/piece", [{outdir, "/Users/jdaily/Documents/Projects/erlang-chess/"}]).
{ok,piece}

Trial and error, error, error

So let’s take this puppy for a spin.

Oops, oy. Anyone spot the obvious oversight? In order for the Maneuver function to sanity-check my new destination, it needs to know the old destination. It also needs the old destination to return in case the new destination is not, in fact, a legal move.

Well, let’s pretend for the moment, and have Maneuver return the new argument. Prototype, remember?

2> Pawn = spawn(piece, piece_loop, [ fun(X, Y) -> { X, Y } end, {1, 2}, white ]).
<0.39.0>

Awesome. So far so good.

3> Pawn ! done.
done
4> Pawn ! done.
done
5> Pawn ! foo.
foo
6> Pawn ! { 1, 2 }.
{1,2}

Oops. Another gaffe: presumably my Pawn process ended its loop when I sent it the done message, but I don’t have a particularly good way to verify that (anyone who knows otherwise, please feel free to fill me in!). In the absence of an active receive, I just see the messages I sent, which is what I saw when receive was, in fact, active.

But wait, I do have a way. If I send { self(), location } as a message, and nothing shows up in my shell’s mailbox, I’ll know (within a reasonable level of doubt) that the loop is finished. Let’s see.

7> flush().
ok
8> Pawn ! { self(), location }.
{<0.31.0>,location}
9> flush().
ok

Empty mailbox before, empty mailbox after. Loop is done.

Attempt #2, same code in place. f() clears all variable bindings so that I can start over cleanly.

10> f().
ok
11> Pawn = spawn(piece, piece_loop, [ fun(X, Y) -> { X, Y } end, {1, 2}, white ]).
<0.50.0>
12> Pawn ! { 3, 4 }.
{3,4}
13> Pawn ! { self(), location }.
{<0.31.0>,location}
14> flush().
Shell got {1,2}
ok

I definitely messed up somewhere. My new location should be {3, 4}, but the Pawn seems to think it’s still at {1, 2}.

Sentry fail

Ah, of course, I mixed my guard logic while trying to be clever. X < 9, Y < 9, but if I’m going to multiply, it should be X * Y < 65. As you can see, a message with no match is simply ignored.

And it occurs to me that multiplication was a terrible idea to begin with: { 12, 2 } would be accepted by the guard. So let’s be wordy.

    piece_loop(Maneuver, Location, Team) ->
        receive
            { X, Y } when X > 0, X < 9, Y > 0, Y < 9 ->
                piece_loop(Maneuver, Maneuver(X, Y), Team);
            { From, location } ->
                From ! Location,
                piece_loop(Maneuver, Location, Team);
            done ->
                done
        end.

Recompile, reinitialize, and:

18> Pawn ! { self(), location }.
{<0.31.0>,location}
19> flush().
Shell got {1,2}
ok
20> Pawn ! { 3, 4 }.
{3,4}
21> Pawn ! { self(), location }.
{<0.31.0>,location}
22> flush().
Shell got {3,4}
ok

Excellent. We have no business logic” yet, but we have a semblance of a framework to hang it on.

Utility

Let’s make one more fix before we call it a night: make the Maneuver function useful by passing both old and new locations.

All it takes is one modified line. The recursive invocation upon receiving a new location is now:

    piece_loop(Maneuver, Maneuver(Location, {X, Y}), Team);

To test this, now that the Maneuver function will be more complicated, I’ll create a PawnMove variable to store the function before creating the Pawn.

25> PawnMove = fun({ OldX, OldY }, { NewX, NewY }) when NewX =:= OldX, NewY - OldY =:= 1 -> { NewX, NewY };
25>            ({X, Y}, _) -> { X, Y } end.
#Fun<erl_eval.12.82930912>
26> Pawn = spawn(piece, piece_loop, [ PawnMove, {1, 2}, white ]).
<0.77.0>
27> Pawn ! { 1, 4 }.
{1,4}
28> Pawn ! { self(), location }.
{<0.31.0>,location}
29> Pawn ! { 1, 3 }.
{1,3}
30> Pawn ! { self(), location }.
{<0.31.0>,location}
31> flush().
Shell got {1,2}
Shell got {1,3}
ok

Note that I was so confident, I waited until the end to check the mailbox, and indeed the first move failed (because I tried to move 2 squares and PawnMove only allowed a difference of 1 row) and the second move succeeded.

Understanding the definition of PawnMove is left as an exercise for the reader. Tomorrow’s first meeting is awfully early.


  1. My first OO programming language was LPC, an interpreted C-like language for online games. Even then I preferred the invisible, back-end code that made everything work; for example, I wrote code that allowed players to manipulate their environment, like take knife from room’s third bag.“

  2. At the moment, the preferred graphics library for Erlang, WX, doesn’t seem to be working on my Mac, so my only native GUI option would be Tk-based, which wouldn’t thrill me even if I cared about graphics.

  3. Yes, less than or equal to is =<, not <= like every other language I’ve ever used. No idea why.

erlang chessboard
Previous post
Learning Erlang: What I've done A little of this, a little of that. Advice for anyone following in my footsteps.
Next post
Chessboard: Day 2 Of Git and finer things.