Flip pieces on the way out

I recently released Faceted Reversi, the first Windows Phone 7 application based on the Correspondence framework. Let me take a few posts to tell you how it works, and how you can build your own Correspondence application.

Faceted Reversi is, of course, a Reversi game. Reversi pieces have two sides: black and white. Each color represents one player. When a player moves, he places a piece with his color up. He then flips all of the enemy pieces that were flanked by that move.

When we describe the rules, we say that the player flips the pieces as he makes a move. This is in fact not how the code is written. On a players turn, all the program does is record the move. Pieces are flipped on the way out.

GameBoard and GameState

The game logic is controlled by two classes: GameBoard and GameState. A GameBoard is an immutable object that records a fixed board position. A GameBoard can tell you:

  • which color is on each square
  • who’s move it is
  • how many pieces of each color are on the board
  • what moves are legal
  • what the board would look like after a legal move

Or saying the same thing in code:

public class GameBoard
{
    public static GameBoard OpeningPosition { get; }
    public int MoveIndex { get; }
    public PieceColor PieceAt(Square square);
    public PieceColor ToMove { get; }
    public int BlackCount { get; }
    public int WhiteCount { get; }
    public IEnumerable<Square> LegalMoves { get; }
    public GameBoard AfterMove(Square square);
}

GameState, on the other hand, is mutable. It records the current GameBoard and lets the user make a move.

The separation of immutable state from mutable state is significant. It makes it clear that the board position is dependent upon the sequence of moves. We represent that dependency using Update Controls. The current position is governed by a Dependent sentry that runs UpdateGameBoard when it becomes out-of-date.

public class GameState
{
    private Game _game;

    private GameBoard _gameBoard;
    private Dependent _depGameBoard;

    public GameState(Game game)
    {
        _game = game;

        _depGameBoard = new Dependent(UpdateGameBoard);
    }

    public PieceColor PieceAt(Square square)
    {
        GameBoard gameBoard = GetGameBoard();
        return gameBoard.PieceAt(square);
    }

    private GameBoard GetGameBoard()
    {
        _depGameBoard.OnGet();
        return _gameBoard;
    }

    private void UpdateGameBoard()
    {
        _gameBoard = GameBoard.OpeningPosition;
        if (_game != null)
        {
            List<Move> moves = _game.Moves.ToList();
            moves.Sort(new LocalMoveComparer());
            int expectedIndex = 0;
            foreach (Move move in moves)
            {
                if (move.Index != expectedIndex)
                    return;
                if (move.Player.Index == 0 && _gameBoard.ToMove != PieceColor.Black)
                    return;
                if (move.Player.Index == 1 && _gameBoard.ToMove != PieceColor.White)
                    return;

                Square square = Square.FromIndex(move.Square);
                if (!_gameBoard.LegalMoves.Contains(square))
                    return;

                _gameBoard = _gameBoard.AfterMove(square);
                ++expectedIndex;
            }
        }
    }
}

UpdateGameBoard runs through the list of moves in the game, sorted by move index. It validates each move against the current position. If an invalid or out-of-place move is found, it just gives up. The assumption is that moves were validated before they were added to the game, but we don’t want to crash the app or enter an invalid state if something goes wrong.

Record a move

So what happens when the player makes a move? Simple:

public partial class Player
{
    public void MakeMove(int index, int square)
    {
        Community.AddFact(new Move(this, index, square));
    }
}

All we do is record that move. This adds it to the Moves collection in the game, stores it in the local database, sends it to the other player, and makes GameBoard out-of-date. Let’s break that down.

Publish a move to other subscribers

The Move fact is declared in a language called “factual”, specifically designed for Correspondence.

fact Move {
    Player player;
    int index;
    int square;
}

Adding a fact to the Correspondence community stores it in the local database. It also sends the fact to the server, which forwards it to all interested subscribers. We can tell that the other player is interested because he subscribed to the Game.

_community = new Community(storageStrategy)
    .AddAsynchronousCommunicationStrategy(new POXAsynchronousCommunicationStrategy(configurationProvider))
    .Register<CorrespondenceModule>()
    .Subscribe(() => _identity.ApprovedUsers
        .SelectMany(user => user.ActivePlayers)
        .Select(player => player.Game)
    );

The linq query in the Subscribe call returns all of the Game facts that the player is currently involved in. When a player subscribes to a game, the server will send him all of the published facts. A Player fact is published to a game, as indicated by the “publish” keyword in the factual model.

fact Player {
    publish User user;
    publish Game game;
    int index;
}

A Move fact belongs to a Player, so it is pushed as well. This is how Correspondence knows to send the move to the other player.

Update the UI when a move arrives

So when the move reaches the other player, how does the game know to refresh the board? The Game fact queries for related moves, again in the factual model.

fact Game {
    unique;

    Move* moves {
        Move m : m.player.game = this
    }
}

This makes _game.Moves a live collection. Whenever a fact is added, whether on this phone or from another one, this collection changes. Update Controls recognizes that GameBoard depends upon this collection. So when it changes, GameBoard becomes out-of-date, and Update Controls notifies the view to redraw itself through data binding.

All Correspondence applications work this way. State changes cannot be side-effects of a user action. Instead, the program simply has to store the user action into the model. State changes occur on the way out, back toward the user interface. They are dependent upon the history of user actions.

It doesn’t matter if the move comes from the user interface, from the local database, or from another user. Flipping the pieces is not a side-effect of making a move. It is dependent upon the sequence of moves, regardless where they come from.

Leave a Reply

You must be logged in to post a comment.