/// <summary>
        /// Load a mock level.
        /// </summary>
        private void LoadMock()
        {
            BoardDimensions = new Point(4, 7);
            Board = new TileInfo[7, 4];

            Board[0, 0] = new TileInfo(TileType.Outputter, false);
            Board[0, 0].SetAttr<Directions>(DIRECTION_ATTRIBUTE, Directions.Up);
            Board[0, 0].SetAttr<LaserColours>(COLOUR_ATTRIBUTE, LaserColours.Red);

            Board[0, 1] = new TileInfo(TileType.Outputter, false);
            Board[0, 1].SetAttr<Directions>(DIRECTION_ATTRIBUTE, Directions.Up);
            Board[0, 1].SetAttr<LaserColours>(COLOUR_ATTRIBUTE, LaserColours.Blue);

            Board[0, 2] = new TileInfo(TileType.Outputter, false);
            Board[0, 2].SetAttr<Directions>(DIRECTION_ATTRIBUTE, Directions.Up);
            Board[0, 2].SetAttr<LaserColours>(COLOUR_ATTRIBUTE, LaserColours.Green);

            Board[0, 3] = new TileInfo(TileType.Outputter, false);
            Board[0, 3].SetAttr<Directions>(DIRECTION_ATTRIBUTE, Directions.Up);
            Board[0, 3].SetAttr<LaserColours>(COLOUR_ATTRIBUTE, LaserColours.Purple);

            Board[1, 0] = new TileInfo(TileType.Empty, true);
            Board[1, 1] = new TileInfo(TileType.Empty, true);
            Board[1, 2] = new TileInfo(TileType.Empty, true);
            Board[1, 3] = new TileInfo(TileType.Empty, true);

            Board[2, 0] = new TileInfo(TileType.Empty, true);
            Board[2, 1] = new TileInfo(TileType.Empty, true);
            Board[2, 2] = new TileInfo(TileType.Empty, true);
            Board[2, 3] = new TileInfo(TileType.Empty, true);

            Board[3, 0] = new TileInfo(TileType.Empty, true);
            Board[3, 1] = new TileInfo(TileType.Empty, true);
            Board[3, 2] = new TileInfo(TileType.Empty, true);
            Board[3, 3] = new TileInfo(TileType.Empty, true);

            Board[4, 0] = new TileInfo(TileType.RF_ULxUR_U, false);
            Board[4, 1] = new TileInfo(TileType.RF_ULxUR_U, false);
            Board[4, 2] = new TileInfo(TileType.RF_ULxUR_U, false);
            Board[4, 3] = new TileInfo(TileType.RF_ULxUR_U, false);

            Board[5, 0] = new TileInfo(TileType.Empty, true);
            Board[5, 1] = new TileInfo(TileType.Empty, true);
            Board[5, 2] = new TileInfo(TileType.Empty, true);
            Board[5, 3] = new TileInfo(TileType.Empty, true);

            Board[6, 0] = new TileInfo(TileType.Receiver, false);
            Board[6, 0].SetAttr<Directions>(DIRECTION_ATTRIBUTE, Directions.Up);
            Board[6, 0].SetAttr<LaserColours>(COLOUR_ATTRIBUTE, LaserColours.Purple);

            Board[6, 1] = new TileInfo(TileType.Receiver, false);
            Board[6, 1].SetAttr<Directions>(DIRECTION_ATTRIBUTE, Directions.Up);
            Board[6, 1].SetAttr<LaserColours>(COLOUR_ATTRIBUTE, LaserColours.Red);

            Board[6, 2] = new TileInfo(TileType.Receiver, false);
            Board[6, 2].SetAttr<Directions>(DIRECTION_ATTRIBUTE, Directions.Up);
            Board[6, 2].SetAttr<LaserColours>(COLOUR_ATTRIBUTE, LaserColours.Green);

            Board[6, 3] = new TileInfo(TileType.Receiver, false);
            Board[6, 3].SetAttr<Directions>(DIRECTION_ATTRIBUTE, Directions.Up);
            Board[6, 3].SetAttr<LaserColours>(COLOUR_ATTRIBUTE, LaserColours.Blue);

            Outputters = new Point[] { new Point(0, 0), new Point(1, 0), new Point(2, 0), new Point(3, 0) };
            Receivers  = new Point[] { new Point(0, 6), new Point(1, 6), new Point(2, 6), new Point(3, 6) };

            foreach (var row in InventoryTileOrder)
                foreach (var tileType in row)
                    Inventory[tileType] = -1;
        }
        /// <summary>
        /// Read a tile element. In this case, a tile element is any of "<Tile>",
        /// "<Outputter>", or "<Receiver>", where the string "type" parameter
        /// determines which of the three it is.
        /// </summary>
        private TileInfo ReadTileElement(XmlElement tile)
        {
            var tileName = tile.GetAttribute(TYPE_ATTRIBUTE);

            TileType tileType;
            var success = Enum.TryParse<TileType>(tileName, out tileType);
            if (!success)
            {
                Error_InvalidAttributeValue("tile type", tile.GetAttribute(TYPE_ATTRIBUTE));
            }

            // All non-empty tiles initially specified in the level file are closed to
            // prevent the player overwriting them.
            var tileInfo = new TileInfo(tileType, false);

            foreach (var attr in tile.Attributes.Cast<XmlAttribute>())
            {
                switch(attr.Name)
                {
                    case DIRECTION_ATTRIBUTE:
                        if (!Directions.DirectionsByName.ContainsKey(attr.InnerText))
                            Error_InvalidAttributeValue("direction", attr.InnerText);

                        var direction = Directions.DirectionsByName[attr.InnerText];
                        tileInfo.SetAttr<Directions>(attr.Name, direction);
                        break;
                    case COLOUR_ATTRIBUTE:
                        if (!LaserColours.ColoursByName.ContainsKey(attr.InnerText))
                            Error_InvalidAttributeValue("colour", attr.InnerText);

                        var colour = LaserColours.ColoursByName[attr.InnerText];
                        tileInfo.SetAttr<LaserColours>(attr.Name, colour);
                        break;
                    default:
                        break;
                }
            }

            // Check to ensure a tile of a specific type has the required
            // attributes.
            switch (tileType)
            {
                case TileType.Outputter:
                case TileType.Receiver:
                    if (!tileInfo.HasAttr<LaserColours>(COLOUR_ATTRIBUTE) ||
                        !tileInfo.HasAttr<Directions>(DIRECTION_ATTRIBUTE))
                        Error_NoColourOrDirectionAttribute(tileName.ToLower());
                    break;
                case TileType.OpenReceiver:
                case TileType.OpenReceiver_pass_L:
                case TileType.OpenReceiver_pass_U:
                case TileType.OpenReceiver_pass_UL:
                case TileType.OpenReceiver_pass_UR:
                    if (!tileInfo.HasAttr<LaserColours>(COLOUR_ATTRIBUTE))
                        Error_NoColourOrDirectionAttribute(tileName.ToLower());
                    break;
                default:
                    break;
            }

            return tileInfo;
        }
        private void LoadBoard(XmlElement boardElement)
        {
            int dimX = INDETERMINATE_ROW_WIDTH, dimY = boardElement.ChildNodes.Count;

            var rows = new List<List<TileInfo>>();
            var currentRow = new List<TileInfo>();

            var emptyRowIndices = new List<EmptyRowIndexStruct>();

            foreach (var child in boardElement.ChildElements())
            {
                if (child.Name == ROW_ELEMENT)
                {
                    var len = child.ChildNodes.Count;
                    if (dimX == INDETERMINATE_ROW_WIDTH)
                        dimX = len;
                    else if (dimX != len)
                        Error_InconsistentBoardRowWidths();
                    LoadRow(child, currentRow);
                }
                else if (child.Name == EMPTY_ROW_ELEMENT)
                {
                    bool rowOpen = AttemptReadOpenAttribute(child);
                    // If we haven't yet determined the width of the board, we
                    // have to insert it afterwards.
                    if (dimX == INDETERMINATE_ROW_WIDTH)
                    {
                        emptyRowIndices.Add(
                            new EmptyRowIndexStruct
                            {
                                Index = rows.Count - 1,
                                Open = rowOpen
                            });
                        continue;
                    }
                    else
                    {
                        for (int i = 0; i < dimX; i++)
                            currentRow.Add(new TileInfo(TileType.Empty, rowOpen));
                    }
                }
                rows.Add(currentRow);
                currentRow = new List<TileInfo>();
            }

            // If we could not determine the width of the board. This occurs when
            // the board is entirely composed of terminal row elements (like "<EmptyRow/>").
            if (dimX == INDETERMINATE_ROW_WIDTH)
                Error_IndeterminateBoardWidth();

            // Reinsert the empty row indices.
            foreach (var index in emptyRowIndices)
            {
                var newRow = new List<TileInfo>();
                for (int i = 0; i < dimX; i++)
                    newRow.Add(new TileInfo(TileType.Empty, index.Open));
                rows.Insert(index.Index, newRow);
            }

            BoardDimensions = new Point(dimX, dimY);
            Board = new TileInfo[dimY, dimX];
            var outputters = new List<Point>();
            var receivers = new List<Point>();

            int j = 0;
            foreach (var row in rows)
            {
                foreach (var info in row)
                {
                    Board[j / dimX, j % dimX] = info;

                    if (info.Type == TileType.Outputter)
                        outputters.Add(new Point(j % dimX, j / dimX));

                    else if (info.Type == TileType.Receiver)
                        receivers.Add(new Point(j % dimX, j / dimX));

                    j++;
                }
            }

            Outputters = outputters.ToArray();
            Receivers = receivers.ToArray();
        }